Android动画效果-更新中

概述

Android系统提供了三种实现动画的方式,一种是补间动画(Tween Animation 在SDK中成为View Animation),另一种是帧动画(Frame Animation 在SDK中称为Drawable Animation) ,第三种 属性动画(property animation )。

  • 补间动画可以实现View组件的移动、放大、缩小以及渐变等效果
  • 帧动画则提供了一种逐帧播放图片的动画方式
  • 属性动画-补间动画和帧动画不能胜任复杂动画,所以属性动画应运而生

无论是补间动画还是帧动画或者是属性动画,Android均为其进行了封装,提供了非常简单的应用接口。

3.0以前,android支持两种动画模式,tween animation,frame animation,在android3.0中又引入了一个新的动画系统:property animation。

这三种动画模式在SDK中被称为
view animation,
drawable animation,
property animation。

可通过NineOldAndroids项目在3.0之前的系统中使用Property Animation。


下面我们分别详细的介绍下这三种动画的使用。

补间动画(Tween Animation/View Animation)

Animation类,动画抽象类

Animation类是Android系统的一个动画抽象类,所有其他一些动画类都要继承该类中的实现方法。Animation类主要用于补间动画效果,提供了动画启动、停止、重复、持续时间等方法。Animation类中的方法适用于任何一种补间动画对象。

setDuration方法:设置持续时间

【功能说明】该方法用于设置动画的持续时间,以毫秒为单位。该方法是设置补间动画时间长度的主要方法,使用非常普遍。

【基本语法】public void setDuration (long durationMillis)

其中,参数durationMillis为动画的持续时间,单位为毫秒(ms)。

setDuration方法的示例可以参阅startNow方法中的示例代码。

startNow方法:立刻启动动画

【功能说明】该方法用于启动执行一个动画。该方法是启动执行动画的主要方法,使用时需要先通过setAnimation方法为某一个View对象设置动画。另外,用户在程序中也可以使用View组件的startAnimation方法来启动执行动画。

【基本语法】public void startNow ()

【实例演示】下面通过代码来演示如何设置一个简单的动画效果。


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;

import com.turing.base.R;

public class Animation_startNow extends AppCompatActivity {


    private ImageView mIdIvFace;

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

        mIdIvFace = (ImageView) findViewById(R.id.id_iv_face);


        // 设置移动效果
        /**
         *
         * float fromXDelta 动画开始的点离当前View X坐标上的差值
         * float toXDelta 动画结束的点离当前View X坐标上的差值
         * float fromYDelta 动画开始的点离当前View Y坐标上的差值
         * float toYDelta 动画开始的点离当前View Y坐标上的差值
         */
        Animation translateAnimation = new TranslateAnimation(0, 200, 0, 200);
        // 设置动画持续时间
        translateAnimation.setDuration(3000);
        // 为ImageView设置动画效果
        mIdIvFace.setAnimation(translateAnimation);
        // 启动动画
        translateAnimation.startNow();
    }

}

在这段代码中,首先初始化了一个移动的动画效果translateAnimation。然后,通过setDuration方法设置动画持续时间,并为image对象设置动画效果,最后使用startNow方法启动动画效果。
显示效果:图片将沿45 方向向右下角移动。

这里写图片描述


start方法:启动动画

【功能说明】该方法用于启动执行一个动画。该方法是启动执行动画的另一个主要方法,使用时需要先通过setAnimation方法为某一个View对象设置动画。start方法区别于startNow方法的地方在于,start方法可以用于在getTransformation方法被调用时启动动画。

【基本语法】public void start ()

start方法的执行效果类似于startNow方法,这里不再赘述。


cancel方法:取消动画

【功能说明】该方法用于取消一个动画的执行。该方法是取得一个正在执行中的动画的主要方法。cancel方法和startNow方法结合可以实现对动画执行过程的控制。需要注意的是,通过cancel方法取消的动画,必须使用reset方法或者setAnimation方法重新设置,才可以再次执行动画。

【基本语法】public void cancel ()

【实例演示】下面通过代码来演示如何取消动画效果执行。


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.ImageView;

import com.turing.base.R;

public class Animation_cancel extends AppCompatActivity implements View.OnClickListener {


    private Button mIdBtnStartAnimation;
    private Button mIdBtnStopAnimation;
    private ImageView mIdIvFlag;

