美文网首页
Android ViewPropertyAnimator

Android ViewPropertyAnimator

作者: as_pixar | 来源:发表于2020-06-15 19:14 被阅读0次

我所学到的任何有价值的知识都是由自学中得来的。——达尔文

在Android 3.0引入了属性动画,并新增了属性的get/set函数,比如setAlpha(),setTranslateX(),setScaleX()等。很明显,ObjectAnimator不仅能设置这些属性,还能设置自定义属性,但在使用中,这些默认的属性动画确实很常见的。Android开发团队也意识到这一点,没有为View的动画操作提供一种更加便捷的用法确实有点不太人性化,于是在Android 3.1 中补充了ViewPropertyAnimator这个机制。
我们先来回顾之前的用法,比如,我们想要让一个TextView 从常规状态变成透明状态

private void objectAnimator() {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ballIv, "alpha", 0);
        objectAnimator.start();
}

看上去不太复杂,确实不太容易理解。
ViewPropertyAnimator提供了更加易懂,更加面向对象的API,如下所示:

ballIv.animate().alpha(0);

非常简单,除此之外,还可以非常容易将多个动画结合起来。比如,将控件引动到(50,250)

ballIv.animate().x(50).y(250);
  • animate 整个系统从调用View 的这个叫做 animate() 的新函数开始,这个函数会返回一个ViewPropertyAnimator对象,可以通过调用这个对象的函数来设置需要实现动作的属性。

  • 自动开始:我们并没有显示调用过start()函数,在新的API中,启动动画是隐式的,在声明完成后,动画就开始了。这里有一个细节,就是这些动画实际上会在下一次界面刷新的时候启动,ViewPropertyAnimator正是通过这个机制来讲所有动画结合在一起的。如果你继续在声明动画,它就会继续将这些动画添加到 下一帧开始的动画列表中。而当你声明完毕并结束对UI线程的控制之后,时间队列机制开始起作用,动画也就开始了。

属性设置

这里小结一下ViewPropertyAnimator常用的用于设置属性的函数,如下所示

函数号 含义
alpha(float value) 设置透明度
scaleY(float value) 设置Y轴方向的缩放大小
scaleX(float value) 设置X轴方向的缩放大小
translationX(float value) 设置X轴方向的移动值
translationY(float value) 设置Y轴方向的移动值
rotation(float value) 设置Z轴旋转度数
rotationX(float value) 设置X轴旋转度数
rotationY(float value) 设置Y轴旋转度数
x(float value) 相对父容器左上角坐标X轴方向最终位置
y(float value) 相对父容器左上角坐标Y轴方向最终位置
alphaBy(float value) 设置透明度增量
rotationXBy(float value) 设置绕X轴旋转增量
rotationYBy(float value) 设置绕Y轴旋转增量
translationXBy(float value) 设置X轴方向的移动值增量
translationYBy(float value) 设置Y轴方向的移动值增量
scaleXBy(float value) 设置X轴方向的缩放大小增量
scaleYBy(float value) 设置Y轴方向的缩放大小增量
xBy(float value) 相对父容器左上角坐标X轴方向位置增量
yBy(float value) 相对父容器左上角坐标Y轴方向位置增量
setInterpolator(TimeInterpolator interpolator) 设置插值器
setStartDelay(long startDelay) 设置开始延迟
setDuration(long duration) 设置动画时长

关于alpha ,rotaion,scale属性,在讲解ObjectAnimator中已经提到,这里就不再赘述。而x(flaot value) 和 y(flaot value) 对应的是在Android 3.0中新增的setX(float value) ,setY(float value)函数,用于将控件移动到指定位置,而这个位置是以控件的父窗口左上角坐标为原点的。与 translationX(float value)和 translationY(float value)不同的是,translation是偏移距离,是相对于当前控件的坐标。

上面的属性设置函数都对应着一个以By结尾的函数,比如scaleY(float value) 与scaleYBy(float value) ,它们的区别与View中的scrollTo()和scrollBy()函数区别一样。scrollTo表示滚动到某个顶点,scrollBy() 表示在当前位置在滚动一段距离,这里的scaleY(float value) 表示将视图放大到一定值,scaleYBy(float value) 表示在当前点在增加value。

下面举个例子说明,有两个TextView ,当点击按钮时,两个TextView分别调用scaleY()和scaleYBy()函数
先来看下布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="100dp"
        android:text="Start Anim"
        android:textAllCaps="false" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="100dp"
        android:background="#FF5722"
        android:padding="10dp"
        android:text="TextView 1"
        android:textColor="@android:color/white" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="100dp"
        android:background="#FF5722"
        android:padding="10dp"
        android:text="TextView 2"
        android:textColor="@android:color/white" />

