From 1a583f27427d264f1b6be0dd1262cc9c22007887 Mon Sep 17 00:00:00 2001 From: boun Date: Mon, 18 Sep 2023 08:53:40 +0200 Subject: [PATCH 1/5] Bump Dependency for Swipeable to include bugfix for Android 12 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ec188ec71..424fae8b8 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { // https://developer.android.com/jetpack/androidx/releases/compose-ui compose = '1.2.0-beta02' // https://github.com/google/accompanist/releases - accompanist = '0.24.7-alpha' + accompanist = '0.24.11-rc' // https://developer.android.com/jetpack/androidx/releases/compose-material3 material3 = '1.0.1' // https://developer.android.com/jetpack/androidx/releases/lifecycle From d73e8ed618bc6f578d546dc6c94d6ebdd8f5b32f Mon Sep 17 00:00:00 2001 From: boun Date: Mon, 18 Sep 2023 08:54:34 +0200 Subject: [PATCH 2/5] Add a previous element in the ReadingViewModel for two-way list transition --- .../me/ash/reader/ui/page/home/reading/ReadingViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt index 876d03a3a..ba1cb6645 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt @@ -140,13 +140,14 @@ class ReadingViewModel @Inject constructor( val cur = _readingUiState.value.articleWithFeed?.article if (cur != null) { var found = false + var prev = "" for (item in pagingItems) { if (item is ArticleFlowItem.Article) { val itemId = item.articleWithFeed.article.id if (itemId == cur.id) { found = true _readingUiState.update { - it.copy(nextArticleId = "") + it.copy(previousArticleId = prev, nextArticleId = "") } } else if (found) { _readingUiState.update { @@ -154,6 +155,7 @@ class ReadingViewModel @Inject constructor( } break } + prev = itemId } } } @@ -168,4 +170,5 @@ data class ReadingUiState( val isLoading: Boolean = true, val listState: LazyListState = LazyListState(), val nextArticleId: String = "", + val previousArticleId: String = "", ) From b00c34ab2a5e538581c250cdb02b8d2f39560649 Mon Sep 17 00:00:00 2001 From: boun Date: Fri, 15 Sep 2023 14:53:49 +0200 Subject: [PATCH 3/5] Implement a Modifier which allows flipping through articles ... in a horizontal manner --- .../java/me/ash/reader/ui/ext/ModifierExt.kt | 103 ++++++++++++- .../ui/page/home/reading/ReadingPage.kt | 143 ++++++++++-------- 2 files changed, 179 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt index 14931de55..5b1dae624 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt @@ -3,25 +3,45 @@ package me.ash.reader.ui.ext import android.annotation.SuppressLint import android.view.HapticFeedbackConstants import android.view.SoundEffectConstants +import androidx.compose.animation.core.snap import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.Orientation 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.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.rememberSwipeableState +import androidx.compose.material.swipeable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer +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.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerScope import com.google.accompanist.pager.calculateCurrentOffsetForPage +import kotlinx.coroutines.launch import kotlin.math.absoluteValue @OptIn(ExperimentalPagerApi::class) @@ -126,4 +146,85 @@ fun Modifier.combinedFeedbackClickable( }, ) } -} \ No newline at end of file +} + +@OptIn(ExperimentalMaterialApi::class) +fun Modifier.swipeableUpDown(onUp: () -> Unit, onDown: () -> Unit): Modifier = composed { + var screenHeight by rememberSaveable { mutableStateOf(0f) } + val swipeableState = rememberSwipeableState( + SwipeDirection.Initial, + animationSpec = snap() + ) + val connection = remember { + object: NestedScrollConnection { + // Let the children eat first, we consume nothing + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { return Offset.Zero } + + // Let it scroll... + override suspend fun onPreFling(available: Velocity): Velocity { return Velocity.Zero } + + // We consume the rest + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + // use leftover delta to swipe parent + return Offset(0f, swipeableState.performDrag(available.y)) + } + + // We fling but with zero speed (needed to trigger the event, but too fast will overshoot) + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + // perform fling on parent to trigger state change + swipeableState.performFling(velocity = 0f) + return available + } + } + } + val anchorHeight = remember(screenHeight) { + if (screenHeight == 0f) { + 1f + } else { + screenHeight + } + } + val scope = rememberCoroutineScope() + if (swipeableState.isAnimationRunning) { + DisposableEffect(Unit) { + onDispose { + when (swipeableState.currentValue) { + SwipeDirection.Up -> { + onUp() + } + SwipeDirection.Down -> { + onDown() + } + else -> { + return@onDispose + } + } + scope.launch { + swipeableState.snapTo(SwipeDirection.Initial) + } + } + } + } + return@composed Modifier + .onSizeChanged { screenHeight = it.height.toFloat() } + .nestedScroll(connection) + .swipeable( + state = swipeableState, + anchors = mapOf( + 0f to SwipeDirection.Up, + anchorHeight / 2 to SwipeDirection.Initial, + anchorHeight to SwipeDirection.Down, + ), + thresholds = { _, _ -> FractionalThreshold(0.3f) }, + orientation = Orientation.Vertical, + ) +} +enum class SwipeDirection(val raw: Int) { + Initial(0), + Up(1), + Down(2), +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index 931b7d616..85863ce40 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -18,6 +18,7 @@ import me.ash.reader.data.model.preference.LocalReadingPageTonalElevation import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.isScrollDown +import me.ash.reader.ui.ext.swipeableUpDown import me.ash.reader.ui.page.home.HomeViewModel @OptIn(ExperimentalAnimationApi::class) @@ -59,79 +60,89 @@ fun ReadingPage( } RYScaffold( - topBarTonalElevation = tonalElevation.value.dp, - containerTonalElevation = tonalElevation.value.dp, - content = { - Log.i("RLog", "TopBar: recomposition") + topBarTonalElevation = tonalElevation.value.dp, + containerTonalElevation = tonalElevation.value.dp, + content = { + Log.i("RLog", "TopBar: recomposition") - Box(modifier = Modifier.fillMaxSize()) { - // Top Bar - TopBar( - navController = navController, - isShow = isShowToolBar, - title = readingUiState.articleWithFeed?.article?.title, - link = readingUiState.articleWithFeed?.article?.link, - onClose = { - navController.popBackStack() - }, - ) + Box(modifier = Modifier.fillMaxSize().swipeableUpDown({ + Log.d("Swipe", "Up") + if (readingUiState.nextArticleId.isNotEmpty()) { + readingViewModel.initData(readingUiState.nextArticleId) + } + }, { + Log.d("Swipe", "Down") + if (readingUiState.previousArticleId.isNotEmpty()) { + readingViewModel.initData(readingUiState.previousArticleId) + } + })) { + // Top Bar + TopBar( + navController = navController, + isShow = isShowToolBar, + title = readingUiState.articleWithFeed?.article?.title, + link = readingUiState.articleWithFeed?.article?.link, + onClose = { + navController.popBackStack() + }, + ) - // Content - if (readingUiState.articleWithFeed != null) { - AnimatedContent( - targetState = readingUiState.content ?: "", - transitionSpec = { - slideInVertically( - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessLow, - ) - ) { height -> height / 2 } with slideOutVertically { height -> -(height / 2) } + fadeOut( - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessLow, - ) + // Content + if (readingUiState.articleWithFeed != null) { + AnimatedContent( + targetState = readingUiState.content ?: "", + transitionSpec = { + slideInVertically( + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessLow, + ) + ) { height -> height / 2 } with slideOutVertically { height -> -(height / 2) } + fadeOut( + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessLow, + ) + ) + } + ) { target -> + Content( + content = target, + feedName = readingUiState.articleWithFeed.feed.name, + title = readingUiState.articleWithFeed.article.title, + author = readingUiState.articleWithFeed.article.author, + link = readingUiState.articleWithFeed.article.link, + publishedDate = readingUiState.articleWithFeed.article.date, + isLoading = readingUiState.isLoading, + listState = readingUiState.listState, + isShowToolBar = isShowToolBar, ) } - ) { target -> - Content( - content = target, - feedName = readingUiState.articleWithFeed.feed.name, - title = readingUiState.articleWithFeed.article.title, - author = readingUiState.articleWithFeed.article.author, - link = readingUiState.articleWithFeed.article.link, - publishedDate = readingUiState.articleWithFeed.article.date, - isLoading = readingUiState.isLoading, - listState = readingUiState.listState, - isShowToolBar = isShowToolBar, + } + // Bottom Bar + if (readingUiState.articleWithFeed != null) { + BottomBar( + isShow = isShowToolBar, + isUnread = readingUiState.articleWithFeed.article.isUnread, + isStarred = readingUiState.articleWithFeed.article.isStarred, + isFullContent = readingUiState.isFullContent, + onUnread = { + readingViewModel.markUnread(it) + }, + onStarred = { + readingViewModel.markStarred(it) + }, + onNextArticle = { + if (readingUiState.nextArticleId.isNotEmpty()) { + readingViewModel.initData(readingUiState.nextArticleId) + } + }, + onFullContent = { + if (it) readingViewModel.renderFullContent() + else readingViewModel.renderDescriptionContent() + }, ) } } - // Bottom Bar - if (readingUiState.articleWithFeed != null) { - BottomBar( - isShow = isShowToolBar, - isUnread = readingUiState.articleWithFeed.article.isUnread, - isStarred = readingUiState.articleWithFeed.article.isStarred, - isFullContent = readingUiState.isFullContent, - onUnread = { - readingViewModel.markUnread(it) - }, - onStarred = { - readingViewModel.markStarred(it) - }, - onNextArticle = { - if (readingUiState.nextArticleId.isNotEmpty()) { - readingViewModel.initData(readingUiState.nextArticleId) - } - }, - onFullContent = { - if (it) readingViewModel.renderFullContent() - else readingViewModel.renderDescriptionContent() - }, - ) - } } - } ) } From bb23a8a88c05b859c9ac0715c43f420b4048afaa Mon Sep 17 00:00:00 2001 From: boun Date: Tue, 19 Sep 2023 09:06:04 +0200 Subject: [PATCH 4/5] Slide In / Out based on the direction of the swipe --- .../me/ash/reader/ui/page/home/reading/ReadingPage.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index 85863ce40..3256d6e66 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -36,7 +39,7 @@ fun ReadingPage( } else { true } - + var slideDirection = remember { mutableStateOf(1) } val pagingItems = homeUiState.pagingData.collectAsLazyPagingItems().itemSnapshotList readingViewModel.recorderNextArticle(pagingItems) @@ -67,11 +70,13 @@ fun ReadingPage( Box(modifier = Modifier.fillMaxSize().swipeableUpDown({ Log.d("Swipe", "Up") + slideDirection.value = 1 if (readingUiState.nextArticleId.isNotEmpty()) { readingViewModel.initData(readingUiState.nextArticleId) } }, { Log.d("Swipe", "Down") + slideDirection.value = -1 if (readingUiState.previousArticleId.isNotEmpty()) { readingViewModel.initData(readingUiState.previousArticleId) } @@ -97,7 +102,7 @@ fun ReadingPage( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow, ) - ) { height -> height / 2 } with slideOutVertically { height -> -(height / 2) } + fadeOut( + ) { height -> slideDirection.value * (height / 2) } with slideOutVertically { height -> slideDirection.value * -(height / 2)} + fadeOut( spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow, @@ -135,6 +140,7 @@ fun ReadingPage( if (readingUiState.nextArticleId.isNotEmpty()) { readingViewModel.initData(readingUiState.nextArticleId) } + slideDirection.value = 1 }, onFullContent = { if (it) readingViewModel.renderFullContent() From e594c7ad7d5e9e25b7ecbea4be79abd10f185a3d Mon Sep 17 00:00:00 2001 From: boun Date: Mon, 18 Sep 2023 10:59:30 +0200 Subject: [PATCH 5/5] DO NOT MERGE: Debug build with red icon --- app/build.gradle | 13 +++++++++++++ app/src/main/AndroidManifest.xml | 4 ++-- app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../res/mipmap-anydpi-v26/ic_launcher_debug.xml | 6 ++++++ .../mipmap-anydpi-v26/ic_launcher_round_debug.xml | 6 ++++++ .../res/values/ic_launcher_background_debug.xml | 4 ++++ 6 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_debug.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_debug.xml create mode 100644 app/src/main/res/values/ic_launcher_background_debug.xml diff --git a/app/build.gradle b/app/build.gradle index 662116e9c..dea6f58b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,11 +58,24 @@ android { } buildTypes { release { + manifestPlaceholders = [ + appIcon: "@mipmap/ic_launcher", + appIconRound: "@mipmap/ic_launcher_round" + ] minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } + debug { + manifestPlaceholders = [ + appIcon: "@mipmap/ic_launcher_debug", + appIconRound: "@mipmap/ic_launcher_round_debug" + ] + applicationIdSuffix ".debug" + versionNameSuffix "-debug" + debuggable true + } } applicationVariants.all { variant -> variant.outputs.each { output -> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ed0debf76..f7c27fefd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,9 +18,9 @@ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 0b0d585e4..0c5e44489 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_debug.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_debug.xml new file mode 100644 index 000000000..0b0d585e4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_debug.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_debug.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_debug.xml new file mode 100644 index 000000000..6abe207bb --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_debug.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background_debug.xml b/app/src/main/res/values/ic_launcher_background_debug.xml new file mode 100644 index 000000000..b723245a0 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background_debug.xml @@ -0,0 +1,4 @@ + + + #F44336 + \ No newline at end of file