1. 程式人生 > >(二十四)自定義動畫框架

(二十四)自定義動畫框架

一、效果

這裡寫圖片描述

這邊程式碼很簡單,主要學習這個動畫框架開發過程的思想,開發出來的動畫框架便於使用,利於擴充套件。

二、分析

實現滾動是使用了 ScrollView,如果說要使用 ListView 的話,理論上也是可以的,但是 Item 型別比較多的時候,估計會比較複雜。

ScrollView 下,裡面的每個 Item 可以有一些動畫效果,支援的動畫有四種:
1.透明度變化
2.X 或 Y 方向縮放
3.顏色漸變
4.平移進場

通過監聽 ScrollView 的滑動,呼叫對應 Item 的設定屬性方法。Item 執行動畫的程度跟這個 Item 從底部滑出來的高度有關,需要先計算 Item 滑出來多少。

每個 Item 執行的動畫不一致,這邊把 Item 要執行什麼動畫作為自定義屬性配置在各自的 Item 上面,這些 Item 是系統自帶的 View,如 TextView、ImageView 等。動畫由 MyFrameLayout 去控制。

    <LinearLayout>
        <MyFrameLayout
            discrollve:discrollve_scaleY="true"
            discrollve:discrollve_translation="fromLeft" >
            <ImageView
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:layout_gravity="center" android:src="@drawable/camera" />
</MyFrameLayout> </LinearLayout
>

三、包裹 Item

1.MyFrameLayout

先定義個 MyFrameLayout 包裹類,這個類包裹每一個 Item,並控制動畫的執行。

public class MyFrameLayout extends FrameLayout {

    public MyFrameLayout(@NonNull Context context) {
        super(context);
    } 
}

2.MyLinearLayout

考慮到再 XML 佈局檔案中, LinearLayout 下每一個 Item 都需要在外新增一個 MyFrameLayout 才可以,這樣使用起來較為麻煩,所以 自定義 MyLinearLayout 擴充套件自 LinearLayout,預設為子 View 新增一個 MyFrameLayout。

public class MyLinearLayout extends LinearLayout {

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //設定排版為豎著
        setOrientation(VERTICAL);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {

        MyFrameLayout mf = new MyFrameLayout(getContext());
        mf.addView(child);
        super.addView(mf, params);
    }
}

這時候佈局檔案樣式為:

    <MyLinearLayout>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@drawable/camera"
            discrollve:discrollve_scaleY="true"
            discrollve:discrollve_translation="fromLeft" 
            />
    </MyLinearLayout>

四、自定義屬性

自定義屬性 attrs.xml

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <declare-styleable name="DiscrollView_LayoutParams">
        <attr name="discrollve_alpha" format="boolean"/>
        <attr name="discrollve_scaleX" format="boolean"/>
        <attr name="discrollve_scaleY" format="boolean"/>
        <attr name="discrollve_fromBgColor" format="color"/>
        <attr name="discrollve_toBgColor" format="color"/>
        <attr name="discrollve_translation"/>
    </declare-styleable>

    <attr name="discrollve_translation">
        <flag name="fromTop" value="0x01" />
        <flag name="fromBottom" value="0x02" />
        <flag name="fromLeft" value="0x04" />
        <flag name="fromRight" value="0x08" />
    </attr>
</resources>

上面為了方便使用,擴充套件了 LinearLayout,讓他自動為子 View 新增一個 MyFrameLayout,這時候自定義屬性只能寫在各個 Item 上,但是 Item 是系統的 View,自身無法識別到這些屬性,所以是讓 MyLinearLayout 去識別子 View 身上的屬性。即重寫 generateLayoutParams 方法。

1.自定義 LayoutParams

    public class MyLayoutParams extends LinearLayout.LayoutParams{
        public int mDiscrollveFromBgColor;//背景顏色變化開始值
        public int mDiscrollveToBgColor;//背景顏色變化結束值
        public boolean mDiscrollveAlpha;//是否需要透明度動畫
        public int mDisCrollveTranslation;//平移值
        public boolean mDiscrollveScaleX;//是否需要x軸方向縮放
        public boolean mDiscrollveScaleY;//是否需要y軸方向縮放

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //解析attrs得到自定義的屬性,儲存
            TypedArray a = getContext().obtainStyledAttributes(attrs,R.styleable.DiscrollView_LayoutParams);
            mDiscrollveAlpha = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_alpha, false);
            mDiscrollveScaleX = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleX, false);
            mDiscrollveScaleY = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleY, false);
            mDisCrollveTranslation = a.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_translation, -1);
            mDiscrollveFromBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_fromBgColor, -1);
            mDiscrollveToBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_toBgColor, -1);
            a.recycle();
        }
    }

