Android系统控件获取自定义属性

Azura ·
更新时间:2024-09-20
· 652 次阅读

我们如果想在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); } /** * 这个函数在加载XML布局时自动调用,可以获取到每一个系统控件配置的布局参数,包括自定义参数。 * 每加载一个系统控件(如Imageview),则调用一次这个函数。 * @param attrs * @return */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { ////从attrs所有参数里提取自定义属性值,并保持在MyLayoutParams对象里,以供“自定义包裹VIEWGROUP"使用并执行动画。 return new AnimatorLayoutParams(getContext(), attrs); } //1.考虑到系统控件不识别自定义属性,所以我门要考虑给控件包一层帧 //2.这里采取有父容器组件给子容器包裹一层的方式 //3.系统是通过夹杂i 布局文件,然后调用VIEW的addView来进行加载的 //4.那么此时我门进行偷天换日 /** * 这个函数是在generateLayoutParams之后执行,在这里我们可以获取到generateLayoutParams函数返回的MyLayoutParams里的自定义属性值。 * 然后在addview系统控件(如Imageview)之前,先创建并添加一个“自定义包裹VIEWGROUP"视图,然后将自定义属性赋给这个视图,最后在把系统控件 * ddview到"自定义包裹VIEWGROUP"里,从而实现了在代码中为XML里的每一个系统控件外层包裹一个“自定义包裹VIEWGROUP"视图。 * @param child * @param params */ @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); } /** * 自定义LayoutParams * 获取自定义属性 */ 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); } /** * * * * * * * 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 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得儿得儿以得儿以得儿得儿



自定义 属性 自定义属性 Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号