Bootstrap

Android Material Design 之 有意义动效

简介

在现实世界中,物体不会凭空出现或凭空消失,它们各归其位,瞬时的出现往往显得很突兀使人产生困扰。就好像我们学习了如何运用物质世界的经验在我们的UI界面上创建有形平面,这么做可以帮助我们传递有关它们的信息。同理运用现实世界的运动原理,也能够使我们的界面更容易让人理解。想想我们在共享平台上的用户界面,元素以有序的方式进入或者移除平台。
这里将介绍如何通过运动来创建更连贯的用户体验,如何引导用户使用我们的UI,以及如何向用户提供使用的乐趣。

Android的动画
补间动画
  • 在最初的Android版本中,即Android 3.0 之前,Android SDK 提供的动画是通过andorid.view.animation中的API来实现的,通过这些API我们可以实现简单的平移,缩放,旋转,淡入淡出的效果,这些动画也被称为Tween Animation 补间动画。

    • Java 代码如下:

      public void animateView(View view){
          Animation alphaAnimation = new AlphaAnimation(0.1, 1.0);
          Animation translateAnimation = new TranslateAnimation(-50f,0f,-50f,0f);
          Animatino scaleAnimation = new ScaleAnimation(.5f, 1.0f, .5f, 1.0f);
          Animatino rotateAnimatin = new RotateAnimatino(-90f, 90f, 0f, 0f);
          // 使用单个动画
          alphaAnimatino.setDuration(1000);
          view.startAnimation(alphaAnimatino);
          // 使用多个动画,需要用到AnimationSet
          AnimationSet animSet = new AnimationSet(false);
          animSet.add(alphaAnimatino);
          animSet.add(translateAnimation);
          animSet.add(scaleAnimation);
          animSet.add(rotateAnimation);
          animSet.setDuration(1000);
          animSet.setAnimationListener(new Animation.AnimationListener(){
              @Override
              public void onAnimationEnd(Animation animation){
                  // TODO
              }
      
              @Override
              public void onAnimationStart(Animation animation){ 
                  // TODO 
              }
      
              @Override
              public void onAnimationRepeat(Animation animatoin){
                  // TODO
              }
          });
          view.startAnimation(animSet);
      }
    • 从xml中定义动画

      <!-- 定义单个动画 location: res/anim/alpha_anim.xml-->
      <alpha android:xmlns="http://schemas.android.com/res/apk/android"
          android:fromAlpha="0.1"
          android:toAlpha="1.0"
          android:duration="1000">
      </alpha>
      <!-- 定义单个动画 location: res/anim/scale_anim.xml-->
      <scale android:xmlns="http://schemas.android.com/res/apk/android"
          android:fromXScale="0.5"
          android:toXScale="1.0"
          android:fromYScale="0.5"
          android:toYscale="1.0"
          android:pivotX="0"
          android:pivotY="1"
          android:duration="1000">
      </scale>
      <!--定义一个动画集 location:   res/anim/my_anim_set.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <set xmlns:android="http://schemas.android.com/apk/res/android">
        <alpha android:fromAlpha="0" android:toAlpha="1" android:duration="2000"/>
        <rotate android:pivotX="0" android:pivotY="0" android:fromDegrees="0" android:toDegrees="360" android:duration="1000"/>
      </set>
    • 代码中加载xml中定义的动画,主要使用到AnimationUtils 类

       public void animView(View view){
              ...
              ...
              // 使用单个的动画
              Animation alphaAnim = AnimationUtils.loadAnimation(this, R.anim.alpha_anim);
              Animatino scaleAnim = AnimationUtils.loadAnimation(this, R.anim.scale_anim);
              view.startAnimation(alphaAnim);
      
              // 使用动画集
              Animation animSet = AnimatinoUtils.loadAnimatino(this, R.anim.my_anim_set);
              view.startAnimation(animSet);
       }
    • AnimationSet 和 Animation 注意点

      1. 对animationSet的属性进行设置的时候,有些设置也会应用到子动画的属性中,有些会被animationSet忽略,有些只能用于animationSet,具体哪些属性如下表:

        属性名称描述
        duration,
        repeatMode,
        fillBefore,
        fillAfter
        当这些属性应用到AnimationSet时,同时也会应用(覆盖)到它的所有的Animation的相应的属性
        repeatCount,
        fillEnable
        这些属性会被AnimationSet给忽略掉
      2. 在4.0以后,代码中动态设置这些属性或者和在XML中声明这些属性的效果都是一样的,例如setDuration(500)和xml中声明android:duration=”500”是一样的效果,但是在4.0之前,在XML中对AnimationSet设置的这些属性是会被忽略的,只有setDuration(500)才有用。

      3. 补间动画只是改变了view的绘制(显示效果),并没有改变view的其他属性,比如将一个button想右平移一段距离后,我们再去点击这个button,发现点击这个button没有任何反应了,但我们点击button以前的那个位置就执行了button的点击事件。
帧动画
  • 在Android 3.0 之前还有一种动画就是Frame Animation 帧动画。即一帧一帧的执行动画,

    • 创建drawable/frame_anim.xml文件,主要使用到animation-list标签

       <?xml version="1.0" encoding="utf-8"?>
       <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
           android:oneshot="true">
          <item android:duration="500" android:drawable="@drawable/frame_one"/>
          <item android:duration="500" android:drawable="@drawable/frame_two"/>
          <item android:duration="500" android:drawable="@drawable/frame_three"/>
          <item android:duration="500" android:drawable="@drawable/frame_four"/>
       <animatino-list/>
    • layout中使用这个drawable

       <ImageView
           android:id="@+id/frame_anim_img"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:background="@drawable/frame_anim"/>
    • 在代码中开始动画

      ......
      ImageView imgView = (ImageView)findViewById(R.id.frame_anim_img);
      AnimationDrawable animDrawable = (AnimationDrawable)imgView.getBackground();
      ......
      animDrawable.start();
      
