Android动画合集之属性动画-又见

Android 动画合集之属性动画 – 又见

属性动画(Property Animation)是 Android 3.0(API 11)引入的强大动画机制,允许开发者通过动态修改对象的属性值(如 View 的位置、透明度、自定义对象的字段等)实现平滑动画效果。前文已介绍属性动画的基础概念、核心类(如 ValueAnimatorObjectAnimatorAnimatorSetViewPropertyAnimator)和基本用法(《属性动画 – 初见》)。作为 Android 动画合集的延续,本文(《属性动画 – 又见》)将深入探讨属性动画的高级用法,包括多属性动画、自定义插值器(Interpolator)、类型评估器(TypeEvaluator)、关键帧(Keyframes)、动画监听器、以及与 Canvas 和 Paint 的结合,为开发者提供更复杂的动画实现方案。帧动画和补间动画已在前文覆盖,后续可探讨过渡动画或 Lottie。

属性动画的高级特性

属性动画的灵活性使其适用于复杂场景,以下是高级特性的核心点:

  • 多属性动画:通过 PropertyValuesHolderAnimatorSet 实现多个属性同时或顺序变化。
  • 自定义插值器:控制动画的时间-值曲线,实现非标准动画效果(如弹簧、抛物线)。
  • 类型评估器:支持非数值类型的动画(如颜色、点坐标)。
  • 关键帧:定义动画中的特定时间点和值,创建非线性动画路径。
  • 动画监听器:监控动画状态(如开始、结束、取消),实现交互逻辑。
  • 结合 Canvas:在自定义 View 中结合属性动画和 Canvas 绘制,实现动态图形效果。

1. 多属性动画

多属性动画允许同时对对象的多个属性(如透明度、平移、旋转)进行动画操作,主要通过 PropertyValuesHolderAnimatorSet 实现。

使用 PropertyValuesHolder

PropertyValuesHolder 用于在单个 ObjectAnimator 中控制多个属性动画。

  • 代码示例
  import android.animation.ObjectAnimator;
  import android.animation.PropertyValuesHolder;
  import android.os.Bundle;
  import android.widget.ImageView;
  import androidx.appcompat.app.AppCompatActivity;

  public class MultiPropertyAnimationActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_property_animation);

          ImageView imageView = findViewById(R.id.image_view);

          // 定义多个属性
          PropertyValuesHolder alphaHolder = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);
          PropertyValuesHolder translateXHolder = PropertyValuesHolder.ofFloat("translationX", 0f, 200f);
          PropertyValuesHolder rotateHolder = PropertyValuesHolder.ofFloat("rotation", 0f, 360f);

          // 创建 ObjectAnimator
          ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(imageView, alphaHolder, translateXHolder, rotateHolder);
          animator.setDuration(1000);

          // 按钮触发
          findViewById(R.id.start_button).setOnClickListener(v -> animator.start());
      }
  }
  • 说明
  • ofPropertyValuesHolder 将多个 PropertyValuesHolder 组合,同步执行。
  • 每个属性独立设置值范围(如 alpha 从 0 到 1,translationX 从 0 到 200)。
  • 比多个 ObjectAnimator 更高效,减少对象创建。
使用 AnimatorSet

AnimatorSet 允许控制动画的播放顺序(如顺序、同时、延迟)。

  • 代码示例
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.os.Bundle;
  import android.widget.ImageView;
  import androidx.appcompat.app.AppCompatActivity;

  public class AnimatorSetActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_property_animation);

          ImageView imageView = findViewById(R.id.image_view);

          // 创建动画
          ObjectAnimator alpha = ObjectAnimator.ofFloat(imageView, "alpha", 0f, 1f);
          ObjectAnimator translate = ObjectAnimator.ofFloat(imageView, "translationX", 0f, 200f);
          ObjectAnimator rotate = ObjectAnimator.ofFloat(imageView, "rotation", 0f, 360f);

          // 组合动画:alpha 和 translate 同时,rotate 在后
          AnimatorSet set = new AnimatorSet();
          set.play(alpha).with(translate).before(rotate);
          set.setDuration(1000);

          // 按钮触发
          findViewById(R.id.start_button).setOnClickListener(v -> set.start());
      }
  }
  • 说明
  • play().with():alpha 和 translate 同时播放。
  • play().before():rotate 在前两者完成后播放。
  • 可通过 setStartDelay(long) 添加延迟。

2. 自定义插值器