    Animation translateAnimation;

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

        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation);
        mIdBtnStopAnimation = (Button) findViewById(R.id.id_btn_stopAnimation);
        mIdIvFlag = (ImageView) findViewById(R.id.id_iv_flag);

        mIdBtnStartAnimation.setOnClickListener(this);
        mIdBtnStopAnimation.setOnClickListener(this);


        // 设置 移动效果
        translateAnimation = new TranslateAnimation(0, 200, 0, 200);
    }

    /**
     * 2.3的版本 translateAnimation.startNow(); 有效,高版本无效 .
     *  mIdIvFlag.startAnimation(translateAnimation); 高版本和低版本都有效。
     * @param v
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation:
                // 设置动画时长
                translateAnimation.setDuration(3000);
                // 为imageView设置动画
                mIdIvFlag.setAnimation(translateAnimation);
                // 开启动画
               // translateAnimation.startNow();
                mIdIvFlag.startAnimation(translateAnimation);
                break;
            case R.id.id_btn_stopAnimation:
                translateAnimation.cancel();
                break;
            default:
                break;
        }
    }
}

在这段代码中,首先初始化了一个移动的动画效果translateAnimation。然后,在第一个按钮监听器中,通过setDuration方法设置动画持续时间,并为image对象设置动画效果,最后使用startNow方法启动动画效果。在第二个按钮监听器中,调用cancel方法取消动画执行。

注意:

translateAnimation.startNow(); 2.3的版本 有效,高版本无效 .
mIdIvFlag.startAnimation(translateAnimation); 高版本和低版本都有效。

这里写图片描述


setRepeatCount方法:设置重复次数

【功能说明】该方法用于设置一个动画效果重复执行的次数。Android系统默认每个动画仅执行一次,通过该方法可以设置动画执行多次。

【基本语法】public void setRepeatCount (int repeatCount)

其中,参数repeatCount为重复执行的次数。如果设置为n,则动画将执行n+1次。

【实例演示】下面通过代码来演示如何连续执行多次动画效果。

  translateAnimation.setRepeatCount(2);     //设置重复次数

注意:这里设置的是动画重复执行的次数,而不是动画执行的次数。故动画执行的次数为动画重复执行的次数加1。

这里写图片描述


setFillEnabled方法:使能填充效果

【功能说明】该方法用于使能填充效果。当该方法设置为true时,将执行setFillBefore和setFillAfter方法,否则将忽略setFillBefore和setFillAfter方法。

【基本语法】public void setFillEnabled (boolean fillEnabled)

其中,参数fillEnabled为是否使能填充效果,true表示使能该效果,false表示禁用该效果。

setFillEnabled方法的示例可以参阅setFillBefore方法和setFillAfter方法中的示例代码。

setFillBefore方法:设置起始填充

【功能说明】该方法用于设置一个动画效果执行完毕后,View对象返回到起始的位置。该方法的效果是系统默认的效果。该方法的执行,需要首先通过setFillEnabled方法使能填充效果,否则设置无效。

【基本语法】public void setFillBefore (boolean fillBefore)

其中,参数fillBefore为是否执行起始填充效果,true表示使能该效果,false表示禁用该效果。

【实例演示】下面通过代码来演示如何让View对象在动画执行完毕后回归到起始位置。

//回归起始位置
translateAnimation.setFillBefore(true);             
//使能填充效果                translateAnimation.setFillEnabled(true);            

通过setFillBefore方法使对象回到起始点,并使用setFillEnabled使能填充效果

当点击”START ANIMATION”按钮时,图片开始移动,当动画结束之后,图片将自动跳回到起始位置。(默认效果)

这里写图片描述


setFillAfter方法:设置终止填充

【功能说明】该方法用于设置一个动画效果执行完毕后,View对象保留在终止的位置。该方法的执行,需要首先通过setFillEnabled方法使能填充效果,否则设置无效。

【基本语法】public void setFillAfter (boolean fillAfter)

其中,参数fillAfter为是否执行终止填充效果,true表示使能该效果,false表示禁用该效果。

【实例演示】下面通过代码来演示如何让View对象在动画执行完毕后保留在终止位置。

 translateAnimation.setFillAfter (true);             //保留在终止位置
                    translateAnimation.setFillEnabled(true);            //使能填充效果  ;

通过setFillAfter方法使对象保留在终止点,并使用setFillEnabled使能填充效果。当点击”START ANIMATION”按钮时,图片开始移动,当动画结束之后,图片将停留在终止位置。

这里写图片描述


setRepeatMode方法:设置重复模式

【功能说明】该方法用于设置一个动画效果执行的重复模式。Android系统中提供了几种重复模式,其中最主要的便是RESTART模式和REVERSE模式。

【基本语法】public void setRepeatMode (int repeatMode)

其中,参数repeatMode为动画效果的重复模式,常用的取值如下。

RESTART:重新从头开始执行。

REVERSE:反方向执行。

【实例演示】下面通过代码来演示如何更改动画路径的方向。

translateAnimation.setRepeatCount(2);   //设置重复次数
                    // translateAnimation.setRepeatMode(Animation.RESTART); //重新从头
                    translateAnimation.setRepeatMode(Animation.REVERSE);  //反方向执

通过setRepeatCount方法设置重复次数,通过setRepeatMode设置重复模式,最后使用startAnimation方法启动动画效果。

如果设置的重复模式为Animation.RESTART,则表示重新从头开始执行。

如果设置的重复模式为Animation.REVERSE,则表示反方向执行,当一次动画执行结束之后,图片将向反方向运动。
这里写图片描述


setStartOffset方法:设置启动时间

【功能说明】该方法用于设置一个动画执行的启动时间,单位为毫秒。系统默认当执行start方法后立刻执行动画,当使用该方法设置后,将延迟一定的时间再启动动画。

【基本语法】public void setStartOffset (long startOffset)

其中,参数startOffset为动画的启动时间,单位为毫秒(ms)。

【实例演示】下面通过代码来演示如何更改动画的启动时间。

translateAnimation.setStartOffset(3000);  //设置启动时间

通过setStartOffset方法设置动画的启动时间,当点击”START ANIMATION”按钮时,将等待3秒之后,图片才开始移动。

这里写图片描述


TranslateAnimation类:位置变化动画类

TranslateAnimation类是Android系统中的位置变化动画类,用于控制View对象的位置变化,该类继承于Animation类。TranslateAnimation类中的很多方法都与Animation类一致,该类中最常用的方法便是TranslateAnimation构造方法。

【基本语法】public TranslateAnimation (float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)

参数说明

fromXDelta:位置变化的起始点X坐标。

toXDelta:位置变化的结束点X坐标。

fromYDelta:位置变化的起始点Y坐标。

toYDelta:位置变化的结束点Y坐标。


RotateAnimation类:旋转变化动画类

RotateAnimation类是Android系统中的旋转变化动画类,用于控制View对象的旋转动作,该类继承于Animation类。RotateAnimation类中的很多方法都与Animation类一致,该类中最常用的方法便是RotateAnimation构造方法。

【基本语法】public RotateAnimation (float fromDegrees, float toDegrees, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)

参数说明

fromDegrees:旋转的开始角度。

toDegrees:旋转的结束角度。

pivotXType:X轴的伸缩模式,可以取值为ABSOLUTE、RELATIVE_TO_SELF、RELATIVE_TO_PARENT。

pivotXValue:X坐标的伸缩值。

pivotYType:Y轴的伸缩模式,可以取值为ABSOLUTE、RELATIVE_TO_SELF、RELATIVE_TO_PARENT。

pivotYValue:Y坐标的伸缩值。

【实例演示】下面通过代码来演示如何设置一个简单的旋转变化动画效果。


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.ImageView;

import com.apkfuns.logutils.LogUtils;
import com.turing.base.R;
import com.turing.base.utils.AlertUtil;

public class RotateAnimationDemo extends AppCompatActivity implements View.OnClickListener {


    private Button mIdBtnStartAnimation;
    private Button mIdBtnStopAnimation;
    private ImageView mIdIvFlag;


    private Animation rotateAnimation;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rotate_animation_demo);
        // 初始组件 注册监听
        assignViews();
        //设置旋转变化动画对象
        rotateAnimation = new RotateAnimation(0f, 360f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);


    }

    private void assignViews() {
        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation1);
        mIdBtnStopAnimation = (Button) findViewById(R.id.id_btn_stopAnimation1);
        mIdIvFlag = (ImageView) findViewById(R.id.id_iv_flag1);

        mIdBtnStartAnimation.setOnClickListener(this);
        mIdBtnStopAnimation.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation1:// 开始动画
                AlertUtil.showToastShort(RotateAnimationDemo.this, "start");
                LogUtils.d("start");
                rotateAnimation.setDuration(3000);              //持续时间
                mIdIvFlag.setAnimation(rotateAnimation);        //设置动画
                mIdIvFlag.startAnimation(rotateAnimation);      //启动动画
                break;
            case R.id.id_btn_stopAnimation1:// 取消动画
                rotateAnimation.cancel();
                break;
            default:
                break;
        }
    }
}

首先通过RotateAnimation构造方法创建了一个旋转变化的动画对象。然后,在第一个按钮监听器中设置了动画的持续时间,之后启动该动画。在第二个按钮监听器中取消该动画

这里写图片描述


ScaleAnimation类:尺寸变化动画类

ScaleAnimation类是Android系统中的尺寸变化动画类,用于控制View对象的尺寸变化,该类继承于Animation类。ScaleAnimation类中的很多方法都与Animation类一致,该类中最常用的方法便是ScaleAnimation构造方法。

【基本语法】public ScaleAnimation (float fromX, float toX, float fromY, float toY, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)

参数说明

fromX:起始X坐标上的伸缩尺寸。

toX:结束X坐标上的伸缩尺寸。

fromY:起始Y坐标上的伸缩尺寸。

toY:结束Y坐标上的伸缩尺寸。

pivotXType:X轴的伸缩模式,可以取值为ABSOLUTE、RELATIVE_TO_SELF、RELATIVE_TO_PARENT。

pivotXValue:X坐标的伸缩值。

pivotYType:Y轴的伸缩模式,可以取值为ABSOLUTE、RELATIVE_TO_SELF、RELATIVE_TO_PARENT。

pivotYValue:Y坐标的伸缩值。

【实例演示】下面通过代码来演示如何设置一个简单的尺寸变化动画效果。

public class ScaleAnimationDemo extends AppCompatActivity implements View.OnClickListener {

    private Button mIdBtnStartAnimation;
    private Button mIdBtnStopAnimation;
    private ImageView mIdIvFlag;


    ScaleAnimation scaleAnimation;

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

        assignViews();
        //设置尺寸变化动画对象
        scaleAnimation = new ScaleAnimation(0f, 1f, 0f, 1f,
                Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    }


    private void assignViews() {
        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation1);
        mIdBtnStopAnimation = (Button) findViewById(R.id.id_btn_stopAnimation1);
        mIdIvFlag = (ImageView) findViewById(R.id.id_iv_flag1);

        mIdBtnStartAnimation.setOnClickListener(this);
        mIdBtnStopAnimation.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation1:// 开始动画
                scaleAnimation.setDuration(3000);              //持续时间
                mIdIvFlag.setAnimation(scaleAnimation);        //设置动画
                mIdIvFlag.startAnimation(scaleAnimation);      //启动动画
                break;
            case R.id.id_btn_stopAnimation1:// 取消动画
                scaleAnimation.cancel();
                break;
            default:
                break;
        }
    }
}

首先通过ScaleAnimation构造方法创建了一个尺寸变化的动画对象。然后,在第一个按钮监听器中设置了动画的持续时间,之后启动该动画。在第二个按钮监听器中取消该动画。读者运行这段代码,将看到图片从小到大逐渐变化,最后,图片增大到原始尺寸的时候停止。

这里写图片描述


AlphaAnimation类:透明度变化动画类

AlphaAnimation类是Android系统中的透明度变化动画类,用于控制View对象的透明度变化,该类继承于Animation类。AlphaAnimation类中的很多方法都与Animation类一致,该类中最常用的方法便是AlphaAnimation构造方法。

【基本语法】public AlphaAnimation (float fromAlpha, float toAlpha)

参数说明

fromAlpha:开始时刻的透明度,取值范围0~1。

toAlpha:结束时刻的透明度,取值范围0~1。

【实例演示】下面通过代码来演示如何设置一个简单的渐变透明度动画效果。

这里写代码片

首先通过AlphaAnimation构造方法创建了一个透明度变化的动画对象。然后,在第一个按钮监听器中设置了动画的持续时间,之后启动该动画。在第二个按钮监听器中取消该动画。运行这段代码,将看到图片的透明度由浅入深逐渐变化。最后,图片变为完全不透明的时候停止。

这里写图片描述


AnimationSet类:动画集合类

AnimationSet类是Android系统中的动画集合类,用于控制View对象进行多个动作的组合,该类继承于Animation类。AnimationSet类中的很多方法都与Animation类一致,该类中最常用的方法便是addAnimation方法,该方法用于为动画集合对象添加动画对象。

【基本语法】public void addAnimation (Animation a)

其中,参数a为Animation动画对象,可以是前述任何一种补间动作。

【实例演示】下面通过代码来演示如何设置一个组合动画效果。


public class AnimationSetDemo extends AppCompatActivity implements View.OnClickListener {

    private Button mIdBtnStartAnimation;
    private Button mIdBtnStopAnimation;
    private ImageView mIdIvFlag;

    Animation translateAnimation, scaleAnimation, alphaAnimation;
    AnimationSet animationSet;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_animation_set_demo);
        assignViews();
        //设置位置变化动画
        translateAnimation = new TranslateAnimation(0, 300, 0, 300);
        //设置尺寸变化动画
        scaleAnimation = new
                ScaleAnimation(0f, 1f, 0f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        //设置透明度变化动画
        alphaAnimation = new AlphaAnimation(0.1f, 1.0f);
    }

    private void assignViews() {
        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation1);
        mIdBtnStopAnimation = (Button) findViewById(R.id.id_btn_stopAnimation1);
        mIdIvFlag = (ImageView) findViewById(R.id.id_iv_flag1);

        mIdBtnStartAnimation.setOnClickListener(this);
        mIdBtnStopAnimation.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation1:// 开始动画

                translateAnimation.setDuration(10000);      //设置位置变化动画的持续时间
                scaleAnimation.setDuration(10000);          //设置尺寸变化动画的持续时间
                alphaAnimation.setDuration(10000);          //设置透明度渐变动画的持续时间

                animationSet = new AnimationSet(true);      //创建动画集对象
                animationSet.addAnimation(translateAnimation);       //添加位置变化动画
                animationSet.addAnimation(scaleAnimation);           //添加尺寸变化动画
                animationSet.addAnimation(alphaAnimation);           //添加透明度渐变动画

                animationSet.setFillAfter(true);                    //停留在最后的位置
                animationSet.setFillEnabled(true);


                mIdIvFlag.setAnimation(animationSet);                    //设置动画
                // animationSet.startNow();    高版本中无效,2.3可以,不建议使用
                mIdIvFlag.startAnimation(animationSet);    //启动动画

                break;
            case R.id.id_btn_stopAnimation1:// 取消动画
                animationSet.cancel();
                break;
            default:
                break;
        }
    }
}

首先构造了位置变化、尺寸变化和透明度变化动画的对象。然后,在第一个按钮监听器中分别设置了动画的持续时间,并通过addAnimation方法添加到动画集中,之后启动该动画。
在第二个按钮监听器中取消该动画。

运行这段代码,将会看到显示效果:图片从小到大,由浅入深,从左上角向右下角移动。当动画结束的时候,图片对象将停留在结束点的位置。

这里写图片描述


AnimationUtils类:动画工具类

AnimationUtils类是Android系统中的动画工具类,提供了控制View对象的一些工具。该类中最常用的方法便是loadAnimation方法,该方法用于加载XML格式的动画配置文件。在Android系统中,除了在代码中设置动画效果外,还可以在XML配置文件中设置动画的组合动作,这种方式适用性更好。

【基本语法】public static Animation loadAnimation (Context context, int id)

参数说明

context:上下文对象。

id:动画配置文件的ID。

【实例演示】下面通过代码来演示如何加载一个XML组合动画效果。

public class AnimationUtilsDemo extends AppCompatActivity implements View.OnClickListener {

    private Button mIdBtnStartAnimation;
    private Button mIdBtnStopAnimation;
    private ImageView mIdIvFlag;
    //动画对象
    Animation loadAnimation;


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

        assignViews();
    }

    private void assignViews() {
        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation1);
        mIdBtnStopAnimation = (Button) findViewById(R.id.id_btn_stopAnimation1);
        mIdIvFlag = (ImageView) findViewById(R.id.id_iv_flag1);

        mIdBtnStartAnimation.setOnClickListener(this);
        mIdBtnStopAnimation.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation1:// 开始动画

                loadAnimation = AnimationUtils.loadAnimation(getApplicationContext(),
                        R.anim.anim);
                mIdIvFlag.setAnimation(loadAnimation);                  //为控件设置动画
                loadAnimation.setFillAfter(true);                   //停留在结束位置
                loadAnimation.setFillEnabled(true);

                mIdIvFlag.startAnimation(loadAnimation);//开始动画
                break;
            case R.id.id_btn_stopAnimation1:// 取消动画
                loadAnimation.cancel();
                break;
            default:
                break;
        }
    }


}

首先声明了动画对象。然后,在第一个按钮监听器中通过loadAnimation方法加载动画配置文件,并设置了动画的一些特征,最后开始执行动画。在第二个按钮监听器中调用cancel方法取消动画执行。这里用到的动画配置文件如下所示,里面定义了位置移动和旋转的组合动画效果。

anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="3000"
        android:toXDelta="0"
        android:toYDelta="300" />
    <rotate
        android:duration="3000"
        android:fromDegrees="270"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toDegrees="360" />
</set>

这里写图片描述


帧动画(Frame Aniamtion/Drawable Animation)

AnimationDrawable类:帧动画类

AnimationDrawable类是Android系统中的帧动画类。帧动画方式类似于放电影的原理,是通过顺序播放多张图片来实现动画效果的,图片之间有一定的动作连贯性,这样人眼看来就像对象真正在运动一样。AnimationDrawable类位于android.graphics.drawable软件包中,本节将介绍帧动画类中的主要编程方法。

start方法:开始动画

【功能说明】该方法用于开始动画执行,其是帧动画效果执行的主要方法。

【基本语法】public void start ()

【实例演示】下面通过代码来演示如何实现一个简单的帧动画。


public class AnimationDrawable_start extends AppCompatActivity implements View.OnClickListener {


    private Button mIdBtnStartAnimation;
    private ImageView mIdIvHorse;

    AnimationDrawable animationDrawable;

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

        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation);
        mIdIvHorse = (ImageView) findViewById(R.id.id_iv_horse);

        mIdBtnStartAnimation.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation:
                //声明帧动画对象 通过imageview.getBackground
                animationDrawable = (AnimationDrawable) mIdIvHorse.getBackground();
                animationDrawable.start();  //开始动画
                AlertUtil.showToastShort(AnimationDrawable_start.this,"start");
                break;
            default:
                break;
        }
    }
}
<?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">

    <Button
        android:id="@+id/id_btn_startAnimation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始帧动画" />

    <ImageView
        android:id="@+id/id_iv_horse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/anim_zhuzhen"
        android:layout_below="@id/id_btn_startAnimation" />

</RelativeLayout>

首先声明了帧动画对象,然后在按钮监听器中直接调用start方法来开始动画执行。为了能够实现动画效果,还需要指定帧动画所需要的图片和动画顺序。然后,在res/drawable目录下新建一个帧动画配置文件,并将其设置为图片控件的背景。帧动画配置文件的内容如下:

<?xml version="1.0" encoding="utf-8"?><!--
    根标签为animation-list,其中oneshot代表着是否只展示一遍,设置为false会不停的循环播放动画
    根标签下,通过item标签对动画中的每一个图片进行声明
    android:duration 表示展示所用的该图片的时间长度
 -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item
        android:drawable="@drawable/horse1"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse2"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse3"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse4"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse5"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse6"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse7"
        android:duration="100" />
    <item
        android:drawable="@drawable/horse8"
        android:duration="100" />
</animation-list>

这里写图片描述


stop方法:停止动画

【功能说明】该方法用于停止动画执行,其是帧动画效果停止的主要方法。start方法常和stop方法一起来使用。

【基本语法】public void stop ()

【实例演示】下面通过代码来演示如何实现一个简单的帧动画。

 @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation:

                animationDrawable.start();  //开始动画
                break;
            case R.id.id_btn_stop:
                animationDrawable.stop(); //停止动画
                break;
            default:
                break;
        }
    }

注意:
如果在Activity中我不想用按钮触发这个动画,要程序运行即播放动画,怎么做呢?
最好是在onWindowFocusChanged这个方法中启动动画。因为在onCreate中启动动画, AnimationDrawable有可能还没有完全attach 到Window上

这里写图片描述


addFrame方法:添加动画帧

【功能说明】该方法用于为帧动画对象添加动画帧。该方法主要用于动态修改帧动画内容的场合,可以根据需要增加一些动画帧。

【基本语法】public void addFrame (Drawable frame, int duration)

参数说明

frame:动画帧的Drawable对象。

duration:动画帧的持续时间,单位为毫秒。

【实例演示】下面通过代码来演示如何实现一个简单的帧动画。

public class AnimationDrawable_addFrame extends AppCompatActivity implements View.OnClickListener {


    private Button mIdBtnStartAnimation, id_btn_stop;
    private ImageView mIdIvHorse;

    AnimationDrawable animationDrawable;

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

        mIdBtnStartAnimation = (Button) findViewById(R.id.id_btn_startAnimation);
        id_btn_stop = (Button) findViewById(R.id.id_btn_stop);

        mIdIvHorse = (ImageView) findViewById(R.id.id_iv_horse);

        //声明帧动画对象
        animationDrawable = (AnimationDrawable) mIdIvHorse.getBackground();
        // 注册监听监听事件
        mIdBtnStartAnimation.setOnClickListener(this);
        id_btn_stop.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.id_btn_startAnimation:


                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx1), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx2), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx3), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx4), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx5), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx6), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx7), 100);//添加帧
                animationDrawable.addFrame(getResources().getDrawable(R.drawable.zzlx8), 100);//添加帧


                animationDrawable.start();  //开始动画

                break;
            case R.id.id_btn_stop:
                animationDrawable.stop();
                break;
            default:
                break;
        }
    }


}

首先声明了帧动画对象。然后,在第一个按钮监听器中使用addFrame方法添加了动画帧,之后直接调用start方法来开始动画执行,在第二个按钮监听器中直接调用stop方法来停止动画执行

这里写图片描述


setOneShot方法:设置播放方式

【功能说明】该方法用于设置帧动画的播放方式,可以是单次播放,也可以是循环播放。在系统默认情况下采用的是单次播放的方式。该方法主要用于循环播放的场合。

【基本语法】public void setOneShot (boolean oneShot)

其中,参数oneShot表示了动画是否执行一次,true表示仅执行一次,false表示无限次循环执行动画效果。

 ad.setOneShot(false);   //循环播放  
 ad.start();            //开始动画  

这里写图片描述


setAlpha方法:设置透明度

【功能说明】该方法用于设置帧动画播放过程中图片的透明度。该方法经常用于一些特效显示效果的场合。

【基本语法】public void setAlpha (int alpha)

其中,参数alpha表示图片的透明度,取值范围为0~255。

 ad.setAlpha(100);                                   //设置透明度  
            ad.start();                                     //开始动画  
        }  

这里写图片描述


getNumberOfFrames方法:获取帧数

【功能说明】该方法用于获取动画的帧个数,也就是按照顺序播放了几张图片。该方法常常用于获取帧动画对象特性的场合。

【基本语法】public int getNumberOfFrames ()

【实例演示】下面通过代码来演示如何获取帧动画播放帧的个数。

  int num;  
            ad.start();                                     //开始动画  
            num=ad.getNumberOfFrames();                     //获取帧数  
            Toast.makeText(getApplicationContext(), "当前动画需要播放"+num+"帧",   
                        Toast.LENGTH_LONG).show();                      //显示  
        }  

这里写图片描述


小结

动画技术能够给应用程序带来丰富的特效,增强用户体验。Android3.0之前的SDK提供了两种动画实现方式,补间动画(Tween Animation)和帧动画(Frame Animation)。补间动画可以对View对象进行简单的移动、旋转、缩放和渐变等效果,帧动画则提供了传统的逐帧播放图片的动画方式。


还有些优质的文章推荐:
http://www.jianshu.com/p/6460d5788406
http://www.jianshu.com/p/7ba70e061bb4


属性动画(Property Animation)

概述

在android3.0中又引入了一个新的动画系统:property animation。

可通过NineOldAndroids项目在3.0之前的系统中使用Property Animation。

和视图动画的区别

视图动画只能作用于View,而且视图动画改变的只是View的绘制效果,View真正的属性并没有改变。

比如,一个按钮做平移的动画,虽然按钮的确做了平移,但按钮可点击的区域并没随着平移而改变,还是在原来的位置。

示例:
在左上角有一个ImageView图标,我们为其设置了点击监听事件,然后当我们使用终止填充效果时,动画结束后图标停留在最后的位置,此时,当我们点击图标时,是触发不到点击事件的,然而我们点击图标原始位置时,却触发了点击事件,由此可见按钮可点击的区域并没随着平移而改变,还是在原来的位置。
这里写图片描述

而属性动画则可以改变真正的属性,从而实现按钮平移时点击区域也跟着平移。通俗点说,属性动画其实就是在一定时间内,按照一定规律来改变对象的属性,从而使对象展现出动画效果。

存放目录res/animator

属性动画和视图动画一样,可以通过xml文件定义。

不同的是:

  • 视图动画的xml文件放于res/anim/目录下,
  • 属性动画的xml文件则放于res/animator/目录下
  • 一个是anim,一个是animator ,务必不要搞混了。
  • 同样的,在Java代码里引用属性动画的xml文件时,则用R.animator.filename,不同于视图动画,引用时为R.anim.filename。

主要元素和类

属性动画主要有三个元素:<animator><objectAnimator><set>
相对应的有三个类:ValueAnimatorObjectAnimatorAnimatorSet

ValueAnimator是基本的动画类,处理值动画,通过监听某一值的变化,进行相应的操作。

ObjectAnimator是ValueAnimator的子类,处理对象动画。

AnimatorSet则为动画集,可以组合另外两种动画或动画集。

相应的三个标签元素的关系也一样。

<animator>标签

概述

<animator>标签与对应的ValueAnimator类提供了属性动画的核心功能,包括计算动画值、动画时间细节、是否重复等。

执行属性动画分两个步骤:

  1. 计算动画值
  2. 将动画值应用到对象和属性上

ValuAnimiator只完成第一步,即只计算值,要实现第二步则需要在值变化的监听器里自行更新对象属性。

通过<animator>标签可以很方便的对ValuAnimiator进行设置,可设置的属性如下:

  • android:duration 动画从开始到结束持续的时长,单位为毫秒
  • android:startOffset 设置动画执行之前的等待时长,单位为毫秒
  • android:repeatCount 设置动画重复执行的次数,默认为0,即不重复;可设为-1或infinite,表示无限重复
  • android:repeatMode 设置动画重复执行的模式,可设为以下两个值其中之一:
    1. restart 动画重复执行时从起点开始,默认为该值
    2. reverse 动画会反方向执行
  • android:valueFrom 动画开始的值,可以为int值、float值或color值
  • android:valueTo 动画结束的值,可以为int值、float值或color值
  • android:valueType 动画值类型,若为color值,则无需设置该属性

    1. intType 指定动画值,即以上两个value属性的值为整型
    2. floatType 指定动画值,即以上两个value属性的值为浮点型,默认值
  • android:interpolator 设置动画速率的变化,比如加速、减速、匀速等,需要指定Interpolator资源 参考http://keeganlee.me/post/android/20151003

示例

将一个按钮的宽度进行缩放,从100%缩放到20%。
这里写图片描述
res/animator/value_animator.xml

<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="100"
    android:valueTo="20"
    android:valueType="intType"/>

可看到,值的变化从100到20,无限重复,反方向执行、动画时长3000毫秒。

ValueAnimatorByXmlAct.java


import android.animation.AnimatorInflater;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.View;

import com.turing.base.R;

public class ValueAnimatorByXmlAct extends AppCompatActivity {

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


    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void onScaleWidth(final View view) {
        // 获取屏幕宽带
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        final int width = metrics.widthPixels;


        /**
         * 属性动画则是通过AnimatorInflater类的loadAnimation()方法获取相应的Animator类实例。
         * 另外,ValueAnimator通过添加AnimatorUpdateListener监听器监听值的变化,从而再手动更新目标对象的属性。
         * 最后,通过调用valueAnimator.start()方法启动动画。
         */

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

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                // 当前动画值,即为当前宽度比例值
                int currentValue = (Integer) animator.getAnimatedValue();
                // 根据比例更改目标view的宽度
                view.getLayoutParams().width = width * currentValue / 100;
                view.requestLayout();
            }
        });

        valueAnimator.start();
    }
}

