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

Commit d72cc8d2 authored by Kazuki Takise's avatar Kazuki Takise
Browse files

Implement expand menu error icon

This change introduces "expand menu error icon", which is shown
when one of the handle menu items need to get special attention.

The current intended usage is restart menu. When an app moves
between displays, the restart menu shows up in the restart menu,
and to nudge the user to user the menu, we want to show an error
icon both on the app header and the handle menu. (This change
handles only the app header part of the change.)

See [1] and [2] for the actual UI visuals.

The key points of the implementation are as follows:

- Enclose app name text view with FrameLayout
  This is needed as the error icon needs to overlap with the app
  name especially when the app name is long [2]. When the name is
  short enough, the text view of the app name still overlaps with
  the icon, but the padding is properly added, so from the user's
  persipective, it looks like they look just horizentally aligned.

- Apply fadeout affect instead of ellipsize="end"
  To improve the visual of the long app name, the new spec [2]
  defines the app name should fade out instead of "...".
  As the fadeout effect isn't provided with TextView by default,
  this changes introduces a custom shader to achieve this effect.

[1] go/app-header-error-icon-short
[2] go/app-header-error-icon-long

Flag: com.android.window.flags.enable_restart_menu_for_connected_displays
Bug: 400575851
Test: DesktopModeWindowDecorationTests
Change-Id: Id0655abda5c295deaa9df80cd366caee5682292e
parent f0fedb8f
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2025 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.
  -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960">

    <path
        android:fillColor="#FFB4AB"
        android:pathData="M480,0 A480,480 0 1,1 480,960 A480,480 0 1,1 480,0Z" />

    <path
        android:fillColor="#000000"
        android:fillType="evenOdd"
        android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520Z" />
</vector>
+35 −14
Original line number Diff line number Diff line
@@ -43,23 +43,44 @@
            android:focusable="false"
            android:scaleType="centerCrop"/>

        <FrameLayout
            android:id="@+id/app_name_layout"
            android:layout_marginStart="8dp"
            android:layout_gravity="center_vertical"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/application_name"
            android:layout_width="0dp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
            android:maxWidth="130dp"
                android:maxWidth="@dimen/desktop_mode_header_app_name_max_width"
                android:textSize="14sp"
                android:textFontWeight="500"
                android:lineHeight="20sp"
            android:layout_gravity="center_vertical"
            android:layout_weight="1"
            android:layout_marginStart="8dp"
                android:layout_gravity="start|center_vertical"
                android:singleLine="true"
            android:ellipsize="end"
                android:ellipsize="none"
                android:clickable="false"
                android:focusable="false"
                tools:text="Gmail"/>

            <ImageView
                android:id="@+id/expand_menu_error"
                android:src="@drawable/expand_menu_error"
                android:layout_width="@dimen/desktop_mode_header_expand_menu_error_image_width"
                android:layout_height="@dimen/desktop_mode_header_expand_menu_error_image_width"
                android:layout_gravity="end|center_vertical"
                android:layout_marginStart="@dimen/desktop_mode_header_expand_menu_error_image_margin"
                android:clickable="false"
                android:focusable="false"
                android:screenReaderFocusable="false"
                android:importantForAccessibility="no"
                android:contentDescription="@null"
                android:scaleType="centerCrop"/>
        </FrameLayout>

        <ImageButton
            android:id="@+id/expand_menu_button"
            android:layout_width="16dp"
+9 −0
Original line number Diff line number Diff line
@@ -659,6 +659,15 @@
    <!-- The horizontal inset to apply to the close button's ripple drawable -->
    <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>

    <!-- The max width of the app name shown on the app header -->
    <dimen name="desktop_mode_header_app_name_max_width">130dp</dimen>
    <!-- The width of the fadeout effect applied to a long app name shown on the app header -->
    <dimen name="desktop_mode_header_app_name_fadeout_width">48dp</dimen>
    <!-- The width of the expand menu error image on the app header -->
    <dimen name="desktop_mode_header_expand_menu_error_image_width">16dp</dimen>
    <!-- The margin added between app name and expand menu error image on the app header -->
    <dimen name="desktop_mode_header_expand_menu_error_image_margin">8dp</dimen>

    <!-- The padding added to all sides of windowing education tooltip -->
    <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen>

+91 −0
Original line number Diff line number Diff line
@@ -22,10 +22,13 @@ import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Rect
import android.graphics.Shader
import android.os.Bundle
import android.view.View
import android.view.View.OnLongClickListener
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
@@ -48,6 +51,7 @@ import com.android.internal.R.color.materialColorSecondaryContainer
import com.android.internal.R.color.materialColorSurfaceContainerHigh
import com.android.internal.R.color.materialColorSurfaceContainerLow
import com.android.internal.R.color.materialColorSurfaceDim
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_ACTION_MAXIMIZE_RESTORE
@@ -66,6 +70,7 @@ import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.common.createBackgroundDrawable
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
import kotlin.math.roundToInt

