Use pager for pull down scaffold
This commit is contained in:
parent
db28ce32ca
commit
dfc52a3c38
@ -2,8 +2,9 @@ package de.mm20.launcher2.ui.launcher
|
|||||||
|
|
||||||
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.Spring
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.spring
|
||||||
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
|
||||||
@ -21,11 +22,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
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.requiredHeight
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.VerticalPager
|
||||||
|
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.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -100,11 +102,14 @@ fun PullDownScaffold(
|
|||||||
val widgetsScrollState = rememberScrollState()
|
val widgetsScrollState = rememberScrollState()
|
||||||
val searchState = rememberLazyListState()
|
val searchState = rememberLazyListState()
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
|
||||||
val isSearchAtTop by remember {
|
val isSearchAtTop by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (reverseSearchResults) {
|
if (reverseSearchResults) {
|
||||||
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
|
||||||
} else {
|
} else {
|
||||||
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
||||||
@ -118,7 +123,8 @@ fun PullDownScaffold(
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,9 +222,22 @@ fun PullDownScaffold(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(isSearchOpen) {
|
LaunchedEffect(isSearchOpen) {
|
||||||
if (isSearchOpen) searchState.scrollToItem(0)
|
launch {
|
||||||
if (!isSearchOpen) searchVM.search("")
|
searchBarOffset.animateTo(0f)
|
||||||
searchBarOffset.animateTo(0f)
|
}
|
||||||
|
if (isSearchOpen) {
|
||||||
|
searchState.scrollToItem(0)
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
1,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
searchVM.search("")
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
0,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isWidgetEditMode) {
|
LaunchedEffect(isWidgetEditMode) {
|
||||||
@ -250,8 +269,8 @@ fun PullDownScaffold(
|
|||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
if (isWidgetEditMode) return Offset.Zero
|
if (isWidgetEditMode || source != NestedScrollSource.Drag) return Offset.Zero
|
||||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
if (available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
}
|
}
|
||||||
val canPullDown = if (isSearchOpen) {
|
val canPullDown = if (isSearchOpen) {
|
||||||
@ -285,8 +304,10 @@ fun PullDownScaffold(
|
|||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource
|
source: NestedScrollSource
|
||||||
): Offset {
|
): Offset {
|
||||||
val deltaSearchBarOffset = consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
|
val deltaSearchBarOffset =
|
||||||
searchBarOffset.value = (searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
|
||||||
|
searchBarOffset.value =
|
||||||
|
(searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||||
return super.onPostScroll(consumed, available, source)
|
return super.onPostScroll(consumed, available, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +342,6 @@ fun PullDownScaffold(
|
|||||||
.offset { IntOffset(0, offsetY.value.toInt()) },
|
.offset { IntOffset(0, offsetY.value.toInt()) },
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -332,109 +352,114 @@ fun PullDownScaffold(
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalOverscrollConfiguration provides null
|
LocalOverscrollConfiguration provides null
|
||||||
) {
|
) {
|
||||||
val offset by animateFloatAsState(if (isSearchOpen) 1f else 0f)
|
VerticalPager(
|
||||||
Column(
|
modifier = Modifier.fillMaxSize(),
|
||||||
modifier = Modifier
|
pageCount = 2,
|
||||||
.fillMaxWidth()
|
beyondBoundsPageCount = 1,
|
||||||
.requiredHeight(height * 2)
|
state = pagerState,
|
||||||
.offset {
|
reverseLayout = true,
|
||||||
IntOffset(
|
userScrollEnabled = false,
|
||||||
0,
|
|
||||||
((-0.5f + offset) * height.toPx()).roundToInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
val webSearchPadding by animateDpAsState(
|
when (it) {
|
||||||
if (actions.isEmpty()) 0.dp else 48.dp
|
|
||||||
)
|
|
||||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
|
||||||
SearchColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.graphicsLayer {
|
|
||||||
transformOrigin = TransformOrigin.Center
|
|
||||||
scaleX = offset
|
|
||||||
scaleY = offset
|
|
||||||
alpha = offset
|
|
||||||
}
|
|
||||||
.fillMaxWidth()
|
|
||||||
.requiredHeight(height)
|
|
||||||
.padding(
|
|
||||||
start = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
|
||||||
end = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
|
||||||
),
|
|
||||||
paddingValues = PaddingValues(
|
|
||||||
top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 60.dp + webSearchPadding else 4.dp,
|
|
||||||
bottom = windowInsets.calculateBottomPadding() + if (bottomSearchBar) 60.dp + webSearchPadding else 4.dp
|
|
||||||
),
|
|
||||||
state = searchState,
|
|
||||||
reverse = reverseSearchResults,
|
|
||||||
)
|
|
||||||
val clockPadding by animateDpAsState(
|
|
||||||
if (isWidgetsAtStart && fillClockHeight)
|
|
||||||
insets.calculateBottomPadding() + if (bottomSearchBar) 64.dp else 0.dp
|
|
||||||
else 0.dp
|
|
||||||
|
|
||||||
)
|
0 -> {
|
||||||
val clockHeight by remember {
|
val clockPadding by animateDpAsState(
|
||||||
derivedStateOf {
|
if (isWidgetsAtStart && fillClockHeight)
|
||||||
if (fillClockHeight) {
|
insets.calculateBottomPadding() + if (bottomSearchBar) 64.dp else 0.dp
|
||||||
height - (insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding + 56.dp)
|
else 0.dp
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
)
|
||||||
modifier = Modifier
|
val clockHeight by remember {
|
||||||
.graphicsLayer {
|
derivedStateOf {
|
||||||
transformOrigin = TransformOrigin.Center
|
if (fillClockHeight) {
|
||||||
scaleX = 1 - offset
|
height - (insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding + 56.dp)
|
||||||
scaleY = 1 - offset
|
} else {
|
||||||
alpha = 1 - offset
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.pointerInput(Unit) {
|
Column(
|
||||||
detectTapGestures(
|
modifier = Modifier
|
||||||
onDoubleTap = {
|
.graphicsLayer {
|
||||||
gestureManager.dispatchDoubleTap(it)
|
val progress = pagerState.currentPage + pagerState.currentPageOffsetFraction
|
||||||
},
|
transformOrigin = TransformOrigin.Center
|
||||||
onLongPress = {
|
alpha = 1 - progress
|
||||||
gestureManager.dispatchLongPress(it)
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
gestureManager.dispatchDoubleTap(it)
|
||||||
|
},
|
||||||
|
onLongPress = {
|
||||||
|
gestureManager.dispatchLongPress(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(widgetsScrollState)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
|
.padding(8.dp)
|
||||||
|
.padding(
|
||||||
|
top = if (bottomSearchBar) 0.dp else 56.dp,
|
||||||
|
bottom = if (bottomSearchBar) 56.dp else 0.dp,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(!isWidgetEditMode) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(clockHeight?.let { Modifier.height(it) }
|
||||||
|
?: Modifier)
|
||||||
|
.padding(bottom = clockPadding),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
ClockWidget(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WidgetColumn(
|
||||||
|
editMode = isWidgetEditMode,
|
||||||
|
onEditModeChange = {
|
||||||
|
viewModel.setWidgetEditMode(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
|
||||||
.requiredHeight(height)
|
|
||||||
.verticalScroll(widgetsScrollState)
|
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
|
||||||
.padding(8.dp)
|
|
||||||
.padding(
|
|
||||||
top = if (bottomSearchBar) 0.dp else 56.dp,
|
|
||||||
bottom = if (bottomSearchBar) 56.dp else 0.dp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(!isWidgetEditMode) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
|
|
||||||
.padding(bottom = clockPadding),
|
|
||||||
contentAlignment = Alignment.BottomCenter
|
|
||||||
) {
|
|
||||||
ClockWidget(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
WidgetColumn(
|
|
||||||
editMode = isWidgetEditMode,
|
1 -> {
|
||||||
onEditModeChange = {
|
val webSearchPadding by animateDpAsState(
|
||||||
viewModel.setWidgetEditMode(it)
|
if (actions.isEmpty()) 0.dp else 48.dp
|
||||||
}
|
)
|
||||||
)
|
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
|
SearchColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
val progress = pagerState.currentPage + pagerState.currentPageOffsetFraction
|
||||||
|
transformOrigin = TransformOrigin.Center
|
||||||
|
alpha = progress
|
||||||
|
}
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = windowInsets.calculateStartPadding(
|
||||||
|
LocalLayoutDirection.current
|
||||||
|
),
|
||||||
|
end = windowInsets.calculateStartPadding(
|
||||||
|
LocalLayoutDirection.current
|
||||||
|
),
|
||||||
|
),
|
||||||
|
paddingValues = PaddingValues(
|
||||||
|
top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 60.dp + webSearchPadding else 4.dp,
|
||||||
|
bottom = windowInsets.calculateBottomPadding() + if (bottomSearchBar) 60.dp + webSearchPadding else 4.dp
|
||||||
|
),
|
||||||
|
state = searchState,
|
||||||
|
reverse = reverseSearchResults,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user