插值器(Interpolator)控制动画的时间-值曲线,决定动画的节奏。Android 提供内置插值器(如 LinearInterpolatorOvershootInterpolator),但复杂动画可能需要自定义插值器。

  • 实现自定义插值器
  import android.view.animation.Interpolator;

  public class BounceInterpolator implements Interpolator {
      @Override
      public float getInterpolation(float input) {
          // 模拟弹跳效果:input 是时间进度 (0-1)
          return (float) (1 - Math.pow(1 - input, 2)); // 抛物线弹跳
      }
  }
  • 使用示例
  ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "translationY", 0f, 200f);
  animator.setInterpolator(new BounceInterpolator());
  animator.setDuration(1000);
  animator.start();
  • 说明
  • getInterpolation(float input):输入时间进度 (0-1),输出值进度 (0-1)。
  • 可实现弹簧、抛物线、随机抖动等效果。
  • API 21+ 推荐实现 TimeInterpolator 接口。

3. 自定义 TypeEvaluator

TypeEvaluator 定义如何在动画值之间插值,支持非数值类型(如颜色、点、自定义对象)。

  • 实现自定义 TypeEvaluator(颜色渐变):
  import android.animation.TypeEvaluator;
  import android.graphics.Color;

  public class ColorEvaluator implements TypeEvaluator<Integer> {
      @Override
      public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
          int startR = Color.red(startValue), startG = Color.green(startValue), startB = Color.blue(startValue);
          int endR = Color.red(endValue), endG = Color.green(endValue), endB = Color.blue(endValue);
          int r = (int) (startR + fraction * (endR - startR));
          int g = (int) (startG + fraction * (endG - startG));
          int b = (int) (startB + fraction * (endB - startB));
          return Color.rgb(r, g, b);
      }
  }
  • 使用示例
  ValueAnimator animator = ValueAnimator.ofObject(new ColorEvaluator(), Color.RED, Color.BLUE);
  animator.setDuration(1000);
  animator.addUpdateListener(animation -> {
      int color = (int) animation.getAnimatedValue();
      imageView.setBackgroundColor(color);
  });
  animator.start();
  • 说明
  • evaluate:根据 fraction(0-1)计算起始值到结束值的中间值。
  • 适用于颜色、PointF、自定义对象等复杂类型。

4. 关键帧(Keyframes)

关键帧(Keyframe)允许在动画中定义特定时间点的值,实现非线性动画路径。

  • 代码示例
  import android.animation.Keyframe;
  import android.animation.ObjectAnimator;
  import android.animation.PropertyValuesHolder;
  import android.os.Bundle;
  import android.widget.ImageView;
  import androidx.appcompat.app.AppCompatActivity;

  public class KeyframeAnimationActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_property_animation);

          ImageView imageView = findViewById(R.id.image_view);

          // 定义关键帧:0% 在 0,50% 在 300,100% 回 0
          Keyframe kf0 = Keyframe.ofFloat(0f, 0f);
          Keyframe kf1 = Keyframe.ofFloat(0.5f, 300f);
          Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
          PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("translationX", kf0, kf1, kf2);

          ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(imageView, holder);
          animator.setDuration(1000);

          findViewById(R.id.start_button).setOnClickListener(v -> animator.start());
      }
  }
  • 说明
  • Keyframe.ofFloat(fraction, value):定义时间点(fraction,0-1)和值。
  • 适合复杂路径动画(如先右移再返回)。

5. 动画监听器

AnimatorListener 监控动画状态(如开始、结束、取消),用于触发后续逻辑。

  • 代码示例
  ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "alpha", 0f, 1f);
  animator.setDuration(1000);
  animator.addListener(new Animator.AnimatorListener() {
      @Override
      public void onAnimationStart(Animator animation) {
          Log.d("Animation", "Started");
      }
      @Override
      public void onAnimationEnd(Animator animation) {
          Log.d("Animation", "Ended");
      }
      @Override
      public void onAnimationCancel(Animator animation) {
          Log.d("Animation", "Cancelled");
      }
      @Override
      public void onAnimationRepeat(Animator animation) {
          Log.d("Animation", "Repeated");
      }
  });
  animator.start();
  • 说明
  • 使用 AnimatorListenerAdapter 可只实现部分方法。
  • 常用于状态切换或触发后续动画。

6. 结合 Canvas 和 Paint