属性动画
  • 在Android3.0开始,针对动画的API进行了重大的升级,引入了全新的API,即Property Animation 属性动画,属性动画使得我们能够自定义实现更多复杂的动画,包括色彩,位置,形状等的变化,而且属性动画也不再局限于view的使用了,属性动画归根结底实际上就是对某个值不停的进行改变,其核心类就是后面介绍的ValueAnimator类。
    推荐这篇Android属性动画完全解析

    • ValueAnimator

      ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
      valueAnimator.setDuration(2000);
      valueAnimator.start();

      这里使用ValueAnimator.ofFloat(float start, float end)方法创建出一个float值从start变化到end的ValueAnimator对象,这个对象完成这一过程需要2000ms。
      这个运行后,并没有任何界面上的改变,因为这只是在对一个值进行不断的变化,我们可以通过ValueAnimator.AnimatorUpdateListener接口来监听这些值的改变。

          ....
          valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
               @Override
               public void onAnimationUpdate(ValueAnimator animation){
                   float currentValue = (float)animation.getAnimatedValue();
                   Log.d(TAG, "current is " + currentValue);
               }
          }
          valueAnimator.start();

      也可以通过xml来定义ValueAnimator, 主要通过animator标签:

          <!--res/animator/my_value_animator.xml-->
          <?xml version="1.0" encoding="utf-8"?>
          <animator xmlns:android="http://schemas.android.com/apk/res/android"
              android:valueFrom="0.0"
              android:valueTo="1.0"
              android:valueType="floatType"/>

      然后使用AnimatorInflator来加载这个动画:

          ValueAnimator animator = AnimatorInflater.loadAnimator(context, R.animator.my_value_animator); 
          animator.setDuration(2000);
          animator.start();
    • ObjectAnimator
      ObjectAnimator 继承自ValueAnimator,这个类是用来对任何对象进行动画的类,例如有一个ImageView我们对其实现渐变,旋转,缩放,平移等操作:

       // 渐变
       ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mImageView, "alpha", 0f, 1.0f);
       alphaAnimator.setDuration(2000);
       alphaAnimator.start();
       // 旋转
       ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(mImageView, "rotation", 0f, 1.0f);
       rotationAnimator.setDuration(2000);
       rotationAnimator.start();
      // Y方向放大3倍在还原
       ObjectAnimator yScaleAnimatro= ObjectAnimator.ofFloat(mImageView, "scaleY", 1f, 3f, 1f);
       yScaleAnimator.setDuration(5000);
       yScaleAnimator.start();
      // X方向向左平移500再复原
      int currentTranslationX = mImageView.getTranslationX();
      ObjectAnimator xTranslation = ObjectAnimator.ofFloat(mImageView, "translationX", currenTranslationX, -500, currentTranslationX);
      xTranslation.setDuration(2000);
      xTranslation.start();

      也可以通过XML来定义,使用objectAnimator标签:

      <!-- res/animator/alpha_animator.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator xmlns:android="http:schemas.android.com/apk/res/android"
              android:valueFrom="0.0"
              android:valueTo="1.0"
              android:valueType="floatType"
              android:propertyName="alpha"/>

      通过AnimatorInflater来加载:

       ObjectAnimator alphaAnimator = AnimatorInflater.loadAnimator(context,R.animator.alpha_animator);
       alphaAnimator.setTarget(mImageView);
       alphaAnimator.setDuration(2000);
       alphaAnimator.start(); 

      看起来和ValueAnimator很像。实际上他最核心的也就是使用到ValueAnimation对值的改变,这个值的就是我们对象的一个叫做alpha的属性的值,那么我们的对象mImageView真的有这个alpha的成员变量吗,其实ObjectAnimator根本不是去寻找对象的“alpha”成员变量进行改变,而是找对象属性alpha对应的getter和setter:

          public void setAlpha(float value);
          public float getAlpha();
    • AnimatorSet 动画集
      单个的动画使用往往很慢满足我们的现实需求,所有属性动画和补间动画一样也有自己的动画集或者叫做动画组合。主要使用到的是AnimatorSet这个类的如下几个方法:

       AnimatorSet animatorSet = new AnimatorSet();
       // AnimatorSet的play() 方法将会返回AnimatorSet.Builder的实例。
       AnimatorSet.Builder builder = animatorSet.play(animator1);
       // AnimatorSet.Builder  有四个主要的方法用来定义各个动画播放的时机
       // with(Animator anim) 将现有的动画和传入的动画同时执行
       // after(Animator anim) 将传入的动画在现有的动画执行完后再执行
       // before(Animator anim) 将传入的动画在现有的动画执行前执行
       // after(long delay) 将现有的动画延迟delay时间后执行
       // 这四个方法返回的也是builder,方便我们进行流式编程
       builder.with(anim2)
                   .after(anim3)
                   .before(anim4)
                   .after(1000);
       animatorSet.setDuration(2000);
       animatorSet.start();

      也可以利用xml来定义动画组合:

      <!--res/animator/my_custom_set.xml-->
       <?xml versino="1.0" encoding="utf-8"?>
       <set xmlns:android="http://schemas.android.com/apk/res/android"
           android:ordering="sequentially">
           <objectAnimator 
               android:duration="2000"
               android:propertyName="translationX"
               android:valueFrom="-500"
               android:valueTo="0"
               android:valueType="floatType"/>
          <set android:ordering="together">
              <objectAnimator 
                  android:duration="3000"
                  android:propertyName="rotation"
                  android:valueFrom="0"
                  android:valueTo="360"
                  android:valueType="floatType"/>
              <set android:ordering="sequentially">
                  <objectAnimator
                      android:duration="2000"
                      android:valueFrom="1.0"
                      android:valueTo="0.0"
                      android:valueType="floatType"
                      android:propertyName="alpha"/>
                  <objectAnimator
                      android:duration="2000"
                      android:valueFrom="1.0"
                      android:valueTo="1.5"
                      android:valueType="floatType"
                      android:propertyName="ratationX"/>
              </set>  
          </set>   
      </set>

      使用AnimatorInflater 加载:

       AnimatorSet set = AnimatorInflater.loadAnimator(context, R.animator.my_cusrom_set);
       set.setTarget(mImageView);
       set.strat();
    • AnimatorListener 监听器
      主要有三种监听器:

      1. AnimatorUpdateListener, 用于监听动画更新,例如我们之前查看ValueAnimator更新时值的变化就是使用的这个监听器:

         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
            @Override
            public void onAnimationUpdate(ValueAnimator animtor){
                float curValue = (float)animtor.getAnimatedValue(); 
            }    
         });
      2. AnimatorPauseListener,用于监听动画暂停或者恢复

            animator.addPauseListener(new Animator.AnimatorPauseListener(){
                @Override
                public void onAnimationPause(Animator animator){
                    // TODO
                }
        
                @Override
                public void onAnimationResume(Animator animator){
                    // TODO
                }
            });
      3. AnimatorListner,用来监听动画的开始,取消,结束,重复等事件。

        animator.addListener(new AnimatorListener(){
            @Override
            public void onAnimationStart(Animator animation){
            // TODO
        
            }
            @Override
            public void onAnimationEnd(Animator animtion){
            // TODO
        
            }
            @Override
            public void onAnimationCancel(Animator animation){
            // TODO
        
            }
            @Override
            public void onAnimationRepeat(Animator animation){
            // TODO
        
            }   
        });
        
        // 如果只想监听上述事件中的某个而不是全部,则可以使用AnimatorListenerAdapter
        animator.addListener(new AnimatorListenerAdapter(){
            @Override
            public void onAnimationStart(Animator animator){
            // TODO
        
            }
        });
        
    • TypeEvaluator
      我们知道ValueAnimator是对一个值进行不断改变,实际上这个值如何从开始值到结束值进行改变,和TypeEvaluator这个类有很大关系。在ValueAnimator调用getAnimatedValue(),跟一下源码就会发现,会经过PropertyValuesHolder类,再到KeyFrameSet类的getValue()函数,而这个函数中获取值就是调用TypeEvaluator的evaluate()函数,而实际上当我们这样调用ValueAnimator.ofFloat()时,就会初始化一个自带的FloatEvaluator对象,这个对象最终是我们KeyFrameSet中要使用到的那个mEvaluator对象,所以说这个值的变化和TypeEvaluator有很大关系。不妨看看FloatEvaluator类的源码:

       class FloatEvaluator implements TypeEvaluator<Numbers>{
      
          public Float evaluate(float fraction, Number startValue, Number endValue){
              float startFloat = startValue.floatValue();
              return startFloat + fraction * (endValue.floatValue() - startFloat);
          }
      
       }

      从这个代码可以看出,返回的值实际上就是起始值加上一个改变的值就得到当前值,而这个改变多少还和fraction这个参数有关,即fraction 乘以结束值和起始值的差值。
      同理可知,ValueAnimator的ofInt()方法会有一个自带的IntEvaluator:

      class IntEvaluator implements TypeEvaluator<Integer>{
          public Integer evaluate(float fraction, Integer startValue, Integer endValue){
              int startInt = startValue;
              return (int)(startInt + fraction*(endValue - startValue));
          }
      }

      利用TypeEvaluator我们就可以控制这个值的改变了,例如我们要实现一个点进行曲线运动,我们首先Point对象,x和y分别代码横纵坐标:

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

      接下来我们定义我们自己的PointEvaluator:

         public class PointEvaluator implements TypeEvaluator<Point>{
              public Point evaluate(float fraction, Point startValue, Point endValue){
                      float x = startValue.getX() + fraction * (endValue.getX() - startValue.getX());
                      float y = (float)(startValue.getY() + Math.cos(Math.PI / 2 * (1 + fraction)) * (endValue.getY() - startValue.getY())); 
                      return new Point(x, y);         
              }
         }

      使用这个PointEvaluator,利用ValueAnimator的ofObject()方法。

       Point startPoint  = new Point(0, 0);
       Point endPoint  = new Point(100, 200);
       ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
       anim.setDuration(2000);
       anim.start(); 

      然后我们可以利用Point对象来进行动画操作,我们主要在自定义的View中使用它:

       public class MyPointView extends View{
              private Point currentPoint;
              private Paint mPaint;
              private static float RADIUS = 50f;
      
              public PointView(Context context) {
                  super(context);
              }
      
              public PointView(Context context, AttributeSet attrs) {
                  super(context, attrs);
                  mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
                  mPaint.setColor(Color.RED);
              }
      
              public PointView(Context context, AttributeSet attrs, int defStyleAttr) {
                  super(context, attrs, defStyleAttr);
              }
      
              public PointView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
                  super(context, attrs, defStyleAttr, defStyleRes);
              }
      
              @Override
              protected void onDraw(Canvas canvas) {
                  if (currentPoint == null){
                      currentPoint = new Point(RADIUS, RADIUS);
                      drawCircle(canvas);
                      startAnimation();
                  }else {
                      drawCircle(canvas);
                  }
              }
      
              private void drawCircle(Canvas canvas){
                  float x = currentPoint.getX();
                  float y = currentPoint.getY();
                  canvas.drawCircle(x, y, RADIUS, mPaint);
              }
      
              private void startAnimation(){
                  Point startPoint = new Point(RADIUS, RADIUS);
                  Point endPoit = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
                  ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoit);
                  animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                      @Override
                      public void onAnimationUpdate(ValueAnimator animation) {
                          currentPoint = (Point) animation.getAnimatedValue();
                          invalidate();
                      }
                  });
                  animator.setDuration(5000);
                  animator.start();
              }
          }

      这是ValueAnimator使用自定义的TypeEvaluator来定义动画,那么我们知道ObjectAnimator是继承自ValueAnimator的当然也可以使用上述方法,但是ObjectAnimator有更方便的使用。
      之前说过ObjecAnimator内部的工作机制是通过寻找某个属性对应的setter和getter方法。然后通过这些方法对这个属性值进行改变,从而实现动画效果,例如我们想实现MyPointView的颜色的变化的动画,首先我们得为MyPointView 添加一个color属性和对应的getter和setter方法:

       public class MyPointView extends View{
              ...
              ...
              // #RRGGBB
              private String color;
              public void setColor(Stirng color){
                  this.color = color;
                  mPaint.setColor(Color.parseColor(color));
                  invalidate();
              }
      
              public  String getColor(){
                  return color;
              } 
       }

      我们定义了这个属性,那color的值如何进行改变呢,当然也需要我们用到TypeEvaluator:

       class ColorEvaluator implements TypeEvaluator<String>{
              private int mCurrentRed = -1;
              private int mCurrentGreen = -1;
              pviate int mCurrentBlue = -1;
      
              public String evaluate(float fraction, String startValue, String endValue){
                      int startRed = Integer.parseInt(startValue.substring(1, 3), 16);
                      int startGreen = Integer.parseInt(startValue.substring(3, 5), 16);
                      int startBlue = Integer.parseInt(startValue.substring(5, 7), 16);
                      int endRed = Integer.parseInt(endValue.substring(1, 3), 16);
                      int endGreen = Integer.parseInt(endValue.substring(3, 5), 16);
                      int endBlue = Integer.parseInt(endValue.substring(5, 7), 16); 
                      //  初始化
                      if (mCurrentRed == -1){
                          mCurrentRed = startRed;
                      }
      
                      if (mCurrentGreen == -1){
                          mCurrentGreen = startGreen;
                      }
      
                      if (mCurrentBlue == -1){
                          mCurrentBlue = startBlue;
                      }
                      int redDiff = Math.abs(startRed - endRed);
                      int greenDiff = Math.abs(startGreen - endGreen);
                      int blueDiff = Math.abs(startBlue - endBlue);
                      int colorDiff = redDiff + greenDiff + blueDiff;
                      if (mCurrentRed != endRed){
                          mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0, fraction);
                      }else if (mCurrentGreen != endGreen){
                          mCurrentGreen  = getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction);
                      }else if (mCurrentBlue != endBlue){
                          mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff, redDiff + greenDiff, fraction);
                      }
                      String currentColor = "#" + getHexString(mCurrentRed) + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
                      return currentColor;
              }
      
      
              private int getCurrentColor(int startColor, int endColor, int colorDiff,  
                      int offset, float fraction) {  
                  int currentColor;  
                  if (startColor > endColor) {  
                      currentColor = (int) (startColor - (fraction * colorDiff - offset));  
                      if (currentColor < endColor) {  
                          currentColor = endColor;  
                      }  
                  } else {  
                      currentColor = (int) (startColor + (fraction * colorDiff - offset));  
                      if (currentColor > endColor) {  
                          currentColor = endColor;  
                      }  
                  }  
                  return currentColor;  
              }  
      
              private String getHexString(int value) {  
                  String hexString = Integer.toHexString(value);  
                  if (hexString.length() == 1) {  
                      hexString = "0" + hexString;  
                  }  
                  return hexString;  
              }  
      
          }

      定义好了TypeEvaluator后,我们就可以对mPointView运用这个color的动画了:

      ObjectAnimator animator = ObjectAnimator(mPointView, "color", new ColorEvaluator(), "#0000FF", "#FF0000");
      animator.setDuration(2000);
      animator.start();

      我们可以将这个动画和前面定义的移动动画结合起来,将上述代码添加进MyPointView中即可:

          ...
          private void startAnimation(){
              Point startPoint = new Point(RADIUD, RADIUS);
              Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
              ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
              anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
                  @Override
                  public void onAnimationUpdate(ValueAnimator animation){
                      currentPoint = (Point)animation.getAnimatedValue();
                      invalidate();
                  }
              });
              ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(),   "#0000FF", "#FF0000");  
              AnimatorSet animSet = new AnimatorSet();  
              animSet.play(anim).with(anim2);  
              animSet.setDuration(5000);  
              animSet.start();  
          }
    • Interpolator
      我们知道属性动画是其实就是对一个值进行改变,之前我们使用TypeEvaluator能够自定义这些值是如何改变的,其中有一个参数是传入到TypeEvaluator的,即fraction,这个值实际上影响了我们值改变的速率或者动画的速度。
      Interpolator补间器,在补间动画就引入进来了的,但是属性动画引入后,使用的是TimeInterpolator接口,这个接口也兼容Interpolator。Android 系统默认已经为我们实现了很多补间器,比如:

      1. AccelerateDecelerateInterpolator(先加速在减速)
      2. AccelerateInterpolator(加速)
      3. DecelerateInterpolator(减速)
      4. LinearInterpolator(线性匀速)等等。。。

      属性动画默认使用的补间器是先加速在减速,淡然我们可以修改这补间器,方法很简单:

          ObjectAnimator anim = ObjectAnimator.ofObject(myPointView, "color", new ColorEvaluator(), "#0000ff", "#ff0000");
          anim.setInterpolator(new LinearInterpolator);
          anim.setDurarion(5000);
          anim.start();

      使用起来很简单的,那么这个Interpolator到底有些啥呢,我们看看源码:

      // 这个接口继承自TimeInterpolator, 可以用于渐变,平移,旋转,缩放等动画。
      public interface Interpolator extends TimeInterpolator{
      
      }

      TimeInterpolator 接口:

      public interface TimeInterpolator{
          // input 参数代表动画的当前的时刻,0代表开始,1代表结束
          // 返回的值,可以大于1也可以小于0;
          float getInterpolation(float input);
      }

      input的值是在0到1之间匀速增加的,这个值是由系统计算出来后传入getInterpolation方法中,而这个值和fraction是有关系的,之前用到的fraction正是根据这个input计算出来的,实际上就是我们getInterpolator()的返回值。看看LinearInterpolator:

      @HasNativeInterpolator
      public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
      
          public LinearInterpolator() {
          }
      
          public LinearInterpolator(Context context, AttributeSet attrs) {
          }
          // 因为input是匀速变化的,所有实现匀速的补间器,我们之间返回input的值就可以了,此时的fraction就是匀速变化的
          public float getInterpolation(float input) {
              return input;
          }
      
          /** @hide */
          @Override
          public long createNativeInterpolator() {
              return NativeInterpolatorFactoryHelper.createLinearInterpolator();
          }
      }

      在看看加速补间器:

      @HasNativeInterpolator
      public class AccelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
          private final float mFactor;
          private final double mDoubleFactor;
      
          public AccelerateInterpolator() {
              mFactor = 1.0f;
              mDoubleFactor = 2.0;
          }
      
          /**
           * Constructor
           *
           * @param factor Degree to which the animation should be eased. Seting
           *        factor to 1.0f produces a y=x^2 parabola. Increasing factor above
           *        1.0f  exaggerates the ease-in effect (i.e., it starts even
           *        slower and ends evens faster)
           */
          public AccelerateInterpolator(float factor) {
              mFactor = factor;
              mDoubleFactor = 2 * mFactor;
          }
      
          public AccelerateInterpolator(Context context, AttributeSet attrs) {
              this(context.getResources(), context.getTheme(), attrs);
          }
      
          /** @hide */
          public AccelerateInterpolator(Resources res, Theme theme, AttributeSet attrs) {
              TypedArray a;
              if (theme != null) {
                  a = theme.obtainStyledAttributes(attrs, R.styleable.AccelerateInterpolator, 0, 0);
              } else {
                  a = res.obtainAttributes(attrs, R.styleable.AccelerateInterpolator);
              }
      
              mFactor = a.getFloat(R.styleable.AccelerateInterpolator_factor, 1.0f);
              mDoubleFactor = 2 * mFactor;
              setChangingConfiguration(a.getChangingConfigurations());
              a.recycle();
          }
      
          public float getInterpolation(float input) {
              if (mFactor == 1.0f) {
                  return input * input;
              } else {
                  return (float)Math.pow(input, mDoubleFactor);
              }
          }
      
          /** @hide */
          @Override
          public long createNativeInterpolator() {
              return NativeInterpolatorFactoryHelper.createAccelerateInterpolator(mFactor);
          }
      }
      

      // 加速补间器的公式即  fraction= input(2mFactor) , mFactor的默认值是1.0f,所以加速补间器默认公式为:  fraction=input2 ;
      我们再来看看先加速后减速的补间器:

      @HasNativeInterpolator
      public class AccelerateDecelerateInterpolator extends BaseInterpolator
              implements NativeInterpolatorFactory {
          public AccelerateDecelerateInterpolator() {
          }
      
          @SuppressWarnings({"UnusedDeclaration"})
          public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
          }
      
          public float getInterpolation(float input) {
              return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
          }
      
          /** @hide */
          @Override
          public long createNativeInterpolator() {
              return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
          }
      }

      其实就是利用三角函数的余弦函数在 [π,2π] 的值为到[-1, 1],通过曲线可以知道其斜率是先增大再减小的,然后在对这个余弦函数除以2得到 [12,12] 在加上0.5,刚好是[0, 1]。
      仿照这个我们也可以制作出先减速在加速的补间器,我们可以考虑前半部利用正弦来实现:

          public class DecelerateAccelerateInterpolator implements Interpolator{
              @Override
              public float getInterpolation(float input){
                  if (input <= 0.5){
                      return (float)(Math.sin(Math.PI * input)) / 2 ;
                  }else {
                      return (float)(2 - Math.sin(Math.PI * input)) / 2;
                  }
              }
          }

      这样我们就实现了自己的补间器,用法都一样:

          anim.setDuration(5000);
          anim.setInterpolator(new DecelerateAccelerateInterpolator());
          anim.start();
    • ViewPropertyAnimation
      这是在Android3.1中引入的新的API。我们属性动画是不局限于View对象的,而是任何对象都可以进行执行,引入这个ViewProprertyAnimation的目的就是专门为View的属性动画服务的,而且使用起来更加清晰简单。
      使一个TextView 透明度从到0.5:

                      // 补间动画
                      Animation anim  = new AlphaAnimation(0.5);
                      anim.setDuration(3000);
                      textView.startAnimation(anim);
                      // 属性动画
                      ObjectAnimation objAnim = ObjectAnimation.ofFloay(textView,"alpha", 0.5);
                      objAnim.setDuration(3000);
                      objAnim.start();
                      // ViewPropertyAnimation
                      textView.animate().alpha(0.5f);

      用ViewPropertyAnimation也很容易编写动画组合:

          floatingActionButton.animate()
                      // 将当前到alpha动画到透明度为0
                      .alphaBy(0f)
                      .alpha(0.5f)
                      .translationY(-floatingActionButton.getHeight())
              .setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime))
                      .withEndAction(new Runnable() {
                          @Override
                          public void run() {
      
                          }
                      });
