feat(ui): elevated style reader toolbars (#894)

* feat(ui): toolbar animation

* feat(ui): elevated variant for reader toolbars
This commit is contained in:
junkfood 2024-11-22 23:34:36 +09:00 committed by GitHub
parent 629f489ee3
commit 6e55c12ca8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 66 additions and 64 deletions

View File

@ -15,42 +15,30 @@ val LocalReadingPageTonalElevation =
compositionLocalOf<ReadingPageTonalElevationPreference> { ReadingPageTonalElevationPreference.default } compositionLocalOf<ReadingPageTonalElevationPreference> { ReadingPageTonalElevationPreference.default }
sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() { sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() {
object Level0 : ReadingPageTonalElevationPreference(ElevationTokens.Level0) data object Outlined : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
object Level1 : ReadingPageTonalElevationPreference(ElevationTokens.Level1) data object Elevated : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
object Level2 : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
object Level3 : ReadingPageTonalElevationPreference(ElevationTokens.Level3)
object Level4 : ReadingPageTonalElevationPreference(ElevationTokens.Level4)
object Level5 : ReadingPageTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
context.dataStore.put(DataStoreKey.readingPageTonalElevation, value) context.dataStore.put(readingPageTonalElevation, value)
} }
} }
fun toDesc(context: Context): String = fun toDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" Outlined -> "${ElevationTokens.Level0}dp"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" Elevated -> "${ElevationTokens.Level2}dp"
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
val default = Level0 val default = Outlined
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5) val values = listOf(Outlined, Elevated)
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) { when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) {
ElevationTokens.Level0 -> Level0 ElevationTokens.Level0 -> Outlined
ElevationTokens.Level1 -> Level1 ElevationTokens.Level2 -> Elevated
ElevationTokens.Level2 -> Level2
ElevationTokens.Level3 -> Level3
ElevationTokens.Level4 -> Level4
ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -2,10 +2,7 @@ package me.ash.reader.ui.page.home.reading
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -33,15 +30,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.infrastructure.preference.LocalReadingRenderer import me.ash.reader.infrastructure.preference.LocalReadingRenderer
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
import me.ash.reader.infrastructure.preference.ReadingRendererPreference import me.ash.reader.infrastructure.preference.ReadingRendererPreference
import me.ash.reader.ui.component.base.CanBeDisabledIconButton import me.ash.reader.ui.component.base.CanBeDisabledIconButton
import me.ash.reader.ui.component.base.RYExtensibleVisibility
import me.ash.reader.ui.component.webview.BionicReadingIcon import me.ash.reader.ui.component.webview.BionicReadingIcon
@Composable @Composable
@ -60,6 +56,7 @@ fun BottomBar(
onReadAloud: () -> Unit = {}, onReadAloud: () -> Unit = {},
) { ) {
val tonalElevation = LocalReadingPageTonalElevation.current val tonalElevation = LocalReadingPageTonalElevation.current
val isOutlined = tonalElevation == ReadingPageTonalElevationPreference.Outlined
val renderer = LocalReadingRenderer.current val renderer = LocalReadingRenderer.current
Box( Box(
@ -70,16 +67,20 @@ fun BottomBar(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isShow, visible = isShow,
enter = expandVertically(), enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically() exit = shrinkVertically(shrinkTowards = Alignment.Top)
) { ) {
val view = LocalView.current val view = LocalView.current
Column { Column {
if (isOutlined) {
HorizontalDivider( HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceContainerHighest, color = MaterialTheme.colorScheme.surfaceContainerHighest,
thickness = 0.5f.dp thickness = 0.5f.dp
) )
Surface() { }
Surface(
color = MaterialTheme.colorScheme.run { if (isOutlined) surface else surfaceContainer }
) {
// TODO: Component styles await refactoring // TODO: Component styles await refactoring
Row( Row(
modifier = Modifier modifier = Modifier

View File

@ -5,9 +5,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
@ -16,9 +14,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -28,11 +24,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -40,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
@ -123,7 +116,7 @@ fun ReadingPage(
TopBar( TopBar(
navController = navController, navController = navController,
isShow = isShowToolBar, isShow = isShowToolBar,
showDivider = showTopDivider, isScrolled = showTopDivider,
title = readerState.title, title = readerState.title,
link = readerState.link, link = readerState.link,
onClick = { bringToTop = true }, onClick = { bringToTop = true },
@ -194,7 +187,7 @@ fun ReadingPage(
showTopDivider = snapshotFlow { showTopDivider = snapshotFlow {
scrollState.value != 0 || listState.firstVisibleItemIndex != 0 scrollState.value >= 120 || listState.firstVisibleItemIndex != 0
}.collectAsStateValue(initial = false) }.collectAsStateValue(initial = false)
CompositionLocalProvider( CompositionLocalProvider(

View File

@ -1,6 +1,9 @@
package me.ash.reader.ui.page.home.reading package me.ash.reader.ui.page.home.reading
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -15,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Share
@ -27,20 +29,22 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.infrastructure.preference.LocalSharedContent import me.ash.reader.infrastructure.preference.LocalSharedContent
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
import me.ash.reader.ui.component.base.FeedbackIconButton import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -48,7 +52,7 @@ import me.ash.reader.ui.page.common.RouteName
fun TopBar( fun TopBar(
navController: NavHostController, navController: NavHostController,
isShow: Boolean, isShow: Boolean,
showDivider: Boolean = false, isScrolled: Boolean = false,
title: String? = "", title: String? = "",
link: String? = "", link: String? = "",
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
@ -56,6 +60,13 @@ fun TopBar(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val sharedContent = LocalSharedContent.current val sharedContent = LocalSharedContent.current
val isOutlined =
LocalReadingPageTonalElevation.current == ReadingPageTonalElevationPreference.Outlined
val containerColor by animateColorAsState(with(MaterialTheme.colorScheme) {
if (isOutlined || !isScrolled) surface else surfaceContainer
}, label = "", animationSpec = spring(stiffness = Spring.StiffnessMediumLow))
Box( Box(
modifier = Modifier modifier = Modifier
@ -63,29 +74,30 @@ fun TopBar(
.zIndex(1f), .zIndex(1f),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
Column(modifier = if (onClick == null) Modifier else Modifier.clickable( Column(
onClick = onClick, modifier = Modifier.drawBehind { drawRect(containerColor) }
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
) { ) {
Surface( Spacer(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height( .height(
WindowInsets.statusBars WindowInsets.statusBars
.asPaddingValues() .asPaddingValues()
.calculateTopPadding() .calculateTopPadding()
),
) )
) {}
AnimatedVisibility( AnimatedVisibility(
visible = isShow, visible = isShow,
enter = expandVertically(expandFrom = Alignment.Top), enter = expandVertically(expandFrom = Alignment.Bottom),
exit = shrinkVertically(shrinkTowards = Alignment.Top) exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
) { ) {
TopAppBar( TopAppBar(
title = {}, title = {},
modifier = Modifier, modifier = if (onClick == null) Modifier else Modifier.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
windowInsets = WindowInsets(0.dp), windowInsets = WindowInsets(0.dp),
navigationIcon = { navigationIcon = {
FeedbackIconButton( FeedbackIconButton(
@ -95,7 +107,8 @@ fun TopBar(
) { ) {
onClose() onClose()
} }
}, actions = { },
actions = {
FeedbackIconButton( FeedbackIconButton(
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Palette, imageVector = Icons.Outlined.Palette,
@ -114,10 +127,11 @@ fun TopBar(
) { ) {
sharedContent.share(context, title, link) sharedContent.share(context, title, link)
} }
}, colors = TopAppBarDefaults.topAppBarColors() },
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
) )
} }
if (showDivider) { if (isOutlined && isScrolled) {
HorizontalDivider( HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceContainerHighest, color = MaterialTheme.colorScheme.surfaceContainerHighest,
thickness = 0.5f.dp thickness = 0.5f.dp

View File

@ -204,14 +204,19 @@ fun ReadingStylePage(
onClick = { pullToSwitchArticle.toggle(context, scope) }) { onClick = { pullToSwitchArticle.toggle(context, scope) }) {
RYSwitch(activated = pullToSwitchArticle.value) RYSwitch(activated = pullToSwitchArticle.value)
} }
/* SettingItem( Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.toolbars)
)
SettingItem(
title = stringResource(R.string.tonal_elevation), title = stringResource(R.string.tonal_elevation),
desc = "${tonalElevation.value}dp", desc = "${tonalElevation.value}dp",
onClick = { onClick = {
tonalElevationDialogVisible = true tonalElevationDialogVisible = true
}, },
) {} ) {}
Spacer(modifier = Modifier.height(24.dp))*/
Spacer(modifier = Modifier.height(24.dp))
} }
// Advanced // Advanced
@ -271,7 +276,7 @@ fun ReadingStylePage(
} }
) )
/* RadioDialog( RadioDialog(
visible = tonalElevationDialogVisible, visible = tonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation), title = stringResource(R.string.tonal_elevation),
options = ReadingPageTonalElevationPreference.values.map { options = ReadingPageTonalElevationPreference.values.map {
@ -284,7 +289,7 @@ fun ReadingStylePage(
} }
) { ) {
tonalElevationDialogVisible = false tonalElevationDialogVisible = false
}*/ }
RadioDialog( RadioDialog(
visible = rendererDialogVisible, visible = rendererDialogVisible,

View File

@ -460,4 +460,5 @@
<string name="mark_as_read_on_scroll">Mark as read on scroll</string> <string name="mark_as_read_on_scroll">Mark as read on scroll</string>
<string name="become_a_sponsor">Become a sponsor</string> <string name="become_a_sponsor">Become a sponsor</string>
<string name="sponsor_desc">We build and upkeep this free, open-source app in our off-hours. If you enjoy it, please consider supporting us with a small donation! ☕️</string> <string name="sponsor_desc">We build and upkeep this free, open-source app in our off-hours. If you enjoy it, please consider supporting us with a small donation! ☕️</string>
<string name="toolbars">Toolbars</string>
</resources> </resources>