目录
动画详解之视图动画

动画的结构脉络

1.动画的定义

动画就是在两个不同的场景之间,添加过渡,让过程看起来尽可能自然(也可以说和现实世界的物理规律相符合),常见的场景有

  • 表现层级关系:
    为了展现层与层的关系,是抽屉,是打开,还是平级切换等等,让用户知道这个界面和上一个、下一个的关系。这已经是非常最常见的运用了,如QQ侧滑打开个人中心

  • 与用户手势/遥控器 结合,更自然的动画表现 :
    当用户手势操作的时候,让界面的动态走向更符合手指的运动,从而让用户感觉到是自己控制了界面的动向,而不是机械化的跳转,通常会配合震动、视图的选中状态来表现

  • 减少不可避免的不适感:
    启动,下载,加载,刷新,发送 等界面,适当的动画效果让等待变的可视化,用户就没有那么无聊,产品体验也会上升;

  • 不易被察觉的动效
    1.比如B站的点赞特效,
    2.随着用户的操作,有的内容已经是用户不再关注的。这时候可以将他们隐藏起来,比如说标题栏。

2.动画的分类和基本使用

视图按照大类分为两类:ViewAnimation(视图动画)和 PropertyAnimator(属性动画);视图动画又包含Tween Animation(补间动画)和Frame Animation(帧动画),属性动画包含ValueAnimator 和 objectAnimator。

下面一个个来介绍一下他们的基本使用
先上效果图

2.1 补间动画--Alpha(透明度)

图片描述

2.1.1. 使用xml 实现Alpha动画:

达到View的透明度从某个值变化到某个值的效果,现在res文件夹下,新建一个文件夹anim,存放xml文件,以后的关于动画的xml文件都放在这个文件夹下,xml文件夹下

xml
复制代码
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="1.0"
    android:toAlpha="0.1"
    android:duration = "2000"
    android:fillAfter="false"
    android:fillBefore="true"
    >
</alpha>

然后再activity中可以这样使用

kotlin
复制代码
binding.btAlpha.setOnClickListener {
    //加载动画
    val animation = AnimationUtils.loadAnimation(this@AnimatorActivity,R.anim.alpha_anim)
    //调用startAnimation 开始动画
    //动画开始之后,会将imageview的透明度从1.0变为0.1,并保持不变
    binding.ivCat.startAnimation(animation)
}
  • android:fromAlpha 取值为0.0 - 1.0 ,0.0表示全透明,1表示不透明

  • android:toAlpha 取值为0.0 - 1.0,0.0表示全透明,1表示不透明

  • android:duration 表示持续时间,持续4s

  • android:fillBefore:设置为true,动画结束之后,复原到初始状态,默认值为true

  • android:fillAfter 设置为true,动画结束之后,保持动画结束时的状态,默认值为false

  • android:fillEnabled 与fillBefore效果相同,

  • 当fillBefore 和 fillAfter 同时设置为true 时,fillAfter的优先级高于fillBefore,动画将维持在最后一帧
    透明度效果图如下:

2.1.2. 使用纯代码的形式

AlphaAnimation的构造函数为

kotlin
复制代码
1.AlphaAnimation(context:Context!,attrs:AttributeSet!) //用于从xml中构建Animation对象
2.AlphaAnimation(fromAlpha:Float,toAlpha:Float,) //代码构建对象

因此可以像下面这样写

kotlin
复制代码
val animation  = AlphaAnimation(1.0f,0.2f)
animation.duration = 2000   // 设置动画持续时间
animation.fillAfter = true  //动画结束之后维持在最后一帧
binding.ivCat.startAnimation(animation) //开始动画

透明度效果图如下:

2.2 补间动画--Scale(缩放)

图片描述

2.2.1.使用xml实现scale动画,

scale标签用于缩放view,从而实现调整控件尺寸的效果

xml
复制代码
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="1.0"
    android:toXScale="1.4"
    android:fromYScale="1.0"
    android:toYScale="1.4"
    android:duration = "1000"
    android:fillAfter = "true">
