【Android】动画简介

Android 动画简介

Android中动画分为帧动画,补间动画以及属性动画。

1. 帧动画

1.1 在XML文件中使用:

<animation-list>必需

​ 该元素必须是根元素。包含一个或多个 <item> 元素。

​ 属性:

  • android:oneshot

    布尔值。仅执行一次动画,则为“true”;如果要循环播放动画,则为“false”。

  • <item>

    单帧动画。必须为 <animation-list> 元素的子项。

    属性:

    • android:drawable

      可绘制资源。要用于此帧的可绘制对象。

    • android:duration

      整型。显示此帧的持续时间,以毫秒为单位。

示例如下:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false" >

    <item android:drawable="@drawable/pikachu_1" android:duration="200" />
    <item android:drawable="@drawable/pikachu_2" android:duration="200" />
    <item android:drawable="@drawable/pikachu_3" android:duration="200" />

</animation-list>

1.2 代码调用:

val image: ImageView = findViewById(R.id.image)
image.setBackgroundResource(R.drawable.pikachu)

val animation = image.background
if (animation is Animatable) {
    animation.start()
}

animation.start()animation.stop() 控制动画的开始以及关闭

1.3 源码分析

动画开始是由animation.start()开始的

/*
 * 动画入口
 * AnimationDrawable 的方法
 */
public void start() {
  mAnimating = true;

  if (!isRunning()) {
    // 从第一帧开始
    setFrame(0, false, mAnimationState.getChildCount() > 1
             || !mAnimationState.mOneShot);
    // --> 分析1
  }
}

/*
 * 分析1
 * AnimationDrawable 的方法
 */
private void setFrame(int frame, boolean unschedule, boolean animate) {
  if (frame >= mAnimationState.getChildCount()) {
    return;
  }
  mAnimating = animate;
  mCurFrame = frame;
  // 设置当前帧 Drawable
  selectDrawable(frame);
  if (unschedule || animate) {
    // 触发 Callback 来停止更新
    unscheduleSelf(this);
  }
  if (animate) {
    // Unscheduling may have clobbered these values; restore them
    mCurFrame = frame;
    mRunning = true;
    // 触发 Callback 来开始更新
    scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
    // --> 分析2
  }
}

/*
 * 分析2
 * Drawable 的方法
 */
public void scheduleSelf(@NonNull Runnable what, long when) {
  // 获取 callback, callback 来自于 View.setBackground(Drawable background)
  final Callback callback = getCallback();
  if (callback != null) {
    callback.scheduleDrawable(this, what, when);
    // --> 分析3
  }
}

/*
 * 分析3
 * View 的方法
 */
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
  if (verifyDrawable(who) && what != null) {
    final long delay = when - SystemClock.uptimeMillis();
    if (mAttachInfo != null) {
      // Choreographer 编排器负责协调动画、输入和绘制的时间。从显示子系统接收定时脉冲(如垂直同步),然后安    					排呈现下一个显示帧的一部分工作。
      // postCallbackDelayed 提交一个回调函数,在指定的延迟之后运行下一帧。回调函数执行一次就会被自动移			  // 除。
      mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
        Choreographer.CALLBACK_ANIMATION, what, who,
        Choreographer.subtractFrameDelay(delay));
      // --> 分析4
    } else {
      // Postpone the runnable until we know
      // on which thread it needs to run.
      getRunQueue().postDelayed(what, delay);
    }
  }
}

/*
 * 分析4
 * Choreographer 的方法
 */
public void postCallbackDelayed(int callbackType,
                                Runnable action, Object token, long delayMillis) {
  if (action == null) {
    throw new IllegalArgumentException("action must not be null");
  }
  if (callbackType < 0 || callbackType > CALLBACK_LAST) {
    throw new IllegalArgumentException("callbackType is invalid");
  }

  postCallbackDelayedInternal(callbackType, action, token, delayMillis);
  // -->分析5
}

/*
 * 分析5
 * Choreographer 的方法
 */
