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

Commit 7c353ae9 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Add support for borders in Expandable (1/2)

This CL adds support for borders in Expandable. See
b/230830644#comment24 for a video.

Bug: 230830644
Test: Manual
Change-Id: If9ce29bac72b725eb01b1839a982a2ce17efc41a
parent c14e8fec
Loading
Loading
Loading
Loading
+126 −23
Original line number Diff line number Diff line
@@ -20,7 +20,9 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -41,10 +43,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathOperation
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
@@ -53,7 +62,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Density
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.ViewTreeViewModelStoreOwner
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.LaunchAnimator
import kotlin.math.max
import kotlin.math.min

/**
@@ -85,10 +96,11 @@ fun Expandable(
    shape: Shape,
    modifier: Modifier = Modifier,
    contentColor: Color = contentColorFor(color),
    borderStroke: BorderStroke? = null,
    content: @Composable (ExpandableController) -> Unit,
) {
    Expandable(
        rememberExpandableController(color, shape, contentColor),
        rememberExpandableController(color, shape, contentColor, borderStroke),
        modifier,
        content,
    )
@@ -189,7 +201,11 @@ fun Expandable(
        }
        else -> {
            Box(
                modifier.clip(shape).background(color, shape).onGloballyPositioned {
                modifier
                    .clip(shape)
                    .background(color, shape)
                    .border(controller)
                    .onGloballyPositioned {
                        controller.boundsInComposeViewRoot.value = it.boundsInRoot()
                    }
            ) { wrappedContent(controller) }
@@ -204,7 +220,7 @@ private fun AnimatedContentInOverlay(
    sizeInOriginalLayout: Size,
    animatorState: State<LaunchAnimator.State?>,
    overlay: ViewGroupOverlay,
    controller: ExpandableController,
    controller: ExpandableControllerImpl,
    content: @Composable (ExpandableController) -> Unit,
    composeViewRoot: View,
    onOverlayComposeViewChanged: (View?) -> Unit,
@@ -254,24 +270,7 @@ private fun AnimatedContentInOverlay(
                                    return@drawWithContent
                                }

                                val topRadius = animatorState.topCornerRadius
                                val bottomRadius = animatorState.bottomCornerRadius
                                if (topRadius == bottomRadius) {
                                    // Shortcut to avoid Outline calculation and allocation.
                                    val cornerRadius = CornerRadius(topRadius)
                                    drawRoundRect(color, cornerRadius = cornerRadius)
                                } else {
                                    val shape =
                                        RoundedCornerShape(
                                            topStart = topRadius,
                                            topEnd = topRadius,
                                            bottomStart = bottomRadius,
                                            bottomEnd = bottomRadius,
                                        )
                                    val outline = shape.createOutline(size, layoutDirection, this)
                                    drawOutline(outline, color = color)
                                }

                                drawBackground(animatorState, color, controller.borderStroke)
                                drawContent()
                            },
                            // We center the content in the expanding container.
@@ -360,3 +359,107 @@ private fun getOverlayViewGroup(context: Context, overlay: ViewGroupOverlay): Vi
    overlay.remove(view)
    return current as ViewGroup
}

private fun Modifier.border(controller: ExpandableControllerImpl): Modifier {
    return if (controller.borderStroke != null) {
        this.border(controller.borderStroke, controller.shape)
    } else {
        this
    }
}

private fun ContentDrawScope.drawBackground(
    animatorState: LaunchAnimator.State,
    color: Color,
    border: BorderStroke?,
) {
    val topRadius = animatorState.topCornerRadius
    val bottomRadius = animatorState.bottomCornerRadius
    if (topRadius == bottomRadius) {
        // Shortcut to avoid Outline calculation and allocation.
        val cornerRadius = CornerRadius(topRadius)

        // Draw the background.
        drawRoundRect(color, cornerRadius = cornerRadius)

        // Draw the border.
        if (border != null) {
            // Copied from androidx.compose.foundation.Border.kt
            val strokeWidth = border.width.toPx()
            val halfStroke = strokeWidth / 2
            val borderStroke = Stroke(strokeWidth)

            drawRoundRect(
                brush = border.brush,
                topLeft = Offset(halfStroke, halfStroke),
                size = Size(size.width - strokeWidth, size.height - strokeWidth),
                cornerRadius = cornerRadius.shrink(halfStroke),
                style = borderStroke
            )
        }
    } else {
        val shape =
            RoundedCornerShape(
                topStart = topRadius,
                topEnd = topRadius,
                bottomStart = bottomRadius,
                bottomEnd = bottomRadius,
            )
        val outline = shape.createOutline(size, layoutDirection, this)

        // Draw the background.
        drawOutline(outline, color = color)

        // Draw the border.
        if (border != null) {
            // Copied from androidx.compose.foundation.Border.kt.
            val strokeWidth = border.width.toPx()
            val path =
                createRoundRectPath(
                    (outline as Outline.Rounded).roundRect,
                    strokeWidth,
                )

            drawPath(path, border.brush)
        }
    }
}

/**
 * Helper method that creates a round rect with the inner region removed by the given stroke width.
 *
 * Copied from androidx.compose.foundation.Border.kt.
 */
