From e25f2db3b5ea86462d3217cfcff81283f32c18c5 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Wed, 27 May 2026 21:12:40 +0200 Subject: [PATCH] refactor launcher scaffold --- .../ui/launcher/SharedLauncherActivity.kt | 26 +- .../ui/launcher/scaffold/LauncherScaffold.kt | 608 +++++++----------- .../launcher/scaffold/animation/Offset3D.kt | 28 + .../PushScaffoldAnimationController.kt | 128 ++++ .../RubberbandScaffoldAnimationController.kt | 95 +++ .../animation/ScaffoldAnimationController.kt | 91 +++ .../ZoomInScaffoldAnimationController.kt | 56 ++ .../ClockAndWidgetsHomeComponent.kt | 3 +- .../{ => components}/ClockHomeComponent.kt | 3 +- .../{ => components}/DismissComponent.kt | 4 +- .../{ => components}/FeedComponent.kt | 3 +- .../{ => components}/LaunchComponent.kt | 3 +- .../NotificationsComponent.kt | 3 +- .../{ => components}/PowerMenuComponent.kt | 3 +- .../QuickSettingsComponent.kt | 3 +- .../{ => components}/RecentsComponent.kt | 4 +- .../{ => components}/ScaffoldComponent.kt | 3 +- .../{ => components}/ScreenOffComponent.kt | 3 +- .../{ => components}/SearchComponent.kt | 3 +- .../{ => components}/SecretComponent.kt | 3 +- .../{ => components}/WidgetsComponent.kt | 3 +- 21 files changed, 678 insertions(+), 398 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/Offset3D.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/PushScaffoldAnimationController.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/RubberbandScaffoldAnimationController.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ScaffoldAnimationController.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ZoomInScaffoldAnimationController.kt rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/ClockAndWidgetsHomeComponent.kt (98%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/ClockHomeComponent.kt (88%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/DismissComponent.kt (92%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/FeedComponent.kt (98%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/LaunchComponent.kt (94%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/NotificationsComponent.kt (97%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/PowerMenuComponent.kt (98%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/QuickSettingsComponent.kt (97%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/RecentsComponent.kt (97%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/ScaffoldComponent.kt (96%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/ScreenOffComponent.kt (96%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/SearchComponent.kt (96%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/SecretComponent.kt (95%) rename app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/{ => components}/WidgetsComponent.kt (97%) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index 99b70cb4d..31f7c854d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -47,25 +47,25 @@ import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.ProvideCompositionLocals import de.mm20.launcher2.ui.component.NavBarEffects import de.mm20.launcher2.ui.ktx.animateTo -import de.mm20.launcher2.ui.launcher.scaffold.ClockAndWidgetsHomeComponent -import de.mm20.launcher2.ui.launcher.scaffold.ClockHomeComponent -import de.mm20.launcher2.ui.launcher.scaffold.DismissComponent -import de.mm20.launcher2.ui.launcher.scaffold.FeedComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.ClockAndWidgetsHomeComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.ClockHomeComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.DismissComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.FeedComponent import de.mm20.launcher2.ui.launcher.scaffold.Gesture -import de.mm20.launcher2.ui.launcher.scaffold.LaunchComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.LaunchComponent import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffold -import de.mm20.launcher2.ui.launcher.scaffold.NotificationsComponent -import de.mm20.launcher2.ui.launcher.scaffold.PowerMenuComponent -import de.mm20.launcher2.ui.launcher.scaffold.QuickSettingsComponent -import de.mm20.launcher2.ui.launcher.scaffold.RecentsComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.NotificationsComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.PowerMenuComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.QuickSettingsComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.RecentsComponent import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldConfiguration import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldGesture -import de.mm20.launcher2.ui.launcher.scaffold.ScreenOffComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.ScreenOffComponent import de.mm20.launcher2.ui.launcher.scaffold.SearchBarPosition -import de.mm20.launcher2.ui.launcher.scaffold.SearchComponent -import de.mm20.launcher2.ui.launcher.scaffold.SecretComponent -import de.mm20.launcher2.ui.launcher.scaffold.WidgetsComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.SearchComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.SecretComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.WidgetsComponent import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt index 99d7a9caf..aa621095d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt @@ -7,6 +7,7 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateIntAsState @@ -98,6 +99,13 @@ import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.launcher.SharedLauncherActivity import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur +import de.mm20.launcher2.ui.launcher.scaffold.animation.Offset3D +import de.mm20.launcher2.ui.launcher.scaffold.animation.PushScaffoldAnimationController +import de.mm20.launcher2.ui.launcher.scaffold.animation.RubberbandScaffoldAnimationController +import de.mm20.launcher2.ui.launcher.scaffold.animation.ScaffoldAnimationController +import de.mm20.launcher2.ui.launcher.scaffold.animation.ZoomInScaffoldAnimationController +import de.mm20.launcher2.ui.launcher.scaffold.components.ScaffoldComponent +import de.mm20.launcher2.ui.launcher.scaffold.components.SearchComponent import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.filters.KeyboardFilterBar import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar @@ -112,6 +120,7 @@ import kotlinx.coroutines.launch import kotlin.math.absoluteValue import kotlin.math.pow import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds enum class ScaffoldAnimation { Rubberband, @@ -246,21 +255,55 @@ internal class LauncherScaffoldState( initialIsLocked: Boolean = false, initialIsSearchBarHidden: Boolean = false, ) { - var currentOffset by mutableStateOf( - when { - initialGesture == null || initialGesture.orientation == null || config[initialGesture]?.animation == ScaffoldAnimation.Rubberband -> Offset.Zero - initialGesture == Gesture.SwipeRight -> Offset(-size.width, 0f) - initialGesture == Gesture.SwipeLeft -> Offset(size.width, 0f) - initialGesture == Gesture.SwipeUp -> Offset(0f, -size.height) - initialGesture == Gesture.SwipeDown -> Offset(0f, size.height) - else -> Offset.Zero + private val rubberbandAnimationController = RubberbandScaffoldAnimationController( + rubberbandThreshold = rubberbandThreshold, + velocityThreshold = velocityThreshold, + ) + private val pushAnimationController = PushScaffoldAnimationController( + velocityThreshold = velocityThreshold, + ) + + private fun getAnimationController(animation: ScaffoldAnimation?): ScaffoldAnimationController? { + return when (animation) { + ScaffoldAnimation.Rubberband -> rubberbandAnimationController + ScaffoldAnimation.Push -> pushAnimationController + ScaffoldAnimation.ZoomIn -> ZoomInScaffoldAnimationController + null -> null } - ) - private set - var currentZOffset by mutableFloatStateOf( - if (initialGesture != null && initialGesture.orientation == null) 1f else 0f - ) - private set + } + + private fun getAnimationController(gesture: Gesture?): ScaffoldAnimationController? { + val animation = gesture?.let { config[it]?.animation } + return getAnimationController(animation) + } + + private val initialOffset3D: Offset3D = initialGesture?.let { gesture -> + getAnimationController(config[gesture]?.animation)?.initialOffset(gesture, size) + } ?: Offset3D.Zero + + private val currentPage: ScaffoldPage + get() = if (isSettledOnSecondaryPage) ScaffoldPage.Secondary else ScaffoldPage.Home + + private var currentOffset3D by mutableStateOf(initialOffset3D) + + private fun isOverThreshold(animation: ScaffoldAnimation, offset: Offset3D): Boolean { + return when (animation) { + ScaffoldAnimation.Rubberband -> { + offset.x.absoluteValue > rubberbandThreshold || offset.y.absoluteValue > rubberbandThreshold + } + + ScaffoldAnimation.Push -> { + offset.x.absoluteValue > size.width * 0.5f || offset.y.absoluteValue > size.height * 0.5f + } + + ScaffoldAnimation.ZoomIn -> false + } + } + + val currentOffset: Offset + get() = currentOffset3D.toOffset2D() + val currentZOffset: Float + get() = currentOffset3D.z var currentGesture by mutableStateOf(initialGesture) private set @@ -390,27 +433,13 @@ internal class LauncherScaffoldState( */ val currentProgress by derivedStateOf { val dir = currentGesture ?: return@derivedStateOf 0f - val gesture = config[dir] ?: return@derivedStateOf 0f - - if (dir.orientation == null) { - return@derivedStateOf currentZOffset - } - - if (gesture.animation == ScaffoldAnimation.Rubberband) { - val offset = - (currentOffset.x + currentOffset.y).absoluteValue.coerceAtMost(rubberbandThreshold) - if (isSettledOnSecondaryPage) { - 1f - offset / (rubberbandThreshold * 2f) - } else { - offset / (rubberbandThreshold * 2f) - } - } else { - if (dir.orientation == Orientation.Horizontal) { - (currentOffset.x.absoluteValue / size.width).coerceIn(0f, 1f) - } else { - (currentOffset.y.absoluteValue / size.height).coerceIn(0f, 1f) - } - } + val controller = getAnimationController(dir) ?: return@derivedStateOf 0f + controller.calculateProgress( + direction = dir, + offset = currentOffset3D, + currentPage = currentPage, + size = size, + ) } val currentAnimation by derivedStateOf { @@ -424,10 +453,22 @@ internal class LauncherScaffoldState( } private val offsetAnimatable = - Animatable(Offset.Zero, Offset.VectorConverter) + Animatable(Offset3D.Zero, Offset3D.VectorConverter) - private val zAnimatable = - Animatable(0f, Float.VectorConverter) + private suspend fun animateOffset3DTo( + target: Offset3D, + initialVelocity: Offset3D = Offset3D.Zero, + animationSpec: AnimationSpec = spring(), + ) { + offsetAnimatable.snapTo(currentOffset3D) + offsetAnimatable.animateTo( + target, + animationSpec = animationSpec, + initialVelocity = initialVelocity, + ) { + currentOffset3D = this.value + } + } private val searchBarAnimatable = Animatable(0f, Float.VectorConverter) @@ -439,7 +480,6 @@ internal class LauncherScaffoldState( if (isLocked || currentZOffset > 0f) return isDragged = true offsetAnimatable.stop() - zAnimatable.stop() } @@ -452,97 +492,21 @@ internal class LauncherScaffoldState( val direction = currentGesture ?: return val gesture = config[direction] ?: return + val controller = getAnimationController(gesture.animation) ?: return + val wasOverThreshold = isOverThreshold(gesture.animation, currentOffset3D) - if (gesture.animation == ScaffoldAnimation.Rubberband) { - performRubberbandDrag(direction, currentOffset, offset) - } else if (gesture.animation == ScaffoldAnimation.Push) { - performPushDrag(direction, currentOffset, offset) - } - } - - private fun performRubberbandDrag(direction: Gesture, offset: Offset, delta: Offset) { - - val delta = when { - !isAtTop && !isAtBottom -> delta.copy(y = 0f) - !isAtTop && isAtBottom -> delta.copy(y = delta.y.coerceAtMost(-offset.y)) - isAtTop -> delta.copy(y = delta.y.coerceAtLeast(-offset.y)) - else -> delta - } - val wasOverThreshold = currentOffset.x.absoluteValue > rubberbandThreshold || - currentOffset.y.absoluteValue > rubberbandThreshold - - val threshold = rubberbandThreshold * 1.5f - currentOffset = when (direction) { - Gesture.SwipeUp -> Offset( - 0f, - (offset.y + delta.y).coerceIn(-threshold, threshold) - ) - - Gesture.SwipeDown -> Offset( - 0f, - (offset.y + delta.y).coerceIn(-threshold, threshold) - ) - - Gesture.SwipeLeft -> Offset( - (offset.x + delta.x).coerceIn(-threshold, threshold), - 0f - ) - - Gesture.SwipeRight -> Offset( - (offset.x + delta.x).coerceIn(-threshold, threshold), - 0f - ) - - else -> Offset.Zero - } - - val isOverThreshold = currentOffset.x.absoluteValue > rubberbandThreshold || - currentOffset.y.absoluteValue > rubberbandThreshold - - if (wasOverThreshold != isOverThreshold) { - onHapticFeedback(HapticFeedbackType.GestureThresholdActivate) - } - } - - /** - * @param direction The direction of the drag (currentDragDirection) - * @param offset The total offset of the drag (currentOffset) - * @param delta The delta of the drag (offset) - */ - private fun performPushDrag(direction: Gesture, offset: Offset, delta: Offset) { - val wasOverThreshold = - currentOffset.x.absoluteValue > size.width * 0.5f || - currentOffset.y.absoluteValue > size.height * 0.5f - - currentOffset = when (direction) { - Gesture.SwipeUp -> Offset( - 0f, - (offset.y + delta.y).coerceIn(-size.height, 0f) - ) - - Gesture.SwipeDown -> Offset( - 0f, - (offset.y + delta.y).coerceIn(0f, size.height) - ) - - Gesture.SwipeLeft -> Offset( - (offset.x + delta.x).coerceIn(-size.width, 0f), - 0f - ) - - Gesture.SwipeRight -> Offset( - (offset.x + delta.x).coerceIn(0f, size.width), - 0f - ) - - else -> Offset.Zero - - } - - val isOverThreshold = - currentOffset.x.absoluteValue > size.width * 0.5f || - currentOffset.y.absoluteValue > size.height * 0.5f + val targetOffset = controller.calculateOffset( + direction = direction, + currentOffset = currentOffset3D, + delta = Offset3D.from2D(offset), + currentPage = currentPage, + size = size, + isAtTop = isAtTop, + isAtBottom = isAtBottom, + ) + currentOffset3D = targetOffset + val isOverThreshold = isOverThreshold(gesture.animation, currentOffset3D) if (wasOverThreshold != isOverThreshold) { onHapticFeedback(HapticFeedbackType.GestureThresholdActivate) } @@ -609,146 +573,70 @@ internal class LauncherScaffoldState( else -> velocity } if (currentGesture == null) { - currentOffset = Offset.Zero + currentOffset3D = Offset3D.Zero } val direction = currentGesture ?: return - val offset = currentOffset val wasPageOpen = isSettledOnSecondaryPage val gesture = config[direction] ?: return - if (gesture.animation == ScaffoldAnimation.Rubberband) { - performRubberbandFling(direction, offset, velocity, disallowPageChange) - } else if (gesture.animation == ScaffoldAnimation.Push) { - performPushFling(direction, offset, velocity, disallowPageChange) + val controller = getAnimationController(gesture.animation) ?: return + val currentOffset3DValue = currentOffset3D + val wasOverThreshold = isOverThreshold(gesture.animation, currentOffset3DValue) + + val targetPage = if (disallowPageChange) { + currentPage + } else { + controller.calculateTargetPage( + direction = direction, + offset = currentOffset3DValue, + velocity = velocity, + currentPage = currentPage, + size = size, + ) + } + isSettledOnSecondaryPage = targetPage == ScaffoldPage.Secondary + + val targetOffset = controller.calculateTargetOffset( + direction = direction, + targetPage = targetPage, + size = size, + ) + + if (gesture.animation == ScaffoldAnimation.Push) { + val isOverThreshold = isOverThreshold(gesture.animation, targetOffset) + if (wasOverThreshold != isOverThreshold) { + onHapticFeedback(HapticFeedbackType.GestureThresholdActivate) + } } - if (isSettledOnSecondaryPage != wasPageOpen) { - if (wasPageOpen) { - deactivateSecondaryPage(gesture.component) - } else { - activateSecondaryPage(gesture.component) + val didPageChange = isSettledOnSecondaryPage != wasPageOpen + if (didPageChange && gesture.animation == ScaffoldAnimation.Rubberband) { + onHapticFeedback(HapticFeedbackType.SegmentFrequentTick) + } + + + val settleTarget = currentOffset3D.copy(x = targetOffset.x, y = targetOffset.y) + if (didPageChange) { + transitionToPage( + targetPage = if (isSettledOnSecondaryPage) ScaffoldPage.Secondary else ScaffoldPage.Home, + component = gesture.component, + ) { + animateOffset3DTo( + target = settleTarget, + initialVelocity = Offset3D(velocity.x, velocity.y, 0f), + ) } + } else { + animateOffset3DTo( + target = settleTarget, + initialVelocity = Offset3D(velocity.x, velocity.y, 0f), + ) } if (!isSettledOnSecondaryPage) currentGesture = null } - private suspend fun performRubberbandFling( - direction: Gesture, - offset: Offset, - velocity: Velocity, - disallowPageChange: Boolean, - ) { - val wasSettledOnSecondaryPage = isSettledOnSecondaryPage - - if (!disallowPageChange) { - if (offset.x <= -rubberbandThreshold || offset.x < 0f && velocity.x < -velocityThreshold) { - isSettledOnSecondaryPage = !isSettledOnSecondaryPage - } else if (offset.x >= rubberbandThreshold || offset.x > 0f && velocity.x > velocityThreshold) { - isSettledOnSecondaryPage = !isSettledOnSecondaryPage - } else if (offset.y <= -rubberbandThreshold || offset.y < 0f && velocity.y < -velocityThreshold) { - isSettledOnSecondaryPage = !isSettledOnSecondaryPage - } else if (offset.y >= rubberbandThreshold || offset.y > 0f && velocity.y > velocityThreshold) { - isSettledOnSecondaryPage = !isSettledOnSecondaryPage - } - } - - if (wasSettledOnSecondaryPage != isSettledOnSecondaryPage) { - onHapticFeedback(HapticFeedbackType.SegmentFrequentTick) - if (isSettledOnSecondaryPage) { - prepareSecondaryPage() - } else { - prepareHomePage() - } - } - - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo( - Offset.Zero, - initialVelocity = Offset(velocity.x, velocity.y), - ) { - currentOffset = this.value - } - } - - private suspend fun performPushFling( - direction: Gesture, - offset: Offset, - velocity: Velocity, - disallowPageChange: Boolean, - ) { - val wasOverThreshold = - currentOffset.x.absoluteValue > size.width * 0.5f || - currentOffset.y.absoluteValue > size.height * 0.5f - - val wasSettledOnSecondaryPage = isSettledOnSecondaryPage - - val lowerPage = when (direction) { - Gesture.SwipeUp -> -size.height - Gesture.SwipeDown -> 0f - Gesture.SwipeLeft -> -size.width - Gesture.SwipeRight -> 0f - else -> return - } - - val upperPage = if (direction.orientation == Orientation.Vertical) { - lowerPage + size.height - } else { - lowerPage + size.width - } - - val threshold = (upperPage + lowerPage) / 2f - - val targetOffset = if (direction.orientation == Orientation.Vertical) { - if (!disallowPageChange) { - if (offset.y > threshold && velocity.y > -velocityThreshold || velocity.y > velocityThreshold) { - Offset(0f, upperPage) - } else { - Offset(0f, lowerPage) - } - } else { - if (offset.y > threshold) Offset(0f, upperPage) else Offset(0f, lowerPage) - } - } else { - if (!disallowPageChange) { - if (offset.x > threshold && velocity.x > -velocityThreshold || velocity.x > velocityThreshold) { - Offset(upperPage, 0f) - } else { - Offset(lowerPage, 0f) - } - } else { - if (offset.x > threshold) Offset(upperPage, 0f) else Offset(lowerPage, 0f) - } - } - - isSettledOnSecondaryPage = targetOffset != Offset.Zero - - val isOverThreshold = - targetOffset.x.absoluteValue > size.width * 0.5f || - targetOffset.y.absoluteValue > size.height * 0.5f - - if (wasOverThreshold != isOverThreshold) { - onHapticFeedback(HapticFeedbackType.GestureThresholdActivate) - } - - if (wasSettledOnSecondaryPage != isSettledOnSecondaryPage) { - if (isSettledOnSecondaryPage) { - prepareSecondaryPage() - } else { - prepareHomePage() - } - } - - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo( - targetOffset, - initialVelocity = Offset(velocity.x, velocity.y), - ) { - currentOffset = this.value - } - } - suspend fun onDoubleTap() { performTapGesture(Gesture.DoubleTap) } @@ -797,7 +685,7 @@ internal class LauncherScaffoldState( when (gesture) { Gesture.TapSearchBar, Gesture.DoubleTap, Gesture.LongPress -> { - currentZOffset = progress + currentOffset3D = Offset3D(z = progress) } else -> { @@ -812,13 +700,13 @@ internal class LauncherScaffoldState( else -> 0f } - currentOffset = if (anim == ScaffoldAnimation.Push) { - Offset( + currentOffset3D = if (anim == ScaffoldAnimation.Push) { + Offset3D( x * size.width * progress, y * size.height * progress ) } else { - Offset( + Offset3D( x * rubberbandThreshold * (1f - progress), y * rubberbandThreshold * (1f - progress) ) @@ -831,14 +719,11 @@ internal class LauncherScaffoldState( private val backInterpolation = PathInterpolator(0f, 0f, 0f, 1f) fun onPredictiveBack(progress: Float) { if (!isSettledOnSecondaryPage) return - val anim = currentAnimation ?: return + val controller = getAnimationController(currentAnimation) ?: return val progress = backInterpolation.getInterpolation(progress) - val p = when(anim) { - ScaffoldAnimation.Push -> 1f - progress * 0.1f - else -> 1f - progress - } + val p = controller.calculatePredictiveBackProgress(progress) setProgress(p) } @@ -848,10 +733,10 @@ internal class LauncherScaffoldState( val anim = currentAnimation ?: return if (gesture.orientation == null) { - zAnimatable.snapTo(currentZOffset) - zAnimatable.animateTo(1f, animationSpec = tween(100)) { - currentZOffset = this.value - } + animateOffset3DTo( + currentOffset3D.copy(z = 1f), + animationSpec = tween(100), + ) } else { val targetOffset = if (anim == ScaffoldAnimation.Rubberband) { Offset.Zero @@ -865,16 +750,12 @@ internal class LauncherScaffoldState( } } - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo(targetOffset) { - currentOffset = this.value - } + animateOffset3DTo(currentOffset3D.copy(x = targetOffset.x, y = targetOffset.y)) } } /** * End the predictive back gesture and return to the home page. - * @param fast use a faster animation */ suspend fun onPredictiveBackEnd() { navigateBack() @@ -887,23 +768,19 @@ internal class LauncherScaffoldState( unlock() isSettledOnSecondaryPage = false - prepareHomePage() - - if (gesture.orientation == null) { - zAnimatable.snapTo(currentZOffset) - zAnimatable.animateTo(0f, animationSpec = tween(100)) { - currentZOffset = this.value - } - } else { - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo( - Offset.Zero, - animationSpec = if (fast) tween(150) else spring() - ) { - currentOffset = this.value + transitionToPage(ScaffoldPage.Home, component) { + if (gesture.orientation == null) { + animateOffset3DTo( + Offset3D.Zero, + animationSpec = tween(100), + ) + } else { + animateOffset3DTo( + Offset3D.Zero, + animationSpec = if (fast) tween(150) else spring(), + ) } } - deactivateSecondaryPage(component) } private suspend fun performTapGesture(gesture: Gesture) { @@ -917,56 +794,58 @@ internal class LauncherScaffoldState( } currentGesture = gesture - zAnimatable.snapTo(0f) - zAnimatable.animateTo(1f, animationSpec = tween(300)) { - currentZOffset = this.value + currentOffset3D = currentOffset3D.copy(z = 0f) + transitionToPage(ScaffoldPage.Secondary, component) { + animateOffset3DTo( + Offset3D(z = 1f), + animationSpec = tween(300), + ) + isSettledOnSecondaryPage = true + } + } + + private suspend fun transitionToPage( + targetPage: ScaffoldPage, + component: ScaffoldComponent, + animate: suspend () -> Unit, + ) { + when (targetPage) { + ScaffoldPage.Secondary -> { + config.homeComponent.onPreDismiss(this) + component.onPreActivate(this) + } + + ScaffoldPage.Home -> { + component.onPreDismiss(this) + config.homeComponent.onPreActivate(this) + } } - isSettledOnSecondaryPage = true + animate() - prepareSecondaryPage() - activateSecondaryPage(component) - } + when (targetPage) { + ScaffoldPage.Secondary -> { + component.onActivate(this) + if (!component.permanent) { + delay(component.resetDelay.milliseconds) + isSettledOnSecondaryPage = false + currentOffset3D = Offset3D.Zero + component.onPreDismiss(this) + component.onDismiss(this) + currentGesture = null + unlock() + } else { + config.homeComponent.onDismiss(this) + homePageSearchBarOffset = 0f + } + } - private suspend fun prepareSecondaryPage() { - config.homeComponent.onPreDismiss(this) - currentComponent?.onPreActivate(this) - } - - private suspend fun prepareHomePage() { - currentComponent?.onPreDismiss(this) - config.homeComponent.onPreActivate(this) - } - - /** - * Unmounts the secondary page component and mounts the home page component. - * Must be called after the animation is completed. - */ - private suspend fun deactivateSecondaryPage(component: ScaffoldComponent) { - config.homeComponent.onActivate(this) - component.onDismiss(this) - currentGesture = null - secondaryPageSearchBarOffset = 0f - } - - /** - * Mount the secondary page component and dismiss it again if not permanent. - * Must be called after the animation is completed. - */ - private suspend fun activateSecondaryPage(component: ScaffoldComponent) { - component.onActivate(this) - - if (!component.permanent) { - delay(component.resetDelay) - isSettledOnSecondaryPage = false - currentOffset = Offset.Zero - component.onPreDismiss(this) - component.onDismiss(this) - currentGesture = null - unlock() - } else { - config.homeComponent.onDismiss(this) - homePageSearchBarOffset = 0f + ScaffoldPage.Home -> { + config.homeComponent.onActivate(this) + component.onDismiss(this) + currentGesture = null + secondaryPageSearchBarOffset = 0f + } } } @@ -997,54 +876,45 @@ internal class LauncherScaffoldState( suspend fun navigateToPage(gesture: Gesture) { currentGesture = gesture val anim = config[gesture]?.animation ?: return + val component = config[gesture]!!.component - prepareSecondaryPage() + transitionToPage(ScaffoldPage.Secondary, component) { + if (anim == ScaffoldAnimation.Rubberband) { + val targetOffset = when (gesture) { + Gesture.SwipeLeft -> Offset(-rubberbandThreshold, 0f) + Gesture.SwipeRight -> Offset(rubberbandThreshold, 0f) + Gesture.SwipeUp -> Offset(0f, -rubberbandThreshold) + Gesture.SwipeDown -> Offset(0f, rubberbandThreshold) + else -> Offset.Zero + } - if (anim == ScaffoldAnimation.Rubberband) { - val targetOffset = when (gesture) { - Gesture.SwipeLeft -> Offset(-rubberbandThreshold, 0f) - Gesture.SwipeRight -> Offset(rubberbandThreshold, 0f) - Gesture.SwipeUp -> Offset(0f, -rubberbandThreshold) - Gesture.SwipeDown -> Offset(0f, rubberbandThreshold) - else -> Offset.Zero + animateOffset3DTo(currentOffset3D.copy(x = targetOffset.x, y = targetOffset.y)) + isSettledOnSecondaryPage = true + animateOffset3DTo(currentOffset3D.copy(x = 0f, y = 0f)) + } else { + val targetOffset = when (gesture) { + Gesture.SwipeLeft -> Offset(-size.width, 0f) + Gesture.SwipeRight -> Offset(size.width, 0f) + Gesture.SwipeUp -> Offset(0f, -size.height) + Gesture.SwipeDown -> Offset(0f, size.height) + else -> Offset.Zero + } + animateOffset3DTo(currentOffset3D.copy(x = targetOffset.x, y = targetOffset.y)) + isSettledOnSecondaryPage = true } - - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo(targetOffset) { - currentOffset = this.value - } - isSettledOnSecondaryPage = true - offsetAnimatable.animateTo(Offset.Zero) { - currentOffset = this.value - } - } else { - val targetOffset = when (gesture) { - Gesture.SwipeLeft -> Offset(-size.width, 0f) - Gesture.SwipeRight -> Offset(size.width, 0f) - Gesture.SwipeUp -> Offset(0f, -size.height) - Gesture.SwipeDown -> Offset(0f, size.height) - else -> Offset.Zero - } - offsetAnimatable.snapTo(currentOffset) - offsetAnimatable.animateTo(targetOffset) { - currentOffset = this.value - } - isSettledOnSecondaryPage = true } - activateSecondaryPage(config[gesture]!!.component) } suspend fun reset() { isSearchBarFocused = false - currentOffset = Offset.Zero - currentZOffset = 0f + currentOffset3D = Offset3D.Zero unlock() isSettledOnSecondaryPage = false currentComponent?.let { - it.onPreDismiss(this) - config.homeComponent.onPreActivate(this) - deactivateSecondaryPage(it) + transitionToPage(ScaffoldPage.Home, it) { + // No extra animation required on reset. + } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/Offset3D.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/Offset3D.kt new file mode 100644 index 000000000..5f5d92a44 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/Offset3D.kt @@ -0,0 +1,28 @@ +package de.mm20.launcher2.ui.launcher.scaffold.animation + +import androidx.compose.animation.core.AnimationVector3D +import androidx.compose.animation.core.TwoWayConverter +import androidx.compose.ui.geometry.Offset + +/** + * 3D displacement used by scaffold animations. + * x/y are screen-space offsets, z is normalized depth progress (0..1). + */ +internal data class Offset3D( + val x: Float = 0f, + val y: Float = 0f, + val z: Float = 0f, +) { + fun toOffset2D(): Offset = Offset(x, y) + + companion object { + val Zero = Offset3D() + + val VectorConverter: TwoWayConverter = TwoWayConverter( + convertToVector = { AnimationVector3D(it.x, it.y, it.z) }, + convertFromVector = { Offset3D(it.v1, it.v2, it.v3) }, + ) + + fun from2D(offset: Offset): Offset3D = Offset3D(x = offset.x, y = offset.y) + } +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/PushScaffoldAnimationController.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/PushScaffoldAnimationController.kt new file mode 100644 index 000000000..7106433a2 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/PushScaffoldAnimationController.kt @@ -0,0 +1,128 @@ +package de.mm20.launcher2.ui.launcher.scaffold.animation + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Velocity +import de.mm20.launcher2.ui.launcher.scaffold.Gesture +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage +import kotlin.math.absoluteValue + +internal class PushScaffoldAnimationController( + private val velocityThreshold: Float, +) : ScaffoldAnimationController { + override val animation: ScaffoldAnimation = ScaffoldAnimation.Push + + override fun initialOffset(direction: Gesture, size: Size): Offset3D { + return when (direction) { + Gesture.SwipeRight -> Offset3D(x = -size.width) + Gesture.SwipeLeft -> Offset3D(x = size.width) + Gesture.SwipeUp -> Offset3D(y = -size.height) + Gesture.SwipeDown -> Offset3D(y = size.height) + else -> Offset3D.Zero + } + } + + override fun calculateOffset( + direction: Gesture, + currentOffset: Offset3D, + delta: Offset3D, + currentPage: ScaffoldPage, + size: Size, + isAtTop: Boolean, + isAtBottom: Boolean, + ): Offset3D { + return when (direction) { + Gesture.SwipeUp -> currentOffset.copy( + x = 0f, + y = (currentOffset.y + delta.y).coerceIn(-size.height, 0f), + ) + + Gesture.SwipeDown -> currentOffset.copy( + x = 0f, + y = (currentOffset.y + delta.y).coerceIn(0f, size.height), + ) + + Gesture.SwipeLeft -> currentOffset.copy( + x = (currentOffset.x + delta.x).coerceIn(-size.width, 0f), + y = 0f, + ) + + Gesture.SwipeRight -> currentOffset.copy( + x = (currentOffset.x + delta.x).coerceIn(0f, size.width), + y = 0f, + ) + + else -> currentOffset + } + } + + override fun calculateProgress( + direction: Gesture, + offset: Offset3D, + currentPage: ScaffoldPage, + size: Size, + ): Float { + return if (direction.orientation == Orientation.Horizontal) { + (offset.x.absoluteValue / size.width).coerceIn(0f, 1f) + } else { + (offset.y.absoluteValue / size.height).coerceIn(0f, 1f) + } + } + + override fun calculateTargetPage( + direction: Gesture, + offset: Offset3D, + velocity: Velocity, + currentPage: ScaffoldPage, + size: Size, + ): ScaffoldPage { + val lowerPage = when (direction) { + Gesture.SwipeUp -> -size.height + Gesture.SwipeDown -> 0f + Gesture.SwipeLeft -> -size.width + Gesture.SwipeRight -> 0f + else -> return currentPage + } + val upperPage = if (direction.orientation == Orientation.Vertical) { + lowerPage + size.height + } else { + lowerPage + size.width + } + val threshold = (upperPage + lowerPage) / 2f + + val target = if (direction.orientation == Orientation.Vertical) { + if (offset.y > threshold && velocity.y > -velocityThreshold || velocity.y > velocityThreshold) { + upperPage + } else { + lowerPage + } + } else { + if (offset.x > threshold && velocity.x > -velocityThreshold || velocity.x > velocityThreshold) { + upperPage + } else { + lowerPage + } + } + + return if (target == 0f) ScaffoldPage.Home else ScaffoldPage.Secondary + } + + override fun calculateTargetOffset( + direction: Gesture, + targetPage: ScaffoldPage, + size: Size, + ): Offset3D { + if (targetPage == ScaffoldPage.Home) return Offset3D.Zero + + return when (direction) { + Gesture.SwipeLeft -> Offset3D(x = -size.width) + Gesture.SwipeRight -> Offset3D(x = size.width) + Gesture.SwipeUp -> Offset3D(y = -size.height) + Gesture.SwipeDown -> Offset3D(y = size.height) + else -> Offset3D.Zero + } + } + + override fun calculatePredictiveBackProgress(progress: Float): Float = 1f - progress * 0.1f +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/RubberbandScaffoldAnimationController.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/RubberbandScaffoldAnimationController.kt new file mode 100644 index 000000000..6a77fe88c --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/RubberbandScaffoldAnimationController.kt @@ -0,0 +1,95 @@ +package de.mm20.launcher2.ui.launcher.scaffold.animation + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Velocity +import de.mm20.launcher2.ui.launcher.scaffold.Gesture +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage +import kotlin.math.absoluteValue + +internal class RubberbandScaffoldAnimationController( + private val rubberbandThreshold: Float, + private val velocityThreshold: Float, +) : ScaffoldAnimationController { + override val animation: ScaffoldAnimation = ScaffoldAnimation.Rubberband + + override fun initialOffset(direction: Gesture, size: Size): Offset3D = Offset3D.Zero + + override fun calculateOffset( + direction: Gesture, + currentOffset: Offset3D, + delta: Offset3D, + currentPage: ScaffoldPage, + size: Size, + isAtTop: Boolean, + isAtBottom: Boolean, + ): Offset3D { + val adjustedDelta = when { + !isAtTop && !isAtBottom -> delta.copy(y = 0f) + !isAtTop && isAtBottom -> delta.copy(y = delta.y.coerceAtMost(-currentOffset.y)) + else -> delta.copy(y = delta.y.coerceAtLeast(-currentOffset.y)) + } + + val threshold = rubberbandThreshold * 1.5f + return when (direction) { + Gesture.SwipeUp, + Gesture.SwipeDown, + -> currentOffset.copy( + x = 0f, + y = (currentOffset.y + adjustedDelta.y).coerceIn(-threshold, threshold), + ) + + Gesture.SwipeLeft, + Gesture.SwipeRight, + -> currentOffset.copy( + x = (currentOffset.x + adjustedDelta.x).coerceIn(-threshold, threshold), + y = 0f, + ) + + else -> currentOffset + } + } + + override fun calculateProgress( + direction: Gesture, + offset: Offset3D, + currentPage: ScaffoldPage, + size: Size, + ): Float { + val axisOffset = (offset.x + offset.y).absoluteValue.coerceAtMost(rubberbandThreshold) + return when (currentPage) { + ScaffoldPage.Secondary -> 1f - axisOffset / (rubberbandThreshold * 2f) + ScaffoldPage.Home -> axisOffset / (rubberbandThreshold * 2f) + } + } + + override fun calculateTargetPage( + direction: Gesture, + offset: Offset3D, + velocity: Velocity, + currentPage: ScaffoldPage, + size: Size, + ): ScaffoldPage { + + val shouldToggle = + offset.x <= -rubberbandThreshold || offset.x < 0f && velocity.x < -velocityThreshold || + offset.x >= rubberbandThreshold || offset.x > 0f && velocity.x > velocityThreshold || + offset.y <= -rubberbandThreshold || offset.y < 0f && velocity.y < -velocityThreshold || + offset.y >= rubberbandThreshold || offset.y > 0f && velocity.y > velocityThreshold + + if (!shouldToggle) return currentPage + + return when (currentPage) { + ScaffoldPage.Home -> ScaffoldPage.Secondary + ScaffoldPage.Secondary -> ScaffoldPage.Home + } + } + + override fun calculateTargetOffset( + direction: Gesture, + targetPage: ScaffoldPage, + size: Size, + ): Offset3D = Offset3D.Zero + + override fun calculatePredictiveBackProgress(progress: Float): Float = 1f - progress +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ScaffoldAnimationController.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ScaffoldAnimationController.kt new file mode 100644 index 000000000..b0a650010 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ScaffoldAnimationController.kt @@ -0,0 +1,91 @@ +package de.mm20.launcher2.ui.launcher.scaffold.animation + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Velocity +import de.mm20.launcher2.ui.launcher.scaffold.Gesture +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage + +/** + * Encapsulates animation-specific movement logic for launcher page transitions. + * + * Contract: + * - x/y in [Offset3D] are screen-space offsets (px). + * - z in [Offset3D] is normalized depth progress (typically 0..1). + * - Drag and release are intentionally split: + * - [calculateOffset] for per-frame drag updates + * - [calculateTargetPage] and [calculateTargetOffset] for settle/fling behavior + */ +internal interface ScaffoldAnimationController { + /** The animation flavor handled by this controller. */ + val animation: ScaffoldAnimation + + /** + * Returns the initial displacement for a restored secondary page. + * + * Called when the scaffold state is reconstructed from saved state. + */ + fun initialOffset(direction: Gesture, size: Size): Offset3D + + /** + * Calculates the next displacement during a drag gesture. + * + * @param direction Locked gesture direction for the current interaction. + * @param currentOffset Current displacement before applying [delta]. + * @param delta Per-frame pointer delta, mapped to 3D space. + * @param currentPage Current settled page before drag update. + * @param size Current scaffold size in px. + * @param isAtTop Whether active content is scrolled to top. + * @param isAtBottom Whether active content is scrolled to bottom. + */ + fun calculateOffset( + direction: Gesture, + currentOffset: Offset3D, + delta: Offset3D, + currentPage: ScaffoldPage, + size: Size, + isAtTop: Boolean, + isAtBottom: Boolean, + ): Offset3D + + /** + * Maps the current displacement to transition progress in range 0..1. + * + * 0 means home page, 1 means secondary page. + */ + fun calculateProgress( + direction: Gesture, + offset: Offset3D, + currentPage: ScaffoldPage, + size: Size, + ): Float + + /** + * Decides the page that should be active after drag release/fling. + */ + fun calculateTargetPage( + direction: Gesture, + offset: Offset3D, + velocity: Velocity, + currentPage: ScaffoldPage, + size: Size, + ): ScaffoldPage + + /** + * Returns the settle target displacement for the given [targetPage]. + * + * This value is used as animation target after release. + */ + fun calculateTargetOffset( + direction: Gesture, + targetPage: ScaffoldPage, + size: Size, + ): Offset3D + + /** + * Maps predictive-back progress to scaffold transition progress. + * + * Input and output are normalized in range 0..1. + */ + fun calculatePredictiveBackProgress(progress: Float): Float +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ZoomInScaffoldAnimationController.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ZoomInScaffoldAnimationController.kt new file mode 100644 index 000000000..ac0518de5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/animation/ZoomInScaffoldAnimationController.kt @@ -0,0 +1,56 @@ +package de.mm20.launcher2.ui.launcher.scaffold.animation + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Velocity +import de.mm20.launcher2.ui.launcher.scaffold.Gesture +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation +import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage + +internal object ZoomInScaffoldAnimationController : ScaffoldAnimationController { + override val animation: ScaffoldAnimation = ScaffoldAnimation.ZoomIn + + override fun initialOffset(direction: Gesture, size: Size): Offset3D { + return if (direction.orientation == null) Offset3D(z = 1f) else Offset3D.Zero + } + + override fun calculateOffset( + direction: Gesture, + currentOffset: Offset3D, + delta: Offset3D, + currentPage: ScaffoldPage, + size: Size, + isAtTop: Boolean, + isAtBottom: Boolean, + ): Offset3D = currentOffset + + override fun calculateProgress( + direction: Gesture, + offset: Offset3D, + currentPage: ScaffoldPage, + size: Size, + ): Float = offset.z.coerceIn(0f, 1f) + + override fun calculateTargetPage( + direction: Gesture, + offset: Offset3D, + velocity: Velocity, + currentPage: ScaffoldPage, + size: Size, + ): ScaffoldPage = currentPage + + override fun calculateTargetOffset( + direction: Gesture, + targetPage: ScaffoldPage, + size: Size, + ): Offset3D { + return when (targetPage) { + ScaffoldPage.Home -> Offset3D.Zero + ScaffoldPage.Secondary -> Offset3D(z = 1f) + } + } + + override fun calculatePredictiveBackProgress(progress: Float): Float = 1f - progress +} + + + diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockAndWidgetsHomeComponent.kt similarity index 98% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockAndWidgetsHomeComponent.kt index b9d300080..fe0b490be 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockAndWidgetsHomeComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -42,6 +42,7 @@ import de.mm20.launcher2.preferences.WidgetScreenTarget import de.mm20.launcher2.preferences.ui.ClockWidgetSettings import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.toDp +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import kotlinx.coroutines.launch diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockHomeComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockHomeComponent.kt similarity index 88% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockHomeComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockHomeComponent.kt index 359e5b754..3a4628276 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockHomeComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ClockHomeComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding @@ -7,6 +7,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget internal object ClockHomeComponent : ScaffoldComponent() { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/DismissComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/DismissComponent.kt similarity index 92% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/DismissComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/DismissComponent.kt index f67c2d161..36b6f2350 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/DismissComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/DismissComponent.kt @@ -1,15 +1,15 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.app.Activity import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.scale +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState /** * A scaffold component that finishes the activity when activated. diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/FeedComponent.kt similarity index 98% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/FeedComponent.kt index 53897f1b5..4128f2135 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/FeedComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import androidx.activity.compose.LocalActivity @@ -32,6 +32,7 @@ import de.mm20.launcher2.feed.FeedService import de.mm20.launcher2.preferences.feed.FeedSettings import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.toIntOffset +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import kotlinx.coroutines.flow.flowOf import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LaunchComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/LaunchComponent.kt similarity index 94% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LaunchComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/LaunchComponent.kt index 4458455c2..bc53dd99c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LaunchComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/LaunchComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.app.Activity @@ -14,6 +14,7 @@ import androidx.core.app.ActivityOptionsCompat import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.ui.component.FakeSplashScreen import de.mm20.launcher2.ui.ktx.toIntOffset +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState internal class LaunchComponent( private val activity: Activity, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/NotificationsComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/NotificationsComponent.kt similarity index 97% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/NotificationsComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/NotificationsComponent.kt index eb947ebce..1212dbc3d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/NotificationsComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/NotificationsComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.view.animation.PathInterpolator @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/PowerMenuComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/PowerMenuComponent.kt similarity index 98% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/PowerMenuComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/PowerMenuComponent.kt index d8f16326e..53850b90d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/PowerMenuComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/PowerMenuComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.view.Surface @@ -38,6 +38,7 @@ import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/QuickSettingsComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/QuickSettingsComponent.kt similarity index 97% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/QuickSettingsComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/QuickSettingsComponent.kt index 20703897a..eb13c251b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/QuickSettingsComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/QuickSettingsComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.view.animation.PathInterpolator @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/RecentsComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/RecentsComponent.kt similarity index 97% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/RecentsComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/RecentsComponent.kt index 359d1184a..c56d8db92 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/RecentsComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/RecentsComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import android.view.RoundedCorner @@ -29,8 +29,8 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.GestureAction +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager -import kotlinx.coroutines.delay import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScaffoldComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScaffoldComponent.kt similarity index 96% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScaffoldComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScaffoldComponent.kt index fc113de25..829bae3cb 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScaffoldComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScaffoldComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import androidx.compose.foundation.layout.PaddingValues @@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState internal abstract class ScaffoldComponent { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScreenOffComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScreenOffComponent.kt similarity index 96% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScreenOffComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScreenOffComponent.kt index 5606a41f6..5734474f8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ScreenOffComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/ScreenOffComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box @@ -21,6 +21,7 @@ import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.GestureAction +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SearchComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt similarity index 96% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SearchComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt index c25f4de7f..f9bb0faca 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SearchComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -18,6 +18,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.search.SearchColumn import de.mm20.launcher2.ui.launcher.search.SearchVM diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SecretComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SecretComponent.kt similarity index 95% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SecretComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SecretComponent.kt index bde0a664d..f6e07b939 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/SecretComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SecretComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import android.content.Intent import androidx.compose.foundation.layout.Arrangement @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.settings.SettingsActivity internal object SecretComponent : ScaffoldComponent() { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/WidgetsComponent.kt similarity index 97% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/WidgetsComponent.kt index 0fd223f3d..d70be6624 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/WidgetsComponent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.ui.launcher.scaffold +package de.mm20.launcher2.ui.launcher.scaffold.components import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import de.mm20.launcher2.preferences.WidgetScreenTarget import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn import kotlinx.coroutines.launch