TP-79343 | common implementation for scroll to top in scrollable lists (#12342)

This commit is contained in:
Soumya Ranjan Patra
2024-09-05 16:00:47 +05:30
committed by GitHub
parent 17186d8814
commit 606d04c04f
8 changed files with 119 additions and 34 deletions

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -68,7 +69,10 @@ fun FrontLayerContent(
) {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(
modifier.semantics { testTagsAsResourceId = true }.testTag("homeScreenWidgetsList")
modifier
.verticalScroll(homeScrollState())
.semantics { testTagsAsResourceId = true }
.testTag("homeScreenWidgetsList")
) {
if (isShimmerVisible.not()) {
val widgetOrderList = homeVM.getHomeContentList()

View File

@@ -14,6 +14,7 @@ import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.commoncomposables.utils.resetScrollToTop
import com.navi.common.model.common.AppUpdateData
import com.navi.common.utils.Constants.APP_UPDATE_DATA
import com.navi.common.utils.Constants.CARD_NAME
@@ -26,6 +27,7 @@ import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.model.BottomStickyNudgeState
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.model.BottomNavBarStateHolder
import com.naviapp.home.model.HomeBottomNavBarData
import com.naviapp.home.viewmodel.HomeVM
@@ -123,6 +125,9 @@ fun onTabClick(
tabId: String,
isResetCall: Boolean = false
) {
if (selectedTabId == tabId && selectedTabId == BottomBarTabType.HOME.name) {
sharedVM.resetScrollToTop(true, tabId)
}
if (selectedTabId != tabId || isResetCall) {
navController.navigate(tabId) {
navController.graph.startDestinationRoute?.let { route ->

View File

@@ -7,6 +7,7 @@
package com.naviapp.home.compose.home.ui.screen
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -15,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropScaffoldState
import androidx.compose.material.BackdropValue
@@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -72,8 +71,8 @@ fun HomeScreen(
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit
) {
val homeScreenData by homeVM.homeScreenState.collectAsStateWithLifecycle()
val context = LocalContext.current as HomePageActivity
val stickyBottomNudgeListener: StickyBottomNudgeListener = remember { context }
val stickyBottomNudgeListener: StickyBottomNudgeListener = remember { activity }
val homeScrollState = rememberScrollState()
InitHomeScreenComponents(
activity = activity,
@@ -85,21 +84,29 @@ fun HomeScreen(
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
callBackToActivityScreen = callBackToActivityScreen,
stickyBottomNudgeListener = stickyBottomNudgeListener,
inAppUpdateVM = inAppUpdateVM
inAppUpdateVM = inAppUpdateVM,
homeScrollState = { homeScrollState },
)
when (homeScreenData) {
HomeScreenState.Loading -> {
HomeScreenScaffoldRoot(
screenDefinition = AlchemistScreenDefinition(),
homeVM = homeVM
homeVM = homeVM,
homeScrollState = { homeScrollState },
) {
true
}
}
is HomeScreenState.Success -> {
val screenDefinition = (homeScreenData as HomeScreenState.Success).data
HomeScreenScaffoldRoot(screenDefinition = screenDefinition, homeVM = homeVM) { false }
HomeScreenScaffoldRoot(
screenDefinition = screenDefinition,
homeVM = homeVM,
homeScrollState = { homeScrollState }
) {
false
}
handleHomeFooterRendering(
screenDefinition = screenDefinition,
sharedVM = sharedVM,
@@ -126,9 +133,10 @@ fun HomeScreen(
private fun HomeScreenScaffoldRoot(
screenDefinition: AlchemistScreenDefinition,
homeVM: HomeVM,
homeScrollState: () -> ScrollState,
showShadowOnFrontLayer: () -> Boolean,
) {
val homeScrollState = rememberScrollState()
val density = LocalDensity.current
val statusBarHeight = remember { with(density) { getStatusBarHeight().toDp() } }
val appBarHeight = remember { HOME_APP_BAR_HEIGHT + statusBarHeight }
@@ -151,7 +159,7 @@ private fun HomeScreenScaffoldRoot(
statusBarHeight = statusBarHeight,
topBarData = screenDefinition.screenStructure?.collapsingToolbar,
homeVM = homeVM,
homeScrollState = { homeScrollState }
homeScrollState = homeScrollState
)
},
backLayer = {
@@ -159,19 +167,16 @@ private fun HomeScreenScaffoldRoot(
modifier = Modifier.requiredHeight(backLayerHeight + HOME_SHAPE_CURVATURE),
backLayerData = screenDefinition.screenStructure?.collapsingToolbar,
homeVM = homeVM,
homeScrollState = { homeScrollState }
homeScrollState = homeScrollState
)
},
frontLayer = {
FrontLayerContent(
modifier =
Modifier.fillMaxHeight()
.background(Color.White, frontLayerShape)
.verticalScroll(homeScrollState),
modifier = Modifier.fillMaxHeight().background(Color.White, frontLayerShape),
isShimmerVisible =
screenDefinition.screenStructure?.content?.widgets.isNullOrEmpty(),
homeVM = homeVM,
homeScrollState = { homeScrollState }
homeScrollState = homeScrollState
)
}
)

View File

@@ -8,8 +8,10 @@
package com.naviapp.home.compose.home.utils
import android.widget.Toast
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.navi.common.commoncomposables.utils.ScrollToTopListener
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.insurance.util.observeNonNull
import com.navi.naviwidgets.utils.toCtaData
@@ -21,6 +23,7 @@ import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
@@ -38,7 +41,8 @@ fun InitHomeScreenComponents(
naviAnalyticsEventTracker: NaviAnalytics.Home,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit,
stickyBottomNudgeListener: StickyBottomNudgeListener?,
inAppUpdateVM: InAppUpdateVM
inAppUpdateVM: InAppUpdateVM,
homeScrollState: () -> ScrollState,
) {
InitLifecycleListener(
@@ -109,4 +113,10 @@ fun InitHomeScreenComponents(
handleCtaAction(event, homeVM, sharedVM, activity, callBackToActivityScreen)
}
}
ScrollToTopListener(
scrollState = homeScrollState,
key = BottomBarTabType.HOME.name,
viewModel = sharedVM,
)
}

View File

@@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.base.utils.orTrue
import com.navi.common.commoncomposables.utils.ScrollToTopListener
import com.navi.common.ui.compose.DrawerState
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.render.UiTronRenderer
@@ -68,18 +69,7 @@ fun ProfileScreen(
drawerState = drawerState
)
LaunchedEffect(Unit) {
profileVM.resetProfileScrollToTop.collect { resetScrollPosition ->
if (resetScrollPosition) {
try {
profileVM.resetProfileScrollToTop(reset = false)
scrollState.scrollTo(0)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
ScrollToTopListener(viewModel = profileVM, key = PROFILE, scrollState = { scrollState })
profileItems.value.let { profileData ->
when (profileData) {

View File

@@ -12,6 +12,7 @@ import com.google.gson.Gson
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.cache.util.NaviSharedDbKeys
import com.navi.common.commoncomposables.utils.resetScrollToTop
import com.navi.common.network.models.RepoResult
import com.navi.common.utils.BiometricPromptUtils
import com.navi.common.utils.Constants.ScreenLockConstants.DISABLED
@@ -29,6 +30,7 @@ import com.naviapp.home.respository.ProfileRepository
import com.naviapp.home.ui.state.ProfileScreenState
import com.naviapp.network.di.DataDeserializers
import com.naviapp.network.di.DataSerializers
import com.naviapp.utils.Constants.PROFILE
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
@@ -53,9 +55,6 @@ constructor(
MutableStateFlow<ProfileScreenState>(ProfileScreenState.Loading)
val profileScreenDataState = _profileScreenDataState.asStateFlow()
private val _resetProfileScrollToTop = MutableSharedFlow<Boolean>()
val resetProfileScrollToTop = _resetProfileScrollToTop.asSharedFlow()
private val _isScreenLockToggled = MutableSharedFlow<String?>()
val isScreenLockToggled = _isScreenLockToggled.asSharedFlow()
@@ -172,6 +171,6 @@ constructor(
}
fun resetProfileScrollToTop(reset: Boolean) {
viewModelScope.launch { _resetProfileScrollToTop.emit(reset) }
resetScrollToTop(reset, PROFILE)
}
}

View File

@@ -0,0 +1,72 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.commoncomposables.utils
import androidx.compose.animation.core.Spring.StiffnessVeryLow
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.navi.base.utils.orFalse
import com.navi.common.constants.SCROLL_TO_TOP_STATE_KEY
import com.navi.uitron.viewmodel.UiTronViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* ScrollToTopListener listens to the scroll state and scrolls to the top of the list when the value
* is true.
*
* @param key: A unique key to identify the composable.
* @param scrollState: Could be a LazyListState or ScrollState.
* @param viewModel: ViewModel to handle the state. This must extend UiTronViewModel.
*/
@Composable
fun ScrollToTopListener(
key: String,
scrollState: () -> ScrollableState,
viewModel: UiTronViewModel,
) {
var job: Job? = null
LaunchedEffect(Unit) {
viewModel.handle.getStateFlow<Boolean?>(getScrollToTopStateKey(key), null).collect { value
->
// Scroll to top when the value is true and job is not in progress
if (value.orFalse() && job?.isActive.orFalse().not()) {
job = launch {
when (scrollState()) {
is LazyListState -> {
(scrollState() as LazyListState).animateScrollToItem(0)
}
is ScrollState -> {
(scrollState() as ScrollState).animateScrollTo(
0,
animationSpec =
spring(
stiffness = StiffnessVeryLow,
)
)
}
}
job?.cancel()
}
}
viewModel.resetScrollToTop(null, key)
}
}
}
private fun getScrollToTopStateKey(key: String): String {
return SCROLL_TO_TOP_STATE_KEY + key
}
fun UiTronViewModel.resetScrollToTop(value: Boolean?, key: String) {
handle[getScrollToTopStateKey(key)] = value
}

View File

@@ -14,7 +14,6 @@ const val PREVIOUS_SCREEN = "PREVIOUS_SCREEN"
const val NPS_SUBMIT_DIALOG = "NPS_SUBMIT_DIALOG"
const val COMMON_DIALOG_BOX = "COMMON_DIALOG_BOX"
const val CONGRATULATORY_OFFER_DIALOG = "CONGRATULATORY_OFFER_DIALOG"
const val ICON_CHAT_DOT = "CHAT_DOT"
const val RAGE_TAP_COUNT = 3
const val RAGE_TAP_TIME_DIFF = 2000L
const val RAGE_TAP_CACHE_DATA = "RAGE_TAP_CACHE_DATA"
@@ -31,6 +30,7 @@ const val ENABLED_SMALL = "enabled"
const val DISABLED_SMALL = "disabled"
const val APP_UPGRADE_DATA = "APP_UPGRADE_DATA"
const val QA = "qa"
const val SCROLL_TO_TOP_STATE_KEY = "ScrollToTopStateKey_"
// Chat constants
const val HELP_CTA_TEXT = "HELP"