过渡动画 Transition
  • Android 4.4 引入了Transition的动画,用于在场景切换之间产生动画效果。了解Transition需要了解其工作的原理和几个重要的类。
    结合官方文档的来学习:Animating Views Using Scenes and Transitions.

    • Transition Framework的特点
      1. 在同一个视图分层结构中可以同时对所有的views应用一个或者多个动画
      2. 动画是基于开始和结束的视图view的属性的值。(可知其动画使用的是属性动画)
      3. 使用系统预定义的自带动画,如淡入淡出和移动动画。
      4. 可以从资源文件中加载视图层次和动画
      5. 提供了对动画和视图变化的过程更好控制的回调函数,便于管理生命周期。
    • Transition Framework 与view hierarchy , animation的关系
      Transition Framework 是和view hierarchy和animation并行工作的,它的目的是保存视图层次的状态与变化进而改变设备屏幕的显示或外表,并且用动画来呈现这些效果。
      Transition Framework 提供了几个很重要的类,抽象来对这些对象进行管理: Scene, Transition, TransionManager。
      这里写图片描述
      1. Scene 场景
        Transition Framework 处理view hierarchy的状态的改变,一个状态就是一个场景,故使用Scene来将view hierarchy的状态全部保存下来,保存在Scene中的包括这个view hierarchy 当前所有的view和所有的属性值;由于不同的Scene保存了view hierarchy的不同信息,这样我们就可以利用Scene中保存的不同状态进行场景的切换了。
        Scene 可以从资源布局文件中创建也可以通过view group在代码中创建。在大多数情况下,我们并不一定需要显式的创建一个开始场景,因为如果我们之前已经应用过一次transition的话,framework会默认使用上次结束的场景作为我们这次的开始场景,或者如果我们之前也没有进行过transition的话,framework 就会当前屏幕状态下的所有的view的信息。
        除了保存view hierarchy以及其属性值,scene也要保存这个view hierarchy的parent view。这个parent view 即是我们的scene root.场景的改变以及影响场景的动画,实际上都是发生在这个parent view 中的,即我们的要想进行transition还必须得定义场景所在的scene root。
      2. Transition 过渡
        Transition 带给用户最直接的就是动画,即在两个场景之间切换产生的动画,场景的定义交给的是Scene,而动画的定义以及创建则是交给了Transition。
      3. TransitionManager
        TransitionManager则负责执行Transition中的动画。
    • Transition Framework 的局限
      1. transition 用于surfaceview的时候有可能导致surfaceview 显示不正常。SurfaceView是从非UI线程更新的,所以在更新的时候,surfaceView上的动画有可能和其他的view的动画的更新不同步。
      2. 当用于TextureView的时候,有些类型的transition的动画可能不会产生预想的效果。
      3. 继承自AdapterView的类,如ListView,管理其child view 的方法和transition framework是不相容的,如果尝试去对基于adapterView的view进行动画,设备很可能会无响应。
      4. 如果尝试用动画来对TextView的大小进行调整,在大小调整完之前,text会被弹到一个新的位置,所以尽量避免对包含text的TextView进行调整大小的动画。
    • 创建Scene
      我们可以通过xml文件或者代码来创建Scene,每一个Scene还必须得有一个用来运行transition的parent view 来作为我们的Scene root。

      • 通过资源文件创建Scene.

        1. 创建包含Scene Root的布局文件

            <?xml versino="1.0" encoding="utf-8"?>
            <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    android:id="@+id/mater_layout
                    android:layout_width ="match_parent"
                    android:layout_height="match_parent">
                  <TextView 
                      android:id="@+id/title"
                      ...
                      android:text="Title"/>
                  <FrameLayout 
                      android:id="@+id/scene_root">
                      <inclue layout="@layout/a_scene"/>
                   </FrameLayout>
          </LinearLayout> 
          
        2. 创建第一个场景 a_scene.xml

            <?xml version="1.0" encoding="utf-8"?>
            <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/scene_container"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <TextView
                    android:id="@+id/text_view1"
                    ...
                    android:text="Text Line 1"/>
                <TextView
                    android:id="@+id/text_view2"
                    ...
                    android:text="Text Line 2"/>
        3. 创建另一个场景another_scene.xml

            <?xml version="1.0" encoding="utf-8"?>
            <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/scene_container"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <TextView
                    android:id="@+id/text_view2"
                    ...
                    android:text="Text Line 2"/>
                <TextView
                    android:id="@+id/text_view1"
                    ...
                    android:text="Text Line 1"/>
        4. 在代码中加载我们定义好的场景,使用Scene.getSceneForLayout();

              Scene mAScene;
              Scene mAnotherScene;
              // 需要先定义我们的SceneRoot
              mSceneRoot = (ViewGroup)findViewById(R.id.scene_root);
          
              // 利用SceneRoot创建我们在resource 文件中定义好的Scene
              mAScene = Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this);
              mAnotherScene = mAnotherScene.getSceneForLayout(mSceneRoot, R.layout.another_scene, this);
      • 在代码中动态的创建一场景,使用Scene(sceneRoot, viewhierarchy)这个构造函数。

            Scene mScene;
            mSceneRoot = (ViewGroup)mSomeLayoutElement;
            mViewHierarchy = (ViewGroup)mOtherLayoutElement;
            mScene = new Scene(mSceneRoot, mViewHierarchy);
      • 创建场景动作
        Transition Framework 允许我们自定义一些系统在进入或者退出场景时用来运行的动画action。一般情况下没有必要去自定义这些action因为framework会自动的在场景切换之间产生动画。
        Scene Action主要用来处理下面这些情况:

        1. 让与Scene不在同一个视图层次结构的view在Scene开始和结束时产生动画效果。
        2. 为一些Transition framework无法自动产生动画的Views产生动画,例如我们前面局限性里的ListView等。

        提供自定义的scene action, 我们需要将action定义为一个Runnable对象,并传递给Scene.setEnterAction(enterRunnable)或者Scene.setExitAction(exitRunnable)。enterRunnable 中的run()方法将会在其scene进入后被调用,exitRunnable中的run()方法将会在其scene离开后背调用。

            mScene = new mScene(mRoot, mHierarchy);
            mScene.setEnterAction(new Runnable(){
                // TODO ...
            });
    • 使用Transition
      创建好场景Scene以后,我们还需要定义要对这些场景应用的transition,系统预制了一些transition提供给我们使用:
    ClassTAGAttributesEffect
    AutoTransition-默认的transition,依次进行淡出,移动,缩放,淡入
    Fadeandroid:fadingMode=”[fade_infade_out
    ChangeBounds-移动和调整大小
    1. 在资源文件中创建Transition

       <!-- res/transition/my_fade.xml-->
       <?xml version="1.0" encoding="utf-8"?>
       <fade xmlns:android="http://schemas.android.com/apk/res/android"
               android:fadingMode="fade_in"/>
      
    2. 在代码中加载定义好的transition

       Transition mFadeTransition = 
                       TransitionInflater.from(mContext).
                       infalteTransition(R.tarnsition.my_fade);
      
    3. 直接在代码中生存transition

       Transition my_fade = new Fade();
      
    4. 使用TransitionManager对场景应用这个定义好的transition

          TransitionManager.go(mEndingScene, mFadeTransition);
      
    5. 在xml中也可以定义TransitionSet对多个transition进行组合应用

       <!--res/transition/my_transition_set.xml-->
       <?xml version="1.0" encoding="utf-8"?>
       <transitionSet xmlns:android=:"http://schemas.android.com/apk/res/android"
           android:tarsitionOrdering="sequential">
           <fade android:fadingMode="fade_out"/>
           <changeBound/>
           <fade android:fadingMode="fade_in/>
      
    6. 代码中加载transitionSet或者直接创建TransitionSet

       // 从xml文件中加载
       TransitionSet set = TransitionInflater.from(this)
                          .inflateTransition(R.transition.my_transion_set);
       // 从代码中直接创建
       TransitionSet set = new TransitionSet();
       Fade fade = new Fade(Fade.IN);
       set.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
       set.addTransition(fade);
      

      Transtion中可以对同一个view hierarchy中的需要进行动画的view进行添加或者移除,使用removieTarget(), addTarget().

  • 在单个场景中也可以应用Transition。
    我们也可以在当前的view hierarchy 中对子view进行增加,修改,删除等操作,使用ViewGroup.removeView(),ViewGroup.addView()。
    对一个ViewHierarchy中子view相当于只是在改变这个ViewHierarchy的状态,这个使用我们没有必要把两个状态定义为两个场景,只需要使用到delayTransition,delayTransition从当前view hierarchy的状态开始,记录view的改变,并且当系统对界面进行重绘的时候执行transition。
    1. 调用TransitionManager.beginDelayedTransition()方法,传入我们的SceneRoot和想要进行的Transition。
    2. 对子view进行改变, framework会记录我们应用到view上状态的改变,包括他们的属性。
    3. 当系统重绘UI的时候就会对这些改变进行transition的动画展示。
    <!--res/layout/activity_main.xml-->
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/mainLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:id="@+id/input_text"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
         ...
    </RelativeLayout>
    private TextView mLabelText;
    private Fade mFade;
    private ViewGroup mRootView;
    ...
    this.setContentView(R.layout.activity_main);
    mLableText = new TextView();
    mLableText.setText("Lable1").setId("1");
    mRootView = (ViewGroup)findViewById(R.id.mainLayout);
    mFade = new Fade(Fade.IN);
    TransitionManager.beginDelayedTransition(mRootView, mFade);
    mRootView.addView(mLableText);
  • transition 的 生命周期
    和Activity一样,transition也有自己的生命周期,我们可以通过TransitionListener接口定义的回调来处理各个生命周期的一些操作。
    Transition fade = new Fade(Fade.IN|Fade.OUT);
    fade.addListener(new Transition.TransitionListener(){
        @Override
            public void onTransitionStart(Transition transition) {
                Log.d("TAG", "transition start");
            }

            @Override
            public void onTransitionEnd(Transition transition) {
                Log.d("TAG", "transition end");
            }

            @Override
            public void onTransitionCancel(Transition transition) {
                Log.d("TAG", "transition cancel");
            }

            @Override
            public void onTransitionPause(Transition transition) {
                Log.d("TAG", "transition pause");
            }

            @Override
            public void onTransitionResume(Transition transition) {
                Log.d("TAG", "transition resume");
            }
    });
  • 自定义Transition
    和前面了解的属性动画差不多,transition也可以自定义,实际上就是对场景切换的一些动画或者检测某些view进行自定义。
    继承Transition类并且重写三个方法:
public class CustomTransition extends Transition{
    @Override
    public void captrueStartValues(TransitionValues values){
        // TODO
    }

    @Override
    public void captureEndValues(TransitionValues values){
        // TODO
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,TransitionValues startValues, Transitions endValues){
        // TODO
    }
}

TransitionValues 用来保存view hierarchy 中的view的引用以及我们想用到一些属性值,这些属性值存在一个Map中。这些方法对sceneRoot 中的view分别调用一次,所以我们可以针对某一个view改变某一种属性值。createAnimator函数是用来创建我们所要使用到的动画。

    public class ChangeColor extends Transition {

        /** Key to store a color value in TransitionValues object */
        private static final String PROPNAME_BACKGROUND = "customtransition:change_color:background";

        /**
         * Convenience method: Add the background Drawable property value
         * to the TransitionsValues.value Map for a target.
         */
        private void captureValues(TransitionValues values) {
            // Capture the property values of views for later use
            values.values.put(PROPNAME_BACKGROUND, values.view.getBackground());
        }

        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }

        // Capture the value of the background drawable property for a target in the ending Scene.
        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }

        // Create an animation for each target that is in both the starting and ending Scene. For each
        // pair of targets, if their background property value is a color (rather than a graphic),
        // create a ValueAnimator based on an ArgbEvaluator that interpolates between the starting and
        // ending color. Also create an update listener that sets the View background color for each
        // animation frame
        @Override
        public Animator createAnimator(ViewGroup sceneRoot,
                                       TransitionValues startValues, TransitionValues endValues) {
            // This transition can only be applied to views that are on both starting and ending scenes.
            if (null == startValues || null == endValues) {
                return null;
            }
            // Store a convenient reference to the target. Both the starting and ending layout have the
            // same target.
            final View view = endValues.view;
            // Store the object containing the background property for both the starting and ending
            // layouts.
            Drawable startBackground = (Drawable) startValues.values.get(PROPNAME_BACKGROUND);
            Drawable endBackground = (Drawable) endValues.values.get(PROPNAME_BACKGROUND);
            // This transition changes background colors for a target. It doesn't animate any other
            // background changes. If the property isn't a ColorDrawable, ignore the target.
            if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) {
                ColorDrawable startColor = (ColorDrawable) startBackground;
                ColorDrawable endColor = (ColorDrawable) endBackground;
                // If the background color for the target in the starting and ending layouts is
                // different, create an animation.
                if (startColor.getColor() != endColor.getColor()) {
                    // Create a new Animator object to apply to the targets as the transitions framework
                    // changes from the starting to the ending layout. Use the class ValueAnimator,
                    // which provides a timing pulse to change property values provided to it. The
                    // animation runs on the UI thread. The Evaluator controls what type of
                    // interpolation is done. In this case, an ArgbEvaluator interpolates between two
                    // #argb values, which are specified as the 2nd and 3rd input arguments.
                    ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(),
                            startColor.getColor(), endColor.getColor());
                    // Add an update listener to the Animator object.
                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            Object value = animation.getAnimatedValue();
                            // Each time the ValueAnimator produces a new frame in the animation, change
                            // the background color of the target. Ensure that the value isn't null.
                            if (null != value) {
                                view.setBackgroundColor((Integer) value);
                            }
                        }
                    });
                    // Return the Animator object to the transitions framework. As the framework changes
                    // between the starting and ending layouts, it applies the animation you've created.
                    return animator;
                }
            }
            // For non-ColorDrawable backgrounds, we just return null, and no animation will take place.
            return null;
        }
}
Activity, Fragment Transition
  • Android 5.0 引入了针对Activity和Fragment场景切换的Transition动画。
    -. content transition 从一个Activity 切换到另一个Activity内容改变所产生的transition。

    • 使用content transition 将应用Style的contentTransition标识设为true

           <!--res/values/styles.xml-->
           <!--如果应用使用的是AppCompay主题,默认是开启了contentTransition支持的-->
           <style name="AppThem" ...>
              <item name="android:windowContentTransition">true</item>
          </style>
      
    • content transition 分为三种 enter transition, exit transition, reenter transition
      前面介绍transition时,我们就知道可以从xml或者code中直接加载transition。
      这里我们从xml中定义exitTransition,从code中定义enterTransition,xml中定义reenter transition。

       <!--res/transition/my_exit_transition.xml-->
       <explode xmlns:android="http://schemas.android.com/apk/res/android"/>
    // enter transition
    Slide slide = new Slide(Gravity.Bottom);
    slide.addTarget(R.id.desctiption);
    slide.setDuration(slideDuration);
        <!--res/transition/my_reenter_transition-->
        <slide xmlns:android="http://schemas.android.com/apk/res/android"
            android:slideEdge="top"/>
    • 使用exit transition

          <!--res/values/styles.xml-->
          <style name="AppThem.Home">
              <item name="android:windowExitTransition">@transition/grid_exit</item>
          </style>
    • 使用enter transition

          // enter transition
          Slide slide = new Slide(Gravity.Bottom);
          slide.addTarget(R.id.desctiption);
          slide.setDuration(slideDuration);
          getWindow().setEnterTransition(slide);
    • 使用reenter transition

          <!--res/values/styles.xml-->
          <style name="AppThem.Home">
              <item name="android:windowExitTransition">@transition/grid_exit</item>
              <item name="android:windowReenterTransition">@transition/my_reenter_transition</item>
          </style>
  • shared element transition
    在两个场景之间针对同一个元素进行过渡动画。这里的同一个元素并非完全一样的是同一个元素,只是通过transitionName字符串来标识他们的在两个场景中是否是相同的。

    • 在xml文件中定义两个场景需要shared的元素的transitionName
      <!--res/layout/activity_list.xml-->
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          ...
          >
          ...
          <ImageView
              android:id="@+id/my_image"
              android:transitionName="@string/shared_element_img"
          />  
      <RelativeLayout/>
     <!--res/layout/activity_detail.xml-->
     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
         ...
        >
    
        <ImageView
            android:id="@+id/detail_image"
            android:transitionName="@string/shared_element_img"
            />
    
    <LinearLayout/>
    • startActivity传入共享的元素和transitionName

       ...
      ImageView mImageView = (ImageView)findViewById(R.id.my_image);
      ....
       Intent intent = new Intent(this, DetailActivity.class);
       Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(this,mImgaeView, mImageView.getTransitionName()).toBundle();
       stratActivity(intent, bundle);
       .....
       .....
      // 也可以传入多个sharedView 
      Pair(View ,String) sharedImg = new Pair(mImageView, mImageView.getTransitionName());
      Paie(View, String)sharedText = new Pair(mTextView, mTextView.getTransitionName());
      Bundle bundle1 = ActivityOptions.makeSceneTransitionAnimation(this, sharedImg, sharedText).toBundle();
      startActivity(intent, bundle1);

      shared element transition 默认使用的transition是

       <!--res/transition/move.xml-->
       <transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
              <changeBounds/>
              <changeTransform/>
              <changeClipBound/>
              <changeImageTransform/>
       </transitionSet>

      查看Theme.Material.Light这个主题的源码:

          <style name="Theme.Material.Light" parent="Theme.Light">
              <item name="windowSharedElementEnterTransition">@transition/move</item>
              <item name="windowSharedElementExitTransition">@transition/move</item>
          </style>

    所以我们也可以定义我们自己的shared element 需要使用到的transition。

        <style name="AppTheme.Detail">
            <item name="windowSharedElementEnterTransition">@transition/my_transition </item>
            <item name="windowSharedElementExitTransition">@transition/my_transition</item>
        </style>

    或者在代码中设置

        Transition transition = TransitionInflater.from(mContext).inflate(R.transition.my_transion);
        getWindow().setSharedEnterTransition(transition);
        getWindow().setSharedExitTransition(transition);
  • 更协调的动效
    有些情况同一个view hierarchy中可能会有多个view都需要动画,如果全部都同时或者突然出现有可能会产生不太协调的效果,这个时候需要对这些动画进行彼此间位置或顺序的协调;
    例如,我们的list中item进入后的动画,我们可以让item都从下往上滑动到其位置。

        @Override
        protected void onCreate(Bundle savedInstance){
            super.onCreate(savedInstance);
            setContentView(R.layout.activity_main);
            animatesViewsIn();
        }
    
        private void animatesViewsIn(){
            ViewGroup root = (ViewGroup)findViewById(R.id.root);
            int count = root.getChildCount();
            float offset = getResources().getDimensionPixelSize(R.dimen.offset_y);
            Interpolator interpolator =      AnimationUtils.loadInterpolator(this,android.R.interpolator.linear_out_slow_in);
            for (int i = 0; i < count; i++) {
                View view = root.getChildAt(i);
                view.setVisibility(View.VISIBLE);
                view.setTranslationY(offset);
                view.setAlpha(0.85f);
                // then animate back to natural position
                view.animate()
                        .translationY(0f)
                        .alpha(1f)
                        .setInterpolator(interpolator)
                        .setDuration(1000L)
                        .start();
                // increase the offset distance for the next view
                offset *= 1.5f;
            }
        }
        // 也可以使用无序的动画组合
        private void animateViewsIn() {
            // setup random initial state
            ViewGroup root = (ViewGroup) findViewById(R.id.root);
            float maxWidthOffset = 2f * getResources().getDisplayMetrics().widthPixels;
            float maxHeightOffset = 2f * getResources().getDisplayMetrics().heightPixels;
            Interpolator interpolator =
                    AnimationUtils.loadInterpolator(this, android.R.interpolator.linear_out_slow_in);
            Random random = new Random();
            int count = root.getChildCount();
            for (int i = 0; i < count; i++) {
                View view = root.getChildAt(i);
                view.setVisibility(View.VISIBLE);
                view.setAlpha(0.85f);
                float xOffset = random.nextFloat() * maxWidthOffset;
                if (random.nextBoolean()) {
                    xOffset *= -1;
                }
                view.setTranslationX(xOffset);
                float yOffset = random.nextFloat() * maxHeightOffset;
                if (random.nextBoolean()) {
                    yOffset *= -1;
                }
                view.setTranslationY(yOffset);
    
                // now animate them back into their natural position
                view.animate()
                        .translationY(0f)
                        .translationX(0f)
                        .alpha(1f)
                        .setInterpolator(interpolator)
                        .setDuration(1000)
                        .start();
                }
        }
  • 曲线运动
    在shared elements transition中,运行的轨迹默认是直线的,可以改成曲线的,这样更突出动画的效果。

        getWindow().setSharedElementEnterTransition(TransitionInflater.from(this)
                .inflateTransition(curve ? R.transition.curve : R.transition.move));

    curve.xml为

        <?xml version="1.0" encoding="utf-8"?>
        <transitionSet
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:duration="450">
            <changeBounds/>
            <changeTransform/>
            <changeClipBounds/>
            <changeImageTransform/>
            <!-- Here we're using arc motion which handles the curved path for us.
                 Also see PathMotion and PatternPathMotion for other techniques  -->
            <arcMotion android:minimumHorizontalAngle="50" android:minimumVerticalAngle="50" />
        </transitionSet>
  • 尺寸的改变
    例如对一个imageView 进行放大缩小,默认都是宽和高同时进行缩放,为来更好的效果实际上我们可以先缩放宽或者高,再缩放另外一处。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_size_change);
        card = (CardView) findViewById(R.id.card);
    }

    public void changeSize(View view) {
        Interpolator interpolator = AnimationUtils.loadInterpolator(this, android.R.interpolator.fast_out_slow_in);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(card, View.SCALE_X, (small ? LARGE_SCALE : 1f));
        scaleX.setInterpolator(interpolator);
        scaleX.setDuration(symmetric ? 600L : 200L);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(card, View.SCALE_Y, (small ? LARGE_SCALE : 1f));
        scaleY.setInterpolator(interpolator);
        scaleY.setDuration(600L);
        scaleX.start();
        scaleY.start();

        // toggle the state so that we switch between large/small and symmetric/asymmetric
        small = !small;
        if (small) {
            symmetric = !symmetric;
        }
    }
