Loading core/java/android/window/DesktopExperienceFlags.java +2 −0 Original line number Diff line number Diff line Loading @@ -231,6 +231,8 @@ public enum DesktopExperienceFlags { Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS), ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, true, Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE), ENABLE_REENABLE_APP_HANDLE_ANIMATIONS(Flags::reenableAppHandleAnimations, false, Flags.FLAG_REENABLE_APP_HANDLE_ANIMATIONS), ENABLE_REJECT_HOME_TRANSITION( Flags::enableRejectHomeTransition, true, Flags.FLAG_ENABLE_REJECT_HOME_TRANSITION), Loading core/java/android/window/flags/lse_desktop_experience.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1717,6 +1717,16 @@ flag { } } flag { name: "reenable_app_handle_animations" namespace: "lse_desktop_experience" description: "Re-enables the app handle animations" bug: "412647178" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "remove_desk_on_last_task_removal" namespace: "lse_desktop_experience" Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt +10 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor.common import android.annotation.ColorInt import android.annotation.IntRange import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityManager.TaskDescription import android.content.Context import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK Loading Loading @@ -71,9 +72,17 @@ internal class DecorThemeUtil(private val context: Context) { /** Returns the [Theme] used by the app with the given [RunningTaskInfo]. */ fun getAppTheme(task: RunningTaskInfo): Theme { return task.taskDescription?.let { getAppTheme(it) } ?: systemTheme } /** Returns the [Theme] used by the app with the given [TaskDescription]. */ fun getAppTheme(description: TaskDescription): Theme { // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the // TaskInfo/TaskDescription. val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme val backgroundColor = description.backgroundColor if (backgroundColor == Color.TRANSPARENT) { return systemTheme } return if (Color.valueOf(backgroundColor).luminance() < 0.5) { Theme.DARK } else { Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt +266 −35 Original line number Diff line number Diff line Loading @@ -14,42 +14,80 @@ * limitations under the License. */ package com.android.wm.shell.windowdecor package com.android.wm.shell.windowdecor.viewholder import android.animation.Animator import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.ColorInt import android.content.res.ColorStateList import android.graphics.Color import android.os.SystemProperties import android.view.View import android.view.View.Visibility import android.view.animation.PathInterpolator import android.widget.ImageButton import android.window.DesktopExperienceFlags import androidx.core.animation.doOnEnd import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION import com.android.wm.shell.shared.animation.Interpolators /** Animates the Desktop View's app handle. */ class AppHandleAnimator(private val appHandleView: View, private val captionHandle: ImageButton) { class AppHandleAnimator(appHandleView: View, private val captionHandle: ImageButton) { companion object { private val DEBUG_ANIMATOR_STEPS = SystemProperties.getBoolean( "persist.wm.debug.window_decoration_app_handle_visibility_anim_debug_steps", false, ) // Constants for animating the whole caption private const val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS: Long = 275L private const val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS: Long = 340 private val APP_HANDLE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f) @VisibleForTesting val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_fade_in_duration_ms", 275L, ) @VisibleForTesting val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_fade_out_duration_ms", 340L, ) private val APP_HANDLE_COLOR_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_color_duration_ms", 275L, ) @VisibleForTesting val APP_HANDLE_FADE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f) // Constants for animating the caption's handle private const val HANDLE_ANIMATION_DURATION: Long = 100 private val HANDLE_ANIMATION_INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN } private val visibilityAnimator = VisibilityAnimator(appHandleView) private val colorAnimator = ColorAnimator(captionHandle) private var animator: ObjectAnimator? = null /** Animates the given caption view to the given visibility after a visibility change. */ fun animateVisibilityChange(@Visibility visible: Int) { when (visible) { View.VISIBLE -> animateShowAppHandle() else -> animateHideAppHandle() /** Animates the app handle to the given visibility after a visibility change. */ fun animateVisibilityChange(visible: Boolean) { cancelCaptionHandleAlphaAnimation() visibilityAnimator.animate(visible) } /** Animates the app handle to the given color. */ fun animateColorChange(@ColorInt color: Int) { colorAnimator.animate(color) } /** Animate appearance/disappearance of caption's handle. */ fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { cancel() cancelCaptionHandleAlphaAnimation() visibilityAnimator.cancel() animator = ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply { duration = HANDLE_ANIMATION_DURATION Loading @@ -58,33 +96,226 @@ class AppHandleAnimator(private val appHandleView: View, private val captionHand } } private fun animateShowAppHandle() { cancel() appHandleView.alpha = 0f appHandleView.visibility = View.VISIBLE animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 1f).apply { duration = APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR start() private fun cancelCaptionHandleAlphaAnimation() { animator?.removeAllListeners() animator?.cancel() animator = null } /** Cancels any active animations. */ fun cancel() { if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { visibilityAnimator.cancel() colorAnimator.cancel() } cancelCaptionHandleAlphaAnimation() } /** Returns the current visibility animator. */ @VisibleForTesting fun getAnimator(): ValueAnimator? = visibilityAnimator.animator private class VisibilityAnimator(private val targetView: View) { private enum class Target( val start: Float, val end: Float, @Visibility val viewVisibility: Int, val duration: Long, ) { VISIBLE(0f, 1f, View.VISIBLE, APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS), INVISIBLE(1f, 0f, View.GONE, APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS), } private var currentAnimator: ObjectAnimator? = null private var currentTarget: Target? = null private val doOnEnd = DoOnEnd(targetView) /** The current animator. */ @VisibleForTesting val animator: ValueAnimator? get() = currentAnimator /** Animates the target view. */ fun animate(visible: Boolean) { animate(if (visible) Target.VISIBLE else Target.INVISIBLE) } /** Cancels the ongoing animation. */ fun cancel() { currentAnimator?.removeAllListeners() currentAnimator?.cancel() reset() } private fun animate(target: Target) { val inProgress = currentTarget logD("animate from=%s to=%s", inProgress?.name, target.name) when { inProgress == null -> { // Not animating, animate from start if needed. if (targetView.visibility == target.viewVisibility) { logD("skipping animation, already at target") return } else { logD("animating from start") targetView.visibility = View.VISIBLE targetView.alpha = target.start doOnEnd.target = target currentAnimator = newAnimator(target).apply { start() } } } target == inProgress -> { logD("skipping animation, already animating to target") return } else -> { logD("was animating to opposite target, reversing") doOnEnd.target = target currentAnimator?.reverse() } } currentTarget = target } @OptIn(ExperimentalStdlibApi::class) private fun newAnimator(target: Target): ObjectAnimator = ObjectAnimator.ofFloat(targetView, View.ALPHA, target.end).apply { duration = target.duration interpolator = APP_HANDLE_FADE_ANIMATION_INTERPOLATOR if (DEBUG_ANIMATOR_STEPS) { addUpdateListener { animator -> logD( "update: animator=ObjectAnimator@%s f=%f alpha=%f", animator.hashCode().toHexString(), animator.animatedFraction, animator.animatedValue, ) } } doOnEnd(doOnEnd) } private fun reset() { currentAnimator = null currentTarget = null } private inner class DoOnEnd(private val targetView: View) : (Animator) -> Unit { var target: Target? = null override fun invoke(animator: Animator) { if (DEBUG_ANIMATOR_STEPS) { logD("end: target=%s", target) } target?.let { targetView.visibility = it.viewVisibility } target = null reset() } } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } companion object { private const val TAG = "AppHandleVisibilityAnimator" } } private class ColorAnimator(private val targetView: ImageButton) { private var currentAnimator: ValueAnimator? = null @ColorInt private var currentTarget: Int? = null private fun animateHideAppHandle() { /** Animates the target view. */ fun animate(@ColorInt color: Int) { val inProgress = currentTarget when { color == inProgress -> { logD("skipping animation, already animating to target") return } else -> { if (inProgress != null) { logD("was animating to other target, cancelling first") currentAnimator?.apply { removeAllListeners() cancel() animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 0f).apply { duration = APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR doOnEnd { appHandleView.visibility = View.GONE } start() } currentAnimator = null } val fromColor = getCurrentColor() if (fromColor == color) { logD("skipping animation, already at target") return } if (fromColor == null) { logD("skipping animation, no current color to animate from") targetView.imageTintList = ColorStateList.valueOf(color) return } logD("animate from=%s to=%s", fromColor.toArgbString(), color.toArgbString()) currentAnimator = newAnimator(fromColor, color).apply { start() } } } currentTarget = color } /** Cancels any active animations. */ /** Cancels the ongoing animation. */ fun cancel() { animator?.removeAllListeners() animator?.cancel() animator = null currentAnimator?.removeAllListeners() currentAnimator?.cancel() reset() } private fun getCurrentColor() = targetView.imageTintList?.defaultColor private fun reset() { currentAnimator = null currentTarget = null } @OptIn(ExperimentalStdlibApi::class) private fun newAnimator(@ColorInt from: Int, @ColorInt to: Int): ValueAnimator = ValueAnimator.ofArgb(from, to) .setDuration(APP_HANDLE_COLOR_ANIMATION_DURATION_MS) .apply { addUpdateListener { animator -> targetView.imageTintList = ColorStateList.valueOf(animator.animatedValue as Int) if (DEBUG_ANIMATOR_STEPS) { logD( "update: animator=ValueAnimator@%s f=%f color=%f", animator.hashCode().toHexString(), animator.animatedFraction, (animator.animatedValue as Int).toArgbString(), ) } } doOnEnd { animator -> if (DEBUG_ANIMATOR_STEPS) { logD( "end: animator=ValueAnimator@%s", animator.hashCode().toHexString(), ) } reset() } } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } private fun @receiver:ColorInt Int.toArgbString() = String.format( "#%02X%02X%02X%02X", Color.alpha(this), Color.red(this), Color.green(this), Color.blue(this), ) companion object { private const val TAG = "AppHandleColorAnimator" } } } libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +65 −15 Original line number Diff line number Diff line Loading @@ -15,20 +15,22 @@ */ package com.android.wm.shell.windowdecor.viewholder import android.animation.ValueAnimator import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Point import android.hardware.input.InputManager import android.os.Bundle import android.os.Handler import android.view.InsetsFlags import android.view.LayoutInflater import android.view.MotionEvent.ACTION_DOWN import android.view.SurfaceControl import android.view.View import android.view.View.OnClickListener import android.view.ViewDebug import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.view.WindowManager import android.view.accessibility.AccessibilityEvent Loading @@ -39,16 +41,20 @@ import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.SystemBarUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_OPENED import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.windowdecor.AppHandleAnimator import com.android.wm.shell.windowdecor.WindowDecorLinearLayout import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.Theme import com.android.wm.shell.windowdecor.extension.identityHashCode /** Loading Loading @@ -86,6 +92,7 @@ class AppHandleViewHolder( private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) private val inputManager = context.getSystemService(InputManager::class.java) private val decorThemeUtil = DecorThemeUtil(context) private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle) private var statusBarInputLayerExists = false Loading Loading @@ -144,7 +151,11 @@ class AppHandleViewHolder( isCaptionVisible: Boolean, ) { setVisibility(isCaptionVisible) if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { animator.animateColorChange(getCaptionHandleBarColor(taskInfo)) } else { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } this.taskInfo = taskInfo if ( DesktopExperienceFlags.ENABLE_REMOVE_STATUS_BAR_INPUT_LAYER.isTrue && Loading Loading @@ -300,6 +311,10 @@ class AppHandleViewHolder( } private fun setVisibility(visible: Boolean) { if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { animator.animateVisibilityChange(visible) return } val v = if (visible) View.VISIBLE else View.GONE if ( captionView.visibility == v || Loading @@ -307,11 +322,10 @@ class AppHandleViewHolder( ) { return } // TODO(b/405251465): animate app handle visibility change after creation and animation are // moved to a background thread. captionView.visibility = v } @ColorInt private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) Loading @@ -325,16 +339,42 @@ class AppHandleViewHolder( * with the caption background color. */ private fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { return taskInfo.taskDescription?.let { taskDescription -> if ( Color.alpha(taskDescription.statusBarColor) != 0 && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM ) { Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 } else { taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 val description = taskInfo.taskDescription if (description == null) { logD("color calculation: using light color, reason: null description") return false } if (description.systemBarsAppearance == 0) { val bgColor = description.backgroundColor when (decorThemeUtil.getAppTheme(description)) { Theme.LIGHT -> { logD( "color calculation: using light color, reason: light app theme (bgColor=%s)", bgColor, ) return false } Theme.DARK -> { logD( "color calculation: using dark color, reason: dark app theme (bgColor=%s)", bgColor, ) return true } } } ?: false } val hasDarkSystemBarsAppearance = description.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 logD( "color calculation: using %s color, reason: systemBarsAppearance=%s", if (hasDarkSystemBarsAppearance) "light" else "dark", ViewDebug.flagsToString( InsetsFlags::class.java, "appearance", description.systemBarsAppearance, ), ) return hasDarkSystemBarsAppearance } /** Sets whether the caption's handle is currently being hovered over. */ Loading @@ -355,6 +395,12 @@ class AppHandleViewHolder( animator.cancel() } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } @VisibleForTesting fun getAnimator(): ValueAnimator? = animator.getAnimator() @OptIn(ExperimentalStdlibApi::class) override fun toString(): String { return "AppHandleViewHolder(" + "rootView=${rootView.identityHashCode.toHexString()}" + ")" Loading Loading @@ -385,4 +431,8 @@ class AppHandleViewHolder( desktopModeUiEventLogger, ) } companion object { private const val TAG = "AppHandleViewHolder" } } Loading
core/java/android/window/DesktopExperienceFlags.java +2 −0 Original line number Diff line number Diff line Loading @@ -231,6 +231,8 @@ public enum DesktopExperienceFlags { Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS), ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, true, Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE), ENABLE_REENABLE_APP_HANDLE_ANIMATIONS(Flags::reenableAppHandleAnimations, false, Flags.FLAG_REENABLE_APP_HANDLE_ANIMATIONS), ENABLE_REJECT_HOME_TRANSITION( Flags::enableRejectHomeTransition, true, Flags.FLAG_ENABLE_REJECT_HOME_TRANSITION), Loading
core/java/android/window/flags/lse_desktop_experience.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1717,6 +1717,16 @@ flag { } } flag { name: "reenable_app_handle_animations" namespace: "lse_desktop_experience" description: "Re-enables the app handle animations" bug: "412647178" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "remove_desk_on_last_task_removal" namespace: "lse_desktop_experience" Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt +10 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor.common import android.annotation.ColorInt import android.annotation.IntRange import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityManager.TaskDescription import android.content.Context import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK Loading Loading @@ -71,9 +72,17 @@ internal class DecorThemeUtil(private val context: Context) { /** Returns the [Theme] used by the app with the given [RunningTaskInfo]. */ fun getAppTheme(task: RunningTaskInfo): Theme { return task.taskDescription?.let { getAppTheme(it) } ?: systemTheme } /** Returns the [Theme] used by the app with the given [TaskDescription]. */ fun getAppTheme(description: TaskDescription): Theme { // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the // TaskInfo/TaskDescription. val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme val backgroundColor = description.backgroundColor if (backgroundColor == Color.TRANSPARENT) { return systemTheme } return if (Color.valueOf(backgroundColor).luminance() < 0.5) { Theme.DARK } else { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt +266 −35 Original line number Diff line number Diff line Loading @@ -14,42 +14,80 @@ * limitations under the License. */ package com.android.wm.shell.windowdecor package com.android.wm.shell.windowdecor.viewholder import android.animation.Animator import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.ColorInt import android.content.res.ColorStateList import android.graphics.Color import android.os.SystemProperties import android.view.View import android.view.View.Visibility import android.view.animation.PathInterpolator import android.widget.ImageButton import android.window.DesktopExperienceFlags import androidx.core.animation.doOnEnd import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION import com.android.wm.shell.shared.animation.Interpolators /** Animates the Desktop View's app handle. */ class AppHandleAnimator(private val appHandleView: View, private val captionHandle: ImageButton) { class AppHandleAnimator(appHandleView: View, private val captionHandle: ImageButton) { companion object { private val DEBUG_ANIMATOR_STEPS = SystemProperties.getBoolean( "persist.wm.debug.window_decoration_app_handle_visibility_anim_debug_steps", false, ) // Constants for animating the whole caption private const val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS: Long = 275L private const val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS: Long = 340 private val APP_HANDLE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f) @VisibleForTesting val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_fade_in_duration_ms", 275L, ) @VisibleForTesting val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_fade_out_duration_ms", 340L, ) private val APP_HANDLE_COLOR_ANIMATION_DURATION_MS = SystemProperties.getLong( "persist.wm.debug.window_decoration_app_handle_color_duration_ms", 275L, ) @VisibleForTesting val APP_HANDLE_FADE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f) // Constants for animating the caption's handle private const val HANDLE_ANIMATION_DURATION: Long = 100 private val HANDLE_ANIMATION_INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN } private val visibilityAnimator = VisibilityAnimator(appHandleView) private val colorAnimator = ColorAnimator(captionHandle) private var animator: ObjectAnimator? = null /** Animates the given caption view to the given visibility after a visibility change. */ fun animateVisibilityChange(@Visibility visible: Int) { when (visible) { View.VISIBLE -> animateShowAppHandle() else -> animateHideAppHandle() /** Animates the app handle to the given visibility after a visibility change. */ fun animateVisibilityChange(visible: Boolean) { cancelCaptionHandleAlphaAnimation() visibilityAnimator.animate(visible) } /** Animates the app handle to the given color. */ fun animateColorChange(@ColorInt color: Int) { colorAnimator.animate(color) } /** Animate appearance/disappearance of caption's handle. */ fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { cancel() cancelCaptionHandleAlphaAnimation() visibilityAnimator.cancel() animator = ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply { duration = HANDLE_ANIMATION_DURATION Loading @@ -58,33 +96,226 @@ class AppHandleAnimator(private val appHandleView: View, private val captionHand } } private fun animateShowAppHandle() { cancel() appHandleView.alpha = 0f appHandleView.visibility = View.VISIBLE animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 1f).apply { duration = APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR start() private fun cancelCaptionHandleAlphaAnimation() { animator?.removeAllListeners() animator?.cancel() animator = null } /** Cancels any active animations. */ fun cancel() { if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { visibilityAnimator.cancel() colorAnimator.cancel() } cancelCaptionHandleAlphaAnimation() } /** Returns the current visibility animator. */ @VisibleForTesting fun getAnimator(): ValueAnimator? = visibilityAnimator.animator private class VisibilityAnimator(private val targetView: View) { private enum class Target( val start: Float, val end: Float, @Visibility val viewVisibility: Int, val duration: Long, ) { VISIBLE(0f, 1f, View.VISIBLE, APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS), INVISIBLE(1f, 0f, View.GONE, APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS), } private var currentAnimator: ObjectAnimator? = null private var currentTarget: Target? = null private val doOnEnd = DoOnEnd(targetView) /** The current animator. */ @VisibleForTesting val animator: ValueAnimator? get() = currentAnimator /** Animates the target view. */ fun animate(visible: Boolean) { animate(if (visible) Target.VISIBLE else Target.INVISIBLE) } /** Cancels the ongoing animation. */ fun cancel() { currentAnimator?.removeAllListeners() currentAnimator?.cancel() reset() } private fun animate(target: Target) { val inProgress = currentTarget logD("animate from=%s to=%s", inProgress?.name, target.name) when { inProgress == null -> { // Not animating, animate from start if needed. if (targetView.visibility == target.viewVisibility) { logD("skipping animation, already at target") return } else { logD("animating from start") targetView.visibility = View.VISIBLE targetView.alpha = target.start doOnEnd.target = target currentAnimator = newAnimator(target).apply { start() } } } target == inProgress -> { logD("skipping animation, already animating to target") return } else -> { logD("was animating to opposite target, reversing") doOnEnd.target = target currentAnimator?.reverse() } } currentTarget = target } @OptIn(ExperimentalStdlibApi::class) private fun newAnimator(target: Target): ObjectAnimator = ObjectAnimator.ofFloat(targetView, View.ALPHA, target.end).apply { duration = target.duration interpolator = APP_HANDLE_FADE_ANIMATION_INTERPOLATOR if (DEBUG_ANIMATOR_STEPS) { addUpdateListener { animator -> logD( "update: animator=ObjectAnimator@%s f=%f alpha=%f", animator.hashCode().toHexString(), animator.animatedFraction, animator.animatedValue, ) } } doOnEnd(doOnEnd) } private fun reset() { currentAnimator = null currentTarget = null } private inner class DoOnEnd(private val targetView: View) : (Animator) -> Unit { var target: Target? = null override fun invoke(animator: Animator) { if (DEBUG_ANIMATOR_STEPS) { logD("end: target=%s", target) } target?.let { targetView.visibility = it.viewVisibility } target = null reset() } } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } companion object { private const val TAG = "AppHandleVisibilityAnimator" } } private class ColorAnimator(private val targetView: ImageButton) { private var currentAnimator: ValueAnimator? = null @ColorInt private var currentTarget: Int? = null private fun animateHideAppHandle() { /** Animates the target view. */ fun animate(@ColorInt color: Int) { val inProgress = currentTarget when { color == inProgress -> { logD("skipping animation, already animating to target") return } else -> { if (inProgress != null) { logD("was animating to other target, cancelling first") currentAnimator?.apply { removeAllListeners() cancel() animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 0f).apply { duration = APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR doOnEnd { appHandleView.visibility = View.GONE } start() } currentAnimator = null } val fromColor = getCurrentColor() if (fromColor == color) { logD("skipping animation, already at target") return } if (fromColor == null) { logD("skipping animation, no current color to animate from") targetView.imageTintList = ColorStateList.valueOf(color) return } logD("animate from=%s to=%s", fromColor.toArgbString(), color.toArgbString()) currentAnimator = newAnimator(fromColor, color).apply { start() } } } currentTarget = color } /** Cancels any active animations. */ /** Cancels the ongoing animation. */ fun cancel() { animator?.removeAllListeners() animator?.cancel() animator = null currentAnimator?.removeAllListeners() currentAnimator?.cancel() reset() } private fun getCurrentColor() = targetView.imageTintList?.defaultColor private fun reset() { currentAnimator = null currentTarget = null } @OptIn(ExperimentalStdlibApi::class) private fun newAnimator(@ColorInt from: Int, @ColorInt to: Int): ValueAnimator = ValueAnimator.ofArgb(from, to) .setDuration(APP_HANDLE_COLOR_ANIMATION_DURATION_MS) .apply { addUpdateListener { animator -> targetView.imageTintList = ColorStateList.valueOf(animator.animatedValue as Int) if (DEBUG_ANIMATOR_STEPS) { logD( "update: animator=ValueAnimator@%s f=%f color=%f", animator.hashCode().toHexString(), animator.animatedFraction, (animator.animatedValue as Int).toArgbString(), ) } } doOnEnd { animator -> if (DEBUG_ANIMATOR_STEPS) { logD( "end: animator=ValueAnimator@%s", animator.hashCode().toHexString(), ) } reset() } } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } private fun @receiver:ColorInt Int.toArgbString() = String.format( "#%02X%02X%02X%02X", Color.alpha(this), Color.red(this), Color.green(this), Color.blue(this), ) companion object { private const val TAG = "AppHandleColorAnimator" } } }
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +65 −15 Original line number Diff line number Diff line Loading @@ -15,20 +15,22 @@ */ package com.android.wm.shell.windowdecor.viewholder import android.animation.ValueAnimator import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Point import android.hardware.input.InputManager import android.os.Bundle import android.os.Handler import android.view.InsetsFlags import android.view.LayoutInflater import android.view.MotionEvent.ACTION_DOWN import android.view.SurfaceControl import android.view.View import android.view.View.OnClickListener import android.view.ViewDebug import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.view.WindowManager import android.view.accessibility.AccessibilityEvent Loading @@ -39,16 +41,20 @@ import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.SystemBarUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_OPENED import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.windowdecor.AppHandleAnimator import com.android.wm.shell.windowdecor.WindowDecorLinearLayout import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.Theme import com.android.wm.shell.windowdecor.extension.identityHashCode /** Loading Loading @@ -86,6 +92,7 @@ class AppHandleViewHolder( private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) private val inputManager = context.getSystemService(InputManager::class.java) private val decorThemeUtil = DecorThemeUtil(context) private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle) private var statusBarInputLayerExists = false Loading Loading @@ -144,7 +151,11 @@ class AppHandleViewHolder( isCaptionVisible: Boolean, ) { setVisibility(isCaptionVisible) if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { animator.animateColorChange(getCaptionHandleBarColor(taskInfo)) } else { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } this.taskInfo = taskInfo if ( DesktopExperienceFlags.ENABLE_REMOVE_STATUS_BAR_INPUT_LAYER.isTrue && Loading Loading @@ -300,6 +311,10 @@ class AppHandleViewHolder( } private fun setVisibility(visible: Boolean) { if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) { animator.animateVisibilityChange(visible) return } val v = if (visible) View.VISIBLE else View.GONE if ( captionView.visibility == v || Loading @@ -307,11 +322,10 @@ class AppHandleViewHolder( ) { return } // TODO(b/405251465): animate app handle visibility change after creation and animation are // moved to a background thread. captionView.visibility = v } @ColorInt private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) Loading @@ -325,16 +339,42 @@ class AppHandleViewHolder( * with the caption background color. */ private fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { return taskInfo.taskDescription?.let { taskDescription -> if ( Color.alpha(taskDescription.statusBarColor) != 0 && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM ) { Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 } else { taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 val description = taskInfo.taskDescription if (description == null) { logD("color calculation: using light color, reason: null description") return false } if (description.systemBarsAppearance == 0) { val bgColor = description.backgroundColor when (decorThemeUtil.getAppTheme(description)) { Theme.LIGHT -> { logD( "color calculation: using light color, reason: light app theme (bgColor=%s)", bgColor, ) return false } Theme.DARK -> { logD( "color calculation: using dark color, reason: dark app theme (bgColor=%s)", bgColor, ) return true } } } ?: false } val hasDarkSystemBarsAppearance = description.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 logD( "color calculation: using %s color, reason: systemBarsAppearance=%s", if (hasDarkSystemBarsAppearance) "light" else "dark", ViewDebug.flagsToString( InsetsFlags::class.java, "appearance", description.systemBarsAppearance, ), ) return hasDarkSystemBarsAppearance } /** Sets whether the caption's handle is currently being hovered over. */ Loading @@ -355,6 +395,12 @@ class AppHandleViewHolder( animator.cancel() } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments) } @VisibleForTesting fun getAnimator(): ValueAnimator? = animator.getAnimator() @OptIn(ExperimentalStdlibApi::class) override fun toString(): String { return "AppHandleViewHolder(" + "rootView=${rootView.identityHashCode.toHexString()}" + ")" Loading Loading @@ -385,4 +431,8 @@ class AppHandleViewHolder( desktopModeUiEventLogger, ) } companion object { private const val TAG = "AppHandleViewHolder" } }