同時,重寫 MyLinearLayout 的 generateLayoutParams 方法。

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(),attrs);
    }

2.MyFrameLayout 儲存自定義屬性

為 MyFrameLayout 新增自定義屬性,生成 set 方法。同時,重寫 onSizeChanged 方法,記錄寬高。

    private static final int TRANSLATION_FROM_TOP = 0x01;
    private static final int TRANSLATION_FROM_BOTTOM = 0x02;
    private static final int TRANSLATION_FROM_LEFT = 0x04;
    private static final int TRANSLATION_FROM_RIGHT = 0x08;

    //顏色估值器
    private static ArgbEvaluator sArgbEvaluator = new ArgbEvaluator();
    /**
     * 自定義屬性的一些接收的變數
     */
    private int mDiscrollveFromBgColor;//背景顏色變化開始值
    private int mDiscrollveToBgColor;//背景顏色變化結束值
    private boolean mDiscrollveAlpha;//是否需要透明度動畫
    private int mDisCrollveTranslation;//平移值
    private boolean mDiscrollveScaleX;//是否需要x軸方向縮放
    private boolean mDiscrollveScaleY;//是否需要y軸方向縮放
    private int mHeight;//本view的高度
    private int mWidth;//寬度

    public void setmDiscrollveFromBgColor(int mDiscrollveFromBgColor) {
        this.mDiscrollveFromBgColor = mDiscrollveFromBgColor;
    }

    public void setmDiscrollveToBgColor(int mDiscrollveToBgColor) {
        this.mDiscrollveToBgColor = mDiscrollveToBgColor;
    }

    public void setmDiscrollveAlpha(boolean mDiscrollveAlpha) {
        this.mDiscrollveAlpha = mDiscrollveAlpha;
    }

    public void setmDisCrollveTranslation(int mDisCrollveTranslation) {
        this.mDisCrollveTranslation = mDisCrollveTranslation;
    }

    public void setmDiscrollveScaleX(boolean mDiscrollveScaleX) {
        this.mDiscrollveScaleX = mDiscrollveScaleX;
    }

    public void setmDiscrollveScaleY(boolean mDiscrollveScaleY) {
        this.mDiscrollveScaleY = mDiscrollveScaleY;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // TODO Auto-generated method stub
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

在 MyLinearLayout 的 addView 方法中,把自定義屬性儲存到 MyFrameLayout 中。添加了一個判斷是否有要執行的自定義動畫,沒有的話則不進行包裹。效能上的一點優化。

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {

        MyLayoutParams p = (MyLayoutParams) params;
        if(!isDiscrollvable(p)){//判斷是否有自定義屬性,沒有則不包裹一層容器
            super.addView(child,params);
        }else {
            //偷天換日
            MyFrameLayout mf = new MyFrameLayout(getContext());
            mf.addView(child);
            mf.setmDiscrollveAlpha(p.mDiscrollveAlpha);
            mf.setmDiscrollveFromBgColor(p.mDiscrollveFromBgColor);
            mf.setmDiscrollveToBgColor(p.mDiscrollveToBgColor);
            mf.setmDiscrollveScaleX(p.mDiscrollveScaleX);
            mf.setmDisCrollveTranslation(p.mDisCrollveTranslation);
            super.addView(mf, params);
        }
    }

    /**
     * 是否要執行自定義動畫
     * @param p
     * @return
     */
    private boolean isDiscrollvable(MyLayoutParams p){
        return p.mDiscrollveAlpha||
                p.mDiscrollveScaleX||
                p.mDiscrollveScaleY||
                p.mDisCrollveTranslation!=-1||
                (p.mDiscrollveFromBgColor!=-1&&
                        p.mDiscrollveToBgColor!=-1);
    }

五、動畫

1.封裝動畫方法

在上面已經把要執行的動畫屬性傳給了 MyFrameLayout,為 MyFrameLayout 實現兩個方法,設定屬性值和初始化。

為了便於擴充套件,這邊採用介面。
介面 DiscrollInterface:

public interface DiscrollInterface {
    /**
     * 當滑動的時候呼叫該方法,用來控制裡面的控制元件執行相應的動畫
     * @param ratio 動畫執行的百分比(child view畫出來的距離百分比)
     */
    void onDiscroll(float ratio);

    /**
     * 重置動畫--讓view所有的屬性都恢復到原來的樣子
     */
    void onResetDiscroll();
}

MyFrameLayout 實現 DiscrollInterface:

public class MyFrameLayout extends FrameLayout implements DiscrollInterface{
    ...

    @Override
    public void onDiscroll(float ratio) {
        //執行動畫ratio:0~1
        if(mDiscrollveAlpha){
            setAlpha(ratio);
        }
        if(mDiscrollveScaleX){
            setScaleX(ratio);
        }
        if(mDiscrollveScaleY){
            setScaleY(ratio);
        }
        //平移動畫  int值:left,right,top,bottom    left|bottom
        if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
            setTranslationY(mHeight*(1-ratio));//height--->0(0代表恢復到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
            setTranslationY(-mHeight*(1-ratio));//-height--->0(0代表恢復到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
            setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢復到本來原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
            setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢復到本來原來的位置)
        }
        //判斷從什麼顏色到什麼顏色
        if(mDiscrollveFromBgColor!=-1&&mDiscrollveToBgColor!=-1){
            setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
        }
    }

    private boolean isTranslationFrom(int translationMask){
        if(mDisCrollveTranslation ==-1){
            return false;
        }
        //fromLeft|fromeBottom & fromBottom = fromBottom
        return (mDisCrollveTranslation & translationMask) == translationMask;
    }

    @Override
    public void onResetDiscroll() {
        if(mDiscrollveAlpha){
            setAlpha(1);
        }
        if(mDiscrollveScaleX){
            setScaleX(1);
        }
        if(mDiscrollveScaleY){
            setScaleY(1);
        }
        //平移動畫  int值:left,right,top,bottom    left|bottom
        if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
            setTranslationY(0);//height--->0(0代表恢復到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
            setTranslationY(0);//-height--->0(0代表恢復到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
            setTranslationX(0);//mWidth--->0(0代表恢復到本來原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
            setTranslationX(0);//-mWidth--->0(0代表恢復到本來原來的位置)
        }
    }
}

