Optimize scaffold performance

This commit is contained in:
MM20 2022-05-10 18:03:13 +02:00
parent 83a33d8ac5
commit 3e33c6362f
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
2 changed files with 103 additions and 82 deletions

View File

@ -11,6 +11,10 @@ suspend fun MutableState<Dp>.animateTo(targetValue: Dp) {
animateTo(targetValue, Dp.VectorConverter) animateTo(targetValue, Dp.VectorConverter)
} }
suspend fun MutableState<Float>.animateTo(targetValue: Float) {
animateTo(targetValue, Float.VectorConverter)
}
suspend inline fun <T, V: AnimationVector> MutableState<T>.animateTo(targetValue: T, converter: TwoWayConverter<T, V>) { suspend inline fun <T, V: AnimationVector> MutableState<T>.animateTo(targetValue: T, converter: TwoWayConverter<T, V>) {
val animatable = Animatable(this.value, converter) val animatable = Animatable(this.value, converter)
animatable.animateTo(targetValue) { animatable.animateTo(targetValue) {

View File

@ -4,8 +4,6 @@ import android.app.Activity
import android.view.WindowManager import android.view.WindowManager
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.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateDpAsState 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
@ -22,20 +20,22 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
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.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
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.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
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.NestedScrollDispatcher
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.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.VerticalPager import com.google.accompanist.pager.VerticalPager
@ -51,10 +51,8 @@ import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
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
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.ranges.coerceIn
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
@ -67,6 +65,8 @@ fun PullDownScaffold(
val searchVM: SearchVM = viewModel() val searchVM: SearchVM = viewModel()
val context = LocalContext.current val context = LocalContext.current
val density = LocalDensity.current
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false) val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false) val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
@ -98,18 +98,25 @@ fun PullDownScaffold(
) )
} }
val dp = LocalDensity.current.density val offsetY = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0.dp) } val maxOffset = with(density) { 64.dp.toPx() }
var searchBarOffset by remember { mutableStateOf(0.dp) } val toggleSearchThreshold = with(density) { 48.dp.toPx() }
val searchBarOffset = remember { mutableStateOf(0f) }
val maxSearchBarOffset = with(density) { 96.dp.toPx() }
val blurWallpaper by derivedStateOf {
isSearchOpen || offsetY.value > toggleSearchThreshold || widgetsScrollState.value > 0
}
val blurWallpaper = isSearchOpen || offsetY.value > 48.dp || widgetsScrollState.value > 0
LaunchedEffect(blurWallpaper) { LaunchedEffect(blurWallpaper) {
if (!isAtLeastApiLevel(31)) return@LaunchedEffect if (!isAtLeastApiLevel(31)) return@LaunchedEffect
(context as Activity).window.attributes = context.window.attributes.also { (context as Activity).window.attributes = context.window.attributes.also {
if (blurWallpaper) { if (blurWallpaper) {
it.blurBehindRadius = (32 * dp).toInt() it.blurBehindRadius = with(density) { 32.dp.toPx().toInt() }
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else { } else {
it.blurBehindRadius = 0 it.blurBehindRadius = 0
@ -119,62 +126,17 @@ fun PullDownScaffold(
} }
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val consumed = when {
isSearchOpen && (offsetY.value > 0.dp || source == NestedScrollSource.Drag && searchScrollState.value - available.y < 0) -> {
val consumed = available.y - searchScrollState.value
offsetY.value = (offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(0.dp, 64.dp)
consumed
}
isSearchOpen && (offsetY.value < 0.dp || source == NestedScrollSource.Drag && searchScrollState.value - available.y > searchScrollState.maxValue) -> {
val consumed =
available.y - (searchScrollState.maxValue - searchScrollState.value)
offsetY.value = (offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(-64.dp, 0.dp)
consumed
}
!isSearchOpen && (offsetY.value > 0.dp || source == NestedScrollSource.Drag && widgetsScrollState.value - available.y < 0) -> {
val consumed = available.y - widgetsScrollState.value
offsetY.value = (offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(0.dp, 64.dp)
consumed
}
else -> {
0f
}
}
searchBarOffset =
((searchBarOffset) + ((available.y - consumed) / dp).dp).coerceIn(-128.dp, 0.dp)
return Offset(0f, consumed)
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (offsetY.value > 48.dp || offsetY.value < -48.dp) {
viewModel.toggleSearch()
}
if (offsetY.value != 0.dp) {
offsetY.animateTo(0.dp)
return available
}
return Velocity.Zero
}
}
}
LaunchedEffect(isSearchOpen) { LaunchedEffect(isSearchOpen) {
searchScrollState.scrollTo(0) if (isSearchOpen) searchScrollState.scrollTo(0)
if (!isSearchOpen) searchVM.search("") if (!isSearchOpen) searchVM.search("")
searchBarOffset = 0.dp
pagerState.animateScrollToPage(if (isSearchOpen) 1 else 0) pagerState.animateScrollToPage(if (isSearchOpen) 1 else 0)
searchBarOffset.animateTo(0f)
} }
LaunchedEffect(isWidgetEditMode) { LaunchedEffect(isWidgetEditMode) {
if (!isWidgetEditMode) searchBarOffset = 0.dp if (!isWidgetEditMode) searchBarOffset.value = 0f
} }
BackHandler { BackHandler {
@ -194,22 +156,65 @@ fun PullDownScaffold(
} }
} }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val diff =
(if (isSearchOpen) searchScrollState.value else widgetsScrollState.value) - available.y
val consumed = when {
(offsetY.value > 0 || source == NestedScrollSource.Drag && diff < 0) -> {
val consumed = -diff
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(0f, maxOffset)
consumed
}
isSearchOpen && (offsetY.value < 0 || source == NestedScrollSource.Drag && diff > searchScrollState.maxValue) -> {
val consumed =
available.y - (searchScrollState.maxValue - searchScrollState.value)
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(-maxOffset, 0f)
consumed
}
else -> {
0f
}
}
searchBarOffset.value =
(searchBarOffset.value + (available.y - consumed)).coerceIn(
-maxSearchBarOffset,
0f
)
return Offset(0f, consumed)
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) {
viewModel.toggleSearch()
}
if (offsetY.value != 0f) {
offsetY.animateTo(0f)
return available
}
return Velocity.Zero
}
}
}
var size by remember { mutableStateOf(IntSize.Zero) } var size by remember { mutableStateOf(IntSize.Zero) }
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.clipToBounds()
.onSizeChanged { .onSizeChanged {
size = it size = it
} }
.offset(y = offsetY.value), .offset { IntOffset(0, offsetY.value.toInt()) },
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
VerticalPager( VerticalPager(
state = pagerState, state = pagerState,
modifier = Modifier modifier = Modifier
.nestedScroll(nestedScrollConnection, nestedScrollDispatcher), .nestedScroll(nestedScrollConnection),
count = 2, count = 2,
reverseLayout = true, reverseLayout = true,
userScrollEnabled = false, userScrollEnabled = false,
@ -222,8 +227,12 @@ fun PullDownScaffold(
val offset = calculateCurrentOffsetForPage(1).absoluteValue val offset = calculateCurrentOffsetForPage(1).absoluteValue
SearchColumn( SearchColumn(
modifier = Modifier modifier = Modifier
.alpha(1 - offset) .graphicsLayer {
.scale(1 - offset, TransformOrigin.Center) transformOrigin = TransformOrigin.Center
scaleX = 1 - offset
scaleY = 1 - offset
alpha = 1 - offset
}
.fillMaxSize() .fillMaxSize()
.verticalScroll(searchScrollState) .verticalScroll(searchScrollState)
.padding(8.dp) .padding(8.dp)
@ -238,8 +247,12 @@ fun PullDownScaffold(
WidgetColumn( WidgetColumn(
modifier = modifier =
Modifier Modifier
.alpha(1 - offset) .graphicsLayer {
.scale(1 - offset, TransformOrigin.Center) transformOrigin = TransformOrigin.Center
scaleX = 1 - offset
scaleY = 1 - offset
alpha = 1 - offset
}
.fillMaxSize() .fillMaxSize()
.verticalScroll(widgetsScrollState) .verticalScroll(widgetsScrollState)
.padding(8.dp) .padding(8.dp)
@ -252,20 +265,6 @@ fun PullDownScaffold(
) )
} }
} }
val searchBarLevel = when {
offsetY.value != 0.dp -> SearchBarLevel.Raised
isSearchOpen && searchScrollState.value == 0 -> SearchBarLevel.Active
isSearchOpen && searchScrollState.value > 0 -> SearchBarLevel.Raised
widgetsScrollState.value > 0 -> SearchBarLevel.Raised
else -> SearchBarLevel.Resting
}
val searchBarFocused by viewModel.searchBarFocused.observeAsState(false)
val editModeSearchBarOffset by animateDpAsState(
if (isWidgetEditMode) -128.dp else 0.dp
)
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) }
@ -281,9 +280,27 @@ fun PullDownScaffold(
} }
) )
} }
val searchBarLevel by derivedStateOf {
when {
offsetY.value != 0f -> SearchBarLevel.Raised
isSearchOpen && searchScrollState.value == 0 -> SearchBarLevel.Active
isSearchOpen && searchScrollState.value > 0 -> SearchBarLevel.Raised
widgetsScrollState.value > 0 -> SearchBarLevel.Raised
else -> SearchBarLevel.Resting
}
}
val searchBarFocused by viewModel.searchBarFocused.observeAsState(false)
val editModeSearchBarOffset by animateDpAsState(
if (isWidgetEditMode) -128.dp else 0.dp
)
SearchBar( SearchBar(
level = searchBarLevel, level = searchBarLevel,
modifier = Modifier.offset(y = searchBarOffset + editModeSearchBarOffset), modifier = Modifier
.clipToBounds()
.offset { IntOffset(0, searchBarOffset.value.toInt()) }
.offset(y = editModeSearchBarOffset),
focused = searchBarFocused, focused = searchBarFocused,
onFocusChange = { onFocusChange = {
if (it) viewModel.openSearch() if (it) viewModel.openSearch()