Add DisplayText and FeedBackIconButton

This commit is contained in:
Ash 2022-04-03 21:07:28 +08:00
parent 5f11616c6a
commit 43bbb87280
9 changed files with 262 additions and 144 deletions

View File

@ -0,0 +1,51 @@
package me.ash.reader.ui.component
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun DisplayText(
modifier: Modifier = Modifier,
text: String,
desc: String,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(
start = 24.dp,
top = 48.dp,
end = 24.dp,
bottom = 24.dp,
)
) {
Text(
text = text,
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedVisibility(
visible = desc.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text(
text = desc,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View File

@ -0,0 +1,40 @@
package me.ash.reader.ui.component
import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
@Composable
fun FeedbackIconButton(
modifier: Modifier = Modifier,
imageVector: ImageVector,
contentDescription: String?,
tint: Color = LocalContentColor.current,
isHaptic: Boolean? = true,
isSound: Boolean? = true,
onClick: () -> Unit = {},
) {
val view = LocalView.current
IconButton(
onClick = {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
},
) {
Icon(
modifier = modifier,
imageVector = imageVector,
contentDescription = contentDescription,
tint = tint,
)
}
}

View File

@ -1,11 +1,21 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.foundation.clickable import android.annotation.SuppressLint
import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.lerp
@ -48,3 +58,70 @@ fun Modifier.roundClick(onClick: () -> Unit = {}) = this
fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this
.padding(horizontal = 10.dp) .padding(horizontal = 10.dp)
.padding(top = top, bottom = bottom) .padding(top = top, bottom = bottom)
@OptIn(
ExperimentalFoundationApi::class,
androidx.compose.ui.ExperimentalComposeUiApi::class
)
@Composable
@SuppressLint("ComposableModifierFactory")
fun Modifier.combinedFeedbackClickable(
isHaptic: Boolean? = false,
isSound: Boolean? = false,
onPressDown: (() -> Unit)? = null,
onPressUp: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: (() -> Unit)? = null,
): Modifier {
val view = LocalView.current
val interactionSource = remember { MutableInteractionSource() }
return if (onPressDown != null || onPressUp != null) {
indication(interactionSource, LocalIndication.current)
.pointerInput(Unit) {
detectTapGestures(
onPress = { offset ->
onPressDown?.let {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
tryAwaitRelease()
interactionSource.emit(PressInteraction.Release(press))
it()
}
},
onTap = {
onPressUp?.let {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
it()
}
}
)
}
} else {
combinedClickable(
onClick = {
onClick?.let {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
it()
}
},
onLongClick = {
onLongClick?.let {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
it()
}
},
onDoubleClick = {
onDoubleClick?.let {
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
it()
}
},
)
}
}

View File

@ -1,9 +1,11 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.page.home
import android.view.SoundEffectConstants
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
@ -17,6 +19,8 @@ fun FilterBar(
filter: Filter, filter: Filter,
filterOnClick: (Filter) -> Unit = {}, filterOnClick: (Filter) -> Unit = {},
) { ) {
val view = LocalView.current
Box( Box(
modifier = Modifier.height(60.dp) modifier = Modifier.height(60.dp)
) { ) {
@ -45,7 +49,11 @@ fun FilterBar(
) )
}, },
selected = filter == item, selected = filter == item,
onClick = { filterOnClick(item) }, onClick = {
// view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
filterOnClick(item)
},
// colors = NavigationBarItemDefaults.colors( // colors = NavigationBarItemDefaults.colors(
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, // selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
// unselectedIconColor = MaterialTheme.colorScheme.outline, // unselectedIconColor = MaterialTheme.colorScheme.outline,

View File

@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@ -12,8 +11,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -22,7 +21,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -31,6 +29,8 @@ import androidx.work.WorkInfo
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.Subtitle import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getDesc import me.ash.reader.ui.ext.getDesc
@ -104,39 +104,35 @@ fun FeedsPage(
SmallTopAppBar( SmallTopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { FeedbackIconButton(
isHaptic = false,
imageVector = Icons.Rounded.Tune,
contentDescription = stringResource(R.string.settings),
tint = MaterialTheme.colorScheme.onSurface,
) {
onScrollToPage(0) onScrollToPage(0)
}) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
} }
}, },
actions = { actions = {
IconButton(onClick = { FeedbackIconButton(
if (!isSyncing) { isHaptic = false,
onSyncClick()
}
}) {
Icon(
modifier = Modifier.rotate(if (isSyncing) angle else 0f), modifier = Modifier.rotate(if (isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh, imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh), contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) ) {
if (!isSyncing) {
onSyncClick()
} }
IconButton(onClick = { }
subscribeViewModel.dispatch(SubscribeViewAction.Show) FeedbackIconButton(
}) { isHaptic = false,
Icon(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe), contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) ) {
subscribeViewModel.dispatch(SubscribeViewAction.Show)
} }
} }
) )
}, },
@ -144,15 +140,8 @@ fun FeedsPage(
SubscribeDialog() SubscribeDialog()
LazyColumn { LazyColumn {
item { item {
Text( DisplayText(
modifier = Modifier modifier = Modifier.pointerInput(Unit) {
.padding(
start = 24.dp,
top = 48.dp,
end = 24.dp,
// bottom = 24.dp
)
.pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
launcher.launch("ReadYou.opml") launcher.launch("ReadYou.opml")
@ -160,32 +149,9 @@ fun FeedsPage(
) )
}, },
text = viewState.account?.name ?: stringResource(R.string.unknown), text = viewState.account?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall, desc = if (isSyncing) stringResource(R.string.syncing) else "",
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
item {
AnimatedVisibility(
visible = isSyncing,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text(
modifier = Modifier.padding(
start = 24.dp,
top = 0.dp,
end = 24.dp,
bottom = 0.dp
),
text = stringResource(R.string.syncing),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
item { item {
Banner( Banner(
title = filterState.filter.getName(), title = filterState.filter.getName(),

View File

@ -10,14 +10,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -29,6 +31,8 @@ import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
@ -87,12 +91,13 @@ fun FlowPage(
SmallTopAppBar( SmallTopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { onScrollToPage(0) }) { FeedbackIconButton(
Icon( isHaptic = false,
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back), contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) ) {
onScrollToPage(0)
} }
}, },
actions = { actions = {
@ -101,13 +106,8 @@ fun FlowPage(
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
IconButton(onClick = { FeedbackIconButton(
scope.launch { isHaptic = false,
viewState.listState.scrollToItem(0)
markAsRead = !markAsRead
}
}) {
Icon(
imageVector = Icons.Rounded.DoneAll, imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read), contentDescription = stringResource(R.string.mark_all_as_read),
tint = if (markAsRead) { tint = if (markAsRead) {
@ -115,15 +115,19 @@ fun FlowPage(
} else { } else {
MaterialTheme.colorScheme.onSurface MaterialTheme.colorScheme.onSurface
}, },
) ) {
scope.launch {
viewState.listState.scrollToItem(0)
markAsRead = !markAsRead
} }
} }
IconButton(onClick = {}) { }
Icon( FeedbackIconButton(
isHaptic = false,
imageVector = Icons.Rounded.Search, imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search), contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) ) {
} }
} }
) )
@ -142,46 +146,16 @@ fun FlowPage(
state = viewState.listState, state = viewState.listState,
) { ) {
item { item {
Text( DisplayText(
modifier = Modifier modifier = Modifier.padding(start = 30.dp),
.fillMaxWidth()
.padding(
start = if (true) 54.dp else 24.dp,
top = 48.dp,
end = 24.dp,
// bottom = 24.dp
),
text = when { text = when {
filterState.group != null -> filterState.group.name filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName() else -> filterState.filter.getName()
}, },
style = MaterialTheme.typography.displaySmall, desc = if (isSyncing) stringResource(R.string.syncing) else "",
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
item {
AnimatedVisibility(
visible = isSyncing,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text(
modifier = Modifier.padding(
start = if (true) 54.dp else 24.dp,
top = 0.dp,
end = 24.dp,
bottom = 0.dp
),
text = stringResource(R.string.syncing),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
item { item {
AnimatedVisibility( AnimatedVisibility(
visible = markAsRead, visible = markAsRead,

View File

@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.WebView import me.ash.reader.ui.component.WebView
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
@ -143,34 +144,33 @@ private fun TopBar(
), ),
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { FeedbackIconButton(
onScrollToPage(1) { isHaptic = false,
onClearArticle()
}
}) {
Icon(
imageVector = Icons.Rounded.Close, imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.back), contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) ) {
onScrollToPage(1) {
onClearArticle()
}
} }
}, },
actions = { actions = {
if (isShowActions) { if (isShowActions) {
IconButton(onClick = {}) { FeedbackIconButton(
Icon( isHaptic = false,
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Headphones, imageVector = Icons.Outlined.Headphones,
contentDescription = stringResource(R.string.mark_all_as_read), contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) ) {
} }
IconButton(onClick = {}) { FeedbackIconButton(
Icon( isHaptic = false,
imageVector = Icons.Outlined.MoreVert, imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.search), contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) ) {
} }
} }
} }

View File

@ -16,6 +16,7 @@
<string name="unknown">未知</string> <string name="unknown">未知</string>
<string name="back">返回</string> <string name="back">返回</string>
<string name="go_to">转到</string> <string name="go_to">转到</string>
<string name="settings">设置</string>
<string name="refresh">刷新</string> <string name="refresh">刷新</string>
<string name="search">搜索</string> <string name="search">搜索</string>
<string name="searching">搜索中…</string> <string name="searching">搜索中…</string>

View File

@ -16,6 +16,7 @@
<string name="unknown">Unknown</string> <string name="unknown">Unknown</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="go_to">Goto</string> <string name="go_to">Goto</string>
<string name="settings">Settings</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="searching">Searching…</string> <string name="searching">Searching…</string>