(fix) home transition to target that is no longer in view
This commit is contained in:
parent
8f4766d28a
commit
46d7293175
@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.android.launcher3.GestureNavContract
|
import com.android.launcher3.GestureNavContract
|
||||||
|
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
|
||||||
|
|
||||||
|
|
||||||
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
||||||
@ -9,7 +10,12 @@ class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
val navContract = intent.let { GestureNavContract.fromIntent(it) }
|
val navContract = intent.let { GestureNavContract.fromIntent(it) }
|
||||||
if (navContract != null) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
var isNewIntent = false
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.absoluteOffset
|
|||||||
import androidx.compose.foundation.layout.add
|
import androidx.compose.foundation.layout.add
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.displayCutout
|
import androidx.compose.foundation.layout.displayCutout
|
||||||
import androidx.compose.foundation.layout.exclude
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.ime
|
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.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@ -245,14 +244,16 @@ internal class LauncherScaffoldState(
|
|||||||
initialIsLocked: Boolean = false,
|
initialIsLocked: Boolean = false,
|
||||||
initialIsSearchBarHidden: Boolean = false,
|
initialIsSearchBarHidden: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var currentOffset by mutableStateOf(when {
|
var currentOffset by mutableStateOf(
|
||||||
initialGesture == null || initialGesture.orientation == null || config[initialGesture]?.animation == ScaffoldAnimation.Rubberband -> Offset.Zero
|
when {
|
||||||
initialGesture == Gesture.SwipeRight -> Offset(-size.width, 0f)
|
initialGesture == null || initialGesture.orientation == null || config[initialGesture]?.animation == ScaffoldAnimation.Rubberband -> Offset.Zero
|
||||||
initialGesture == Gesture.SwipeLeft -> Offset(size.width, 0f)
|
initialGesture == Gesture.SwipeRight -> Offset(-size.width, 0f)
|
||||||
initialGesture == Gesture.SwipeUp -> Offset(0f, -size.height)
|
initialGesture == Gesture.SwipeLeft -> Offset(size.width, 0f)
|
||||||
initialGesture == Gesture.SwipeDown -> Offset(0f, size.height)
|
initialGesture == Gesture.SwipeUp -> Offset(0f, -size.height)
|
||||||
else -> Offset.Zero
|
initialGesture == Gesture.SwipeDown -> Offset(0f, size.height)
|
||||||
})
|
else -> Offset.Zero
|
||||||
|
}
|
||||||
|
)
|
||||||
private set
|
private set
|
||||||
var currentZOffset by mutableFloatStateOf(
|
var currentZOffset by mutableFloatStateOf(
|
||||||
if (initialGesture != null && initialGesture.orientation == null) 1f else 0f
|
if (initialGesture != null && initialGesture.orientation == null) 1f else 0f
|
||||||
@ -1176,10 +1177,10 @@ internal fun LauncherScaffold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.isAtTop, state.isAtBottom) {
|
LaunchedEffect(state.isAtTop, state.isAtBottom) {
|
||||||
if (state.currentProgress > 0f && state.currentProgress < 1f){
|
if (state.currentProgress > 0f && state.currentProgress < 1f) {
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
when(state.currentComponent?.reverseScrolling) {
|
when (state.currentComponent?.reverseScrolling) {
|
||||||
true -> if (state.isAtBottom) state.resetSearchBarOffset()
|
true -> if (state.isAtBottom) state.resetSearchBarOffset()
|
||||||
false -> if (state.isAtTop) state.resetSearchBarOffset()
|
false -> if (state.isAtTop) state.resetSearchBarOffset()
|
||||||
else -> {}
|
else -> {}
|
||||||
@ -1211,10 +1212,11 @@ internal fun LauncherScaffold(
|
|||||||
config.homeComponent.onPreActivate(state)
|
config.homeComponent.onPreActivate(state)
|
||||||
config.homeComponent.onActivate(state)
|
config.homeComponent.onActivate(state)
|
||||||
|
|
||||||
var pauseTime = 0L
|
val activity = (activity as? SharedLauncherActivity) ?: return@LaunchedEffect
|
||||||
|
|
||||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
try {
|
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.isLocked) {
|
||||||
if (state.currentProgress > 0f) {
|
if (state.currentProgress > 0f) {
|
||||||
state.onPredictiveBackEnd()
|
state.onPredictiveBackEnd()
|
||||||
@ -1224,7 +1226,7 @@ internal fun LauncherScaffold(
|
|||||||
} else {
|
} else {
|
||||||
activity.onBackPressedDispatcher.onBackPressed()
|
activity.onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
} else if (pauseTime > 0L && System.currentTimeMillis() - pauseTime > 5000L) {
|
} else if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime > 5000L) {
|
||||||
if (!state.isLocked) {
|
if (!state.isLocked) {
|
||||||
state.reset()
|
state.reset()
|
||||||
searchVM.reset()
|
searchVM.reset()
|
||||||
@ -1232,7 +1234,8 @@ internal fun LauncherScaffold(
|
|||||||
}
|
}
|
||||||
awaitCancellation()
|
awaitCancellation()
|
||||||
} finally {
|
} finally {
|
||||||
pauseTime = System.currentTimeMillis()
|
activity.pauseTime = System.currentTimeMillis()
|
||||||
|
activity.pauseOnHome = !state.isSettledOnSecondaryPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1260,8 +1263,10 @@ internal fun LauncherScaffold(
|
|||||||
|
|
||||||
if (config.wallpaperBlurRadius > 0.dp) {
|
if (config.wallpaperBlurRadius > 0.dp) {
|
||||||
val wallpaperBlur by animateIntAsState(
|
val wallpaperBlur by animateIntAsState(
|
||||||
if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground ?: config.homeComponent.drawBackground)
|
if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground
|
||||||
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground) {
|
?: config.homeComponent.drawBackground)
|
||||||
|
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground
|
||||||
|
) {
|
||||||
8.dp.toPixels().toInt()
|
8.dp.toPixels().toInt()
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
@ -1412,40 +1417,44 @@ internal fun LauncherScaffold(
|
|||||||
bottom = filterBarHeight
|
bottom = filterBarHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
config.homeComponent.Component(
|
CompositionLocalProvider(
|
||||||
Modifier
|
LocalScaffoldPage provides ScaffoldPage.Home,
|
||||||
.fillMaxSize()
|
) {
|
||||||
.combinedClickable(
|
config.homeComponent.Component(
|
||||||
enabled = config.longPress != null || config.doubleTap != null,
|
Modifier
|
||||||
onClick = {},
|
.fillMaxSize()
|
||||||
onLongClick = if (config.longPress != null) {
|
.combinedClickable(
|
||||||
{ scope.launch { state.onLongPress() } }
|
enabled = config.longPress != null || config.doubleTap != null,
|
||||||
} else null,
|
onClick = {},
|
||||||
onDoubleClick = if (config.doubleTap != null) {
|
onLongClick = if (config.longPress != null) {
|
||||||
{ scope.launch { state.onDoubleTap() } }
|
{ scope.launch { state.onLongPress() } }
|
||||||
} else null,
|
} else null,
|
||||||
hapticFeedbackEnabled = false,
|
onDoubleClick = if (config.doubleTap != null) {
|
||||||
indication = null,
|
{ scope.launch { state.onDoubleTap() } }
|
||||||
interactionSource = null,
|
} else null,
|
||||||
)
|
hapticFeedbackEnabled = false,
|
||||||
.homePageAnimation(
|
indication = null,
|
||||||
state,
|
interactionSource = null,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
}
|
.homePageAnimation(
|
||||||
.asPaddingValues(),
|
state,
|
||||||
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(
|
SecondaryPage(
|
||||||
state = state,
|
state = state,
|
||||||
@ -1494,7 +1503,7 @@ internal fun LauncherScaffold(
|
|||||||
highlightedAction = highlightedResult as? SearchAction,
|
highlightedAction = highlightedResult as? SearchAction,
|
||||||
darkColors = config.darkSearchBar,
|
darkColors = config.darkSearchBar,
|
||||||
isSearchOpen = state.currentComponent is SearchComponent && state.isSettledOnSecondaryPage ||
|
isSearchOpen = state.currentComponent is SearchComponent && state.isSettledOnSecondaryPage ||
|
||||||
config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage,
|
config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isFilterBarVisible) {
|
if (isFilterBarVisible) {
|
||||||
@ -1566,6 +1575,7 @@ private fun SecondaryPage(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
insets: PaddingValues,
|
insets: PaddingValues,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val components = remember(config) {
|
val components = remember(config) {
|
||||||
setOfNotNull(
|
setOfNotNull(
|
||||||
config.swipeUp?.component,
|
config.swipeUp?.component,
|
||||||
@ -1602,7 +1612,11 @@ private fun SecondaryPage(
|
|||||||
)
|
)
|
||||||
val composable = composables[component]
|
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
|
// Keep other components alive, but out of the viewport
|
||||||
@ -1616,7 +1630,6 @@ private fun SecondaryPage(
|
|||||||
v.invoke(modifier, insets, state)
|
v.invoke(modifier, insets, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Modifier.homePageAnimation(
|
private fun Modifier.homePageAnimation(
|
||||||
@ -1657,12 +1670,16 @@ private fun Modifier.homePageAnimation(
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this then component.homePageModifier(state, Modifier.background(backgroundColor).absoluteOffset {
|
return this then component.homePageModifier(
|
||||||
IntOffset(
|
state,
|
||||||
x = if (dir.orientation == Orientation.Horizontal) state.currentOffset.x.toInt() else 0,
|
Modifier
|
||||||
y = if (dir.orientation == Orientation.Vertical) state.currentOffset.y.toInt() else 0
|
.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(
|
private fun Modifier.secondaryPageAnimation(
|
||||||
|
|||||||
@ -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 }
|
||||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.transitions
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import com.android.launcher3.GestureNavContract
|
import com.android.launcher3.GestureNavContract
|
||||||
|
import de.mm20.launcher2.ui.launcher.scaffold.LocalScaffoldPage
|
||||||
|
|
||||||
fun interface EnterHomeTransitionHandler {
|
fun interface EnterHomeTransitionHandler {
|
||||||
fun handle(gestureNavContract: GestureNavContract): EnterHomeTransitionParams?
|
fun handle(gestureNavContract: GestureNavContract): EnterHomeTransitionParams?
|
||||||
@ -11,11 +12,14 @@ fun interface EnterHomeTransitionHandler {
|
|||||||
@Composable
|
@Composable
|
||||||
fun HandleEnterHomeTransition(handler: EnterHomeTransitionHandler) {
|
fun HandleEnterHomeTransition(handler: EnterHomeTransitionHandler) {
|
||||||
val transitionManager = LocalEnterHomeTransitionManager.current
|
val transitionManager = LocalEnterHomeTransitionManager.current
|
||||||
DisposableEffect(handler) {
|
val page = LocalScaffoldPage.current
|
||||||
transitionManager?.registerHandler(handler)
|
if (page != null && transitionManager != null) {
|
||||||
|
DisposableEffect(handler, page) {
|
||||||
|
transitionManager.registerHandler(handler, page)
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
transitionManager?.unregisterHandler(handler)
|
transitionManager.unregisterHandler(handler, page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,21 +3,24 @@ package de.mm20.launcher2.ui.launcher.transitions
|
|||||||
import android.view.Window
|
import android.view.Window
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.graphics.toAndroidRect
|
import androidx.compose.ui.graphics.toAndroidRect
|
||||||
import androidx.compose.ui.graphics.toAndroidRectF
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntRect
|
import androidx.compose.ui.unit.IntRect
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.core.graphics.toRectF
|
import androidx.core.graphics.toRectF
|
||||||
import com.android.launcher3.GestureNavContract
|
import com.android.launcher3.GestureNavContract
|
||||||
|
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
class EnterHomeTransitionManager {
|
class EnterHomeTransitionManager {
|
||||||
|
|
||||||
val currentTransition = MutableSharedFlow<EnterHomeTransition?>(1)
|
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) {
|
for (handler in handlers) {
|
||||||
val result = handler.handle(gestureNavContract)
|
val result = handler.handle(gestureNavContract)
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@ -44,12 +47,23 @@ class EnterHomeTransitionManager {
|
|||||||
currentTransition.tryEmit(null)
|
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) {
|
fun unregisterHandler(handler: EnterHomeTransitionHandler, page: ScaffoldPage) {
|
||||||
handlers.remove(handler)
|
if (page == ScaffoldPage.Home) {
|
||||||
|
homeHandlers.remove(handler)
|
||||||
|
} else if (page == ScaffoldPage.Secondary) {
|
||||||
|
secondaryHandlers.remove(handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user