TP-79343 | common implementation for scroll to top in scrollable lists (#12342)
This commit is contained in:
committed by
GitHub
parent
17186d8814
commit
606d04c04f
@@ -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()
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user