Loading libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml 0 → 100644 +28 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- ~ 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. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/backLayer"> <shape android:shape="rectangle"> <solid android:color="#000000" /> </shape> </item> <item android:id="@+id/frontLayer"> <shape android:shape="rectangle"> <solid android:color="#000000" /> </shape> </item> </layer-list> No newline at end of file libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +1 −1 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/desktop_mode_caption" android:background="@drawable/desktop_mode_header_background" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" Loading Loading @@ -96,7 +97,6 @@ android:paddingHorizontal="10dp" android:paddingVertical="8dp" android:layout_marginEnd="8dp" android:tint="?androidprv:attr/materialColorOnSurface" android:background="?android:selectableItemBackgroundBorderless" android:contentDescription="@string/close_button_text" android:src="@drawable/desktop_mode_header_ic_close" Loading libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +0 −2 Original line number Diff line number Diff line Loading @@ -31,8 +31,6 @@ android:layout_height="34dp" android:padding="5dp" android:contentDescription="@string/maximize_button_text" android:tint="?androidprv:attr/materialColorOnSurface" android:background="?android:selectableItemBackgroundBorderless" android:src="@drawable/decor_desktop_mode_maximize_button_dark" android:scaleType="fitCenter" /> </merge> No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +64 −7 Original line number Diff line number Diff line Loading @@ -20,15 +20,19 @@ import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.RippleDrawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ProgressBar import androidx.annotation.ColorInt import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat import com.android.window.flags.Flags import com.android.wm.shell.R private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350 Loading Loading @@ -90,7 +94,44 @@ class MaximizeButtonView( progressBar.visibility = View.INVISIBLE } fun setAnimationTints(darkMode: Boolean) { /** * Set the color tints of the maximize button views. * * @param darkMode whether the app's theme is in dark mode. * @param iconForegroundColor the color tint to use for the maximize icon to match the rest of * the App Header icons * @param baseForegroundColor the base foreground color tint used by the App Header, used to style * views within this button using the same base color but with different opacities. */ fun setAnimationTints( darkMode: Boolean, iconForegroundColor: ColorStateList? = null, baseForegroundColor: Int? = null ) { if (Flags.enableThemedAppHeaders()) { requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" } requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" } maximizeWindow.imageTintList = iconForegroundColor maximizeWindow.background = RippleDrawable( ColorStateList( arrayOf( intArrayOf(android.R.attr.state_hovered), intArrayOf(android.R.attr.state_pressed), intArrayOf(), ), intArrayOf( replaceColorAlpha(baseForegroundColor, OPACITY_8), replaceColorAlpha(baseForegroundColor, OPACITY_12), Color.TRANSPARENT ) ), null, null ) progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor) .withAlpha(OPACITY_12) progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) } else { if (darkMode) { progressBar.progressTintList = ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark)) Loading @@ -104,3 +145,19 @@ class MaximizeButtonView( } } } @ColorInt private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { return Color.argb( alpha, Color.red(color), Color.green(color), Color.blue(color) ) } companion object { private const val OPACITY_8 = 20 private const val OPACITY_12 = 31 } } libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +327 −0 Original line number Diff line number Diff line Loading @@ -4,8 +4,11 @@ import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.content.res.ColorStateList import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.view.View import android.view.View.OnLongClickListener import android.widget.ImageButton Loading @@ -15,10 +18,13 @@ import androidx.core.content.withStyledAttributes import androidx.core.view.isVisible import com.android.internal.R.attr.materialColorOnSecondaryContainer import com.android.internal.R.attr.materialColorOnSurface import com.android.internal.R.attr.materialColorOnSurfaceInverse import com.android.internal.R.attr.materialColorSecondaryContainer import com.android.internal.R.attr.materialColorSurfaceContainerHigh import com.android.internal.R.attr.materialColorSurfaceContainerLow import com.android.internal.R.attr.materialColorSurfaceDim import com.android.internal.R.attr.materialColorSurfaceInverse import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance Loading Loading @@ -71,6 +77,14 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { if (Flags.enableThemedAppHeaders()) { bindDataWithThemedHeaders(taskInfo) } else { bindDataLegacy(taskInfo) } } private fun bindDataLegacy(taskInfo: RunningTaskInfo) { captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo)) val color = getAppNameAndButtonColor(taskInfo) val alpha = Color.alpha(color) Loading @@ -87,6 +101,45 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( maximizeButtonView.setAnimationTints(isDarkMode()) } private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) { val header = fillHeaderInfo(taskInfo) val headerStyle = getHeaderStyle(header) // Caption Background val headerBackground = captionView.background as LayerDrawable val backLayer = headerBackground.findDrawableByLayerId(R.id.backLayer) as GradientDrawable val frontLayer = headerBackground.findDrawableByLayerId(R.id.frontLayer) as GradientDrawable when (headerStyle.background) { is HeaderStyle.Background.Opaque -> { backLayer.setColor(headerStyle.background.backLayerColor ?: Color.BLACK) frontLayer.setColor(headerStyle.background.frontLayerColor) frontLayer.alpha = headerStyle.background.frontLayerOpacity } HeaderStyle.Background.Transparent -> { backLayer.setColor(Color.TRANSPARENT) frontLayer.setColor(Color.TRANSPARENT) frontLayer.alpha = OPACITY_100 } } // Caption Foreground val foregroundColor = headerStyle.foreground.color val foregroundAlpha = headerStyle.foreground.opacity val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha) closeWindowButton.imageTintList = colorStateList expandMenuButton.imageTintList = colorStateList with (appNameTextView) { isVisible = header.type == Header.Type.DEFAULT setTextColor(colorStateList) } appIconImageView.imageAlpha = foregroundAlpha maximizeButtonView.setAnimationTints( darkMode = header.appTheme == Header.Theme.DARK, iconForegroundColor = colorStateList, baseForegroundColor = foregroundColor ) } override fun onHandleMenuOpened() {} override fun onHandleMenuClosed() {} Loading @@ -107,6 +160,273 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( maximizeButtonView.startHoverAnimation() } private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( background = getHeaderBackground(header), foreground = getHeaderForeground(header) ) } private fun getHeaderBackground( header: Header ): HeaderStyle.Background { when (header.type) { Header.Type.DEFAULT -> { if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSecondaryContainer), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceContainerLow), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceContainerHigh), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceDim), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_30, backLayerColor = Color.BLACK ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_55, backLayerColor = Color.WHITE ) } error("No other combination expected header=$header") } Header.Type.CUSTOM -> return HeaderStyle.Background.Transparent } } private fun getHeaderForeground(header: Header): HeaderStyle.Foreground { when (header.type) { Header.Type.DEFAULT -> { if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_55 ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_70 ) } error("No other combination expected header=$header") } Header.Type.CUSTOM -> { if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_55 ) } if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_70 ) } error("No other combination expected header=$header") } } } private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header { return Header( type = if (taskInfo.isTransparentCaptionBarAppearance) { Header.Type.CUSTOM } else { Header.Type.DEFAULT }, systemTheme = getSystemTheme(), appTheme = getAppTheme(taskInfo), isFocused = taskInfo.isFocused, isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance ) } private fun getSystemTheme(): Header.Theme { return if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) { Header.Theme.DARK } else { Header.Theme.LIGHT } } private fun getAppTheme(taskInfo: RunningTaskInfo): Header.Theme { // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the // TaskInfo/TaskDescription. val backgroundColor = taskInfo.taskDescription?.backgroundColor ?: return getSystemTheme() return if (Color.valueOf(backgroundColor).luminance() < 0.5) { Header.Theme.DARK } else { Header.Theme.LIGHT } } @ColorInt private fun attrToColor(attr: Int): Int { context.withStyledAttributes(null, intArrayOf(attr), 0, 0) { return getColor(0, 0) } return Color.BLACK } data class Header( val type: Type, val systemTheme: Theme, val appTheme: Theme, val isFocused: Boolean, val isAppearanceCaptionLight: Boolean, ) { enum class Type { DEFAULT, CUSTOM } enum class Theme { LIGHT, DARK } } private fun Header.Theme.isLight(): Boolean = this == Header.Theme.LIGHT private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK data class HeaderStyle( val background: Background, val foreground: Foreground ) { data class Foreground( @ColorInt val color: Int, val opacity: Int ) sealed class Background { data object Transparent : Background() data class Opaque( @ColorInt val frontLayerColor: Int, val frontLayerOpacity: Int, @ColorInt val backLayerColor: Int? ) : Background() } } @ColorInt private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { if (taskInfo.isTransparentCaptionBarAppearance) { Loading Loading @@ -171,8 +491,15 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( companion object { private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% private const val FOCUSED_OPACITY = 255 private const val OPACITY_100 = 255 private const val OPACITY_30 = 77 private const val OPACITY_55 = 140 private const val OPACITY_65 = 166 private const val OPACITY_70 = 179 } } Loading
libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml 0 → 100644 +28 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- ~ 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. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/backLayer"> <shape android:shape="rectangle"> <solid android:color="#000000" /> </shape> </item> <item android:id="@+id/frontLayer"> <shape android:shape="rectangle"> <solid android:color="#000000" /> </shape> </item> </layer-list> No newline at end of file
libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +1 −1 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/desktop_mode_caption" android:background="@drawable/desktop_mode_header_background" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" Loading Loading @@ -96,7 +97,6 @@ android:paddingHorizontal="10dp" android:paddingVertical="8dp" android:layout_marginEnd="8dp" android:tint="?androidprv:attr/materialColorOnSurface" android:background="?android:selectableItemBackgroundBorderless" android:contentDescription="@string/close_button_text" android:src="@drawable/desktop_mode_header_ic_close" Loading
libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +0 −2 Original line number Diff line number Diff line Loading @@ -31,8 +31,6 @@ android:layout_height="34dp" android:padding="5dp" android:contentDescription="@string/maximize_button_text" android:tint="?androidprv:attr/materialColorOnSurface" android:background="?android:selectableItemBackgroundBorderless" android:src="@drawable/decor_desktop_mode_maximize_button_dark" android:scaleType="fitCenter" /> </merge> No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +64 −7 Original line number Diff line number Diff line Loading @@ -20,15 +20,19 @@ import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.RippleDrawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ProgressBar import androidx.annotation.ColorInt import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat import com.android.window.flags.Flags import com.android.wm.shell.R private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350 Loading Loading @@ -90,7 +94,44 @@ class MaximizeButtonView( progressBar.visibility = View.INVISIBLE } fun setAnimationTints(darkMode: Boolean) { /** * Set the color tints of the maximize button views. * * @param darkMode whether the app's theme is in dark mode. * @param iconForegroundColor the color tint to use for the maximize icon to match the rest of * the App Header icons * @param baseForegroundColor the base foreground color tint used by the App Header, used to style * views within this button using the same base color but with different opacities. */ fun setAnimationTints( darkMode: Boolean, iconForegroundColor: ColorStateList? = null, baseForegroundColor: Int? = null ) { if (Flags.enableThemedAppHeaders()) { requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" } requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" } maximizeWindow.imageTintList = iconForegroundColor maximizeWindow.background = RippleDrawable( ColorStateList( arrayOf( intArrayOf(android.R.attr.state_hovered), intArrayOf(android.R.attr.state_pressed), intArrayOf(), ), intArrayOf( replaceColorAlpha(baseForegroundColor, OPACITY_8), replaceColorAlpha(baseForegroundColor, OPACITY_12), Color.TRANSPARENT ) ), null, null ) progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor) .withAlpha(OPACITY_12) progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) } else { if (darkMode) { progressBar.progressTintList = ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark)) Loading @@ -104,3 +145,19 @@ class MaximizeButtonView( } } } @ColorInt private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { return Color.argb( alpha, Color.red(color), Color.green(color), Color.blue(color) ) } companion object { private const val OPACITY_8 = 20 private const val OPACITY_12 = 31 } }
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +327 −0 Original line number Diff line number Diff line Loading @@ -4,8 +4,11 @@ import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.content.res.ColorStateList import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.view.View import android.view.View.OnLongClickListener import android.widget.ImageButton Loading @@ -15,10 +18,13 @@ import androidx.core.content.withStyledAttributes import androidx.core.view.isVisible import com.android.internal.R.attr.materialColorOnSecondaryContainer import com.android.internal.R.attr.materialColorOnSurface import com.android.internal.R.attr.materialColorOnSurfaceInverse import com.android.internal.R.attr.materialColorSecondaryContainer import com.android.internal.R.attr.materialColorSurfaceContainerHigh import com.android.internal.R.attr.materialColorSurfaceContainerLow import com.android.internal.R.attr.materialColorSurfaceDim import com.android.internal.R.attr.materialColorSurfaceInverse import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance Loading Loading @@ -71,6 +77,14 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { if (Flags.enableThemedAppHeaders()) { bindDataWithThemedHeaders(taskInfo) } else { bindDataLegacy(taskInfo) } } private fun bindDataLegacy(taskInfo: RunningTaskInfo) { captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo)) val color = getAppNameAndButtonColor(taskInfo) val alpha = Color.alpha(color) Loading @@ -87,6 +101,45 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( maximizeButtonView.setAnimationTints(isDarkMode()) } private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) { val header = fillHeaderInfo(taskInfo) val headerStyle = getHeaderStyle(header) // Caption Background val headerBackground = captionView.background as LayerDrawable val backLayer = headerBackground.findDrawableByLayerId(R.id.backLayer) as GradientDrawable val frontLayer = headerBackground.findDrawableByLayerId(R.id.frontLayer) as GradientDrawable when (headerStyle.background) { is HeaderStyle.Background.Opaque -> { backLayer.setColor(headerStyle.background.backLayerColor ?: Color.BLACK) frontLayer.setColor(headerStyle.background.frontLayerColor) frontLayer.alpha = headerStyle.background.frontLayerOpacity } HeaderStyle.Background.Transparent -> { backLayer.setColor(Color.TRANSPARENT) frontLayer.setColor(Color.TRANSPARENT) frontLayer.alpha = OPACITY_100 } } // Caption Foreground val foregroundColor = headerStyle.foreground.color val foregroundAlpha = headerStyle.foreground.opacity val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha) closeWindowButton.imageTintList = colorStateList expandMenuButton.imageTintList = colorStateList with (appNameTextView) { isVisible = header.type == Header.Type.DEFAULT setTextColor(colorStateList) } appIconImageView.imageAlpha = foregroundAlpha maximizeButtonView.setAnimationTints( darkMode = header.appTheme == Header.Theme.DARK, iconForegroundColor = colorStateList, baseForegroundColor = foregroundColor ) } override fun onHandleMenuOpened() {} override fun onHandleMenuClosed() {} Loading @@ -107,6 +160,273 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( maximizeButtonView.startHoverAnimation() } private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( background = getHeaderBackground(header), foreground = getHeaderForeground(header) ) } private fun getHeaderBackground( header: Header ): HeaderStyle.Background { when (header.type) { Header.Type.DEFAULT -> { if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSecondaryContainer), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceContainerLow), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceContainerHigh), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceDim), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_30, backLayerColor = Color.BLACK ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_100, backLayerColor = null ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Background.Opaque( frontLayerColor = attrToColor(materialColorSurfaceInverse), frontLayerOpacity = OPACITY_55, backLayerColor = Color.WHITE ) } error("No other combination expected header=$header") } Header.Type.CUSTOM -> return HeaderStyle.Background.Transparent } } private fun getHeaderForeground(header: Header): HeaderStyle.Foreground { when (header.type) { Header.Type.DEFAULT -> { if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_55 ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_70 ) } error("No other combination expected header=$header") } Header.Type.CUSTOM -> { if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSecondaryContainer), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurface), opacity = OPACITY_55 ) } if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_65 ) } if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_100 ) } if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && !header.isFocused) { return HeaderStyle.Foreground( color = attrToColor(materialColorOnSurfaceInverse), opacity = OPACITY_70 ) } error("No other combination expected header=$header") } } } private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header { return Header( type = if (taskInfo.isTransparentCaptionBarAppearance) { Header.Type.CUSTOM } else { Header.Type.DEFAULT }, systemTheme = getSystemTheme(), appTheme = getAppTheme(taskInfo), isFocused = taskInfo.isFocused, isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance ) } private fun getSystemTheme(): Header.Theme { return if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) { Header.Theme.DARK } else { Header.Theme.LIGHT } } private fun getAppTheme(taskInfo: RunningTaskInfo): Header.Theme { // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the // TaskInfo/TaskDescription. val backgroundColor = taskInfo.taskDescription?.backgroundColor ?: return getSystemTheme() return if (Color.valueOf(backgroundColor).luminance() < 0.5) { Header.Theme.DARK } else { Header.Theme.LIGHT } } @ColorInt private fun attrToColor(attr: Int): Int { context.withStyledAttributes(null, intArrayOf(attr), 0, 0) { return getColor(0, 0) } return Color.BLACK } data class Header( val type: Type, val systemTheme: Theme, val appTheme: Theme, val isFocused: Boolean, val isAppearanceCaptionLight: Boolean, ) { enum class Type { DEFAULT, CUSTOM } enum class Theme { LIGHT, DARK } } private fun Header.Theme.isLight(): Boolean = this == Header.Theme.LIGHT private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK data class HeaderStyle( val background: Background, val foreground: Foreground ) { data class Foreground( @ColorInt val color: Int, val opacity: Int ) sealed class Background { data object Transparent : Background() data class Opaque( @ColorInt val frontLayerColor: Int, val frontLayerOpacity: Int, @ColorInt val backLayerColor: Int? ) : Background() } } @ColorInt private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { if (taskInfo.isTransparentCaptionBarAppearance) { Loading Loading @@ -171,8 +491,15 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( companion object { private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% private const val FOCUSED_OPACITY = 255 private const val OPACITY_100 = 255 private const val OPACITY_30 = 77 private const val OPACITY_55 = 140 private const val OPACITY_65 = 166 private const val OPACITY_70 = 179 } }