Merge "Allow single root candidate for app pair launch for pip edge case" into 24D1-dev
This commit is contained in:
@@ -23,6 +23,7 @@ import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.ActivityManager.RunningTaskInfo
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
@@ -51,6 +52,7 @@ import com.android.launcher3.anim.PendingAnimation
|
||||
import com.android.launcher3.apppairs.AppPairIcon
|
||||
import com.android.launcher3.config.FeatureFlags
|
||||
import com.android.launcher3.logging.StatsLogManager.EventEnum
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo
|
||||
import com.android.launcher3.statehandlers.DepthController
|
||||
import com.android.launcher3.statemanager.StateManager
|
||||
import com.android.launcher3.statemanager.StatefulActivity
|
||||
@@ -69,6 +71,7 @@ import com.android.quickstep.views.TaskThumbnailView
|
||||
import com.android.quickstep.views.TaskView
|
||||
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
|
||||
import com.android.quickstep.views.TaskViewIcon
|
||||
import com.android.wm.shell.shared.TransitionUtil
|
||||
import java.util.Optional
|
||||
import java.util.function.Supplier
|
||||
|
||||
@@ -532,8 +535,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
check(info != null && t != null) {
|
||||
"trying to launch an app pair icon, but encountered an unexpected null"
|
||||
}
|
||||
|
||||
composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
|
||||
val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
|
||||
if (appPairLaunchingAppIndex == -1) {
|
||||
// Launch split app pair animation
|
||||
composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
|
||||
} else {
|
||||
composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t,
|
||||
finishCallback, appPairLaunchingAppIndex)
|
||||
}
|
||||
} else {
|
||||
// Fallback case: simple fade-in animation
|
||||
check(info != null && t != null) {
|
||||
@@ -597,6 +606,39 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise
|
||||
* the integer index corresponding to [launchingIconView]'s contents for the single app
|
||||
* to be animated
|
||||
*/
|
||||
fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon,
|
||||
transitionInfo: TransitionInfo) : Int {
|
||||
val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
|
||||
val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
|
||||
var launchFullscreenAppIndex = -1
|
||||
for (change in transitionInfo.changes) {
|
||||
val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
|
||||
if (TransitionUtil.isOpeningType(change.mode) &&
|
||||
taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) {
|
||||
val baseIntent = taskInfo.baseIntent.component?.packageName
|
||||
if (baseIntent == intent1) {
|
||||
if (launchFullscreenAppIndex > -1) {
|
||||
launchFullscreenAppIndex = -1
|
||||
break
|
||||
}
|
||||
launchFullscreenAppIndex = 0
|
||||
} else if (baseIntent == intent2) {
|
||||
if (launchFullscreenAppIndex > -1) {
|
||||
launchFullscreenAppIndex = -1
|
||||
break
|
||||
}
|
||||
launchFullscreenAppIndex = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return launchFullscreenAppIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user taps an app pair icon to launch split, this will play the tasks' launch
|
||||
* animation from the position of the icon.
|
||||
@@ -632,7 +674,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
|
||||
// use the scale-up animation
|
||||
if (launchingIconView.context is TaskbarActivityContext) {
|
||||
composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
|
||||
composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
|
||||
WINDOWING_MODE_MULTI_WINDOW)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -642,11 +685,6 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
|
||||
// Create an AnimatorSet that will run both shell and launcher transitions together
|
||||
val launchAnimation = AnimatorSet()
|
||||
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
|
||||
val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
|
||||
progressUpdater.setDuration(timings.getDuration().toLong())
|
||||
progressUpdater.interpolator = Interpolators.LINEAR
|
||||
|
||||
var rootCandidate: Change? = null
|
||||
|
||||
for (change in transitionInfo.changes) {
|
||||
@@ -690,27 +728,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
// Make sure nothing weird happened, like getChange() returning null.
|
||||
check(rootCandidate != null) { "Failed to find a root leash" }
|
||||
|
||||
// Shell animation: the apps are revealed toward end of the launch animation
|
||||
progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
|
||||
val progress =
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.LINEAR,
|
||||
valueAnimator.animatedFraction,
|
||||
timings.appRevealStartOffset,
|
||||
timings.appRevealEndOffset
|
||||
)
|
||||
|
||||
// Set the alpha of the shell layer (2 apps + divider)
|
||||
t.setAlpha(rootCandidate.leash, progress)
|
||||
t.apply()
|
||||
}
|
||||
|
||||
// Create a new floating view in Launcher, positioned above the launching icon
|
||||
val drawableArea = launchingIconView.iconDrawableArea
|
||||
val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context)
|
||||
val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context)
|
||||
appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
|
||||
appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
|
||||
|
||||
val floatingView =
|
||||
FloatingAppPairView.getFloatingAppPairView(
|
||||
launcher,
|
||||
@@ -721,84 +745,189 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
)
|
||||
floatingView.bringToFront()
|
||||
|
||||
// Launcher animation: animate the floating view, expanding to fill the display surface
|
||||
progressUpdater.addUpdateListener(
|
||||
object : MultiValueUpdateListener() {
|
||||
var mDx =
|
||||
FloatProp(
|
||||
floatingView.startingPosition.left,
|
||||
dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
|
||||
Interpolators.clampToProgress(
|
||||
timings.getStagedRectXInterpolator(),
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mDy =
|
||||
FloatProp(
|
||||
floatingView.startingPosition.top,
|
||||
dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mScaleX =
|
||||
FloatProp(
|
||||
1f /* start */,
|
||||
dp.widthPx / floatingView.startingPosition.width(),
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mScaleY =
|
||||
FloatProp(
|
||||
1f /* start */,
|
||||
dp.heightPx / floatingView.startingPosition.height(),
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
|
||||
override fun onUpdate(percent: Float, initOnly: Boolean) {
|
||||
floatingView.progress = percent
|
||||
floatingView.x = mDx.value
|
||||
floatingView.y = mDy.value
|
||||
floatingView.scaleX = mScaleX.value
|
||||
floatingView.scaleY = mScaleY.value
|
||||
floatingView.invalidate()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// When animation ends, remove the floating view and run finishCallback
|
||||
progressUpdater.addListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
safeRemoveViewFromDragLayer(launcher, floatingView)
|
||||
finishCallback.run()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
launchAnimation.play(progressUpdater)
|
||||
launchAnimation.play(
|
||||
getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
|
||||
rootCandidate))
|
||||
launchAnimation.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate
|
||||
* a single fullscreen icon + background instead of for a pair
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun composeFullscreenIconSplitLaunchAnimator(
|
||||
launchingIconView: AppPairIcon,
|
||||
transitionInfo: TransitionInfo,
|
||||
t: Transaction,
|
||||
finishCallback: Runnable,
|
||||
launchFullscreenIndex: Int
|
||||
) {
|
||||
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
|
||||
// use the scale-up animation
|
||||
if (launchingIconView.context is TaskbarActivityContext) {
|
||||
composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
|
||||
WINDOWING_MODE_FULLSCREEN)
|
||||
return
|
||||
}
|
||||
|
||||
// Else we are in Launcher and can launch with the full icon stretch-and-split animation.
|
||||
val launcher = Launcher.getLauncher(launchingIconView.context)
|
||||
val dp = launcher.deviceProfile
|
||||
|
||||
// Create an AnimatorSet that will run both shell and launcher transitions together
|
||||
val launchAnimation = AnimatorSet()
|
||||
|
||||
val appInfo = launchingIconView.info
|
||||
.getContents()[launchFullscreenIndex] as WorkspaceItemInfo
|
||||
val intentToLaunch = appInfo.intent.component?.packageName
|
||||
var rootCandidate: Change? = null
|
||||
for (change in transitionInfo.changes) {
|
||||
val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
|
||||
val baseIntent = taskInfo.baseIntent.component?.packageName
|
||||
if (TransitionUtil.isOpeningType(change.mode) &&
|
||||
taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN &&
|
||||
baseIntent == intentToLaunch) {
|
||||
rootCandidate = change
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not find a proper root candidate, something went wrong.
|
||||
check(rootCandidate != null) { "Could not find a split root candidate" }
|
||||
|
||||
// Recurse up the tree until parent is null, then we've found our root.
|
||||
var parentToken: WindowContainerToken? = rootCandidate.parent
|
||||
while (parentToken != null) {
|
||||
rootCandidate = transitionInfo.getChange(parentToken) ?: break
|
||||
parentToken = rootCandidate.parent
|
||||
}
|
||||
|
||||
// Make sure nothing weird happened, like getChange() returning null.
|
||||
check(rootCandidate != null) { "Failed to find a root leash" }
|
||||
|
||||
// Create a new floating view in Launcher, positioned above the launching icon
|
||||
val drawableArea = launchingIconView.iconDrawableArea
|
||||
val appIcon = appInfo.newIcon(launchingIconView.context)
|
||||
appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
|
||||
|
||||
val floatingView =
|
||||
FloatingAppPairView.getFloatingAppPairView(
|
||||
launcher,
|
||||
drawableArea,
|
||||
appIcon,
|
||||
null /*appIcon2*/,
|
||||
0 /*dividerPos*/
|
||||
)
|
||||
floatingView.bringToFront()
|
||||
launchAnimation.play(
|
||||
getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
|
||||
rootCandidate))
|
||||
launchAnimation.start()
|
||||
}
|
||||
|
||||
private fun getIconLaunchValueAnimator(t: Transaction,
|
||||
dp: com.android.launcher3.DeviceProfile,
|
||||
finishCallback: Runnable,
|
||||
launcher: Launcher,
|
||||
floatingView: FloatingAppPairView,
|
||||
rootCandidate: Change) : ValueAnimator {
|
||||
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
|
||||
val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
|
||||
progressUpdater.setDuration(timings.getDuration().toLong())
|
||||
progressUpdater.interpolator = Interpolators.LINEAR
|
||||
|
||||
// Shell animation: the apps are revealed toward end of the launch animation
|
||||
progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
|
||||
val progress =
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.LINEAR,
|
||||
valueAnimator.animatedFraction,
|
||||
timings.appRevealStartOffset,
|
||||
timings.appRevealEndOffset
|
||||
)
|
||||
|
||||
// Set the alpha of the shell layer (2 apps + divider)
|
||||
t.setAlpha(rootCandidate.leash, progress)
|
||||
t.apply()
|
||||
}
|
||||
|
||||
progressUpdater.addUpdateListener(
|
||||
object : MultiValueUpdateListener() {
|
||||
var mDx =
|
||||
FloatProp(
|
||||
floatingView.startingPosition.left,
|
||||
dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
|
||||
Interpolators.clampToProgress(
|
||||
timings.getStagedRectXInterpolator(),
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mDy =
|
||||
FloatProp(
|
||||
floatingView.startingPosition.top,
|
||||
dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mScaleX =
|
||||
FloatProp(
|
||||
1f /* start */,
|
||||
dp.widthPx / floatingView.startingPosition.width(),
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
var mScaleY =
|
||||
FloatProp(
|
||||
1f /* start */,
|
||||
dp.heightPx / floatingView.startingPosition.height(),
|
||||
Interpolators.clampToProgress(
|
||||
Interpolators.EMPHASIZED,
|
||||
timings.stagedRectSlideStartOffset,
|
||||
timings.stagedRectSlideEndOffset
|
||||
)
|
||||
)
|
||||
|
||||
override fun onUpdate(percent: Float, initOnly: Boolean) {
|
||||
floatingView.progress = percent
|
||||
floatingView.x = mDx.value
|
||||
floatingView.y = mDy.value
|
||||
floatingView.scaleX = mScaleX.value
|
||||
floatingView.scaleY = mScaleY.value
|
||||
floatingView.invalidate()
|
||||
}
|
||||
}
|
||||
)
|
||||
progressUpdater.addListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
safeRemoveViewFromDragLayer(launcher, floatingView)
|
||||
finishCallback.run()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return progressUpdater
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
|
||||
* there is no visible associated tile to expand from.
|
||||
* [windowingMode] helps determine whether we are looking for a split or a single fullscreen
|
||||
* [Change]
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun composeScaleUpLaunchAnimation(
|
||||
transitionInfo: TransitionInfo,
|
||||
t: Transaction,
|
||||
finishCallback: Runnable
|
||||
finishCallback: Runnable,
|
||||
windowingMode: Int
|
||||
) {
|
||||
val launchAnimation = AnimatorSet()
|
||||
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
|
||||
@@ -812,9 +941,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
|
||||
// TODO (b/316490565): Replace this logic when SplitBounds is available to
|
||||
// startAnimation() and we can know the precise taskIds of launching tasks.
|
||||
// Find a change that has WINDOWING_MODE_MULTI_WINDOW.
|
||||
if (
|
||||
taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
|
||||
taskInfo.windowingMode == windowingMode &&
|
||||
(change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
|
||||
) {
|
||||
// Found one!
|
||||
|
||||
@@ -37,17 +37,18 @@ import com.android.systemui.shared.system.QuickStepContract
|
||||
* animation. Consists of a rectangular background that splits into two, and two app icons that
|
||||
* increase in size during the animation.
|
||||
*/
|
||||
class FloatingAppPairBackground(
|
||||
context: Context,
|
||||
private val floatingView: FloatingAppPairView, // the view that we will draw this background on
|
||||
private val appIcon1: Drawable,
|
||||
private val appIcon2: Drawable,
|
||||
dividerPos: Int
|
||||
open class FloatingAppPairBackground(
|
||||
context: Context,
|
||||
// the view that we will draw this background on
|
||||
protected val floatingView: FloatingAppPairView,
|
||||
private val appIcon1: Drawable,
|
||||
private val appIcon2: Drawable?,
|
||||
dividerPos: Int
|
||||
) : Drawable() {
|
||||
companion object {
|
||||
// Design specs -- app icons start small and expand during the animation
|
||||
private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
|
||||
private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
|
||||
internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
|
||||
internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
|
||||
|
||||
// Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
|
||||
// API for drawing rectangles with 4 different corner radii.
|
||||
@@ -59,13 +60,13 @@ class FloatingAppPairBackground(
|
||||
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
// Animation interpolators
|
||||
private val expandXInterpolator: Interpolator
|
||||
private val expandYInterpolator: Interpolator
|
||||
protected val expandXInterpolator: Interpolator
|
||||
protected val expandYInterpolator: Interpolator
|
||||
private val cellSplitInterpolator: Interpolator
|
||||
private val iconFadeInterpolator: Interpolator
|
||||
protected val iconFadeInterpolator: Interpolator
|
||||
|
||||
// Device-specific measurements
|
||||
private val deviceCornerRadius: Float
|
||||
protected val deviceCornerRadius: Float
|
||||
private val deviceHalfDividerSize: Float
|
||||
private val desiredSplitRatio: Float
|
||||
|
||||
@@ -217,7 +218,7 @@ class FloatingAppPairBackground(
|
||||
canvas.save()
|
||||
canvas.translate(changingIcon2Left, changingIconTop)
|
||||
canvas.scale(changingIconScaleX, changingIconScaleY)
|
||||
appIcon2.alpha = changingIconAlpha
|
||||
appIcon2!!.alpha = changingIconAlpha
|
||||
appIcon2.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
@@ -317,7 +318,7 @@ class FloatingAppPairBackground(
|
||||
canvas.save()
|
||||
canvas.translate(changingIconLeft, changingIcon2Top)
|
||||
canvas.scale(changingIconScaleX, changingIconScaleY)
|
||||
appIcon2.alpha = changingIconAlpha
|
||||
appIcon2!!.alpha = changingIconAlpha
|
||||
appIcon2.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
@@ -330,7 +331,7 @@ class FloatingAppPairBackground(
|
||||
* @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
|
||||
* right y, bottom right x, and so on.
|
||||
*/
|
||||
private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
|
||||
protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Canvas.drawDoubleRoundRect is supported from Q onward
|
||||
c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
|
||||
|
||||
@@ -40,8 +40,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
fun getFloatingAppPairView(
|
||||
launcher: StatefulActivity<*>,
|
||||
originalView: View,
|
||||
appIcon1: Drawable,
|
||||
appIcon2: Drawable,
|
||||
appIcon1: Drawable?,
|
||||
appIcon2: Drawable?,
|
||||
dividerPos: Int
|
||||
): FloatingAppPairView {
|
||||
val dragLayer: ViewGroup = launcher.getDragLayer()
|
||||
@@ -64,8 +64,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
fun init(
|
||||
launcher: StatefulActivity<*>,
|
||||
originalView: View,
|
||||
appIcon1: Drawable,
|
||||
appIcon2: Drawable,
|
||||
appIcon1: Drawable?,
|
||||
appIcon2: Drawable?,
|
||||
dividerPos: Int
|
||||
) {
|
||||
val viewBounds = Rect(0, 0, originalView.width, originalView.height)
|
||||
@@ -92,7 +92,14 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
layoutParams = lp
|
||||
|
||||
// Prepare to draw app pair icon background
|
||||
background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
|
||||
background = if (appIcon1 == null || appIcon2 == null) {
|
||||
val iconToAnimate = appIcon1 ?: appIcon2
|
||||
checkNotNull(iconToAnimate)
|
||||
FloatingFullscreenAppPairBackground(context, this, iconToAnimate,
|
||||
dividerPos)
|
||||
} else {
|
||||
FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
|
||||
}
|
||||
background.setBounds(0, 0, lp.width, lp.height)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.quickstep.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
class FloatingFullscreenAppPairBackground(
|
||||
context: Context,
|
||||
floatingView: FloatingAppPairView,
|
||||
private val iconToLaunch: Drawable,
|
||||
dividerPos: Int) :
|
||||
FloatingAppPairBackground(
|
||||
context,
|
||||
floatingView,
|
||||
iconToLaunch,
|
||||
null /*appIcon2*/,
|
||||
dividerPos
|
||||
) {
|
||||
|
||||
/** Animates the background as if launching a fullscreen task. */
|
||||
override fun draw(canvas: Canvas) {
|
||||
val progress = floatingView.progress
|
||||
|
||||
// Since the entire floating app pair surface is scaling up during this animation, we
|
||||
// scale down most of these drawn elements so that they appear the proper size on-screen.
|
||||
val scaleFactorX = floatingView.scaleX
|
||||
val scaleFactorY = floatingView.scaleY
|
||||
|
||||
// Get the bounds where we will draw the background image
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
|
||||
// Get device-specific measurements
|
||||
val cornerRadiusX = deviceCornerRadius / scaleFactorX
|
||||
val cornerRadiusY = deviceCornerRadius / scaleFactorY
|
||||
|
||||
// Draw background
|
||||
drawCustomRoundedRect(
|
||||
canvas,
|
||||
RectF(0f, 0f, width, height),
|
||||
floatArrayOf(
|
||||
cornerRadiusX,
|
||||
cornerRadiusY,
|
||||
cornerRadiusX,
|
||||
cornerRadiusY,
|
||||
cornerRadiusX,
|
||||
cornerRadiusY,
|
||||
cornerRadiusX,
|
||||
cornerRadiusY,
|
||||
)
|
||||
)
|
||||
|
||||
// Calculate changing measurements for icon.
|
||||
val changingIconSizeX =
|
||||
(STARTING_ICON_SIZE_PX +
|
||||
((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
|
||||
expandXInterpolator.getInterpolation(progress))) / scaleFactorX
|
||||
val changingIconSizeY =
|
||||
(STARTING_ICON_SIZE_PX +
|
||||
((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
|
||||
expandYInterpolator.getInterpolation(progress))) / scaleFactorY
|
||||
|
||||
val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f)
|
||||
val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
|
||||
val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width()
|
||||
val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height()
|
||||
val changingIconAlpha =
|
||||
(255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
|
||||
|
||||
// Draw icon
|
||||
canvas.save()
|
||||
canvas.translate(changingIcon1Left, changingIconTop)
|
||||
canvas.scale(changingIconScaleX, changingIconScaleY)
|
||||
iconToLaunch.alpha = changingIconAlpha
|
||||
iconToLaunch.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package com.android.quickstep.util
|
||||
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ContextThemeWrapper
|
||||
@@ -39,8 +41,10 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doNothing
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.verify
|
||||
@@ -255,6 +259,9 @@ class SplitAnimationControllerTest {
|
||||
doNothing()
|
||||
.whenever(spySplitAnimationController)
|
||||
.composeIconSplitLaunchAnimator(any(), any(), any(), any())
|
||||
doReturn(-1)
|
||||
.whenever(spySplitAnimationController)
|
||||
.hasChangesForBothAppPairs(any(), any())
|
||||
|
||||
spySplitAnimationController.playSplitLaunchAnimation(
|
||||
null /* launchingTaskView */,
|
||||
@@ -276,13 +283,45 @@ class SplitAnimationControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() {
|
||||
fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() {
|
||||
val spySplitAnimationController = spy(splitAnimationController)
|
||||
whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
|
||||
doNothing()
|
||||
.whenever(spySplitAnimationController)
|
||||
.composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
|
||||
doReturn(0)
|
||||
.whenever(spySplitAnimationController)
|
||||
.hasChangesForBothAppPairs(any(), any())
|
||||
|
||||
spySplitAnimationController.playSplitLaunchAnimation(
|
||||
null /* launchingTaskView */,
|
||||
mockAppPairIcon,
|
||||
taskId,
|
||||
taskId2,
|
||||
null /* apps */,
|
||||
null /* wallpapers */,
|
||||
null /* nonApps */,
|
||||
stateManager,
|
||||
depthController,
|
||||
transitionInfo,
|
||||
transaction,
|
||||
{} /* finishCallback */
|
||||
)
|
||||
|
||||
verify(spySplitAnimationController)
|
||||
.composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
|
||||
val spySplitAnimationController = spy(splitAnimationController)
|
||||
whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
|
||||
doNothing()
|
||||
.whenever(spySplitAnimationController)
|
||||
.composeScaleUpLaunchAnimation(any(), any(), any())
|
||||
|
||||
.composeScaleUpLaunchAnimation(any(), any(), any(), any())
|
||||
doReturn(-1)
|
||||
.whenever(spySplitAnimationController)
|
||||
.hasChangesForBothAppPairs(any(), any())
|
||||
spySplitAnimationController.playSplitLaunchAnimation(
|
||||
null /* launchingTaskView */,
|
||||
mockAppPairIcon,
|
||||
@@ -298,7 +337,37 @@ class SplitAnimationControllerTest {
|
||||
{} /* finishCallback */
|
||||
)
|
||||
|
||||
verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
|
||||
verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
|
||||
eq(WINDOWING_MODE_MULTI_WINDOW))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() {
|
||||
val spySplitAnimationController = spy(splitAnimationController)
|
||||
whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
|
||||
doNothing()
|
||||
.whenever(spySplitAnimationController)
|
||||
.composeScaleUpLaunchAnimation(any(), any(), any(), any())
|
||||
doReturn(0)
|
||||
.whenever(spySplitAnimationController)
|
||||
.hasChangesForBothAppPairs(any(), any())
|
||||
spySplitAnimationController.playSplitLaunchAnimation(
|
||||
null /* launchingTaskView */,
|
||||
mockAppPairIcon,
|
||||
taskId,
|
||||
taskId2,
|
||||
null /* apps */,
|
||||
null /* wallpapers */,
|
||||
null /* nonApps */,
|
||||
stateManager,
|
||||
depthController,
|
||||
transitionInfo,
|
||||
transaction,
|
||||
{} /* finishCallback */
|
||||
)
|
||||
|
||||
verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
|
||||
eq(WINDOWING_MODE_FULLSCREEN))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user