Allow single root candidate for app pair launch for pip edge case
* Shell will launch single task if requested split apps have one of them already in Pip * Create a separate method to set animation for launching from the appPair icon on workspace * Reuse the animation method for launching an AppPair icon from taskbar by specifying which windowing mode to look for if we're launching the actual split pair vs just one in fullscreen Bug: 323089902 Test: Launches fine visually Change-Id: I415343a48e980afd7f4e511558d350cf15b97ca1
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
|
||||
@@ -50,6 +51,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.taskbar.TaskbarActivityContext
|
||||
@@ -69,6 +71,7 @@ import com.android.quickstep.views.TaskThumbnailViewDeprecated
|
||||
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
|
||||
|
||||
@@ -553,8 +556,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) {
|
||||
@@ -618,6 +627,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.
|
||||
@@ -653,7 +695,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
|
||||
}
|
||||
|
||||
@@ -663,11 +706,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) {
|
||||
@@ -711,27 +749,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,
|
||||
@@ -742,84 +766,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 = QuickstepLauncher.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: QuickstepLauncher,
|
||||
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)
|
||||
@@ -833,9 +962,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!
|
||||
|
||||
@@ -36,17 +36,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.
|
||||
@@ -58,13 +59,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
|
||||
|
||||
@@ -214,7 +215,7 @@ class FloatingAppPairBackground(
|
||||
canvas.save()
|
||||
canvas.translate(changingIcon2Left, changingIconTop)
|
||||
canvas.scale(changingIconScaleX, changingIconScaleY)
|
||||
appIcon2.alpha = changingIconAlpha
|
||||
appIcon2!!.alpha = changingIconAlpha
|
||||
appIcon2.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
@@ -312,7 +313,7 @@ class FloatingAppPairBackground(
|
||||
canvas.save()
|
||||
canvas.translate(changingIconLeft, changingIcon2Top)
|
||||
canvas.scale(changingIconScaleX, changingIconScaleY)
|
||||
appIcon2.alpha = changingIconAlpha
|
||||
appIcon2!!.alpha = changingIconAlpha
|
||||
appIcon2.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
@@ -325,7 +326,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
|
||||
@@ -273,6 +277,9 @@ class SplitAnimationControllerTest {
|
||||
doNothing()
|
||||
.whenever(spySplitAnimationController)
|
||||
.composeIconSplitLaunchAnimator(any(), any(), any(), any())
|
||||
doReturn(-1)
|
||||
.whenever(spySplitAnimationController)
|
||||
.hasChangesForBothAppPairs(any(), any())
|
||||
|
||||
spySplitAnimationController.playSplitLaunchAnimation(
|
||||
null /* launchingTaskView */,
|
||||
@@ -294,13 +301,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,
|
||||
@@ -316,7 +355,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