diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherActivity.kt index 45a332cd..0e4a6f36 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherActivity.kt @@ -14,9 +14,11 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle @@ -111,7 +113,6 @@ class LauncherActivity : BaseActivity() { PullDownScaffold( modifier = Modifier .fillMaxSize() - .systemBarsPadding() .graphicsLayer { scaleX = 0.5f + enterTransition.value * 0.5f scaleY = 0.5f + enterTransition.value * 0.5f @@ -125,7 +126,6 @@ class LauncherActivity : BaseActivity() { PagerScaffold( modifier = Modifier .fillMaxSize() - .systemBarsPadding() .graphicsLayer { scaleX = enterTransition.value scaleY = enterTransition.value diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt index 7da00a5c..96d4281f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt @@ -22,21 +22,17 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.systemuicontroller.rememberSystemUiController import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.toPixels @@ -61,7 +57,6 @@ fun PagerScaffold( ) { val viewModel: LauncherScaffoldVM = viewModel() val searchVM: SearchVM = viewModel() - val context = LocalContext.current val isSearchOpen by viewModel.isSearchOpen.observeAsState(false) val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false) @@ -70,6 +65,25 @@ fun PagerScaffold( val searchScrollState = rememberScrollState() val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets) + val showStatusBarScrim by remember { + derivedStateOf { + if (isSearchOpen) { + searchScrollState.value < searchScrollState.maxValue + } else { + widgetsScrollState.value > 0 + } + } + } + val showNavBarScrim by remember { + derivedStateOf { + if (isSearchOpen) { + searchScrollState.value > 0 + } else { + widgetsScrollState.value > 0 && widgetsScrollState.value < widgetsScrollState.maxValue + } + } + } + val isWidgetsScrollZero by remember { derivedStateOf { widgetsScrollState.value == 0 @@ -79,11 +93,15 @@ fun PagerScaffold( val systemUiController = rememberSystemUiController() val colorSurface = MaterialTheme.colorScheme.surface - LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface) { + LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) { if (isWidgetEditMode) { systemUiController.setStatusBarColor( colorSurface ) + } else if (showStatusBarScrim) { + systemUiController.setStatusBarColor( + colorSurface.copy(0.75f), + ) } else { systemUiController.setStatusBarColor( Color.Transparent, @@ -92,12 +110,18 @@ fun PagerScaffold( } } - LaunchedEffect(darkNavBarIcons) { - systemUiController.setNavigationBarColor( - Color.Transparent, - darkIcons = darkNavBarIcons, - navigationBarContrastEnforced = false - ) + LaunchedEffect(darkNavBarIcons, showNavBarScrim) { + if (showNavBarScrim) { + systemUiController.setNavigationBarColor( + colorSurface.copy(0.75f), + ) + } else { + systemUiController.setNavigationBarColor( + Color.Transparent, + darkIcons = darkNavBarIcons, + navigationBarContrastEnforced = false + ) + } } val blurWallpaper by remember { @@ -138,11 +162,11 @@ fun PagerScaffold( } } - val notificationDragThreshold = with(LocalDensity.current) {200.dp.toPx()} + val notificationDragThreshold = with(LocalDensity.current) { 200.dp.toPx() } val notificationShadeController = rememberNotificationShadeController() val nestedScrollConnection = remember { - object: NestedScrollConnection { + object : NestedScrollConnection { private var pullDownTotalY: Float? = 0f override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val diff = widgetsScrollState.value - available.y @@ -171,6 +195,8 @@ fun PagerScaffold( } } + val insets = WindowInsets.systemBars.asPaddingValues() + Box( modifier = modifier ) { @@ -218,12 +244,12 @@ fun PagerScaffold( val editModePadding by animateDpAsState(if (isWidgetEditMode) 56.dp else 0.dp) val clockPadding by animateDpAsState( - if (isWidgetsScrollZero) 64.dp else 0.dp + if (isWidgetsScrollZero) 64.dp + insets.calculateBottomPadding() else 0.dp ) val clockHeight by remember { derivedStateOf { - height - (64.dp - clockPadding) + height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding) } } @@ -231,10 +257,10 @@ fun PagerScaffold( modifier = Modifier .requiredWidth(width) .fillMaxHeight() - .padding(horizontal = 8.dp) - .clip(MaterialTheme.shapes.medium) .nestedScroll(nestedScrollConnection) .verticalScroll(widgetsScrollState) + .systemBarsPadding() + .padding(horizontal = 8.dp) .padding(top = 8.dp, bottom = 64.dp) .padding(top = editModePadding), clockHeight = { clockHeight }, @@ -254,10 +280,10 @@ fun PagerScaffold( modifier = Modifier .requiredWidth(width) .fillMaxHeight() - .padding(horizontal = 8.dp) - .clip(MaterialTheme.shapes.medium) .verticalScroll(searchScrollState, reverseScrolling = true) .imePadding() + .systemBarsPadding() + .padding(horizontal = 8.dp) .padding(top = 8.dp, bottom = 64.dp) .padding(bottom = webSearchPadding), reverse = true, @@ -302,6 +328,7 @@ fun PagerScaffold( modifier = Modifier .align(Alignment.BottomCenter) .padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + .systemBarsPadding() .imePadding() .offset(y = widgetEditModeOffset), level = { searchBarLevel }, focused = focusSearchBar, onFocusChange = { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt index 46987961..05c30a28 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt @@ -1,6 +1,5 @@ package de.mm20.launcher2.ui.launcher -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState @@ -19,7 +18,6 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin @@ -43,6 +41,7 @@ import de.mm20.launcher2.ui.launcher.search.SearchBarLevel import de.mm20.launcher2.ui.launcher.search.SearchColumn import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn +import de.mm20.launcher2.ui.modifier.verticalFadingEdges import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -55,7 +54,6 @@ fun PullDownScaffold( ) { val viewModel: LauncherScaffoldVM = viewModel() val searchVM: SearchVM = viewModel() - val context = LocalContext.current val density = LocalDensity.current @@ -67,12 +65,41 @@ fun PullDownScaffold( val systemUiController = rememberSystemUiController() + val isWidgetsScrollZero by remember { + derivedStateOf { + widgetsScrollState.value == 0 + } + } + + val showStatusBarScrim by remember { + derivedStateOf { + if (isSearchOpen) { + searchScrollState.value > 0 + } else { + widgetsScrollState.value > 0 + } + } + } + val showNavBarScrim by remember { + derivedStateOf { + if (isSearchOpen) { + searchScrollState.value < searchScrollState.maxValue + } else { + widgetsScrollState.value > 0 && widgetsScrollState.value < widgetsScrollState.maxValue + } + } + } + val colorSurface = MaterialTheme.colorScheme.surface - LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface) { + LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) { if (isWidgetEditMode) { systemUiController.setStatusBarColor( colorSurface ) + } else if (showStatusBarScrim) { + systemUiController.setStatusBarColor( + colorSurface.copy(0.75f), + ) } else { systemUiController.setStatusBarColor( Color.Transparent, @@ -81,12 +108,18 @@ fun PullDownScaffold( } } - LaunchedEffect(darkNavBarIcons) { - systemUiController.setNavigationBarColor( - Color.Transparent, - darkIcons = darkNavBarIcons, - navigationBarContrastEnforced = false - ) + LaunchedEffect(darkNavBarIcons, showNavBarScrim) { + if (showNavBarScrim) { + systemUiController.setNavigationBarColor( + colorSurface.copy(0.75f), + ) + } else { + systemUiController.setNavigationBarColor( + Color.Transparent, + darkIcons = darkNavBarIcons, + navigationBarContrastEnforced = false + ) + } } val offsetY = remember { mutableStateOf(0f) } @@ -151,7 +184,7 @@ fun PullDownScaffold( consumed } isSearchOpen && (offsetY.value < 0 || source == NestedScrollSource.Drag && newValue > searchScrollState.maxValue) -> { - val consumed = available.y - (value- searchScrollState.maxValue) + val consumed = available.y - (value - searchScrollState.maxValue) offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(-maxOffset, 0f) consumed } @@ -182,11 +215,13 @@ fun PullDownScaffold( } } - + val insets = WindowInsets.systemBars.asPaddingValues() Box( modifier = modifier - .padding(horizontal = 8.dp) - .clip(MaterialTheme.shapes.medium) + .verticalFadingEdges( + top = insets.calculateTopPadding(), + amount = 0.85f + ) .nestedScroll(nestedScrollConnection) .offset { IntOffset(0, offsetY.value.toInt()) }, contentAlignment = Alignment.TopCenter @@ -230,12 +265,21 @@ fun PullDownScaffold( .fillMaxWidth() .requiredHeight(height) .verticalScroll(searchScrollState) - .padding(vertical = 8.dp) + .systemBarsPadding() + .padding(8.dp) .padding(top = 56.dp) .padding(top = webSearchPadding) .imePadding() ) val editModePadding by animateDpAsState(if (isWidgetEditMode) 56.dp else 0.dp) + val clockPadding by animateDpAsState( + if (isWidgetsScrollZero) insets.calculateBottomPadding() else 0.dp + ) + val clockHeight by remember { + derivedStateOf { + height - (insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding) + } + } WidgetColumn( modifier = Modifier @@ -248,9 +292,11 @@ fun PullDownScaffold( .fillMaxWidth() .requiredHeight(height) .verticalScroll(widgetsScrollState) - .padding(vertical = 8.dp) + .systemBarsPadding() + .padding(8.dp) .padding(top = editModePadding), - clockHeight = { height }, + clockHeight = { clockHeight }, + clockBottomPadding = { clockPadding }, editMode = isWidgetEditMode, onEditModeChange = { viewModel.setWidgetEditMode(it) @@ -268,6 +314,8 @@ fun PullDownScaffold( exit = slideOut { IntOffset(0, -it.height) } ) { CenterAlignedTopAppBar( + modifier = Modifier + .systemBarsPadding(), title = { Text(stringResource(R.string.menu_edit_widgets)) }, @@ -285,7 +333,7 @@ fun PullDownScaffold( offsetY.value != 0f -> SearchBarLevel.Raised isSearchOpen && searchScrollState.value == 0 -> SearchBarLevel.Active isSearchOpen && searchScrollState.value > 0 -> SearchBarLevel.Raised - widgetsScrollState.value > 0 -> SearchBarLevel.Raised + !isWidgetsScrollZero -> SearchBarLevel.Raised else -> SearchBarLevel.Resting } } @@ -300,7 +348,8 @@ fun PullDownScaffold( modifier = Modifier .fillMaxWidth() .wrapContentHeight() - .padding(vertical = 8.dp) + .systemBarsPadding() + .padding(8.dp) .offset { IntOffset(0, searchBarOffset.value.toInt()) } .offset { IntOffset( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/modifier/FadingEdges.kt b/ui/src/main/java/de/mm20/launcher2/ui/modifier/FadingEdges.kt new file mode 100644 index 00000000..1198eaa7 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/modifier/FadingEdges.kt @@ -0,0 +1,89 @@ +package de.mm20.launcher2.ui.modifier + +import androidx.annotation.FloatRange +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.pow + +fun Modifier.verticalFadingEdges( + enabled: Boolean = true, + top: Dp = 0.dp, + bottom: Dp = 0.dp, + /** + * How strong the fading effect should be. + * If 1, edges will be completely transparent. + * If 0, the modifier will have no effect at all. + */ + @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true) amount: Float = 1f +): Modifier { + if(!enabled) return this + if (top == 0.dp && bottom == 0.dp) return this + + return this then drawWithContent { + + val topColors = if (top > 0.dp) createColors( + 1f - amount, + top.roundToPx() + 1, + ) else emptyList() + val bottomColors = if (bottom > 0.dp) createColors( + 1f - amount, + bottom.roundToPx() + 1, + reverse = true + ) else emptyList() + + val topSteps = if (top > 0.dp) createColorSteps( + size.height, + top.toPx() * 1.3f, + top.roundToPx() + 1 + ) else emptyList() + val bottomSteps = if (bottom > 0.dp) createColorSteps( + size.height, + bottom.toPx() * 1.3f, + bottom.roundToPx() + 1, + reverse = true + ) else emptyList() + + val paint = Paint().apply { + blendMode = BlendMode.DstIn + shader = LinearGradientShader( + Offset.Zero, + Offset(0f, size.height), + colors = topColors + bottomColors, + colorStops = topSteps + bottomSteps + ) + } + drawContent() + drawIntoCanvas { + it.drawRect( + Rect(0f, 0f, size.width, size.height), + paint + ) + } + } +} + +private fun createColors(alpha: Float, steps: Int, reverse: Boolean = false): List { + val interval = 1f / (steps - 1) + return (0 until steps).map { + val x = interval * if (reverse) (steps - 1 - it) else it + val y = (1 - alpha) * (1f - (x - 1f).pow(2)) + alpha + Color.Black.copy(alpha = y) + } +} + +private fun createColorSteps(height: Float, size: Float, steps: Int, reverse: Boolean = false): List { + val interval = 1f / (steps - 1) + return (0 until steps).map { + val x = interval * if (reverse) (steps - 1 - it) else it + if (reverse) 1 - (x * size / height) else (x * size / height) + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/modifier/Scrims.kt b/ui/src/main/java/de/mm20/launcher2/ui/modifier/Scrims.kt new file mode 100644 index 00000000..bdffbec0 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/modifier/Scrims.kt @@ -0,0 +1,93 @@ +package de.mm20.launcher2.ui.modifier + +import androidx.annotation.FloatRange +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.pow + +fun Modifier.verticalScrims( + enabled: Boolean = true, + top: Dp = 0.dp, + bottom: Dp = 0.dp, + /** + * How strong the fading effect should be. + * If 1, edges will be completely transparent. + * If 0, the modifier will have no effect at all. + */ + @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true) amount: Float = 1f +): Modifier { + if (!enabled) return this + if (top == 0.dp && bottom == 0.dp) return this + + return this then drawWithCache { + onDrawWithContent { + val topColors = if (top > 0.dp) createColors( + 1f - amount, + top.roundToPx() + 1, + ) else emptyList() + val bottomColors = if (bottom > 0.dp) createColors( + 1f - amount, + bottom.roundToPx() + 1, + reverse = true + ) else emptyList() + + val topSteps = if (top > 0.dp) createColorSteps( + size.height, + top.toPx() * 1.3f, + top.roundToPx() + 1 + ) else emptyList() + val bottomSteps = if (bottom > 0.dp) createColorSteps( + size.height, + bottom.toPx() * 1.3f, + bottom.roundToPx() + 1, + reverse = true + ) else emptyList() + + val paint = Paint().apply { + shader = LinearGradientShader( + Offset.Zero, + Offset(0f, size.height), + colors = topColors + bottomColors, + colorStops = topSteps + bottomSteps + ) + } + drawContent() + drawIntoCanvas { + it.drawRect( + Rect(0f, 0f, size.width, size.height), + paint + ) + } + } + } +} + +private fun createColors(alpha: Float, steps: Int, reverse: Boolean = false): List { + val interval = 1f / (steps - 1) + return (0 until steps).map { + val x = interval * if (reverse) (steps - 1 - it) else it + val y = 1f - ((1 - alpha) * (1f - (x - 1f).pow(2)) + alpha) + Color.Black.copy(alpha = y) + } +} + +private fun createColorSteps( + height: Float, + size: Float, + steps: Int, + reverse: Boolean = false +): List { + val interval = 1f / (steps - 1) + return (0 until steps).map { + val x = interval * if (reverse) (steps - 1 - it) else it + if (reverse) 1 - (x * size / height) else (x * size / height) + } +} \ No newline at end of file