mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +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