</scale>
  • android:fromXscale 动画开始时,view在X轴上相对于自身的缩放比例,1.0 等于view 自身的大小,0.5为view 自身的0.5倍,2.0为view 自身的2 倍

  • android:toXscale 动画结束时,view在X 轴上相对于自身的缩放比例

  • android:fromYScale 动画开始时,view 在Y轴上相对于自身的缩放比例

  • android:toYScale 动画结束时,view在Y轴上相对于自身的缩放比例

  • 在scale 标签中,pivotX 和pivotY 指定动画的开始坐标,有3种不同的取值,20,20%,20%p,20表示动画从距离左上角20单位的距离开始,20%表示动画从距离左边 (view的宽度*0.2)的位置开始,20%p 表示从距离view的最左上角边(父容器的宽度或者高度 乘以 0.2)的开始,view 在

  • 在rotate 标签种,pivotX 和 pivotY 指定动画旋转中心的坐标

    上面整个xml 表示这个view 在动画开始时,从自身的1.0倍变化到自身的1.4倍,持续时间为1s,并在动画结束之后维持在最后一帧。

调用过程为

kotlin
复制代码
 binding.btScale.setOnClickListener {
       val animation = AnimationUtils.loadAnimation(this@AnimatorActivity,R.anim.scale_anim)
       binding.ivCat.startAnimation(animation)
 }

2.2.2.使用纯代码的形式实现scale动画

plaintext
复制代码
//ScaleAnimation对象 的构建方法有4个,
//第一个是我们从xml中加载animation时会用到的构造函数,
//第2,3,4是我们自己使用代码构建ScaleAnimation对象时会使用的构造方法。
1.ScaleAnimation(context:Context!,attrs:AttributeSet!) //用于从xml中构建Animation对象
2.ScaleAnimation(fromx:Float,toX:Float,fromY:Float,toY:Float)//动画默认从左上角开始
3.ScaleAnimation(fromX:Float,toX:Float,fromY:Float,toY:Float,pivotX:Float,pivotY:Float)//动画从开发者指定的位置开始
4.ScaleAnimation(fromX:Float,toX:Float,fromY:Float,toY:Float,pivotXType:Int,povitXValue:Float,
              povitYType:Int,povitYValue:Float)//动画从开发者指定的位置开始

在xml中 标签中的pivotx 中有3种取值模式,分别是数值,百分数,百分数p,体现在构建函数中就是用pivotXTYPE和pivotYTYPE 区分它们的类型,取值可以是下面这3种

Animation.ABSOLUTE,Animation_RELATEIVE_TO_SELF和 Animation_RELATIVE_TO_PARENT

xml
复制代码
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="1.0"
    android:toXScale="1.4"
    android:fromYScale="1.0"
    android:toYScale="1.4"
    android:povitX = "40%p"
    android:povitY = "40%p"
    android:duration = "1000"
    android:fillAfter = "true">
</scale>

上面标签用代码表示可以表示为

kotlin
复制代码
val scaleAnimation = ScaleAnimation(1.0f,1.4f,1.0f,1.4f,40%,RELETIVE_TO_PARENT,
                                   40%,RELETIVE_TO_PARENT)
scaleAnimation.apply{
    duration = 1000
    fillAfter = true
}
binding.ivCat.startAnimation(scaleAnimation) //开始动画

2.3 补间动画--translate(移动)

图片描述

Translate 主要用于控件位置的移动,可以实现类似于上滑解锁,滑块等动画

2.3.1 使用xml定义translate动画

xml
复制代码
<translate xmlns:android="http://schemas.android.com/apk/res/android"
           android:fromXDelta ="0"
           android:toXDelta = "-180"
           android:fromYDelta = "0"
           android:toYDelta = "100"
           android:duration = "2000"
           android:fillAfter = "true"
           />
  • fromXDelta 控件开始移动时左上角的位置的X轴坐标
  • fromYDelta 控件开始移动时左上角的位置的Y轴坐标
  • toXDelata 控件结束移动式左上角的位置的X轴坐标,负值代表向反方向移动,即向左移动,正值即向右边移动
  • toYDelata 控件结束移动式左上角的位置的Y轴坐标,负值代表向反方向移动,即向上移动,正值即向下边移动