<objectAnimator>标签

概述

<objectAnimator>标签对应的类为ObjectAnimator,为ValueAnimator的子类。

<objectAnimator>标签与<animator>标签不同的是,<objectAnimator>可以直接指定动画的目标对象的属性。

标签可设置的属性除了和<animator>一样的那些,另外多了一个:

  • android:propertyName 目标对象的属性名,要求目标对象必须提供该属性的setter方法,如果动画的时候没有初始值,还需要提供getter方法

示例

还是用上面的例子,将一个按钮的宽度进行缩放,从100%缩放到20%,但这次改用<objectAnimator>实现。

res/animtor/object_animator.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator  xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:propertyName="width"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="100"
    android:valueTo="20"
    android:valueType="intType" />

<animator>的例子相比,就只是多了一个android:propertyName的属性,设置值为width。也就是说,动画改变的属性为width,值将从100逐渐减到20。另外,值是从setWidth()传递过去的,再从getWidth()获取。而且,这里设置的值代表的是比例值,因此,还需要进行计算转化为实际的宽度值。最后,对象实际的宽度值为view.getLayoutParams().width。因此,我们用一个包装类来包装原始的view对象,对其提供setWidth()和getWidth()方法,代码见Activity中的内部类。


import android.animation.AnimatorInflater;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.View;