</LinearLayout>

很简单,截个图



我们再来点击按钮后这个TextView

public class ViewPropertyAnimatorActivity extends AppCompatActivity {

    private AppCompatTextView tv1;
    private AppCompatTextView tv2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_property_animator);

        AppCompatButton btnStart = findViewById(R.id.btnStart);
        tv1 = findViewById(R.id.tv1);
        tv2 = findViewById(R.id.tv2);

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv1.animate().scaleY(2);
                tv2.animate().scaleYBy(2);
            }
        });
    }

}

点击按钮多次,tv1 在Y轴方向始终放大到当前控件的2倍,tv2在Y轴方向点击一次增量放大2倍。



从效果图中可以明显的看出这两个函数的区别。

设置监听器

虽然ViewPropertyAnimator并非派生自Animator,但它仍允许我们设置Animator.AnimatorListener监听器

tv1.animate().scaleX(2).scaleY(2).setListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        Log.d(TAG, "----onAnimationStart");
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        Log.d(TAG, "----onAnimationEnd");
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        Log.d(TAG, "----onAnimationCancel");
                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        Log.d(TAG, "----onAnimationRepeat");
                    }
                });

打印结果

06-15 16:35:05.377 9508-9508/com.as.propertyanimator D/ViewPropertyAnimatorActivity: ----onAnimationStart
06-15 16:35:05.701 9508-9508/com.as.propertyanimator D/ViewPropertyAnimatorActivity: ----onAnimationEnd

ViewPropertyAnimator 用起来非常方便,这是它的很大优势。

ViewGroup 内的组件添加动画

前面一直讲述的ValueAnimator,ObjectAnimator,AnimatorSet 都只能针对一个控件做动画。如果我们相对ViewGroup 内部控件做同意入场动画,出场动画等,比如ListView 中的每个Item 在入场时添加动画,在出场时添加动画,在数据变更时添加动画,那么使用ValueAnimator,ObjectAnimator,AnimatorSet 时无法实现的。
为ViewGroup 内组件添加动画,Android 共提供了4中方法

  • 1 layoutAnimation标签与LayoutAnimationController
    layoutAnimation标签在API 1时就已经引入了,它是专门针对ListView 添加入场动画所使用的。LayoutAnimationController 是它的代码实现,它可以实现ListView创建时对其中的每个Item 添加入场动画,而且动画可以自定义。然而,ListView 创建完成后,如果在添加数据,则新添加的数据是不会有入场动画的。

  • 2 gridLaygoutAnimation 标签 与 GridLaygoutAnimationController
    gridLaygoutAnimation标签也是在API 1 时引入的,它是专门针对GridView 添加入场动画的,GridLaygoutAnimationController是它的代码实现。它可以实现GridView创建时对其中的每个Item 添加入场动画,而且动画可以自定义。与layoutAnimation标签相同的事,动画只会在GridView 初次创建时出现,GridView 创建完成后,如果在添加数据,则新添加的数据是不会有入场动画的。

  • 3 android:animateLayoutChanges属性
    在API 11 之后,Android 为了支持ViewGroup 类控件,在添加或者移除其中控件时自动添加动画,提供了一个非常简单的属性android:animateLayoutChanges="true/false",所有派生自ViewGroup 类的控件都具有此属性。而且只要在XML中添加这个属性,就能实现在添加/删除其中的控件时带有默认动画,遗憾的是,动画不能自定义。

  • 4 LayoutTransition
    LayoutTransition 在API 11之后引入,可以实现ViewGroup 动态添加或删除其中的控件时指定动画,动画可以自定义。对比以上4中方法,LayoutTransition 是最强大的,这也是本节的重点;而有关layoutAnimation 和 gridLaygoutAnimation 标签,由于自身存在缺陷,新增数据时无法添加动画,这里就不在讲解。

下面看个例子

1 布局代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/addBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="添加控件" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/delBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="删除控件" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/llContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:orientation="vertical" />

</LinearLayout>

布局代码中有两个按钮,而位于代码底部的是一个LinearLayout标签,作为动态添加btn的容器,需要注意的时,这里给它添加了android:animateLayoutChanges="true"属性,也就是说,它内部控件在添加或者删除时会带有默认动画。

package com.as.propertyanimator;

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;

import java.util.Locale;

public class ViewGroupAnimatorActivity extends AppCompatActivity {

