运用弹簧物理学原理为图形运动添加动画

一、前言

Android系统已经给我们提供了属性动画,ValueAnimator以及ObjectAnimator,这两个动画效果已经满足我们开发中的大多应用场景,但是基于物理特性的动画用上述的两个动画效果就很难实现,但是官方也给我们提供了运用弹簧物理学原理的动画效果库dynamicanimation,让我们在开发中快速的使用。下面的这个效果就是实际项目中基于dynamicanimation来完成的.

image

二、使用流程

2.1 添加支持库

implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'

使用过程中如有升级,可在官网的版本介绍中查看最新版本:https://developer.android.google.cn/jetpack/androidx/versions

2.2 为对应的View添加动画

SpringAnimation三个构造函数,一个是传入FloatValueHolder对象,这个和我们使用ValueAnimator的作用类似,第二个传入Object和FloatPropertyCompat,这里的Object是任何对象,但对我UI而言就是View对象;FloatPropertyCompat就是我们要改变的属性,这里系统为我们提供了常见的View属性值动画:ALPHA,TRANSLATION_X、TRANSLATION_Y、TRANSLATION_Z等,我们直接使用即可;下面看一个TRANSLATION_Y的代码,就会明白他的本质了:

    public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
        @Override
        public void setValue(View view, float value) {
            view.setTranslationY(value);
        }

        @Override
        public float getValue(View view) {
            return view.getTranslationY();
        }
    };

这里就是完成对我们平时对view属性的相关操作。第三个构造函数在第二个的基础上传入一个介绍值,下面我们使用带三个构造函数的来进行测试:

fun onClickAnimation(view: View) {
    SpringAnimation(binding.tarView, DynamicAnimation.TRANSLATION_Y, 400f).apply {
        start()
    }
}

查看效果:

image

从上面的效果图中,直观感觉和弹簧物理运动效果有一定的差距,这里的主要原因就是我们没有设置弹簧的阻尼比和刚度以及其他的属性

2.3 通过StringForce设置刚度

刚度定义了用于衡量弹簧强度的弹簧常量。不在静止位置的坚硬弹簧可对锁连接的对象施加更大的力。

设置刚度通过StringForce.setStiffness()方法进行设置。

注意 刚度必须为正数

系统系统以下刚度常量:

  • STIFFNESS_HIGH
  • STIFFNESS_MEDIUM
  • STIFFNESS_LOW
  • STIFFNESS_VERY_LOW

image

默认的刚度值为 STIFFNESS_MEDIUM

下面为我们的代码添加刚度,设置为STIFFNESS_VERY_LOW

fun onClickAnimation(view: View) {
    SpringAnimation(binding.tarView, DynamicAnimation.TRANSLATION_Y, 400f).apply {
        spring.stiffness = SpringForce.STIFFNESS_VERY_LOW
        start()
    }
}

image

通过上面的效果图,设置刚度为STIFFNESS_VERY_LOW后,弹簧效果变得柔软,直观效果也变得柔和。除了刚度外,我们还可以改变弹簧的阻尼比。

2.4 通过StringForce设置阻尼比

阻尼比用于描述弹簧振动逐渐筛减的情况。通过阻尼比,可以定义振动从一次弹跳到下一次弹跳所筛减的速度有多快。

注意 阻尼比必须为非负数。如果将阻尼比设置为零,弹簧永远不会到达静止位置。换句话说,它会永远振动下去

系统系统以下阻尼比常量:

  • DAMPING_RATIO_HIGH_BOUNCY
  • DAMPING_RATIO_MEDIUM_BOUNCY
  • DAMPING_RATIO_LOW_BOUNCY
  • DAMPING_RATIO_NO_BOUNCY

image

默认阻尼比为 DAMPING_RATIO_MEDIUM_BOUNCY

下面为我们的代码添加阻尼比,设置为DAMPING_RATIO_HIGH_BOUNCY

fun onClickAnimation(view: View) {
    SpringAnimation(binding.tarView, DynamicAnimation.TRANSLATION_Y, 400f).apply {
        spring.stiffness = SpringForce.STIFFNESS_VERY_LOW
        spring.dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
        start()
    }
}

效果图:

image

2.5 其他操作

一、取消动画