2.3.2 使用代码实现translate动画

TranslationAnimation对象的构造方法有3个,分别是

kotlin
复制代码
Translation(context:Context!,attrs:AttributeSet!) //用于从xml中加载动画
Translation(fromXDelta:Float,toXDelta:Float,fromYDelta:Float,toYDelta:Float)
Translation(fromXType:Int,fromXValue:Float,toXType:Int,toXValue:Float,fromYType:Int,fromYValue:Float             toYType:Int,toYValue:Float)

fromXType,toXType,fromYType,toYType的取值同样是3个,分别是

  • RELATIVE_TO_SELF
  • RELATIVE_TO_PARENT
  • RELATIVE_TO_ABSOULTE

实现和上面xml一样效果可以这样写

xml
复制代码
val tranalationAnimation = TranslationAnimation(0f,-180f,0f,200f).apply{
    duration = 2000
    fillAfter =  true
}
binding.ivCat.startAnimation(animation)

2.4 补间动画--rotate(旋转动画)

图片描述

2.4.1 使用xml实现如下

xml
复制代码
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:toDegrees="650"
    android:pivotX="0" 
    android:pivotY="0"
    android:duration = "4000"
    android:fillAfter = "true"
    />

使用

kotlin
复制代码
binding.btRotate.setOnClickListener {
            val animation = AnimationUtils.loadAnimation(this@AnimatorActivity,R.anim.rotate_anim)
            binding.ivCat.startAnimation(animation)
        }
  • android:fromDegrees 旋转的开始角度

  • android:toDegrees 旋转的结束角度,正数为顺时针方向,负数为逆时针方向

  • pivotX 旋转动画的中心的横坐标,可以为整数,百分数,百分数+p,20,30%,30%p类似于上面的解释

  • pivotY 旋转动画的中心的纵坐标

  • android:interpolator 用于设定插值器,其实就是指定动画效果,比如先慢后快,弹跳效果

2.4.2 实现代码实现旋转动画

构造函数如下:

kotlin
复制代码
TranslateAnimation(context:Context!,attrs:AttributeSet) //从xml中加载动画
TranslateAnimation(fromDegrees:Float,toDegrees:Float)
TranslateAnimation(fromDegress:Float,toDegrees:Float,pivotX:Float,pivotY:Float)
TranslateAnimation(fromDegress:Float,toDegrees:Float,pivotXType:Int,pivotXValue:Float,pivotType:Int,pivotYValue:Float)

pivotXType和pivotYType的value的和上面TranslationAnimation的值相同

实现类似2.4.1的效果可以使用代码如下

kotlin
复制代码
val rotateAnimation = RotateAnimation(0f,-650f,Animation_TO_ABSOLUTE,0,Animation_TO_ABSOLUTE,0).apply{
    duration = 4000
    fillAfter = true
}

2.5 动画集合

图片描述
前面的动画只能完整特定的动画效果,而set标签可以将这些标签都包含在一个容器里面,一起生效,下面是一个例子

2.5.1 xml实现如下

xml
复制代码
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:repeatMode="reverse"
    android:duration="2000"
    >
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:repeatCount = "4"/>
    <scale
        android:fromXScale="0.5"
        android:toXScale="1.4"
        android:fromYScale="0.5"
        android:toYScale="1.4"
        android:pivotY="50%"
        android:pivotX="50%"
        android:repeatCount = "4"/>
    <rotate
        android:fromDegrees="0"
        android:toDegrees="710"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount = "4"
        />
</set>

上面的set 标签实现了透明度从0到1.0,长度和宽度从0.5拉伸到1.4倍,并旋转710,重复4次,单次持续时间为2s,重复模式为reverse

2.5.2 纯代码实现如下

