Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c8b035dd authored by Jorge Gil's avatar Jorge Gil Committed by Android (Google) Code Review
Browse files

Merge "Update App Header background/foreground style" into main

parents 29f30aa3 0f52381b
Loading
Loading
Loading
Loading
+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
+1 −1
Original line number Diff line number Diff line
@@ -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"
@@ -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"
+0 −2
Original line number Diff line number Diff line
@@ -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
+64 −7
Original line number Diff line number Diff line
@@ -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
@@ -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))
@@ -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
    }
}
+327 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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() {}
@@ -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) {
@@ -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
    }
}