我们如果想在ImageView,Button,TextView等系统控件中在XML中配置自定义属性该如何实现呢?例如我们有一个scrollView,在ScrollView里面有上述的一些控件的自定义属性,实现在滑动Scrollview时,里面的控件根据滑动的距离执行各自的动画进度。scrollivew里包含的这些控件可以是任意常用的控件,如 ImageView,Button,TextView等。我们将给这些普通的系统控件配置自定义属性!看到这里是不是觉得无法实现,因为系统的ImageView,Button等是无法识别我们自定义的属性值的,系统的控件怎么识别我们随便定义的属性呢。今天我们就来解决这个问题,解决这个问题的意义在于让系统控件能像我们自定义控件一样,配置了属性就可以执行相应的动画。我们先来看一下运行效果,完整项目见:https://github.com/buder-cp/CustomView/tree/master/buder_DN_view/buderdn13
首先我们看下自定义的控件的xml布局文件:
AnimatorScrollView(重写了Scrollview) --->AnimatorLinerLayout(重写了LinearLayout)--->包含的系统控件。
现在我们来解释下为什么要重写AnimatorScrollView与AnimatorLinerLayout以及如何让系统控件如Imageview等来识别我们的自定义属性。
1.1 为什么需要自定义AnimatorScrollView为什么要重写AnimatorScrollView,这个是为了重写
protected void onScrollChanged(int l, int t, int oldl, int oldt)
目的是用来获取滑动的距离t的,然后t/控件的height就可以得出一个比例,执行每个内部控件的动画(透明度,平移X,平移Y等)的比例。
1.2 为什么需要AnimatorLinerLayout这个是关键,为了解决系统控件(如Imageview)不识别我们的自定义属性的问题。
AnimatorLinerLayout继承于LinearLayout,我们自定义LinearLayout,无非是想改变LinearLayout的行为。那么想改变什么行为呢。我们想利用AnimatorLinerLayout来获取AnimatorLinerLayout包含的各个系统控件,并且解析到为系统控件配置的自定义属性值。这个我们很容易用一个for循环获取到各个子控件及相关XML自定义属性的值。但是我们获取到了这些自定义控件属性又能如何,imageview等系统控件又不识别,就不能执行动画。那我们获取这些自定义属性给谁用??
我们可以在imageview外再包裹一个自定义父布局ViewGroup,然后把这些获取到的自定义属性(动画属性值)赋予这个包裹的VIEWGROUP,然后让父布局可以根据属性值来执行动画,那么里面的imageview是不是也就跟着动起来了呢?这个想法应该可以实现,整体布局都执行动画飞了起来,子布局自然就跟着动了起来,相当于我们的系统控件(如Imageview)执行了动画。这是一个瞒天过海的做法,关于如何在LinearLayout addView之前给每一个系统控件包裹VIEWGROUP的事情,就交给了我们自定义的LinearLayout:AnimatorLinerLayout。这就是我们为什么需要AnimatorLinerLayout的原因:包裹+动画 = 子控件动画 = 瞒天过海
我们总结一下上面的分析:
1. 自定义LinearLayout:DisScrollviewContent,改变布局结构,用自定义VIEWGROUP包裹系统控件如imageview等。
2. 自定义VIEWGROUP(用于包裹)
3. 自定义Scrollview:DisScrollview,根据滑动的距离来计算动画执行的进度比例。
2.1AnimatorLinerLayout(自定义LinearLayout)
public class AnimatorLinerLayout extends LinearLayout {
public AnimatorLinerLayout(Context context) {
this(context, null);
}
public AnimatorLinerLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimatorLinerLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
////从attrs所有参数里提取自定义属性值,并保持在MyLayoutParams对象里,以供“自定义包裹VIEWGROUP"使用并执行动画。
return new AnimatorLayoutParams(getContext(), attrs);
}
//1.考虑到系统控件不识别自定义属性,所以我门要考虑给控件包一层帧
//2.这里采取有父容器组件给子容器包裹一层的方式
//3.系统是通过夹杂i 布局文件,然后调用VIEW的addView来进行加载的
//4.那么此时我门进行偷天换日
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
//获取自定义属性,这时考虑到自定义属性在子控件当中,
//那么系统控件不识别自定义属性,怎么让自定义属性到这个里面来
//来看源码
//根据源码流程-->先调用generateLayoutParams组装XML属性参数
//在调用addView进行添加,所以,自定义属性在generateLayoutParams中进行组装获取
//在addView当中将具体的值进行封装
AnimatorLayoutParams layoutParams = (AnimatorLayoutParams) params;
AnimatorFramelayout view = new AnimatorFramelayout(child.getContext());
if (!isDiscrollvable(layoutParams)) {
//没有自定义属性的系统控件,我们就不需要外层包裹一个“自定义包裹VIEWGROUP"视图。直接addview即可。
super.addView(view);
} else {
//有自定义属性的系统控件,我们需要外层包裹一个“自定义包裹VIEWGROUP"视图。
view.addView(child);
view.setmDiscrollveAlpha(layoutParams.mDiscrollveAlpha);
view.setmDiscrollveFromBgColor(layoutParams.mDiscrollveFromBgColor);
view.setmDiscrollveToBgColor(layoutParams.mDiscrollveToBgColor);
view.setmDiscrollveScaleX(layoutParams.mDiscrollveScaleX);
view.setmDiscrollveScaleX(layoutParams.mDiscrollveScaleY);
view.setmDisCrollveTranslation(layoutParams.mDisCrollveTranslation);
super.addView(view, params);
}
//至此到这一步就已经获取到了自己的自定义属性,可以进行操作了
}
private boolean isDiscrollvable(AnimatorLayoutParams layoutParams) {
return layoutParams.mDiscrollveAlpha ||
layoutParams.mDiscrollveScaleX ||
layoutParams.mDiscrollveScaleY ||
layoutParams.mDisCrollveTranslation != -1 ||
(layoutParams.mDiscrollveFromBgColor != -1 &&
layoutParams.mDiscrollveToBgColor != -1);
}
private class AnimatorLayoutParams extends LinearLayout.LayoutParams {
private boolean mDiscrollveAlpha;
private boolean mDiscrollveScaleX;
private boolean mDiscrollveScaleY;
private int mDisCrollveTranslation;
private int mDiscrollveFromBgColor;
private int mDiscrollveToBgColor;
private AnimatorLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DiscrollView_LayoutParams);
//没有传属性过来,给默认值FALSE
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();
}
}
}
想要系统获取我们自定义的属性,关键就是重写这两个函数:
generateLayoutParams:这个函数在加载XML布局时自动调用,可以获取到每一个系统控件配置的布局参数,包括自定义参数。每加载一个系统控件(如Imageview),则调用一次这个函数。
addView:这个函数是在generateLayoutParams之后执行,在这里我们可以获取到generateLayoutParams函数返回的MyLayoutParams里的自定义属性值。在addview系统控件(如Imageview)之前,先创建并添加一个“自定义包裹VIEWGROUP"视图,然后将自定义属性赋给这个视图,最后在把系统控件addview到"自定义包裹VIEWGROUP"里,从而实现了在代码中为XML里的每一个系统控件外层包裹一个“自定义包裹VIEWGROUP"视图。
OK,至此我们已经实现了在系统控件外包裹一层可以识别自定义属性的VIEWGROUP父布局,接下来我们就来看一下这个自定义VIEWGROUP是如何执行动画的。
2.1 自定义VIEWGROUP: AnimatorFramelayout
public class AnimatorFramelayout extends FrameLayout implements DiscrollInterface {
public AnimatorFramelayout(@NonNull Context context) {
this(context, null);
}
public AnimatorFramelayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimatorFramelayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//保存自定义属性
//定义很多的自定义属性
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) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@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));
}
}
@Override
public void onResetDiscroll() {
if (mDiscrollveAlpha) {
setAlpha(0);
}
if (mDiscrollveScaleX) {
setScaleX(0);
}
if (mDiscrollveScaleY) {
setScaleY(0);
}
//平移动画 int值:left,right,top,bottom left|bottom
if (isTranslationFrom(TRANSLATION_FROM_BOTTOM)) {//是否包含bottom
setTranslationY(mHeight);//height--->0(0代表恢复到原来的位置)
}
if (isTranslationFrom(TRANSLATION_FROM_TOP)) {//是否包含bottom
setTranslationY(-mHeight);//-height--->0(0代表恢复到原来的位置)
}
if (isTranslationFrom(TRANSLATION_FROM_LEFT)) {
setTranslationX(-mWidth);//mWidth--->0(0代表恢复到本来原来的位置)
}
if (isTranslationFrom(TRANSLATION_FROM_RIGHT)) {
setTranslationX(mWidth);//-mWidth--->0(0代表恢复到本来原来的位置)
}
}
private boolean isTranslationFrom(int translationMask) {
if (mDisCrollveTranslation == -1) {
return false;
}
//fromLeft|fromeBottom & fromBottom = fromBottom
return (mDisCrollveTranslation & translationMask) == translationMask;
}
}
我们发现自定义控件里实现了接口DiscrollvableInterface并重写了
void onDiscrollve(float ratio) //根据比例,执行动画进度
void onResetDiscrollve();//逆向动画,恢复到初始状态。
在这两个函数里会根据自定义属性值与ratio来执行 这个“自定义VIEWGROUP包裹”的动画,从而内部包含的系统控件(如Imageview)等也会跟着动起来。那这两个函数是在什么地方调用的,以及ratio是怎么算出来的,那这个与Scrollview的滑动有关系。那我们就来看一下自定义Scrollview。
2.2自定义Scrollview
public class AnimatorScrollView extends ScrollView {
private AnimatorLinerLayout mContent;
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);
}
//渲染完毕之后
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = (AnimatorLinerLayout) getChildAt(0);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
View first = mContent.getChildAt(0);
first.getLayoutParams().height = getHeight();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//监听滑动程度--CHILD从下面冒出来多少距离
//需要一个百分比来执行动画,
//百分比为 滑出高度/child实际高度=百分比
//实际高度
int scrollViewHeight = getHeight();
//监听滑动的程度---childView从下面冒出来多少距离/childView.getHeight();----0~1:动画执行的百分比ratio
//动画执行的百分比ratio控制动画执行
for (int i = 0; i < mContent.getChildCount(); i++) {
View child = mContent.getChildAt(i);
int childHeight = child.getHeight();
if (!(child instanceof DiscrollInterface)) {
continue;
}
//接口回掉,传递执行的百分比给MyFrameLayout
//低耦合高内聚
DiscrollInterface discrollInterface = (DiscrollInterface) child;
//child离parent顶部的高度
int childTop = child.getTop();
//滑出去的这一截高度: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();
}
}
}
private float clamp(float value, float max, float min) {
return Math.max(Math.min(value, max), min);
}
}
K,所有流程就分析完了。现在总结一下思路:
1. 自定义LinearLayout的addview,让添加imageview等系统控件前,先在外层包裹一个自定义VIEWGROUP,并赋予它自定义属性的配置。
2. 自定义VIEWGROUP,接收滑动的ratio来执行动画进度
3. 自定义Scrollview,计算滑动的ratio,并调用自定义VIEWGROUP里的执行动画函数。
完整项目
buder得儿得儿以得儿以得儿得儿 原创文章 235获赞 112访问量 27万+ 关注 私信 展开阅读全文作者:buder得儿得儿以得儿以得儿得儿