kotlin
复制代码
val animationSet = AnimationSet(true)
              val alphaAnimation = AlphaAnimation(0f,1f).apply { repeatCount = 4 }
              val scaleAnimation = ScaleAnimation(0.5f,1.4f,0.5f,1.4f,
                  Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f).apply { repeatCount = 4 }
              val rotateAnimation = RotateAnimation(0f,710f,0.5f,0.5f).apply { repeatCount = 4 }
              animationSet.apply {
                    addAnimation(alphaAnimation)
                    addAnimation(scaleAnimation)
                    addAnimation(rotateAnimation)
                    duration = 2000
                    fillAfter = true
                    repeatMode = Animation.REVERSE  //重复时,从最终状态向开始状态变化

              }
             binding.ivCat.startAnimation(animationSet)

2.6 视图动画的其他方法

AlphaAnimation、ScaleAnimation、TranslateAnimation、RotateAnimation、AnimationSet都是Animation类的抽象类,Animation里面定义了一些公共方法

  • void cancel() 取消动画 ,例如 animationSet.cancel(),scaleAnimation.cancel()

  • void reset() 将view 重置到动画开始的状态

  • void setAnimationListener(Animation.AnimationListener listener) 设置动画监听,可以在某个时刻进行事件的处理

xml
复制代码
animationSet.setAnimationListener(object :Animation.AnimationListener {
                 override fun onAnimationStart(animation: Animation?) {
                     TODO("Not yet implemented")
                     //动画开始时
                 }

                 override fun onAnimationEnd(animation: Animation?) {
                     TODO("Not yet implemented")
                      //动画结束时
                 }

                 override fun onAnimationRepeat(animation: Animation?) {
                     //动画重复时
                 }

             })

2.7 视图动画的优缺点总结:

  • 优点:
    可以实现基本的缩放,旋转,平移,透明度变换,以及这四种变换的组
    合动画
  • 缺点:
    不能实现复杂动画效果。
    可以实现组合动画(AnimationSet),但是组合动画只能同时播,不能控制动画播放的先后顺序
    不能监听动画的执行过程,只能监听到动画开始,结束和重复事件。

2.8 视图动画的原理解析

2.8.1 动画开始的入口

kotlin
复制代码
 val translateAnimation = TranslateAnimation(0f,180f,0f,200f)
            translateAnimation.duration = 2000
            translateAnimation.fillAfter = true
    //动画开始  
          binding.ivCat.startAnimation(translateAnimation)

跟踪下去

kotlin
复制代码
// View.java中
 public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
       //设置关联动画
        setAnimation(animation);
      //用于指示此视图的父级应清除其缓存。此功能用于强制父级重建其显示列表(当硬件加速时),这在视图的各种父级管理属性发生更改时是必要的,例如 alpha、translationXY、scrollXY、scaleXY 和 rotationXY。此方法仅清除父缓存,不会导致无效事件
        invalidateParentCaches();
      //触发视图重绘
        invalidate(true);
    }

最后跟踪到view.java中

java
复制代码
void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }

        if (skipInvalidate()) {
            return;
        }

        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            //部分代码省略
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                //执行ViewParent的invalidateChild方法
                p.invalidateChild(this, damage);
            }
            //部分代码省略
        }
    }
}

注意到p的类型是ViewParent,是一个抽象类,我们找到它的子类ViewGroup
ViewGroup中

java
复制代码
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /***部分代码省略***/
    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            /***部分代码省略***/
            do {
                /***部分代码省略***/
                //向顶部的View便利找到根View,即:ViewRootImpl
                //执行ViewRootImpl的invalidateChildInParent方法
                parent = parent.invalidateChildInParent(location, dirty);
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) (boundingRect.left - 0.5f),
                                (int) (boundingRect.top - 0.5f),
                                (int) (boundingRect.right + 0.5f),
                                (int) (boundingRect.bottom + 0.5f));
                    }
                }
                /***部分代码省略***/
            } while (parent != null);
        }
    }
}

这里调用了函数invalidateChildInParent(),需要注意的是这里这个函数的实现有两个,一个是ViewGroup中,而另一个是ViewRootImpl。
ViewRootImpl中:

java
复制代码
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {  
    /***部分代码省略***/

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        /***部分代码省略***/
        invalidateRectOnScreen(dirty);
        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        /***部分代码省略***/
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            //开始View的绘制任务
            scheduleTraversals();
        }
    }
}