import com.turing.base.R;

public class ObjectAnimatorByXmlAct extends AppCompatActivity {

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

    /**
     * Button点击响应事件
     *
     * @param view
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void onScaleWidth(View view) {
        // 获取屏幕的宽度
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        final int width = displayMetrics.widthPixels;

        // 将目标view进行包装
        ViewWrapper wrapper = new ViewWrapper(view, width);

        // 将xml转化为ObjectAnimator对象
        ObjectAnimator objectAnimator = (ObjectAnimator) AnimatorInflater.loadAnimator(this, R.animator.object_animator);

        // 设置动画的目标对象为包装后的view
        objectAnimator.setTarget(wrapper);

        // 启动动画
        objectAnimator.start();

    }


    /**
     * 与<animator>的例子相比,就只是多了一个android:propertyName的属性,设置值为width。
     * 也就是说,动画改变的属性为width,值将从100逐渐减到20。
     * 另外,值是从setWidth()传递过去的,再从getWidth()获取。
     * 而且,这里设置的值代表的是比例值,因此,还需要进行计算转化为实际的宽度值。
     * 最后,对象实际的宽度值为view.getLayoutParams().width。
     * 因此,我将用一个包装类来包装原始的view对象,对其提供setWidth()和getWidth()方法
     */

    private static class ViewWrapper {

