結(jié)合源碼自定義ScrollView滑動動畫

效果圖:


animator.gif

代碼地址AnimationDemo
實現(xiàn)的思路是通過監(jiān)聽ScrollView 的滑動距離,對View的屬性進行更改,從而達到動畫效果。
當然了,可以獲取控件,監(jiān)聽ScrollView的滑動直接對其屬性進行操作,偽代碼:


       View view1 =  findViewById(R.id.id1);
       view1.setAlpha(0.5f);
       view1.setTranslationX(15);

        View view2 =  findViewById(R.id.id2);
        view2.setScaleX(0.5f);
        view2.setScaleY(15);

但是這種方式代碼量大,并且耦合程度高,當需要修改某一個view 的動畫時,就需要直接修改源文件,本著復(fù)用性和易用性的原則,這里考慮直接通過 xml布局文件 配置的方式來完成,例如:

  <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_gravity="center"
            android:src="@mipmap/sweet"
            discrollve:discrollve_alpha="true"
            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="@mipmap/camera"
            discrollve:discrollve_scaleY="true"
            discrollve:discrollve_translation="fromLeft" />

通過配置自定義 view 屬性,discrollve_alpha 、discrollve_scaleY、discrollve_translation等屬性來自動完成動畫的操作。

自定義屬性

<resources>
    <declare-styleable name="AnimatorFrame">
        <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>

設(shè)計定義自定義屬性
discrollve_alpha 透明度
discrollve_scaleX discrollve_scaleY 縮放
discrollve_fromBgColor discrollve_toBgColor 顏色的漸變
discrollve_translation 平移

通過xml 文件配置動畫實現(xiàn)有兩個難點:

  • 如何獲取自定義屬性
  • 如何根據(jù)自定義屬性進行動畫

接下來圍繞這兩個難點來進行分析

如何獲取自定義屬性

自定義屬性不被系統(tǒng)控件所接收,我們需要手動去解析獲取自定義屬性并保存,通常我們自定義view 獲取自定義屬性是在構(gòu)造函數(shù)中 完成

public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.MyView);
        .....
        a.recycle();
   }

但是這里我們需要獲取自定義View 的子View xml中的屬性,那么系統(tǒng)控件是怎么解析這些屬性的呢,我們來分析一下源碼

UI 的繪制流程

PhoneWindow 的setContentView 方法


phonewindow.png

重點關(guān)心代碼418行, 調(diào)用的是 LayoutInflater 的inflate方法,接下來看LayoutInflater的 inflate方法

LayoutInflater.png

421行: 根據(jù) layoutResource 獲取 xml文件解析
422行:調(diào)用 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法進行繪制

轉(zhuǎn)到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法,這個方法稍微有點長,就截取重點部分


inflate1.png

492 行 根據(jù) xml 的tag 創(chuàng)建view
496-508 根據(jù) 父view 創(chuàng)建 layoutparams
515行 繪制子View,我們重點關(guān)心這個方法

rInflateChildren方法會調(diào)用LayoutInflater的 rInflate方法


addView.png

863- 根據(jù)tag 創(chuàng)建子view
865行- 通過 viewGroup的 generateLayoutParams(attrs)方法來創(chuàng)建LayoutParams
866行- 將剛剛創(chuàng)建的子View添加到viewGroup中

在 863行viewGroup的 generateLayoutParams(attrs)方法看到了一個參數(shù) attrs,而我們上面自定義view 獲取自定義屬性正好需要 attars。
而LinearLayout也是這么操作的,查看 LinearLayout.LayoutParams的源碼


        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a =
                    c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

            a.recycle();
        }

通過分析源碼,可以通過 重寫 viewGroup 的generateLayoutParams(attrs)的方法創(chuàng)建自定義的LayoutParams,并且在LayoutParam中保存我們自定義屬性

代碼實現(xiàn)

自定義LinearLayout
public class AnimatorLinearLayout extends LinearLayout {

    public AnimatorLinearLayout(Context context) {
        super(context);
    }

    public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //創(chuàng)建自定義的LayoutParams  ,解析并保存自定義屬性
        return new AnimatorLayoutParam(getContext(), attrs);
    }
}

自定義的LayoutParams

public class AnimatorLayoutParam extends LinearLayout.LayoutParams {