函数scheduleTraversals()的逻辑其实是执行一个Runnable,而这个Runnable其实就是去执行函数doTraversal(),而函数doTraversal()会调用performTraversals(),到这里我们发现它开始重绘了。总体来说就是动画的执行会导致整个View Tree重绘,但是Android内部有一些优化,比如一张图片做移动,我们不需要真正的去重新绘制,Android内部提供缓存机制,不会显示的再调用onDraw(canvas)函数

java
复制代码
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {    
    //部分代码省略
    public void draw(Canvas canvas) {
        /***部分代码省略***/
        //如果有子 View(DecorView当然有子View),就会调用dispatchDraw() 将绘制事件通知给子 View。
        //ViewGroup 重写了 dispatchDraw(),调用了 drawChild()
        //drawChild() 调用了子 View 的 draw(Canvas, ViewGroup, long)
    }
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /***部分代码省略***/
        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
            /***部分代码省略***/
        }
        /***部分代码省略***/
        // 动画数据应用在RenderNode或者Canvas上的!!!!
        if (transformToApply != null) {
            if (concatMatrix) {
                if (drawingWithRenderNode) {
                    // 应用动画数据
                    renderNode.setAnimationMatrix(transformToApply.getMatrix());
                } else {
                    canvas.translate(-transX, -transY);
                    // 应用动画数据
                    canvas.concat(transformToApply.getMatrix());
                    canvas.translate(transX, transY);
                }
                parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
            }
    
            float transformAlpha = transformToApply.getAlpha();
            if (transformAlpha < 1) {
                // 应用动画数据
                alpha *= transformAlpha;
                parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
            }
        }
    }

    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        /***部分代码省略***/
        //绘制动画的当前帧,并获取当前动画的状态(是否继续运行)
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }
        //如果动画没有结果
        if (more) {
            if (!a.willChangeBounds()) {
                if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                        ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                    parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
                } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                    // The child need to draw an animation, potentially offscreen, so
                    // make sure we do not cancel invalidate requests
                    parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    //进行绘制
                    parent.invalidate(mLeft, mTop, mRight, mBottom);
                }
            } else {
                /***部分代码省略***/
                //进行绘制
                parent.invalidate(left, top, left + (int) (region.width() + .5f),
                        top + (int) (region.height() + .5f));
            }
        }
        return more;
    }
}

View.draw(Canvas)
—> ViewGroup.dispatchDraw(Canvas)
—> ViewGroup.drawChild(Canvas, View, long)
—> View.draw(Canvas, ViewGroup, long)
—> View.applyLegacyAnimation(ViewGroup, long, Animation, boolean)

2.2.3 计算动画进度,进行矩阵变换

java
复制代码
public abstract class Animation implements Cloneable { 
    /***部分代码省略***/
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        /***部分代码省略***/
        //执行时间是否过期
        final boolean expired = normalizedTime >= 1.0f;
        mMore = !expired;
        //动画进度为0.0~1.0之间
        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            /***部分代码省略***/
            //插值器计算动画执行进度
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            //真正的动画效果代码执行处(通过矩阵变化)
            applyTransformation(interpolatedTime, outTransformation);
        }
        //如果动画绘制完成
        if (expired) {
            //判断动画是否需要继续循环
            if (mRepeatCount == mRepeated) {
                if (!mEnded) {
                    mEnded = true;
                    guard.close();
                    fireAnimationEnd();
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                fireAnimationRepeat();
            }
        }
        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }
        return mMore;
    }
}

以RotateAnimation动画为例,分析它的applyTransformation方法

java
复制代码

 @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
        float scale = getScaleFactor();
        
        if (mPivotX == 0.0f && mPivotY == 0.0f) {
    //获取绘制矩阵,设置旋转角度
            t.getMatrix().setRotate(degrees);
        } else {
            t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
        }
    }

说明:Animation的运行依赖Android本身的机制回调(每帧都得回调getTransformation和applyTransformation)无法自身进行运算计算fraction,并且可参与运算的只有Transformation对象里的alpha和matrix,所以Animation只能实现简单的Alpha,Scale,Translate,Rotate变换效果。