private void postCallbackDelayedInternal(int callbackType,
                                         Object action, Object token, long delayMillis) {
  ... 
  synchronized (mLock) {
    final long now = SystemClock.uptimeMillis();
    final long dueTime = now + delayMillis;
    mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

    // 判断是否仍需要延迟执行
    if (dueTime <= now) {
      scheduleFrameLocked(now);
      // --> 分析8
    } else {
      Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
      msg.arg1 = callbackType;
      msg.setAsynchronous(true);
      mHandler.sendMessageAtTime(msg, dueTime);
      // --> 分析6
    }
  }
}

/*
 * 分析6
 * Choreographer 的内部Handler
 */
private final class FrameHandler extends Handler {
  public FrameHandler(Looper looper) {
    super(looper);
  }

  @Override
  public void handleMessage(Message msg) {
    switch (msg.what) {
      case MSG_DO_FRAME:
        doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());
        break;
      case MSG_DO_SCHEDULE_VSYNC:
        doScheduleVsync();
        break;
      // 此处对 分析6 中所传递的消息进行处理
      case MSG_DO_SCHEDULE_CALLBACK:
        doScheduleCallback(msg.arg1);
        // --> 分析7
        break;
    }
  }
}

/*
 * 分析7
 * Choreographer 的内部Handler
 */
void doScheduleCallback(int callbackType) {
  synchronized (mLock) {
    if (!mFrameScheduled) {
      final long now = SystemClock.uptimeMillis();
      if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
        // 可以看到此处仍调用了 分析8的方法
        scheduleFrameLocked(now);
      }
    }
  }
}

/*
 * 分析8
 * Choreographer 的方法
 */
private void scheduleFrameLocked(long now) {
  if (!mFrameScheduled) {
    mFrameScheduled = true;
    if (USE_VSYNC) {
      if (DEBUG_FRAMES) {
        Log.d(TAG, "Scheduling next frame on vsync.");
      }

      // 如果在Looper线程上运行,则立即调度垂直同步,否则尽快从UI线程发布消息调度垂直同步。
      if (isRunningOnLooperThreadLocked()) {
        scheduleVsyncLocked();
      } else {
        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtFrontOfQueue(msg);
      }
    } else {
      final long nextFrameTime = Math.max(
        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
      if (DEBUG_FRAMES) {
        Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
      }
      Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
      msg.setAsynchronous(true);
      mHandler.sendMessageAtTime(msg, nextFrameTime);
    }
  }
}

// 接下来在一系列消息传递以及方法回调后,最终将调用至 AnimationDrawable 的 run 方法
public void run() {
  nextFrame(false);
}

private void nextFrame(boolean unschedule) {
  int nextFrame = mCurFrame + 1;
  final int numFrames = mAnimationState.getChildCount();
  final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);

  // 多次触发的动画在必要的时候循环。
  if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
    nextFrame = 0;
  }

  // 开始循环调用 setFrame 方法
  setFrame(nextFrame, unschedule, !isLastFrame);
}

2. 补间动画

2.1 在XML文件中使用:

文件位置:res/anim/filename.xml

资源引用:

  • 在 Kotlin / Java 中:R.anim.filename
  • 在 XML 中:@[package:]anim/filename
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"] >
    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float" />
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float" />
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float" />
    <set>
        ...
    </set>
</set>

该文件必须具有一个根元素,可以是 <alpha><scale><translate><rotate> 或包含一组(或多组)其他动画元素(包括嵌套的 <set> 元素)的 <set> 元素。

