Pager layout: scroll search bar out of view and prepare for gestures
This commit is contained in:
parent
4e25bd5cd0
commit
4179172b0b
@ -0,0 +1,30 @@
|
|||||||
|
package de.mm20.launcher2.ui.gestures
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
||||||
|
class GestureManager {
|
||||||
|
private var dragStart: Offset? = null
|
||||||
|
private var currentDrag : Offset? = null
|
||||||
|
|
||||||
|
fun reportDoubleTap(position: Offset) {
|
||||||
|
Log.d("MM20", "double tap: $position")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportLongPress(position: Offset) {
|
||||||
|
Log.d("MM20", "long press: $position")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportDrag(offset: Offset) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportDragEnd() {
|
||||||
|
dragStart = null
|
||||||
|
currentDrag = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalGestureManager = staticCompositionLocalOf<GestureManager> {
|
||||||
|
GestureManager()
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@ -7,6 +8,7 @@ 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.Orientation
|
||||||
|
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
|
||||||
@ -16,6 +18,7 @@ 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
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
@ -26,6 +29,8 @@ import androidx.compose.foundation.layout.safeDrawing
|
|||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
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.FractionalThreshold
|
||||||
@ -44,15 +49,20 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
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.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@ -67,6 +77,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.ktx.animateTo
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
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
|
||||||
@ -99,7 +111,8 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
val widgetsScrollState = rememberScrollState()
|
val widgetsScrollState = rememberScrollState()
|
||||||
val searchState = rememberLazyListState()
|
val searchState = rememberLazyListState()
|
||||||
val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets)
|
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
|
||||||
val isSearchAtBottom by remember {
|
val isSearchAtBottom by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@ -107,7 +120,8 @@ fun PagerScaffold(
|
|||||||
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
||||||
} else {
|
} else {
|
||||||
val lastItem =
|
val lastItem =
|
||||||
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
searchState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
?: return@derivedStateOf true
|
||||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,10 +208,7 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
val blurWallpaper by remember {
|
val blurWallpaper by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
blurEnabled && (
|
blurEnabled && (isSearchOpen || !isWidgetsScrollZero)
|
||||||
isSearchOpen || swipeableState.progress.to == Page.Widgets && swipeableState.progress.fraction <= 0.5f ||
|
|
||||||
swipeableState.progress.to == Page.Search && swipeableState.progress.fraction > 0.5f ||
|
|
||||||
!isWidgetsScrollZero)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,16 +216,16 @@ fun PagerScaffold(
|
|||||||
blurWallpaper
|
blurWallpaper
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentPage = swipeableState.currentValue
|
val currentPage = pagerState.currentPage
|
||||||
LaunchedEffect(currentPage) {
|
LaunchedEffect(currentPage) {
|
||||||
if (currentPage == Page.Search) viewModel.openSearch()
|
if (currentPage == 1) viewModel.openSearch()
|
||||||
else viewModel.closeSearch()
|
else viewModel.closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isSearchOpen) {
|
LaunchedEffect(isSearchOpen) {
|
||||||
if (isSearchOpen) swipeableState.animateTo(Page.Search)
|
if (isSearchOpen) pagerState.animateScrollToPage(1)
|
||||||
else {
|
else {
|
||||||
swipeableState.animateTo(Page.Widgets)
|
pagerState.animateScrollToPage(0)
|
||||||
searchVM.search("")
|
searchVM.search("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,42 +250,38 @@ fun PagerScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationDragThreshold = with(LocalDensity.current) { 200.dp.toPx() }
|
|
||||||
val notificationShadeController = rememberNotificationShadeController()
|
|
||||||
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
val gestureManager = LocalGestureManager.current
|
||||||
|
|
||||||
|
val searchBarOffset = remember { mutableStateOf(0f) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
|
||||||
|
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
private var pullDownTotalY: Float? = 0f
|
override fun onPostScroll(
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
consumed: Offset,
|
||||||
if (!isWidgetsScrollZero) return Offset.Zero
|
available: Offset,
|
||||||
val diff = -available.y
|
source: NestedScrollSource
|
||||||
var totalY = pullDownTotalY ?: return available
|
): Offset {
|
||||||
if (diff >= 0) return super.onPreScroll(available, source)
|
if (source == NestedScrollSource.Drag) gestureManager.reportDrag(available)
|
||||||
|
val deltaSearchBarOffset = consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
|
||||||
totalY += diff
|
searchBarOffset.value = (searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||||
|
return super.onPostScroll(consumed, available, source)
|
||||||
if (totalY < -notificationDragThreshold) {
|
|
||||||
notificationShadeController.expandNotifications()
|
|
||||||
pullDownTotalY = null
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
pullDownTotalY = totalY
|
|
||||||
|
|
||||||
return super.onPreScroll(available, source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
if (pullDownTotalY == null) {
|
gestureManager.reportDragEnd()
|
||||||
pullDownTotalY = 0f
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
return super.onPreFling(available)
|
return super.onPreFling(available)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
|
searchBarOffset.animateTo(0f)
|
||||||
|
}
|
||||||
|
|
||||||
val searchNestedScrollConnection = remember {
|
val searchNestedScrollConnection = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
@ -290,6 +297,7 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
@ -302,43 +310,21 @@ fun PagerScaffold(
|
|||||||
derivedStateOf { maxWidth }
|
derivedStateOf { maxWidth }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val widthPx = width.toPixels()
|
|
||||||
|
|
||||||
val originalLayoutDirection = LocalLayoutDirection.current
|
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalOverscrollConfiguration provides null,
|
LocalOverscrollConfiguration provides null,
|
||||||
LocalLayoutDirection provides if (reverse) LayoutDirection.Rtl else LayoutDirection.Ltr
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Row(
|
HorizontalPager(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.requiredWidth(width * 2)
|
pageCount = 2,
|
||||||
.fillMaxHeight()
|
beyondBoundsPageCount = 1,
|
||||||
.swipeable(
|
reverseLayout = reverse,
|
||||||
swipeableState,
|
state = pagerState,
|
||||||
orientation = Orientation.Horizontal,
|
userScrollEnabled = !isWidgetEditMode,
|
||||||
anchors = mapOf(
|
|
||||||
-widthPx / 2f to Page.Search,
|
|
||||||
widthPx / 2f to Page.Widgets,
|
|
||||||
),
|
|
||||||
thresholds = { _, _ ->
|
|
||||||
FractionalThreshold(0.5f)
|
|
||||||
},
|
|
||||||
enabled = !isWidgetEditMode,
|
|
||||||
reverseDirection = reverse,
|
|
||||||
)
|
|
||||||
.offset {
|
|
||||||
IntOffset(swipeableState.offset.value.roundToInt(), 0)
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
|
val pagerProgress = pagerState.currentPage + pagerState.currentPageOffsetFraction
|
||||||
CompositionLocalProvider(
|
when(it) {
|
||||||
LocalLayoutDirection provides originalLayoutDirection
|
0 -> {
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
val editModePadding by animateDpAsState(if (isWidgetEditMode && bottomSearchBar) 56.dp else 0.dp)
|
val editModePadding by animateDpAsState(if (isWidgetEditMode && bottomSearchBar) 56.dp else 0.dp)
|
||||||
|
|
||||||
val clockPadding by animateDpAsState(
|
val clockPadding by animateDpAsState(
|
||||||
@ -361,9 +347,21 @@ fun PagerScaffold(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.requiredWidth(width)
|
.requiredWidth(width)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.nestedScroll(nestedScrollConnection)
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
gestureManager.reportDoubleTap(it)
|
||||||
|
},
|
||||||
|
onLongPress = {
|
||||||
|
gestureManager.reportLongPress(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.verticalScroll(widgetsScrollState)
|
.verticalScroll(widgetsScrollState)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = 1f - pagerProgress
|
||||||
|
}
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.padding(
|
.padding(
|
||||||
top = if (bottomSearchBar) 0.dp else 56.dp,
|
top = if (bottomSearchBar) 0.dp else 56.dp,
|
||||||
@ -376,7 +374,8 @@ fun PagerScaffold(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
|
.then(clockHeight?.let { Modifier.height(it) }
|
||||||
|
?: Modifier)
|
||||||
.padding(bottom = clockPadding),
|
.padding(bottom = clockPadding),
|
||||||
contentAlignment = Alignment.BottomCenter
|
contentAlignment = Alignment.BottomCenter
|
||||||
) {
|
) {
|
||||||
@ -393,7 +392,8 @@ fun PagerScaffold(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
val webSearchPadding by animateDpAsState(
|
val webSearchPadding by animateDpAsState(
|
||||||
if (actions.isEmpty()) 0.dp else 48.dp
|
if (actions.isEmpty()) 0.dp else 48.dp
|
||||||
)
|
)
|
||||||
@ -413,10 +413,17 @@ fun PagerScaffold(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.requiredWidth(width)
|
.requiredWidth(width)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = pagerProgress
|
||||||
|
}
|
||||||
.nestedScroll(searchNestedScrollConnection)
|
.nestedScroll(searchNestedScrollConnection)
|
||||||
.padding(
|
.padding(
|
||||||
start = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
start = windowInsets.calculateStartPadding(
|
||||||
end = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
LocalLayoutDirection.current
|
||||||
|
),
|
||||||
|
end = windowInsets.calculateStartPadding(
|
||||||
|
LocalLayoutDirection.current
|
||||||
|
),
|
||||||
),
|
),
|
||||||
reverse = reverseSearchResults,
|
reverse = reverseSearchResults,
|
||||||
state = searchState,
|
state = searchState,
|
||||||
@ -426,6 +433,7 @@ fun PagerScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
AnimatedVisibility(visible = isWidgetEditMode,
|
AnimatedVisibility(visible = isWidgetEditMode,
|
||||||
enter = slideIn { IntOffset(0, -it.height) },
|
enter = slideIn { IntOffset(0, -it.height) },
|
||||||
exit = slideOut { IntOffset(0, -it.height) }
|
exit = slideOut { IntOffset(0, -it.height) }
|
||||||
@ -445,8 +453,9 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
val searchBarLevel by remember {
|
val searchBarLevel by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
Log.d("MM20", pagerState.currentPageOffsetFraction.toString())
|
||||||
when {
|
when {
|
||||||
swipeableState.direction != 0f -> SearchBarLevel.Raised
|
pagerState.currentPageOffsetFraction != 0f -> SearchBarLevel.Raised
|
||||||
!isSearchOpen && isWidgetsScrollZero && fillClockHeight -> SearchBarLevel.Resting
|
!isSearchOpen && isWidgetsScrollZero && fillClockHeight -> SearchBarLevel.Resting
|
||||||
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
|
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
|
||||||
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
|
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
|
||||||
@ -472,6 +481,7 @@ fun PagerScaffold(
|
|||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.imePadding()
|
.imePadding()
|
||||||
|
.offset { IntOffset(0, if (focusSearchBar) 0 else searchBarOffset.value.toInt() * if (bottomSearchBar) 1 else -1) }
|
||||||
.offset(y = widgetEditModeOffset),
|
.offset(y = widgetEditModeOffset),
|
||||||
level = { searchBarLevel },
|
level = { searchBarLevel },
|
||||||
focused = focusSearchBar,
|
focused = focusSearchBar,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user