3 逐帧动画

逐帧动画是指一帧一帧的播放动画,每一帧由一个图片组成,

3.1xml 定义和使用

xml
复制代码
<?xml version = "1.0" encoding = "utf-8"?>
<animation-list xmlns:android = "http//schemas.android.como/apk/res/android"
android:oneshot= "true" || "false"
>
   <item 
	android:drawable="@drawable/drawable_name_1
	android:duration = "1000"
  <item 
	android:drawable="@drawable/drawable_name_2
	android:duration = "1000"
>
</animation-list>
  • 根元素为animation-list,是必须的,可以包含一个或者以上的item,
    android:oneshot = "true"动画只会执行一次,false则会一直循环
  • item 中 drawable指向资源文件的名字,duration 指的是持续时间,第一个item的持续时间完了,就播放第二个item,以此类推

然后通过ImageView 设置动画资源,通常有两种方法设置imageView的背景,一是android:src ;而是android:background
在imageview中设置第一帧的图片

xml
复制代码
<ImageView
  android:layout_width = "match_parent"
  android:layout_height = "match_parent"
   android:background = "@drawable/drawable_name_1"
/>

或者是

xml
复制代码
<ImageView
  android:layout_width = "match_parent"
  android:layout_height = "match_parent"
   android:src = "@drawable/drawable_name_1"
/>

接着在代码处调用

kotlin
复制代码
val imageView = findViewById<ImageView>(R.id.frame_image)
val animation = image.background
//或者是(src 对应drawable ;background 对应backround)
val animation = image.drawable
animation.start() //开始动画

3.2 使用代码实现

使用代码时,我们要用到的是AnimationDrawable类,常用方法有

  • void start() //开始播放逐帧动画
  • void stop() //停止播放
  • int getDuration(int index) //获取第index帧的持续时间
  • Drawable getFrame(int index) //获取指定index帧对应的drawable对象
  • int getNumberOfFrames() 获取帧数
  • boolean isRunning() 动画是否在播放
  • void setOneShot(boolean oneShot) true为播放一次,false循环播放
  • boolean isOneShot() 判断动画的播放类型,是播放一次还是循环播放的
  • void addFrame(Drawable frame,int duration) 为动画添加一帧,表明持续时间

代码实现如下

kotlin
复制代码
val imageview = findViewById<ImageView>(R.id.frame_image)
val anim = AnimationDrawable()
for(i in 1..4){
val id = resources.getIdentifier("drawable_name_$i","drawable",packageName)
val drawable = resouces.getDrawable(id)
anim.addFrame(drawable,1000)
}
anim.isOneShot = false
imageview.setBackgroundDrawable(anim)
anim.start()

ps:objectValue和ValueObject的后面另起一篇文章太长了

