From 71d8d094fc3031f384de467dce45e906c1f4bf7f Mon Sep 17 00:00:00 2001 From: Patryk Michalik Date: Mon, 13 Dec 2021 18:33:48 +0100 Subject: [PATCH] Update dependencies and rework bottom sheet implementation --- build.gradle | 8 +- .../lawnchair/ui/preferences/Preferences.kt | 87 ++++--- .../ui/preferences/components/BottomSheet.kt | 186 --------------- .../preferences/components/ListPreference.kt | 85 ++++--- .../components/NotificationDotsPreference.kt | 96 ++++---- .../colorpreference/ColorPreference.kt | 151 ++++++------ .../lawnchair/ui/util/BottomSheetContent.kt | 31 +++ .../ui/util/ProvideBottomSheetHandler.kt | 44 ++++ .../lawnchair/views/ComposeFloatingView.kt | 221 ------------------ 9 files changed, 303 insertions(+), 606 deletions(-) delete mode 100644 lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt create mode 100644 lawnchair/src/app/lawnchair/ui/util/BottomSheetContent.kt create mode 100644 lawnchair/src/app/lawnchair/ui/util/ProvideBottomSheetHandler.kt delete mode 100644 lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt diff --git a/build.gradle b/build.gradle index d59b113ac6..d7a8fc297c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { - kotlin_version = '1.5.31' - compose_version = '1.0.5' - accompanist_version = '0.21.0-beta' + kotlin_version = '1.6.0' + compose_version = '1.1.0-beta04' + accompanist_version = '0.21.3-beta' libsu_version = '3.1.2' protocVersion = '3.18.0' } @@ -307,7 +307,7 @@ dependencies { implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.foundation:foundation:$compose_version" implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.material3:material3:1.0.0-alpha01" + implementation "androidx.compose.material3:material3:1.0.0-alpha02" implementation "androidx.constraintlayout:constraintlayout:2.1.2" implementation 'androidx.activity:activity-compose:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0' diff --git a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt index 432f6310bf..538bc27a4a 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt @@ -18,10 +18,11 @@ package app.lawnchair.ui.preferences import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection @@ -29,12 +30,16 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import app.lawnchair.ui.preferences.about.aboutGraph import app.lawnchair.ui.preferences.components.SystemUi +import app.lawnchair.ui.util.BottomSheetContent +import app.lawnchair.ui.util.BottomSheetHandler +import app.lawnchair.ui.util.ProvideBottomSheetHandler +import app.lawnchair.ui.util.emptyBottomSheetContent import app.lawnchair.ui.util.portal.ProvidePortalNode import app.lawnchair.util.ProvideLifecycleState import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController +import kotlinx.coroutines.launch import soup.compose.material.motion.materialSharedAxisX -import soup.compose.material.motion.rememberSlideDistance object Routes { const val GENERAL: String = "general" @@ -65,33 +70,50 @@ fun Preferences(interactor: PreferenceInteractor = viewModel + bottomSheetContent = BottomSheetContent(content = content) + coroutineScope.launch { bottomSheetState.show() } + }, + hide = { + coroutineScope.launch { bottomSheetState.hide() } + } + ) SystemUi() - Providers { - Surface { - CompositionLocalProvider( - LocalNavController provides navController, - LocalPreferenceInteractor provides interactor, - ) { - AnimatedNavHost( - navController = navController, - startDestination = "/", - enterTransition = { _, _ -> motionSpec.enter.transition(!isRtl, density) }, - exitTransition = { _, _ -> motionSpec.exit.transition(!isRtl, density) }, - popEnterTransition = { _, _ -> motionSpec.enter.transition(isRtl, density) }, - popExitTransition = { _, _ -> motionSpec.exit.transition(isRtl, density) }, + Providers(bottomSheetHandler = bottomSheetHandler) { + ModalBottomSheetLayout( + sheetContent = { bottomSheetContent.content() }, + sheetState = bottomSheetState + ) { + Surface { + CompositionLocalProvider( + LocalNavController provides navController, + LocalPreferenceInteractor provides interactor, ) { - preferenceGraph(route = "/", { PreferencesDashboard() }) { subRoute -> - generalGraph(route = subRoute(Routes.GENERAL)) - homeScreenGraph(route = subRoute(Routes.HOME_SCREEN)) - dockGraph(route = subRoute(Routes.DOCK)) - appDrawerGraph(route = subRoute(Routes.APP_DRAWER)) - folderGraph(route = subRoute(Routes.FOLDERS)) - quickstepGraph(route = subRoute(Routes.QUICKSTEP)) - aboutGraph(route = subRoute(Routes.ABOUT)) - fontSelectionGraph(route = subRoute(Routes.FONT_SELECTION)) - debugMenuGraph(route = subRoute(Routes.DEBUG_MENU)) - selectIconGraph(route = subRoute(Routes.SELECT_ICON)) + AnimatedNavHost( + navController = navController, + startDestination = "/", + enterTransition = { motionSpec.enter.transition(!isRtl, density) }, + exitTransition = { motionSpec.exit.transition(!isRtl, density) }, + popEnterTransition = { motionSpec.enter.transition(isRtl, density) }, + popExitTransition = { motionSpec.exit.transition(isRtl, density) }, + ) { + preferenceGraph(route = "/", { PreferencesDashboard() }) { subRoute -> + generalGraph(route = subRoute(Routes.GENERAL)) + homeScreenGraph(route = subRoute(Routes.HOME_SCREEN)) + dockGraph(route = subRoute(Routes.DOCK)) + appDrawerGraph(route = subRoute(Routes.APP_DRAWER)) + folderGraph(route = subRoute(Routes.FOLDERS)) + quickstepGraph(route = subRoute(Routes.QUICKSTEP)) + aboutGraph(route = subRoute(Routes.ABOUT)) + fontSelectionGraph(route = subRoute(Routes.FONT_SELECTION)) + debugMenuGraph(route = subRoute(Routes.DEBUG_MENU)) + selectIconGraph(route = subRoute(Routes.SELECT_ICON)) + } } } } @@ -100,10 +122,15 @@ fun Preferences(interactor: PreferenceInteractor = viewModel Unit) { +private fun Providers( + bottomSheetHandler: BottomSheetHandler, + content: @Composable () -> Unit +) { ProvidePortalNode { ProvideLifecycleState { - content() + ProvideBottomSheetHandler(handler = bottomSheetHandler) { + content() + } } } } diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt deleted file mode 100644 index d99e265f26..0000000000 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt +++ /dev/null @@ -1,186 +0,0 @@ -package app.lawnchair.ui.preferences.components - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.shape.CornerBasedShape -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.dp -import app.lawnchair.ui.util.portal.Portal -import app.lawnchair.util.BackHandler -import com.google.accompanist.insets.LocalWindowInsets -import kotlinx.coroutines.launch -import java.lang.Integer.max - -@ExperimentalMaterialApi -@Composable -fun BottomSheet( - modifier: Modifier = Modifier, - sheetState: BottomSheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), - scrimColor: Color = BottomSheetDefaults.scrimColor, - sheetShape: CornerBasedShape = MaterialTheme.shapes.large, - sheetBackgroundColor: Color = MaterialTheme.colors.surface, - sheetContent: @Composable () -> Unit, -) { - val currentSheetContent by rememberUpdatedState(sheetContent) - val modalBottomSheetState = sheetState.modalBottomSheetState - val scope = rememberCoroutineScope() - - if (modalBottomSheetState != null) { - Portal { - ModalBottomSheetLayout( - modifier = modifier, - sheetState = modalBottomSheetState, - sheetContent = { StatusBarOffset(currentSheetContent) }, - scrimColor = scrimColor, - sheetShape = sheetShape.copy( - bottomStart = CornerSize(0.dp), - bottomEnd = CornerSize(0.dp) - ), - sheetBackgroundColor = sheetBackgroundColor - ) { - BackHandler { - scope.launch { sheetState.onBackPressed() } - } - } - } - if (!sheetState.isChangingState && !modalBottomSheetState.isVisible) { - sheetState.modalBottomSheetState = null - } - } -} - -@Composable -fun StatusBarOffset(content: @Composable () -> Unit) { - val windowInsets = LocalWindowInsets.current - val statusBarHeight = max(windowInsets.statusBars.top, windowInsets.displayCutout.top) - val topOffset = statusBarHeight + with(LocalDensity.current) { 8.dp.roundToPx() } - - Box( - modifier = Modifier - .layout { measurable, constraints -> - val newConstraints = Constraints( - minWidth = constraints.minWidth, - maxWidth = constraints.maxWidth, - minHeight = constraints.minHeight, - maxHeight = when (constraints.maxHeight) { - Constraints.Infinity -> Constraints.Infinity - else -> constraints.maxHeight - topOffset - } - ) - val placeable = measurable.measure(newConstraints) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(0, 0) - } - } - ) { - content() - } -} - -@ExperimentalMaterialApi -@Composable -fun rememberBottomSheetState( - initialValue: ModalBottomSheetValue, - animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, - confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } -): BottomSheetState = - rememberSaveable( - saver = BottomSheetState.Saver( - animationSpec = animationSpec, - confirmStateChange = confirmStateChange - ) - ) { - BottomSheetState(initialValue, animationSpec, confirmStateChange) - } - -@ExperimentalMaterialApi -class BottomSheetState( - private val initialValue: ModalBottomSheetValue, - private val animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, - private val confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } -) { - internal var isChangingState by mutableStateOf(false) - internal var modalBottomSheetState by mutableStateOf(null) - - suspend fun show() { - transitionState { it.show() } - } - - suspend fun hide() { - transitionState { it.hide() } - } - - suspend fun snapTo(targetValue: ModalBottomSheetValue) { - transitionState { - it.snapTo(targetValue) - } - } - - suspend fun animateTo(targetValue: ModalBottomSheetValue, anim: AnimationSpec = animationSpec) { - transitionState { - it.animateTo(targetValue, anim) - } - } - - private inline fun transitionState(block: (ModalBottomSheetState) -> Unit) { - try { - isChangingState = true - block(getModalBottomSheetState()) - } finally { - isChangingState = false - } - } - - private fun getModalBottomSheetState(): ModalBottomSheetState { - if (modalBottomSheetState == null) { - modalBottomSheetState = - ModalBottomSheetState(initialValue, animationSpec, confirmStateChange) - } - return modalBottomSheetState!! - } - - suspend fun onBackPressed() { - if (confirmStateChange(ModalBottomSheetValue.Hidden)) { - hide() - } - } - - companion object { - /** - * The default [Saver] implementation for [BottomSheetState]. - */ - fun Saver( - animationSpec: AnimationSpec, - confirmStateChange: (ModalBottomSheetValue) -> Boolean - ): Saver = Saver( - save = { it.modalBottomSheetState?.currentValue }, - restore = { - BottomSheetState( - initialValue = it, - animationSpec = animationSpec, - confirmStateChange = confirmStateChange - ) - } - ) - } -} - -object BottomSheetDefaults { - val scrimColor: Color - @Composable - get() = if (MaterialTheme.colors.isLight) { - MaterialTheme.colors.onBackground.copy(alpha = 0.32F) - } else { - MaterialTheme.colors.background.copy(alpha = 0.32F) - } -} \ No newline at end of file diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt index 5e810b1b9a..2ac15b8b30 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt @@ -1,21 +1,35 @@ +/* + * Copyright 2021, Lawnchair + * + * 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 app.lawnchair.ui.preferences.components import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.RadioButton import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.lawnchair.preferences.PreferenceAdapter import app.lawnchair.ui.AlertBottomSheetContent -import kotlinx.coroutines.launch +import app.lawnchair.ui.util.bottomSheetHandler @ExperimentalMaterialApi @Composable @@ -26,9 +40,7 @@ fun ListPreference( enabled: Boolean = true, showDivider: Boolean = false ) { - val sheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val scope = rememberCoroutineScope() - + val bottomSheetHandler = bottomSheetHandler val currentValue = adapter.state.value val currentLabel = entries .firstOrNull { it.value == currentValue } @@ -38,43 +50,40 @@ fun ListPreference( title = { Text(text = label) }, description = { currentLabel?.let { Text(text = it) } }, enabled = enabled, - modifier = Modifier - .clickable(enabled) { scope.launch { sheetState.show() } }, - showDivider = showDivider - ) - - BottomSheet(sheetState = sheetState) { - AlertBottomSheetContent( - title = { Text(label) }, - buttons = { - OutlinedButton( - onClick = { scope.launch { sheetState.hide() } } + showDivider = showDivider, + modifier = Modifier.clickable(enabled) { + bottomSheetHandler.show { + AlertBottomSheetContent( + title = { Text(label) }, + buttons = { + OutlinedButton(onClick = { bottomSheetHandler.hide() }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + } ) { - Text(text = stringResource(id = android.R.string.cancel)) - } - } - ) { - LazyColumn { - itemsIndexed(entries) { index, item -> - PreferenceTemplate( - title = { Text(item.label()) }, - modifier = Modifier.clickable { - adapter.onChange(item.value) - scope.launch { sheetState.hide() } - }, - startWidget = { - RadioButton( - selected = item.value == currentValue, - onClick = null + LazyColumn { + itemsIndexed(entries) { index, item -> + PreferenceTemplate( + title = { Text(item.label()) }, + modifier = Modifier.clickable { + adapter.onChange(item.value) + bottomSheetHandler.hide() + }, + startWidget = { + RadioButton( + selected = item.value == currentValue, + onClick = null + ) + }, + showDivider = index > 0, + dividerIndent = 40.dp ) - }, - showDivider = index > 0, - dividerIndent = 40.dp - ) + } + } } } } - } + ) } class ListPreferenceEntry( diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/NotificationDotsPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/NotificationDotsPreference.kt index 4402f05269..f0e9635df8 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/NotificationDotsPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/NotificationDotsPreference.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import app.lawnchair.ui.AlertBottomSheetContent +import app.lawnchair.ui.util.bottomSheetHandler import app.lawnchair.util.lifecycleState import com.android.launcher3.R import com.android.launcher3.notification.NotificationListener @@ -49,13 +50,10 @@ import kotlinx.coroutines.launch @Composable @ExperimentalMaterialApi fun NotificationDotsPreference() { + val bottomSheetHandler = bottomSheetHandler val context = LocalContext.current - val sheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val coroutineScope = rememberCoroutineScope() - val enabled = notificationDotsEnabled() val serviceEnabled = notificationServiceEnabled() - val showWarning = enabled && !serviceEnabled val summary = when { showWarning -> R.string.missing_notification_access_description @@ -63,15 +61,6 @@ fun NotificationDotsPreference() { else -> R.string.notification_dots_desc_off } - NotificationAccessConfirmation( - sheetState = sheetState, - onDismissRequest = { - coroutineScope.launch { - sheetState.hide() - } - } - ) - PreferenceTemplate( title = { Text(text = stringResource(id = R.string.notification_dots)) }, description = { Text(text = stringResource(id = summary)) }, @@ -88,8 +77,10 @@ fun NotificationDotsPreference() { modifier = Modifier .clickable { if (showWarning) { - coroutineScope.launch { - sheetState.show() + bottomSheetHandler.show { + NotificationAccessConfirmation { + bottomSheetHandler.hide() + } } } else { val extras = Bundle().apply { @@ -105,49 +96,44 @@ fun NotificationDotsPreference() { @Composable @ExperimentalMaterialApi -fun NotificationAccessConfirmation( - sheetState: BottomSheetState, - onDismissRequest: () -> Unit -) { +fun NotificationAccessConfirmation(onDismissRequest: () -> Unit) { val context = LocalContext.current - BottomSheet(sheetState = sheetState) { - AlertBottomSheetContent( - title = { Text(text = stringResource(id = R.string.missing_notification_access_label)) }, - text = { - val appName = stringResource(id = R.string.derived_app_name) - Text(text = stringResource(id = R.string.msg_missing_notification_access, appName)) - }, - buttons = { - OutlinedButton( - onClick = onDismissRequest - ) { - Text(text = stringResource(id = android.R.string.cancel)) - } - Spacer(modifier = Modifier.requiredWidth(8.dp)) - Button( - onClick = { - onDismissRequest() - - val cn = ComponentName(context, NotificationListener::class.java) - val showFragmentArgs = Bundle() - showFragmentArgs.putString( - EXTRA_FRAGMENT_ARG_KEY, - cn.flattenToString() - ) - - val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()) - .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, showFragmentArgs) - context.startActivity(intent) - } - ) { - Text(text = stringResource(id = R.string.title_change_settings)) - } + AlertBottomSheetContent( + title = { Text(text = stringResource(id = R.string.missing_notification_access_label)) }, + text = { + val appName = stringResource(id = R.string.derived_app_name) + Text(text = stringResource(id = R.string.msg_missing_notification_access, appName)) + }, + buttons = { + OutlinedButton( + onClick = onDismissRequest + ) { + Text(text = stringResource(id = android.R.string.cancel)) } - ) - } + Spacer(modifier = Modifier.requiredWidth(8.dp)) + Button( + onClick = { + onDismissRequest() + + val cn = ComponentName(context, NotificationListener::class.java) + val showFragmentArgs = Bundle() + showFragmentArgs.putString( + EXTRA_FRAGMENT_ARG_KEY, + cn.flattenToString() + ) + + val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()) + .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, showFragmentArgs) + context.startActivity(intent) + } + ) { + Text(text = stringResource(id = R.string.title_change_settings)) + } + } + ) } @Composable diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt index 36a6571220..9d86f0765b 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt @@ -1,12 +1,25 @@ +/* + * Copyright 2021, Lawnchair + * + * 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 app.lawnchair.ui.preferences.components.colorpreference import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.RadioButton import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme @@ -21,11 +34,10 @@ import androidx.compose.ui.unit.dp import app.lawnchair.preferences.PreferenceAdapter import app.lawnchair.theme.color.ColorOption import app.lawnchair.ui.AlertBottomSheetContent -import app.lawnchair.ui.preferences.components.BottomSheet import app.lawnchair.ui.preferences.components.Chip import app.lawnchair.ui.preferences.components.PreferenceTemplate -import app.lawnchair.ui.preferences.components.rememberBottomSheetState import app.lawnchair.ui.theme.lightenColor +import app.lawnchair.ui.util.bottomSheetHandler import com.android.launcher3.R import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager @@ -44,89 +56,84 @@ fun ColorPreference( val selectedEntry = dynamicEntries.firstOrNull { it.value == selectedColor } ?: staticEntries.firstOrNull { it.value == selectedColor } val defaultTabIndex = if (dynamicEntries.any { it.value == selectedColor }) 0 else 1 - val bottomSheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val coroutineScope = rememberCoroutineScope() val description = selectedEntry?.label?.invoke() + val bottomSheetHandler = bottomSheetHandler PreferenceTemplate( title = { Text(text = label) }, endWidget = { ColorDot(color = MaterialTheme.colorScheme.primary) }, - modifier = Modifier.clickable { - coroutineScope.launch { - bottomSheetState.show() - } - }, description = { if (description != null) { Text(text = description) } - } - ) - - BottomSheet(sheetState = bottomSheetState) { - val pagerState = rememberPagerState(defaultTabIndex) - val scope = rememberCoroutineScope() - val scrollToPage = { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } } - AlertBottomSheetContent( - title = { Text(text = label) }, - buttons = { - Button( - onClick = { coroutineScope.launch { bottomSheetState.hide() } } - ) { - Text(text = stringResource(id = R.string.done)) - } - } - ) { - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - modifier = Modifier.padding(horizontal = 16.dp) - ) { - Chip( - label = stringResource(id = R.string.dynamic), - onClick = { scrollToPage(0) }, - currentOffset = pagerState.currentPage + pagerState.currentPageOffset, - page = 0 - ) - Chip( - label = stringResource(id = R.string.presets), - onClick = { scrollToPage(1) }, - currentOffset = pagerState.currentPage + pagerState.currentPageOffset, - page = 1 - ) - } - HorizontalPager( - count = 2, - modifier = Modifier - .pagerHeight( - dynamicCount = dynamicEntries.size, - staticCount = staticEntries.size - ), - state = pagerState, - verticalAlignment = Alignment.Top - ) { page -> - when (page) { - 0 -> { - PresetsList(dynamicEntries, adapter) + }, + modifier = Modifier.clickable { + bottomSheetHandler.show { + val pagerState = rememberPagerState(defaultTabIndex) + val scope = rememberCoroutineScope() + val scrollToPage = { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } } + AlertBottomSheetContent( + title = { Text(text = label) }, + buttons = { + Button( + onClick = { bottomSheetHandler.hide() } + ) { + Text(text = stringResource(id = R.string.done)) } - 1 -> { - SwatchGrid( - entries = staticEntries, - modifier = Modifier.padding( - start = 16.dp, - top = 20.dp, - end = 16.dp, - bottom = 16.dp - ), - onSwatchClick = { selectedColor = it }, - isSwatchSelected = { it == selectedColor } + } + ) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Chip( + label = stringResource(id = R.string.dynamic), + onClick = { scrollToPage(0) }, + currentOffset = pagerState.currentPage + pagerState.currentPageOffset, + page = 0 ) + Chip( + label = stringResource(id = R.string.presets), + onClick = { scrollToPage(1) }, + currentOffset = pagerState.currentPage + pagerState.currentPageOffset, + page = 1 + ) + } + HorizontalPager( + count = 2, + modifier = Modifier + .pagerHeight( + dynamicCount = dynamicEntries.size, + staticCount = staticEntries.size + ), + state = pagerState, + verticalAlignment = Alignment.Top + ) { page -> + when (page) { + 0 -> { + PresetsList(dynamicEntries, adapter) + } + 1 -> { + SwatchGrid( + entries = staticEntries, + modifier = Modifier.padding( + start = 16.dp, + top = 20.dp, + end = 16.dp, + bottom = 16.dp + ), + onSwatchClick = { selectedColor = it }, + isSwatchSelected = { it == selectedColor } + ) + } + } } } } } } - } + ) } @Composable diff --git a/lawnchair/src/app/lawnchair/ui/util/BottomSheetContent.kt b/lawnchair/src/app/lawnchair/ui/util/BottomSheetContent.kt new file mode 100644 index 0000000000..120863041a --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/util/BottomSheetContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2021, Lawnchair + * + * 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 app.lawnchair.ui.util + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +val emptyBottomSheetContent = BottomSheetContent { + Spacer(modifier = Modifier.height(1.dp)) +} + +data class BottomSheetContent( + val content: @Composable () -> Unit +) diff --git a/lawnchair/src/app/lawnchair/ui/util/ProvideBottomSheetHandler.kt b/lawnchair/src/app/lawnchair/ui/util/ProvideBottomSheetHandler.kt new file mode 100644 index 0000000000..c96a3963c5 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/util/ProvideBottomSheetHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2021, Lawnchair + * + * 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 app.lawnchair.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf + +internal val LocalBottomSheetHandler = staticCompositionLocalOf { BottomSheetHandler() } + +val bottomSheetHandler: BottomSheetHandler + @Composable + @ReadOnlyComposable + get() = LocalBottomSheetHandler.current + +@Composable +fun ProvideBottomSheetHandler( + handler: BottomSheetHandler, + content: @Composable () -> Unit +) { + CompositionLocalProvider(LocalBottomSheetHandler provides handler) { + content() + } +} + +data class BottomSheetHandler( + val show: (@Composable () -> Unit) -> Unit = {}, + val hide: () -> Unit = {} +) diff --git a/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt b/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt deleted file mode 100644 index 05745c73f2..0000000000 --- a/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt +++ /dev/null @@ -1,221 +0,0 @@ -package app.lawnchair.views - -import android.content.Context -import android.graphics.Rect -import android.util.FloatProperty -import android.view.MotionEvent -import android.view.View -import android.view.animation.Interpolator -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.LocalContentColor -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material3.LocalContentColor as M3LocalContentColor -import androidx.compose.material3.MaterialTheme as Material3Theme -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp -import app.lawnchair.LawnchairLauncher -import app.lawnchair.launcher -import app.lawnchair.theme.color.ColorTokens -import app.lawnchair.theme.color.colorToken -import app.lawnchair.ui.preferences.components.BottomSheet -import app.lawnchair.ui.preferences.components.BottomSheetState -import app.lawnchair.ui.preferences.components.rememberBottomSheetState -import app.lawnchair.ui.theme.LawnchairTheme -import app.lawnchair.ui.util.portal.PortalNode -import app.lawnchair.ui.util.portal.PortalNodeView -import app.lawnchair.util.ProvideLifecycleState -import app.lawnchair.util.minus -import com.android.launcher3.AbstractFloatingView -import com.android.launcher3.Insettable -import com.android.launcher3.anim.PendingAnimation -import com.android.launcher3.util.SystemUiController -import com.google.accompanist.insets.LocalWindowInsets -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.rememberInsetsPaddingValues -import kotlinx.coroutines.launch - -typealias CloseHandler = (animate: Boolean) -> Unit - -class ComposeFloatingView(context: Context) : - AbstractFloatingView(context, null), PortalNode, Insettable { - - private val launcher = context.launcher - private val container = object : PortalNodeView(context) { - override fun removeView(view: View?) { - super.removeView(view) - if (childCount == 1) { - removeFromDragLayer() - } - } - } - private var _hintCloseProgress = mutableStateOf(0f) - val hintCloseProgress get() = _hintCloseProgress.value - var hintCloseDistance = 0f - private set - var closeHandler: CloseHandler? = null - - init { - mIsOpen = true - addView(container) - } - - override fun handleClose(animate: Boolean) { - launcher.hideKeyboard() - val handler = closeHandler ?: throw IllegalStateException("Close handler is null") - handler(animate) - } - - fun setSystemUiFlags(flags: Int) { - launcher.systemUiController.updateUiState(SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, flags) - } - - fun removeFromDragLayer() { - launcher.dragLayer.removeView(this) - launcher.systemUiController.updateUiState(SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0) - } - - override fun setInsets(insets: Rect) { - - } - - override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { - return false - } - - override fun onBackPressed(): Boolean { - return false - } - - override fun isOfType(type: Int): Boolean { - return type and TYPE_COMPOSE_VIEW != 0 - } - - override fun addHintCloseAnim( - distanceToMove: Float, - interpolator: Interpolator, - target: PendingAnimation - ) { - super.addHintCloseAnim(distanceToMove, interpolator, target) - hintCloseDistance = distanceToMove - target.setFloat(this, HINT_CLOSE_PROGRESS, 1f, interpolator) - } - - companion object { - - private val HINT_CLOSE_PROGRESS = object : FloatProperty("hintCloseDistance") { - - override fun setValue(floatingView: ComposeFloatingView, value: Float) { - floatingView._hintCloseProgress.value = value - } - - override fun get(floatingView: ComposeFloatingView) = floatingView._hintCloseProgress.value - } - - fun show(launcher: LawnchairLauncher, content: @Composable ComposeFloatingView.() -> Unit) { - val view = ComposeFloatingView(launcher) - view.container.setContent { - LawnchairTheme { - ProvideWindowInsets { - ProvideLifecycleState { - CompositionLocalProvider( - LocalContentColor provides MaterialTheme.colors.onSurface, - M3LocalContentColor provides Material3Theme.colorScheme.onSurface - ) { - content(view) - } - } - } - } - } - launcher.dragLayer.addView(view) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -fun LawnchairLauncher.showBottomSheet( - contentPaddings: PaddingValues = PaddingValues(all = 0.dp), - content: @Composable (state: BottomSheetState) -> Unit -) { - ComposeFloatingView.show(this) { - val state = rememberBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - confirmStateChange = { - if (it == ModalBottomSheetValue.Hidden) hideKeyboard() - true - } - ) - val scope = rememberCoroutineScope() - - closeHandler = { animate -> - scope.launch { - if (animate) { - state.hide() - } else { - state.snapTo(ModalBottomSheetValue.Hidden) - } - } - } - - LaunchedEffect("") { - state.show() - } - - val windowInsets = LocalWindowInsets.current - val imePaddings = rememberInsetsPaddingValues( - insets = windowInsets.ime, - applyStart = true, applyEnd = true, applyBottom = true - ) - - SystemUi(setStatusBar = false) - BottomSheet( - modifier = Modifier - .padding(imePaddings - contentPaddings), - sheetState = state, - sheetContent = { - Box( - modifier = Modifier - .padding(contentPaddings) - .graphicsLayer( - alpha = 1f - (hintCloseProgress * 0.5f), - translationY = hintCloseProgress * -hintCloseDistance - ) - ) { - content(state) - } - }, - scrimColor = colorToken(ColorTokens.WidgetsPickerScrim), - sheetShape = LauncherSheetShape, - sheetBackgroundColor = colorToken(ColorTokens.Surface) - ) - } -} - -private val LauncherSheetShape = RoundedCornerShape(24.dp) - -@Composable -fun ComposeFloatingView.SystemUi(setStatusBar: Boolean = true, setNavBar: Boolean = true) { - val useDarkIcons = MaterialTheme.colors.isLight - - SideEffect { - var flags = 0 - if (setStatusBar) { - flags = flags or ( - if (useDarkIcons) SystemUiController.FLAG_LIGHT_STATUS - else SystemUiController.FLAG_DARK_STATUS) - } - if (setNavBar) { - flags = flags or ( - if (useDarkIcons) SystemUiController.FLAG_LIGHT_NAV - else SystemUiController.FLAG_DARK_NAV) - } - setSystemUiFlags(flags) - } -}