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

Commit 6c31f52f authored by Olivier St-Onge's avatar Olivier St-Onge Committed by Android (Google) Code Review
Browse files

Merge "Apply a bounce effect on icons/labels" into main

parents e41e427d df1e2170
Loading
Loading
Loading
Loading
+25 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
@@ -67,6 +68,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
@@ -107,6 +109,8 @@ import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
import com.android.systemui.qs.ui.compose.borderOnFocus
import com.android.systemui.res.R
import kotlin.math.abs
import platform.test.motion.compose.values.MotionTestValueKey
import platform.test.motion.compose.values.motionTestValues

private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"

@@ -122,6 +126,7 @@ fun LargeTileContent(
    isVisible: () -> Boolean = { true },
    accessibilityUiState: AccessibilityUiState? = null,
    iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
    textScale: () -> Float = { 1f },
    toggleClick: (() -> Unit)? = null,
    onLongClick: (() -> Unit)? = null,
) {
@@ -178,7 +183,7 @@ fun LargeTileContent(
            colors = colors,
            accessibilityUiState = accessibilityUiState,
            isVisible = isVisible,
            modifier = Modifier.weight(1f),
            modifier = Modifier.weight(1f).bounceScale(TransformOrigin(0f, .5f), textScale),
        )

        if (sideDrawable != null) {
@@ -381,6 +386,25 @@ private fun DrawScope.drawFadedEdge(startX: Float, endX: Float, colors: List<Col
    )
}

fun Modifier.bounceScale(
    transformOrigin: TransformOrigin = TransformOrigin.Center,
    scale: () -> Float,
): Modifier {
    return motionTestValues { scale() exportAs TileBounceMotionTestKeys.BounceScale }
        .graphicsLayer {
            scale().let {
                scaleY = it
                scaleX = it
                this.transformOrigin = transformOrigin
            }
        }
}

@VisibleForTesting
object TileBounceMotionTestKeys {
    val BounceScale = MotionTestValueKey<Float>("bounceScale")
}

object CommonTileDefaults {
    val IconSize = 32.dp
    val LargeTileIconSize = 28.dp
+17 −11
Original line number Diff line number Diff line
@@ -229,19 +229,21 @@ fun Tile(
                        hapticsViewModel?.setTileInteractionState(
                            TileHapticsViewModel.TileInteractionState.CLICKED
                        )
                        if (uiState.accessibilityUiState.toggleableState != null) {
                            // Bounce unless we're a large dual target tile. These don't toggle on
                            // main click.
                            if (iconOnly || !isDualTarget) {
                        val bounceContainer = uiState.isToggleable && (iconOnly || !isDualTarget)
                        coroutineScope.launch {
                                    currentBounceableInfo.bounceable.animateBounce()
                            // Bounce the tile's container if it is toggleable and is not a large
                            // dual target tile. These don't toggle on main click. Otherwise bounce
                            // the content of the tile.
                            if (bounceContainer) {
                                currentBounceableInfo.bounceable.animateContainerBounce()
                            } else {
                                currentBounceableInfo.bounceable.animateContentBounce(iconOnly)
                            }
                        }
                        if (uiState.isToggleable && iconOnly) {
                            // And show footer text feedback for icons
                            if (iconOnly) {
                            requestToggleTextFeedback(tile.spec)
                        }
                        }
                    },
                onLongClick = longClick,
                accessibilityUiState = uiState.accessibilityUiState,
@@ -253,7 +255,10 @@ fun Tile(
                    SmallTileContent(
                        iconProvider = iconProvider,
                        color = colors.icon,
                        modifier = Modifier.align(Alignment.Center),
                        modifier =
                            Modifier.align(Alignment.Center).bounceScale {
                                bounceableInfo.bounceable.iconBounceScale
                            },
                    )
                } else {
                    val iconShape by TileDefaults.animateIconShapeAsState(uiState.state)
@@ -277,6 +282,7 @@ fun Tile(
                        accessibilityUiState = uiState.accessibilityUiState,
                        squishiness = squishiness,
                        isVisible = isVisible,
                        textScale = { bounceableInfo.bounceable.textBounceScale },
                        modifier =
                            Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick),
                    )
+56 −5
Original line number Diff line number Diff line
@@ -17,22 +17,73 @@
package com.android.systemui.qs.panels.ui.viewmodel

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Bounceable

class BounceableTileViewModel : Bounceable {
    private val animatableBounce = Animatable(0.dp, Dp.VectorConverter)
    private val animatableContainerBounce = Animatable(0.dp, Dp.VectorConverter)
    private val animatableIconBounceScale = Animatable(1f)
    private val animatableTextBounceScale = Animatable(1f)

    override val bounce: Dp
        get() = animatableBounce.value
        get() = animatableContainerBounce.value

    val iconBounceScale: Float
        get() = animatableIconBounceScale.value

    val textBounceScale: Float
        get() = animatableTextBounceScale.value

    suspend fun animateContainerBounce() {
        animatableContainerBounce.animateToBounce(BounceSize)
        animatableContainerBounce.animateToRest(ContainerBounceAtRest)
    }

    suspend fun animateContentBounce(iconOnly: Boolean) {
        if (iconOnly) {
            animateIconBounce()
        } else {
            animateTextBounce()
        }
    }

    private suspend fun animateIconBounce() {
        animatableIconBounceScale.animateToBounce(ICON_BOUNCE_SCALE)
        animatableIconBounceScale.animateToRest(SCALE_BOUNCE_AT_REST)
    }

    private suspend fun animateTextBounce() {
        animatableTextBounceScale.animateToBounce(TEXT_BOUNCE_SCALE)
        animatableTextBounceScale.animateToRest(SCALE_BOUNCE_AT_REST)
    }

    private suspend fun <T, V : AnimationVector> Animatable<T, V>.animateToBounce(targetValue: T) {
        animateTo(
            targetValue,
            tween(durationMillis = BOUNCE_DURATION_MILLIS, easing = BounceStartEasing),
        )
    }

    suspend fun animateBounce() {
        animatableBounce.animateTo(BounceSize)
        animatableBounce.animateTo(0.dp)
    private suspend fun <T, V : AnimationVector> Animatable<T, V>.animateToRest(targetValue: T) {
        animateTo(
            targetValue,
            tween(durationMillis = BOUNCE_DURATION_MILLIS, easing = BounceEndEasing),
        )
    }

    private companion object {
        val BounceSize = 8.dp
        val BounceStartEasing = CubicBezierEasing(.05f, 0f, 0f, 1f)
        val BounceEndEasing = CubicBezierEasing(1f, 0f, .95f, 1f)
        val ContainerBounceAtRest = 0.dp
        const val ICON_BOUNCE_SCALE = 1.1f
        const val TEXT_BOUNCE_SCALE = 1.06f
        const val SCALE_BOUNCE_AT_REST = 1f
        const val BOUNCE_DURATION_MILLIS = 167
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -43,7 +43,10 @@ data class TileUiState(
    val handlesSecondaryClick: Boolean,
    val sideDrawable: Drawable?,
    val accessibilityUiState: AccessibilityUiState,
)
) {
    val isToggleable: Boolean
        get() = accessibilityUiState.toggleableState != null
}

data class AccessibilityUiState(
    val contentDescription: String,
+252 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    "before",
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272,
    288,
    304,
    320,
    336,
    352,
    368,
    "after"
  ],
  "features": [
    {
      "name": "tile-small_previous",
      "type": "dpSize",
      "data_points": [
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 93.71429,
          "height": 72
        },
        {
          "width": 92.28571,
          "height": 72
        },
        {
          "width": 91.14286,
          "height": 72
        },
        {
          "width": 90.28571,
          "height": 72
        },
        {
          "width": 89.71429,
          "height": 72
        },
        {
          "width": 89.42857,
          "height": 72
        },
        {
          "width": 89.14286,
          "height": 72
        },
        {
          "width": 89.14286,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 88.85714,
          "height": 72
        },
        {
          "width": 89.14286,
          "height": 72
        },
        {
          "width": 89.42857,
          "height": 72
        },
        {
          "width": 89.71429,
          "height": 72
        },
        {
          "width": 90,
          "height": 72
        },
        {
          "width": 90.85714,
          "height": 72
        },
        {
          "width": 91.71429,
          "height": 72
        },
        {
          "width": 92.85714,
          "height": 72
        },
        {
          "width": 95.42857,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        }
      ]
    },
    {
      "name": "tile-small_clicked",
      "type": "dpSize",
      "data_points": [
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        },
        {
          "width": 100,
          "height": 72
        },
        {
          "width": 101.42857,
          "height": 72
        },
        {
          "width": 102.57143,
          "height": 72
        },
        {
          "width": 103.42857,
          "height": 72
        },
        {
          "width": 104,
          "height": 72
        },
        {
          "width": 104.28571,
          "height": 72
        },
        {
          "width": 104.57143,
          "height": 72
        },
        {
          "width": 104.57143,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.85714,
          "height": 72
        },
        {
          "width": 104.57143,
          "height": 72
        },
        {
          "width": 104.28571,
          "height": 72
        },
        {
          "width": 104,
          "height": 72
        },
        {
          "width": 103.71429,
          "height": 72
        },
        {
          "width": 102.85714,
          "height": 72
        },
        {
          "width": 102,
          "height": 72
        },
        {
          "width": 100.85714,
          "height": 72
        },
        {
          "width": 98.28571,
          "height": 72
        },
        {
          "width": 96.85714,
          "height": 72
        }
      ]
    }
  ]
}
Loading