Android动画精炼详解第(二)节:属性动画讲解及实现

一、前期基础知识储备

上节讲解的《Android动画精炼详解第(一)课:帧动画、补间动画讲解和示例》讲解了最常见最常用的三种动画的分类:帧动画、补间动画、属性动画,并带领大家简单实现了帧动画和补间动画,接下来的本节内容将为大家继续讲解第三种动画——属性动画。

属性动画(Property Animation)是在Android3.0中引入的,为什么要引入属性动画呢?

(1)补间动画的缺陷:在补间动画中,我们只能改变View的绘制效果,View的真实属性没有变化,而属性动画则是直接改变View对象的属性值。什么意思?假如你把一个Button从屏幕的左上角移到右下角,你在补间动画中点击右下角的Button按钮是没有任何作用的,因为Button的真实属性位置还在左上角。这是补间动画的致命缺陷。其次补间动画另一一个缺陷,就是它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,补间动画机制就是使用硬编码的方式来完成的,功能限定死就是这些,基本上没有任何扩展性可言。

(2)属性动画的优势:相较于补间动画,属性动画真实改变了View的属性,而之前的那两种动画则只是视图上的改变,其触发位置仍旧没有变化,因此属性动画更适合做一些需要交互较强的动画。补间动画是只能够作用在View上,只要是任意继承View的控件都可以使用,但是对于非View控件,补间动画就爱莫能助了。

二、上代码,具体实现属性动画

在属性动画中,有2个类需要我们重点关注:ValueAnimator类及其子类ObjectAnimator类.

①ValueAnimator类,上官方文档:

This class provides a simple timing engine for running animations which calculate animated values and set them on target objects.There is a single timing pulse that all animations use. It runs in a custom handler to ensure that property changes happen on the UI thread.

By default, ValueAnimator uses non-linear time interpolation, via the AccelerateDecelerateInterpolator class, which accelerates into and decelerates out of an animation. This behavior can be changed by calling setInterpolator(TimeInterpolator).

由官方文档,我们知道,ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,是一个非常重要的类。

举一个最简单的例子来讲解ValueAnimator类的使用:

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);  
    anim.setDuration(300);  
    anim.start(); 

在这里调用ValueAnimator的ofFloat()方法可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和1就表示将值从0平滑过渡到1,然后调用ValueAnimator的setDuration()方法来设置动画运行的时长,最后调用start()方法启动动画。

————————————————————我是分隔线—————————————————————

②ObjectAnimator类,上官方文档:

This subclass of ValueAnimator provides support for animating properties on target objects. The constructors of this class take parameters to define the target object that will be animated as well as the name of the property that will be animated. Appropriate set/get functions are then determined internally and the animation will call these functions as necessary to animate the property.

由官方文档,我们可以知道属性动画在实际开发中打交道最多的一个类就是ObjectAnimator类,继承自ValueAnimator类,它是属性动画中最重要的一个执行类。ObjectAnimator使用起来也相对较方便,举一个常见的例子:

public class MainActivity extends AppCompatActivity {
   
	private ObjectAnimator anim;
        private TextView textview;
	
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textview = (TextView) findViewById(R.id.text_view);

        ObjectAnimator anim = ObjectAnimator.ofFloat(textview,"alpha", 1f, 0f, 1f);
        anim.setDuration(6000);
        anim.start();
 
   }
}

这里我们实现的是将一个TextView在6秒中内从常规变换成全透明,再从全透明变换成常规。第一个参数是要操作的View,第二个参数是要操作的属性,第三个参数是对应的属性变化,其是一个可变数组,即后面可继续添加参数,如这个效果就是将对textView控件加上了一个淡入淡出的动画。第三个参数及其之后的参数为关键转折点。重点说下第二个参数,我们对View的动画操作常用的有(即第二个参数的值):

pivotX和pivotY:设置View的支点位置,默认为View的中心点

translationX和translationY:View的X轴和Y轴的偏移量,或者说位移距离。

rotation、rotationX和rotationY:View围绕支点,分别以Z轴、X轴和Y轴旋转,其中Z轴垂直屏幕。

scaleX和scaleY:View以支点做缩放。

x和y:直接设置View的位置,相当于原位置移动x和y距离。

alpha:设置View的透明度,1不透明,0全透明。

运行效果图:

 

PS:ObjectAnimator类中获取ObjectAnimator实例的ofFloat()方法官方文档描述参见:ofFloat()

③实现组合动画—AnimatorSet类

独立的动画能够实现的视觉效果毕竟是相当有限的,因此将多个动画组合到一起播放就显得尤为重要。Android团队在设计属性动画的时候也充分考虑到了组合动画的功能,因此提供了一套非常丰富的API来让我们将多个动画组合到一起。