AnimatedVectorDrawable
  • vector drawable 类似于网页设计的SVG是一种可缩放的矢量图,要是能用的话尽量用这种格式的图片,特别是一些图标。例如:

    <?xml version="1.0" encoding="utf-8"?>
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="120dp"
        android:height="120dp"
        android:viewportWidth="24"
        android:viewportHeight="24">

        <path
            android:name="cross"
            android:pathData="M6.4,6.4,L17.6,17.6,M6.4,17.6,L17.6,6.4"
            android:strokeWidth="6"
            android:strokeLineCap="square"
            android:strokeColor="#999"/>

    </vector>

vector drawable 形状由pathData决定,修改pathData就能修改vector drawable的形状。
VectorDrawable官方文档
关于vector drawable的兼容和一些问题,推荐看Android Vector曲折的兼容之路这篇博客。
VectorDrawable 的标签中有几个很重要:

    <!-- group 标签作用
        1. 对path或者子group进行分组,便与管理各个相关的group或者path
        2. 对path进行动画扩展,path 中的动画没有scale, rotate, translate
    -->
    <group
        android:name="group_c"        //名称,AnimatedVectorDrawable 的<target>通过 android:name识别
        android:translateX="1"       // x方向平移多少
        android:translateY="2"       // y方向平移多少
        android:scaleX="1"           // x 方向放大多少
        android:scaleY="2"           // y 方向放大多少 
        android:rotation="90"        // 旋转的角度
        android:pivotX="12"          // scale和rotation变化的中心点x坐标
        android:pivotY="12">         // scale和rotation变化的中心点y坐标
        <path
            android:name="cross"      // 名称,AnimatedVectorDrawable 的<target>通过android:name识别 
            android:pathData="M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4 M6.4,6.4"
            android:strokeColor="#090909"      // 线条的颜色
            android:fillColor="#c0c0c0"        // 填充颜色
            android:trimPathEnd="0.9"          // 进行裁剪的结束位置,值为0~1,0为头,1为尾
            android:trimPathStart="0.5"/>      // 进行裁剪的开始位置,值为0~1,0为头,1为尾
    </group>

