AOSP Android14 Launcher3——RectFSpringAnim窗口动画类详解
在阅读源码研究从第三方应用回到桌面的过程中,接触到RectFSpringAnim这个类,确认这个类就是最终动画的实现类。因此对这个类的源码进行阅读分析。
本文来详细分析 quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
这个类。
核心作用 (Core Purpose):
RectFSpringAnim
的核心作用是提供一个基于物理弹簧模型 (Spring Physics) 的动画机制,用于平滑地、带有物理惯性地将一个矩形 (RectF
) 从起始状态 (位置和大小) 动画到目标状态 (位置和大小)。
它主要用于 QuickstepTransitionManager
中处理**应用窗口关闭(返回桌面)**时的动画,模拟窗口从全屏状态自然地、带有速度感地收缩并移动到其在 Launcher 上的对应图标或小部件位置的过程。
工作原理 (How it Works):
与传统的基于时间插值器 (Time Interpolator) 的动画(如 ValueAnimator
, ObjectAnimator
) 不同,RectFSpringAnim
使用的是 AndroidX DynamicAnimation 库中的 SpringAnimation
和自定义的 FlingSpringAnim
。其原理是:
-
分离动画维度: 它并不直接动画
RectF
的 left, top, right, bottom 四个值。而是将矩形的动画分解为三个独立的、但同时进行的物理模拟:- 水平中心 (
RECT_CENTER_X
): 使用FlingSpringAnim
控制矩形水平中心点 (centerX
) 的动画。FlingSpringAnim
结合了初始的抛掷 (Fling) 速度和最终稳定到目标值的弹簧 (Spring) 效果。 - 垂直位置 (
RECT_Y
): 使用FlingSpringAnim
控制矩形一个垂直参考点的动画。这个参考点由mTracking
变量决定:TRACKING_TOP
: 动画矩形的top
值。TRACKING_BOTTOM
: 动画矩形的bottom
值。TRACKING_CENTER
: 动画矩形的centerY
值。
选择哪个参考点取决于动画的具体场景(例如,窗口是从屏幕上方移入还是下方移入,目标位置更靠近哪边)。
- 缩放进度 (
RECT_SCALE_PROGRESS
): 使用标准的SpringAnimation
控制一个从 0 到 1 的进度值 (mCurrentScaleProgress
)。这个进度值随后被用来线性插值计算矩形当前的宽度和高度(从起始宽高到目标宽高)。
- 水平中心 (
-
物理参数: 每个维度的弹簧动画都由物理参数控制:
- Stiffness (刚度): 弹簧的“硬度”,影响回弹的速度和振荡频率。 (
mStiffnessX
,mStiffnessY
,mRectStiffness
)。 - Damping Ratio (阻尼比): 控制振荡衰减的速度。值小于 1 会产生振荡,等于 1 是临界阻尼(最快到达且不振荡),大于 1 是过阻尼(缓慢到达)。(
mDampingX
,mDampingY
)。
- Stiffness (刚度): 弹簧的“硬度”,影响回弹的速度和振荡频率。 (
-
动画启动 (
start
方法):- 接收一个初始速度 (
velocityPxPerMs
),通常是用户手指离开屏幕时的速度。 - 对速度进行阻尼处理 (
OverScroll.dampedScroll
),防止过大的速度导致动画过于剧烈或不自然。 - 根据起始/目标位置、阻尼后的速度、物理参数(刚度、阻尼比)创建并启动
RECT_CENTER_X
和RECT_Y
的FlingSpringAnim
。 - 根据Y轴速度和物理参数创建并启动
RECT_SCALE_PROGRESS
的SpringAnimation
。 - 通知外部监听器动画开始。
- 接收一个初始速度 (
-
动画更新 (
onUpdate
方法):- 当任何一个底层的
SpringAnimation
或FlingSpringAnim
更新其值时,会触发其对应FloatPropertyCompat
的setValue
方法,进而调用RectFSpringAnim
的onUpdate()
。 - 在
onUpdate()
中:- 根据当前的
mCurrentScaleProgress
插值计算出当前的宽度 (currentWidth
) 和高度 (currentHeight
)。 - 根据当前的
mCurrentCenterX
,mCurrentY
以及mTracking
模式,结合计算出的currentWidth
,currentHeight
,重新构建出完整的mCurrentRect
(当前的矩形状态)。例如,如果mTracking == TRACKING_TOP
,则mCurrentRect.top = mCurrentY
,mCurrentRect.bottom = mCurrentY + currentHeight
。 - 遍历所有注册的
OnUpdateListener
,调用它们的onUpdate(mCurrentRect, mCurrentScaleProgress)
方法,将最新计算出的矩形状态和缩放进度传递出去。
- 根据当前的
- 当任何一个底层的
-
动画结束 (
maybeOnEnd
方法):- 每个底层的弹簧动画结束后,会设置对应的结束标志 (
mRectXAnimEnded
,mRectYAnimEnded
,mRectScaleAnimEnded
)。 maybeOnEnd()
会检查是否所有三个动画都已结束。- 只有当全部结束后,才会通知外部的
Animator.AnimatorListener
动画结束,并设置setCanRelease(true)
(用于RemoteAnimationTargets
的资源释放检查)。
- 每个底层的弹簧动画结束后,会设置对应的结束标志 (
-
配置 (
SpringConfig
子类):- 提供了不同的配置类(
DefaultSpringConfig
,TaskbarHotseatSpringConfig
)来为不同的动画场景(如普通返回桌面 vs 返回到 Taskbar/Hotseat 上的图标)提供预设的、经过调整的物理参数(刚度、阻尼、追踪模式),以达到最佳的视觉效果。
- 提供了不同的配置类(
在 Launcher3 中的作用:
RectFSpringAnim
在 Launcher3 (Quickstep) 中扮演着一个特定场景下的动画引擎角色:
- 驱动窗口关闭动画: 它是
QuickstepTransitionManager
在处理应用关闭返回桌面动画时的核心驱动力之一 (尤其是在createWallpaperOpenAnimations
->getClosingWindowAnimators
中被创建和使用)。
// 应用关闭返回桌面动画
//quickstep/src/com/android/launcher3/QuickstepTransitionManager.javaprotected RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS,RectF closingWindowStartRectF, float startWindowCornerRadius) {FloatingIconView floatingIconView = null;FloatingWidgetView floatingWidget = null;RectF targetRect = new RectF();...... 省略boolean useTaskbarHotseatParams = mDeviceProfile.isTaskbarPresent && isInHotseat;//创建RectFSpringAnim动画实例RectFSpringAnim anim = new RectFSpringAnim(useTaskbarHotseatParams? new TaskbarHotseatSpringConfig(mLauncher, closingWindowStartRectF, targetRect): new DefaultSpringConfig(mLauncher, mDeviceProfile, closingWindowStartRectF,targetRect));// Hook up floating views to the closing window animators.// note the coordinate of closingWindowStartRect is based on launcherRect closingWindowStartRect = new Rect();closingWindowStartRectF.round(closingWindowStartRect);Rect closingWindowOriginalRect =new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx);if (floatingIconView != null) {anim.addAnimatorListener(floatingIconView);floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged);floatingIconView.setFastFinishRunnable(anim::end);FloatingIconView finalFloatingIconView = floatingIconView;// We want the window alpha to be 0 once this threshold is met, so that the// FolderIconView can be seen morphing into the icon shape.final float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION;RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect,closingWindowStartRect, closingWindowOriginalRect, startWindowCornerRadius) {@Overridepublic void onUpdate(RectF currentRectF, float progress) {finalFloatingIconView.update(1f, currentRectF, progress, windowAlphaThreshold,getCornerRadius(progress), false);super.onUpdate(currentRectF, progress);}};anim.addOnUpdateListener(runner);} else if (floatingWidget != null) {anim.addAnimatorListener(floatingWidget);floatingWidget.setOnTargetChangeListener(anim::onTargetPositionChanged);floatingWidget.setFastFinishRunnable(anim::end);final float floatingWidgetAlpha = isTransluscent ? 0 : 1;FloatingWidgetView finalFloatingWidget = floatingWidget;RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect,closingWindowStartRect, closingWindowOriginalRect, startWindowCornerRadius) {@Overridepublic void onUpdate(RectF currentRectF, float progress) {final float fallbackBackgroundAlpha =1 - mapBoundToRange(progress, 0.8f, 1, 0, 1, EXAGGERATED_EASE);final float foregroundAlpha =mapBoundToRange(progress, 0.5f, 1, 0, 1, EXAGGERATED_EASE);finalFloatingWidget.update(currentRectF, floatingWidgetAlpha, foregroundAlpha,fallbackBackgroundAlpha, 1 - progress);super.onUpdate(currentRectF, progress);}};anim.addOnUpdateListener(runner);} else {// If no floating icon or widget is present, animate the to the default window// target rect.anim.addOnUpdateListener(new SpringAnimRunner(targets, targetRect, closingWindowStartRect, closingWindowOriginalRect,startWindowCornerRadius));}// Use a fixed velocity to start the animation.animation.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationStart(Animator animation) {anim.start(mLauncher, mDeviceProfile, velocityPxPerS);}});return anim;}
- 提供窗口变换参数: 它的
OnUpdateListener
(通常是QuickstepTransitionManager.SpringAnimRunner
的实例) 在每一帧接收到计算好的mCurrentRect
。这个mCurrentRect
就代表了应用窗口 Leash 在该帧应该呈现的大小和在 Launcher 坐标系下的位置。
//quickstep/src/com/android/launcher3/QuickstepTransitionManager.java@Overridepublic void onUpdate(RectF currentRectF, float progress) {SurfaceTransaction transaction = new SurfaceTransaction();for (int i = mAppTargets.length - 1; i >= 0; i--) {RemoteAnimationTarget target = mAppTargets[i];SurfaceProperties builder = transaction.forSurface(target.leash);if (target.localBounds != null) {mTmpPos.set(target.localBounds.left, target.localBounds.top);} else {mTmpPos.set(target.position.x, target.position.y);}if (target.mode == MODE_CLOSING) {transferRectToTargetCoordinate(target, currentRectF, false, currentRectF);currentRectF.round(mCurrentRect);// Scale the target window to match the currentRectF.final float scale;// We need to infer the crop (we crop the window to match the currentRectF).if (mWindowStartBounds.height() > mWindowStartBounds.width()) {scale = Math.min(1f, currentRectF.width() / mWindowOriginalBounds.width());int unscaledHeight = (int) (mCurrentRect.height() * (1f / scale));int croppedHeight = mWindowStartBounds.height() - unscaledHeight;mTmpRect.set(0, 0, mWindowOriginalBounds.width(),mWindowStartBounds.height() - croppedHeight);} else {scale = Math.min(1f, currentRectF.height()/ mWindowOriginalBounds.height());int unscaledWidth = (int) (mCurrentRect.width() * (1f / scale));int croppedWidth = mWindowStartBounds.width() - unscaledWidth;mTmpRect.set(0, 0, mWindowStartBounds.width() - croppedWidth,mWindowOriginalBounds.height());}// Match size and position of currentRect.mMatrix.setScale(scale, scale);mMatrix.postTranslate(mCurrentRect.left, mCurrentRect.top);builder.setMatrix(mMatrix).setWindowCrop(mTmpRect).setAlpha(getWindowAlpha(progress)).setCornerRadius(getCornerRadius(progress) / scale);} else if (target.mode == MODE_OPENING) {mMatrix.setTranslate(mTmpPos.x, mTmpPos.y);builder.setMatrix(mMatrix).setAlpha(1f);}}mSurfaceApplier.scheduleApply(transaction);}
- 参数转换:
SpringAnimRunner
接收到mCurrentRect
后,会将其转换为应用到SurfaceControl
Leash 所需的Matrix
(变换矩阵,包含平移和缩放)、WindowCrop
(裁剪区域) 和CornerRadius
(圆角)。 - 实现物理效果: 它使得窗口关闭动画不仅仅是简单的线性缩小和平移,而是带有速度感、惯性、可能的回弹或过冲效果,这得益于其底层的弹簧物理模型。这让过渡感觉更加生动和自然。
- 处理用户输入速度: 它能够接受用户手势结束时的速度作为输入,使得动画的初始状态能匹配用户的操作,增强了响应性。
总结:
RectFSpringAnim
是一个专门用于基于物理弹簧模型动画化矩形变换的工具类。在 Launcher3 中,它被 QuickstepTransitionManager
用来驱动应用关闭返回桌面时的窗口动画,通过模拟水平中心、垂直参考点和缩放进度的弹簧运动,计算出每一帧窗口应有的位置和大小,并将这些信息通过回调传递出去,最终应用到真实的窗口 Surface 上,实现了带有物理特性、流畅自然的过渡效果。