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

Commit ee5144fb authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Automerger Merge Worker
Browse files

Merge "[flexiglass] Pattern bouncer UX polish." into udc-dev am: e2ccf110

parents 9e8b53e3 e2ccf110
Loading
Loading
Loading
Loading
+69 −101
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.systemui.bouncer.ui.composable
package com.android.systemui.bouncer.ui.composable


import android.view.HapticFeedbackConstants
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Canvas
@@ -32,14 +33,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.integerResource
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import com.android.internal.R
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import kotlin.math.min
import kotlin.math.min
@@ -82,17 +84,26 @@ internal fun PatternBouncer(
    val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
    val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()


    // Map of animatables for the scale of each dot, keyed by dot.
    // Map of animatables for the scale of each dot, keyed by dot.
    val scales = remember(dots) { dots.associateWith { Animatable(1f) } }
    val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
    // Map of animatables for the lines that connect between selected dots, keyed by the destination
    // Map of animatables for the lines that connect between selected dots, keyed by the destination
    // dot of the line.
    // dot of the line.
    val lines = remember(dots) { dots.associateWith { Animatable(1f) } }
    val lineFadeOutAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
    val lineFadeOutAnimationDurationMs =
        integerResource(R.integer.lock_pattern_line_fade_out_duration)
    val lineFadeOutAnimationDelayMs = integerResource(R.integer.lock_pattern_line_fade_out_delay)


    val scope = rememberCoroutineScope()
    val scope = rememberCoroutineScope()
    val view = LocalView.current


    // When the current dot is changed, we need to update our animations.
    // When the current dot is changed, we need to update our animations.
    LaunchedEffect(currentDot) {
    LaunchedEffect(currentDot) {
        view.performHapticFeedback(
            HapticFeedbackConstants.VIRTUAL_KEY,
            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
        )

        // Make sure that the current dot is scaled up while the other dots are scaled back down.
        // Make sure that the current dot is scaled up while the other dots are scaled back down.
        scales.entries.forEach { (dot, animatable) ->
        dotScalingAnimatables.entries.forEach { (dot, animatable) ->
            val isSelected = dot == currentDot
            val isSelected = dot == currentDot
            launch {
            launch {
                animatable.animateTo(if (isSelected) 2f else 1f)
                animatable.animateTo(if (isSelected) 2f else 1f)
@@ -102,20 +113,31 @@ internal fun PatternBouncer(
            }
            }
        }
        }


        // Make sure that all dot-connecting lines are decaying, if they're not already animating.
        selectedDots.forEach { dot ->
        selectedDots.forEach {
            lineFadeOutAnimatables[dot]?.let { line ->
            lines[it]?.let { line ->
                if (!line.isRunning) {
                if (!line.isRunning) {
                    scope.launch {
                    scope.launch {
                        if (dot == currentDot) {
                            // Reset the fade-out animation for the current dot. When the current
                            // dot is switched, this entire code block runs again for the newly
                            // selected dot.
                            line.snapTo(1f)
                        } else {
                            // For all non-current dots, make sure that the lines are fading out.
                            line.animateTo(
                            line.animateTo(
                                targetValue = 0f,
                                targetValue = 0f,
                            animationSpec = tween(durationMillis = 500),
                                animationSpec =
                                    tween(
                                        durationMillis = lineFadeOutAnimationDurationMs,
                                        delayMillis = lineFadeOutAnimationDelayMs,
                                    ),
                            )
                            )
                        }
                        }
                    }
                    }
                }
                }
            }
            }
        }
        }
    }


    // This is the position of the input pointer.
    // This is the position of the input pointer.
    var inputPosition: Offset? by remember { mutableStateOf(null) }
    var inputPosition: Offset? by remember { mutableStateOf(null) }
@@ -134,7 +156,7 @@ internal fun PatternBouncer(
                    },
                    },
                    onDragEnd = {
                    onDragEnd = {
                        inputPosition = null
                        inputPosition = null
                        lines.values.forEach { animatable ->
                        lineFadeOutAnimatables.values.forEach { animatable ->
                            scope.launch { animatable.animateTo(1f) }
                            scope.launch { animatable.animateTo(1f) }
                        }
                        }
                        viewModel.onDragEnd()
                        viewModel.onDragEnd()
@@ -154,14 +176,22 @@ internal fun PatternBouncer(
        selectedDots.forEachIndexed { index, dot ->
        selectedDots.forEachIndexed { index, dot ->
            if (index > 0) {
            if (index > 0) {
                val previousDot = selectedDots[index - 1]
                val previousDot = selectedDots[index - 1]
                val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
                val startLerp = 1 - lineFadeOutAnimationProgress
                val from = pixelOffset(previousDot, spacing, verticalOffset)
                val to = pixelOffset(dot, spacing, verticalOffset)
                val lerpedFrom =
                    Offset(
                        x = from.x + (to.x - from.x) * startLerp,
                        y = from.y + (to.y - from.y) * startLerp,
                    )
                drawLine(
                drawLine(
                    from = previousDot,
                    start = lerpedFrom,
                    to = dot,
                    end = to,
                    alpha = { distance -> lineAlpha(spacing, distance) },
                    cap = StrokeCap.Round,
                    spacing = spacing,
                    alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
                    verticalOffset = verticalOffset,
                    color = lineColor,
                    lineColor = lineColor,
                    strokeWidth = lineStrokeWidth,
                    lineStrokeWidth = lineStrokeWidth,
                )
                )
            }
            }
        }
        }
@@ -169,93 +199,31 @@ internal fun PatternBouncer(
        // Draw the line between the most recently-selected dot and the input pointer position.
        // Draw the line between the most recently-selected dot and the input pointer position.
        inputPosition?.let { lineEnd ->
        inputPosition?.let { lineEnd ->
            currentDot?.let { dot ->
            currentDot?.let { dot ->
                val from = pixelOffset(dot, spacing, verticalOffset)
                val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
                drawLine(
                drawLine(
                    from = dot,
                    start = from,
                    to = lineEnd,
                    end = lineEnd,
                    alpha = { distance -> lineAlpha(spacing, distance) },
                    cap = StrokeCap.Round,
                    spacing = spacing,
                    alpha = lineAlpha(spacing, lineLength),
                    verticalOffset = verticalOffset,
                    color = lineColor,
                    lineColor = lineColor,
                    strokeWidth = lineStrokeWidth,
                    lineStrokeWidth = lineStrokeWidth,
                )
                )
            }
            }
        }
        }


        // Draw each dot on the grid.
        // Draw each dot on the grid.
        dots.forEach { dot ->
        dots.forEach { dot ->
            drawDot(
                dot = dot,
                scaleFactor = { scales[dot]?.value ?: 1f },
                spacing = spacing,
                verticalOffset = verticalOffset,
                dotColor = dotColor,
                dotRadius = dotRadius,
            )
        }
    }
}

/** Draws the given [dot]. */
private fun DrawScope.drawDot(
    dot: PatternDotViewModel,
    scaleFactor: () -> Float,
    spacing: Float,
    verticalOffset: Float,
    dotColor: Color,
    dotRadius: Float,
) {
            drawCircle(
            drawCircle(
        color = dotColor,
        radius = dotRadius * scaleFactor.invoke(),
                center = pixelOffset(dot, spacing, verticalOffset),
                center = pixelOffset(dot, spacing, verticalOffset),
                color = dotColor,
                radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
            )
            )
        }
        }

/** Draws a line from the [from] origin dot to the [to] destination dot. */
private fun DrawScope.drawLine(
    from: PatternDotViewModel,
    to: PatternDotViewModel,
    alpha: (distance: Float) -> Float,
    spacing: Float,
    verticalOffset: Float,
    lineColor: Color,
    lineStrokeWidth: Float,
) {
    drawLine(
        from = from,
        to = pixelOffset(to, spacing, verticalOffset),
        alpha = alpha,
        spacing = spacing,
        verticalOffset = verticalOffset,
        lineColor = lineColor,
        lineStrokeWidth = lineStrokeWidth,
    )
    }
    }

/** Draws a line from the [from] origin dot to the [to] destination. */
private fun DrawScope.drawLine(
    from: PatternDotViewModel,
    to: Offset,
    alpha: (distance: Float) -> Float,
    spacing: Float,
    verticalOffset: Float,
    lineColor: Color,
    lineStrokeWidth: Float,
) {
    val fromAsOffset = pixelOffset(from, spacing, verticalOffset)
    val distance = sqrt((to.y - fromAsOffset.y).pow(2) + (to.x - fromAsOffset.x).pow(2))

    drawLine(
        color = lineColor,
        start = fromAsOffset,
        end = to,
        strokeWidth = lineStrokeWidth,
        cap = StrokeCap.Round,
        alpha = alpha.invoke(distance),
    )
}
}


/** Returns an [Offset] representation of the given [dot] in pixel coordinates. */
/** Returns an [Offset] representation of the given [dot], in pixel coordinates. */
private fun pixelOffset(
private fun pixelOffset(
    dot: PatternDotViewModel,
    dot: PatternDotViewModel,
    spacing: Float,
    spacing: Float,
@@ -268,14 +236,14 @@ private fun pixelOffset(
}
}


/**
/**
 * Returns the alpha for a line between dots where dots are [spacing] apart from each other on the
 * Returns the alpha for a line between dots where dots are normally [gridSpacing] apart from each
 * dot grid and the line ends [distance] away from the origin dot.
 * other on the dot grid and the line ends [lineLength] away from the origin dot.
 *
 *
 * The reason [distance] can be different from [spacing] is that all lines originate in dots but one
 * The reason [lineLength] can be different from [gridSpacing] is that all lines originate in dots
 * line might end where the user input pointer is, which isn't always a dot position.
 * but one line might end where the user input pointer is, which isn't always a dot position.
 */
 */
private fun lineAlpha(spacing: Float, distance: Float): Float {
private fun lineAlpha(gridSpacing: Float, lineLength: Float = gridSpacing): Float {
    // Custom curve for the alpha of a line as a function of its distance from its source dot. The
    // Custom curve for the alpha of a line as a function of its distance from its source dot. The
    // farther the user input pointer goes from the line, the more opaque the line gets.
    // farther the user input pointer goes from the line, the more opaque the line gets.
    return ((distance / spacing - 0.3f) * 4f).coerceIn(0f, 1f)
    return ((lineLength / gridSpacing - 0.3f) * 4f).coerceIn(0f, 1f)
}
}