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

Commit 961a2b11 authored by Ale Nijamkin's avatar Ale Nijamkin
Browse files

[flexiglass] Anchor dual shade tooltips to their elements

Properly sources the position of the elements that the educational
tooltips are anchored to, from the actual elements themselves.

Bug: 406213664
Test: manually verified that the tooltips are properly anchored
Flag: com.android.systemui.scene_container
Change-Id: I125e3b998921cf793cbed83713fc299e0314b62b
parent d6941c1e
Loading
Loading
Loading
Loading
+32 −26
Original line number Diff line number Diff line
@@ -18,12 +18,9 @@

package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.RichTooltip
import androidx.compose.material3.Text
@@ -32,17 +29,20 @@ import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.height
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.scene.ui.viewmodel.DualShadeEducationalTooltipsViewModel

@Composable
fun DualShadeEducationalTooltips(viewModelFactory: DualShadeEducationalTooltipsViewModel.Factory) {
fun DualShadeEducationalTooltips(
    viewModelFactory: DualShadeEducationalTooltipsViewModel.Factory,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val viewModel =
        rememberViewModel(traceName = "DualShadeEducationalTooltips") {
@@ -51,24 +51,30 @@ fun DualShadeEducationalTooltips(viewModelFactory: DualShadeEducationalTooltipsV

    val visibleTooltip = viewModel.visibleTooltip ?: return

    val anchorBottomY = visibleTooltip.anchorBottomY
    // This Box represents the bounds of the top edge that the user can swipe down on to reveal
    // either of the dual shade overlays. It's used as a convenient way to position the anchor for
    // each of the tooltips that can be shown. As such, this Box is the same size as the status bar.
    Box(
        contentAlignment =
            if (visibleTooltip.isAlignedToStart) {
                Alignment.CenterStart
            } else {
                Alignment.CenterEnd
            },
        modifier = Modifier.fillMaxWidth().height { anchorBottomY }.padding(horizontal = 24.dp),
    ) {
    Layout(
        content = {
            AnchoredTooltip(
                text = visibleTooltip.text,
                onShown = visibleTooltip.onShown,
                onDismissed = visibleTooltip.onDismissed,
            )
        },
        modifier = modifier.fillMaxSize(),
    ) { measurables, constraints ->
        check(measurables.size == 1)
        val placeable =
            measurables[0].measure(
                Constraints.fixed(
                    width = visibleTooltip.anchorBounds.width,
                    height = visibleTooltip.anchorBounds.height,
                )
            )
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeable.place(
                x = visibleTooltip.anchorBounds.left,
                y = visibleTooltip.anchorBounds.top,
            )
        }
    }
}

@@ -102,6 +108,6 @@ private fun AnchoredTooltip(
        onDismissRequest = onDismissed,
        modifier = modifier,
    ) {
        Spacer(modifier = Modifier.width(48.dp).fillMaxHeight())
        Spacer(modifier = Modifier.fillMaxSize())
    }
}
+40 −4
Original line number Diff line number Diff line
@@ -55,12 +55,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
@@ -87,6 +90,7 @@ import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.kairos.buildSpec
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.DualShadeEducationElement
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim
import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingHorizontal
@@ -351,7 +355,16 @@ fun ContentScope.OverlayShadeHeaderPartialStateless(
                NotificationsChip(
                    onClick = viewModel::onNotificationIconChipClicked,
                    backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
                    modifier = Modifier.bouncy(isEnabled = viewModel.animateNotificationsChipBounce),
                    modifier =
                        Modifier.bouncy(
                            isEnabled = viewModel.animateNotificationsChipBounce,
                            onBoundsChange = { bounds ->
                                viewModel.onDualShadeEducationElementBoundsChange(
                                    element = DualShadeEducationElement.Notifications,
                                    bounds = bounds,
                                )
                            },
                        ),
                ) {
                    VariableDayDate(
                        longerDateText = viewModel.longerDateText,
@@ -371,7 +384,16 @@ fun ContentScope.OverlayShadeHeaderPartialStateless(
                SystemIconChip(
                    backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
                    onClick = viewModel::onSystemIconChipClicked,
                    modifier = Modifier.bouncy(isEnabled = viewModel.animateSystemIconChipBounce),
                    modifier =
                        Modifier.bouncy(
                            isEnabled = viewModel.animateSystemIconChipBounce,
                            onBoundsChange = { bounds ->
                                viewModel.onDualShadeEducationElementBoundsChange(
                                    element = DualShadeEducationElement.QuickSettings,
                                    bounds = bounds,
                                )
                            },
                        ),
                ) {
                    val paddingEnd =
                        with(LocalDensity.current) {
@@ -803,7 +825,10 @@ private fun ContentScope.PrivacyChip(

/** Modifies the given [Modifier] such that it shows a looping vertical bounce animation. */
@Composable
private fun Modifier.bouncy(isEnabled: Boolean): Modifier {
private fun Modifier.bouncy(
    isEnabled: Boolean,
    onBoundsChange: (bounds: IntRect) -> Unit,
): Modifier {
    val density = LocalDensity.current
    val animatable = remember { Animatable(0f) }
    LaunchedEffect(isEnabled) {
@@ -853,7 +878,18 @@ private fun Modifier.bouncy(isEnabled: Boolean): Modifier {
        }
    }

    return this.offset { IntOffset(x = 0, y = animatable.value.roundToInt()) }
    return this.thenIf(isEnabled) {
        Modifier.offset { IntOffset(x = 0, y = animatable.value.roundToInt()) }
            .onGloballyPositioned { coordinates ->
                val offset = coordinates.positionInWindow()
                onBoundsChange(
                    IntRect(
                        offset = IntOffset(x = offset.x.roundToInt(), y = offset.y.roundToInt()),
                        size = coordinates.size,
                    )
                )
            }
    }
}

private fun shouldUseExpandedFormat(state: TransitionState): Boolean {
+16 −0
Original line number Diff line number Diff line
@@ -18,8 +18,11 @@ package com.android.systemui.scene.data.repository

import android.annotation.UserIdInt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.unit.IntRect
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.android.systemui.common.data.datastore.DataStoreWrapper
@@ -27,6 +30,7 @@ import com.android.systemui.common.data.datastore.DataStoreWrapperFactory
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.data.model.DualShadeEducationImpressionModel
import com.android.systemui.scene.shared.model.DualShadeEducationElement
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -54,6 +58,14 @@ constructor(
        mutableStateOf(DualShadeEducationImpressionModel())
        private set

    private val _elementBounds: SnapshotStateMap<DualShadeEducationElement, IntRect> =
        mutableStateMapOf(
            DualShadeEducationElement.Notifications to IntRect.Zero,
            DualShadeEducationElement.QuickSettings to IntRect.Zero,
        )
    val elementBounds: Map<DualShadeEducationElement, IntRect>
        get() = _elementBounds

    private var dataStore: DataStoreWrapper? = null
    private var hydrationJob: Job? = null

@@ -106,6 +118,10 @@ constructor(
        persist(Keys.EverShownQuickSettingsTooltip, value)
    }

    fun setElementBounds(element: DualShadeEducationElement, bounds: IntRect) {
        _elementBounds[element] = bounds
    }

    /** Each time data store data changes, passes it to the given [receiver]. */
    private suspend fun repeatWhenPrefsChange(
        dataStore: DataStoreWrapper,
+12 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
@@ -29,6 +30,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.scene.data.repository.DualShadeEducationRepository
import com.android.systemui.scene.domain.model.DualShadeEducationModel
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.DualShadeEducationElement
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
@@ -64,6 +66,9 @@ constructor(
    var education: DualShadeEducationModel by mutableStateOf(DualShadeEducationModel.None)
        private set

    val elementBounds: Map<DualShadeEducationElement, IntRect>
        get() = repository.elementBounds

    override fun start() {
        if (!SceneContainerFlag.isEnabled) {
            return
@@ -108,6 +113,13 @@ constructor(
        }
    }

    fun onDualShadeEducationElementBoundsChange(
        element: DualShadeEducationElement,
        bounds: IntRect,
    ) {
        repository.setElementBounds(element, bounds)
    }

    /** Keeps the repository data fresh for the selected user. */
    private suspend fun hydrateRepository() {
        repeatWhenUserSelected { selectedUserId -> repository.activateFor(selectedUserId) }
+22 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.systemui.scene.shared.model

enum class DualShadeEducationElement {
    Notifications,
    QuickSettings,
}
Loading