(fix) home transition to target that is no longer in view

This commit is contained in:
MM20 2025-06-30 20:11:03 +02:00
parent 8f4766d28a
commit 46d7293175
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 129 additions and 72 deletions

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher
import android.content.Intent
import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
@ -9,7 +10,12 @@ class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
super.onNewIntent(intent)
val navContract = intent.let { GestureNavContract.fromIntent(it) }
if (navContract != null) {
enterHomeTransitionManager.resolve(navContract, window)
val page = if (System.currentTimeMillis() - pauseTime > 5000L || pauseOnHome) {
ScaffoldPage.Home
} else {
ScaffoldPage.Secondary
}
enterHomeTransitionManager.resolve(navContract, window, page)
}
}

View File

@ -432,6 +432,12 @@ abstract class SharedLauncherActivity(
}
}
var pauseTime = 0L
/**
* True if the scaffold was on home screen when the activity was paused.
*/
var pauseOnHome = false
var isNewIntent = false
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

View File

@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
@ -39,7 +38,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
@ -49,6 +47,7 @@ import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
@ -245,14 +244,16 @@ 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
})
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 set
var currentZOffset by mutableFloatStateOf(
if (initialGesture != null && initialGesture.orientation == null) 1f else 0f
@ -1176,10 +1177,10 @@ internal fun LauncherScaffold(
}
LaunchedEffect(state.isAtTop, state.isAtBottom) {
if (state.currentProgress > 0f && state.currentProgress < 1f){
if (state.currentProgress > 0f && state.currentProgress < 1f) {
return@LaunchedEffect
}
when(state.currentComponent?.reverseScrolling) {
when (state.currentComponent?.reverseScrolling) {
true -> if (state.isAtBottom) state.resetSearchBarOffset()
false -> if (state.isAtTop) state.resetSearchBarOffset()
else -> {}
@ -1211,10 +1212,11 @@ internal fun LauncherScaffold(
config.homeComponent.onPreActivate(state)
config.homeComponent.onActivate(state)
var pauseTime = 0L
val activity = (activity as? SharedLauncherActivity) ?: return@LaunchedEffect
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
try {
if (pauseTime > 0L && System.currentTimeMillis() - pauseTime < 50L && (activity as? SharedLauncherActivity)?.isNewIntent == true) {
if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime < 50L && activity.isNewIntent) {
if (!state.isLocked) {
if (state.currentProgress > 0f) {
state.onPredictiveBackEnd()
@ -1224,7 +1226,7 @@ internal fun LauncherScaffold(
} else {
activity.onBackPressedDispatcher.onBackPressed()
}
} else if (pauseTime > 0L && System.currentTimeMillis() - pauseTime > 5000L) {
} else if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime > 5000L) {
if (!state.isLocked) {
state.reset()
searchVM.reset()
@ -1232,7 +1234,8 @@ internal fun LauncherScaffold(
}
awaitCancellation()
} finally {
pauseTime = System.currentTimeMillis()
activity.pauseTime = System.currentTimeMillis()
activity.pauseOnHome = !state.isSettledOnSecondaryPage
}
}
}
@ -1260,8 +1263,10 @@ internal fun LauncherScaffold(
if (config.wallpaperBlurRadius > 0.dp) {
val wallpaperBlur by animateIntAsState(
if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground ?: config.homeComponent.drawBackground)
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground) {
if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground
?: config.homeComponent.drawBackground)
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground
) {
8.dp.toPixels().toInt()
} else {
0
@ -1412,40 +1417,44 @@ internal fun LauncherScaffold(
bottom = filterBarHeight
)
config.homeComponent.Component(
Modifier
.fillMaxSize()
.combinedClickable(
enabled = config.longPress != null || config.doubleTap != null,
onClick = {},
onLongClick = if (config.longPress != null) {
{ scope.launch { state.onLongPress() } }
} else null,
onDoubleClick = if (config.doubleTap != null) {
{ scope.launch { state.onDoubleTap() } }
} else null,
hapticFeedbackEnabled = false,
indication = null,
interactionSource = null,
)
.homePageAnimation(
state,
if (config.homeComponent.drawBackground) {
config.backgroundColor.copy(alpha = MaterialTheme.transparency.background)
} else {
Color.Transparent
}
),
insets = systemBarInsets
.let { if (config.homeComponent.hasIme) it.union(WindowInsets.ime) else it }
.let {
if (config.searchBarStyle == SearchBarStyle.Hidden) it else it.add(
searchBarInsets
CompositionLocalProvider(
LocalScaffoldPage provides ScaffoldPage.Home,
) {
config.homeComponent.Component(
Modifier
.fillMaxSize()
.combinedClickable(
enabled = config.longPress != null || config.doubleTap != null,
onClick = {},
onLongClick = if (config.longPress != null) {
{ scope.launch { state.onLongPress() } }
} else null,
onDoubleClick = if (config.doubleTap != null) {
{ scope.launch { state.onDoubleTap() } }
} else null,
hapticFeedbackEnabled = false,
indication = null,
interactionSource = null,
)
}
.asPaddingValues(),
state
)
.homePageAnimation(
state,
if (config.homeComponent.drawBackground) {
config.backgroundColor.copy(alpha = MaterialTheme.transparency.background)
} else {
Color.Transparent
}
),
insets = systemBarInsets
.let { if (config.homeComponent.hasIme) it.union(WindowInsets.ime) else it }
.let {
if (config.searchBarStyle == SearchBarStyle.Hidden) it else it.add(
searchBarInsets
)
}
.asPaddingValues(),
state
)
}
SecondaryPage(
state = state,
@ -1494,7 +1503,7 @@ internal fun LauncherScaffold(
highlightedAction = highlightedResult as? SearchAction,
darkColors = config.darkSearchBar,
isSearchOpen = state.currentComponent is SearchComponent && state.isSettledOnSecondaryPage ||
config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage,
config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage,
)
}
if (isFilterBarVisible) {
@ -1566,6 +1575,7 @@ private fun SecondaryPage(
modifier: Modifier = Modifier,
insets: PaddingValues,
) {
val components = remember(config) {
setOfNotNull(
config.swipeUp?.component,
@ -1602,7 +1612,11 @@ private fun SecondaryPage(
)
val composable = composables[component]
composable?.invoke(mod, insets, state)
CompositionLocalProvider(
LocalScaffoldPage provides ScaffoldPage.Secondary
) {
composable?.invoke(mod, insets, state)
}
}
// Keep other components alive, but out of the viewport
@ -1616,7 +1630,6 @@ private fun SecondaryPage(
v.invoke(modifier, insets, state)
}
}
}
private fun Modifier.homePageAnimation(
@ -1657,12 +1670,16 @@ private fun Modifier.homePageAnimation(
.background(backgroundColor)
}
return this then component.homePageModifier(state, Modifier.background(backgroundColor).absoluteOffset {
IntOffset(
x = if (dir.orientation == Orientation.Horizontal) state.currentOffset.x.toInt() else 0,
y = if (dir.orientation == Orientation.Vertical) state.currentOffset.y.toInt() else 0
)
})
return this then component.homePageModifier(
state,
Modifier
.background(backgroundColor)
.absoluteOffset {
IntOffset(
x = if (dir.orientation == Orientation.Horizontal) state.currentOffset.x.toInt() else 0,
y = if (dir.orientation == Orientation.Vertical) state.currentOffset.y.toInt() else 0
)
})
}
private fun Modifier.secondaryPageAnimation(

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.ui.launcher.scaffold
import androidx.compose.runtime.compositionLocalOf
enum class ScaffoldPage {
Home,
Secondary,
}
val LocalScaffoldPage = compositionLocalOf<ScaffoldPage?> { null }

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.transitions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.LocalScaffoldPage
fun interface EnterHomeTransitionHandler {
fun handle(gestureNavContract: GestureNavContract): EnterHomeTransitionParams?
@ -11,11 +12,14 @@ fun interface EnterHomeTransitionHandler {
@Composable
fun HandleEnterHomeTransition(handler: EnterHomeTransitionHandler) {
val transitionManager = LocalEnterHomeTransitionManager.current
DisposableEffect(handler) {
transitionManager?.registerHandler(handler)
val page = LocalScaffoldPage.current
if (page != null && transitionManager != null) {
DisposableEffect(handler, page) {
transitionManager.registerHandler(handler, page)
onDispose {
transitionManager?.unregisterHandler(handler)
onDispose {
transitionManager.unregisterHandler(handler, page)
}
}
}
}

View File

@ -3,21 +3,24 @@ package de.mm20.launcher2.ui.launcher.transitions
import android.view.Window
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.toAndroidRectF
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.core.graphics.toRectF
import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
import kotlinx.coroutines.flow.MutableSharedFlow
class EnterHomeTransitionManager {
val currentTransition = MutableSharedFlow<EnterHomeTransition?>(1)
private val handlers = mutableSetOf<EnterHomeTransitionHandler>()
private val homeHandlers = mutableSetOf<EnterHomeTransitionHandler>()
private val secondaryHandlers = mutableSetOf<EnterHomeTransitionHandler>()
fun resolve(gestureNavContract: GestureNavContract, window: Window, page: ScaffoldPage) {
val handlers = if (page === ScaffoldPage.Secondary) secondaryHandlers else homeHandlers
fun resolve(gestureNavContract: GestureNavContract, window: Window) {
for (handler in handlers) {
val result = handler.handle(gestureNavContract)
if (result != null) {
@ -44,12 +47,23 @@ class EnterHomeTransitionManager {
currentTransition.tryEmit(null)
}
fun registerHandler(handler: EnterHomeTransitionHandler) {
handlers.add(handler)
/**
* The scaffold page that needs to be active for this handler to be considered.
*/
fun registerHandler(handler: EnterHomeTransitionHandler, page: ScaffoldPage) {
if (page == ScaffoldPage.Home) {
homeHandlers.add(handler)
} else if (page == ScaffoldPage.Secondary) {
secondaryHandlers.add(handler)
}
}
fun unregisterHandler(handler: EnterHomeTransitionHandler) {
handlers.remove(handler)
fun unregisterHandler(handler: EnterHomeTransitionHandler, page: ScaffoldPage) {
if (page == ScaffoldPage.Home) {
homeHandlers.remove(handler)
} else if (page == ScaffoldPage.Secondary) {
secondaryHandlers.remove(handler)
}
}
}