元素标签:

  • <set>

    容纳其他动画元素(<alpha><scale><translate><rotate>)或其他 <set> 元素的容器。代表 AnimationSet

    • android:interpolator插值器资源,要应用于动画的 Interpolator。该值是对指定插值器的资源的引用。
    • android:shareInterpolator布尔值。如果为“true”则表示要在所有子元素中共用同一插值器。
  • <alpha>

    淡入或淡出动画。代表 AlphaAnimation

    • android:fromAlpha浮点数。起始不透明度偏移,0.0 表示透明,1.0 表示不透明。
    • android:toAlpha浮点数。结束不透明度偏移。
  • <scale>

    大小调整动画。您可以通过指定 pivotXpivotY,来指定图片向外(或向内)扩展的中心点。例如,如果这两个值为 0、0(左上角),则所有扩展均向右下方向进行。代表 ScaleAnimation

    • android:fromXScale浮点数。起始 X 尺寸偏移,其中 1.0 表示不变。
    • android:toXScale浮点数。结束 X 尺寸偏移。
    • android:fromYScale浮点数。起始 Y 尺寸偏移。
    • android:toYScale浮点数。结束 Y 尺寸偏移。
    • android:pivotX浮点数。在对象缩放时要保持不变的 X 坐标。
    • android:pivotY浮点数。在对象缩放时要保持不变的 Y 坐标。
  • <translate>

    竖直和/或水平移动。支持采用以下三种格式之一的以下属性:从 -100 到 100 的以“%”结尾的值,表示相对于自身的百分比;从 -100 到 100 的以“%p”结尾的值,表示相对于其父项的百分比;不带后缀的浮点值,表示绝对值。代表 TranslateAnimation

    • android:fromXDelta浮点数或百分比。起始 X 偏移。表示方式:相对于正常位置的像素数(例如 "5"),相对于元素宽度的百分比(例如 "5%"),或相对于父项宽度的百分比(例如 "5%p")。
    • android:toXDelta浮点数或百分比。结束 X 偏移。
    • android:fromYDelta浮点数或百分比。起始 Y 偏移。
    • android:toYDelta浮点数或百分比。结束 Y 偏移。
  • <rotate>

    旋转动画。代表 RotateAnimation

    • android:fromDegrees浮点数。起始角度位置,以度为单位。
    • android:toDegrees浮点数。结束角度位置,以度为单位。
    • android:pivotX浮点数或百分比。旋转中心的 X 坐标。表示方式:相对于对象左边缘的像素数(例如 "5"),相对于对象左边缘的百分比(例如 "5%"),或相对于父级容器左边缘的百分比(例如 "5%p")。
    • android:pivotY浮点数或百分比。旋转中心的 Y 坐标。

2.2 代码调用

val image: ImageView = findViewById(R.id.image)
val animation: Animation = AnimationUtils.loadAnimation(this, R.anim.animation)
image.startAnimation(animation)

2.3 源码分析

// 利用配置文件生成动画对象
val animation: Animation = AnimationUtils.loadAnimation(this, R.anim.animation)
// 启动动画
image.startAnimation(animation)
// 生成动画对象的入口
public static Animation loadAnimation(Context context, @AnimRes int id)
  throws NotFoundException {

  XmlResourceParser parser = null;
  try {
    // 获取资源中对应的动画
    parser = context.getResources().getAnimation(id);
    //
    return createAnimationFromXml(context, parser);
  } 
  ...
}

private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
  throws XmlPullParserException, IOException {

  // --> 分析1
  return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
}

/*
 * 分析1
 * AnimationUtils 的方法
 */
private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
                                                AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {

  Animation anim = null;
  int type;
  int depth = parser.getDepth();

  // 利用 XmlPullParser 对配置文件进行解析并最终返回动画对象
  while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
         && type != XmlPullParser.END_DOCUMENT) {

    if (type != XmlPullParser.START_TAG) {
      continue;
    }
    String  name = parser.getName();
    if (name.equals("set")) {
      anim = new AnimationSet(c, attrs);
      createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
    } else if (name.equals("alpha")) {
      anim = new AlphaAnimation(c, attrs);
    } else if (name.equals("scale")) {
      anim = new ScaleAnimation(c, attrs);
    }  else if (name.equals("rotate")) {
      anim = new RotateAnimation(c, attrs);
    }  else if (name.equals("translate")) {
      anim = new TranslateAnimation(c, attrs);
    } else if (name.equals("cliprect")) {
      anim = new ClipRectAnimation(c, attrs);
    } else {
      throw new RuntimeException("Unknown animation name: " + parser.getName());
    }
    if (parent != null) {
      parent.addAnimation(anim);
    }
  }
  return anim;
}
// 启动动画的入口 View 的方法
public void startAnimation(Animation animation) {
  // 当start time设置为START_ON_FIRST_FRAME时
  //	动画将在第一次调用getTransformation(long, Transformation)时开始
  animation.setStartTime(Animation.START_ON_FIRST_FRAME);
  
  setAnimation(animation);
  // --> 分析2
  
  // 标记此视图的父视图应清除其缓存,用于强制父视图重建其显示列表
  invalidateParentCaches();
  
  // 使绘图缓存失效重新绘制
  invalidate(true);
}

