(二十四)自定义动画框架

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、效果

这里写图片描述

这边代码很简单,主要学习这个动画框架开发过程的思想,开发出来的动画框架便于使用,利于扩展。

二、分析

实现滚动是使用了 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 执行动画即可。

七、附

代码链接:http://download.csdn.net/download/qq_18983205/10008170

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值