属性动画可以与 CanvasPaint 结合,在自定义 View 中实现动态绘制效果。

  • 代码示例(动态渐变矩形):
  import android.content.Context;
  import android.graphics.*;
  import android.util.AttributeSet;
  import android.view.View;
  import androidx.appcompat.app.AppCompatActivity;
  import android.animation.ValueAnimator;
  import android.os.Bundle;

  public class CanvasAnimationActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_canvas_animation);

          CustomView customView = findViewById(R.id.custom_view);
          ValueAnimator animator = ValueAnimator.ofFloat(0f, 360f);
          animator.setDuration(2000);
          animator.setRepeatCount(ValueAnimator.INFINITE);
          animator.addUpdateListener(animation -> {
              customView.setRotationAngle((float) animation.getAnimatedValue());
              customView.invalidate(); // 重绘
          });
          findViewById(R.id.start_button).setOnClickListener(v -> animator.start());
      }
  }

  class CustomView extends View {
      private Paint paint;
      private float rotationAngle = 0f;

      public CustomView(Context context, AttributeSet attrs) {
          super(context, attrs);
          paint = new Paint(Paint.ANTI_ALIAS_FLAG);
          paint.setStyle(Paint.Style.FILL);
      }

      public void setRotationAngle(float angle) {
          rotationAngle = angle;
      }

      @Override
      protected void onDraw(Canvas canvas) {
          super.onDraw(canvas);
          // 使用 Shader 实现渐变
          LinearGradient gradient = new LinearGradient(0, 0, 100, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
          paint.setShader(gradient);
          canvas.rotate(rotationAngle, getWidth() / 2f, getHeight() / 2f);
          canvas.drawRect(50, 50, 150, 150, paint);
      }
  }
  • 布局 XML(res/layout/activity_canvas_animation.xml):
  <?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:gravity="center"
      android:orientation="vertical">
      <com.example.CustomView
          android:id="@+id/custom_view"
          android:layout_width="200dp"
          android:layout_height="200dp" />
      <Button
          android:id="@+id/start_button"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="Start Animation" />
  </LinearLayout>
  • 说明
  • ValueAnimator 驱动 rotationAngle 变化。
  • Canvas.rotate 应用旋转,结合 PaintShader 实现渐变矩形。
  • invalidate() 触发重绘。

注意事项

  • 性能:复杂动画或频繁重绘可能影响性能,建议优化动画范围或使用 ViewPropertyAnimator。
  • 兼容性:属性动画需 API 11+,低版本可用 NineOldAndroids(已废弃,建议迁移到 Compose)。
  • 属性支持ObjectAnimator 要求属性有 getter/setter 方法,自定义对象需提供公开 setter。
  • 生命周期:在 Activity/Fragment 暂停时(如 onPause)调用 cancel() 释放资源。
  • 调试:使用 AnimatorListener 监控状态,Log 输出值变化或使用 Visual Inspector 检查效果。

扩展:Jetpack Compose 中的等效实现

Compose 使用声明式动画,高级功能通过 updateTransitionAnimatable 实现:

  • 示例(多属性 + 自定义插值器):
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp

@Composable
fun AdvancedAnimation() {
    var enabled by remember { mutableStateOf(false) }
    val transition = updateTransition(targetState = enabled, label = "Animation")
    val alpha by transition.animateFloat(
        transitionSpec = { tween(durationMillis = 1000, easing = FastOutSlowInEasing) },
        label = "alpha"
    ) { if (it) 1f else 0f }
    val translationX by transition.animateFloat(
        transitionSpec = { tween(durationMillis = 1000, easing = FastOutSlowInEasing) },
        label = "translationX"
    ) { if (it) 200f else 0f }
    val rotation by transition.animateFloat(
        transitionSpec = { tween(durationMillis = 1000, easing = FastOutSlowInEasing) },
        label = "rotation"
    ) { if (it) 360f else 0f }

    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = "Advanced Animation",
        modifier = Modifier
            .size(100.dp)
            .graphicsLayer(alpha = alpha, translationX = translationX, rotationZ = rotation)
            .clickable { enabled = !enabled }
    )
}
  • 说明
  • updateTransition 管理多个属性动画。
  • tween 支持自定义 easing(如 FastOutSlowInEasing)。
  • Compose 内置支持关键帧(keyframes)和自定义插值器。

示例:复杂交互动画

一个按钮点击时的弹跳效果:

Button button = findViewById(R.id.start_button);
button.setOnClickListener(v -> {
    AnimatorSet set = new AnimatorSet();
    ObjectAnimator scaleUpX = ObjectAnimator.ofFloat(v, "scaleX", 1f, 1.2f);
    ObjectAnimator scaleUpY = ObjectAnimator.ofFloat(v, "scaleY", 1f, 1.2f);
    ObjectAnimator scaleDownX = ObjectAnimator.ofFloat(v, "scaleX", 1.2f, 1f);
    ObjectAnimator scaleDownY = ObjectAnimator.ofFloat(v, "scaleY", 1.2f, 1f);
    set.play(scaleUpX).with(scaleUpY).before(scaleDownX).with(scaleDownY);
    set.setDuration(200);
    set.setInterpolator(new OvershootInterpolator());
    set.start();
});

如果需要更深入的内容(如复杂关键帧、自定义 TypeEvaluator 的高级用法、与 Lottie 对比、Canvas 复杂动画),或继续探讨 Android 动画合集的其他部分(如过渡动画、Activity 切换动画),请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注