Update dependencies and rework bottom sheet implementation

This commit is contained in:
Patryk Michalik
2021-12-13 18:33:48 +01:00
parent 5767b6ccd2
commit 71d8d094fc
9 changed files with 303 additions and 606 deletions
+4 -4
View File
@@ -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'
@@ -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<PreferenceViewModel
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val motionSpec = materialSharedAxisX()
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
var bottomSheetContent by remember { mutableStateOf(emptyBottomSheetContent) }
val bottomSheetHandler = BottomSheetHandler(
show = { content ->
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<PreferenceViewModel
}
@Composable
private fun Providers(content: @Composable () -> Unit) {
private fun Providers(
bottomSheetHandler: BottomSheetHandler,
content: @Composable () -> Unit
) {
ProvidePortalNode {
ProvideLifecycleState {
content()
ProvideBottomSheetHandler(handler = bottomSheetHandler) {
content()
}
}
}
}
@@ -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<Float> = 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<Float> = SwipeableDefaults.AnimationSpec,
private val confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
) {
internal var isChangingState by mutableStateOf(false)
internal var modalBottomSheetState by mutableStateOf<ModalBottomSheetState?>(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<Float> = 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<Float>,
confirmStateChange: (ModalBottomSheetValue) -> Boolean
): Saver<BottomSheetState, *> = 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)
}
}
@@ -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 <T> 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 <T> 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<T>(
@@ -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
@@ -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
@@ -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
)
@@ -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 = {}
)
@@ -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<ComposeFloatingView>("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)
}
}