diff --git a/CanonicalLayouts/list-detail-compose/app/build.gradle b/CanonicalLayouts/list-detail-compose/app/build.gradle index 071e514a..1f3218b5 100644 --- a/CanonicalLayouts/list-detail-compose/app/build.gradle +++ b/CanonicalLayouts/list-detail-compose/app/build.gradle @@ -62,18 +62,21 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2023.10.01') + def composeBom = platform('androidx.compose:compose-bom:2024.03.00') implementation(composeBom) implementation "com.google.accompanist:accompanist-adaptive:0.32.0" implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' - implementation "androidx.compose.foundation:foundation:1.6.0-rc01" - implementation "androidx.compose.ui:ui:1.6.0-rc01" + implementation "androidx.compose.foundation:foundation:1.6.4" + implementation "androidx.compose.ui:ui:1.6.4" implementation "androidx.compose.ui:ui-tooling-preview" implementation "androidx.window:window:1.2.0" - implementation 'androidx.compose.material3:material3' - implementation "androidx.compose.material3:material3-window-size-class" + implementation 'androidx.compose.material3:material3:1.3.0-alpha03' + implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-alpha09' + implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha09' + implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha09' + implementation "androidx.compose.material3:material3-window-size-class:1.3.0-alpha03" testImplementation 'junit:junit:4.13.2' } diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/MainActivity.kt b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/MainActivity.kt index c56dd200..46b41979 100644 --- a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/MainActivity.kt +++ b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/MainActivity.kt @@ -20,24 +20,17 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.ui.Modifier import com.example.listdetailcompose.ui.ListDetailSample import com.example.listdetailcompose.ui.theme.ListDetailComposeTheme -import com.google.accompanist.adaptive.calculateDisplayFeatures class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ListDetailComposeTheme { Surface(modifier = Modifier.fillMaxSize()) { - ListDetailSample( - windowSizeClass = calculateWindowSizeClass(this), - displayFeatures = calculateDisplayFeatures(this) - ) + ListDetailSample() } } } diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetail.kt b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetail.kt deleted file mode 100644 index e65636e7..00000000 --- a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetail.kt +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright 2022 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 - * - * https://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.example.listdetailcompose.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState -import androidx.compose.foundation.interaction.collectIsHoveredAsState -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.systemGestureExclusion -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.movableContentOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.window.layout.DisplayFeature -import com.google.accompanist.adaptive.FoldAwareConfiguration -import com.google.accompanist.adaptive.SplitResult -import com.google.accompanist.adaptive.TwoPane -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlin.math.max -import kotlin.math.roundToInt - -/** - * A higher-order component displaying an opinionated list-detail format. - * - * The [list] slot is the primary content, and is in a parent relationship with the content - * displayed in [detail]. - * - * This relationship implies that different detail screens may be swapped out for each other, and - * should be distinguished by passing a [detailKey] to control when a different detail is being - * shown (to reset instance state. - * - * When there is enough space to display both list and detail, pass `true` to [showListAndDetail] - * to show both the list and the detail at the same time. This content is displayed in a [TwoPane]. - * - * When there is not enough space to display both list and detail, which slot is displayed is based - * on [isDetailOpen]. Internally, this state is changed in an opinionated way via [setIsDetailOpen]. - * For instance, when showing just the detail screen, a back button press will call - * [setIsDetailOpen] passing `false`. - */ -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ListDetail( - isDetailOpen: Boolean, - setIsDetailOpen: (Boolean) -> Unit, - showListAndDetail: Boolean, - detailKey: Any?, - list: @Composable (isDetailVisible: Boolean) -> Unit, - detail: @Composable (isListVisible: Boolean) -> Unit, - displayFeatures: List, - modifier: Modifier = Modifier, -) { - val currentIsDetailOpen by rememberUpdatedState(isDetailOpen) - val currentShowListAndDetail by rememberUpdatedState(showListAndDetail) - val currentDetailKey by rememberUpdatedState(detailKey) - - // Determine whether to show the list and/or the detail. - // This is a function of current app state, and the width size class. - val showList by remember { - derivedStateOf { - currentShowListAndDetail || !currentIsDetailOpen - } - } - val showDetail by remember { - derivedStateOf { - currentShowListAndDetail || currentIsDetailOpen - } - } - // Validity check: we should always be showing something - check(showList || showDetail) - - val listSaveableStateHolder = rememberSaveableStateHolder() - val detailSaveableStateHolder = rememberSaveableStateHolder() - - val start = remember { - movableContentOf { - // Set up a SaveableStateProvider so the list state will be preserved even while it - // is hidden if the detail is showing instead. - listSaveableStateHolder.SaveableStateProvider(0) { - Box( - modifier = Modifier - .userInteractionNotification { - // When interacting with the list, consider the detail to no longer be - // open in the case of resize. - setIsDetailOpen(false) - } - ) { - list(showDetail) - } - } - } - } - - val end = remember { - movableContentOf { - // Set up a SaveableStateProvider against the selected word index to save detail - // state while switching between details. - // If this behavior isn't desired, this can be replaced with a key on the - // selectedWordIndex. - detailSaveableStateHolder.SaveableStateProvider(currentDetailKey ?: "null") { - Box( - modifier = Modifier - .userInteractionNotification { - // When interacting with the detail, consider the detail to be - // open in the case of resize. - setIsDetailOpen(true) - } - ) { - detail(showList) - } - } - } - } - - val density = LocalDensity.current - val anchoredDraggableState = rememberSaveable( - saver = AnchoredDraggableState.Saver( - animationSpec = spring(), - positionalThreshold = { distance: Float -> distance * 0.5f }, - velocityThreshold = { with(density) { 400.dp.toPx() } }, - ) - ) { - AnchoredDraggableState( - initialValue = ExpandablePaneState.ListAndDetail, - animationSpec = spring(), - positionalThreshold = { distance: Float -> distance * 0.5f }, - velocityThreshold = { with(density) { 400.dp.toPx() } }, - ) - } - - val coroutineScope = rememberCoroutineScope() - - // Sync the `isDetailOpen` as a side-effect to the expandable pane state. - LaunchedEffect(isDetailOpen) { - if (isDetailOpen) { - when (anchoredDraggableState.currentValue) { - ExpandablePaneState.ListOnly -> { - anchoredDraggableState.animateTo(ExpandablePaneState.DetailOnly) - } - ExpandablePaneState.ListAndDetail, - ExpandablePaneState.DetailOnly - -> Unit - } - } else { - when (anchoredDraggableState.currentValue) { - ExpandablePaneState.ListOnly, - ExpandablePaneState.ListAndDetail -> Unit - ExpandablePaneState.DetailOnly -> { - anchoredDraggableState.animateTo(ExpandablePaneState.ListOnly) - } - } - } - } - - // Update the `isDetailOpen` boolean as a side-effect of the expandable pane reaching a specific value. - // We only do this if both the list and detail are capable of being shown, as - if (showListAndDetail) { - LaunchedEffect(anchoredDraggableState) { - snapshotFlow { anchoredDraggableState.currentValue } - .onEach { - when (anchoredDraggableState.currentValue) { - ExpandablePaneState.ListOnly -> setIsDetailOpen(false) - ExpandablePaneState.ListAndDetail -> setIsDetailOpen(true) - ExpandablePaneState.DetailOnly -> setIsDetailOpen(true) - } - } - .collect() - } - } - - // If showing just the detail due to the expandable pane state, allow a back press to hide the detail to return to - // the list. - BackHandler( - enabled = showListAndDetail && anchoredDraggableState.currentValue == ExpandablePaneState.DetailOnly - ) { - coroutineScope.launch { - anchoredDraggableState.animateTo(ExpandablePaneState.ListOnly) - } - } - - // If showing just the detail, allow a back press to hide the detail to return to - // the list. - BackHandler( - enabled = !showListAndDetail && !showList - ) { - setIsDetailOpen(false) - } - - val minListPaneWidth = 300.dp - val minDetailPaneWidth = 300.dp - - Box( - modifier = modifier.layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - anchoredDraggableState.updateAnchors( - newAnchors = DraggableAnchors { - ExpandablePaneState.ListOnly at placeable.width.toFloat() - ExpandablePaneState.ListAndDetail at placeable.width.toFloat() / 2f - ExpandablePaneState.DetailOnly at 0f - }, - // Keep the current target, even if resizing causes the offset to be closer to a different one - newTarget = anchoredDraggableState.targetValue - ) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(0, 0) - } - } - ) { - if (showList && showDetail) { - TwoPane( - first = { - // Enforce the minimum list pane width, aligning to the start edge of the screen - // Modifier.requiredWidthIn(min = minListPaneWidth) doesn't work because the content - // would be centered in the available space - Box( - Modifier - .clipToBounds() - .layout { measurable, constraints -> - val width = max(minListPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minListPaneWidth.roundToPx(), - maxWidth = width - ) - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = 0, - y = 0 - ) - } - } - ) { - start() - } - }, - second = { - // Enforce the minimum detail pane width, aligning to the end edge of the screen - // Modifier.requiredWidthIn(min = minDetailPaneWidth) doesn't work because the content - // would be centered in the available space - Box( - Modifier - .clipToBounds() - .layout { measurable, constraints -> - val width = max(minDetailPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minDetailPaneWidth.roundToPx(), - maxWidth = width - ) - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = constraints.maxWidth - max(constraints.maxWidth, placeable.width), - y = 0 - ) - } - } - ) { - end() - } - }, - strategy = { _, layoutDirection, layoutCoordinates -> - val xOffset = when (layoutDirection) { - LayoutDirection.Ltr -> anchoredDraggableState.offset - LayoutDirection.Rtl -> layoutCoordinates.size.width - anchoredDraggableState.offset - } - - SplitResult( - gapOrientation = Orientation.Vertical, - gapBounds = Rect( - offset = Offset(xOffset, 0f), - size = Size(0f, layoutCoordinates.size.height.toFloat()) - ) - ) - }, - displayFeatures = displayFeatures, - foldAwareConfiguration = FoldAwareConfiguration.VerticalFoldsOnly, - modifier = Modifier.fillMaxSize(), - ) - - val dragHandleInteractionSource = remember { MutableInteractionSource() } - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .align(Alignment.CenterStart) - .size(64.dp) - // Offset back half the width so that we are positing the center of the handle - .offset(x = -32.dp) - .offset { - IntOffset( - anchoredDraggableState - .requireOffset() - .roundToInt(), - 0 - ) - } - .anchoredDraggable( - state = anchoredDraggableState, - reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl, - orientation = Orientation.Horizontal, - interactionSource = dragHandleInteractionSource, - ) - .hoverable(dragHandleInteractionSource) - // TODO: Workaround for https://issuetracker.google.com/issues/319881002 to allow isPressed - // to be true - .clickable( - interactionSource = dragHandleInteractionSource, - indication = null, - onClickLabel = null, - role = null, - onClick = {}, - ) - // Allow the drag handle to override the system navigation gesture - .systemGestureExclusion() - ) { - val isHovered by dragHandleInteractionSource.collectIsHoveredAsState() - val isPressed by dragHandleInteractionSource.collectIsPressedAsState() - val isDragged by dragHandleInteractionSource.collectIsDraggedAsState() - val isActive = isHovered || isPressed || isDragged - - val width by animateDpAsState( - if (isActive) 12.dp else 4.dp, - label = "Drag Handle Width" - ) - val color by animateColorAsState( - if (isActive) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.outline - }, - label = "Drag Handle Color" - ) - - Canvas( - modifier = Modifier.fillMaxSize() - ) { - val height = 48.dp - val rectSize = DpSize(width, height).toSize() - - drawRoundRect( - color = color, - topLeft = Offset( - (size.width - rectSize.width) / 2, - (size.height - rectSize.height) / 2, - ), - size = rectSize, - cornerRadius = CornerRadius(rectSize.width / 2f), - ) - } - } - } else if (showList) { - start() - } else { - end() - } - } -} - -enum class ExpandablePaneState { - ListOnly, ListAndDetail, DetailOnly -} diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt index 935db372..cc17b4eb 100644 --- a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt +++ b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt @@ -16,9 +16,9 @@ package com.example.listdetailcompose.ui +import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,26 +30,24 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.window.layout.DisplayFeature import com.example.listdetailcompose.R -import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy // Create some simple sample data private val loremIpsum = """ @@ -73,36 +71,24 @@ private data class DefinedWord( val definition: String ) +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun ListDetailSample( - windowSizeClass: WindowSizeClass, - displayFeatures: List -) { - // Query for the current window size class - val widthSizeClass by rememberUpdatedState(windowSizeClass.widthSizeClass) - - /** - * The index of the currently selected word, or `null` if none is selected - */ +fun ListDetailSample() { var selectedWordIndex: Int? by rememberSaveable { mutableStateOf(null) } + val navigator = rememberListDetailPaneScaffoldNavigator() - /** - * True if the detail is currently open. This is the primary control for "navigation". - */ - var isDetailOpen by rememberSaveable { mutableStateOf(false) } + BackHandler(enabled = navigator.canNavigateBack()) { + navigator.navigateBack() + } - ListDetail( - isDetailOpen = isDetailOpen, - setIsDetailOpen = { isDetailOpen = it }, - showListAndDetail = - when (widthSizeClass) { - WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> false - WindowWidthSizeClass.Expanded -> true - else -> true - }, - detailKey = selectedWordIndex, - list = { isDetailVisible -> + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { val currentSelectedWordIndex = selectedWordIndex + val isDetailVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded + ListContent( words = sampleWords.map(DefinedWord::word), selectionState = if (isDetailVisible && currentSelectedWordIndex != null) { @@ -112,31 +98,16 @@ fun ListDetailSample( }, onIndexClick = { index -> selectedWordIndex = index - // Consider the detail to now be open. This acts like a navigation if - // there isn't room for both list and detail, and also will result - // in the detail remaining open in the case of resize. - isDetailOpen = true - }, - modifier = if (isDetailVisible) { - Modifier.padding(end = 8.dp) - } else { - Modifier + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) } ) }, - detail = { isListVisible -> + detailPane = { val definedWord = selectedWordIndex?.let(sampleWords::get) DetailContent( - definedWord = definedWord, - modifier = if (isListVisible) { - Modifier.padding(start = 8.dp) - } else { - Modifier - } + definedWord = definedWord ) - }, - displayFeatures = displayFeatures, - modifier = Modifier.padding(horizontal = 16.dp) + } ) } @@ -183,21 +154,17 @@ private fun ListContent( ) ) { itemsIndexed(words) { index, word -> - val interactionSource = remember { MutableInteractionSource() } val interactionModifier = when (selectionState) { SelectionVisibilityState.NoSelection -> { Modifier.clickable( - interactionSource = interactionSource, - indication = rememberRipple(), onClick = { onIndexClick(index) } ) } + is SelectionVisibilityState.ShowSelection -> { Modifier.selectable( selected = index == selectionState.selectedWordIndex, - interactionSource = interactionSource, - indication = rememberRipple(), onClick = { onIndexClick(index) } ) } @@ -217,6 +184,7 @@ private fun ListContent( 1.dp, MaterialTheme.colorScheme.outline ) + is SelectionVisibilityState.ShowSelection -> if (index == selectionState.selectedWordIndex) { null diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/UserInteractionNotification.kt b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/UserInteractionNotification.kt deleted file mode 100644 index 5ed1d3c5..00000000 --- a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/UserInteractionNotification.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2022 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 - * - * https://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.example.listdetailcompose.ui - -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive - -/** - * A helper modifier that tracks any interaction happening on the element. - * - * This is usually helpful to understand which side of the [TwoPane] was interacted last so when the - * layout is switch to a single pane the most appropriate UI is shown. - * - * @param onInteracted a callback to be invoked when the modifier element is interacted - */ -fun Modifier.userInteractionNotification(onInteracted: () -> Unit): Modifier { - return pointerInput(onInteracted) { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - while (currentContext.isActive) { - val event = awaitPointerEvent(PointerEventPass.Initial) - // if user taps (down) or scrolls - consider it an interaction signal - if ( - event.type == PointerEventType.Press || event.type == PointerEventType.Scroll - ) { - onInteracted.invoke() - } - } - } - } -} diff --git a/CanonicalLayouts/list-detail-compose/build.gradle b/CanonicalLayouts/list-detail-compose/build.gradle index b15d04da..525604a4 100644 --- a/CanonicalLayouts/list-detail-compose/build.gradle +++ b/CanonicalLayouts/list-detail-compose/build.gradle @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id 'com.android.application' version '8.2.1' apply false - id 'com.android.library' version '8.2.1' apply false + id 'com.android.application' version '8.3.2' apply false + id 'com.android.library' version '8.3.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false } diff --git a/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties b/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties index b63e47b3..98ab018b 100644 --- a/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties +++ b/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed May 25 14:11:15 UTC 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME