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

Commit 3ea629ce authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "[multishade] Deletes unused multi-shade code." into udc-qpr-dev

parents 6ff0eddd a4d1515d
Loading
Loading
Loading
Loading
+0 −849

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −10
Original line number Diff line number Diff line
@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock

/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
@@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade {
        throwComposeUnavailableError()
    }

    override fun createMultiShadeView(
        context: Context,
        viewModel: MultiShadeViewModel,
        clock: SystemClock,
    ): View {
        throwComposeUnavailableError()
    }

    override fun createSceneContainerView(
        context: Context,
        viewModel: SceneContainerViewModel,
+0 −20
Original line number Diff line number Diff line
@@ -23,8 +23,6 @@ import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
import com.android.systemui.multishade.ui.composable.MultiShade
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -34,7 +32,6 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock

/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
@@ -60,23 +57,6 @@ object ComposeFacade : BaseComposeFacade {
        }
    }

    override fun createMultiShadeView(
        context: Context,
        viewModel: MultiShadeViewModel,
        clock: SystemClock,
    ): View {
        return ComposeView(context).apply {
            setContent {
                PlatformTheme {
                    MultiShade(
                        viewModel = viewModel,
                        clock = clock,
                    )
                }
            }
        }
    }

    override fun createSceneContainerView(
        context: Context,
        viewModel: SceneContainerViewModel,
+0 −145
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.multishade.ui.composable

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntSize
import com.android.systemui.R
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.statusbar.ui.composable.StatusBar
import com.android.systemui.util.time.SystemClock

@Composable
fun MultiShade(
    viewModel: MultiShadeViewModel,
    clock: SystemClock,
    modifier: Modifier = Modifier,
) {
    val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
    val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState()

    // TODO(b/273298030): find a different way to get the height constraint from its parent.
    BoxWithConstraints(modifier = modifier) {
        val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }

        Scrim(
            modifier = Modifier.fillMaxSize(),
            remoteTouch = viewModel::onScrimTouched,
            alpha = { scrimAlpha },
            isScrimEnabled = isScrimEnabled,
        )
        Shade(
            viewModel = viewModel.leftShade,
            currentTimeMillis = clock::elapsedRealtime,
            containerHeightPx = maxHeightPx,
            modifier = Modifier.align(Alignment.TopStart),
        ) {
            Column {
                StatusBar()
                Notifications()
            }
        }
        Shade(
            viewModel = viewModel.rightShade,
            currentTimeMillis = clock::elapsedRealtime,
            containerHeightPx = maxHeightPx,
            modifier = Modifier.align(Alignment.TopEnd),
        ) {
            Column {
                StatusBar()
                QuickSettings()
            }
        }
        Shade(
            viewModel = viewModel.singleShade,
            currentTimeMillis = clock::elapsedRealtime,
            containerHeightPx = maxHeightPx,
            modifier = Modifier,
        ) {
            Column {
                StatusBar()
                Notifications()
                QuickSettings()
            }
        }
    }
}

@Composable
private fun Scrim(
    remoteTouch: (ProxiedInputModel) -> Unit,
    alpha: () -> Float,
    isScrimEnabled: Boolean,
    modifier: Modifier = Modifier,
) {
    var size by remember { mutableStateOf(IntSize.Zero) }

    Box(
        modifier =
            modifier
                .graphicsLayer { this.alpha = alpha() }
                .background(colorResource(R.color.opaque_scrim))
                .fillMaxSize()
                .onSizeChanged { size = it }
                .then(
                    if (isScrimEnabled) {
                        Modifier.pointerInput(Unit) {
                                detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
                            }
                            .pointerInput(Unit) {
                                detectVerticalDragGestures(
                                    onVerticalDrag = { change, dragAmount ->
                                        remoteTouch(
                                            ProxiedInputModel.OnDrag(
                                                xFraction = change.position.x / size.width,
                                                yDragAmountPx = dragAmount,
                                            )
                                        )
                                    },
                                    onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
                                    onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
                                )
                            }
                    } else {
                        Modifier
                    }
                )
    )
}
+0 −336
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.multishade.ui.composable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
import com.android.compose.swipeable.FixedThreshold
import com.android.compose.swipeable.SwipeableState
import com.android.compose.swipeable.ThresholdConfig
import com.android.compose.swipeable.rememberSwipeableState
import com.android.compose.swipeable.swipeable
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
import kotlin.math.min
import kotlin.math.roundToInt
import kotlinx.coroutines.launch

/**
 * Renders a shade (container and content).
 *
 * This should be allowed to grow to fill the width and height of its container.
 *
 * @param viewModel The view-model for this shade.
 * @param currentTimeMillis A provider for the current time, in milliseconds.
 * @param containerHeightPx The height of the container that this shade is being shown in, in
 *   pixels.
 * @param modifier The Modifier.
 * @param content The content of the shade.
 */
@Composable
fun Shade(
    viewModel: ShadeViewModel,
    currentTimeMillis: () -> Long,
    containerHeightPx: Float,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit = {},
) {
    val isVisible: Boolean by viewModel.isVisible.collectAsState()
    if (!isVisible) {
        return
    }

    val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
    ReportNonProxiedInput(viewModel, interactionSource)

    val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
    HandleForcedCollapse(viewModel, swipeableState)
    HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
    ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)

    val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
    val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
    val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()

    val width: ShadeViewModel.Size by viewModel.width.collectAsState()
    val density = LocalDensity.current

    val anchors: Map<Float, ShadeState> =
        remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }

    ShadeContent(
        shadeHeightPx = { swipeableState.offset.value },
        overstretch = { swipeableState.overflow.value / containerHeightPx },
        isSwipingEnabled = isSwipingEnabled,
        swipeableState = swipeableState,
        interactionSource = interactionSource,
        anchors = anchors,
        thresholds = { _, to ->
            swipeableThresholds(
                to = to,
                swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
                swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
            )
        },
        modifier = modifier.shadeWidth(width, density),
        content = content,
    )
}

/**
 * Draws the content of the shade.
 *
 * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
 *   fully collapsed.
 * @param overstretch Provider for the current amount of vertical "overstretch" that the shade
 *   should be rendered with. This is `0` or a positive number that is a percentage of the total
 *   height of the shade when fully expanded. A value of `0` means that the shade is not stretched
 *   at all.
 * @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
 * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
 *   addition to direct control (proxied user input in addition to non-proxied/direct user input).
 * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
 *   occurs; this is used to configure the [swipeable] modifier.
 * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
 *   another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
 *   user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
 * @param content The content to render inside the shade.
 * @param modifier The [Modifier].
 */
@Composable
private fun ShadeContent(
    shadeHeightPx: () -> Float,
    overstretch: () -> Float,
    isSwipingEnabled: Boolean,
    swipeableState: SwipeableState<ShadeState>,
    interactionSource: MutableInteractionSource,
    anchors: Map<Float, ShadeState>,
    thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit = {},
) {
    /**
     * Returns a function that takes in [Density] and returns the current padding around the shade
     * content.
     */
    fun padding(
        shadeHeightPx: () -> Float,
    ): Density.() -> Int {
        return {
            min(
                12.dp.toPx().roundToInt(),
                shadeHeightPx().roundToInt(),
            )
        }
    }

    Surface(
        shape = RoundedCornerShape(32.dp),
        modifier =
            modifier
                .fillMaxWidth()
                .height { shadeHeightPx().roundToInt() }
                .padding(
                    horizontal = padding(shadeHeightPx),
                    vertical = padding(shadeHeightPx),
                )
                .graphicsLayer {
                    // Applies the vertical over-stretching of the shade content that may happen if
                    // the user keep dragging down when the shade is already fully-expanded.
                    transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
                    this.scaleY = 1 + overstretch().coerceAtLeast(0f)
                }
                .swipeable(
                    enabled = isSwipingEnabled,
                    state = swipeableState,
                    interactionSource = interactionSource,
                    anchors = anchors,
                    thresholds = thresholds,
                    orientation = Orientation.Vertical,
                ),
        content = content,
    )
}