        private View target;// 目标对象
        private int maxWidth; //最长宽度值

        public ViewWrapper(View target, int maxWidth) {
            this.target = target;
            this.maxWidth = maxWidth;
        }


        public int getWidth() {
            return target.getLayoutParams().width;
        }

        public void setWidth(int widthValue) {
            //widthValue的值从100到20变化
            target.getLayoutParams().width = maxWidth * widthValue / 100;
            target.requestLayout();
        }
    }
}

setWidth()的代码里,根据比例值转化为了实际的宽度值。
动画处理的代码在onScaleWidth方法中。

activity_object_animator_by_xml.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.turing.base.android_hero.chapter6_Draw.annimation_propertyAnimation.ObjectAnimatorByXmlAct">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="点我"
        android:background="@drawable/bg_btn_normal"
        android:onClick="onScaleWidth"/>

</RelativeLayout>

bg_btn_normal.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#2F90BD" />
    <padding
        android:bottom="12dp"
        android:left="12dp"
        android:right="12dp"
        android:top="12dp" />
    <corners android:radius="10dp" />
</shape>

ObjectAnimator提供了属性的设置,但相应的需要有该属性的setter和getter方法。而ValueAnimator则只是定义了值的变化,并不指定目标属性,所以也不需要提供setter和getter方法,但只能在AnimatorUpdateListener监听器里手动更新属性。不过,也因为没有指定属性,所以其实更具灵活性了,你可以在监听器里根据值的变化做任何事情,比如更新多个属性,比如在缩放宽度的同时做垂直移动。

