diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ClawFab.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ClawFab.kt new file mode 100644 index 00000000..8b7701e3 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ClawFab.kt @@ -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, + ) + } + } +} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt index 14a85cf9..b10d0246 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt @@ -1,21 +1,33 @@ package dev.msfjarvis.claw.android.ui +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.primarySurface import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable 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.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.lifecycle.viewmodel.compose.viewModel import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.insets.ProvideWindowInsets +import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.swiperefresh.SwipeRefresh 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.urllauncher.UrlLauncher +private const val ScrollDelta = 50 + +@OptIn(ExperimentalAnimationApi::class) @Composable fun LobstersApp( viewModel: ClawViewModel = viewModel(), @@ -31,6 +46,24 @@ fun LobstersApp( ) { val systemUiController = rememberSystemUiController() 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()) { ProvideWindowInsets { val useDarkIcons = MaterialTheme.colors.isLight @@ -44,7 +77,13 @@ fun LobstersApp( Scaffold( scaffoldState = scaffoldState, topBar = { ClawAppBar(modifier = Modifier.statusBarsPadding()) }, - modifier = Modifier, + floatingActionButton = { + ClawFab( + isFabVisible = isFabVisible, + listState = listState, + modifier = Modifier.navigationBarsPadding(), + ) + }, ) { val isRefreshing = items.loadState.refresh == LoadState.Loading SwipeRefresh( @@ -57,7 +96,8 @@ fun LobstersApp( NetworkPosts( items = items, launchUrl = urlLauncher::launch, - modifier = Modifier.padding(top = 16.dp), + listState = listState, + modifier = Modifier.padding(top = 16.dp).nestedScroll(nestedScrollConnection), ) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt index d3e28af4..e2d0d25a 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/NetworkPosts.kt @@ -2,6 +2,7 @@ package dev.msfjarvis.claw.android.ui import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -14,10 +15,12 @@ import dev.msfjarvis.claw.common.posts.LobstersCard @Composable fun NetworkPosts( items: LazyPagingItems, + listState: LazyListState, launchUrl: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( + state = listState, modifier = Modifier.then(modifier), ) { items(items) { item -> diff --git a/android/src/main/res/drawable/ic_arrow_upward_24.xml b/android/src/main/res/drawable/ic_arrow_upward_24.xml new file mode 100644 index 00000000..e93a1483 --- /dev/null +++ b/android/src/main/res/drawable/ic_arrow_upward_24.xml @@ -0,0 +1,10 @@ + + +