复杂动画设计-Android动画分类

1,328 阅读4分钟

1. Android动画分类:

  • View动画
  • 帧动画
  • 属性动画
1. View动画
  1. View动画的作用对象是View
  2. View动画是对View的影像做动画,并不会真正地改变View的状态(如View的可见性,View的位置等).
    • 有时候出现动画完成后View无法隐藏问题,即view.setVisibility(View.GONE)失效,这时调用view.clearAnimation()清除View动画即可解决.
  3. View动画包含4种
    • 平移动画 TranslateAnimation
    • 缩放动画 ScaleAnimation
    • 旋转动画 RotateAnimation
    • 透明度动画 AlphaAnimation
  4. View动画的几个方法
    • setFillBefore
      • View动画结束后,是否还原到动画开始前的状态
    • setFillEnabled
      • 经试验效果同 setFillBefore
    • setFillAfter
      • View动画结束后,是否保持在动画最后的状态
    • setZAdjustment : 没有试验出什么效果
    • setHasRoundedCorners : 不知道怎么使用
    • <View
          android:id="@+id/v1"
          android:layout_width="200dp"
          android:layout_height="200dp"
          android:background="@color/colorPrimary"
          />
      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="horizontal"
          android:gravity="center_vertical"
          android:background="@android:color/transparent"
          >
          <Button
              android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="1.0"
              android:layout_margin="2dp"
              android:background="@color/colorAccent"
              android:text="setFillAfter true"
              android:onClick="setFillAfterTrue"
              />
          <Button
              android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="1.0"
              android:layout_margin="2dp"
              android:background="@color/colorAccent"
              android:text="setFillBefore true"
              android:onClick="setFillBeforeTrue"
              />
          <Button
              android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="1.0"
              android:layout_margin="2dp"
              android:background="@color/colorAccent"
              android:text="setFillEnabled true"
              android:onClick="setFillEnabledTrue"
              />
      </LinearLayout>
      
      private ScaleAnimation gainScaleAnimation(){
          ScaleAnimation scaleAnimation = new ScaleAnimation(0.5F,1.5F,0.5F,1.5F);
          scaleAnimation.setDuration(4000);
          return scaleAnimation;
      }
      public void setFillAfterTrue(View view) {
          v1.clearAnimation();
          ScaleAnimation scaleAnimation = gainScaleAnimation();
          //经试验,单独使用 setFillAfter(true) ,以及使用 setFillAfter(true) + setFillEnabled(true),
          //效果是一致的,都会保持动画最后的状态
          scaleAnimation.setFillAfter(true);
          scaleAnimation.setFillEnabled(true);
          v1.startAnimation(scaleAnimation);
      }
      public void setFillBeforeTrue(View view) {
          v1.clearAnimation();
          ScaleAnimation scaleAnimation = gainScaleAnimation();
          scaleAnimation.setFillBefore(true);
          v1.startAnimation(scaleAnimation);
      }
      public void setFillEnabledTrue(View view) {
          v1.clearAnimation();
          ScaleAnimation scaleAnimation = gainScaleAnimation();
          scaleAnimation.setFillEnabled(true);
          v1.startAnimation(scaleAnimation);
      }
      
  5. TimeInterpolator/插值器
    • A time interpolator defines the rate of change of an animation.
    • 插值器定义了 动画执行时间 和 动画执行完成度 的对应关系.Android提供了多种实现,不详述.
  6. View动画的特殊使用场景
    1. LayoutAnimation/布局动画
      • LayoutAnimation用于为ViewGroup的子View添加出场效果.
      • LayoutAnimation可以通过在xml中实现,也可以通过java代码实现
        //通过在布局文件中引用anim文件夹下定义的LayoutAnimation
        <layoutAnimation 
            android:delay="500"
            android:animation="@anim/customanim"
            android:animationOrder="normal"
            xmlns:android="http://schemas.android.com/apk/res/android" />
        <ListView
            *
            android:layoutAnimation="@anim/anim_layout"/>
        
        //通过LayoutAnimationController实现ViewGroup的布局动画
        ListView lv;
        Animation anim = AnimationUtils.loadAnimation(this,R.anim.customanim);
        LayoutAnimationController controller = new LayoutAnimationController(anim);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
        lv.setLayoutAnimation(controller);
        
      • 无论是通过xml,还是通过java代码实现,本质都是通过LayoutAnimationController.见ViewGroup源码:
        public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            initViewGroup();
            //解析xml中设置的属性
            initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
        }
        private void initFromAttributes(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            ***
            //解析到xml中设置了 android:layoutAnimation ,则生成LayoutAnimationController实例,执行 setLayoutAnimation
            case R.styleable.ViewGroup_layoutAnimation:
                int id = a.getResourceId(attr, -1);
                if (id > 0) {
                    setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, id));
                }
                break;
            ***
        }
        public void setLayoutAnimation(LayoutAnimationController controller) {
            mLayoutAnimationController = controller;
            if (mLayoutAnimationController != null) {
                mGroupFlags |= FLAG_RUN_ANIMATION;
            }
        }
        
      • LayoutAnimation系统默认提供3种子元素动画执行顺序: 顺序执行/normal,逆向执行/reverse,随机顺序执行/random.
      • LayoutAnimationController通过执行getTransformedIndex获取当下要为哪个索引值对应的Item执行动画.
      • 通过重写getTransformedIndex,我们可以实现任意顺序的ViewGroup布局动画.
    2. Activity的切换效果
      • overridePendingTransition(int enterAnim, int exitAnim)
      • overridePendingTransition必须位于startActivity或finish后面,否则不生效.