为了对View更方便的设置属性动画,Android系统也提供了View的一些属性和相应的setter和getter方法:

  • alpha:透明度,默认为1,表示不透明,0表示完全透明
  • pivotX 和 pivotY:旋转的轴点和缩放的基准点,默认是View的中心点
  • scaleX 和 scaleY:基于pivotX和pivotY的缩放,1表示无缩放,小于1表示收缩,大于1则放大
  • rotation、rotationX 和 rotationY:基于轴点(pivotX和pivotY)的旋转,rotation为平面的旋转,rotationX和rotationY为立体的旋转
  • translationX 和 translationY:View的屏幕位置坐标变化量,以layout容器的左上角为坐标原点
  • x 和 y:View在父容器内的最终位置,是左上角坐标和偏移量(translationX,translationY)的和。

<set>标签

<set>标签对应于AnimatorSet类,可以将多个动画组合成一个动画集,如上面提到的在缩放宽度的同时做垂直移动,可以将一个缩放宽度的动画和一个垂直移动的动画组合在一起。
<set>标签有一个属性可以设置动画的时序关系:

  • android:ordering 设置动画的时序关系,取值可为以下两个值之一:

1.together 动画同时执行,默认值 sequ
2.entially 动画按顺序执行

那如果想有些动画同时执行,有些按顺序执行,该怎么办呢?因为<set>标签是可以嵌套其他<set>标签的,也就是说可以将同时执行的组合在一个<set>标签,再嵌在按顺序执行的<set>标签内。