本文由 波鲁萨利诺.黄猿 原创发布于 阳光沙滩 , 未经作者授权,禁止转载
评论
0 / 1024
用户头像
有没有自由落体运动的动画 用属性动画吗
2022-08-18 06:37回复
回复@阿肥: 是的,争取明天之前出来吧,加个自由落体[doge]
2022-08-18 07:59回复
回复@波鲁萨利诺.黄猿: [墨镜]请call 我 或者发新文章
2022-08-18 08:02回复
用户头像
顶
2022-08-18 05:03回复
用户头像
像这种是大作文了!
2022-08-18 02:06回复
推荐文章
一篇文章讲清楚雅思、托福、GRE、SAT、GMAT:它们是什么?有什么区别?应该考哪个?
面对IELTS、TOEFL、GRE、SAT和GMAT,你是否感到困惑?本文深度解析五大考试的核心区别:从语言证明到学术能力评估,明确各自适用人群与场景。特别更新2026年托福改革及最新评分体系,通过对比表格与真实申请案例,助你精准定位目标考试,避免无效备考,高效规划海外升学之路。
一篇文章讲清楚 Android 签名:PK8、X509.PEM、JKS 是什么?它们有什么关系?
深入解析Android应用与系统签名的核心文件:PK8、X509.PEM与JKS。本文从非对称加密原理出发,厘清私钥、证书与密钥库的本质区别及相互转换方法,涵盖AOSP平台签名与普通应用发布的实际场景。通过对比V1-V4签名方案及解答常见疑难,助你掌握保障APK完整性与身份认证的关键技术,规避签名丢失或冲突风险。
系统监控运维系统
你是否也深受证书过期、磁盘满载等运维痛点的折磨?排查耗时,影响线上体验。本文分享一款自研运维系统面板,集成证书统一监测、资源预警及WebSSH等功能,实现问题快速定位与处理。附代码共享链接,欢迎参考或二次开发,提升运维效率。
断点说搞个万年历,怎么搞?
想快速拥有一个专属的程序员日历?只需一篇清晰的设计文档,剩下的交给AI!本文带你从零搭建“sob-calendar”项目,集成农历、节气及独特的程序员运势功能。通过实际案例展示如何利用AI高效生成代码并部署上线,让繁琐的开发工作变得轻松有趣,快来体验科技与传统历法的奇妙碰撞吧!
linux服务器的日志、容器清理
本手册提供了一套全面的Linux服务器磁盘清理与排查方法,帮助用户高效管理磁盘空间。通过查看磁盘使用情况、分析Docker占用、清理无用容器和日志,确保系统运行流畅。适合需要优化服务器性能的运维人员参考。
记给网站接入图形验证码
本文分享了如何为个人站点添加图形验证码以提升安全性,详细介绍了使用第三方接口实现验证码功能的过程,并通过代码示例展示了具体实现方式。对于关注网站安全的开发者来说,这篇文章提供了实用的参考价值。
Java开发环境搭建
本文是一份详细的Java开发环境搭建指南,涵盖了Java的发展历史、版本选择及安装配置过程。无论你是初学者还是有经验的开发者,都能从中找到实用的信息和步骤,帮助你快速上手Java开发。
Vue3.js项目的创建
本文详细介绍了如何使用Vue3创建项目,包括Node.js的安装和配置,以及项目的初始化过程。适合初学者学习如何快速搭建Vue3开发环境,并推荐了相关课程供进一步学习。
Network Tools : 您的运维“助手”升级了
Network Tools是一款专为解决运维难题而设计的软件,集设备管理、配置备份、性能监控和告警处理于一体。通过统一的工作台,帮助运维人员更高效地管理资产、定位故障并提升团队协作效率。无论是设备在线状态还是具体问题排查,都能得到全面支持。
Ubuntu 软件安装与彻底卸载指南
本文详细介绍了在 Ubuntu 系统中如何规范安装和彻底卸载 .deb 格式软件,包括依赖修复、残留清理以及图形化工具推荐,适合需要维护系统环境的用户。
JavaWeb开发客户程序更新特殊环境下的骚操作
本文分享了在无外网环境下,如何利用Docker和docker-compose实现高效部署的实战经验。通过基础镜像和本地jar包挂载的方式,解决了内网环境下的依赖问题,适合需要灵活部署方案的技术人员参考。
Linux 防火墙完全指南:从原理到多发行版实战
本文全面解析Linux防火墙的底层原理与主流工具配置,涵盖UFW、firewalld、iptables、nftables等方案,适合不同发行版用户。从基础概念到生产环境加固建议,助你掌握核心安全策略,确保服务器与网络的安全运行。
水上派对英语单词篇
探索FLOATOPIA水上派对的完整指南,涵盖签到流程、场地指引、安全须知、活动安排及夜场舞会等实用英语词汇与对话。无论是初学者还是老手,都能轻松掌握关键术语,享受完美水上体验。
麒麟操作系统下使用 virt-customize 重置虚拟机 root 密码实战
本文详细介绍了在麒麟操作系统环境下使用 virt-customize 工具重置 KVM 虚拟机 root 密码的完整流程,包含安装步骤、常见问题及解决方案,帮助运维人员高效处理密码遗忘或批量初始化场景,避免数据丢失。
浅析JS预编译-函数篇
本文详细讲解了JavaScript中函数的预编译过程,包括AO对象的创建、变量和函数声明的提升,以及执行阶段的变化。适合初学者了解JS执行机制,帮助深入理解代码运行逻辑。
访问Github的另一种姿势
本文介绍了如何通过Watt ToolKit等工具加速访问Github,适合对网络技术感兴趣的读者。内容涵盖工具介绍、下载和使用方法,以及一些实用技巧,值得一看。
使用Gulp压缩JS
本文详细介绍了如何使用Gulp压缩JavaScript文件,特别是支持ES6语法的处理方法。通过实际操作和效果对比,帮助开发者提升代码性能,适合对前端自动化工具感兴趣的读者。
使用Gulp压缩CSS
本文详细介绍了如何使用Gulp工具对CSS文件进行压缩,提升网页加载速度。适合需要优化静态资源的开发者,提供清晰的步骤和示例,帮助快速上手。
从0部署Nuxtjs项目
本文详细介绍了如何将Nuxt.js项目部署到公网,涵盖Node.js安装、环境配置、源码上传及使用PM2启动服务的全过程,适合开发者学习和实践。
Linux 命令行美化利器:tree 命令完全指南(安装 + 全部参数详解)
掌握 Linux 下的 `tree` 命令,快速直观地查看目录结构。本文详细讲解安装方法、参数含义及实战场景,适合开发者和系统管理员提升工作效率。
Linux常用命令(一)
本文详细介绍了Linux基础命令,包括文件操作、系统显示、网络状态、软件包管理等,适合初学者学习和查阅,是掌握Linux系统的实用指南。
大模型推理参数解码:温度、Top-p 与核采样如何塑造生成文本的灵魂
探索大语言模型的采样参数如何塑造生成内容,从温度到Top-p,从惩罚机制到熵控制,揭示背后的信息论原理与实践策略。了解如何通过参数调优,让AI既保持知识的严谨,又拥有创造力的自由。
记从0使用Claude code写代码
本文介绍了如何使用Claude Code搭配DeepSeek进行编程,详细讲解了安装Node.js、全局安装Claude、配置模型以及使用方法。对于开发者来说,这是一份实用的技术教程,尤其适合对AI编程工具感兴趣的读者。
Android-Studio-Gradle同步失败-Library为null的通用排查指南
遇到 Android Studio Gradle Sync 失败时,不要轻易删除全局缓存。本文详细解析了错误原因,并提供了一系列精准的排查步骤和解决方案,帮助开发者快速定位并解决问题,提升开发效率。
PHP实现密码加密
本文详细介绍了Bcrypt密码加密技术及其在PHP中的应用,帮助开发者提升用户密码的安全性。通过实际代码示例,展示了如何使用password_hash和password_verify函数进行密码加密与验证,是学习网络安全知识的实用指南。
WordCloud效果,滚动标签
本文详细介绍了如何在Vue项目中集成WordCloud组件,包括依赖安装、属性配置和使用方法。适合开发者学习如何实现数据可视化功能,提升项目交互体验。
从0使用WordPress搭建一个优美的网站
本文详细介绍了如何使用WordPress搭建一个美观的网站,从安装到主题配置和功能拓展,为读者提供了实用的操作指南。无论是初学者还是有一定经验的开发者,都能从中获得有价值的参考。
JS实现在网站底部添加运行时间
想知道如何在网站底部显示运行时间?本文详细讲解了通过JavaScript实现这一功能的方法,包括时间计算逻辑和代码实现。适合前端开发者学习参考,轻松为网站添加实用功能。
在Vercel上部署Hexo博客
本文详细介绍了如何在Vercel上快速部署Hexo博客,无需后端服务即可实现高效发布。相比传统方式,省去了手动生成和上传静态文件的步骤,更加便捷。适合想要搭建个人博客的开发者参考。
从0搭建一个Hexo博客
本文详细介绍了Hexo博客框架的使用方法,从安装到部署全流程讲解,适合想快速搭建个人博客的技术爱好者。内容清晰易懂,是入门Hexo的理想指南。