/*
 * 分析2
 * Animation 的方法
 */
public void setAnimation(Animation animation) {
  mCurrentAnimation = animation;

  if (animation != null) {
    // 如果屏幕是关闭的,仍将动画的开始时间设置为现在而不是我们绘制的下一帧。    
    // 若开始时间保持为START_ON_FIRST_FRAME 将导致动画在屏幕重新打开时开始
    if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
        && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
      animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
    }
    animation.reset();
  }
}

3. 属性动画

3.1 在XML中使用

文件位置:res/animator/filename.xml

资源引用:

  • 在 Kotlin / Java 代码中:R.animator.filename
  • 在 XML 中:@[package:]animator/filename
<set
  android:ordering=["together" | "sequentially"]>
    <objectAnimator
        android:propertyName="string"
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["restart" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <animator
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["restart" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <set>
        ...
    </set>
</set>

该文件必须具有一个根元素,可以是 <set><objectAnimator><valueAnimator>。可以将动画元素(包括其他 <set> 元素)组合到 <set> 元素中。

元素:

  • <set>

    容纳其他动画元素(<objectAnimator><valueAnimator> 或其他 <set> 元素)的容器。代表 AnimatorSet。可以指定嵌套的 <set> 标记来将动画进一步组合在一起。每个 <set> 都可以定义自己的 ordering 属性。

    • android:ordering关键字。指定集合中动画的播放顺序。
说明
sequentially依序播放此集合中的动画
together(默认)同时播放此集合中的动画。
  • <objectAnimator>

    在特定的一段时间内为对象的特定属性创建动画。代表 ObjectAnimator

    • android:propertyName字符串。必需。通过其名称引用来添加动画效果的对象属性。例如为 View 对象指定 "alpha""backgroundColor"。但是,objectAnimator 元素不包含 target 属性,因此无法在 XML 声明中设置要添加动画效果的对象。必须通过调用 loadAnimator() 来扩充动画资源,然后调用 setTarget() 来设置包含此属性的目标。
    • android:valueTo浮点数、整数或颜色。必需。动画属性的结束值。颜色以六位十六进制数字表示(例如,#333333)。
    • android:valueFrom浮点数、整数或颜色。动画属性的起始值。如果未指定,则动画将从属性的 get 方法获得的值开始。
    • android:duration整数。动画的时间,以毫秒为单位。默认为 300 毫秒。
    • android:startOffset整数。调用 start() 后动画延迟的毫秒数。
    • android:repeatCount整数。动画的重复次数。设为 "-1" 表示无限次重复。值 "1" 表示动画在初次播放后重复播放一次。默认值为 "0",不重复。
    • android:repeatMode整数。动画播放到结尾处的行为。android:repeatCount 必须设置为正整数或 "-1",该属性才有效。设置为 "reverse" 可让动画在每次迭代时反向播放,设置为 "restart" 则可让动画每次从头开始循环播放。
  • <animator>

    在指定的时间段内执行动画。 代表 ValueAnimator

    • 其属性与<objectAnimator>类似。

3.2 代码调用

val set: AnimatorSet = AnimatorInflater.loadAnimator(context, R.animator.animator)
.apply {
  setTarget(mObject)
  start()
}

3.3 源码分析

val set: AnimatorSet = AnimatorInflater.loadAnimator(context, R.animator.animator)
.apply {
  // 设置动画的目标对象
  setTarget(mObject)
  // 启动动画
  start()
}
// 设置动画目标对象
public void setTarget(@Nullable Object target) {
  final Object oldTarget = getTarget();
  // 若不是原对象则取消动画并重新配置
  if (oldTarget != target) {
    if (isStarted()) {
      cancel();
    }
    mTarget = target == null ? null : new WeakReference<Object>(target);
    // New target should cause re-initialization prior to starting
    mInitialized = false;
  }
}

// 启动动画
public void start() {
  start(false, true);
  // --> 分析1
}

/*
 * 分析1
 * ObjectAnimator 的方法
 */
private void start(boolean inReverse, boolean selfPulse) {
  if (Looper.myLooper() == null) {
    throw new AndroidRuntimeException("Animators may only be run on Looper threads");
  }
  ...
  // 初始化动画
  initAnimation();
  // --> 分析2
  ...
  boolean isEmptySet = isEmptySet(this);
  if (!isEmptySet) {
    // 启动方法
    startAnimation();
  }

  if (mListeners != null) {
    ArrayList<AnimatorListener> tmpListeners =
      (ArrayList<AnimatorListener>) mListeners.clone();
    int numListeners = tmpListeners.size();
    for (int i = 0; i < numListeners; ++i) {
      tmpListeners.get(i).onAnimationStart(this, inReverse);
    }
  }
  if (isEmptySet) {
    // 在AnimatorSet为空或者持续时间为0的情况下,将立即触发onAnimationEnd()
    end();
  }
}

/*
 * 分析2
 * ObjectAnimator 的方法
 */
private void initAnimation() {
  if (mInterpolator != null) {
    for (int i = 0; i < mNodes.size(); i++) {
      Node node = mNodes.get(i);
      // 设置差值器
      node.mAnimation.setInterpolator(mInterpolator);
    }
  }
  // 更新动画持续时间
  updateAnimatorsDuration();
  //创建依赖视图
  createDependencyGraph();
}

4. Lottie动画

4.1简介

lottie是一个使用极其简单的开源动画库,支持Android,iOS,React Native 以及 windows。

其存在以下几个优势:

  • 与帧动画使用图片相比,Lottie使用JSON文件可以有效减少APP 打包之后的体积
  • 可以通过URL加载动画,从而使得程序与动画解耦并且可以不用更新APP就可以更新动画
  • 可以通过代码设置使得动画响应交互,并且可以控制动画的前进以及后退

4.2 使用

使用lottie十分简单,我们只需要在项目的build.gradle中引入如下依赖

dependencies {
    ...
    implementation "com.airbnb.android:lottie:$lottieVersion"
    ...
}

接下来我们既可以通过布局文件引入,也可以在代码中动态加载。

  • 首先我们需要将json文件放入res/raw [lottie_rawRes]或者 assets/[lottie_fileName]

  • 若要在布局文件中引入:

<com.airbnb.lottie.LottieAnimationView
        android:id="@+id/animation_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

        app:lottie_rawRes="@raw/hello_world"
        // 或者
        app:lottie_fileName="hello_world.json"

        // 无限循环
        app:lottie_loop="true"
        // 加载后自动播放
        app:lottie_autoPlay="true" />
  • 在代码中动态加载
val animationView = LottieAnimationView(context).apply {
  setAnimation(R.raw.hello_world)
  // 播放结束后下次循环依旧从头开始
  repeatMode = LottieDrawable.RESTART
  // 无限循环
  repeatCount = LottieDrawable.INFINITE
}
animationView.playAnimation()
  • 我们可以通过代码动态控制动画只播放一部分
// 动画从15帧播放至49帧
animationView.setMinFrame(15)
animationView.setMaxFrame(49)

// 动画从20%进度播放至80%进度
animationView.setMinProgress(0.2)
animationView.setMaxProgress(0.8)
  • 设置监听器来处理交互
animationView.addAnimatorUpdateListener{
  // TODO
}

animationView.addAnimatorPauseListener{
  // TODO
}

// 将动画设置到40%进度的帧
animationView.setProgress(0.4)

这些就是lottie动画的简单使用,更为详细的介绍可以参考官方文档:http://airbnb.io/lottie/#/android

5. Demo

代码如下:

class MainActivity : AppCompatActivity(), OnClickListener {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.frameAnimation.setOnClickListener(this)
        binding.tweenAnimation.setOnClickListener(this)
        binding.propertyAnimation.setOnClickListener(this)
    }

    override fun onClick(view: View) {
        when (view.id) {
            R.id.frameAnimation -> {
                binding.layout.removeAllViews()
                val textView = TextView(this)
                textView.setBackgroundResource(R.drawable.pikachu)
                val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT
                    , LinearLayout.LayoutParams.MATCH_PARENT)
                binding.layout.addView(textView, layoutParams)
                val animation = textView.background
                if (animation is Animatable) {
                    animation.start()
                }
            }

            R.id.tweenAnimation -> {
                val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT
                    , LinearLayout.LayoutParams.WRAP_CONTENT)
                binding.layout.removeAllViews()
                binding.layout.addView(Button(this).apply {
                    "translate".also { text = it }
                    setOnClickListener {
                        it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.translate))
                    }
                }, layoutParams)
                binding.layout.addView(Button(this).apply {
                    "alpha".also { text = it }
                    setOnClickListener {
                        it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.alpha))
                    }
                }, layoutParams)
                binding.layout.addView(Button(this).apply {
                    "rotate".also { text = it }
                    setOnClickListener {
                        it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.rotate))
                    }
                }, layoutParams)
                binding.layout.addView(Button(this).apply {
                    "scale".also { text = it }
                    setOnClickListener {
                        it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.scale))
                    }
                }, layoutParams)
            }
            
            R.id.propertyAnimation -> {
                binding.layout.removeAllViews()
                val button = Button(this).apply { text = context.getString(R.string.property_animation_text) }
                binding.layout.addView(button, LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT
                    , LinearLayout.LayoutParams.WRAP_CONTENT))
                button.setOnClickListener{
                    AnimatorSet().apply {
                        playTogether(ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)
                            , ObjectAnimator.ofFloat(button, "translationY", 0f, 450f))
                        interpolator = BounceInterpolator()
                        duration = 800
                        start()
                    }
                }
            }
        }
    }
}

