ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
#### 7.3.4 对任意属性做动画 这里先提出一个问题:给Button加一个动画,让这个Button的宽度从当前宽度增加到500px。也许你会说,这很简单,用View动画就可以搞定,我们可以来试试,你能写出来吗?很快你就会恍然大悟,原来View动画根本不支持对宽度进行动画。没错,View动画只支持四种类型:平移(Translate)、旋转(Rotate)、缩放(Scale)、不透明度(Alpha)。当然用x方向缩放(scaleX)可以让Button在x方向放大,看起来好像是宽度增加了,实际上不是,只是Button被放大了而已,而且由于只x方向被放大,这个时候Button的背景以及上面的文本都被拉伸了,甚至有可能Button会超出屏幕,如图7-2所示。 :-: ![](https://img.kancloud.cn/1f/79/1f7979a36d64bb57f177951571cd382a_1145x602.png) 图7-2 属性动画的缩放效果 图7-2中的效果显然是很差的,而且也不是真正地对宽度做动画,不过,所幸我们还有属性动画,我们用属性动画试试,如下所示。 private void performAnimate() { ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start(); } @Override public void onClick(View v) { if (v == mButton) { performAnimate(); } } 上述代码运行后发现没效果,其实没效果是对的,如果随便传递一个属性过去,轻则没动画效果,重则程序直接Crash。 下面分析属性动画的原理:属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想让动画生效,要同时满足两个条件: * (1)object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。 * (2)object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的(如果这条不满足,动画无效果但不会Crash)。 以上条件缺一不可。那么为什么我们对Button的width属性做动画会没有效果?这是因为Button内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不是改变视图的大小,它是TextView新添加的方法,View是没有这个setWidth方法的,由于Button继承了TextView,所以Button也就有了setWidth方法。下面看一下这个getWidth和setWidth方法的源码: /** * Makes the TextView exactly this many pixels wide. * You could do the same thing by specifying this number in the * LayoutParams. * * @see #setMaxWidth(int) * @see #setMinWidth(int) * @see #getMinWidth() * @see #getMaxWidth() * * @attr ref android.R.styleable#TextView_width */ @android.view.RemotableViewMethod public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; requestLayout(); invalidate(); } /** * Return the width of the your view. * * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; } 从上述源码可以看出,getWidth的确是获取View的宽度的,而setWidth是TextView和其子类的专属方法,它的作用不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的,这个和TextView的宽度不是一个东西。具体来说,TextView的宽度对应XML中的android:layout_width属性,而TextView还有一个属性android:width,这个android:width属性就对应了TextView的setWidth方法。总之,TextView和Button的setWidth、getWidth干的不是同一件事情,通过setWidth无法改变控件的宽度,所以对width做属性动画没有效果。对应于属性动画的两个条件来说,本例中动画不生效的原因是只满足了条件1而未满足条件2。 * 针对上述问题,官方文档上告诉我们有3种解决方法: * · 给你的对象加上get和set方法,如果你有权限的话; * · 用一个类来包装原始对象,间接为其提供get和set方法; * · 采用ValueAnimator,监听动画过程,自己实现属性的改变。 针对上面提出的三种解决方法,下面给出具体的介绍。 * 1.给你的对象加上get和set方法,如果你有权限的话 这个的意思很好理解,如果你有权限的话,加上get和set就搞定了。但是很多时候我们没权限去这么做。比如本文开头所提到的问题,你无法给Button加上一个合乎要求的setWidth方法,因为这是Android SDK内部实现的。这个方法最简单,但是往往是不可行的,这里就不对其进行更多的分析了。 * 2.用一个类来包装原始对象,间接为其提供get和set方法 这是一个很有用的解决方法,是笔者最喜欢用的,因为用起来很方便,也很好理解,下面将通过一个具体的例子来介绍它。 private void performAnimate() { ViewWrapper wrapper = new ViewWrapper(mButton); ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start(); } @Override public void onClick(View v) { if (v == mButton) { performAnimate(); } } private static class ViewWrapper { private View mTarget; public ViewWrapper(View target) { mTarget = target; } public int getWidth() { return mTarget.getLayoutParams().width; } public void setWidth(int width) { mTarget.getLayoutParams().width = width; mTarget.requestLayout(); } } 上述代码在5s内让Button的宽度增加到了500px,为了达到这个效果,我们提供了ViewWrapper类专门用于包装View,具体到本例是包装Button。然后我们对ViewWrapper的width属性做动画,并且在setWidth方法中修改其内部的target的宽度,而target实际上就是我们包装的Button。这样一个间接属性动画就搞定了,上述代码同样适用于一个对象的其他属性。如图7-3所示,很显然效果达到了,真正实现了对宽度做动画。 :-: ![](https://img.kancloud.cn/9b/f3/9bf3c54e883560892f9414bb0547cc76_1154x603.png) 图7-3 属性动画对宽度做动画的效果 * 3.采用VaIueAnimator,监听动画过程,自己实现属性的改变 首先说说什么是ValueAnimator, ValueAnimator本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。下面用例子来说明: private void performAnimate(final View target, final int start, final int end) { ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100); valueAnimator.addUpdateListener(new AnimatorUpdateListener() { // 持有一个IntEvaluator对象,方便下面估值的时候使用 private IntEvaluator mEvaluator = new IntEvaluator(); @Override public void onAnimationUpdate(ValueAnimator animator) { // 获得当前动画的进度值,整型,1~100之间 int currentValue = (Integer) animator.getAnimatedValue(); Log.d(TAG, "current value: " + currentValue); // 获得当前进度占整个动画过程的比例,浮点型,0~1之间 float fraction = animator.getAnimatedFraction(); // 直接调用整型估值器,通过比例计算出宽度,然后再设给Button target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end); target.requestLayout(); } }); valueAnimator.setDuration(5000).start(); } @Override public void onClick(View v) { if (v == mButton) { performAnimate(mButton, mButton.getWidth(), 500); } } 上述代码的效果图和采用ViewWrapper是一样的,请参看图7-3。关于这个ValueAnimator要再说一下,拿上面的例子来说,它会在5000ms内将一个数从1变到100,然后动画的每一帧会回调onAnimationUpdate方法。在这个方法里,我们可以获取当前的值(1~100)和当前值所占的比例,我们可以计算出Button现在的宽度应该是多少。比如时间过了一半,当前值是50,比例为0.5,假设Button的起始宽度是100px,最终宽度是500px,那么Button增加的宽度也应该占总增加宽度的一半,总增加宽度是500-100=400,所以这个时候Button应该增加的宽度是400×0.5=200,那么当前Button的宽度应该为初始宽度 + 增加宽度(100+200=300)。上述计算过程很简单,其实它就是整型估值器IntEvaluator的内部实现,所以我们不用自己写了,直接用吧。