关于path中的pathData,参照SVG中定义的path标签的中的命令的定义。
Path中的命令详解

  1. 指令
    • M = moveto (M X,Y) : 将画笔移动到指定的坐标位置
    • L = lineto (L X, Y) : 画直线到指定的坐标位置
    • H = horizontal lineto (H X) : 画水平线到指定的坐标
    • V = vertical lineto (V Y) : 画垂直线到指定的坐标
    • C = curveto (C X1, Y1, X2, Y2, ENDX, ENDY) : 三次贝塞尔曲线
    • S = smooth curveto (S X2, Y2, ENDX, ENDY)
    • Q = quadratic Belzier curveto (Q X, Y, ENDX, ENDY) : 二次贝塞尔曲线
    • T = smooth quadratic Belzier curveto (T ENDX, ENDY) : 映射
    • A = elliptical Arc(A RX,RY,XROTATION,FLAG_LARGE, FLAG_SWEEP, X, Y) :画弧线。
    • Z = closepath (z) : 关闭路径。
  2. 使用的原则
    • 坐标轴为(0,0)为中心,X轴水平向右,Y轴水平
    • 所有指令大小写区分,大写采用绝对定位,参照全局的坐标系,小写采用相对定位,参照父坐标系(上一次绘制停留的那个位置)
    • 指令和数据之间的空格可以省略
    • 同一指令出现多次可以只用一个。