    public boolean mDiscrollveAlpha;  //透明度
    public boolean mDiscrollveScaleX; //X縮放
    public boolean mDiscrollveScaleY;  //Y縮放
    public int mDisCrollveTranslation;  //平移
    public int mDiscrollveFromBgColor;   //漸變起始色
    public int mDiscrollveToBgColor;    //漸變結(jié)束色


    public AnimatorLayoutParam(Context c, AttributeSet attrs) {
        super(c, attrs);
      //獲取自定義屬性并且保存
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AnimatorFrame);
        //沒有傳屬性過來,給默認值FALSE
        mDiscrollveAlpha = a.getBoolean(R.styleable.AnimatorFrame_discrollve_alpha, false);
        mDiscrollveScaleX = a.getBoolean(R.styleable.AnimatorFrame_discrollve_scaleX, false);
        mDiscrollveScaleY = a.getBoolean(R.styleable.AnimatorFrame_discrollve_scaleY, false);
        mDisCrollveTranslation = a.getInt(R.styleable.AnimatorFrame_discrollve_translation, -1);
        mDiscrollveFromBgColor = a.getColor(R.styleable.AnimatorFrame_discrollve_fromBgColor, -1);
        mDiscrollveToBgColor = a.getColor(R.styleable.AnimatorFrame_discrollve_toBgColor, -1);
        a.recycle();
    }
}

如何根據(jù)自定義屬性進行動畫

  • 監(jiān)聽scrollView的滑動
  • 計算比例
  • 修改view屬性

監(jiān)聽滑動

繼承 ScrollView 重寫onScrollChanged 方法

public class AnimatorScrollView extends ScrollView {
    public AnimatorScrollView(Context context) {
        super(context);
    }

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

    public AnimatorScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    AnimatorLinearLayout mLinearLayout;


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        try {
            mLinearLayout = (AnimatorLinearLayout) getChildAt(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
     
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
        //例如 scroll向上滑動了  50px  那么 t 的值就為 50
        int scrollViewHeight = getHeight(); 
   }
}

注意 方法的參數(shù)t 這里的參數(shù)t 表示內(nèi)容滑動的距離,
黑色邊框表示屏幕,紫色邊框表示scrollView內(nèi)容,t 實際為 黑色箭頭到紅色標線之間的距離

xuqiu.png

計算比例

需要計算 控件滑出的比例,即 控件顯示在屏幕上的高度


通過 child.getTop能夠獲取到 當前view在scrollView中的坐標
int disTop = childAtTop - t 計算子視圖 頂部距離scrollView頂部 距離
screenHeight - disTop 既可以計算出 view在屏幕中顯示的高度

  @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
        //例如 scroll向上滑動了  50px  那么 t 的值就為 50

        int scrollViewHeight = getHeight();

        for (int i = 0; i < mLinearLayout.getChildCount(); i++) {

            View childAt = mLinearLayout.getChildAt(i);

                int childAtTop = childAt.getTop();

                int disTop = childAtTop - t; //子視圖 頂部距離 界面頂部 距離

                if (disTop < 0) {
                        //視圖已經(jīng)被向上劃出屏幕
                } else if (disTop < scrollViewHeight ) {
                    //說明視圖 處于可見位置
                    int visiableGap = scrollViewHeight - disTop;

                    if(visiableGap<=childAt.getHeight()) {

                        //計算滑動的比例
                        float ratio = visiableGap * 1.0f / childAt.getHeight();
                    }else {
                      
                    }

                }
        }
    }

執(zhí)行動畫

1、可以直接在 onScrollChanged監(jiān)聽中執(zhí)行每個view的動畫,但是為了代碼的復(fù)用性和可維護性,采用接口抽取的形式

public interface AnimatorProvider{
    //執(zhí)行動畫
    abstract void onAnim(float ratio);
    //重置動畫
    abstract void resetAnim();
}

接口的實現(xiàn)

 @Override
    void onAnim(float ratio) {
        //執(zhí)行動畫ratio:0~1
        if(mDiscrollveAlpha){
            mView.setAlpha(ratio);
        }
        if(mDiscrollveScaleX){
            Log.e("TAG", "scaleX:"+ratio);
            mView.setScaleX(ratio);
        }
        if(mDiscrollveScaleY){
            Log.e("TAG", "scaleY:"+ratio);
            mView.setScaleY(ratio);
        }
        //平移動畫  int值:left,right,top,bottom    left|bottom
        if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
            mView. setTranslationY(mHeight*(1-ratio));//height--->0(0代表恢復(fù)到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
            mView.setTranslationY(-mHeight*(1-ratio));//-height--->0(0代表恢復(fù)到原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
            mView. setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢復(fù)到本來原來的位置)
        }
        if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
            mView.setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢復(fù)到本來原來的位置)
        }
        //判斷從什么顏色到什么顏色
        if(mDiscrollveFromBgColor!=-1&&mDiscrollveToBgColor!=-1){
            mView.setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
        }
    }
