Update dependencies and rework bottom sheet implementation
This commit is contained in:
+4
-4
@@ -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>(
|
||||
|
||||
+41
-55
@@ -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
|
||||
|
||||
+79
-72
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user