布局很简单:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
        tools:context=".MainActivity">

    <Button
            android:id="@+id/frameAnimation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
            android:textAllCaps="false"
            android:text="@string/frame_animation" />
    <Button
            android:id="@+id/tweenAnimation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
            android:textAllCaps="false"
            android:text="@string/tween_animation" />
    <Button
            android:id="@+id/propertyAnimation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
            android:textAllCaps="false"
            android:text="@string/property_animation" />

    <LinearLayout
            android:id="@+id/layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal" />

</LinearLayout>

补间动画设置如下:

<set xmlns:android="http://schemas.android.com/apk/res/android"
        android:shareInterpolator="false">
    <translate
            android:toYDelta="400"
            android:duration="2000" />
</set>

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
            android:fromAlpha="1"
            android:toAlpha="0.1"
            android:duration="2000" />
</set>

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate
            android:fromDegrees="0"
            android:toDegrees="90"
            android:pivotX="50%"
            android:pivotY="50%"
            android:duration="2000" />
</set>

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
            android:fromXScale="1.0"
            android:toXScale="2.0"
            android:fromYScale="1.0"
            android:toYScale="2.0"
            android:pivotX="0"
            android:pivotY="0"
            android:duration="2000" />
</set>

Demo演示效果如下:

在这里插入图片描述

