android: add FAB to scroll to top

This commit is contained in:
Harsh Shandilya 2021-09-25 19:58:55 +05:30
parent dd424d6af9
commit c0f2089482
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
4 changed files with 108 additions and 2 deletions

View file

@ -0,0 +1,53 @@
package dev.msfjarvis.claw.android.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import dev.msfjarvis.claw.android.R
import kotlinx.coroutines.launch
private const val AnimationDuration = 100
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ClawFab(
isFabVisible: Boolean,
listState: LazyListState,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
AnimatedVisibility(
visible = isFabVisible,
enter =
slideInVertically(
// Enters by sliding up from offset 0 to fullHeight.
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = AnimationDuration, easing = LinearOutSlowInEasing),
),
exit =
slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
),
modifier = Modifier.then(modifier),
) {
FloatingActionButton(onClick = { coroutineScope.launch { listState.animateScrollToItem(0) } }) {
Icon(
painter = painterResource(R.drawable.ic_arrow_upward_24),
contentDescription = null,
)
}
}
}

View file

@ -1,21 +1,33 @@
package dev.msfjarvis.claw.android.ui package dev.msfjarvis.claw.android.ui
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.primarySurface import androidx.compose.material.primarySurface
import androidx.compose.material.rememberScaffoldState import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
@ -24,6 +36,9 @@ import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.theme.LobstersTheme import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
private const val ScrollDelta = 50
@OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun LobstersApp( fun LobstersApp(
viewModel: ClawViewModel = viewModel(), viewModel: ClawViewModel = viewModel(),
@ -31,6 +46,24 @@ fun LobstersApp(
) { ) {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
val listState = rememberLazyListState()
var isFabVisible by remember { mutableStateOf(true) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
if (delta > ScrollDelta) {
isFabVisible = true
} else if (delta < -ScrollDelta) {
isFabVisible = false
}
// We didn't consume any offset here so return Offset.Zero
return Offset.Zero
}
}
}
LobstersTheme(darkTheme = isSystemInDarkTheme()) { LobstersTheme(darkTheme = isSystemInDarkTheme()) {
ProvideWindowInsets { ProvideWindowInsets {
val useDarkIcons = MaterialTheme.colors.isLight val useDarkIcons = MaterialTheme.colors.isLight
@ -44,7 +77,13 @@ fun LobstersApp(
Scaffold( Scaffold(
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
topBar = { ClawAppBar(modifier = Modifier.statusBarsPadding()) }, topBar = { ClawAppBar(modifier = Modifier.statusBarsPadding()) },
modifier = Modifier, floatingActionButton = {
ClawFab(
isFabVisible = isFabVisible,
listState = listState,
modifier = Modifier.navigationBarsPadding(),
)
},
) { ) {
val isRefreshing = items.loadState.refresh == LoadState.Loading val isRefreshing = items.loadState.refresh == LoadState.Loading
SwipeRefresh( SwipeRefresh(
@ -57,7 +96,8 @@ fun LobstersApp(
NetworkPosts( NetworkPosts(
items = items, items = items,
launchUrl = urlLauncher::launch, launchUrl = urlLauncher::launch,
modifier = Modifier.padding(top = 16.dp), listState = listState,
modifier = Modifier.padding(top = 16.dp).nestedScroll(nestedScrollConnection),
) )
} }
} }

View file

@ -2,6 +2,7 @@ package dev.msfjarvis.claw.android.ui
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -14,10 +15,12 @@ import dev.msfjarvis.claw.common.posts.LobstersCard
@Composable @Composable
fun NetworkPosts( fun NetworkPosts(
items: LazyPagingItems<LobstersPost>, items: LazyPagingItems<LobstersPost>,
listState: LazyListState,
launchUrl: (String) -> Unit, launchUrl: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
state = listState,
modifier = Modifier.then(modifier), modifier = Modifier.then(modifier),
) { ) {
items(items) { item -> items(items) { item ->

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>