AndroidUI 自定义View--任意系统控件上添加自定义属性

           看到标题,我们想到,如果要在系统控件上加上自定义属性,并且能够解析出来。这好像有点不可能。我们经常容易想到的是,自定义一个View来继承系统控件,然后解析自己写的attr,这样可以达到使用自定义属性,但是此时就不是系统控件了,是自定义控件了,我们想要实现的效果是下面这样的:

<ImageView
        android:id="@+id/iv_0"
        android:layout_width="103dp"
        android:layout_height="19dp"
        android:layout_centerInParent="true"
        android:src="@drawable/intro1_item_0"
        app:x_in="1.2"
        app:x_out="1.2" />

系统控件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("<merge /> 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 或者布局容器 甚至自定义控件都可完成这样的动画,只要我们设置了,对应的平移动画,缩放动画,或者透明度动画。因此需要做到灵活 可配置。排除了所有添加的控件的自定义控件的实现形式(因为要灵活,可配置,每种控件自定义,这样会很繁琐,麻烦,也不可能)

<?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>

就像设置这些自定义属性,我们只需要在控件上面设置上面的属性的值就可以,实现相应的动画。类似下面这样的控件:

 <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#007788"
            android:textColor="@android:color/black"
            android:textSize="25sp"
            android:padding="25dp"
            android:gravity="center"
            android:fontFamily="serif"
            android:text="带上您的行李箱,准备shopping!"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_scaleY="true"
             />

上面的控件可以执行透明度动画和Y方向上的缩放动画。

这里需要解决的难题是:系统控件无法识别自定义属性 (类似上例子)。

我们可以借鉴系统support里面的控件CardView的效果,给原本不属于TextView,ImageView自身的效果,放在包装容器CardView上面实现

<android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </android.support.v7.widget.CardView>

我们可以借鉴这种的实现绘制,在我们的View上面,解析XML的时候,自定在代码中(java代码中,默默添加,不需要用户自己在Xml文件中添加)添加一个我们自定义的容器类控件,然后去解析我们自己设置的自定义属性。这样我们可以将ImageView上的自定义属性的数值,作用在外面包裹的容器控件上,这样我们将动画作用在容器控件上,里面的系统控件也可以实现对应的动画。(注意是在java代码中实现自定义容器的包裹,不在xml中实现,尽量少让使用者写自己的包裹容器)

<!--不建议这样写,这样写使用者每次都要在外面包裹一层MyFrameLayout-->
<MyFrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </MyFrameLayout>

这里就需要用到上面的说的源码,干扰系统控件的加载inflate方法。在java代码中,在系统控件中封装我们自定义的MyFrameLayout。实现在XML中不需要配置,让使用者无感知。

具体的代码实现:

<com.widget.discrollvedemo.MyScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:discrollve="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.widget.discrollvedemo.MyLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="662dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/tb_bg" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#007788"
            android:textColor="@android:color/black"
            android:textSize="25sp"
            android:padding="25dp"
            android:gravity="center"
            android:fontFamily="serif"
            android:text="带上您的行李箱,准备shopping!"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_scaleY="true"
             />

        <ImageView
            android:layout_width="200dp"
            android:layout_height="120dp"
            android:layout_gravity="top|right"
            discrollve:discrollve_translation="fromLeft|fromBottom"
            discrollve:discrollve_alpha="true"
            android:src="@mipmap/baggage" />

         <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:textColor="@android:color/black"
            android:textSize="25sp"
            android:padding="25dp"
            android:gravity="center"
            android:fontFamily="serif"
            android:text="准备好相机,这里有你想象不到的惊喜!"
            discrollve:discrollve_fromBgColor="#ff0000"
            discrollve:discrollve_toBgColor="#88EE66"
             />

        ......

    </com.widget.discrollvedemo.MyLinearLayout>

</com.widget.discrollvedemo.MyScrollView>

在我们自定义的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);
    }

    //定义很多的自定义属性
    /**
     * <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>
     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<mContent.getChildCount();i++){
            View child = mContent.getChildAt(i);
            if(!(child instanceof MyFrameLayout)){
                continue;
            }
            DiscrollInterface discrollInterface = (DiscrollInterface) child;
            //child离parent顶部的高度
            int childTop = child.getTop();
            //什么时候执行动画呢?当child滑进屏幕的时候
            int childHeight = child.getHeight();
            //t:就是滑出去的高度
            //child离屏幕顶部的高度
            int absoluteTop = (childTop - t);

            if(absoluteTop<=scrollveiwHeight){
                //child浮现的高度=ScrollView的高度 - child离屏幕顶部的高度
                int visibleGap =scrollveiwHeight - absoluteTop;
//				float ratio = child浮现的高度/child的高度;
                float ratio = visibleGap/(float)childHeight;
                //确保ratio是在0~1的范围。得到ratio在0~1的中间值
                discrollInterface.onDiscroll(clamp(ratio, 1f, 0f));
            }else{
                //恢复
                discrollInterface.onResetDiscroll();
            }
        }

    }
    //求三个数的中间大小的一个数。
    public static float clamp(float value, float max, float min){
        return Math.max(Math.min(value, max), min);
    }
}

通过以上的三个自定义控件 MyFrameLayout java代码中注入的自定义控件,存放自定义属性,和控制动画的运行和回复的操作,实现在XML中不需要封装自定义View去解析相关的自定属性。MyLinearLayout解析系统控件上面的自定义属性,在generateLayoutParams和addView上面做文章,MyScrollView中重写onScrollChanged方法,然后通过在MyLinearLayout中解析的自定义属性,生成的MyFrameLayout 然后根据边界条件作出相应的动画

自定义LayoutInflater的实现

我们都知道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<View> 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;i<linearLayout.getChildCount();i++){
            View child = linearLayout.getChildAt(i);
            /*if(!(child instanceof MyFrameLayout)){
                continue;
            }*/
            if(child== null ||child.getTag()==null){
                continue;
            }
            LayoutTag tag = (LayoutTag) child.getTag();
            Log.e("MyScrollView2","class="+child.getClass().getSimpleName()+"   "+tag.toString());
            //child离parent顶部的高度
            int childTop = child.getTop();
            //什么时候执行动画呢?当child滑进屏幕的时候
            int childHeight = child.getHeight();
            //t:就是滑出去的高度
            //child离屏幕顶部的高度
            int absoluteTop = (childTop - t);

            if(absoluteTop<=scrollveiwHeight){
                //child浮现的高度=ScrollView的高度 - child离屏幕顶部的高度
                int visibleGap =scrollveiwHeight - absoluteTop;
//				float ratio = child浮现的高度/child的高度;
                float ratio = visibleGap/(float)childHeight;
                //确保ratio是在0~1的范围。得到ratio在0~1的中间值
                onDiscroll(child,tag,clamp(ratio, 1f, 0f),child.getHeight(),child.getWidth());
            }else{
                //恢复
                onResetDiscroll(child,tag,child.getHeight(),child.getWidth());
            }
        }

    }
    //求三个数的中间大小的一个数。
    public static float clamp(float value, float max, float min){
        return Math.max(Math.min(value, max), min);
    }


    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;


    public void onDiscroll(View view,LayoutTag tag,float ratio,int mHeight,int mWidth) {
        //判断是否有动画的属性,开启动画
        //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代表恢复到本来原来的位置)
        }
        //判断从什么颜色到什么颜色
        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文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.widget.discrollvedemo.MyScrollView2
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:discrollve="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MyActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:layout_width="200dp"
            android:layout_height="120dp"
            android:layout_gravity="top|right"
            android:src="@mipmap/baggage"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_translation="fromLeft|fromBottom"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:fontFamily="serif"
            android:gravity="center"
            android:padding="25dp"
            android:text="准备好相机,这里有你想象不到的惊喜!"
            android:textColor="@android:color/black"
            android:textSize="25sp"
            discrollve:discrollve_fromBgColor="#ff0000"
            discrollve:discrollve_toBgColor="#88EE66"/>

      

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#D97C1F"
            android:fontFamily="serif"
            android:gravity="center"
            android:padding="20dp"
            android:text="这次淘宝造物节真的来了,我们都在造,你造吗?\n
							7月22日-7月24日\n
							上海世博展览馆\n
							在现场,我们造什么?"
            android:textSize="23sp"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_translation="fromBottom"/>

        <ImageView
            discrollve:discrollve_scaleX="true"
            discrollve:discrollve_scaleY="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="20dp"
            android:src="@mipmap/sweet" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:src="@mipmap/shoes"
            discrollve:discrollve_alpha="true"
            discrollve:discrollve_scaleX="true"
            discrollve:discrollve_scaleY="true"
            discrollve:discrollve_translation="fromLeft|fromBottom"/>

        <ImageView
            discrollve:discrollve_scaleX="true"
            discrollve:discrollve_translation="fromLeft"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:src="@mipmap/camera" />

    </LinearLayout>

</com.widget.discrollvedemo.MyScrollView2>

在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传送门

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值