    private int btnCount = 0;
    private LinearLayout llContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_group_animator);

        AppCompatButton addBtn = findViewById(R.id.addBtn);
        AppCompatButton delBtn = findViewById(R.id.delBtn);
        llContainer = findViewById(R.id.llContainer);

        addBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addButtonView();
            }
        });

        delBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                removeButtonView();
            }
        });
    }

    private void addButtonView() {
        btnCount++;
        Button button = new Button(this);
        button.setText("button " + btnCount);
        LinearLayout.LayoutParams layoutParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        button.setLayoutParams(layoutParams);
        llContainer.addView(button, 0);
    }

    private void removeButtonView() {
        btnCount--;
        if (btnCount < 0) {
            return;
        }
        llContainer.removeViewAt(0);
    }

}

单击添加按钮,我们动态创建一个按钮实例,并将其添加到LinearLayout容器的第一个元素。在单机删除控件时,总是将LinearLayout 容器中的第一个元素删除。
通过对实例的讲解,可以发现,只需在ViewGroup 的xml中添加一行代码,android:animateLayoutChanges="true"即可实现内部控件在添加/删除时都带有默认动画,但是通过android:animateLayoutChanges="true"属性添加的动画效果不可以自定义。

LayoutTransition

要使用LayoutTransition是非常容易的,只需要三步

private void addButtonView() {
        //第一步
        LayoutTransition layoutTransition = new LayoutTransition();

        //第二步
        ObjectAnimator rotation = ObjectAnimator.ofFloat(null, "rotation", 0, 90, 0);
        layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, rotation);

        //第三步
        llContainer.setLayoutTransition(layoutTransition);

        btnCount++;
        Button button = new Button(this);
        button.setText("button " + btnCount);
        LinearLayout.LayoutParams layoutParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        button.setLayoutParams(layoutParams);
        llContainer.addView(button, 0);
    }

在第二步中, layoutTransition.setAnimator设置动画函数声明如下

public void setAnimator(int transitionType, Animator animator)

其中,int transitionType表示当前动画的应用范围

  • APPEARING : 元素在容器中出现时所定义的动画
  • DISAPPEARING : 元素在容器中消失时所定义的动画
  • CHANGE_APPEARING :由于容器中要显示一个新的元素,其它变化的元素所应用的动画。
  • CHANGE_DISAPPEARING :容器中当消失一个新的元素,其它变化的元素所应用的动画。
    Animation animation :表示当前所选控件所使用的动画。
LayoutTransition.APPEARING 与 LayoutTransition.DISAPPEARING

LayoutTransition.APPEARING 用于指定当前控件被添加时的动画,LayoutTransition.DISAPPEARING 则用于控件被删除时的动画。下面举例说明其用法与效果!

我们来看看代码

package com.as.propertyanimator;

import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;

import java.util.Locale;

public class ViewGroupAnimatorActivity extends AppCompatActivity {

    private int btnCount = 0;
    private LinearLayout llContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_group_animator);

        AppCompatButton addBtn = findViewById(R.id.addBtn);
        AppCompatButton delBtn = findViewById(R.id.delBtn);
        llContainer = findViewById(R.id.llContainer);

        //第一步
        LayoutTransition layoutTransition = new LayoutTransition();

        //入场动画:视图出现时显示的动画
        ObjectAnimator animInRotationY = ObjectAnimator.ofFloat(null, "RotationY", 0, 180, 0);
        layoutTransition.setAnimator(LayoutTransition.APPEARING, animInRotationY);

        //出场动画:视图消失时的动画
        ObjectAnimator animOutRotation = ObjectAnimator.ofFloat(null, "Rotation", 0, 90, 0);
        layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOutRotation);


        //第三步
        llContainer.setLayoutTransition(layoutTransition);

        addBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addButtonView();
            }
        });

        delBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                removeButtonView();
            }
        });
    }


    private void addButtonView() {
        btnCount++;
        Button button = new Button(this);
        button.setText("button " + btnCount);
        LinearLayout.LayoutParams layoutParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        button.setLayoutParams(layoutParams);
        llContainer.addView(button, 0);
    }

    private void removeButtonView() {
        btnCount--;
        if (btnCount < 0) {
            return;
        }
        llContainer.removeViewAt(0);
    }

}

其中addButtonView()和removeButtonView()函数代码很简单,为ViewGroup添加控件,只需将创建好的LayoutTransition变量设置为ViewGroup容器即可。这里设置LayoutTransition的过程就是本小节提到的三个步骤。
第一步

LayoutTransition layoutTransition = new LayoutTransition();

第二步,创建动画并进行设置

      //入场动画:视图出现时显示的动画
        ObjectAnimator animInRotationY = ObjectAnimator.ofFloat(null, "RotationY", 0, 180, 0);
        layoutTransition.setAnimator(LayoutTransition.APPEARING, animInRotationY);

        //出场动画:视图消失时的动画
        ObjectAnimator animOutRotation = ObjectAnimator.ofFloat(null, "Rotation", 0, 90, 0);
        layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOutRotation);