2. 帧动画
  1. 帧动画是按照顺序播放一组预先定义好的图片,类似于电影播放.

  2. 可以使用xml,也可以使用java代码创建

    //在drawable文件夹下创建animation-list
    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
        <item
            android:drawable="@mipmap/ii1"
            android:duration="@android:integer/config_shortAnimTime" />
        <item
            android:drawable="@mipmap/ii2"
            android:duration="@android:integer/config_shortAnimTime" />
        <item
            android:drawable="@mipmap/ii3"
            android:duration="@android:integer/config_shortAnimTime" />
        <item
            android:drawable="@mipmap/ii4"
            android:duration="@android:integer/config_shortAnimTime" />
        <item
            android:drawable="@mipmap/ii5"
            android:duration="@android:integer/config_shortAnimTime" />
    </animation-list>
    //在布局文件中引用
    <View
        android:id="@+id/v1"
        android:layout_width="500px"
        android:layout_height="500px"
        android:background="@drawable/bg_animation_list"
        />
    //在布局文件中引用后,依然需要在java代码中手动开启动画
    View v1 = findViewById(R.id.v1);
    //1:XML中指定的AnimationDrawable需要手动开启
    ((AnimationDrawable)v1.getBackground()).start();
    
    View v2 = findViewById(R.id.v2);
    AnimationDrawable anim = new AnimationDrawable();
    anim.setOneShot(false);
    anim.addFrame(getResources().getDrawable(R.mipmap.ii1),200);
    anim.addFrame(getResources().getDrawable(R.mipmap.ii2),200);
    anim.addFrame(getResources().getDrawable(R.mipmap.ii3),200);
    anim.addFrame(getResources().getDrawable(R.mipmap.ii4),200);
    anim.addFrame(getResources().getDrawable(R.mipmap.ii5),200);
    v2.setBackgroundDrawable(anim);
    anim.start();
    

    AnimationDrawable录屏.gif

  3. 对于AnimationDrawable,即使是在drawable文件下定义的animation-list在布局文件xml中被引用,也需要在Java代码中手动开启才会执行动画!

  4. 帧动画在使用的图片较多,且图片较大时易出现OOM,要尽量规避.如果一定要用,可以使用如下优化方案

    //按照文章思路,自己简单写了1个
    public class AnimationsContainer {
        private Context context = null;
        private int arraysId = -1;
        private ImageView targetImageView;
        private int delayPerFrame = -1;
        private Bitmap lastBitmap = null;
        private int index = -1;
        private int[] drawables = null;
        private Handler handler = null;
        private boolean started = false;
        private Thread gainBitmapThread = null;
    
        public AnimationsContainer(Context context, int arraysId, ImageView targetImageView, int delayPerFrame) {
            this.context = context;
            this.arraysId = arraysId;
            this.targetImageView = targetImageView;
            this.delayPerFrame = delayPerFrame;
            initHandler();
            drawables = getData(this.arraysId);
        }
    
        private void initHandler() {
            handler = new Handler(Looper.getMainLooper()) {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    Bitmap bitmap = (Bitmap) msg.obj;
                    targetImageView.setImageBitmap(bitmap);
                    recycleCurrBitmap();
                    lastBitmap = bitmap;
                }
            };
        }
    
        private void recycleCurrBitmap() {
            if (lastBitmap != null) {
                lastBitmap.recycle();
                lastBitmap = null;
            }
        }
    
        private int getNextIndex() {
            int result = index;
            if (result < 0) {
                result = 0;
            } else {
                result = (result + 1) % drawables.length;
            }
            return result;
        }
    
        private Bitmap gainSpecificSizeBitmap(int index) {
            Bitmap bitmap = null;
            bitmap = BitmapFactory.decodeResource(context.getResources(), drawables[index]);
            return bitmap;
        }
    
        public void startAnimation() {
            started = true;
            if (gainBitmapThread == null) {
                gainBitmapThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (started) {
                            index = getNextIndex();
                            Bitmap bitmap = gainSpecificSizeBitmap(index);
                            Message message = Message.obtain(handler, 0, bitmap);
                            handler.sendMessage(message);
                            try {
                                Thread.sleep(delayPerFrame);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            if (index >= drawables.length - 1) {
                                stopAnimation();
                                break;
                            }
                        }
                    }
                });
            }
            gainBitmapThread.start();
        }
    
        public void stopAnimation() {
            started = false;
            handler.removeCallbacksAndMessages(null);
            try {
                if (gainBitmapThread != null) {
                    gainBitmapThread.stop();
                    gainBitmapThread = null;
                }
            } catch (Exception e) {
                gainBitmapThread = null;
            }
            index = -1;
            recycleCurrBitmap();
        }
    
        /*
        <?xml version="1.0" encoding="utf-8"?>
        <resources>
            <string-array name="loading_anim">
                <item>@drawable/img0</item>
                <item>@drawable/img1</item>
                <item>@drawable/img2</item>
                <item>@drawable/img3</item>
                <item>@drawable/img4</item>
                <item>@drawable/img5</item>
                <item>@drawable/img6</item>
                <item>@drawable/img7</item>
                <item>@drawable/img8</item>
                <item>@drawable/img9</item>
                <item>@drawable/img10</item>
                <item>@drawable/img11</item>
                <item>@drawable/img12</item>
                <item>@drawable/img13</item>
                <item>@drawable/img14</item>
                <item>@drawable/img15</item>
                <item>@drawable/img16</item>
                <item>@drawable/img17</item>
                <item>@drawable/img18</item>
                <item>@drawable/img19</item>
                <item>@drawable/img20</item>
                <item>@drawable/img21</item>
                <item>@drawable/img22</item>
                <item>@drawable/img23</item>
                <item>@drawable/img24</item>
            </string-array>
        </resources>
        */
    
        /**
         * 从xml中读取帧数组
         *
         * @param resId
         * @return
         */
        private int[] getData(int resId) {
            TypedArray array = context.getResources().obtainTypedArray(resId);
            int len = array.length();
            int[] intArray = new int[array.length()];
    
            for (int i = 0; i < len; i++) {
                intArray[i] = array.getResourceId(i, 0);
            }
            array.recycle();
            return intArray;
        }
    }
    //使用方法
    AnimationsContainer controller = new AnimationsContainer(this,R.array.loading_anim,imageView,20);
    controller.startAnimation();
    
