1.从实现看门道
其实从效果看无非就是点击切换图片,并添加一些简单动画效果而已,确实没什么难度。这里是因为引入了两个不错的新内容,使用下,权当新手尝鲜。
1.1 Checkable接口实现CheckedImageView
系统本身提供了android.widget.Checkable这样一个接口,方便我们继承实现View的选中和取消的状态。看下这个类:
public interface Checkable { /** * 设置view的选中状态 */ void setChecked(boolean checked); /** * 当前view是否被选中 */ boolean isChecked(); /** *改变view的选中状态到相反的状态 */ void toggle();}通常这个接口用来帮助我们快速实现view的可选效果,增加了选中和取消两种状态和切换方法。另外为了方便View在状态改变时候快速地变看到效果(更背景或图片),我们可以直接通过selector控制图片,而其本身并不会自动改变drawable状态,因此这里还有必要重写drawableStateChanged
public class CheckedImageView extends ImageView implements Checkable{ protected boolean isChecked;//选中状态 protected OnCheckedChangeListener onCheckedChangeListener;//状态改变事件监听 public static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked }; public CheckedImageView(Context context) { super(context); initialize(); } public CheckedImageView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } private void initialize() { isChecked = false; } @Override public boolean isChecked() { return isChecked; } @Override public void setChecked(boolean isChecked) { if (this.isChecked != isChecked) {this.isChecked = isChecked;refreshDrawableState();if (onCheckedChangeListener != null) {onCheckedChangeListener.onCheckedChanged(this, isChecked);} } } @Override public void toggle() {//改变状态 setChecked(!isChecked); } //初始DrawableState时候为它添加一个CHECKED_STATE,ImageView本身是没有这个状态的 @Override public int[] onCreateDrawableState(int extraSpace) { int[] states = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) {mergeDrawableStates(states, CHECKED_STATE_SET); } return states; } //当view的选中状态被改变的时候通知ImageView改变背景或内容,这个view会自动在drawable状态集中选择与当前状态匹配的图片 @Override protected void drawableStateChanged() { super.drawableStateChanged(); Drawable drawable = getDrawable(); if (drawable != null) {int[] myDrawableState = getDrawableState();drawable.setState(myDrawableState);invalidate(); } } //设置状态改变监听事件 public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckedChangeListener) { this.onCheckedChangeListener = onCheckedChangeListener; } //当选中状态改变时监听接口触发该事件 public static interface OnCheckedChangeListener { public void onCheckedChanged(CheckedImageView checkedImgeView, boolean isChecked); }}这是一个通用的可被选中ImageView,当点击之后被选中,再次点击则取消。而其背景/内容也会随之改变。比如下图所示效果:
从代码上看,我们本身并没有直接定义当view点击之后,调用setImage()或者setBackground()来改变内容,而是通过使用View本身的DrawableState来绘制和更改,查找与它对应匹配的图片,而这些状态所对应的图片,都预先在selector中配置好。关于selector这里不做介绍,自行查阅学习。
既然提到selector,顺带提下之前遇到的坑,关于他的匹配原则。比如下边这样一个selector:
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/icon_pressed"></item> <item android:state_checked="true" android:drawable="@drawable/icon_checked"></item> <item android:drawable="@drawable/icon_normal"></item></selector>当view同时有上边两个状态(如state_pressed和state_checked)的时候,view优先显示第一个状态时候的图片(icon_pressed)。这是因为它是由上到下有序查找的,当找到第一个状态与他定义的所相符所在行时,就优先显示这行的图片。所以当我们将最后一行
public class PraiseView extends FrameLayout implements Checkable{//同样继承Checkable protected OnPraisCheckedListener praiseCheckedListener; protected CheckedImageView imageView; //点赞图片 protected TextView textView; //+1 protected int padding; public PraiseView(Context context) { super(context); initalize(); } public PraiseView(Context context, AttributeSet attrs) { super(context, attrs); initalize(); } protected void initalize() { setClickable(true); imageView = new CheckedImageView(getContext()); imageView.setImageResource(R.drawable.blog_praise_selector); FrameLayout.LayoutParams flp = new LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,Gravity.CENTER); addView(imageView, flp); textView = new TextView(getContext()); textView.setTextSize(10); textView.setText("+1"); textView.setTextColor(Color.parseColor("#A24040")); textView.setGravity(Gravity.CENTER); addView(textView, flp); textView.setVisibility(View.GONE); } @Override public boolean performClick() { checkChange(); return super.performClick(); } @Override public void toggle() { checkChange(); } public void setChecked(boolean isCheacked) { imageView.setChecked(isCheacked); } public void checkChange() {//点击切换状态 if (imageView.isChecked) {imageView.setChecked(false); } else {imageView.setChecked(true);textView.setVisibility(View.VISIBLE);//放大动画AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView);//飘 “+1”动画AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView); } //调用点赞事件 if (praiseCheckedListener != null) {praiseCheckedListener.onPraisChecked(imageView.isChecked); } } public boolean isChecked() { return imageView.isChecked; } public void setOnPraisCheckedListener(OnPraisCheckedListener praiseCheckedListener) { this.praiseCheckedListener = praiseCheckedListener; } public interface OnPraisCheckedListener{ void onPraisChecked(boolean isChecked); }}过于自定的View大概就这么多了,Checkable这个小巧方便的类,不知道你会用了没。至于上边用到的两个动画效果集:
public abstract class BaseViewAnimator { public static final long DURATION = 1000; private AnimatorSet mAnimatorSet; private long mDuration = DURATION; { mAnimatorSet = new AnimatorSet(); } protected abstract void prepare(View target); public BaseViewAnimator setTarget(View target) { reset(target); prepare(target); return this; } public void animate() { start(); } /** * reset the view to default status * * @param target */ public void reset(View target) { ViewHelper.setAlpha(target, 1); ViewHelper.setScaleX(target, 1); ViewHelper.setScaleY(target, 1); ViewHelper.setTranslationX(target, 0); ViewHelper.setTranslationY(target, 0); ViewHelper.setRotation(target, 0); ViewHelper.setRotationY(target, 0); ViewHelper.setRotationX(target, 0); ViewHelper.setPivotX(target, target.getMeasuredWidth() / 2.0f); ViewHelper.setPivotY(target, target.getMeasuredHeight() / 2.0f); } /** * start to animate */ public void start() { mAnimatorSet.setDuration(mDuration); mAnimatorSet.start(); } public BaseViewAnimator setDuration(long duration) { mDuration = duration; return this; } public BaseViewAnimator setStartDelay(long delay) { getAnimatorAgent().setStartDelay(delay); return this; } public long getStartDelay() { return mAnimatorSet.getStartDelay(); } public BaseViewAnimator addAnimatorListener(AnimatorListener l) { mAnimatorSet.addListener(l); return this; } public void cancel(){ mAnimatorSet.cancel(); } public boolean isRunning(){ return mAnimatorSet.isRunning(); } public boolean isStarted(){ return mAnimatorSet.isStarted(); } public void removeAnimatorListener(AnimatorListener l) { mAnimatorSet.removeListener(l); } public void removeAllListener() { mAnimatorSet.removeAllListeners(); } public BaseViewAnimator setInterpolator(Interpolator interpolator) { mAnimatorSet.setInterpolator(interpolator); return this; } public long getDuration() { return mDuration; } public AnimatorSet getAnimatorAgent() { return mAnimatorSet; }}复杂动画效果基类BaseViewAnimator使用一个AnimatorSet集合来添加各种动画 ,并绑定到目标targetView ,使用 prepare(View target) 的abstract方法供其子类实现具体的动画效果。
/***上划消失(飘+1)*/public class SlideOutUpAnimator extends BaseViewAnimator { @Override public void prepare(View target) { ViewGroup parent = (ViewGroup)target.getParent(); getAnimatorAgent().playTogether(ObjectAnimator.ofFloat(target, "alpha", 1, 0),ObjectAnimator.ofFloat(target,"translationY",0,-parent.getHeight()/2) ); }}/***放大效果*/public class PulseAnimator extends BaseViewAnimator { @Override public void prepare(View target) { getAnimatorAgent().playTogether(ObjectAnimator.ofFloat(target, "scaleY", 1, 1.2f, 1),ObjectAnimator.ofFloat(target, "scaleX", 1, 1.2f, 1) ); }}上边两种动画效果就是对BaseViewAnimator的两种实现,动画本身使用的库是NineOldAndroids。
public class AnimHelper { private static final long DURATION = BaseViewAnimator.DURATION; private static final long NO_DELAY = 0; /** *实例化得到AnimationComposer的唯一接口 */ public static AnimationComposer with(BaseViewAnimator animator) { return new AnimationComposer(animator); } /** *定义与动画效果相关联的各种参数, *使用这种方法可以保证对象的构建和他的表示相互隔离开来 */ public static final class AnimationComposer { private List<Animator.AnimatorListener> callbacks = new ArrayList<Animator.AnimatorListener>(); private BaseViewAnimator animator; private long duration = DURATION; private long delay = NO_DELAY; private Interpolator interpolator; private View target; private AnimationComposer(BaseViewAnimator animator) {this.animator = animator; } public AnimationComposer duration(long duration) {this.duration = duration;return this; } public AnimationComposer delay(long delay) {this.delay = delay;return this; } public AnimationComposer interpolate(Interpolator interpolator) {this.interpolator = interpolator;return this; } public AnimationComposer withListener(Animator.AnimatorListener listener) {callbacks.add(listener);return this; } public AnimManager playOn(View target) {this.target = target;return new AnimManager(play(), this.target); } private BaseViewAnimator play() {animator.setTarget(target);animator.setDuration(duration) .setInterpolator(interpolator) .setStartDelay(delay);if (callbacks.size() > 0) {for (Animator.AnimatorListener callback : callbacks) { animator.addAnimatorListener(callback);}}animator.animate();return animator; } } /** *动画管理类 */ public static final class AnimManager{ private BaseViewAnimator animator; private View target; private AnimManager(BaseViewAnimator animator, View target){this.target = target;this.animator = animator; } public boolean isStarted(){return animator.isStarted(); } public boolean isRunning(){return animator.isRunning(); } public void stop(boolean reset){animator.cancel();if(reset)animator.reset(target); } }}这段代码使用了类似Dialog的builder模式,感兴趣的可以搜一下 JAVA设计模式-Builder.晚点会另开一篇讲解。