6. 差值器与估值器

6.1 差值器(Interpolator)

在补间动画以及属性动画中,我们都是用到了差值器。比如在Demo中:

AnimatorSet().apply {
  playTogether(ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)
               , ObjectAnimator.ofFloat(button, "translationY", 0f, 450f))
  // 设置差值器为 BounceInterpolator
  interpolator = BounceInterpolator()
  duration = 800
  start()
}

差值器是一个设置属性从初始值到结束值变化规律的接口

我们既可以在XML文件中通过android:interpolator引入,也可以在代码中通过setInterpolator动态设置。

系统已经为我们内置了9种差值器,基本可以满足大部分使用场景。

类/接口说明
AccelerateDecelerateInterpolator该插值器的变化率在开始和结束时缓慢但在中间会加快。
AccelerateInterpolator该插值器的变化率在开始时较为缓慢,然后会加快。
AnticipateInterpolator该插值器先反向变化,然后再急速正向变化。
AnticipateOvershootInterpolator该插值器先反向变化,再急速正向变化,然后超过定位值,最后返回到最终值。
BounceInterpolator该插值器的变化会跳过结尾处。
CycleInterpolator该插值器的动画会在指定数量的周期内重复。
DecelerateInterpolator该插值器的变化率开始很快,然后减速。
LinearInterpolator该插值器的变化率恒定不变。
OvershootInterpolator该插值器会急速正向变化,再超出最终值,然后返回。
TimeInterpolator该接口用于实现您自己的插值器。