六、ScrollView 監聽

到這裡,就缺一個滑動時候對執行動畫的監聽,為了方便使用,擴充套件 ScrollView ,實現滑動監聽執行動畫效果。

這裡比較複雜的就是計算最後一個 Item 滑出來的距離,從而確認執行動畫的百分比 ratio。ratio = child 浮現的高度/ child 的高度,浮現的高度沒有辦法直接獲取,只能用 ScrollView 的高度減去 Child 離 ScrollView 的頂部距離(紅色箭頭距離)再減去 ScrollView 滑出去的距離(綠色部分)。

這裡寫圖片描述

public class MyScrollView extends ScrollView {

    private MyLinearLayout mContent;

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mContent = (MyLinearLayout) getChildAt(0);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //為了好看,讓第一個子 View 佔滿
        View first = mContent.getChildAt(0);
        first.getLayoutParams().height = getHeight();
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        int scrollViewHeight = getHeight();

        for (int i=0;i<mContent.getChildCount();i++){
            View child = mContent.getChildAt(i);
            int childHeight = child.getHeight();

            if(!(child instanceof DiscrollInterface)){
                continue;
            }
            DiscrollInterface discrollInterface = (DiscrollInterface) child;
            //child離parent頂部的高度
            int childTop = child.getTop();
            //滑出去的這一截高度:t (t 為負的)
            //child離螢幕頂部的高度
            int absoluteTop = childTop - t;
            if(absoluteTop <= scrollViewHeight) {
                //child浮現的高度 = ScrollView 的高度 - child 離螢幕頂部的高度
                int visibleGap = scrollViewHeight - absoluteTop;
                //float ratio = child浮現的高度/child的高度
                float ratio = visibleGap / (float) childHeight;
                //確保ratio是在0~1的範圍
                discrollInterface.onDiscroll(clamp(ratio, 1f, 0f));
            }else{
                discrollInterface.onResetDiscroll();
            }
        }
    }

    /**
     * 求三個數的中間大小的一個數
     * @param value 輸入的值
     * @param max 最大值限制
     * @param min 最小值限制
     */
    public static float clamp(float value, float max, float min){
        return Math.max(Math.min(value, max), min);
    }
}

注:在 onScrollChanged 中迴圈遍歷對每一個子 View 都進行操作,實際上只需要對最後一個 Item 進行動畫屬性的設定,這邊對所有的 Item 都進行了設定,在效能上實際是由一點影響的,正常情況下,這裡的 Item 不會很多,如果說 Item 比較多的話,可以考慮像 ListView 記錄最後一個 Item,在滑動的時候根據計算進行重新獲取,每次繪製的時候只需要呼叫這個 Item 執行動畫即可。

七、附