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 package de.mm20.launcher2.ui.gestures
import android.util.Log
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
class GestureManager { class GestureDetector {
private var dragStart: Offset? = null private var dragStart: Offset? = null
private var currentDrag : 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) { private var hasDragEnded = false
currentDrag = (currentDrag ?: Offset.Zero) + offset 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 dragStart = null
currentDrag = 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> { val LocalGestureDetector = staticCompositionLocalOf<GestureDetector> {
GestureManager() 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.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateStartPadding 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.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Done 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.CenterAlignedTopAppBar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -52,10 +47,8 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer 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.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.preferences.Settings.SearchBarSettings.SearchBarStyle
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel 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.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM 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.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@Composable @Composable
fun PagerScaffold( fun PagerScaffold(
@ -252,7 +241,7 @@ fun PagerScaffold(
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureManager.current val gestureManager = LocalGestureDetector.current
val searchBarOffset = remember { mutableStateOf(0f) } val searchBarOffset = remember { mutableStateOf(0f) }
val density = LocalDensity.current val density = LocalDensity.current
@ -265,14 +254,14 @@ fun PagerScaffold(
available: Offset, available: Offset,
source: NestedScrollSource source: NestedScrollSource
): Offset { ): 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 val deltaSearchBarOffset = consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.value = (searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset) searchBarOffset.value = (searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source) return super.onPostScroll(consumed, available, source)
} }
override suspend fun onPreFling(available: Velocity): Velocity { override suspend fun onPreFling(available: Velocity): Velocity {
gestureManager.reportDragEnd() gestureManager.dispatchDragEnd()
return super.onPreFling(available) return super.onPreFling(available)
} }
} }
@ -350,10 +339,10 @@ fun PagerScaffold(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onDoubleTap = { onDoubleTap = {
gestureManager.reportDoubleTap(it) gestureManager.dispatchDoubleTap(it)
}, },
onLongPress = { onLongPress = {
gestureManager.reportLongPress(it) gestureManager.dispatchLongPress(it)
} }
) )
} }
@ -453,7 +442,6 @@ fun PagerScaffold(
val searchBarLevel by remember { val searchBarLevel by remember {
derivedStateOf { derivedStateOf {
Log.d("MM20", pagerState.currentPageOffsetFraction.toString())
when { when {
pagerState.currentPageOffsetFraction != 0f -> SearchBarLevel.Raised pagerState.currentPageOffsetFraction != 0f -> SearchBarLevel.Raised
!isSearchOpen && isWidgetsScrollZero && fillClockHeight -> SearchBarLevel.Resting !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.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures 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.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column 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.preferences.Settings
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel 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.animateTo
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn import de.mm20.launcher2.ui.launcher.search.SearchColumn
@ -248,7 +245,7 @@ fun PullDownScaffold(
} }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureManager.current val gestureManager = LocalGestureDetector.current
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
@ -297,7 +294,7 @@ fun PullDownScaffold(
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) { if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) {
viewModel.toggleSearch() viewModel.toggleSearch()
} }
gestureManager.reportDragEnd() gestureManager.dispatchDragEnd()
if (offsetY.value != 0f) { if (offsetY.value != 0f) {
offsetY.animateTo(0f) offsetY.animateTo(0f)
return available return available
@ -313,10 +310,10 @@ fun PullDownScaffold(
.pointerInput(Unit) { .pointerInput(Unit) {
detectHorizontalDragGestures( detectHorizontalDragGestures(
onDragEnd = { onDragEnd = {
gestureManager.reportDragEnd() gestureManager.dispatchDragEnd()
}, },
onHorizontalDrag = { _, dragAmount -> onHorizontalDrag = { _, dragAmount ->
gestureManager.reportDrag(Offset(dragAmount, 0f)) gestureManager.dispatchDrag(Offset(dragAmount, 0f))
} }
) )
} }
@ -399,10 +396,10 @@ fun PullDownScaffold(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onDoubleTap = { onDoubleTap = {
gestureManager.reportDoubleTap(it) gestureManager.dispatchDoubleTap(it)
}, },
onLongPress = { 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.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.background 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.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle 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.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.component.NavBarEffects 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.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.sheets.CustomizeSearchableSheet import de.mm20.launcher2.ui.launcher.sheets.CustomizeSearchableSheet
import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet 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.LauncherBottomSheetManager
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.transitions.HomeTransition 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.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme import de.mm20.launcher2.ui.theme.LauncherTheme
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
import kotlin.math.absoluteValue
import kotlin.math.pow 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) viewModel.setSystemInDarkMode(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
val bottomSheetManager = LauncherBottomSheetManager() val bottomSheetManager = LauncherBottomSheetManager()
val gestureDetector = GestureDetector()
setContent { setContent {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@ -93,15 +100,20 @@ abstract class SharedLauncherActivity(
LocalWallpaperColors provides wallpaperColors, LocalWallpaperColors provides wallpaperColors,
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText), LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
LocalBottomSheetManager provides bottomSheetManager, LocalBottomSheetManager provides bottomSheetManager,
LocalGestureDetector provides gestureDetector,
) { ) {
LauncherTheme { LauncherTheme {
ProvideCurrentTime { ProvideCurrentTime {
ProvideSettings { 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 navBarColor by viewModel.navBarColor.observeAsState(SystemBarColors.Auto)
val lightStatus = !dimBackground && (statusBarColor == SystemBarColors.Dark || statusBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText) val lightStatus =
val lightNav = !dimBackground && (navBarColor == SystemBarColors.Dark || navBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText) !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 hideStatus by viewModel.hideStatusBar.observeAsState(false)
val hideNav by viewModel.hideNavBar.observeAsState(false) val hideNav by viewModel.hideNavBar.observeAsState(false)
@ -207,7 +219,8 @@ abstract class SharedLauncherActivity(
enterTransition?.let { enterTransition?.let {
val dX = it.startBounds.center.x - it.targetBounds.center.x val dX = it.startBounds.center.x - it.targetBounds.center.x
val dY = it.startBounds.center.y - it.targetBounds.center.y 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( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
@ -220,17 +233,60 @@ abstract class SharedLauncherActivity(
scaleX = 1f + s * (1 - p) scaleX = 1f + s * (1 - p)
scaleY = 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 { bottomSheetManager.customizeSearchableSheetShown.value?.let {
CustomizeSearchableSheet(searchable = it, onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() }) CustomizeSearchableSheet(
searchable = it,
onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
} }
if (bottomSheetManager.editFavoritesSheetShown.value) { if (bottomSheetManager.editFavoritesSheetShown.value) {
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() }) 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
}
}
)
} }
} }
} }