Compare commits

...

5 Commits

Author SHA1 Message Date
Ash 1f133ce443
Merge c45b867607 into 571840a2fa 2024-05-06 19:55:59 +05:30
junkfood 571840a2fa
fix(ui): disable pull to load when no articles available 2024-04-28 17:35:08 +08:00
junkfood 1bf597d32e
fix(ui): menu position jitters when animating content height 2024-04-28 16:24:50 +08:00
Moderpach 1199c6850b
feat(ui): switch to androidx edge to edge implementation (#690)
* switch to androidx edge to edge implementation

* switch to androidx edge to edge implementation for CrashReportActivity

* Remove systemuicontroller
enableEdgeToEdge() has replaced systemuicontroller

* Remove systemuicontroller dependency

* clean code
2024-04-28 00:21:04 +08:00
Ash c45b867607 refactor(ui): redesign key vision 2024-01-21 15:29:56 +08:00
10 changed files with 162 additions and 122 deletions

View File

@ -179,7 +179,6 @@ dependencies {
implementation "androidx.compose.material3:material3:$material3"
// https://github.com/google/accompanist/releases
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist"
implementation "com.google.accompanist:accompanist-pager:$accompanist"
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist"
implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist"

View File

@ -1,9 +1,11 @@
package me.ash.reader.infrastructure.android
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -41,8 +43,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalDarkTheme
import me.ash.reader.infrastructure.preference.LocalOpenLink
@ -55,11 +55,9 @@ import me.ash.reader.ui.theme.AppTheme
class CrashReportActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
enableEdgeToEdge()
val errorMessage: String = intent.getStringExtra(ERROR_REPORT_KEY).toString()
setContent {

View File

@ -9,6 +9,7 @@ import android.util.Log
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider
@ -45,12 +46,10 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.addFlags(FLAG_LAYOUT_IN_SCREEN or FLAG_LAYOUT_NO_LIMITS)
}
Log.i("RLog", "onCreate: ${ProfileInstallerInitializer().create(this)}")
enableEdgeToEdge()
// Set the language
if (Build.VERSION.SDK_INT < 33) {
LanguagesPreference.fromValue(languages).let {

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.component.menu
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.LinearEasing
@ -26,7 +27,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
@ -38,12 +38,14 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.window.PopupPositionProvider
import me.ash.reader.domain.model.constant.ElevationTokens
import me.ash.reader.ui.component.menu.MenuPosition.Horizontal
import me.ash.reader.ui.component.menu.MenuPosition.Vertical
import me.ash.reader.ui.motion.EmphasizedAccelerate
import me.ash.reader.ui.motion.EmphasizedDecelerate
import me.ash.reader.ui.motion.EnterDuration
import me.ash.reader.ui.motion.ExitDuration
import kotlin.math.max
import kotlin.math.min
private const val TAG = "DropdownMenuImpl"
/**
* Interfaces for positioning a menu within a window. This is the same purpose as the interface
@ -385,25 +387,32 @@ internal data class DropdownMenuPositionProvider(
it >= 0 && it + popupContentSize.width <= windowSize.width
} ?: xCandidates.last()
val yCandidates = listOf(
topToAnchorBottom,
bottomToAnchorTop,
centerToAnchorTop,
if (anchorBounds.center.y < windowSize.height / 2) {
topToWindowTop
} else {
bottomToWindowBottom
}
).fastMap {
it.position(
/* val yCandidates = listOf(
topToAnchorBottom,
bottomToAnchorTop,
centerToAnchorTop,
if (anchorBounds.center.y < windowSize.height / 2) {
topToWindowTop
} else {
bottomToWindowBottom
}
).fastMap {
it.position(
anchorBounds = anchorBounds,
windowSize = windowSize,
menuHeight = popupContentSize.height
)
}
val y = yCandidates.fastFirstOrNull {
it >= verticalMargin && it + popupContentSize.height <= windowSize.height - verticalMargin
} ?: yCandidates.last()*/
val y =
(if (anchorBounds.top < windowSize.height / 2) topToAnchorBottom else bottomToAnchorTop).position(
anchorBounds = anchorBounds,
windowSize = windowSize,
menuHeight = popupContentSize.height
)
}
val y = yCandidates.fastFirstOrNull {
it >= verticalMargin && it + popupContentSize.height <= windowSize.height - verticalMargin
} ?: yCandidates.last()
val menuOffset = IntOffset(x, y)
onPositionCalculated(/* anchorBounds = */anchorBounds,/* menuBounds = */

View File

@ -12,12 +12,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import me.ash.reader.domain.model.general.Filter
@ -144,12 +142,6 @@ fun HomeEntry(
else LocalDarkTheme.current.isDarkTheme()
) {
rememberSystemUiController().run {
setStatusBarColor(Color.Transparent, !useDarkTheme)
setSystemBarsColor(Color.Transparent, !useDarkTheme)
setNavigationBarColor(Color.Transparent, !useDarkTheme)
}
AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController,

View File

@ -90,7 +90,7 @@ fun FeedsPage(
) {
var accountTabVisible by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
// val scope = rememberCoroutineScope()
val context = LocalContext.current
val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current
val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current

View File

@ -377,9 +377,7 @@ fun SwipeableArticleItem(
expanded = expanded,
onDismissRequest = { expanded = false },
offset = density.run {
if (LocalLayoutDirection.current == LayoutDirection.Ltr)
DpOffset(menuOffset.x.toDp(), 0.dp)
else DpOffset(0.dp, 0.dp)
DpOffset(menuOffset.x.toDp(), 0.dp)
},
) {
ArticleItemMenuContent(

View File

@ -2,8 +2,10 @@ package me.ash.reader.ui.page.home.reading
import androidx.compose.animation.core.FloatExponentialDecaySpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.spring
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.offset
import androidx.compose.material.ExperimentalMaterialApi
@ -33,7 +35,7 @@ import me.ash.reader.ui.page.home.reading.PullToLoadDefaults.ContentOffsetMultip
import kotlin.math.abs
import kotlin.math.sqrt
private const val TAG = "PullRelease"
private const val TAG = "PullToLoad"
/**
* A [NestedScrollConnection] that provides scroll events to a hoisted [state].
@ -51,7 +53,7 @@ private class ReaderNestedScrollConnection(
private val enabled: Boolean,
private val onPreScroll: (Float) -> Float,
private val onPostScroll: (Float) -> Float,
private val onRelease: (Float) -> Unit,
private val onRelease: () -> Unit,
private val onScroll: ((Float) -> Unit)? = null
) : NestedScrollConnection {
@ -81,7 +83,7 @@ private class ReaderNestedScrollConnection(
}
override suspend fun onPreFling(available: Velocity): Velocity {
onRelease(available.y)
onRelease()
return Velocity.Zero
}
}
@ -217,21 +219,22 @@ class PullToLoadState internal constructor(
return if (offsetPulled.signOpposites(pullDelta)) onPull(pullDelta) else 0f
}
internal fun onRelease(velocity: Float): Float {
internal fun onRelease(): Float {
// Snap to 0f and hide the indicator
animateDistanceTo(0f)
when (status) {
// We don't change the pull offset here because the animation for loading another content
// should be handled outside, and this state will be soon disposed
Status.PulledDown -> {
onLoadPrevious.value()
}
Status.PulledUp -> {
animateDistanceTo(0f)
onLoadNext.value()
}
else -> {
// Snap to 0f and hide the indicator
animateDistanceTo(0f)
}
}
return 0f
@ -247,7 +250,8 @@ class PullToLoadState internal constructor(
animate(
initialValue = offsetPulled,
targetValue = float,
initialVelocity = velocity
initialVelocity = velocity,
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
) { value, _ ->
offsetPulled = value
}
@ -310,7 +314,7 @@ fun Modifier.pullToLoad(
state: PullToLoadState,
contentOffsetMultiple: Int = ContentOffsetMultiple,
onScroll: ((Float) -> Unit)? = null,
enabled: Boolean = true
enabled: Boolean = true,
): Modifier =
nestedScroll(
ReaderNestedScrollConnection(

View File

@ -20,70 +20,117 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.Idle
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PulledDown
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PulledUp
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PullingDown
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PullingUp
import kotlin.math.abs
@Composable
fun BoxScope.PullToLoadIndicator(state:PullToLoadState) {
state.status.run {
val fraction = state.offsetFraction
val absFraction = abs(fraction)
val imageVector = when (this) {
PullToLoadState.Status.PulledDown -> Icons.Outlined.KeyboardArrowUp
PullToLoadState.Status.PulledUp -> Icons.Outlined.KeyboardArrowDown
else -> null
}
fun BoxScope.PullToLoadIndicator(
state: PullToLoadState,
canLoadPrevious: Boolean = true,
canLoadNext: Boolean = true
) {
val hapticFeedback = LocalHapticFeedback.current
val status = state.status
val alignment = if (fraction < 0f) {
Alignment.BottomCenter
} else {
Alignment.TopCenter
}
if (this != PullToLoadState.Status.Idle) {
Surface(
modifier = Modifier
.align(alignment)
.padding(vertical = 80.dp)
.offset(y = (fraction * 48).dp)
.width(36.dp),
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.extraLarge
) {
Column(
modifier = Modifier
.align(Alignment.Center),
) {
AnimatedContent(
targetState = imageVector, modifier = Modifier.align(
Alignment.CenterHorizontally
), transitionSpec = {
(fadeIn(animationSpec = tween(220, delayMillis = 0)))
.togetherWith(fadeOut(animationSpec = tween(90)))
}, label = ""
) {
if (it != null) {
Icon(
imageVector = it,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.padding(horizontal = 4.dp)
.padding(vertical = (2 * absFraction).dp)
.size(32.dp)
)
} else {
Spacer(
modifier = Modifier
.width(36.dp)
.height((12 * absFraction).dp)
)
}
}
LaunchedEffect(status) {
when {
canLoadPrevious && status == PulledDown -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
canLoadNext && status == PulledUp -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
else -> {}
}
}
val fraction = state.offsetFraction
val absFraction = abs(fraction)
val imageVector = when (status) {
PulledDown -> Icons.Outlined.KeyboardArrowUp
PulledUp -> Icons.Outlined.KeyboardArrowDown
else -> null
}
val alignment = if (fraction < 0f) {
Alignment.BottomCenter
} else {
Alignment.TopCenter
}
val visible = remember(status, canLoadPrevious, canLoadNext) {
when (status) {
Idle -> {
false
}
PullingUp, PulledUp -> {
canLoadNext
}
PulledDown, PullingDown -> {
canLoadPrevious
}
}
}
if (visible) {
Surface(
modifier = Modifier
.align(alignment)
.padding(vertical = 80.dp)
.offset(y = (fraction * 48).dp)
.width(36.dp),
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.extraLarge
) {
Column(
modifier = Modifier
.align(Alignment.Center),
) {
AnimatedContent(
targetState = imageVector, modifier = Modifier.align(
Alignment.CenterHorizontally
), transitionSpec = {
(fadeIn(animationSpec = tween(220, delayMillis = 0)))
.togetherWith(fadeOut(animationSpec = tween(90)))
}, label = ""
) {
if (it != null) {
Icon(
imageVector = it,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.padding(horizontal = 4.dp)
.padding(vertical = (2 * absFraction).dp)
.size(32.dp)
)
} else {
Spacer(
modifier = Modifier
.width(36.dp)
.height((12 * absFraction).dp)
)
}
}
}
}
}
}

View File

@ -23,7 +23,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.TextUnit
@ -115,10 +114,10 @@ fun ReadingPage(
navController.popBackStack()
},
)
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
val isNextArticleAvailable = !readerState.nextArticleId.isNullOrEmpty()
val isPreviousArticleAvailable = !readerState.previousArticleId.isNullOrEmpty()
if (readerState.articleId != null) {
// Content
@ -159,16 +158,6 @@ fun ReadingPage(
)
LaunchedEffect(state.status) {
when (state.status) {
PullToLoadState.Status.PulledDown, PullToLoadState.Status.PulledUp -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
else -> {}
}
}
val listState = rememberSaveable(
inputs = arrayOf(content),
saver = LazyListState.Saver
@ -210,7 +199,11 @@ fun ReadingPage(
showFullScreenImageViewer = true
}
)
PullToLoadIndicator(state = state)
PullToLoadIndicator(
state = state,
canLoadPrevious = isPreviousArticleAvailable,
canLoadNext = isNextArticleAvailable
)
}
}
}
@ -252,7 +245,8 @@ fun ReadingPage(
onSuccess = { context.showToast(context.getString(R.string.image_saved)) },
onFailure = {
// FIXME: crash the app for error report
th -> throw th
th ->
throw th
}
)
},