对vector drawable 进行动画,可以进行下面这些动画,最重要的是最后三点:

  • translate 平移
  • scale 缩放
  • rotate 旋转
  • opacity 透明度修改
  • color 色彩修改 (android:stokeColor ,android:fillColor)
  • path 路径改变
  • trim start/end (android:trimPathStart / android:trimPathEnd)路径剪裁
  • clip-path 剪切路径

vector drawable的原理也只是使用属性动画,查看其源码,上述的属性都能找到对应的setter和getter函数,所有能进行这些动画。

  1. 路径变化动画 : 例如我们将一个符号”√”变换成一个”x”符号。

    • 创建两个vector drawable

          <!-- res/drawable/tick_drawable.xml-->
          <vector xmlns:android="http://schemas.android.com/apk/res/android"
                       android:width="24dp"
                       android:height="24dp"
                       android:viewportWith="24"
                       android:viewportHeight="24">
                       <group
                           android:name="group_tick"
                           android:pivotX="12"
                           android:pivotY="12">
                           <path 
                               android:name="tick"
                               android:stokeWidth="2"
                               android:stokeColor="$c2c2c2"
                               android:pathData="@string/path_tick"/>
                      </group>         
              </vector>   
      
          <!--res/drawable/cross_drawable.xml-->
          <vector xmlns:android="http://schemas.android.com/apk/res/android"
                      android:width="24dp"
                      android:height="24dp"
                      android:viewportWith="12"
                      android:viewportHeight="12">
                      <group 
                          android:name="group_cross"
                          android:pivotX="12"
                          android:pivotY="12">
                          <path
                              android:name="corss"
                              android:strokeWidth="2"
                              android:strokeHeight="2"
                              android:strokeColor="#c2c2c2"
                              android:pathData="@string/path_cross"/>
                      </group>
              </vector>
          <!-- 分别存储cross和tick的pathData-->
          <string name="path_cross">M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4</string>
          <string name="path_tick>M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7</string>
    • 创建属性动画

          <!--res/animator/cross_to_tick.xml-->
          <objectAnimator 
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:propertyName="pathData"
              android:fromValue="@string/path_cross"
              android:toValue="@string/path_tick"
              android:duration="1000"
              android:interpolator="@android:interpolator/fast_out_slow_in"
              android:valueType="pathType"/>
      <!--res/animator/tick_to_cross.xml-->
      <objectAnimator
         xmlns:android="http://chemas.android.com/apk/res/android"
         android:propertyName="pathData"
         android:fromValue="@string/path_tick"
         android:toValue="@string/path_cross"
         android:duration="1000"
         android:interpolator="@android:interpolator/fast_out_slow_in"
         android:valueType="pathType">   
      <!--res/animator/rotate_tick_to_cross.xml-->
      <objectAnimator
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:propertyName="rotation"
         android:formValue="0"
         android:toValue="180"
         android:duration="1000"
         android:valueType="floatType"
         android:interpolator="@android:interpolator/fast_out_slow_in"/>
      <!--res/animator/rotate_cross_to_tick.xml-->
      <objectAnimator
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:propertyName="rotation"
         android:formValue="-180"
         android:toValue="0"
         android:duration="1000"
         android:valueType="floatType"
         android:interpolator="@android:interpolator/fast_out_slow_in"/>
    • 创建AnimatedVectorDrawable

          <!-- res/drawable/avd_cross_to_tick.xml 从叉到勾的路径变化-->
          <animated-vector
                  xmlns:android="http://schemas.android.com/apk/res/android"
                  android:drawable="@drawable/cross_drawable">
                  <target
                      android:name="cross"
                      android:animation="@animator/cross_to_tick"/>
                  <target
                      android:name="group_cross"
                      android:animation="@animator/rotate_cross_to_tick"/>
      
          </animated-vector>          
          <!-- res/drawable/avd_tick_to_cross.xml 从勾到叉的路径变化-->
          <animated-vector
                  xmlns:android="http://schemas.android.com/apk/res/android"
                  android:drawable="@drawable/tick_drawable">
                  <target
                      android:name="cross"
                      android:animation="@animator/tick_to_cross"/>
                  <target
                      android:name="group_cross"
                      android:animation="@animator/rotate_tick_to_cross"/>
      
          </animated-vector>      
    • 使用AnimatedVectorDrawable
      ImageView imgView = (ImageView)findViewById(R.id.my_img);
      AnimatedVectorDrawable avd = (AnimatedVectorDrawable)getDrawable(R.drawable.avd_tick_to_cross);
      ...
      // 触发动画,例如点击某个view后触发这个动画
      @Overrid
      public void onClick(View v){
          img.setImageDrawable(avd);
          avd.start();
      }

    注意:要想对pathData进行改变,这个两个pathData中的命令必须是一样的,只是参数不一样而已,否则会报错误的。
    cross为: M 6.4,6.4 L 17.6,17.6 M 6.4,17.6 L 17.6,6.4
    tick   为:M 4.8,13.4 L 9,17.6 M 10.4,16.2 L 19.6,7
    两者都分别采用了:M, L, M,L 四个命令,只是参数不一样而已,故两者之间可以进行改变。

  2. path 绘制动态效果,例如显示”Android Design“这两个单词使其看起来像在书写出来一样。这里使用到的关键点就是path的android:trimPathEnd属性。

    • 将”Android Design”拆成多个路径再定义vector drawable

      <!--res/drawable/android_design.xml-->
      <vector
       xmlns:android="http://schemas.android.com/apk/res/android"
       android:with="308dp"
       android:heigh="68dp"
       android:viewportWidth="308"
       android:viewpoerHeigh="69">
       <path
           andorid:name="andro"
           android:strokeWidth="3"
           android:strokeLineCap="round"
           android:strokeLineJoin="round"
           android:strokeColor="#fc1418"
           android:trimPathEnd="0"
           android:pathData="M0.341920533,40.5760313 C10.4153396,48.6685632 17.8034264,14.3620789 25.1846643,3.56823233 C22.6808659,17.4367383 24.2427442,35.0729292 30.8190658,37.8244796 C37.3953873,40.57603 41.5672433,24.9138787 44.6864294,17.4367377 C44.6864297,24.9138787 37.5249051,37.3372753 39.249546,38.3131369 C42.8471123,31.0871747 50.0182725,22.9181478 52.3256959,21.7592998 C54.6331193,20.6004517 50.8848435,36.4943726 53.2676171,36.1358718 C62.1953141,39.082309 62.1473214,16.7555802 74.563028,23.7653072 C62.1473214,18.8909047 62.0474335,34.9249451 63.0693063,36.4087435 C76.0701005,34.9249451 85.9997193,6.61579217 81.6328141,0.899274471 C72.6738297,-0.871651751 72.1813599,37.3152891 76.0701025,38.1418785 C86.6180283,37.824479 90.9662415,19.7790174 89.21512,15.293806 C83.852121,23.0597104 91.383788,21.276977 93.8478663,24.9138796 C96.3540742,28.3130578 90.4737717,39.4542398 96.3540742,38.8211488 C101.209825,37.9762811 111.517305,15.6548547 113.471544,21.0005578 C107.744981,18.6286267 102.662668,37.2240495 109.328684,37.824479 C117.918245,38.1418771 118.454797,21.0005578 113.471544,20.4832582"/>
      <path
          android:name="id"
          android:strokeWidth="3"
          android:strokeLineCap="round"
          android:strokeLineJoin="bevel"
          android:strokeColor="#fc1418"
          android:trimPathEnd="1"
          android:pathData="M126.046387,22.4013672 C121.762207,28.8041992 123.087402,37.2265625 125.954102,38.3725586 C130.26416,41.6142578 138.382324,19.9448242 144.455078,21.7612305 C131.391113,27.4980469 135.289551,36.3017578 137.201172,36.3017578 C152.215819,34.4545898 159.176759,1.63085934 155.48291,0.109375 C146.004882,5.33300781 145.556151,36.3017578 150.474609,38.1679688 C157.431153,38.1679688 160.515137,28.8041992 160.515137,28.8041992" />
      
      <path
          android:name="a_stroke"
          android:strokeWidth="5"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#2c6481"
          android:trimPathEnd="1"
          android:pathData="M15.5131836,25.2182617 C19.5947266,25.2182617 31.4887695,22.9897461 31.4887695,22.9897461" />
      
      <path
          android:name="i_dot"
          android:strokeWidth="6"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#ff00ff"
          android:trimPathEnd="1"
          android:pathData="M127.723145,15.8867188 L127.163086,17.0029297" />
      
      <path
          android:name="d"
          android:strokeWidth="6"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#2c6481"
          android:trimPathEnd="1"
          android:pathData="M179.80127,2.60498047 C176.131917,10.1152344 174.223633,34.0673828 173.55957,38.5478516 C175.206055,36.9267578 174.533691,36.8710938 175.60498,36.8710938 C212.008789,26.9985352 192.196777,-0.39453125 172.428711,6.56152344" />
      
      <path
          android:name="esig"
          android:strokeWidth="3"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#2c6481"
          android:trimPathEnd="1"
          android:pathData="M204.027344,29.0200195 C212.498535,24.2231445 209.48584,20.0551758 208.53125,20.0551758 C205.774902,18.3828125 196.044922,32.4404297 204.596191,37.5283203 C214.817871,41.4614258 218.684081,16.0166016 223.237792,16.0166016 C217.423339,27.6240234 235.10498,37.5283203 215.530274,38.2475586 C230.764648,44.2109375 235.949706,24.9003906 237.895507,23.5888672 C234.370117,35.1958008 236.134765,37.5283203 238.70166,38.2475586 C243.201171,39.519043 251.621093,13.8134766 264.673828,20.8544922 C251.621093,13.8134766 244.347656,35.7421875 249.693848,35.7421875 C255.04004,35.7421875 259.597167,24.2231445 262.54248,24.2231445 C262.54248,35.0126419 259.476562,60.7124023 249.032714,52.6586914" />
      
      <path
          android:name="n"
          android:strokeWidth="3"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#2c6481"
          android:trimPathEnd="1"
          android:pathData="M274.092285,17.934082 C271.665527,21.6083984 270.089844,38.465332 270.089844,38.465332 C275.562012,24.7871094 280.663086,22.0395508 282.294434,22.5825195 C283.466797,28.0629883 281.084961,34.3491211 283.559082,36.1098633 C286.033203,37.8706055 289.920898,35.0537109 293.011719,28.9052734" />
      
      <path
          android:name="second_i_dot"
          android:strokeWidth="3"
          android:strokeLineCap="round"
          android:strokeLineJoin="round"
          android:strokeColor="#2c6481"
          android:trimPathEnd="1"
          android:pathData="M239.723145,15.8867188 L239.163086,17.0029297" />
      
    • 定义每个path所需要用到的动画

       <!--res/animator/andro.xml-->
       <objectAnimator
           xmlns:android="http://schemas.android.com/apk/res/android"
           android:propertyName="trimPathEnd"
           android:valueFrom="0"
           android:valueTo="1"
           android:duration="1000"
           android:interpolator="@android:interpolator/fase_out_slow_in"/>
      <!--res/animator/id.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="1050"
          android:duration="250"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
      <!--res/animator/d.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="1550"
          android:duration="200"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
      
       <!--res/animator/esig.xml-->
       <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="1800"
          android:duration="600"
          android:interpolator="@android:interpolator/fast_out_linear_in" />
       <!--res/animator/n.xml-->
       <?xml version="1.0" encoding="utf-8"?>
       <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="2450"
          android:duration="200"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
      <!--res/animator/i_dot.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="1400"
          android:duration="50"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
      <!--res/animator/second_i_dot.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="2700"
          android:duration="50"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
      <!--res/animator/a_stroke.xml-->
      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="trimPathEnd"
          android:valueFrom="0"
          android:valueTo="1"
          android:startOffset="1300"
          android:duration="50"
          android:interpolator="@android:interpolator/fast_out_slow_in" />
    • 创建AnimatedVectorDrawable

        <animated-vector
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:drawable="@drawable/android_design">
            <target
          android:name="andro"
          android:animation="@animator/andro" />
      
          <target
              android:name="id"
              android:animation="@animator/id" />
      
          <target
              android:name="a_stroke"
              android:animation="@animator/a_stroke" />
      
          <target
              android:name="i_dot"
              android:animation="@animator/i_dot" />
      
          <target
              android:name="d"
              android:animation="@animator/d" />
      
          <target
              android:name="esig"
              android:animation="@animator/esig" />
      
          <target
              android:name="n"
              android:animation="@animator/n" />
      
          <target
              android:name="second_i_dot"
              android:animation="@animator/second_i_dot" />
      
          <target
              android:name="andro"
              android:animation="@animator/color"/>
      </animated-vector>
    • 使用AnimatedVectorDrawerable

      private ImageView imageView;
      private AnimatedVectorDrawable avd;
      
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          imageView = (ImageView) findViewById(R.id.image_view);
          avd = (AnimatedVectorDrawable) getDrawable(R.drawable.avd_android_design);
          imageView.setImageDrawable(avd);
      }
      
      public void click(View view) {
          avd.start();
      }
  3. clip-path 实现填充或者镂空。
    主要使用到的是clip-path这个标签。这个标签对当前group和child group 有效。而且,定义clip-path后,知会对clip-path后面的path产生效果。而不会对clip-path前产生效果。例如:

    • 定义vectorDrawable

      <!--res/drawable/ic_heart.xml-->
      <vector
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="280dp"
          android:height="280dp"
          android:viewportWidth="56"
          android:viewportHeight="56">
          <path
              android:name="full"
              android:pathData="@string/heart_full_path"
              android:fillColor="#2c32f1" />
          <clip-path
              android:name="clip"
              android:pathData="@string/heart_clip_hidden" />
          <path
              android:name="empty"
              android:pathData="@string/heart_empty_path"
              android:fillColor="#2cf2af" />
      </vector>

      这个例子中的clip-path值对empty这个path有效,而无法影响full这个path。

    • 定义需要用到的动画

          <!--res/animator/empty_heart.xml-->
          <objectAnimator 
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:propertyName="pathData"
              android:fromValue="@string/heart_clip_shown"
              android:toValue="@string/heart_clip_hidden"
              android:duration="1000"
              android:interpolator="@android:interpolator/fast_out_slow_in"
              android:valueType="pathType"/>
      
      <!--res/animator/full_heart.xml-->
      <objectAnimator 
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="pathData"
          android:fromValue="@string/heart_clip_hidden"
          android:toValue="@string/heart_clip_shown"
          android:duration="1000"
          android:interpolator="@android:interpolator/fast_out_slow_in"
          android:valueType="pathType"/>
      
          <!--各个path要用到的pathData-->
          <string name="heart_empty_path">M32.95,19 C31.036,19 29.199,19.8828338 28,21.2724796 C26.801,19.8828338 24.964,19 23.05,19 C19.6565,19 17,21.6321526 17,24.9945504 C17,29.1089918 20.74,32.4713896 26.405,37.5667575 L28,39 L29.595,37.5667575 C35.26,32.4713896 39,29.1089918 39,24.9945504 C39,21.6321526 36.3435,19 32.95,19 L32.95,19 Z M28.1155,35.9536785 L28,36.0572207 L27.8845,35.9536785 C22.654,31.2506812 19.2,28.1444142 19.2,24.9945504 C19.2,22.8201635 20.8555,21.1798365 23.05,21.1798365 C24.744,21.1798365 26.394,22.2643052 26.9715,23.7520436 L29.023,23.7520436 C29.606,22.2643052 31.256,21.1798365 32.95,21.1798365 C35.1445,21.1798365 36.8,22.8201635 36.8,24.9945504 C36.8,28.1444142 33.346,31.2506812 28.1155,35.9536785 L28.1155,35.9536785 Z</string>
          <string name="heart_full_path">M28,39 L26.405,37.5667575 C20.74,32.4713896 17,29.1089918 17,24.9945504 C17,21.6321526 19.6565,19 23.05,19 C24.964,19 26.801,19.8828338 28,21.2724796 C29.199,19.8828338 31.036,19 32.95,19 C36.3435,19 39,21.6321526 39,24.9945504 C39,29.1089918 35.26,32.4713896 29.595,37.5667575 L28,39 L28,39 Z</string>
          <string name="heart_clip_hidden">M18 37 L38 37 L38 37 L18 37 Z</string>
          <string name="heart_clip_shown">M0 0 L56 0 L56 56 L0 56 Z</string>
      
    • 定义AnimatedVectorDrawable

          <!--res/drawable/avd_heart_empty.xml-->
          <animated-vector
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:drawable="@drawable/ic_heart">
              <target
                  android:name="clip"
                  android:animation="@animator/empty_heart"/>
          <!--res/drawable/avd_heart_full.xml-->
          <animated-vector
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:drawable="@drawable/ic_heart">
              <target
                  android:name="clip"
                  android:animation="@animator/ful_heart"/>
    • 代码中使用AnimatedVectorDrawable
          ...
          private ImageView mHeartImg;
          private AnimatedVectorDrawable avdHeart;
      
          @Override
          public void onCreate(Bundle savedInstance){
              mHeartImg = (ImageView)findViewById(R.id.img_heart);
              avdHeart = (AnimatedVectorDrawable)getDrawable(R.drawable.avd_heart_full);
          }
      
          @Overide
          public void onClick(View v){
              mHeartImg .setImageDrawable(avdHeart);
              avdHeart.start();
          }
          ...
制作vector

先制作svg,再将svg转换成android的vector drawable.
1. 在线制作svg,
2. 在线svg转换为vector

;