/**
     * <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>
     * 0000000001
     * 0000000010
     * 0000000100
     * 0000001000
     * top|left
     * 0000000001 top
     * 0000000100 left 或運算 |
     * 0000000101
     * 反過來就使用& 與運算
     */

    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 boolean isTranslationFrom(int translationMask){
        if(mDisCrollveTranslation ==-1){
            return false;
        }
        //fromLeft|fromeBottom & fromBottom = fromBottom
        return (mDisCrollveTranslation & translationMask) == translationMask;
    }

isTranslationFrom方法 根據(jù)位運算結(jié)果特性來判斷移動的方向

2、考慮性能的問題,對于不需要執(zhí)行動畫的 view 不執(zhí)行比例的計算,使用容器保存需要動畫的view。
在自定義的LinearLayout中初始化容器并保存需要動畫的view,對該類改造如下

public class AnimatorLinearLayout extends LinearLayout {


    private Map<View, AnimatorProvider> mAnimatorProviderMap = new HashMap<>();

    public Map<View, AnimatorProvider> getAnimatorProviderMap() {
        return mAnimatorProviderMap;
    }

    public AnimatorLinearLayout(Context context) {
        super(context);
    }

    public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


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

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

        AnimatorLayoutParam layoutParam = (AnimatorLayoutParam) params;

        if (isDiscrollvable(layoutParam)) {
            mAnimatorProviderMap.put(child, new AnimatorProviderImpl(child,layoutParam));
        }

        super.addView(child, params);
    }

    private boolean isDiscrollvable(AnimatorLayoutParam layoutParams) {
        return layoutParams.mDiscrollveAlpha ||
                layoutParams.mDiscrollveScaleX ||
                layoutParams.mDiscrollveScaleY ||
                layoutParams.mDisCrollveTranslation != -1 ||
                (layoutParams.mDiscrollveFromBgColor != -1 &&
                        layoutParams.mDiscrollveToBgColor != -1);
    }

}

scrollView中執(zhí)行動畫代碼改造

  @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
        //例如 scroll向上滑動了  50px  那么 t 的值就為 50

        int scrollViewHeight = getHeight();

        for (int i = 0; i < mLinearLayout.getChildCount(); i++) {

            View childAt = mLinearLayout.getChildAt(i);

            if (mAnimatorProviderMap.containsKey(childAt)) {

                //需要執(zhí)行動畫
                AnimatorProvider provider = mAnimatorProviderMap.get(childAt);

                int childAtTop = childAt.getTop();

                int disTop = childAtTop - t; //子視圖 頂部距離 界面頂部 距離

                if (disTop < 0) {

                } else if (disTop < scrollViewHeight ) {
                    //說明視圖 處于可見位置
                    int visiableGap = scrollViewHeight - disTop;

                    if(visiableGap<=childAt.getHeight()) {

                        float ratio = visiableGap * 1.0f / childAt.getHeight();

                        provider.onAnim(ratio);
                    }else {
                        provider.onAnim(1.0f);
                    }

                } else {
//                    //說明視圖 在下方 不可見
                    provider.resetAnim();
                }
            } else {
                continue;
            }


        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 【Android 自定義View】 [TOC] 自定義View基礎(chǔ) 接觸到一個類,你不太了解他,如果貿(mào)然翻閱源碼只...
    Rtia閱讀 4,126評論 1 14
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,171評論 3 119
  • 文|李凌 也許你還不知道南寧有這樣一間服裝店提供專業(yè)的形象設(shè)計服務(wù)。它就是坐落在新竹路上的“完美空間.慧生活”...
    花間精凌閱讀 2,618評論 6 3
  • 持續(xù)分享51天,20170902,張紅。 今天周末,窩在家里,裹上被子,老老實實的寫教案。還好今天沒有再打噴嚏...
    啊呦a7_94閱讀 196評論 0 0
  • 韓靜萱閱讀 327評論 0 0

友情鏈接更多精彩內(nèi)容