实现组合动画功能主要需要借助AnimatorSet这个类,这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或ObjectAnimator)将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:

after(Animator anim) 将现有动画插入到传入的动画之后执行

after(long delay) 将现有动画延迟指定毫秒后执行

before(Animator anim) 将现有动画插入到传入的动画之前执行

with(Animator anim) 将现有动画和传入的动画同时执行

举一个简单的动画逻辑:让TextView先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,代码如下:

ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);  
    ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);  
    ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);  
    AnimatorSet animSet = new AnimatorSet();  
    
    animSet.play(rotate).with(fadeInOut).after(moveIn);  
    animSet.setDuration(5000);  
    animSet.start();    

运行结果如图:

④动画事件的监听

在属性动画执行的过程中,如果需要在某个执行过程做其他操作,则需添加监听事件,Animator类当中提供了一个addListener()方法,这个方法接收一个AnimatorListener,我们只需要去实现这个AnimatorListener就可以监听动画的各种事件了,代码如下:

objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        // 动画开始
    }
    @Override
    public void onAnimationEnd(Animator animation) {
        // 动画结束
    }
    @Override
    public void onAnimationCancel(Animator animation) {
        // 取消动画
    }
    @Override
    public void onAnimationRepeat(Animator animation) {
        // 动画重复时
    }
});

但是也许很多时候我们并不想要监听那么多个事件,可能我只想要监听动画结束这一个事件,那么每次都要将四个接口全部实现一遍就显得非常繁琐。没关系,为此Android提供了一个另一个接口,叫作AnimatorListenerAdapter,使用这个类就可以解决掉实现接口繁琐的问题了,代码如下:

anim.addListener(new AnimatorListenerAdapter() {  
    @Override  
    public void onAnimationEnd(Animator animation) {  
    }  
}); 

对于ValueAnimator来说,系统定义了另外一个方法addUpdateListener方法用于添加监听,接口为AnimatorUpdateListener

ValueAnimator floatAnimator = ValueAnimator.ofFloat(new float[]{0.0F, finalFloat});
            floatAnimator.setDuration((long)this.duration);
            floatAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                public void onAnimationUpdate(ValueAnimator animation) {
                    int curValue = (int) animation.getAnimatedValue();
                    ...
                }
            });
            floatAnimator.setEvaluator(new IntEvaluator());
            floatAnimator.setInterpolator(new BounceInterpolator());
            floatAnimator.start();

最后,再次看看ValueAnimator的官方文档描述:

Animators can be created from either code or resource files.

由官方文档,我们知道,属性动画和补间动画一样,有两种实现方式,第一种是在Activity代码中设置,第二种是在XML布局中写好代码,然后再在Activity中引入这个布局。

通过XML来编写动画可能会比通过代码来编写动画要慢一些,但是在重用方面将会变得非常轻松,比如某个将通用的动画编写到XML里面,我们就可以在各个界面当中轻松去重用它。如果想要使用XML来编写动画,首先要在res目录下面新建一个animator文件夹,所有属性动画的XML文件都应该存放在这个文件夹当中。在XML文件中我们一共可以使用如下三种标签:

animator> 对应代码中的ValueAnimator

objectAnimator> 对应代码中的ObjectAnimator

set> 对应代码中的AnimatorSet

看看官方文档中的XML代码的例子:

<-- animator节点 - 对应ValueAnimator -->
<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:valueFrom="1"
    android:valueTo="0"
    android:valueType="floatType"
    android:repeatCount="1"
    android:repeatMode="reverse"/>

<-- set节点 - 对应AnimatorSet -->
<-- 缩放动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000">

    <scale android:fromXScale="0.2" android:toXScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
         android:fromYScale="0.2" android:toYScale="1"/>
</set>
<-- 淡入淡出动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:repeatMode="reverse">

    <alpha android:fromAlpha="0.2" android:toAlpha="1"
        android:repeatCount="infinite"/>
</set>
<-- 平移动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:fillAfter="true"
    android:interpolator="@android:anim/overshoot_interpolator">    
    <!--android:fillAfter="true" 设置是否保留最终状态 -->

    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="400"
        android:toYDelta="0">        
       <!-- android:repeatCount="infinite" 无限重复 只能在子节点中单独设置-->
    </translate>
</set>

animator节点动画加载到代码里

ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(MyActivity.this,R.animator.animator);
valueAnimator.start();

objectAnimator节点动画加载到代码里

ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
        R.animator.object_animator);
animator.setTarget(mTv1);
animator.start();

set节点动画加载到代码里

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(MyActivity.this,
        R.animator.set_animator);
