diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index ee9f070f67653e4d79d70fb00a22715b18f99efb..87e0b2867090c13d1b3fb11f9a723442aeb15191 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -22,6 +22,7 @@ android:orientation="vertical"> <LinearLayout + android:id="@+id/app_info_pill" android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_margin_top" @@ -66,6 +67,7 @@ </LinearLayout> <LinearLayout + android:id="@+id/windowing_pill" android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_handle_menu_windowing_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" @@ -116,6 +118,7 @@ </LinearLayout> <LinearLayout + android:id="@+id/more_actions_pill" android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index bb262d3df07fc92dd835cf8791566cbc7b8f9ef0..3aed9ebc6c5eac6005e6be817ea5b8e3da820027 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -455,7 +455,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Create and display handle menu window + * Create and display handle menu window. */ void createHandleMenu() { mHandleMenu = new HandleMenu.Builder(this) @@ -466,15 +466,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setLayoutId(mRelayoutParams.mLayoutResId) .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY) .setWindowingButtonsVisible(DesktopModeStatus.isEnabled()) + .setCaptionHeight(mResult.mCaptionHeight) .build(); + mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); } /** - * Close the handle menu window + * Close the handle menu window. */ void closeHandleMenu() { if (!isHandleMenuActive()) return; + mWindowDecorViewHolder.onHandleMenuClosed(); mHandleMenu.close(); mHandleMenu = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index 15f8f1cfadf2d38b31533ff310d4a68a93ed04e8..6391518b5911745ec1ce46e8830e070ecbba0993 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -71,10 +71,13 @@ class HandleMenu { private int mMenuHeight; private int mMenuWidth; + private final int mCaptionHeight; + HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY, View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, - Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill) { + Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill, + int captionHeight) { mParentDecor = parentDecor; mContext = mParentDecor.mDecorWindowContext; mTaskInfo = mParentDecor.mTaskInfo; @@ -86,6 +89,7 @@ class HandleMenu { mAppIcon = appIcon; mAppName = appName; mShouldShowWindowingPill = shouldShowWindowingPill; + mCaptionHeight = captionHeight; loadHandleMenuDimensions(); updateHandleMenuPillPositions(); } @@ -98,6 +102,7 @@ class HandleMenu { ssg.addTransaction(t); ssg.markSyncReady(); setupHandleMenu(); + animateHandleMenu(); } private void createHandleMenuWindow(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { @@ -108,6 +113,21 @@ class HandleMenu { t, ssg, x, y, mMenuWidth, mMenuHeight); } + /** + * Animates the appearance of the handle menu and its three pills. + */ + private void animateHandleMenu() { + final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView(); + final HandleMenuAnimator handleMenuAnimator = new HandleMenuAnimator(handleMenuView, + mMenuWidth, mCaptionHeight); + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { + handleMenuAnimator.animateCaptionHandleExpandToOpen(); + } else { + handleMenuAnimator.animateOpen(); + } + } + /** * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions * pill. @@ -322,6 +342,7 @@ class HandleMenu { private int mCaptionX; private int mCaptionY; private boolean mShowWindowingPill; + private int mCaptionHeight; Builder(@NonNull WindowDecoration parent) { @@ -364,9 +385,14 @@ class HandleMenu { return this; } + Builder setCaptionHeight(int captionHeight) { + mCaptionHeight = captionHeight; + return this; + } + HandleMenu build() { return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener, - mOnTouchListener, mAppIcon, mName, mShowWindowingPill); + mOnTouchListener, mAppIcon, mName, mShowWindowingPill, mCaptionHeight); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt new file mode 100644 index 0000000000000000000000000000000000000000..531de1f79ea82de94f7dbdb6a507e26e08838749 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2023 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.wm.shell.windowdecor + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.view.View +import android.view.View.ALPHA +import android.view.View.SCALE_X +import android.view.View.SCALE_Y +import android.view.View.TRANSLATION_Y +import android.view.View.TRANSLATION_Z +import android.view.ViewGroup +import androidx.core.view.children +import com.android.wm.shell.R +import com.android.wm.shell.animation.Interpolators + +/** Animates the Handle Menu opening. */ +class HandleMenuAnimator( + private val handleMenu: View, + private val menuWidth: Int, + private val captionHeight: Float +) { + companion object { + private const val MENU_Y_TRANSLATION_DURATION: Long = 150 + private const val HEADER_NONFREEFORM_SCALE_DURATION: Long = 150 + private const val HEADER_FREEFORM_SCALE_DURATION: Long = 217 + private const val HEADER_ELEVATION_DURATION: Long = 83 + private const val HEADER_CONTENT_ALPHA_DURATION: Long = 100 + private const val BODY_SCALE_DURATION: Long = 180 + private const val BODY_ALPHA_DURATION: Long = 150 + private const val BODY_ELEVATION_DURATION: Long = 83 + private const val BODY_CONTENT_ALPHA_DURATION: Long = 167 + + private const val ELEVATION_DELAY: Long = 33 + private const val HEADER_CONTENT_ALPHA_DELAY: Long = 67 + private const val BODY_SCALE_DELAY: Long = 50 + private const val BODY_ALPHA_DELAY: Long = 133 + + private const val HALF_INITIAL_SCALE: Float = 0.5f + private const val NONFREEFORM_HEADER_INITIAL_SCALE_X: Float = 0.6f + private const val NONFREEFORM_HEADER_INITIAL_SCALE_Y: Float = 0.05f + } + + private val animators: MutableList<Animator> = mutableListOf() + + private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) + private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) + private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) + + /** Animates the opening of the handle menu. */ + fun animateOpen() { + prepareMenuForAnimation() + appInfoPillExpand() + animateAppInfoPill() + animateWindowingPill() + animateMoreActionsPill() + runAnimations() + } + + /** + * Animates the opening of the handle menu. The caption handle in full screen and split screen + * will expand until it assumes the shape of the app info pill. Then, the other two pills will + * appear. + */ + fun animateCaptionHandleExpandToOpen() { + prepareMenuForAnimation() + captionHandleExpandIntoAppInfoPill() + animateAppInfoPill() + animateWindowingPill() + animateMoreActionsPill() + runAnimations() + } + + /** + * Prepares the handle menu for animation. Presets the opacity of necessary menu components. + * Presets pivots of handle menu and body pills for scaling animation. + */ + private fun prepareMenuForAnimation() { + // Preset opacity + appInfoPill.children.forEach { it.alpha = 0f } + windowingPill.alpha = 0f + moreActionsPill.alpha = 0f + + // Setup pivots. + handleMenu.pivotX = menuWidth / 2f + handleMenu.pivotY = 0f + + windowingPill.pivotX = menuWidth / 2f + windowingPill.pivotY = appInfoPill.measuredHeight.toFloat() + + moreActionsPill.pivotX = menuWidth / 2f + moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() + } + + private fun animateAppInfoPill() { + // Header Elevation Animation + animators += + ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Z, 1f).apply { + startDelay = ELEVATION_DELAY + duration = HEADER_ELEVATION_DURATION + } + + // Content Opacity Animation + appInfoPill.children.forEach { + animators += + ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + startDelay = HEADER_CONTENT_ALPHA_DELAY + duration = HEADER_CONTENT_ALPHA_DURATION + } + } + } + + private fun captionHandleExpandIntoAppInfoPill() { + // Header scaling animation + animators += + ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X, 1f) + .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION } + + animators += + ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y, 1f) + .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION } + + // Downward y-translation animation + val yStart: Float = -captionHeight / 2 + animators += + ObjectAnimator.ofFloat(handleMenu, TRANSLATION_Y, yStart, 0f).apply { + duration = MENU_Y_TRANSLATION_DURATION + } + } + + private fun appInfoPillExpand() { + // Header scaling animation + animators += + ObjectAnimator.ofFloat(appInfoPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + duration = HEADER_FREEFORM_SCALE_DURATION + } + + animators += + ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + duration = HEADER_FREEFORM_SCALE_DURATION + } + } + + private fun animateWindowingPill() { + // Windowing X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_DELAY + duration = BODY_SCALE_DURATION + } + + animators += + ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_DELAY + duration = BODY_SCALE_DURATION + } + + // Windowing Opacity Animation + animators += + ObjectAnimator.ofFloat(windowingPill, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_DELAY + duration = BODY_ALPHA_DURATION + } + + // Windowing Elevation Animation + animators += + ObjectAnimator.ofFloat(windowingPill, TRANSLATION_Z, 1f).apply { + startDelay = ELEVATION_DELAY + duration = BODY_ELEVATION_DURATION + } + + // Windowing Content Opacity Animation + windowingPill.children.forEach { + animators += + ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_DELAY + duration = BODY_CONTENT_ALPHA_DURATION + interpolator = Interpolators.FAST_OUT_SLOW_IN + } + } + } + + private fun animateMoreActionsPill() { + // More Actions X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_DELAY + duration = BODY_SCALE_DURATION + } + + animators += + ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_DELAY + duration = BODY_SCALE_DURATION + } + + // More Actions Opacity Animation + animators += + ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_DELAY + duration = BODY_ALPHA_DURATION + } + + // More Actions Elevation Animation + animators += + ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Z, 1f).apply { + startDelay = ELEVATION_DELAY + duration = BODY_ELEVATION_DURATION + } + + // More Actions Content Opacity Animation + moreActionsPill.children.forEach { + animators += + ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_DELAY + duration = BODY_CONTENT_ALPHA_DURATION + interpolator = Interpolators.FAST_OUT_SLOW_IN + } + } + } + + /** Runs the list of animators concurrently. */ + private fun runAnimations() { + val animatorSet = AnimatorSet() + animatorSet.playTogether(animators) + animatorSet.start() + animators.clear() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 044c0331282c93f45e3a6481c322275f4267ede3..634b7558c7d81360998732f6aa4b4be1edc6ca59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -269,10 +269,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); final int captionWidth = taskBounds.width(); - startT.setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight) + startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight) .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER) .show(mCaptionContainerSurface); @@ -283,7 +283,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionInsetsRect.set(taskBounds); if (mIsCaptionVisible) { mCaptionInsetsRect.bottom = - mCaptionInsetsRect.top + captionHeight + params.mCaptionY; + mCaptionInsetsRect.top + outResult.mCaptionHeight + params.mCaptionY; wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect); wct.addInsetsSource(mTaskInfo.token, @@ -348,7 +348,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption view mCaptionWindowManager.setConfiguration(taskConfig); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(captionWidth, captionHeight, + new WindowManager.LayoutParams(captionWidth, outResult.mCaptionHeight, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); lp.setTitle("Caption of Task=" + mTaskInfo.taskId); @@ -569,6 +569,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } static class RelayoutResult<T extends View & TaskFocusStateConsumer> { + int mCaptionHeight; int mWidth; int mHeight; T mRootView; @@ -576,6 +577,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> void reset() { mWidth = 0; mHeight = 0; + mCaptionHeight = 0; mRootView = null; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index 6b59ccec5148bf9efcd0c807c98c514bd40e8aab..400dec4df50645fc20e809f430835f5b50d11bfa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -48,7 +48,6 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { - val captionDrawable = captionView.background as GradientDrawable taskInfo.taskDescription?.statusBarColor?.let { captionDrawable.setColor(it) @@ -63,6 +62,10 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo)) } + override fun onHandleMenuOpened() {} + + override fun onHandleMenuClosed() {} + private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_app_name_light) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index 9374ac95e83dd9e51a723fb51a295acab9d8c615..9dc86db4f59bb2c2aee1222cef8852e9458d83a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -1,11 +1,13 @@ package com.android.wm.shell.windowdecor.viewholder +import android.animation.ObjectAnimator import android.app.ActivityManager.RunningTaskInfo import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable import android.view.View import android.widget.ImageButton import com.android.wm.shell.R +import com.android.wm.shell.animation.Interpolators /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It @@ -17,6 +19,10 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( onCaptionButtonClickListener: View.OnClickListener ) : DesktopModeWindowDecorationViewHolder(rootView) { + companion object { + private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 + } + private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) @@ -35,6 +41,14 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } + override fun onHandleMenuOpened() { + animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) + } + + override fun onHandleMenuClosed() { + animateCaptionHandleAlpha(startValue = 1f, endValue = 0f) + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) @@ -42,4 +56,14 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( context.getColor(R.color.desktop_mode_caption_handle_bar_dark) } } + + /** Animate appearance/disappearance of caption handle as the handle menu is animated. */ + private fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { + val animator = + ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply { + duration = CAPTION_HANDLE_ANIMATION_DURATION + interpolator = Interpolators.FAST_OUT_SLOW_IN + } + animator.start() + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt index 49e8d15dcc02db21fc07192d889263cef8028ee1..8b405f02ef2965a6faf815fbdd565238fd0b2759 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -35,4 +35,10 @@ internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { } } ?: false } + + /** Callback when the handle menu is opened. */ + abstract fun onHandleMenuOpened() + + /** Callback when the handle menu is closed. */ + abstract fun onHandleMenuClosed() }