<!-- res/animator/animator_set.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">
    <objectAnimator
        android:duration="3000"
        android:propertyName="width"
        android:valueFrom="100"
        android:valueTo="20"
        android:valueType="intType" />
    <objectAnimator
        android:duration="3000"
        android:propertyName="marginTop"
        android:valueFrom="0"
        android:valueTo="100"
        android:valueType="intType" />
</set>

以上代码可实现两个同时执行的动画,一个将width从100缩放到20,一个将marginTop从0增加到100。多了一个marginTop属性,那么,在ViewWrapper添加setMarginTop()方法,添加后的ViewWrapper类代码如下:

private static class ViewWrapper {
    private View target;
    private int maxWidth;

    public ViewWrapper(View target, int maxWidth) {
        this.target = target;
        this.maxWidth = maxWidth;
    }

    public int getWidth() {
        return target.getLayoutParams().width;
    }

    public void setWidth(int widthValue) {
        target.getLayoutParams().width = maxWidth * widthValue / 100;
        target.requestLayout();
    }

    public void setMarginTop(int margin) {
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) target.getLayoutParams();
        layoutParams.setMargins(0, margin, 0, 0);
        target.setLayoutParams(layoutParams);
    }
}

最后,动画处理的代码:

public void onScaleWidth(View view) {
    // 获取屏幕宽度
    int maxWidth = getWindowManager().getDefaultDisplay().getWidth();
    // 将目标view进行包装
    ViewWrapper wrapper = new ViewWrapper(view, maxWidth);
    // 将xml转化为ObjectAnimator对象
    AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.animator_set);
    // 设置动画的目标对象为包装后的view
    animatorSet.setTarget(wrapper);
    // 启动动画
    animatorSet.start();
}

这样就搞定了,实现了宽度缩放和垂直移动的效果。

这里写图片描述

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值