什么是框架呢?肯定是给别人用的,既然要给别人用,我们就需要考虑什么样的框架别人才会用,不会被骂,并且会一直用。想要做到以上这些我们的框架必须功能强大,能够解决我们每一个人开发过程中的痛点,稳定,不会给程序带来负面影响,使用简单,结构清晰,易于理解,最终要的是要易于扩展等等吧。今天手写一个动画方面的框架玩玩,先看一下效果,如下:
效果看到了,一个滚动效果,每一个滚动出来的View都有不同的动画,包括缩放、透明度渐变、颜色变化、移动等,具体我们要怎么实现呢,分析一下,首先是滑动出来时开始动画,那么我们可以自定义ScrollView来监听滑动,当内部子View滑动出屏幕的时候显示动画,但是我们都知道ScrollView只能包含一个子ViewGroup子类,这个ViewGroup里边可以放置多个View,那么我们怎么在ScrollView中操作隔一层的子View呢?这是一个问题。那么接下来的问题是这既然是一个框架,那么我们是要让别人用的,别人如何为每一个ViewGroup子类View(可能是TextView、ImageView等系统View)更方便的设置不同动画呢?即使设置了动画通过什么方式去执行呢?接下来,咱们一个一个的来解决上边的问题。
首先,如何让用户给每一个View设置不同的动画?也许你会想到我们可以为每一个子View外层再套一个自定义ViewGroup,给每一个自定义ViewGroup设置动画就可以了,那么在ScrollView子类ViewGroup中的代码就会想下边这样:
<MyViewGrop anitation1="" anitation2="" anitation3=""> <TextView /> </MyViewGrop> <MyViewGrop anitation1="" anitation2="" anitation3=""> <Button /> </MyViewGrop> <MyViewGrop anitation1="" anitation2="" anitation3=""> <Button /> </MyViewGrop> <MyViewGrop anitation1="" anitation2="" anitation3=""> <ImageView /> </MyViewGrop>
没错,这样也能实现,但是是不是感觉有点坑爹呢,我们每天加一个View都需要添加一层包装,你要知道这些都是用户做的,那么我们通过什么方式可以让用户更简单更方便的实现呢?我们可以尝试一个大胆的做法:为每一个系统控件添加自定义属性,然后在在每一个系统控件外层动态添加一层自定义ViewGroup获取内部系统控件的自定义属性,复制给自定义的ViewGroup,在适当的时机给外部包裹类进行动画操作不就可以了。那好有了想法,那就该去实现了。首先values文件夹下创建attrs文件,写上支持的自定义动画属性,代码如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyFrameLayout"> <attr name="anitation_alpha" format="boolean" />//是否支持透明度动画; <attr name="anitation_scaleX" format="boolean" />//是否支持X轴缩放动画; <attr name="anitation_scaleY" format="boolean" />//是否支持Y轴缩放动画; <attr name="bgColorStart" format="color" />//背景渐变颜色的开始颜色值; <attr name="bgColorEnd" format="color" />//背景渐变颜色的结束颜色值,与bgColorStart成对出现; <attr name="anitation_translation">//移动动画,是一个枚举类型,支持上下左右四种值。 <flag name="left" value="0x01" /> <flag name="top" value="0x02" /> <flag name="right" value="0x04" /> <flag name="bottom" value="0x08" /> </attr> </declare-styleable> </resources>
上边是我自定义的几种动画属性,当然不仅限于此,你还可以自定义很多其他的,每一个属性代表什么意思,已经给出了注释,这里就不做过多解释。接下来我们继承FrameLayout来写一个自定义控件,它的作用就是包裹每一个系统View(TextView、ImageView等),获取系统View内部自定义属性保存到自身,在恰当时机根据保存的值执行相应动画。通过上边的解释我们知道了他里边需要定义成员变量来保存自定义属性值好了,看代码吧还是:
public class MyFrameLayout extends FrameLayout implements MyFrameLayoutAnitationCallBack { //从哪个方向开始动画; private static final int TRANSLATION_LEFT = 0x01; private static final int TRANSLATION_TOP = 0x02; private static final int TRANSLATION_RIGHT = 0x04; private static final int TRANSLATION_BOTTOM = 0x08; //是否支持透明度; private boolean mAlphaSupport; //颜色变化的起始值; private int mBgColorStart; private int mBgColorEnd; //是否支持X Y轴缩放; private boolean mScaleXSupport; private boolean mScaleYSupport; //移动值; private int mTranslationValue; //当前View宽高; private int mHeight, mWidth; /** * 颜色估值器; */ private static ArgbEvaluator mArgbEvaluator = new ArgbEvaluator(); public MyFrameLayout(@NonNull Context context) { super(context); } public MyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public MyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); } public boolean ismAlphaSupport() { return mAlphaSupport; } public void setmAlphaSupport(boolean mAlphaSupport) { this.mAlphaSupport = mAlphaSupport; } public int getmBgColorStart() { return mBgColorStart; } public void setmBgColorStart(int mBgColorStart) { this.mBgColorStart = mBgColorStart; } public int getmBgColorEnd() { return mBgColorEnd; } public void setmBgColorEnd(int mBgColorEnd) { this.mBgColorEnd = mBgColorEnd; } public boolean ismScaleXSupport() { return mScaleXSupport; } public void setmScaleXSupport(boolean mScaleXSupport) { this.mScaleXSupport = mScaleXSupport; } public boolean ismScaleYSupport() { return mScaleYSupport; } public void setmScaleYSupport(boolean mScaleYSupport) { this.mScaleYSupport = mScaleYSupport; } public int getmTranslationValue() { return mTranslationValue; } public void setmTranslationValue(int mTranslationValue) { this.mTranslationValue = mTranslationValue; } }
接下来,我们需要考虑另一个问题,就是我们什么时候创建MyFrameLayout并读取需要包裹的子View(系统View)的自定义属性赋值给自己呢?我们知道按照之前的想法是想下边这样的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView custom_anitation1="" custom_anitation2="" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView custom_anitation1="" custom_anitation2="" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView custom_anitation1="" custom_anitation2="" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView custom_anitation1="" custom_anitation1="" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView custom_anitation1="" custom_anitation2="" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
我们要读取里边的custom_anitation1,custom_anitation2的值并动态生成MyFrameLayout保存这些值,我们怎么读取的,没错,如果我们需要达到这样的效果就必须自定义外层的LinearLayout来读取动画值,并创建MyFrameLayout将值保存并将系统子View添加到创建的MyFrameLayout对象中,最后将MyFrameLayout对象保存到自定义的MyLinearLayout中,想法是好的,能实现吗?来一步一步看吧,我们先要解决的问题是怎么在外部View读取到子View的属性呢,系统是如何添加对象的呢?让我们看看系统代码是如何根据XMl创建解析View的,看哪个类呢?当然是LayoutInflater类的inflate()方法了,翻翻,你最终会找到这么一段代码:
final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflateChildren(parser, view, attrs, true); viewGroup.addView(view, params);
这段代码是关键,第一行通过方法名字也知道通过Tag创建对应的View,比如在XMl中定义的<TextView>,在这里就会解析成TextView对象。第二行获取了父布局,第三行调用generateLayoutParams创建LayoutParams ,这里子控件的属性就在attrs中了,第五行将子控件添加到了父控件中,没错,这里就是我们实现上述功能的关键所在,我们重写generateLayoutParams(attrs)就可以拿到子控件的属性了,通过重写addView(view, params)我们就可以做动态添加View了,因为用户在定义系统View并添加自定义属性的时候,是在LinearLayout中的,所以我们要在自定义的LinearLayout中做这些操作的,说来就来,看下边代码:
/** * @Explain:自定义LinearLayout,主要功能是判断每一个子View是否包含自定义动画属性, * @1.如果包括解析自定义属性,赋值给自定义LinearLayout.LayoutParams,创建MyFrameLayout赋值包裹子View; * @2.不包含自定义属性,不做处理。 * @Author:LYL * @Version: * @Time:2017/6/14 */ public class MyLinearLayout extends LinearLayout { public MyLinearLayout(Context context) { super(context); } public MyLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { //返回自己定义的Params. return new MyLayoutParams(getContext(), attrs); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { //看源码可知addView的第三个参数(params)就是我们在generateLayoutParams()方法中设置的自定义Params; MyLayoutParams myLayoutParams = (MyLayoutParams) params; if (myLayoutParams.isHaveMyProperty()) { MyFrameLayout myFrameLayout = new MyFrameLayout(getContext()); myFrameLayout.addView(child); myFrameLayout.setmAlphaSupport(myLayoutParams.mAlphaSupport); myFrameLayout.setmScaleXSupport(myLayoutParams.mScaleXSupport); myFrameLayout.setmScaleYSupport(myLayoutParams.mScaleYSupport); myFrameLayout.setmBgColorStart(myLayoutParams.mBgColorStart); myFrameLayout.setmBgColorEnd(myLayoutParams.mBgColorEnd); myFrameLayout.setmTranslationValue(myLayoutParams.mTranslationValue); super.addView(myFrameLayout, index, params); } else { super.addView(child, index, params); } } //自定义LayoutParams,存储从子控件获取的自定义属性。 public class MyLayoutParams extends LinearLayout.LayoutParams { //是否支持透明度; public boolean mAlphaSupport; //是否支持X Y轴缩放; public boolean mScaleXSupport; public boolean mScaleYSupport; //颜色变化的起始值; public int mBgColorStart; public int mBgColorEnd; //移动值; public int mTranslationValue; public MyLayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.MyFrameLayout); mAlphaSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_alpha, false); mBgColorStart = typedArray.getColor(R.styleable.MyFrameLayout_bgColorStart, -1); mBgColorEnd = typedArray.getColor(R.styleable.MyFrameLayout_bgColorEnd, -1); mScaleXSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_scaleX, false); mScaleYSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_scaleY, false); mTranslationValue = typedArray.getInt(R.styleable.MyFrameLayout_anitation_translation, -1); typedArray.recycle(); } } }
在generateLayoutParams(AttributeSet attrs)中因为返回的是一个LayoutParams,通过看源码我们已经知道这个LayoutParams就是下边addView(View child, int index, ViewGroup.LayoutParams params
)方法中的第三个参数,为了更好的封装我继承LinearLayout.LayoutParams自定义了一个LayoutParams,所有的解析操作都在它的内部实现,看一下代码:
//自定义LayoutParams,存储从子控件获取的自定义属性。 public class MyLayoutParams extends LinearLayout.LayoutParams { //是否支持透明度; public boolean mAlphaSupport; //是否支持X Y轴缩放; public boolean mScaleXSupport; public boolean mScaleYSupport; //颜色变化的起始值; public int mBgColorStart; public int mBgColorEnd; //移动值; public int mTranslationValue; public MyLayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.MyFrameLayout); mAlphaSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_alpha, false); mBgColorStart = typedArray.getColor(R.styleable.MyFrameLayout_bgColorStart, -1); mBgColorEnd = typedArray.getColor(R.styleable.MyFrameLayout_bgColorEnd, -1); mScaleXSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_scaleX, false); mScaleYSupport = typedArray.getBoolean(R.styleable.MyFrameLayout_anitation_scaleY, false); mTranslationValue = typedArray.getInt(R.styleable.MyFrameLayout_anitation_translation, -1); typedArray.recycle(); } /** * 判断当前params是否包含自定义属性; * * @return */ public boolean isHaveMyProperty() { if (mAlphaSupport || mScaleXSupport || mScaleYSupport || (mBgColorStart != -1 && mBgColorEnd != -1) || mTranslationValue != -1) { return true; } return false; } }
他的内部没什么,就是通过generateLayoutParams(AttributeSet attrs)方法中传的sttrs解析所有自定义进行保存,并定义了一个是否包含自定义属性的方法。回到MyLinerLayout中的addView(View child, int index, ViewGroup.LayoutParams params)方法,我们知道这时的第三个参数就是我们的自定义LayoutParams了,因为在它的内部如果有自定义属性的话就会被解析,所以这里判断一下有没有自定义属性了,如果有就创建我们的MyFrameLayout并将自定义信息保存到MyFrameLayout中,最后将系统View添加到我们的MyFrameLayout中,再将MyFrameLayout添加到我们自定义的MyLinerLayout中,这样就完成了我们最初的设想了,如果子View没有自定义属性的话就没有必要包裹MyFrameLayout了,我们在代码中确实也是这么做的。这样一来,用户就只需要向下边这样布局了:
<MyLinerLayout> <TextView Android:id="tv1" custom_anitation1=""/> <ImageView Android:id="iv1" custom_anitation1=""/> <TextView Android:id="tv2"/> </MyLinerLayout>
只看思路啊,细节不要在意,那么根据我们上边的做法,我们的tv1,iv1便会被包裹一层<MyFrameLayout>,在MyFrameLayout中保存了Custom_anitation1动画值。而tv2不会被包裹。是不是比在用户用起来,比上边说的简单了不少。做框架一定要站在用户的角度考虑问题。现在,动画有了,如何执行?何时执行呢?我们知道,当每一View滑出屏幕时执行动画,那么我们需要在自定义一个ScrollView实现它的onScrollChanged(int l, int t, int oldl, int oldt)方法,在他的内部实现滚动监听,看代码:
/** * @Explain:自定义ScrollView,获取滚动监听,根据不同情况进行动画; * @Author:LYL * @Version: * @Time:2017/6/14 */ public class MyScrollView extends ScrollView { private MyLinearLayout mMyLinearLayout; public MyScrollView(Context context) { super(context); } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } //获取内部LinearLayout; @Override protected void onFinishInflate() { super.onFinishInflate(); mMyLinearLayout = (MyLinearLayout) getChildAt(0); } //将第一张图片设置成全屏; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mMyLinearLayout.getChildAt(0).getLayoutParams().height = getHeight(); mMyLinearLayout.getChildAt(0).getLayoutParams().width = getWidth(); } @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 < mMyLinearLayout.getChildCount(); i++) { //如果子控件不是MyFrameLayout则循环下一个子控件; View child = mMyLinearLayout.getChildAt(i); if (!(child instanceof MyFrameLayout)) { continue; } //以下为执行动画逻辑; MyFrameLayoutAnitationCallBack myCallBack = (MyFrameLayoutAnitationCallBack) child; //获取子View高度; int childHeight = child.getHeight(); //子控件到父控件的距离; int childTop = child.getTop(); //滚动过程中,子View距离父控件顶部距离; int childAbsluteTop = childTop - t; //进入了屏幕 if (childAbsluteTop <= scrollViewHeight) { //当前子控件显示出来的高度; int childShowHeight = scrollViewHeight - childAbsluteTop; float moveRadio = childShowHeight / (float) childHeight;//这里一定要转化成float类型; //执行动画; myCallBack.excuteAnitation(getMiddleValue(moveRadio, 0, 1)); } else { //没在屏幕内,恢复数据; myCallBack.resetViewAnitation(); } } } /** * 求中间大小的值; * * @param radio * @param minValue * @param maxValue * @return */ private float getMiddleValue(float radio, float minValue, float maxValue) { return Math.max(Math.min(maxValue, radio), minValue); } }
这里同时重写了onFinishInflate(),onSizeChanged(int w, int h, int oldw, int oldh)两个方法,在onFinishInflate()方法中我获取到了内部的MyLinearLayout,在onSizeChanged(int w, int h, int oldw, int oldh)方法中为了方便动画加载,我将MyLinearLayout的第一个View设置成了全屏模式,就是那个妹子的图片。关键代码是onScrollChanged(int l, int t, int oldl, int oldt)方法,因为有自定义方法的控件我们才包裹了MyFrameLayout,所以需要判断一下,在这个方法中主要功能就是在恰当的时机执行动画,这个判断时机的代码就不讲了,都有注释,我在这里在调用执行动画的时候为了更好的体现封装,定义了一个接口,接口里边有两个方法,一个是根据当前子View(也就是包装的MyFrameLayout)根据滑动过程中在屏幕中不同位置的百分比回调执行动画,这个百分比就是滑动出来的部分除以当前View的高度(view.getHeight()),另一个方法就是恢复原值。那么哪个类要实现这个接口呢,当然是哪个类执行动画哪个类实现接口喽,也就是我们的MyFrameLayout类,他里边本身就保存了执行什么动画等信息。重点看一下MyFrameLayout的这几个方法:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; } @Override public void excuteAnitation(float moveRadio) { //设置动画; if (mAlphaSupport) setAlpha(moveRadio); if (mScaleXSupport) setScaleX(moveRadio); if (mScaleYSupport) setScaleY(moveRadio); //从左边移动到原位置; if (isContainDirection(TRANSLATION_LEFT)) setTranslationX(-mWidth * (1 - moveRadio)); if (isContainDirection(TRANSLATION_TOP)) setTranslationY(-mHeight * (1 - moveRadio)); if (isContainDirection(TRANSLATION_RIGHT)) setTranslationX(mWidth * (1 - moveRadio)); if (isContainDirection(TRANSLATION_BOTTOM)) setTranslationY(mHeight * (1 - moveRadio)); if (mBgColorStart != -1 && mBgColorEnd != -1) setBackgroundColor((int) mArgbEvaluator.evaluate(moveRadio, mBgColorStart, mBgColorEnd)); } // @Override public void resetViewAnitation() { if (mAlphaSupport) setAlpha(0); if (mScaleXSupport) setScaleX(0); if (mScaleYSupport) setScaleY(0); //从左边移动到原位置; if (isContainDirection(TRANSLATION_LEFT)) setTranslationX(-mWidth); if (isContainDirection(TRANSLATION_TOP)) setTranslationY(-mHeight); if (isContainDirection(TRANSLATION_RIGHT)) setTranslationX(mWidth); if (isContainDirection(TRANSLATION_BOTTOM)) setTranslationY(mHeight); } private boolean isContainDirection(int direction) { if (mTranslationValue == -1) return false; return (mTranslationValue & direction) == direction; }
在onSizeChanged(intw, inth, intoldw, intoldh) 方法中获取了当前MyFrameLayout的宽高。我们在excuteAnitation(floatmoveRadio)方法中通过判断有哪种动画就根据传进来的执行动画百分比来执行相关动画。这里在判断有没有移动动画的其中一种的时候用到了位运算,我们在定义四个方向的时候代码如下:
private static final int TRANSLATION_LEFT = 0x01; private static final int TRANSLATION_TOP = 0x02; private static final int TRANSLATION_RIGHT = 0x04; private static final int TRANSLATION_BOTTOM = 0x08;
0x01转换成二进制代表 0001
0x02转换成二进制代表 0010
0x04转换成二进制代表 0100
0x08转换成二进制代表 1000
用户在自定义控件赋值时比如说Android:layout_gravity="left|top",那么我们通过解析属性时因为是或运算,所以按照我们给的值就会得到0011;当我判断是否有left时在isContainDirection(intdirection)方法中又与判断的left做了与运算如0011 & 0001最后的值是0001,判断是否等于left,通过这种方式就能判断我们自定义属性中是否有left值了。top、right、bottom都是一模一样的。这个就解释到这里,最后在执行颜色渐变时用到了颜色估值器,也就是ArgbEvaluator类。在resetViewAnitation()方法中进行了恢复处理。好了,我们一切工作都做好了,那么就在我们的XML中进行布局吧,代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:lyl="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.jason.myscollanitationdemo.MyScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent"> <com.jason.myscollanitationdemo.MyLinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/timg2" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/share_weixin_up" lyl:anitation_alpha="true" lyl:anitation_scaleX="true" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="静夜思\n作者:李白\n床前明月光,疑是地上霜。\n举头望明月,低头思故乡。" android:textColor="@android:color/white" android:textSize="17dp" lyl:anitation_alpha="true" lyl:anitation_translation="bottom" lyl:bgColorEnd="#FF00FF" lyl:bgColorStart="#7EB445" /> <ImageView android:layout_width="200dp" android:layout_height="180dp" android:background="@drawable/discount" lyl:anitation_alpha="true" lyl:anitation_translation="left|bottom" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="念奴娇·赤壁怀古\n 作者:苏轼\n 大江东去,浪淘尽,千古风流人物。\n 故垒西边,人道是,三国周郎赤壁。\n 乱石穿空,惊涛拍岸,卷起千堆雪。\n 江山如画,一时多少豪杰。\n 遥想公瑾当年,小乔初嫁了,雄姿英发。\n 羽扇纶巾,谈笑间,樯橹灰飞烟灭。\n 故国神游,多情应笑我,早生华发。\n 人生如梦,一尊还酹江月。" android:textSize="17dp" android:gravity="center" lyl:anitation_alpha="true" lyl:anitation_translation="top|right" lyl:bgColorEnd="#FFFF21" lyl:bgColorStart="#21FF21" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/img_head_classtwo_high" lyl:anitation_alpha="true" lyl:anitation_translation="top" /> </com.jason.myscollanitationdemo.MyLinearLayout> </com.jason.myscollanitationdemo.MyScrollView> </LinearLayout>
将资源都准备好,运行就会出现上边的结果了,好了,一个不算完美的框架就封装好了,当然里边还有很多可以优化的地方。如果你想下载源码,点击这里。上边可能存在疏漏的地方,不过源码是完整的,可以统一看一遍流程。
今天就写到这吧,不早了,该睡了,抽时间在整理到博客吧。有什么不足之处,还望指点一二,谢谢。
作者:刘永雷