可通过cancel()和skipToEnd()来终止动画。

  • cancel(),动画在当前所在值处终止
  • skipToEnd(),使动画跳至最终值,然后终止他。

二、注册和移除监听器

在实际中,我们可能会需要监听动画在执行过程中值的变化情况,那么我们就可以添加监听器来接受该值的变化。

  • OnAnimationUpdateListener
  • OnAnimationEndListener

开发中可使用如上两个监听器来接受值的变化

2.6 使用小结

  1. 添加支持库
  2. 实例化SpringAnimation指定view对象、动画属性以及最总位置
  3. 通过SpringForce设置弹簧阻尼比和刚度
  4. finalPosition必须要设置的参数,可以在构造函数中或者SpringForce中设置。

三、源码分析

在了解基本的使用流程,达到了项目需求后,了解背后的实现原理,有助于我们日后能够更加快速和高效的使用。

在完成SpringAnimation和SprinFouce的相关设置后,我们会调用start()方法来启动动画:

@Override
public void start() {
    //动画执行的前置条件检测
    sanityCheck();
    //设置阈值
    mSpring.setValueThreshold(getValueThreshold());
    super.start();
}

接下来执行到父类DynamicAnimation的start()方法中

public void start() {
    //Main Thread中调用该方法
    if (Looper.myLooper() != Looper.getMainLooper()) {
        throw new AndroidRuntimeException("Animations may only be started on the main thread");
    }
    if (!mRunning) {
        startAnimationInternal();
    }
}

下面进入startAnimationInternal()

private void startAnimationInternal() {
    if (!mRunning) {
        mRunning = true;
        if (!mStartValueIsSet) {
            mValue = getPropertyValue();
        }
        // Sanity check:
        if (mValue > mMaxValue || mValue < mMinValue) {
            throw new IllegalArgumentException("Starting value need to be in between min"
                    + " value and max value");
        }
        AnimationHandler.getInstance().addAnimationFrameCallback(this, 0);
    }
}

剩下的流程进入了AnimationHandler,这里的我们的DynamicAnimation实现了AnimationHandler.AnimationFrameCallback。下面进入AnimationHandler的分析。

通过AnimationHandler.getInstance(),我们可以猜到AnimationHandler应该是基于单列模式来设计的。

public static final ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();

public static AnimationHandler getInstance() {
    if (sAnimatorHandler.get() == null) {
        sAnimatorHandler.set(new AnimationHandler());
    }
    return sAnimatorHandler.get();
}

这里AnimationHandler的实例是通过当前线程的ThreadLocal来进行存储,这里我们的start()方法是在main thread中调用,因此该值是存储在MainThread的ThreadLocal中。

下面进入AnimationHandler的addAnimationFrameCallback()方法

public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    
    if (mAnimationCallbacks.size() == 0) {
        getProvider().postFrameCallback();
    }
    //将callback添加到对应的List中
    if (!mAnimationCallbacks.contains(callback)) {
        mAnimationCallbacks.add(callback);
    }

    if (delay > 0) {
        mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
}

这个方法中会进入到getProivder()方法

AnimationFrameCallbackProvider getProvider() {
    if (mProvider == null) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            mProvider = new FrameCallbackProvider16(mCallbackDispatcher);
        } else {
            mProvider = new FrameCallbackProvider14(mCallbackDispatcher);
        }
    }
    return mProvider;
}

这里我们的只分析FrameCallbackProvider16,现在我们开发的应用大部分都不会去兼容14以下的版本了

private static class FrameCallbackProvider16 extends AnimationFrameCallbackProvider {

    private final Choreographer mChoreographer = Choreographer.getInstance();
    private final Choreographer.FrameCallback mChoreographerCallback;

    FrameCallbackProvider16(AnimationCallbackDispatcher dispatcher) {
        super(dispatcher);
        mChoreographerCallback = new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long frameTimeNanos) {
                    mDispatcher.dispatchAnimationFrame();
                }
            };
    }

    @Override
    void postFrameCallback() {
        mChoreographer.postFrameCallback(mChoreographerCallback);
    }
}

这里出现我们在属性动画中提到过的Choreographer,系统在每一帧刷新的时候,都会回调注册的Choreographer.FrameCallback,这里需要注意mChoreographer.postFrameCallback注册一次,监听一次 意思就是如果需要不停的监听帧的刷新回调,那么每次FrameCallback的doFrame()执行完成后,都需要再次调用mChoreographer.postFrameCallback() 。当屏幕刷新机制回调的时候,会进入doFrame()方法,紧接着执行AnimationCallbackDispatcher.dispatchAnimationFrame()方法

