美文网首页我爱编程Android源码解析
从源码分析一次属性动画失效原因

从源码分析一次属性动画失效原因

作者: your_genius | 来源:发表于2018-05-27 22:47 被阅读133次

    属性动画是我们开发过程中经常使用的一种动画,不仅使用方便,而且改变view的属性.不过如果使用不当就会造成动画失效

    一位兄弟跟我说他在自定义的view中使用了属性动画ValueAnimator,可是动画突然不起作用,而之前在Activity中使用一直都是好好的,让我帮忙看一下,查看他的代码发现他的view是这么定义的(核心部分代码)

    public UiXKMenuView(Context context) {
        this(context, null);
    }
    
    public UiXKMenuView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public UiXKMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initView();
    }
    
    
    private void initView() {
        View view = LayoutInflater.from(mContext).inflate(R.layout.view_xkemenu, null);
        addView(view);
        ...
    }
    

    他的xml文件R.layout.view_xkemenu是这么写的

    <?xml version="1.0" encoding="utf-8"?>
    
    
    <RelativeLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
    
        <ImageView
                android:id="@+id/ui_xkbackground"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:src="#99000000"
        />
    
        <LinearLayout
                android:id="@+id/menu_content_layout"
                android:layout_width="wrap_content"
                android:layout_height="325dp"
                android:layout_alignParentBottom="true"
                android:layout_alignParentRight="true"
                android:layout_marginRight="72px"
    
    
                android:layout_marginBottom="-325dp"
                android:gravity="right"
                android:orientation="vertical"
        >
    
            <RelativeLayout
                    android:layout_width="wrap_content"
                    android:layout_height="48dp">
    
                <ImageView
                        android:id="@+id/menu_pic"
                        android:layout_width="48dp"
                        android:layout_height="48dp"
                        android:scaleType="centerCrop"
                        android:layout_alignParentRight="true"
                        android:src="@drawable/mm1"
                />
    
                <TextView
                        android:id="@+id/image_text"
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:layout_centerVertical="true"
                        android:layout_marginRight="30px"
                        android:layout_toLeftOf="@+id/menu_pic"
                        android:gravity="center"
                        android:text="妹纸"
                        android:textColor="#fff"
                        android:textSize="39px"
                />
            </RelativeLayout>
    
            ...
    
        </LinearLayout>
    </RelativeLayout>
    

    使用动画的过程是

    public void showView() {
            final int height = dp2px(425f);
            if (animator != null) animator.cancel();
            animator = new ValueAnimator();
            animator.setDuration(500);
            animator.setFloatValues(1f, 0f);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) menuLayout.getLayoutParams(); //menuLayout对应xml中id="@+id/menu_content_layout"的LinearLayout
                    lp.bottomMargin = new Float((-1 * ((Float) animator.getAnimatedValue()) * height)).intValue();
                    menu_content_layout.setLayoutParams(lp);
                    if(UiXKMenuView.this.getVisibility() == View.GONE)
                    UiXKMenuView.this.setVisibility(VISIBLE);
                }
            });
            animator.start();
        }
    

    使用的效果是:


    失效动画.gif

    代码很常规, 乍一看也没毛病, 但机智如你一定看出了一些端倪,于是我给他的xml做了一点修改:

    <?xml version="1.0" encoding="utf-8"?>
    
    <!-- 包一层LinearLayout-->
    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
    
            <ImageView
                    android:id="@+id/ui_xkbackground"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:src="#99000000"
            />
    
            <LinearLayout
                    android:id="@+id/menu_content_layout"
                    android:layout_width="wrap_content"
                    android:layout_height="325dp"
                    android:layout_alignParentBottom="true"
                    android:layout_alignParentRight="true"
                    android:layout_marginRight="72px"
                    android:layout_marginBottom="-325dp"
                    android:gravity="right"
                    android:orientation="vertical"
            >
    
                <RelativeLayout
                        android:layout_width="wrap_content"
                        android:layout_height="48dp">
    
                    <ImageView
                            android:id="@+id/menu_pic"
                            android:layout_width="48dp"
                            android:layout_height="48dp"
                            android:scaleType="centerCrop"
                            android:layout_alignParentRight="true"
                            android:src="@drawable/mm1"
                    />
    
                    <TextView
                            android:id="@+id/image_text"
                            android:layout_width="wrap_content"
                            android:layout_height="match_parent"
                            android:layout_centerVertical="true"
                            android:layout_marginRight="30px"
                            android:layout_toLeftOf="@+id/menu_pic"
                            android:gravity="center"
                            android:text="妹纸"
                            android:textColor="#fff"
                            android:textSize="39px"
                    />
                </RelativeLayout>
    
                ...
        </RelativeLayout>
    </LinearLayout>
    

    run后效果是:


    正常动画.gif

    动画正常了.
    现在我们回头分析一下动画为何失效了,问题出在他的这行代码

    rivate void initView() {
    
    //造成xml文件最外层layout的layout_width ,layout_width值失效
        View view = LayoutInflater.from(mContext).inflate(R.layout.view_xkemenu, null);  
    //默认layout_width = "wrap_content", layout_width="wrap_content"
        addView(view);
        ...
    }
    

    根据经验, 我们知道这样写会导致失效, 可是为什么会失效?我们看看LayoutInflater.from(mContext).inflate方法的源码

    /**
         * Inflate a new view hierarchy from the specified xml resource. Throws
         * {@link InflateException} if there is an error.
         *
         * @param resource ID for an XML layout resource to load (e.g.,
         *        <code>R.layout.main_page</code>)
         * @param root Optional view to be the parent of the generated hierarchy.
         * @return The root View of the inflated hierarchy. If root was supplied,
         *         this is the root View; otherwise it is the root of the inflated
         *         XML file.
         */
        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
            return inflate(resource, root, root != null);
        }
    

    这里的注释说param root(就是我们传null的那个参数)是生成的view的父容器.我们知道layout_width,layout_width是对父容器起作用的, 如果父容器为null,那失效就是必然的了.
    可能有同学会问那我们在给activity添加布局layout.xml的时候根布局的layout_width,layout_width不也失效了吗?就像我那兄弟说的,这个ValueAnimator在Activity中一直用的好好的.那是因为你Activity的layout.xml不是直接被转换成一个view,而是被添加进了一个FrameLayout.
    继续回到这个问题,我们刚才说addView(v)方法默认layout_width = "wrap_content", layout_width="wrap_content",为啥这么说呢?
    且看源码

      protected LayoutParams generateDefaultLayoutParams() {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        }
    

    调用addView方法,系统会自动给生成一个默认的LayoutParams.那重点来了为什么根布局
    layout_width = "wrap_content", layout_width="wrap_content"
    会导致动画失效呢?
    敲黑板!
    为了研究这个问题,我们写一个demo来分析一下,demo很简单,只有一个button和一个ImageView.xml如下

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.ocean.testlanchdaha.MainActivity">
    
        <Button
                android:text="开始动画"
                android:id="@+id/launcher"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
        <ImageView
                android:id="@+id/img"
                android:src="@drawable/ic_launcher_background"
                android:layout_alignParentBottom="true"
                android:layout_marginBottom="30dp"
                android:scaleType="centerCrop"
                android:layout_width="40dp"
                android:layout_height="40dp"/>
    
    </RelativeLayout>
    

    Activity核心部分如下

    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            btn = findViewById(R.id.launcher);
            img = findViewById(R.id.img);
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    startAnimator();
    
                }
            });
        }
    
        private void startAnimator(){
            if(animator != null) animator.cancel();
            final float height = dp2px(200f);
            animator = new ValueAnimator();
            animator.setDuration(500);
            animator.setFloatValues(0,1);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) img.getLayoutParams();
                    params.bottomMargin = new Float(((Float) animation.getAnimatedValue()) * height).intValue();
                    img.setLayoutParams(params);
                }
            });
            animator.start();
    
        }
    

    现在我们点击button ,


    动画正常.gif

    动画正常.然后我们把xml修改一下,
    android:layout_height="wrap_content"

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
        tools:context="com.ocean.testlanchdaha.MainActivity">
    
        <Button
                android:text="开始动画"
                android:id="@+id/launcher"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
        <ImageView
                android:id="@+id/img"
                android:src="@drawable/ic_launcher_background"
                android:layout_alignParentBottom="true"
                android:layout_marginBottom="30dp"
                android:scaleType="centerCrop"
                android:layout_width="40dp"
                android:layout_height="40dp"/>
    
    </RelativeLayout>
    

    预览的时候就发现android:layout_marginBottom="30dp"失效了,点击"开始动画"button也没有动画效果.


    marginBottom失效

    那么, 为什么android:layout_marginBottom="30dp"失效了?我们查看RelativeLayout源码,先看看onLayout

     @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            //  The layout has actually already been performed and the positions
            //  cached.  Apply the cached values to the children.
            final int count = getChildCount();
    
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    RelativeLayout.LayoutParams st =
                            (RelativeLayout.LayoutParams) child.getLayoutParams();
                    child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
                }
            }
        }
    

    onLayout方法还是很简单的, 我们看一下child = Image的时候st值


    layout_height="wrap_content" 时st的值

    图中标出的分别是ImageView四个点的坐标,我们再修改
    layout_height="match_parent" ,这次布局和动画都正常,我们对比看一下onLayout


    layout_height="match_parent" 是的st的值
    发现mBottom与mTop的值不一样.显然, mBottom = 1680是父容器底部的坐标,mBottom = 1590显示才会正常.
    那么,什么原因导致mBottom的值不一样呢?我们继续查看onMeasure的源码,看看mBottom再哪里被赋值的.

    当layout_height="match_parent" ,mBottom在这里被赋值

     private void applyVerticalSizeRules(LayoutParams childParams, int myHeight, int myBaseline) {
            final int[] rules = childParams.getRules();
    
            // Baseline alignment overrides any explicitly specified top or bottom.
            int baselineOffset = getRelatedViewBaselineOffset(rules);
            if (baselineOffset != -1) {
                if (myBaseline != -1) {
                    baselineOffset -= myBaseline;
                }
                childParams.mTop = baselineOffset;
                childParams.mBottom = VALUE_NOT_SET;
                return;
            }
    
            RelativeLayout.LayoutParams anchorParams;
    
            childParams.mTop = VALUE_NOT_SET;
            childParams.mBottom = VALUE_NOT_SET;
    
            anchorParams = getRelatedViewParams(rules, ABOVE);
            if (anchorParams != null) {
                childParams.mBottom = anchorParams.mTop - (anchorParams.topMargin +
                        childParams.bottomMargin);
            } else if (childParams.alignWithParent && rules[ABOVE] != 0) {
                if (myHeight >= 0) {
                    childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
                }
            }
    
            anchorParams = getRelatedViewParams(rules, BELOW);
            if (anchorParams != null) {
                childParams.mTop = anchorParams.mBottom + (anchorParams.bottomMargin +
                        childParams.topMargin);
            } else if (childParams.alignWithParent && rules[BELOW] != 0) {
                childParams.mTop = mPaddingTop + childParams.topMargin;
            }
    
            anchorParams = getRelatedViewParams(rules, ALIGN_TOP);
            if (anchorParams != null) {
                childParams.mTop = anchorParams.mTop + childParams.topMargin;
            } else if (childParams.alignWithParent && rules[ALIGN_TOP] != 0) {
                childParams.mTop = mPaddingTop + childParams.topMargin;
            }
    
            anchorParams = getRelatedViewParams(rules, ALIGN_BOTTOM);
            if (anchorParams != null) {
                childParams.mBottom = anchorParams.mBottom - childParams.bottomMargin;
            } else if (childParams.alignWithParent && rules[ALIGN_BOTTOM] != 0) {
                if (myHeight >= 0) {
                    childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
                }
            }
    
            if (0 != rules[ALIGN_PARENT_TOP]) {
                childParams.mTop = mPaddingTop + childParams.topMargin;
            }
    
            //mBottom在这里被赋值
            if (0 != rules[ALIGN_PARENT_BOTTOM]) {
                if (myHeight >= 0) {
                    childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
                }
            }
        }
    

    而当layout_height="wrap_content",mBottom在这里被赋值

    if (isWrapContentHeight) {
                // Height already has top padding in it since it was calculated by looking at
                // the bottom of each child view
                height += mPaddingBottom;
    
                if (mLayoutParams != null && mLayoutParams.height >= 0) {
                    height = Math.max(height, mLayoutParams.height);
                }
    
                height = Math.max(height, getSuggestedMinimumHeight());
                height = resolveSize(height, heightMeasureSpec);
    
                if (offsetVerticalAxis) {
                    for (int i = 0; i < count; i++) {
                        final View child = views[i];
                        if (child.getVisibility() != GONE) {
                            final LayoutParams params = (LayoutParams) child.getLayoutParams();
                            final int[] rules = params.getRules(layoutDirection);
                            if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
                                centerVertical(child, params, height);
                            } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
                                final int childHeight = child.getMeasuredHeight();
                                params.mTop = height - mPaddingBottom - childHeight;
                                //mBottom在这里被赋值
                                params.mBottom = params.mTop + childHeight;
                            }
                        }
                    }
                }
            }
    

    这是onMeasure内的代码,在这里被赋值原因是layout_height="wrap_content"时isWrapContentHeight = true,
    那么,isWrapContentHeight又是在哪里被赋值的呢?

    private void measureChildHorizontal(
                View child, LayoutParams params, int myWidth, int myHeight) {
            final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
                    params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
                    myWidth);
    
            final int childHeightMeasureSpec;
            if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
                if (params.height >= 0) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            params.height, MeasureSpec.EXACTLY);
                } else {
                    // Negative values in a mySize/myWidth/myWidth value in
                    // RelativeLayout measurement is code for, "we got an
                    // unspecified mode in the RelativeLayout's measure spec."
                    // Carry it forward.
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                }
            } else {
                final int maxHeight;
                if (mMeasureVerticalWithPaddingMargin) {
                    maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
                            - params.topMargin - params.bottomMargin);
                } else {
                    maxHeight = Math.max(0, myHeight);
                }
    
                final int heightMode;
                if (params.height == LayoutParams.MATCH_PARENT) {
                    heightMode = MeasureSpec.EXACTLY;
                } else {
                    //heightMode赋值
                    heightMode = MeasureSpec.AT_MOST;
                }
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
            }
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    match_parent或者具体的值, 对应的Mode是MeasureSpec.EXACTLY.wrap_content对应的Mode是MeasureSpec.AT_MOST,不同的Mode赋值方式不同.
    现在我们从头梳理一下:
    layout_height="match_parent" 和layout_height="wrap_content" 对应的Mode不同-->mBottom赋值的方式不同-->显示效果不同.
    当layout_height="wrap_content"时设置layout_marginBottom达不到预期的显示效果.

    这就是我们最初的问题, 动画失效的原因.
    写这篇博客的初衷,是想以这个问题未切入点,过一下RelativeLayout的源码,但是发现有博主写了一篇非常详细的了,也推荐给有兴趣的同学,知其然知其所以然对我们开发来说是很有必要的,花上几个小时研究下源码,画出流程图一定会受益匪浅.
    推荐博客地址:
    https://blog.csdn.net/wz249863091/article/details/51757069
    向这位博主致谢.

    相关文章

      网友评论

        本文标题:从源码分析一次属性动画失效原因

        本文链接:https://www.haomeiwen.com/subject/wrmijftx.html