set.setTarget(mTv1);
set.start();

其他内容,感兴趣的读者可参考下《安卓动画学习(六)--xml实现属性动画》

三、一个使用属性动画的综合实例

要求:在一个属性动画执行完之后,执行另一个属性动画

public class PaintPosition extends View {
    private Paint paint;
    private String str = "Hello World";
    private float mRadius;
    public static final float INT = 60f;
    private Point currentPoint;

    public PaintPosition(Context context) {
        super(context);
        init();
    }

    public PaintPosition(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PaintPosition(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setTextSize(50);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
    }

    protected void onDraw(Canvas canvas) {
        canvas.drawText(str, getWidth() / 2 - paint.measureText(str) / 2,
                getHeight() / 2, paint);
        //当一个值为空时没有处理,就一点效果都没有,只有当动画运行之后,才会有值,所以需要判断初始值为0的时候
        canvas.save();
        if (mRadius == 0) {
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, paint);
//            startCircleAnimation();
        }else {
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, paint);
        }
        //当一个值为空时没有判断为空的情况就会报空指针错误
        canvas.save();
        if (currentPoint == null) {
            currentPoint = new Point(INT, INT);
            drawCircle(canvas);
            startTestAnimation();
        } else {
            drawCircle(canvas);
        }
    }

    private void drawCircle(Canvas canvas) {
        float x = currentPoint.getX();
        float y = currentPoint.getY();
        canvas.drawCircle(x, y, INT, paint);
    }

    public class Point {
        private float x;
        private float y;
        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
        public float getX() {
            return x;
        }
        public float getY() {
            return y;
        }
    }

    public class PointEvaluator implements TypeEvaluator {
        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            Point startPoint = (Point) startValue;
            Point endPoint = (Point) endValue;
            float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
            float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
            Point point = new Point(x, y);
            return point;
        }
    }

    private void startTestAnimation() {
        Point startPoint = new Point(INT, INT);
        Point endPoint = new Point(getWidth() - INT, getHeight() - INT);
        ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPoint = (Point) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.setDuration(3000);
        anim.start();
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startCircleAnimation();
            }
        });
    }

    private void startCircleAnimation() {
        ValueAnimator anim = ValueAnimator.ofFloat(100f, 200f);
        anim.setDuration(3000);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRadius = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.start();
    }
}

运行效果如图:

属性动画有两个动画监听器:

①anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 用于监听动画值的改变;

在该例中,该监听器用于监听动画发生时小圆的位置坐标和大圆的半径坐标。

②anim.addListener(new AnimatorListenerAdapter() 用于监听动画发生过程中的事件;

在该例中,该监听器用于监听小圆动画结束时大圆动画的开始。

补充:animate()属性动画

/*插值器类型

* AnticipateOvershootInterpolator() 会有一个往返的动作

* AccelerateDecelerateInterpolator() 加减速插值器

* AccelerateInterpolator() 加速插值器

* AnticipateInterpolator() 先慢后快 开始会有一个往返的动作

****BounceInterpolator() 贼酷炫 有回响的味道 弹跳

* CycleInterpolator(10f) 往返跳 次数可控制

* DecelerateInterpolator() 减速插值器

* FastOutLinearInInterpolator() 肉眼看不见有什么分别

* FastOutSlowInInterpolator() 肉眼看不见有什么分别

* LinearOutSlowInInterpolator() 线性减速插值器

* LinearInterpolator() 线性插值器

* OvershootInterpolator() 会有一个往返的动作

* PathInterpolator 没测出来 需要传值

* */

/*移动

* translationX translationY 横坐标方向移动 纵坐标方向移动 遵从Android系统XY正方向 移动到

* translationXBy translationYBy 移动 实际开发中使用

* */

/*缩放

* scaleX scaleY 横坐标方向缩放 纵坐标方向缩放 大于1为增大 小于1为缩小 缩放到 实际开发中使用

* scaleXBy scaleYBy 缩放

* */

/*旋转

* rotationX() rotationY() 绕着横坐标立体内旋转 绕着纵坐标立体内旋转 旋转到

* rotationXBy rotationYBy 立体内旋转

* rotationBy 平面内旋转 实际开发中使用

* rotation 平面内旋转

* */

/*淡入淡出

* alpha() 0为完全透明 1为完全不透明

* */

mTextView.animate().translationXBy(100f).translationYBy(100f).alpha(0.1f).setDuration(3000).setInterpolator(new BounceInterpolator()).start();

mTextView.animate().scaleX(1.5f).scaleY(1.5f).alpha(0.1f).setDuration(3000).setInterpolator(new BounceInterpolator()).start();

mTextView.animate().rotation(150f).alpha(0.1f).setDuration(3000).setInterpolator(new BounceInterpolator()).start();

实际开发中,使用animate()动画是十分迅速的,如果不用考虑动画执行前后的监听事件,只需要一个动画效果,可直接使用animate()动画。

提一句:系统内置了很多差值器,但是能用到的不多 一个加减速差值器和一个弹跳插值器可用 其余的适应性都比较狭窄,所以开发中经常会需要自己定义差值器和估值器。

为animate()动画添加监听器

takePhotoBtn.animate().setListener(takePhotoBtnUpEnd).translationY(animate_offset).scaleX(0.9f).scaleY(0.9f).setDuration(ANIM_TIME).start();
private Animator.AnimatorListener takePhotoBtnUpEnd = new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animator) {
        takePhotoBtn.drawPoint(false);
    }
    @Override
    public void onAnimationEnd(Animator animator) {
    }
    @Override
    public void onAnimationCancel(Animator animator) {
    }
    @Override
    public void onAnimationRepeat(Animator animator) {
    }
};