/** Funnels current shade expansion values into the view-model. */
@Composable
private fun ReportShadeExpansion(
    viewModel: ShadeViewModel,
    swipeableState: SwipeableState<ShadeState>,
    containerHeightPx: Float,
) {
    LaunchedEffect(swipeableState.offset, containerHeightPx) {
        snapshotFlow { swipeableState.offset.value / containerHeightPx }
            .collect { expansion -> viewModel.onExpansionChanged(expansion) }
    }
}

/** Funnels drag gesture start and end events into the view-model. */
@Composable
private fun ReportNonProxiedInput(
    viewModel: ShadeViewModel,
    interactionSource: InteractionSource,
) {
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect {
            when (it) {
                is DragInteraction.Start -> {
                    viewModel.onDragStarted()
                }
                is DragInteraction.Stop -> {
                    viewModel.onDragEnded()
                }
            }
        }
    }
}

/** When told to force collapse, collapses the shade. */
@Composable
private fun HandleForcedCollapse(
    viewModel: ShadeViewModel,
    swipeableState: SwipeableState<ShadeState>,
) {
    LaunchedEffect(viewModel) {
        viewModel.isForceCollapsed.collect {
            launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
        }
    }
}

/**
 * Handles proxied input (input originating outside of the UI of the shade) by driving the
 * [SwipeableState] accordingly.
 */
@Composable
private fun HandleProxiedInput(
    viewModel: ShadeViewModel,
    swipeableState: SwipeableState<ShadeState>,
    currentTimeMillis: () -> Long,
) {
    val velocityTracker: VelocityTracker = remember { VelocityTracker() }
    LaunchedEffect(viewModel) {
        viewModel.proxiedInput.collect {
            when (it) {
                is ProxiedInputModel.OnDrag -> {
                    velocityTracker.addPosition(
                        timeMillis = currentTimeMillis.invoke(),
                        position = Offset(0f, it.yDragAmountPx),
                    )
                    swipeableState.performDrag(it.yDragAmountPx)
                }
                is ProxiedInputModel.OnDragEnd -> {
                    launch {
                        val velocity = velocityTracker.calculateVelocity().y
                        velocityTracker.resetTracking()
                        // We use a VelocityTracker to keep a record of how fast the pointer was
                        // moving such that we know how far to fling the shade when the gesture
                        // ends. Flinging the SwipeableState using performFling is required after
                        // one or more calls to performDrag such that the swipeable settles into one
                        // of the states. Without doing that, the shade would remain unmoving in an
                        // in-between state on the screen.
                        swipeableState.performFling(velocity)
                    }
                }
                is ProxiedInputModel.OnDragCancel -> {
                    launch {
                        velocityTracker.resetTracking()
                        swipeableState.animateTo(swipeableState.progress.from)
                    }
                }
                else -> Unit
            }
        }
    }
}

/**
 * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
 *
 * @param density The [Density] of the display.
 * @param wholePx The whole amount that the given [Float] is a fraction of.
 * @return The dp size that's a fraction of the whole amount.
 */
private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
    return with(density) { (this@fractionToDp * wholePx).toDp() }
}

private fun Modifier.shadeWidth(
    size: ShadeViewModel.Size,
    density: Density,
): Modifier {
    return then(
        when (size) {
            is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
            is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
        }
    )
}

/** Returns the pixel positions for each of the supported shade states. */
private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
    return mapOf(
        0f to ShadeState.FullyCollapsed,
        containerHeightPx to ShadeState.FullyExpanded,
    )
}

/**
 * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
 * actually completes the expansion or collapse after the user lifts their pointer.
 */
private fun swipeableThresholds(
    to: ShadeState,
    swipeExpandThreshold: Dp,
    swipeCollapseThreshold: Dp,
): ThresholdConfig {
    return FixedThreshold(
        when (to) {
            ShadeState.FullyExpanded -> swipeExpandThreshold
            ShadeState.FullyCollapsed -> swipeCollapseThreshold
        }
    )
}

/** Enumerates the shade UI states for [SwipeableState]. */
private enum class ShadeState {
    FullyCollapsed,
    FullyExpanded,
}
Loading