Add gesture handler

This commit is contained in:
MM20 2023-01-16 12:02:34 +01:00
parent 3778e8405b
commit 7cd1269c33
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
5 changed files with 137 additions and 44 deletions

View File

@ -1,29 +1,49 @@
package de.mm20.launcher2.ui.gestures
import android.util.Log
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.Offset
class GestureManager {
class GestureDetector {
private var dragStart: Offset? = null
private var currentDrag : Offset? = null
fun reportDoubleTap(position: Offset) {
var gestureListener: OnGestureListener? = null
fun dispatchDoubleTap(position: Offset) {
gestureListener?.onDoubleTap(position)
}
fun reportLongPress(position: Offset) {
fun dispatchLongPress(position: Offset) {
gestureListener?.onLongPress(position)
}
fun reportDrag(offset: Offset) {
currentDrag = (currentDrag ?: Offset.Zero) + offset
private var hasDragEnded = false
fun dispatchDrag(offset: Offset) {
if (hasDragEnded) return
val totalDrag = currentDrag?.plus(offset) ?: offset
currentDrag = totalDrag
if (gestureListener?.onDrag(totalDrag) == true) hasDragEnded = true
}
fun reportDragEnd() {
fun dispatchDragEnd() {
dragStart = null
currentDrag = null
hasDragEnded = false
}
interface OnGestureListener {
fun onDoubleTap(position: Offset) {}
fun onLongPress(position: Offset) {}
/**
* @return true if the drag gesture has been handled.
* The gesture detector will no longer track the drag gesture in this case.
*/
fun onDrag(offset: Offset): Boolean = false
}
}
val LocalGestureManager = staticCompositionLocalOf<GestureManager> {
GestureManager()
val LocalGestureDetector = staticCompositionLocalOf<GestureDetector> {
GestureDetector()
}

View File

@ -0,0 +1,32 @@
package de.mm20.launcher2.ui.gestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.geometry.Offset
@Composable
fun GestureHandler(
detector: GestureDetector,
onLongPress: (Offset) -> Unit = {},
onDoubleTap: (Offset) -> Unit = {},
onDrag: (Offset) -> Boolean = { false },
) {
DisposableEffect(detector) {
detector.gestureListener = object : GestureDetector.OnGestureListener {
override fun onLongPress(position: Offset) {
onLongPress(position)
}
override fun onDoubleTap(position: Offset) {
onDoubleTap(position)
}
override fun onDrag(offset: Offset): Boolean {
return onDrag(offset)
}
}
onDispose {
detector.gestureListener = null
}
}
}

View File

@ -7,13 +7,11 @@ import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateStartPadding
@ -33,11 +31,8 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -52,10 +47,8 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
@ -68,7 +61,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@ -77,9 +69,8 @@ import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureManager
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
@ -87,10 +78,8 @@ import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@Composable
fun PagerScaffold(
@ -252,7 +241,7 @@ fun PagerScaffold(
val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureManager.current
val gestureManager = LocalGestureDetector.current
val searchBarOffset = remember { mutableStateOf(0f) }
val density = LocalDensity.current
@ -265,14 +254,14 @@ fun PagerScaffold(
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.Drag) gestureManager.reportDrag(available)
if (source == NestedScrollSource.Drag) gestureManager.dispatchDrag(available)
val deltaSearchBarOffset = consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.value = (searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
gestureManager.reportDragEnd()
gestureManager.dispatchDragEnd()
return super.onPreFling(available)
}
}
@ -350,10 +339,10 @@ fun PagerScaffold(
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
gestureManager.reportDoubleTap(it)
gestureManager.dispatchDoubleTap(it)
},
onLongPress = {
gestureManager.reportLongPress(it)
gestureManager.dispatchLongPress(it)
}
)
}
@ -453,7 +442,6 @@ fun PagerScaffold(
val searchBarLevel by remember {
derivedStateOf {
Log.d("MM20", pagerState.currentPageOffsetFraction.toString())
when {
pagerState.currentPageOffsetFraction != 0f -> SearchBarLevel.Raised
!isSearchOpen && isWidgetsScrollZero && fillClockHeight -> SearchBarLevel.Resting

View File

@ -7,11 +7,8 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@ -69,7 +66,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureManager
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn
@ -248,7 +245,7 @@ fun PullDownScaffold(
}
val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureManager.current
val gestureManager = LocalGestureDetector.current
val nestedScrollConnection = remember {
object : NestedScrollConnection {
@ -297,7 +294,7 @@ fun PullDownScaffold(
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) {
viewModel.toggleSearch()
}
gestureManager.reportDragEnd()
gestureManager.dispatchDragEnd()
if (offsetY.value != 0f) {
offsetY.animateTo(0f)
return available
@ -313,10 +310,10 @@ fun PullDownScaffold(
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
gestureManager.reportDragEnd()
gestureManager.dispatchDragEnd()
},
onHorizontalDrag = { _, dragAmount ->
gestureManager.reportDrag(Offset(dragAmount, 0f))
gestureManager.dispatchDrag(Offset(dragAmount, 0f))
}
)
}
@ -399,10 +396,10 @@ fun PullDownScaffold(
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
gestureManager.reportDoubleTap(it)
gestureManager.dispatchDoubleTap(it)
},
onLongPress = {
gestureManager.reportLongPress(it)
gestureManager.dispatchLongPress(it)
}
)
}

View File

@ -4,6 +4,7 @@ import android.app.WallpaperManager
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
@ -29,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
@ -41,10 +43,13 @@ import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.gestures.GestureDetector
import de.mm20.launcher2.ui.gestures.GestureHandler
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.sheets.CustomizeSearchableSheet
import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.transitions.HomeTransition
@ -56,6 +61,7 @@ import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
import kotlin.math.absoluteValue
import kotlin.math.pow
@ -81,6 +87,7 @@ abstract class SharedLauncherActivity(
viewModel.setSystemInDarkMode(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
val bottomSheetManager = LauncherBottomSheetManager()
val gestureDetector = GestureDetector()
setContent {
val snackbarHostState = remember { SnackbarHostState() }
@ -93,15 +100,20 @@ abstract class SharedLauncherActivity(
LocalWallpaperColors provides wallpaperColors,
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
LocalBottomSheetManager provides bottomSheetManager,
LocalGestureDetector provides gestureDetector,
) {
LauncherTheme {
ProvideCurrentTime {
ProvideSettings {
val statusBarColor by viewModel.statusBarColor.observeAsState(SystemBarColors.Auto)
val statusBarColor by viewModel.statusBarColor.observeAsState(
SystemBarColors.Auto
)
val navBarColor by viewModel.navBarColor.observeAsState(SystemBarColors.Auto)
val lightStatus = !dimBackground && (statusBarColor == SystemBarColors.Dark || statusBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
val lightNav = !dimBackground && (navBarColor == SystemBarColors.Dark || navBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
val lightStatus =
!dimBackground && (statusBarColor == SystemBarColors.Dark || statusBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
val lightNav =
!dimBackground && (navBarColor == SystemBarColors.Dark || navBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
val hideStatus by viewModel.hideStatusBar.observeAsState(false)
val hideNav by viewModel.hideNavBar.observeAsState(false)
@ -207,7 +219,8 @@ abstract class SharedLauncherActivity(
enterTransition?.let {
val dX = it.startBounds.center.x - it.targetBounds.center.x
val dY = it.startBounds.center.y - it.targetBounds.center.y
val s = (it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
val s =
(it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
Box(
modifier = Modifier
.align(Alignment.TopStart)
@ -220,17 +233,60 @@ abstract class SharedLauncherActivity(
scaleX = 1f + s * (1 - p)
scaleY = 1f + s * (1 - p)
}) {
it.icon?.invoke(Offset(dX, dY)) { enterTransitionProgress.value }
it.icon?.invoke(
Offset(
dX,
dY
)
) { enterTransitionProgress.value }
}
}
bottomSheetManager.customizeSearchableSheetShown.value?.let {
CustomizeSearchableSheet(searchable = it, onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
CustomizeSearchableSheet(
searchable = it,
onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
}
if (bottomSheetManager.editFavoritesSheetShown.value) {
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
}
}
val swipeThreshold = 150.dp.toPixels()
GestureHandler(
detector = gestureDetector,
onDoubleTap = {
Log.d("MM20", "Double tap")
},
onLongPress = {
Log.d("MM20", "Long press")
},
onDrag = {
return@GestureHandler when {
it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
Log.d("MM20", "Swipe right")
true
}
it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
Log.d("MM20", "Swipe left")
true
}
it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
Log.d("MM20", "Swipe down")
true
}
it.y < -swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
Log.d("MM20", "Swipe up")
true
}
else -> false
}
}
)
}
}
}