上述代码的含义是:当一个控件被插入时,这个被插入的控件,所使用的动画绕Y轴旋转180度再回来,当第一个控件被移除,这个被移除的控件所使用的动画,既绕Z轴旋转90度再回来。

LayoutTransition.CHANGE_APPEARING

用于指定在容器中添加控件时,其它已有控件需要移动时的动画。LayoutTransition.CHANGE_DISAPPEARING 用于指定在容器中删除控件时,其它已有控件需要移动时的动画。

核心代码如下

 PropertyValuesHolder pvLeft = PropertyValuesHolder.ofInt("left", 0, 0);
        PropertyValuesHolder pvTop = PropertyValuesHolder.ofInt("top", 0, 0);
        PropertyValuesHolder pvScaleX = PropertyValuesHolder.ofFloat("scaleX", 1,0, 1);


        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(llContainer, pvLeft, pvTop, pvScaleX);
        layoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, objectAnimator);

        //第三步
        llContainer.setLayoutTransition(layoutTransition);

在这个示例中,只添加了LayoutTransition.CHANGE_APPEARING 效果,这里有几个注意项,LayoutTransition.CHANGE_APPEARING 和LayoutTransition.CHANGE_DISAPPEARING 必须使用PropertyValuesHolder所构造的动画才会有效。也就是,ObjectAnimator构造的动画,在这里不会有效果。

在构造PropertyValuesHolder动画时,left,top属性的变化时必须写的。如果不需要变动,则直接写为:

        PropertyValuesHolder pvLeft = PropertyValuesHolder.ofInt("left", 0, 0);
        PropertyValuesHolder pvTop = PropertyValuesHolder.ofInt("top", 0, 0);

LayoutTransition.CHANGE_DISAPPEARING

核心代码如下:

 PropertyValuesHolder outLeft = PropertyValuesHolder.ofInt("left", 0, 0);
        PropertyValuesHolder outTop = PropertyValuesHolder.ofInt("top", 0, 0);

        Keyframe keyframe0 = Keyframe.ofFloat(0, 0);
        Keyframe keyframe1 = Keyframe.ofFloat(0.1f, -20);
        Keyframe keyframe2 = Keyframe.ofFloat(0.2f, 20);
        Keyframe keyframe3 = Keyframe.ofFloat(0.3f, -20);
        Keyframe keyframe4 = Keyframe.ofFloat(0.4f, 20);
        Keyframe keyframe5 = Keyframe.ofFloat(0.5f, -20);
        Keyframe keyframe6 = Keyframe.ofFloat(0.6f, 20);
        Keyframe keyframe7 = Keyframe.ofFloat(0.7f, -20);
        Keyframe keyframe8 = Keyframe.ofFloat(0.8f, 20);
        Keyframe keyframe9 = Keyframe.ofFloat(0.9f, -20);
        Keyframe keyframe10 = Keyframe.ofFloat(1f, 0);

        PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation",
                keyframe0, keyframe1, keyframe2,
                keyframe3, keyframe4, keyframe5, keyframe6,
                keyframe7, keyframe8, keyframe9, keyframe10);

        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(llContainer, outLeft, outTop, rotationHolder);
        layoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, objectAnimator);

        //第三步
        llContainer.setLayoutTransition(layoutTransition);

第一步,有left,top 属性是必须的,但我们在做响铃时不需要left,top属性变化,所以将它们设置为无效值。

第二步 用Keyframe 构造PropertyValuesHolder 动画。PropertyValuesHolder 动画由4个构造方法:ofInt(),ofFloat(),ofObject(),ofKeyframe(),这里使用的时ofKeyframe()。有关PropertyValuesHolder动画的方法在上一篇博客已经讲过了,这里就不再讲解了。

第三步,设置LayoutTransition.CHANGE_DISAPPEARING动画。

LayoutTransition 还有其它函数,我么一起看一下

// 所有动画完成所需要的时长
public void setDuration(long duration)

// 针对单个Type设置动画时长
public void setDuration(int transitionType, long duration)

// 针对单个Type设置插值器
// transitionType 有 APPEARING ,DISAPPEARING , CHANGE_APPEARING,CHANGE_DISAPPEARING,
public void setInterpolator(int transitionType, TimeInterpolator interpolator) 

// 针对单个Type设置动画延迟时间
public void setStartDelay(int transitionType, long delay)

// 针对单个Type 设置每个子item 动画时间间隔
public void setStagger(int transitionType, long duration)

这些函数有兴趣动手!

相关文章

网友评论

      本文标题:Android ViewPropertyAnimator

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