private fun createRoundRectPath(
    roundedRect: RoundRect,
    strokeWidth: Float,
): Path {
    return Path().apply {
        addRoundRect(roundedRect)
        val insetPath =
            Path().apply { addRoundRect(createInsetRoundedRect(strokeWidth, roundedRect)) }
        op(this, insetPath, PathOperation.Difference)
    }
}

/* Copied from androidx.compose.foundation.Border.kt. */
private fun createInsetRoundedRect(widthPx: Float, roundedRect: RoundRect) =
    RoundRect(
        left = widthPx,
        top = widthPx,
        right = roundedRect.width - widthPx,
        bottom = roundedRect.height - widthPx,
        topLeftCornerRadius = roundedRect.topLeftCornerRadius.shrink(widthPx),
        topRightCornerRadius = roundedRect.topRightCornerRadius.shrink(widthPx),
        bottomLeftCornerRadius = roundedRect.bottomLeftCornerRadius.shrink(widthPx),
        bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.shrink(widthPx)
    )

/**
 * Helper method to shrink the corner radius by the given value, clamping to 0 if the resultant
 * corner radius would be negative.
 *
 * Copied from androidx.compose.foundation.Border.kt.
 */
private fun CornerRadius.shrink(value: Float): CornerRadius =
    CornerRadius(max(0f, this.x - value), max(0f, this.y - value))
+13 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
import android.view.ViewRootImpl
import androidx.compose.foundation.BorderStroke
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -62,6 +63,7 @@ fun rememberExpandableController(
    color: Color,
    shape: Shape,
    contentColor: Color = contentColorFor(color),
    borderStroke: BorderStroke? = null,
): ExpandableController {
    val composeViewRoot = LocalView.current
    val density = LocalDensity.current
@@ -87,11 +89,20 @@ fun rememberExpandableController(
    val isComposed = remember { mutableStateOf(true) }
    DisposableEffect(Unit) { onDispose { isComposed.value = false } }

    return remember(color, contentColor, shape, composeViewRoot, density, layoutDirection) {
    return remember(
        color,
        contentColor,
        shape,
        borderStroke,
        composeViewRoot,
        density,
        layoutDirection,
    ) {
        ExpandableControllerImpl(
            color,
            contentColor,
            shape,
            borderStroke,
            composeViewRoot,
            density,
            animatorState,
@@ -109,6 +120,7 @@ internal class ExpandableControllerImpl(
    internal val color: Color,
    internal val contentColor: Color,
    internal val shape: Shape,
    internal val borderStroke: BorderStroke?,
    internal val composeViewRoot: View,
    internal val density: Density,
    internal val animatorState: MutableState<LaunchAnimator.State?>,