使用示例:

    private boolean isHideProBar;         
    pro_mode_hide_show_ll.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                float pro_mode_bar_height = pro_mode_tabLl.getHeight();
                if (isHideProBar){
                    pro_mode_tabLl.animate().translationYBy(-pro_mode_bar_height).alpha(1f).setDuration(300).start();
                    pro_mode_hide_show_ll.animate().translationYBy(-pro_mode_bar_height).alpha(1f).setDuration(300).start();
                    proModeHide_Show.setImageResource(R.drawable.ic_pro_arrow_down);
                    isHideProBar = false;
                } else {
                    pro_mode_tabLl.animate().translationYBy(pro_mode_bar_height).alpha(0.3f).setDuration(300).start();
                    pro_mode_hide_show_ll.animate().translationYBy(pro_mode_bar_height).alpha(0.3f).setDuration(300).start();
                    proModeHide_Show.setImageResource(R.drawable.ic_pro_arrow_up);
                    isHideProBar = true;
                }
            }
        });

效果如下:

实现上图效果,亦可直接使用ObjectAnimator属性动画实现:

①下沉隐藏

ObjectAnimator animator = ObjectAnimator.ofFloat(mSnackbar,"translationY",0,mSnackbar.getHeight());
                animator.setDuration(400L);
                animator.start();
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        mSnackbar.setVisibility(View.GONE);
                    }
                });

②上浮显示

ObjectAnimator animator = ObjectAnimator.ofFloat(mSnackbar,"translationY",mSnackbar.getHeight(),0);
                animator.setDuration(350);
                animator.start();

注意二者的区别在于 ofFloat()方法的第三个和第四个参数,ObjectAnimator动画同时指定动画开始值和结束值。

ObjectAnimator动画 ofFloat平移动画坐标系如下图:

补充,HorizontalScrollView的初始平移动画

效果如图:

注意,HorizontalScrollView本身并没有内置移动的API,上述效果实际是利用ObjectAnimator的平移动画实现的。

将HorizontalScrollView直接子view设置为需要直接平移动画的view。代码如下:

                final int x = pro_mode_tabLl.getWidth();
                Log.d(TAG, "pro_mode_tabLl: ," + x * 1/3);
                mAnimatorSetLeft = new AnimatorSet();
                mAnimatorSetRight = new AnimatorSet();
                mItemsliding = ObjectAnimator.ofFloat(pro_mode_tabLl,"translationX",0,x * 1/3);
                mItemsAlpha = ObjectAnimator.ofFloat(pro_mode_tabLl,"alpha",0,0);
                mAnimatorSetLeft.setDuration(0);
                mAnimatorSetLeft.play(mItemsliding).with(mItemsAlpha);
                mAnimatorSetLeft.start();
                mAnimatorSetLeft.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        mItemsliding = ObjectAnimator.ofFloat(pro_mode_tabLl,"translationX",-(x * 1/3),0);
                        mItemsAlpha = ObjectAnimator.ofFloat(pro_mode_tabLl,"alpha",1,1);
                        mAnimatorSetRight.setStartDelay(200);
                        mAnimatorSetRight.setDuration(1000);
                        mAnimatorSetRight.play(mItemsliding).with(mItemsAlpha);
                        mAnimatorSetRight.start();
                    }
                });

因为HorizontalScrollView中的有6个TextView,屏幕上可以显示四个,剩余两个需要第一次进入引入界面是显示,所以取了HorizontalScrollView直接子view的1/3做平移距离。同时注意,此时平移动画的起始位置为负值。

注:取消补间动画的方法为:view.clearAnimation();

取消属性动画的方法为:animator.cancel();

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值