3. 属性动画
  1. 属性动画没有找到确切的定义.自己理解:
    • 属性动画定义了一个指定类型的属性P的变化
    • 属性P可以属于某一个对象,也可以脱离具体对象单独存在
      • 属于某个对象的指定类型的属性P的变化: ObjectAnimator
      • 脱离具体对象的指定类型的属性P的变化: ValueAnimator
  2. 属性动画按照使用场景分类
    • ViewPropertyAnimator
    • ObjectAnimator
    • ValueAnimator
  3. ViewPropertyAnimator专门针对View做属性动画,可以操作的属性是指定的,数量有限,但是使用非常方便.
    View target = **;
    tareget.animate().scaleX(1.50F).alpha(0.70F);
    
  4. ObjectAnimator
    • ObjectAnimator使用方式
      CustomView cv = **;
      ObjectAnimator anim = ObjectAnimator.ofFloat(cv,"prop1",1.0F,2.0F);
      anim.start();
      
      public class CustomView extends View{
          //字段是 'prop2'
          public String prop2 = null;
          //方法是 'Prop1'
          public void setProp1(String prop){
              this.prop2 = prop;
              invalidate();
          }
          public void getProp1(){
              return this.prop2;
          }
          ***
      }
      
    • 从上述示例可见ObjectAnimator特性
      • 针对特定对象 target
      • ObjectAnimator.of** 中的propertyName,并不需要是 target 的1个字段.只要存在propertyName对应的setter及getter方法即可.
    • 使用Keyframe,PropertyValuesHolder可以实现'对复杂属性关系做动画'
      • Keyframe用于把1个属性拆分为多段,对1个属性进行精细的控制
      • 1个PropertyValuesHolder控制1个属性,使用多个PropertyValuesHolder创建ObjectAnimator实例,可以对多个属性同时做动画
        public class CustomView extends View{
            public float a1 = 0.0F;
            public float a2 = 0.0F;
            public float a3 = 0.0F;
            //a1,a2,a3的setter及getter方法
            //setter方法会触发CustomView实例的重绘/invalidate
        }
        
        CustomView cv = ***;
        Keyframe kf1 = Keyframe.ofFloat(0.0F,0.0F);
        Keyframe kf2 = Keyframe.ofFloat(0.5F,120.0F);
        Keyframe kf2 = Keyframe.ofFloat(10,10.0F);
        PropertyValuesHolder holder1= PropertyValuesHolder.ofKeyframe("a1", kf1, kf2, kf3);
        ***
        PropertyValuesHolder holder2= PropertyValuesHolder.ofKeyframe("a2", kf4, kf5, kf6);
        ***
        PropertyValuesHolder holder3= PropertyValuesHolder.ofKeyframe("a3", kf7, kf8, kf9);
        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(cv, holder1,holder2,holder3);
        animator.start();
        
  5. ValueAnimator
    • ValueAnimator是ObjectAnimator的父类.
    • ValueAnimator用于声明1个脱离具体对象的指定类型属性的变化.
    • ValueAnimator因为不与具体实例关联,所以需要在动画监听器回调中获取当下属性值,并执行具体逻辑,相对于ObjectAnimator稍微麻烦,但是也没有束缚,适用范围更广.
    • ValueAnimator使用场景:
      • 简单来说,ObjectAnimator实现不了的场景,交给ValueAnimator.
        比如1个第三方库中的某个字段,没有setter方法,或想用1个动画控制多个对象的属性变化等等.

N. 参考资料