看到标题,我们想到,如果要在系统控件上加上自定义属性,并且能够解析出来。这好像有点不可能。我们经常容易想到的是,自定义一个View来继承系统控件,然后解析自己写的attr,这样可以达到使用自定义属性,但是此时就不是系统控件了,是自定义控件了,我们想要实现的效果是下面这样的:
系统控件ImageView上面有我们的自定义属性 x_in 和 x_out,并且能够解析使用(这里以ImageView为例子,其他任何系统控件都可以这么使用)。
我们可以从系统源码做文章。看看 android的控件是怎么加载的。所有xml中的控件加载最终需要LayoutInflater来进行处理的,处理的最终方法为:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//********************************************************************
// 大部分控件都走这个else分支 === 这是本文主题需要做文章的地方
//********************************************************************
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
最后一个else分支 通过createViewFromTag创建出对应的View,然后通过attr 和 父容器root 的 generateLayoutParams给子控件添加对应的布局属性,然后调用root的addView方法,将渲染出的View添加到已经构造好的ViewTree中。 对于上面的系统控件上添加自定义属性,我们就可以通过重写generateLayoutParams和addView方法来做文章,达到上面的效果。例如要实现下面一个效果:
我们看到这样一个效果,任意一个控件随着scrollView的滑动,按照指定的动画(平移,缩放,透明度变化)。我们思考根据上面提示的,思考怎么利用上面的源码分析的attr和 generateLayoutParams,addView做文章了额。
1.首先我们希望任何一个系统控件TextView ,ImageView 或者布局容器 甚至自定义控件都可完成这样的动画,只要我们设置了,对应的平移动画,缩放动画,或者透明度动画。因此需要做到灵活 可配置。排除了所有添加的控件的自定义控件的实现形式(因为要灵活,可配置,每种控件自定义,这样会很繁琐,麻烦,也不可能)
就像设置这些自定义属性,我们只需要在控件上面设置上面的属性的值就可以,实现相应的动画。类似下面这样的控件:
上面的控件可以执行透明度动画和Y方向上的缩放动画。
这里需要解决的难题是:系统控件无法识别自定义属性 (类似上例子)。
我们可以借鉴系统support里面的控件CardView的效果,给原本不属于TextView,ImageView自身的效果,放在包装容器CardView上面实现
我们可以借鉴这种的实现绘制,在我们的View上面,解析XML的时候,自定在代码中(java代码中,默默添加,不需要用户自己在Xml文件中添加)添加一个我们自定义的容器类控件,然后去解析我们自己设置的自定义属性。这样我们可以将ImageView上的自定义属性的数值,作用在外面包裹的容器控件上,这样我们将动画作用在容器控件上,里面的系统控件也可以实现对应的动画。(注意是在java代码中实现自定义容器的包裹,不在xml中实现,尽量少让使用者写自己的包裹容器)
这里就需要用到上面的说的源码,干扰系统控件的加载inflate方法。在java代码中,在系统控件中封装我们自定义的MyFrameLayout。实现在XML中不需要配置,让使用者无感知。
具体的代码实现:
......
在我们自定义的MyLinearLayout中 去解读里面的控件ImageView ,TextView或者其他自定义View的自定义属性。需要在ImageView,TextView上面包裹一层自定义容器控件MyFrameLayout,在MyFrameLayout做平移,透明度,缩放的动画。先看MyFrameLyout的实现:
package com.widget.discrollvedemo;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class MyFrameLayout extends FrameLayout implements DiscrollInterface{
public MyFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
//定义很多的自定义属性
/**
*
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) {
// TODO Auto-generated method stub
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
// onResetDiscroll();
}
@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)){//是否是fromBottom
setTranslationY(mHeight*(1-ratio));//height-->0 (0代表原来的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_TOP)){//从顶部平移进来
setTranslationY(-mHeight*(1-ratio));//-height--->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() {
int ratio = 0;
//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)){//是否是fromBottom
setTranslationY(mHeight*(1-ratio));//height-->0 (0代表原来的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_TOP)){//从顶部平移进来
setTranslationY(-mHeight*(1-ratio));//-height--->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代表恢复到本来原来的位置)
}
}
}
MyFrameLayout就是一个包裹容器,里面存放我们需要解析的自定义的属性,但是解析不是在MyFrameLayout中进行,通过View中的setXXX方法,实现View的相关动画。刚刚说MyFrameLayout中存放我们的自定义属性,那么作为MyFrameLayout的父容器MyLinearLayout需要解析相关的属性,并存放在MyFrameLayout中
package com.widget.discrollvedemo;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
}
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//采花大盗---childview里面的自定义属性--->MyFrameLayout
return new MyLayoutParams(getContext(), attrs);
}
//结合上面的源码分析,重写addView方法 就可以包裹我们自己的自定义容器了
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
//在child view外面包裹一层容器----偷梁换柱
MyLayoutParams p = (MyLayoutParams) params;
if(!isDiscrollvable(p)){//判断如果没有设置自定义属性,就不用包裹一层MyFrameLayout
super.addView(child, index, params);
}else {
MyFrameLayout mf = new MyFrameLayout(getContext(), null);
mf.setmDiscrollveAlpha(p.mDiscrollveAlpha);
mf.setmDiscrollveFromBgColor(p.mDiscrollveFromBgColor);
mf.setmDiscrollveToBgColor(p.mDiscrollveToBgColor);
mf.setmDiscrollveScaleX(p.mDiscrollveScaleX);
mf.setmDiscrollveScaleY(p.mDiscrollveScaleY);
mf.setmDisCrollveTranslation(p.mDisCrollveTranslation);
mf.addView(child);
super.addView(mf, index, params);
}
}
private boolean isDiscrollvable(MyLayoutParams p) {
return p.mDiscrollveAlpha||
p.mDiscrollveScaleX||
p.mDiscrollveScaleY||
p.mDisCrollveTranslation!=-1||
(p.mDiscrollveFromBgColor!=-1&&
p.mDiscrollveToBgColor!=-1);
}
private 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);
//获取自定义属性
TypedArray a = c.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();
}
}
}
package com.widget.discrollvedemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ScrollView;
public class MyScrollView extends ScrollView {
MyLinearLayout mContent;
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = (MyLinearLayout) getChildAt(0);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
int scrollveiwHeight = getHeight();
//监听滑动状态--->childView从下面冒出来多少/childView.getHeight() = 动画的执行的百分比ratio
//拿到里面每一个子控件,让他们按照ratio动起来!
for (int i=0;i
通过以上的三个自定义控件 MyFrameLayout java代码中注入的自定义控件,存放自定义属性,和控制动画的运行和回复的操作,实现在XML中不需要封装自定义View去解析相关的自定属性。MyLinearLayout解析系统控件上面的自定义属性,在generateLayoutParams和addView上面做文章,MyScrollView中重写onScrollChanged方法,然后通过在MyLinearLayout中解析的自定义属性,生成的MyFrameLayout 然后根据边界条件作出相应的动画
我们都知道Android的控件都是通过LayoutInflater的inflate方法将他们渲染到界面的。那么我们可以通过自定义LayoutInflater来干预系统控件的加载,将设置在系统控件上的自定义属性解析出来,存放在系统控件自身之上(这里调用view.setTag将解析的系统控件的自定义属性,与自己绑定),在需要用到的时候取出,并做相应的操作。
public class MyLayoutInflater extends LayoutInflater {
public MyLayoutInflater(Context context){
super(context);
setFactory2(new Factory());
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new MyLayoutInflater(newContext);
}
public static class Factory implements Factory2{
private final String[] sClassPrefix = {
"android.widget.",
"android.view."
};
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = null;
if(name.contains(".")){
view = createMyView(name,context,attrs);
}else{
for (String prefix : sClassPrefix) {
view = createMyView(prefix + name, context, attrs);
if (view != null) {
break;
}
}
}
//获取
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscrollView_LayoutParams);
if (a != null && a.length() > 0) {
//获取自定义属性的值
LayoutTag tag = new LayoutTag();
tag.discrollve_alpha = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_alpha,false);
tag.discrollve_scaleX = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleX, false);
tag.discrollve_scaleY = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleY, false);
tag.discrollve_translation = a.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_translation, -1);
tag.discrollve_fromBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_fromBgColor, -1);
tag.discrollve_toBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_toBgColor, -1);
//index
view.setTag(tag);
}
a.recycle();
return view;
}
private View createMyView(String name, Context context, AttributeSet attrs){
try {
Class clazz = Class.forName(name);
Constructor constructor = clazz.getConstructor(Context.class, AttributeSet.class);
return constructor.newInstance(context, attrs);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
}
这里因为将系统属性存放在View的tag中,因此想要获取到对应的属性,你必须获取到对应的View,因此动画没法封装在自定义的View中,我们需要在自定义的ScrollView中获取到对应的View,然后做相应的动画的操作。
package com.widget.discrollvedemo;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
public class MyScrollView2 extends ScrollView {
LinearLayout linearLayout;
public MyScrollView2(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
linearLayout = (LinearLayout)getChildAt(0);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
int scrollveiwHeight = getHeight();
//监听滑动状态--->childView从下面冒出来多少/childView.getHeight() = 动画的执行的百分比ratio
//拿到里面每一个子控件,让他们按照ratio动起来!
for (int i=0;i0 (0代表原来的位置)
}
if(isTranslationFrom(tag,TRANSLATION_FROM_TOP)){//从顶部平移进来
view.setTranslationY(-mHeight*(1-ratio));//-height--->0
}
if(isTranslationFrom(tag,TRANSLATION_FROM_LEFT)){
view.setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢复到本来原来的位置)
}
if(isTranslationFrom(tag,TRANSLATION_FROM_RIGHT)){
view.setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢复到本来原来的位置)
}
//判断从什么颜色到什么颜色
if(tag.discrollve_fromBgColor!=-1&&tag.discrollve_toBgColor!=-1){
ArgbEvaluator sArgbEvaluator = new ArgbEvaluator();
view.setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, tag.discrollve_fromBgColor, tag.discrollve_toBgColor));
}
}
private boolean isTranslationFrom(LayoutTag tag,int translationMask){
if(tag.discrollve_translation ==-1){
return false;
}
//fromLeft|fromeBottom & fromBottom = fromBottom
return (tag.discrollve_translation & translationMask) == translationMask;
}
public void onResetDiscroll(View view,LayoutTag tag,int mHeight,int mWidth) {
int ratio = 0;
//ratio:0~1
if(tag.discrollve_alpha){
view.setAlpha(ratio);
}
if(tag.discrollve_scaleX){
view.setScaleX(ratio);
}
if(tag.discrollve_scaleY){
view.setScaleY(ratio);
}
//平移---int值: left,right,top,bottom, left|bottom
if(isTranslationFrom(tag,TRANSLATION_FROM_BOTTOM)){//是否是fromBottom
view.setTranslationY(mHeight*(1-ratio));//height-->0 (0代表原来的位置)
}
if(isTranslationFrom(tag,TRANSLATION_FROM_TOP)){//从顶部平移进来
view.setTranslationY(-mHeight*(1-ratio));//-height--->0
}
if(isTranslationFrom(tag,TRANSLATION_FROM_LEFT)){
view.setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢复到本来原来的位置)
}
if(isTranslationFrom(tag,TRANSLATION_FROM_RIGHT)){
view.setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢复到本来原来的位置)
}
}
}
此时我们不需要自定义LinearLayout和自定义的FrameLayout了,xml中只需要自定义的ScrollView了。activty_my.xml文件如下:
在Activity中就需要使用自定义的LayoutInflator去解析渲染对应的控件:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = new MyLayoutInflater(this).inflate(R.layout.activity_my,null);
setContentView(view);
}
至此 ,两种方式实现系统空间上自定义属性的解析已经实现。在平时工作学习中,我们还是可以从源代码中去找灵感,找到很多问题的突破口。
Demo传送门