refactor launcher scaffold

This commit is contained in:
MM20
2026-05-27 21:12:40 +02:00
parent 2fd780d9bc
commit e25f2db3b5
21 changed files with 678 additions and 398 deletions

View File

@@ -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

View File

@@ -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<Gesture?>(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<Offset3D> = 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.
}
}
}

View File

@@ -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<Offset3D, AnimationVector3D> = 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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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.

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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() {

View File

@@ -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