1. 程式人生 > >UI--微博(動態)點贊,簡單效果中的不簡單門道

UI--微博(動態)點贊,簡單效果中的不簡單門道

《程式碼裡的世界》UI篇

【導航】
- 單行文字水平觸控滑動效果 通過EditText實現TextView單行長文字水平滑動效果
- 多行文字摺疊展開 自定義佈局View實現多行文字摺疊和展開

1.概述

  說起空間動態、微博的點贊效果,網上也是很氾濫,各種實現與效果一大堆。而詳細實現的部分,講述的也是參差不齊,另一方面估計也有很多大俠也不屑一顧,覺得完全沒必要單獨開篇來寫和講解吧。畢竟,也就是兩個view和一些簡單的動畫效果罷了。
  單若是隻講這些,我自然也是不願花這番功夫的。雖然自己很菜,可也不甘於太菜。所以偶爾看到些好東西,可以延伸學寫下,我還是很情願拿出來用用,順帶秀一秀逼格什麼的。
  不扯太多,先說說今天實現點贊效果用到的自以為不錯的兩個點:

  • Checkable 用來擴充套件View實現選中狀態切換
  • AndroidViewAnimations 基於nineoldandroids封裝的android動畫簡易類庫。究竟有多簡單呢,就像這樣

    AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView);
    作用: 在imageView上使用PulseAnimator這個動畫效果,播放一秒。

      當然是從實現角度來看這個庫啦,如果僅僅是使用,google/百度一大堆啦。
      
    結合前兩篇富文字摺疊展開,加上我們的點贊view 做出的demo整合效果圖:


    點贊效果

2.從實現看門道

  其實從效果看無非就是點選切換圖片,並新增一些簡單動畫效果而已,確實沒什麼難度。這裡是因為引入了兩個不錯的新內容,使用下,權當新手嚐鮮。

2.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
方法。我們先以定義一個通用的CheckedImageView為例:


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)。這是因為它是由上到下有序查詢的,當找到第一個狀態與他定義的所相符所在行時,就優先顯示這行的圖片。所以當我們將最後一行

< item android:drawable=”@drawable/icon_normal”>< /item>

放在第一行時,無論是否選中狀態或按下狀態,都顯示的是icon_normal。初學者一定要注意,我當初就因為這個原因耗費了很多時間查詢緣由。

  回到我們的點贊實現。這裡實現的點贊View PraiseView 包含了一個 CheckedImageView 和一個 TextView ,點贊之後,ImageView會放大回縮並彈出一個TextView”+1”的動畫效果。

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這個小巧方便的類,不知道你會用了沒。至於上邊用到的兩個動畫效果集:

AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView);
AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView);

感覺封裝的挺簡潔實用,所以很有必要學習分析一下。

2.2 動畫庫的封裝和快速框架

  提到動畫,Android本身自帶的動畫類Animation已經做到支援3.0及以上了,雖然也做了很好的封裝,但是做起復雜動畫來還是不夠像上邊那樣簡潔。在關於動畫相容方面,github上的大牛Jake Wharton又做了一套動畫開源庫NineOldAndroids,效果很好而且支援3.0級以前的版本,確實很值得稱讚。而在此基礎上,有很多高手又做了二次封裝,實現了複雜動畫,同時保證方便簡潔,而且通用性和擴充套件性更高。我們這裡的動畫使用的就是這樣一個簡單的封裝。
  比如,要在XXView上時用XXAnimator這樣的動畫,持續Duration秒。就這麼一行程式碼:

AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView);

  來看一下基於NineOldAndroids的ViewAnimations具體實現。

1. 首先定義一個基本動畫效果類BaseViewAnimator

  這個BaseViewAnimator動畫類使用一個動畫集合AnimatorSet,包裝成單個動畫類似的用法,並定義了一個abstract方法prepare():

 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方法供其子類實現具體的動畫效果。
  

2. 其次基於這個類實現我們的各種動畫效果XXAnimator

 當我們要實現具體的動畫效果時,可以直接繼承這個類並實現prepaer方法。比如這裡定義的上劃消失SlideOutUpAnimator 和放大回縮動畫PulseAnimator

/**
*上劃消失(飄+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。

3. 最後封裝一個動畫管理工具類AnimHelper供外部使用

 首先定義了一個靜態類,使用helper來例項化這個靜態類,並設定各個引數選項。

 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.晚點會另開一篇講解。
(注: 複雜動畫這一部分的內容這裡只是拿出來展示和使用,包裝和實現是由程式碼家大大原創,有想了解更多動畫及效果的請點其名字連結)

 執行一下,就可以看到前面所演示的效果了。點選第一下,,伴隨著圖示變大一下並飄出“+1”的效果,圖片切換到選中狀態;再點則恢復未選中,而且不會觸發動畫。
 
 至此,點贊這塊內容和關注點也說完了,希望各位能有點兒收穫,另外便於自己也能加深理解。
 最後,附上示例原始碼地址:
 
 點選下載原始碼示例demo