Android动画

Android开发中我们经常使用动画来实现复杂的动态效果,本文通过记录和梳理动画相关使用和原理,以巩固相关知识点。

动画分类

Android中动画分为三类,View动画、帧动画和属性动画,从实现原理上来说View动画和帧动画属于同一类。

View动画

View动画的抽象类是Animation,其有四个基础实现类,分别对应平移(TranslateAnimation)、旋转(RotateAnimation)、缩放(ScaleAnimation)、透明度(AlphaAnimation).

使用

把一个view的透明度在400ms内从0变化到1的动画如下:

java 复制代码
    AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
    alphaAnimation.setDuration(400);
    view.startAnimation(alphaAnimation);
原理

view动画的原理我们主要跟踪View使用过程来分析,以前文透明度动画的使用为例,其最后调用了view的startAnimation方法,参数为alphaAnimation

java 复制代码
	public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }
    public void setAnimation(Animation animation) {
        mCurrentAnimation = animation;
        ///****省略部分代码
    }

上述代码主要做了三件事:

  1. 调用animation的setStartTime方法,该方法实际上只是设置了Animation对象的成员变量状态
java 复制代码
 	public void setStartTime(long startTimeMillis) {
        mStartTime = startTimeMillis;
        mStarted = mEnded = false;
        mCycleFlip = false;
        mRepeated = 0;
        mMore = true;
    }
  1. 将动画赋值给view的mCurrentAnimation
  2. 调用invalidate请求重绘

可见View的startAnimation方法并没有直接开启动画,那么动画从哪里开始呢?我们查找下View的mCurrentAnimation的使用的地方,在View的draw方法里。

java 复制代码
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

        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);
            //获取动画对应的转换对象,
            transformToApply = parent.getChildTransformation();
        } 
       	 if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                        //通过动画转换器获取View渲染相关Matrix,通过Matrix来修改View的效果
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            canvas.translate(-transX, -transY);
                            //通过动画转换器获取View渲染相关Matrix,通过Matrix来修改View的效果
                            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) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            //初始化动画
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
             //注入动画涉及到的Handler**重要
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            //回调动画开始的接口
            onAnimationStart();
        }

        final Transformation t = parent.getChildTransformation();
        //根据时间获取当前转换相关状态,这个方法内部会根据时间计算对应的动画值,并设置给Transformation,这个Transformation的相关属性会作用于View。
        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) {
            //**** 计算需要绘制的脏区域,并发起重绘请求,注意这里本来就是在View的draw方法中调用,这里再发起重绘以驱动View的动画继续向后执行
            parent.invalidate(left, top, left + (int) (region.width() + .5f),
                        top + (int) (region.height() + .5f));
        }
        return more;
    }
注意事项
  1. View动画无法改变View的自身位置,只是改变了View的内容的位置,所以移动动画后的View所在的位置无法响应点击时间,因为此时View的位置还在原来
  2. 动画不需要的时候记得要取消

属性动画

使用

把一个view的透明度在400ms内从0变化到1的属性动画实现如下:

java 复制代码
ObjectAnimator.ofFloat(mRlApplyAll, "alpha", 0f, 1f).start();
原理

属性动画的原理我们主要跟踪使用过程来分析,以前文透明度动画的使用为例

java 复制代码
	public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
	    //创建ObjectAnimator对象并设置值
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setFloatValues(values);
        return anim;
    }
    
	public void start() {
	    //如果这个动画还在执行中,则先取消
        AnimationHandler.getInstance().autoCancelBasedOn(this);
        //调用父类的start方法,父类的start会调用其重载的start(boolean)方法
        super.start();
    }
java 复制代码
 	private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            //必须在有looper的线程
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        //初始化变量
        mReversing = playBackwards;
        mSelfPulse = !mSuppressSelfPulseRequested;
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        //注意这个方法,很重要
        addAnimationCallback(0);

        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            //调用startAnimation方法,该方法只是初始化了动画,并回调了onAnimationStart方法
            startAnimation();
         }
    }

    private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        //将当前Animation注册到AnimationHandler中,
        getAnimationHandler().addAnimationFrameCallback(this, delay);
    }
    
	private void startAnimation() {
        mAnimationEndRequested = false;
        initAnimation();
        mRunning = true;
        if (mSeekFraction >= 0) {
            mOverallFraction = mSeekFraction;
        } else {
            mOverallFraction = 0f;
        }
        if (mListeners != null) {
            notifyStartListeners();
        }
    }
java 复制代码
#AnimationHandler.java
    
	public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        if (mAnimationCallbacks.size() == 0) {
        //调用provider的postFrameCallback方法将成员变量mFrameCallback注册,此处的provider实为MyFrameCallbackProvider
            getProvider().postFrameCallback(mFrameCallback);
        }
        if (!mAnimationCallbacks.contains(callback)) {
        //将动画callback注册到mAnimationCallbacks中,此处的callback就是当前Animation对象
            mAnimationCallbacks.add(callback);
        }
    }
	private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

        final Choreographer mChoreographer = Choreographer.getInstance();

        @Override
        public void postFrameCallback(Choreographer.FrameCallback callback) {
            //这里可以看到,实际上是将成员变量注册到了Choreographer中,这个Choreographer翻译过来是编舞者,主要负责协调VSYNC信号来开始绘制相关流程,其请求&接受垂直同信号,回调到主线程,开启performTraversal()。
            //postFrameCallback方法会做两件事:1.将当前callback注册到集合中,2.请求垂直同步信号,当收到垂直同步信号后,会开始处理集合中的callback,也就是会回调到mFrameCallback中的doFrame()
            mChoreographer.postFrameCallback(callback);
        }

        //***省略
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
           //计算当前时间动画状态
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                //继续将会调注册给Choreographer,接受下一个时间点到会调,这样动画就跑起来了
                getProvider().postFrameCallback(this);
            }
        }
    };
    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i++) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }
            if (isCallbackDue(callback, currentTime)) {
            //回调到callback的doAnimationFrame方法,这个callback就是之前注册Animator实例,在doAnimationFrame方法中会去根据插值器和估值器计算当前时间点到值,并设置给对应view,设置的方式是通过反射调用set相关方法
                callback.doAnimationFrame(frameTime);
                
            }
        }
        cleanUpList();
    }
    }

注意事项

  1. 注意内存泄漏
  2. start方法必须要在有looper的动画调用
相关推荐
兰琛6 分钟前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法1 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter2 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快3 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl3 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江4 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-4 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记
佛系小嘟嘟4 小时前
Android Studio不显示需要的tag日志解决办法《All logs entries are hidden by the filter》
android·ide·android studio
mariokkm4 小时前
Django一分钟:django中收集关联对象关联数据的方法
android·django·sqlite
长亭外的少年5 小时前
如何查看 Android 项目的依赖结构树
android