class AnimationCallbackDispatcher {
    void dispatchAnimationFrame() {
        mCurrentFrameTime = SystemClock.uptimeMillis();
        AnimationHandler.this.doAnimationFrame(mCurrentFrameTime);
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback();
        }
    }
}

这里会在doAnimationFrame()方法中执行AnimationFrameCallback的doAnimationFrame()方法,最终执行DynamicAnimation中的

    public boolean doAnimationFrame(long frameTime) {
        if (mLastFrameTime == 0) {
            // First frame.
            mLastFrameTime = frameTime;
            setPropertyValue(mValue);
            return false;
        }
        long deltaT = frameTime - mLastFrameTime;
        mLastFrameTime = frameTime;
        boolean finished = updateValueAndVelocity(deltaT);
        // Clamp value & velocity.
        mValue = Math.min(mValue, mMaxValue);
        mValue = Math.max(mValue, mMinValue);

        setPropertyValue(mValue);

        if (finished) {
            endAnimationInternal(false);
        }
        return finished;
    }

这里的 updateValueAndVelocity()在DynamicAnimation中是一个抽象方法,具体的实现SpringAnimation中:

    @Override
    boolean updateValueAndVelocity(long deltaT) {
        // If user had requested end, then update the value and velocity to end state and consider
        // animation done.
        if (mEndRequested) {
            if (mPendingPosition != UNSET) {
                mSpring.setFinalPosition(mPendingPosition);
                mPendingPosition = UNSET;
            }
            mValue = mSpring.getFinalPosition();
            mVelocity = 0;
            mEndRequested = false;
            return true;
        }

        if (mPendingPosition != UNSET) {
            double lastPosition = mSpring.getFinalPosition();
            // Approximate by considering half of the time spring position stayed at the old
            // position, half of the time it's at the new position.
            MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
            mSpring.setFinalPosition(mPendingPosition);
            mPendingPosition = UNSET;

            massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
            mValue = massState.mValue;
            mVelocity = massState.mVelocity;

        } else {
            MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
            mValue = massState.mValue;
            mVelocity = massState.mVelocity;
        }

        mValue = Math.max(mValue, mMinValue);
        mValue = Math.min(mValue, mMaxValue);

        if (isAtEquilibrium(mValue, mVelocity)) {
            mValue = mSpring.getFinalPosition();
            mVelocity = 0f;
            return true;
        }
        return false;
    }

这里会进行当前值和速度的技术,主要在SpingForce的updateValues()中,感兴趣的可自行研究,这里不在深入分析。

当mValue计算完成后,进入setPropertyValue(mValue)设置属性的值

void setPropertyValue(float value) {
    //设置属性值
    mProperty.setValue(mTarget, value);
    //监听器回调
    for (int i = 0; i < mUpdateListeners.size(); i++) {
        if (mUpdateListeners.get(i) != null) {
            mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity);
        }
    }
    removeNullEntries(mUpdateListeners);
}

这里的mProperty就是在构造函数设置的是,下面通过TRANSLATION_Y了解其工作逻辑:

    public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
        @Override
        public void setValue(View view, float value) {
            view.setTranslationY(value);
        }

        @Override
        public float getValue(View view) {
            return view.getTranslationY();
        }
    };

通过TRANSLATION_Y的代码,我们很直观的看出,setValue()方法改变的是view的TranslationY属性。

到此SpringAnimation的源码分析完成,总结一些要点:

  1. 通过屏幕刷新机制的帧回调来实时计算当前mValue,mVelocity的变化
  2. mValue,mVelocity的计算过程有SpringForce的updateVaues()方法具体实现
  3. 值计算完成后,通过我们在构造函数中指定的FloatPropertyCompat来改变相关的属性
  4. SpringAnimation对比ValueAnimator不在有时间的概念,其实现动画原理是基于弹簧物理学来添加的动画
  5. 如果再构造函数中使用FloatValueHolder,其使用流程和使用ValueAnimator类似

推荐阅读:
属性动画执行原理解析:https://blog.csdn.net/mrRuby/article/details/112685657

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值