/**
 * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
@@ -104,6 +109,30 @@ class AppHeaderViewHolder(
    private val headerButtonsRippleRadius = context.resources
        .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius)

    /**
     * The max width of the app name shown on the app header.
     **/
    private val appNameMaxWidth = context.resources
        .getDimensionPixelSize(R.dimen.desktop_mode_header_app_name_max_width)

    /**
     * The width of the fadeout effect applied to a long app name shown on the app header.
     **/
    private val appNameFadeoutWidth = context.resources
        .getDimensionPixelSize(R.dimen.desktop_mode_header_app_name_fadeout_width)

    /**
     * The width of the expand menu error image on the app header.
     **/
    private val expandMenuErrorImageWidth = context.resources
        .getDimensionPixelSize(R.dimen.desktop_mode_header_expand_menu_error_image_width)

    /**
     * The margin added between app name and expand menu error image on the app header.
     **/
    private val expandMenuErrorImageMargin = context.resources
        .getDimensionPixelSize(R.dimen.desktop_mode_header_expand_menu_error_image_margin)

    /**
     * The app chip, minimize, maximize and close button's height extends to the top & bottom edges
     * of the header, and their width may be larger than their height. This is by design to increase
@@ -147,6 +176,9 @@ class AppHeaderViewHolder(
    private val minimizeWindowButton: ImageButton = rootView.requireViewById(R.id.minimize_window)
    private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
    private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
    private val expandMenuErrorImageView: ImageView =
        rootView.requireViewById(R.id.expand_menu_error)

    val appNameTextWidth: Int
        get() = appNameTextView.width

@@ -349,6 +381,60 @@ class AppHeaderViewHolder(
        a11yTextRestore = context.getString(R.string.restore_button_text, name)

        updateMaximizeButtonContentDescription()
        updateAppNameLayoutAndEffect()
    }

    private fun updateAppNameLayoutAndEffect() {
        if (!Flags.enableRestartMenuForConnectedDisplays()) return
        appNameTextView.viewTreeObserver.addOnPreDrawListener(
            object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    appNameTextView.viewTreeObserver.removeOnPreDrawListener(this)
                    val errorIconWidth =
                        expandMenuErrorImageWidth + expandMenuErrorImageMargin
                    val textWidth =
                        appNameTextView.paint.measureText(appNameTextView.text.toString())
                            .roundToInt()
                    val isRestartMenuShown =
                        currentTaskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove

                    // Adjust the right padding of the app text so the error icon will be placed
                    // properly. In case the text is short enough, the padding will be
                    // |errorIconWidth| so the error icon will look like being placed to the right
                    // of the text. Otherwise, the error icon will overlap with the text.
                    val errorIconPadding = if (isRestartMenuShown && textWidth <= appNameMaxWidth) {
                        minOf(appNameMaxWidth - textWidth, errorIconWidth)
                    } else {
                        0
                    }
                    appNameTextView.setPaddingRelative(0, 0, errorIconPadding, 0)

                    // In case the app text (and the error icon) is too long to fit in the app
                    // header, fade out the text by applying the custom shader.
                    val availableWidth = if (isRestartMenuShown) {
                        appNameMaxWidth - errorIconWidth
                    } else {
                        appNameMaxWidth
                    }
                    if (textWidth > availableWidth) {
                        val textColor = appNameTextView.currentTextColor
                        val transparentColor = Color.argb(
                            0, Color.red(textColor),
                            Color.green(textColor), Color.blue(textColor)
                        )
                        appNameTextView.paint.shader = LinearGradient(
                            (availableWidth - appNameFadeoutWidth).toFloat(),
                            0f,
                            availableWidth.toFloat(),
                            0f,
                            textColor,
                            transparentColor,
                            Shader.TileMode.CLAMP
                        )
                    }
                    return true
                }
        })
    }

    private fun updateMaximizeButtonContentDescription() {
@@ -412,6 +498,7 @@ class AppHeaderViewHolder(
        minimizeWindowButton.imageAlpha = alpha
        closeWindowButton.imageAlpha = alpha
        expandMenuButton.imageAlpha = alpha
        expandMenuErrorImageView.imageAlpha = alpha
        context.withStyledAttributes(
            set = null,
            attrs = intArrayOf(
@@ -467,6 +554,9 @@ class AppHeaderViewHolder(
                drawableInsets = appChipDrawableInsets,
            )
            expandMenuButton.imageTintList = colorStateList
            expandMenuErrorImageView.visibility =
                if (currentTaskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove)
                    View.VISIBLE else View.GONE
            appNameTextView.apply {
                isVisible = header.type == Header.Type.DEFAULT
                setTextColor(colorStateList)
@@ -524,6 +614,7 @@ class AppHeaderViewHolder(
                }
            }
            updateMaximizeButtonContentDescription()
            updateAppNameLayoutAndEffect()
        }
        // Close button.
        closeWindowButton.apply {