mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Add scrollbar indicator to LazyColumn (#7164)
This commit is contained in:
		| @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Settings | ||||
| @@ -52,6 +51,7 @@ import eu.kanade.presentation.components.DIVIDER_ALPHA | ||||
| import eu.kanade.presentation.components.Divider | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| @@ -80,7 +80,7 @@ fun ExtensionDetailsScreen( | ||||
|  | ||||
|     var showNsfwWarning by remember { mutableStateOf(false) } | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Close | ||||
| @@ -41,6 +40,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh | ||||
| import com.google.accompanist.swiperefresh.rememberSwipeRefreshState | ||||
| import eu.kanade.presentation.browse.components.BaseBrowseItem | ||||
| import eu.kanade.presentation.browse.components.ExtensionIcon | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.components.SwipeRefreshIndicator | ||||
| import eu.kanade.presentation.theme.header | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| @@ -113,7 +113,7 @@ fun ExtensionContent( | ||||
| ) { | ||||
|     var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, | ||||
|     ) { | ||||
|         items( | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package eu.kanade.presentation.browse | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| @@ -15,6 +14,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.manga.components.BaseMangaListItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState | ||||
| @@ -54,7 +54,7 @@ fun MigrateMangaContent( | ||||
|         EmptyScreen(textResource = R.string.empty_screen) | ||||
|         return | ||||
|     } | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| @@ -21,6 +20,7 @@ import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.ItemBadges | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.theme.header | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.presentation.util.plus | ||||
| @@ -62,7 +62,7 @@ fun MigrateSourceList( | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, | ||||
|     ) { | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package eu.kanade.presentation.browse | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.Checkbox | ||||
| import androidx.compose.material3.Switch | ||||
| @@ -20,6 +19,7 @@ import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourceFilterState | ||||
| @@ -60,7 +60,7 @@ fun SourcesFilterContent( | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PushPin | ||||
| @@ -36,6 +35,7 @@ import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.theme.header | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.presentation.util.plus | ||||
| @@ -87,7 +87,7 @@ fun SourceList( | ||||
|  | ||||
|     var sourceState by remember { mutableStateOf<Source?>(null) } | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollConnection), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, | ||||
|     ) { | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.gestures.FlingBehavior | ||||
| import androidx.compose.foundation.gestures.ScrollableDefaults | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.calculateEndPadding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.LazyListScope | ||||
| import androidx.compose.foundation.lazy.LazyListState | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.util.drawVerticalScrollbar | ||||
|  | ||||
| /** | ||||
|  * LazyColumn with scrollbar. | ||||
|  */ | ||||
| @Composable | ||||
| fun ScrollbarLazyColumn( | ||||
|     modifier: Modifier = Modifier, | ||||
|     state: LazyListState = rememberLazyListState(), | ||||
|     contentPadding: PaddingValues = PaddingValues(0.dp), | ||||
|     reverseLayout: Boolean = false, | ||||
|     verticalArrangement: Arrangement.Vertical = | ||||
|         if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, | ||||
|     horizontalAlignment: Alignment.Horizontal = Alignment.Start, | ||||
|     flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), | ||||
|     userScrollEnabled: Boolean = true, | ||||
|     content: LazyListScope.() -> Unit, | ||||
| ) { | ||||
|     val direction = LocalLayoutDirection.current | ||||
|     val density = LocalDensity.current | ||||
|     val positionOffset = remember(contentPadding) { | ||||
|         with(density) { contentPadding.calculateEndPadding(direction).toPx() } | ||||
|     } | ||||
|     LazyColumn( | ||||
|         modifier = modifier | ||||
|             .drawVerticalScrollbar( | ||||
|                 state = state, | ||||
|                 reverseScrolling = reverseLayout, | ||||
|                 positionOffsetPx = positionOffset, | ||||
|             ), | ||||
|         state = state, | ||||
|         contentPadding = contentPadding, | ||||
|         reverseLayout = reverseLayout, | ||||
|         verticalArrangement = verticalArrangement, | ||||
|         horizontalAlignment = horizontalAlignment, | ||||
|         flingBehavior = flingBehavior, | ||||
|         userScrollEnabled = userScrollEnabled, | ||||
|         content = content, | ||||
|     ) | ||||
| } | ||||
| @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.foundation.selection.toggleable | ||||
| import androidx.compose.material.icons.Icons | ||||
| @@ -45,6 +44,7 @@ import eu.kanade.domain.history.model.HistoryWithRelations | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.MangaCover | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.presentation.util.plus | ||||
| import eu.kanade.presentation.util.topPaddingValues | ||||
| @@ -107,7 +107,7 @@ fun HistoryContent( | ||||
|     var removeState by remember { mutableStateOf<HistoryWithRelations?>(null) } | ||||
|  | ||||
|     val scrollState = rememberLazyListState() | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier | ||||
|             .nestedScroll(nestedScroll), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package eu.kanade.presentation.more | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.CloudOff | ||||
| import androidx.compose.material.icons.outlined.GetApp | ||||
| @@ -24,6 +23,7 @@ import androidx.compose.ui.res.painterResource | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.Divider | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.components.SwitchPreference | ||||
| import eu.kanade.presentation.util.quantityStringResource | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -44,7 +44,7 @@ fun MoreScreen( | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|     val downloadQueueState by presenter.downloadQueueState.collectAsState() | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Public | ||||
| import androidx.compose.runtime.Composable | ||||
| @@ -20,6 +19,7 @@ import androidx.compose.ui.res.painterResource | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.LinkIcon | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.more.LogoHeader | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -37,7 +37,7 @@ fun AboutScreen( | ||||
|     val context = LocalContext.current | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|  | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.painter.Painter | ||||
| @@ -12,13 +11,14 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
|  | ||||
| @Composable | ||||
| fun SettingsMainScreen( | ||||
|     nestedScrollInterop: NestedScrollConnection, | ||||
|     sections: List<SettingsSection>, | ||||
| ) { | ||||
|     LazyColumn( | ||||
|     ScrollbarLazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|   | ||||
							
								
								
									
										257
									
								
								app/src/main/java/eu/kanade/presentation/util/Scrollbar.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								app/src/main/java/eu/kanade/presentation/util/Scrollbar.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,257 @@ | ||||
| package eu.kanade.presentation.util | ||||
|  | ||||
| /* | ||||
|  * MIT License | ||||
|  * | ||||
|  * Copyright (c) 2022 Albert Chang | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Code taken from https://gist.github.com/mxalbert1996/33a360fcab2105a31e5355af98216f5a | ||||
|  * with some modifications to handle contentPadding. | ||||
|  * | ||||
|  * Modifiers for regular scrollable list is omitted. | ||||
|  */ | ||||
|  | ||||
| import android.view.ViewConfiguration | ||||
| import androidx.compose.animation.core.Animatable | ||||
| import androidx.compose.animation.core.tween | ||||
| import androidx.compose.foundation.gestures.Orientation | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.LazyListState | ||||
| import androidx.compose.foundation.lazy.LazyRow | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.composed | ||||
| import androidx.compose.ui.draw.CacheDrawScope | ||||
| import androidx.compose.ui.draw.DrawResult | ||||
| import androidx.compose.ui.draw.drawWithCache | ||||
| import androidx.compose.ui.geometry.Offset | ||||
| import androidx.compose.ui.geometry.Size | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.drawscope.DrawScope | ||||
| 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.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.LayoutDirection | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.util.fastSumBy | ||||
| import kotlinx.coroutines.channels.BufferOverflow | ||||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
|  | ||||
| fun Modifier.drawHorizontalScrollbar( | ||||
|     state: LazyListState, | ||||
|     reverseScrolling: Boolean = false, | ||||
|     // The amount of offset the scrollbar position towards the top of the layout | ||||
|     positionOffsetPx: Float = 0f, | ||||
| ): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling, positionOffsetPx) | ||||
|  | ||||
| fun Modifier.drawVerticalScrollbar( | ||||
|     state: LazyListState, | ||||
|     reverseScrolling: Boolean = false, | ||||
|     // The amount of offset the scrollbar position towards the start of the layout | ||||
|     positionOffsetPx: Float = 0f, | ||||
| ): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling, positionOffsetPx) | ||||
|  | ||||
| private fun Modifier.drawScrollbar( | ||||
|     state: LazyListState, | ||||
|     orientation: Orientation, | ||||
|     reverseScrolling: Boolean, | ||||
|     positionOffset: Float, | ||||
| ): Modifier = drawScrollbar( | ||||
|     orientation, reverseScrolling, | ||||
| ) { reverseDirection, atEnd, thickness, color, alpha -> | ||||
|     val layoutInfo = state.layoutInfo | ||||
|     val viewportSize = if (orientation == Orientation.Horizontal) { | ||||
|         layoutInfo.viewportSize.width | ||||
|     } else { | ||||
|         layoutInfo.viewportSize.height | ||||
|     } - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding | ||||
|     val items = layoutInfo.visibleItemsInfo | ||||
|     val itemsSize = items.fastSumBy { it.size } | ||||
|     val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize | ||||
|     val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size | ||||
|     val totalSize = estimatedItemSize * layoutInfo.totalItemsCount | ||||
|     val thumbSize = viewportSize / totalSize * viewportSize | ||||
|     val startOffset = if (items.isEmpty()) 0f else items | ||||
|         .first() | ||||
|         .run { | ||||
|             val startPadding = if (reverseDirection) layoutInfo.afterContentPadding else layoutInfo.beforeContentPadding | ||||
|             startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize) | ||||
|         } | ||||
|     val drawScrollbar = onDrawScrollbar( | ||||
|         orientation, reverseDirection, atEnd, showScrollbar, | ||||
|         thickness, color, alpha, thumbSize, startOffset, positionOffset, | ||||
|     ) | ||||
|     onDrawWithContent { | ||||
|         drawContent() | ||||
|         drawScrollbar() | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun CacheDrawScope.onDrawScrollbar( | ||||
|     orientation: Orientation, | ||||
|     reverseDirection: Boolean, | ||||
|     atEnd: Boolean, | ||||
|     showScrollbar: Boolean, | ||||
|     thickness: Float, | ||||
|     color: Color, | ||||
|     alpha: () -> Float, | ||||
|     thumbSize: Float, | ||||
|     scrollOffset: Float, | ||||
|     positionOffset: Float, | ||||
| ): DrawScope.() -> Unit { | ||||
|     val topLeft = if (orientation == Orientation.Horizontal) { | ||||
|         Offset( | ||||
|             if (reverseDirection) size.width - scrollOffset - thumbSize else scrollOffset, | ||||
|             if (atEnd) size.height - positionOffset - thickness else positionOffset, | ||||
|         ) | ||||
|     } else { | ||||
|         Offset( | ||||
|             if (atEnd) size.width - positionOffset - thickness else positionOffset, | ||||
|             if (reverseDirection) size.height - scrollOffset - thumbSize else scrollOffset, | ||||
|         ) | ||||
|     } | ||||
|     val size = if (orientation == Orientation.Horizontal) { | ||||
|         Size(thumbSize, thickness) | ||||
|     } else { | ||||
|         Size(thickness, thumbSize) | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         if (showScrollbar) { | ||||
|             drawRect( | ||||
|                 color = color, | ||||
|                 topLeft = topLeft, | ||||
|                 size = size, | ||||
|                 alpha = alpha(), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun Modifier.drawScrollbar( | ||||
|     orientation: Orientation, | ||||
|     reverseScrolling: Boolean, | ||||
|     onBuildDrawCache: CacheDrawScope.( | ||||
|         reverseDirection: Boolean, | ||||
|         atEnd: Boolean, | ||||
|         thickness: Float, | ||||
|         color: Color, | ||||
|         alpha: () -> Float, | ||||
|     ) -> DrawResult, | ||||
| ): Modifier = composed { | ||||
|     val scrolled = remember { | ||||
|         MutableSharedFlow<Unit>( | ||||
|             extraBufferCapacity = 1, | ||||
|             onBufferOverflow = BufferOverflow.DROP_OLDEST, | ||||
|         ) | ||||
|     } | ||||
|     val nestedScrollConnection = remember(orientation, scrolled) { | ||||
|         object : NestedScrollConnection { | ||||
|             override fun onPostScroll( | ||||
|                 consumed: Offset, | ||||
|                 available: Offset, | ||||
|                 source: NestedScrollSource, | ||||
|             ): Offset { | ||||
|                 val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y | ||||
|                 if (delta != 0f) scrolled.tryEmit(Unit) | ||||
|                 return Offset.Zero | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val alpha = remember { Animatable(0f) } | ||||
|     LaunchedEffect(scrolled, alpha) { | ||||
|         scrolled.collectLatest { | ||||
|             alpha.snapTo(1f) | ||||
|             alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr | ||||
|     val reverseDirection = if (orientation == Orientation.Horizontal) { | ||||
|         if (isLtr) reverseScrolling else !reverseScrolling | ||||
|     } else reverseScrolling | ||||
|     val atEnd = if (orientation == Orientation.Vertical) isLtr else true | ||||
|  | ||||
|     val context = LocalContext.current | ||||
|     val thickness = remember { ViewConfiguration.get(context).scaledScrollBarSize.toFloat() } | ||||
|     val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.364f) | ||||
|     Modifier | ||||
|         .nestedScroll(nestedScrollConnection) | ||||
|         .drawWithCache { | ||||
|             onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha::value) | ||||
|         } | ||||
| } | ||||
|  | ||||
| private val FadeOutAnimationSpec = tween<Float>( | ||||
|     durationMillis = ViewConfiguration.getScrollBarFadeDuration(), | ||||
|     delayMillis = ViewConfiguration.getScrollDefaultDelay(), | ||||
| ) | ||||
|  | ||||
| @Preview(widthDp = 400, heightDp = 400, showBackground = true) | ||||
| @Composable | ||||
| fun LazyListScrollbarPreview() { | ||||
|     val state = rememberLazyListState() | ||||
|     LazyColumn( | ||||
|         modifier = Modifier.drawVerticalScrollbar(state), | ||||
|         state = state, | ||||
|     ) { | ||||
|         items(50) { | ||||
|             Text( | ||||
|                 text = "Item ${it + 1}", | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(16.dp), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Preview(widthDp = 400, showBackground = true) | ||||
| @Composable | ||||
| fun LazyListHorizontalScrollbarPreview() { | ||||
|     val state = rememberLazyListState() | ||||
|     LazyRow( | ||||
|         modifier = Modifier.drawHorizontalScrollbar(state), | ||||
|         state = state, | ||||
|     ) { | ||||
|         items(50) { | ||||
|             Text( | ||||
|                 text = (it + 1).toString(), | ||||
|                 modifier = Modifier | ||||
|                     .padding(horizontal = 8.dp, vertical = 16.dp), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user