差值器中的 override fun getInterpolation(input: Float): Float 方法的输入参数是动画运行的进度百分比,输出参数为属性的变化百分比。

我们给出几个不同的内置差值器的方法实现

  • AccelerateDecelerateInterpolator 【先加速后减速】
public class AccelerateDecelerateInterpolator extends BaseInterpolator
  implements NativeInterpolator {
  ...
  public float getInterpolation(float input) {
    return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
  }
  ...
}
  • LinearInterpolator 【线性】
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolator {
	...
  public float getInterpolation(float input) {
    return input;
  }
  ...
}
  • BounceInterpolator 【回弹】
public class BounceInterpolator extends BaseInterpolator implements NativeInterpolator {
  ...
  private static float bounce(float t) {
    return t * t * 8.0f;
  }

  public float getInterpolation(float t) {
    t *= 1.1226f;
    if (t < 0.3535f) return bounce(t);
    else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
    else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
    else return bounce(t - 1.0435f) + 0.95f;
  }
	...
}

根据getInterpolation方法我们可以做出getInterpolation-input的关系图,如下:

在这里插入图片描述

  • LinearInterpolator只是简单地将输入输出
  • AccelerateDecelerateInterpolator通过平移以及松弛余弦图像得到了斜率先增加后降低并且值域在[0,1]的图像
  • BounceInterpolator通过分段函数得到了会在结尾处进行衰减式回弹

通过以上分析,我们可以知道如果这九种内置插值器无法满足需要,我们只需要实现TimeInterpolator接口并重写getInterpolation方法即可。

6.2 估值器(TypeEvaluator)

估值器是一个设置属性从 初始值 变化到 结束值 具体数值的接口

假设我们使用线性插值器并且设置的属性是从20变化到100,那么当动画运行到一半时,插值器返回值为0.5;而估值器返回值为20 + (100 - 20) * 0.5 = 60

系统为我们内置了7种估值器

说明
ArgbEvaluator这个估值器可用于在表示ARGB颜色的整数值之间进行类型插值
FloatArrayEvaluator这个估值器可用于在float[]之间进行类型插值
FloatEvaluator这个估值器可用于在float之间进行类型插值
IntArrayEvaluator这个估值器可用于在int[]之间进行类型插值
IntEvaluator这个估值器可用于在int之间进行类型插值
PointFEvaluator这个估值器可用于在PointF之间进行类型插值
RectEvaluator这个估值器可用于在Rect之间进行类型插值

估值器中的 Integer evaluate(float fraction, Integer startValue, Integer endValue) 方法的输入参数是属性变化的百分比,属性的开始值以及属性的结束值,输出参数为属性应设置的当前值。

我们给出几个不同的内置估值器的方法实现

  • IntEvaluator【整型】
public class IntEvaluator implements TypeEvaluator<Integer> {
  public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
    int startInt = startValue;
    return (int)(startInt + fraction * (endValue - startInt));
  }
}
  • FloatArrayEvaluator【浮点数组】
public class FloatArrayEvaluator implements TypeEvaluator<float[]> {

  private float[] mArray;

 
  public FloatArrayEvaluator() {
  }

  public FloatArrayEvaluator(float[] reuseArray) {
    mArray = reuseArray;
  }

  @Override
  public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
    float[] array = mArray;
    if (array == null) {
      array = new float[startValue.length];
    }

    for (int i = 0; i < array.length; i++) {
      float start = startValue[i];
      float end = endValue[i];
      array[i] = start + (fraction * (end - start));
    }
    return array;
  }
}

可以总结出当前动画的值就是用结束值与初始值的差值乘以fraction系数,再加上初始值

如同demo中的ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)底层就是使用了FloatEvaluator对translationX的属性值进行计算

而如果我们用到了ValueAnimator.ofObject()方法就需要使用到自定义估值器,我们只需要实现TypeEvaluator接口并重写evaluate方法即可

相关文章

  1. 【Android】自定义View / ViewGroup
  2. 【Android】Handler机制详解
  3. 【Android】事件分发详解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小黄才不管那么多

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

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

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

打赏作者

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

抵扣说明:

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

余额充值