属性动画是我们开发过程中经常使用的一种动画,不仅使用方便,而且改变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
向这位博主致谢.
网友评论