From 13a6f857fa8b4b26ff462b172097f96d7f638dfa Mon Sep 17 00:00:00 2001 From: Don Turner Date: Tue, 2 May 2023 23:53:04 +0100 Subject: [PATCH 01/94] Change JAVA_HOME path to use JDK 17 from prebuilts repo Change-Id: I91933628d32de2a703c4a6955827044533dd7cdd --- build_android_release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_android_release.sh b/build_android_release.sh index c7e5fc835..3817d4cda 100755 --- a/build_android_release.sh +++ b/build_android_release.sh @@ -21,7 +21,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" APP_OUT=$DIR/app/build/outputs -export JAVA_HOME="$(cd $DIR/../../../prebuilts/studio/jdk/jdk11/linux && pwd )" +export JAVA_HOME="$(cd $DIR/../nowinandroid-prebuilts/jdk17/linux && pwd )" echo "JAVA_HOME=$JAVA_HOME" export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )" From 8c9f656eec0ecfa31b6c695416964b4e928aeccc Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 4 May 2023 12:04:46 +0100 Subject: [PATCH 02/94] Add path to JDK 17 Change-Id: If6fd6fab4517242f7bad9149ae2fdbdbbca7ef34 --- kokoro/build.sh | 5 ++++- kokoro/continuous.cfg | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/kokoro/build.sh b/kokoro/build.sh index c217e995c..456752516 100755 --- a/kokoro/build.sh +++ b/kokoro/build.sh @@ -36,7 +36,10 @@ echo y | ${ANDROID_HOME}/tools/bin/sdkmanager --licenses cd $KOKORO_ARTIFACTS_DIR/git/nowinandroid # The build needs Java 17, set it as the default Java version. -sudo update-java-alternatives --set java-1.17.0-openjdk-amd64 +sudo apt-get update +sudo apt-get install -y openjdk-17-jdk +sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java +java -version # Also clear JAVA_HOME variable so java -version is used instead export JAVA_HOME= diff --git a/kokoro/continuous.cfg b/kokoro/continuous.cfg index e0db4f172..b7f4c4ac8 100644 --- a/kokoro/continuous.cfg +++ b/kokoro/continuous.cfg @@ -1,2 +1,4 @@ # Location of the bash script. -build_file: "nowinandroid/kokoro/build.sh" \ No newline at end of file +build_file: "nowinandroid/kokoro/build.sh" + +gfile_resources: "/x20/projects/java-platform/linux-amd64/jdk-17-latest" \ No newline at end of file From db5198f08546fa53c9996c091085a6ac17b4fe31 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 4 May 2023 15:49:06 +0100 Subject: [PATCH 03/94] Remove path to JDK 17 (this is installed using apt instead) Change-Id: I6650999f5f750366d2c7277614e26cbb05828b98 --- kokoro/continuous.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kokoro/continuous.cfg b/kokoro/continuous.cfg index b7f4c4ac8..e0db4f172 100644 --- a/kokoro/continuous.cfg +++ b/kokoro/continuous.cfg @@ -1,4 +1,2 @@ # Location of the bash script. -build_file: "nowinandroid/kokoro/build.sh" - -gfile_resources: "/x20/projects/java-platform/linux-amd64/jdk-17-latest" \ No newline at end of file +build_file: "nowinandroid/kokoro/build.sh" \ No newline at end of file From 0829cb73f732c65e45a048497a062eaaad2b6ae6 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Tue, 9 May 2023 17:05:59 +0100 Subject: [PATCH 04/94] Bump version name to 0.1.0 (versionCode 6) Change-Id: I880683a37b4324f91499eeb47b418ab5e2d1242b --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e172ed8bb..00568cce7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,8 +28,8 @@ plugins { android { defaultConfig { applicationId = "com.google.samples.apps.nowinandroid" - versionCode = 5 - versionName = "0.0.5" // X.Y.Z; X = Major, Y = minor, Z = Patch level + versionCode = 6 + versionName = "0.1.0" // X.Y.Z; X = Major, Y = minor, Z = Patch level // Custom test runner to set up Hilt dependency graph testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" From 9f6a94dee2e39ee24d6d31d37544c0584aa37b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 26 May 2023 12:05:49 +0200 Subject: [PATCH 05/94] Change folder for compose-reports Change-Id: I7757f13c36c60991586224707f91f8ac8c0beae8 --- .../com/google/samples/apps/nowinandroid/AndroidCompose.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index 5997f7d4e..ebf82b970 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -61,7 +61,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") val enableMetrics = (enableMetricsProvider.orNull == "true") if (enableMetrics) { - val metricsFolder = File(project.buildDir, "compose-metrics") + val metricsFolder = File(project.rootProject.buildDir, "compose-metrics") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath @@ -71,7 +71,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") val enableReports = (enableReportsProvider.orNull == "true") if (enableReports) { - val reportsFolder = File(project.buildDir, "compose-reports") + val reportsFolder = File(project.rootProject.buildDir, "compose-reports") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath From ca8b22e798f01aa7df87c1d2fe1babd15b341b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 26 May 2023 12:07:38 +0200 Subject: [PATCH 06/94] Update README Change-Id: I36e98760b254be00d591d8d719dcbc57a8dd8d62 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9aca22cbd..cb97c2a6b 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,8 @@ Run the following command to get and analyse compose compiler metrics: ./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true ``` -The reports files will be added to build/compose-reports in each module. The metrics files will be -added to build/compose-metrics in each module. +The reports files will be added to build/compose-reports in root folder. The metrics files will also be +added to build/compose-metrics in root folder. For more information on Compose compiler metrics, see [this blog post](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8). From d549ca6c7a4fdb12366c87a565a01e3b4c76439d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 26 May 2023 16:15:05 +0200 Subject: [PATCH 07/94] Apply suggestions from code review Co-authored-by: Simon Marquis --- README.md | 4 ++-- .../com/google/samples/apps/nowinandroid/AndroidCompose.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cb97c2a6b..ae8c3b3aa 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,8 @@ Run the following command to get and analyse compose compiler metrics: ./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true ``` -The reports files will be added to build/compose-reports in root folder. The metrics files will also be -added to build/compose-metrics in root folder. +The reports files will be added to [build/compose-reports](build/compose-reports). The metrics files will also be +added to [build/compose-metrics](build/compose-metrics). For more information on Compose compiler metrics, see [this blog post](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8). diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index ebf82b970..393c68535 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -61,7 +61,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") val enableMetrics = (enableMetricsProvider.orNull == "true") if (enableMetrics) { - val metricsFolder = File(project.rootProject.buildDir, "compose-metrics") + val metricsFolder = rootProject.buildDir.resolve("compose-metrics") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath @@ -71,7 +71,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") val enableReports = (enableReportsProvider.orNull == "true") if (enableReports) { - val reportsFolder = File(project.rootProject.buildDir, "compose-reports") + val reportsFolder = rootProject.buildDir.resolve("compose-reports") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath From f577fb06cc9e3d3695587c5a6b8eac6c0f8b7db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 26 May 2023 16:29:36 +0200 Subject: [PATCH 08/94] Add project path subfolder Change-Id: I1b680a37ecbd80b894b0d1b649633936cbe41328 --- .../com/google/samples/apps/nowinandroid/AndroidCompose.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index 393c68535..ed0d38335 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -23,7 +23,6 @@ import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import java.io.File /** * Configure Compose-specific options @@ -59,9 +58,11 @@ internal fun Project.configureAndroidCompose( private fun Project.buildComposeMetricsParameters(): List { val metricParameters = mutableListOf() val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") + val projectPath = project.path.replace(':', '_').drop(1) + val enableMetrics = (enableMetricsProvider.orNull == "true") if (enableMetrics) { - val metricsFolder = rootProject.buildDir.resolve("compose-metrics") + val metricsFolder = rootProject.buildDir.resolve("compose-metrics/$projectPath") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath @@ -71,7 +72,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") val enableReports = (enableReportsProvider.orNull == "true") if (enableReports) { - val reportsFolder = rootProject.buildDir.resolve("compose-reports") + val reportsFolder = rootProject.buildDir.resolve("compose-reports/$projectPath") metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath From 375480a2f912044362d6518cd1ccd94668330bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 09:42:16 +0200 Subject: [PATCH 09/94] Make paths relative Change-Id: Iefdf9b2a0313bbdc0532450bb10ca9b7cca41014 --- .../com/google/samples/apps/nowinandroid/AndroidCompose.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index ed0d38335..3051d0635 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -58,11 +58,11 @@ internal fun Project.configureAndroidCompose( private fun Project.buildComposeMetricsParameters(): List { val metricParameters = mutableListOf() val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") - val projectPath = project.path.replace(':', '_').drop(1) + val relativePath = projectDir.relativeTo(rootDir) val enableMetrics = (enableMetricsProvider.orNull == "true") if (enableMetrics) { - val metricsFolder = rootProject.buildDir.resolve("compose-metrics/$projectPath") + val metricsFolder = rootProject.buildDir.resolve("compose-metrics").resolve(relativePath) metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath @@ -72,7 +72,7 @@ private fun Project.buildComposeMetricsParameters(): List { val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") val enableReports = (enableReportsProvider.orNull == "true") if (enableReports) { - val reportsFolder = rootProject.buildDir.resolve("compose-reports/$projectPath") + val reportsFolder = rootProject.buildDir.resolve("compose-reports").resolve(relativePath) metricParameters.add("-P") metricParameters.add( "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath From 0f98e4b7099f94d568b48a1a13830894aa2dc9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 10:46:02 +0200 Subject: [PATCH 10/94] Simplify StartupBenchmark to just use StartupMode.COLD Change-Id: I70186c3fdab43fb1ceebcce62105666551ede269 --- .../nowinandroid/startup/StartupBenchmark.kt | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt index 8e396eda3..678102f78 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt @@ -19,45 +19,24 @@ package com.google.samples.apps.nowinandroid.startup import androidx.benchmark.macro.BaselineProfileMode.Disable import androidx.benchmark.macro.BaselineProfileMode.Require import androidx.benchmark.macro.CompilationMode -import androidx.benchmark.macro.StartupMode import androidx.benchmark.macro.StartupMode.COLD -import androidx.benchmark.macro.StartupMode.HOT -import androidx.benchmark.macro.StartupMode.WARM import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import com.google.samples.apps.nowinandroid.PACKAGE_NAME +import com.google.samples.apps.nowinandroid.allowNotifications import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** + * Enables app startups from various states of baseline profile or [CompilationMode]s. * Run this benchmark from Studio to see startup measurements, and captured system traces * for investigating your app's performance from a cold state. */ @RunWith(AndroidJUnit4ClassRunner::class) -class ColdStartupBenchmark : AbstractStartupBenchmark(COLD) - -/** - * Run this benchmark from Studio to see startup measurements, and captured system traces - * for investigating your app's performance from a warm state. - */ -@RunWith(AndroidJUnit4ClassRunner::class) -class WarmStartupBenchmark : AbstractStartupBenchmark(WARM) - -/** - * Run this benchmark from Studio to see startup measurements, and captured system traces - * for investigating your app's performance from a hot state. - */ -@RunWith(AndroidJUnit4ClassRunner::class) -class HotStartupBenchmark : AbstractStartupBenchmark(HOT) - -/** - * Base class for benchmarks with different startup modes. - * Enables app startups from various states of baseline profile or [CompilationMode]s. - */ -abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { +class StartupBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @@ -80,9 +59,10 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { metrics = listOf(StartupTimingMetric()), compilationMode = compilationMode, iterations = 10, - startupMode = startupMode, + startupMode = COLD, setupBlock = { pressHome() + allowNotifications() }, ) { startActivityAndWait() From 7bee6d2f37b857c82c34997ffffc3e3248dd3442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 10:58:29 +0200 Subject: [PATCH 11/94] Add waitAndFind helper Change-Id: I7498dda3eb686d15829d89688695866ae8382c6e --- .../androidx/test/uiautomator/UiAutomatorHelpers.kt | 12 ++++++++++++ .../apps/nowinandroid/foryou/ForYouActions.kt | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt b/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt index 85867b982..766de00e1 100644 --- a/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt +++ b/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt @@ -46,3 +46,15 @@ enum class HasChildrenOp { EXACTLY, AT_MOST, } + +/** + * Waits until an object with [selector] if visible on screen and returns the object. + * If the element is not available in [timeout], throws [AssertionError] + */ +fun UiDevice.waitAndFindObject(selector: BySelector, timeout: Long): UiObject2 { + if (!wait(Until.hasObject(selector), timeout)) { + throw AssertionError("Element not found on screen in ${timeout}ms (selector=$selector)") + } + + return findObject(selector) +} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt index 5e4952fbb..bb21f3d39 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt @@ -20,6 +20,7 @@ import androidx.benchmark.macro.MacrobenchmarkScope import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import androidx.test.uiautomator.untilHasChildren +import androidx.test.uiautomator.waitAndFindObject import com.google.samples.apps.nowinandroid.flingElementDownUp fun MacrobenchmarkScope.forYouWaitForContent() { @@ -27,7 +28,7 @@ fun MacrobenchmarkScope.forYouWaitForContent() { device.wait(Until.gone(By.res("loadingWheel")), 5_000) // Sometimes, the loading wheel is gone, but the content is not loaded yet // So we'll wait here for topics to be sure - val obj = device.findObject(By.res("forYou:topicSelection")) + val obj = device.waitAndFindObject(By.res("forYou:topicSelection"), 10_000) // Timeout here is quite big, because sometimes data loading takes a long time! obj.wait(untilHasChildren(), 60_000) } From 951e968aa4967042532296675c3f7555f9973814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 16:44:16 +0200 Subject: [PATCH 12/94] Allow notification for BP Generator Change-Id: Ie9a432ab04e56cd42664c8aacbc898b622767c62 --- .../nowinandroid/baselineprofile/BaselineProfileGenerator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt index 3dfafd647..9286b2bef 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt @@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.baselineprofile import androidx.benchmark.macro.ExperimentalBaselineProfilesApi import androidx.benchmark.macro.junit4.BaselineProfileRule import com.google.samples.apps.nowinandroid.PACKAGE_NAME +import com.google.samples.apps.nowinandroid.allowNotifications import com.google.samples.apps.nowinandroid.bookmarks.goToBookmarksScreen import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp import com.google.samples.apps.nowinandroid.foryou.forYouSelectTopics @@ -41,7 +42,7 @@ class BaselineProfileGenerator { // This block defines the app's critical user journey. Here we are interested in // optimizing for app startup. But you can also navigate and scroll // through your most important UI. - + allowNotifications() pressHome() startActivityAndWait() From 51713c71e7825239074ed9b408f2396b7f82611a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 16:51:17 +0200 Subject: [PATCH 13/94] Fix FrameTimingMetric using StartupMode.WARM Change-Id: I1d08af6622f893eb6fef83b2b145cf8d6098b372 --- .../apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt index 3008fdc0d..18a7a717b 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt @@ -43,7 +43,7 @@ class ScrollForYouFeedBenchmark { metrics = listOf(FrameTimingMetric()), compilationMode = compilationMode, iterations = 10, - startupMode = StartupMode.COLD, + startupMode = StartupMode.WARM, setupBlock = { // Start the app pressHome() From 4fcc867dd445b58f2a4b50a4e2ebe4d2c73d79f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 May 2023 17:33:25 +0200 Subject: [PATCH 14/94] Move waitAndFind + dumpWindowHierarchy to Utils Change-Id: Id4e05bf2392553179672155b9c1f935a933525dc --- .../test/uiautomator/UiAutomatorHelpers.kt | 12 ---------- .../google/samples/apps/nowinandroid/Utils.kt | 24 +++++++++++++++++++ .../apps/nowinandroid/foryou/ForYouActions.kt | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt b/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt index 766de00e1..85867b982 100644 --- a/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt +++ b/benchmarks/src/main/java/androidx/test/uiautomator/UiAutomatorHelpers.kt @@ -46,15 +46,3 @@ enum class HasChildrenOp { EXACTLY, AT_MOST, } - -/** - * Waits until an object with [selector] if visible on screen and returns the object. - * If the element is not available in [timeout], throws [AssertionError] - */ -fun UiDevice.waitAndFindObject(selector: BySelector, timeout: Long): UiObject2 { - if (!wait(Until.hasObject(selector), timeout)) { - throw AssertionError("Element not found on screen in ${timeout}ms (selector=$selector)") - } - - return findObject(selector) -} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/Utils.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/Utils.kt index 494e451d0..9ece991c4 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/Utils.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/Utils.kt @@ -16,10 +16,13 @@ package com.google.samples.apps.nowinandroid +import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.Direction import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until import com.google.samples.apps.nowinandroid.benchmarks.BuildConfig +import java.io.ByteArrayOutputStream /** * Convenience parameter to use proper package name with regards to build type and build flavor. @@ -38,3 +41,24 @@ fun UiDevice.flingElementDownUp(element: UiObject2) { waitForIdle() element.fling(Direction.UP) } + +/** + * Waits until an object with [selector] if visible on screen and returns the object. + * If the element is not available in [timeout], throws [AssertionError] + */ +fun UiDevice.waitAndFindObject(selector: BySelector, timeout: Long): UiObject2 { + if (!wait(Until.hasObject(selector), timeout)) { + throw AssertionError("Element not found on screen in ${timeout}ms (selector=$selector)") + } + + return findObject(selector) +} + +/** + * Helper to dump window hierarchy into a string. + */ +fun UiDevice.dumpWindowHierarchy(): String { + val buffer = ByteArrayOutputStream() + dumpWindowHierarchy(buffer) + return buffer.toString() +} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt index bb21f3d39..9dea9dc4a 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt @@ -20,8 +20,8 @@ import androidx.benchmark.macro.MacrobenchmarkScope import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import androidx.test.uiautomator.untilHasChildren -import androidx.test.uiautomator.waitAndFindObject import com.google.samples.apps.nowinandroid.flingElementDownUp +import com.google.samples.apps.nowinandroid.waitAndFindObject fun MacrobenchmarkScope.forYouWaitForContent() { // Wait until content is loaded by checking if topics are loaded From d332f7b70d07d1f86d7571671bf1a1284080e0c4 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 18 Jun 2023 11:23:17 +0200 Subject: [PATCH 15/94] Remove forced objenesis resolution strategy The linked issue is marked as resolved, and not able to reproduce the mentioned compilation error. --- app/build.gradle.kts | 2 -- .../src/main/kotlin/AndroidLibraryConventionPlugin.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0c6e3eeb9..3783928f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -126,7 +126,5 @@ dependencies { configurations.configureEach { resolutionStrategy { force(libs.junit4) - // Temporary workaround for https://issuetracker.google.com/174733673 - force("org.objenesis:objenesis:2.6") } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 287b09cf5..ca955d754 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -49,8 +49,6 @@ class AndroidLibraryConventionPlugin : Plugin { configurations.configureEach { resolutionStrategy { force(libs.findLibrary("junit4").get()) - // Temporary workaround for https://issuetracker.google.com/174733673 - force("org.objenesis:objenesis:2.6") } } dependencies { From 0a3069a0d82de1fde29ef3f8ee5726babdd11a5a Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 18 Jun 2023 11:42:12 +0200 Subject: [PATCH 16/94] Remove forced junit resolution strategy By looking at the Gradle scan's dependency tab, junit 4.12 is already replaced by 4.13.2. There is no need to manually force this at the configuration level. --- app/build.gradle.kts | 7 ------- .../src/main/kotlin/AndroidLibraryConventionPlugin.kt | 6 ------ 2 files changed, 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3783928f5..1d8383b00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -121,10 +121,3 @@ dependencies { implementation(libs.androidx.profileinstaller) implementation(libs.coil.kt) } - -// androidx.test is forcing JUnit, 4.12. This forces it to use 4.13 -configurations.configureEach { - resolutionStrategy { - force(libs.junit4) - } -} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index ca955d754..864034561 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -21,7 +21,6 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests -import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -46,11 +45,6 @@ class AndroidLibraryConventionPlugin : Plugin { configurePrintApksTask(this) disableUnnecessaryAndroidTests(target) } - configurations.configureEach { - resolutionStrategy { - force(libs.findLibrary("junit4").get()) - } - } dependencies { add("androidTestImplementation", kotlin("test")) add("testImplementation", kotlin("test")) From 7dc03367117ae153a3a9a3584d0f8daebbdd9b70 Mon Sep 17 00:00:00 2001 From: Murat Yener Date: Thu, 29 Jun 2023 15:04:12 -0700 Subject: [PATCH 17/94] Ads PowerMetric benchmark which scrolls through the topics list in Light vs Dark theme --- .../interests/InterestsActions.kt | 14 ++++ .../ScrollTopicListPowerMetricsBenchmark.kt | 82 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt index e94369ce2..7bc1feaae 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt @@ -48,3 +48,17 @@ fun MacrobenchmarkScope.interestsToggleBookmarked() { checkable.click() device.waitForIdle() } + +fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) { + when (isDark){ + true -> device.findObject(By.text("Dark")).click() + false -> device.findObject(By.text("Light")).click() + } + device.waitForIdle() + device.findObject(By.text("OK")).click() + + // Wait until interests are shown on screen + device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000) + val topAppBar = device.findObject(By.res("niaTopAppBar")) + topAppBar.wait(Until.hasObject(By.text("Now in Android")), 2_000) +} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt new file mode 100644 index 000000000..4a996a9d6 --- /dev/null +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.interests + +import android.os.Build.VERSION_CODES +import androidx.annotation.RequiresApi +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.ExperimentalMetricApi +import androidx.benchmark.macro.FrameTimingMetric +import androidx.benchmark.macro.PowerCategory +import androidx.benchmark.macro.PowerCategoryDisplayLevel +import androidx.benchmark.macro.PowerMetric +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.By +import com.google.samples.apps.nowinandroid.PACKAGE_NAME +import com.google.samples.apps.nowinandroid.allowNotifications +import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp +import com.google.samples.apps.nowinandroid.foryou.forYouSelectTopics +import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalMetricApi::class) +@RequiresApi(VERSION_CODES.Q) +@RunWith(AndroidJUnit4::class) +class ScrollTopicListPowerMetricsBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + private val categories = PowerCategory.values() + .associateWith { PowerCategoryDisplayLevel.TOTAL } + + @Test + fun benchmarkStateChangeCompilationLight() = + benchmarkStateChangeWithTheme(CompilationMode.Partial(), false) + + @Test + fun benchmarkStateChangeCompilationDark() = + benchmarkStateChangeWithTheme(CompilationMode.Partial(), true) + + private fun benchmarkStateChangeWithTheme(compilationMode: CompilationMode, isDark: Boolean) = + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = listOf(FrameTimingMetric(), PowerMetric(PowerMetric.Energy(categories))), + compilationMode = compilationMode, + iterations = 2, + startupMode = StartupMode.WARM, + setupBlock = { + // Start the app + pressHome() + startActivityAndWait() + allowNotifications() + // Navigate to interests screen + device.findObject(By.desc("Settings")).click() + device.waitForIdle() + setAppTheme(isDark) + }, + ) { + forYouWaitForContent() + forYouSelectTopics() + repeat(3) { + forYouScrollFeedDownUp() + } + } +} From f79b19c3779d6dd683f51b72ffa2c66431219080 Mon Sep 17 00:00:00 2001 From: Murat Yener Date: Thu, 29 Jun 2023 15:10:42 -0700 Subject: [PATCH 18/94] Spotless fixes --- .../interests/InterestsActions.kt | 2 +- .../ScrollTopicListPowerMetricsBenchmark.kt | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt index 7bc1feaae..a6420a7fd 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt @@ -50,7 +50,7 @@ fun MacrobenchmarkScope.interestsToggleBookmarked() { } fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) { - when (isDark){ + when (isDark) { true -> device.findObject(By.text("Dark")).click() false -> device.findObject(By.text("Light")).click() } diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt index 4a996a9d6..8ede75e4b 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt @@ -1,17 +1,17 @@ /* - * Copyright 2023 The Android Open Source Project + * Copyright 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.google.samples.apps.nowinandroid.interests From 445c18a80363bb03fc3d3d39f2620652fa145699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Wed, 5 Jul 2023 13:18:57 +0200 Subject: [PATCH 19/94] Refactor InterestsItem to Material3 ListItem Change-Id: I12adc8820964aecd97ea0b4e22ae13e95e1428ab --- .../feature/interests/InterestsItem.kt | 101 +++++++----------- .../feature/search/SearchScreenTest.kt | 24 +++-- 2 files changed, 56 insertions(+), 69 deletions(-) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index ec9fd8f10..7456ba92b 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -18,22 +18,20 @@ package com.google.samples.apps.nowinandroid.feature.interests import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton @@ -51,63 +49,46 @@ fun InterestsItem( modifier: Modifier = Modifier, iconModifier: Modifier = Modifier, description: String = "", - itemSeparation: Dp = 16.dp, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .weight(1f) - .clickable { onClick() } - .padding(vertical = itemSeparation), - ) { + ListItem( + leadingContent = { InterestsIcon(topicImageUrl, iconModifier.size(64.dp)) - Spacer(modifier = Modifier.width(24.dp)) - InterestContent(name, description) - } - NiaIconToggleButton( - checked = following, - onCheckedChange = onFollowButtonClick, - icon = { - Icon( - imageVector = NiaIcons.Add, - contentDescription = stringResource( - id = string.card_follow_button_content_desc, - ), - ) - }, - checkedIcon = { - Icon( - imageVector = NiaIcons.Check, - contentDescription = stringResource( - id = string.card_unfollow_button_content_desc, - ), - ) - }, - ) - } -} - -@Composable -private fun InterestContent(name: String, description: String, modifier: Modifier = Modifier) { - Column(modifier) { - Text( - text = name, - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.padding( - vertical = if (description.isEmpty()) 0.dp else 4.dp, - ), - ) - if (description.isNotEmpty()) { - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, + }, + headlineContent = { + Text(text = name) + }, + supportingContent = { + Text(text = description) + }, + trailingContent = { + NiaIconToggleButton( + checked = following, + onCheckedChange = onFollowButtonClick, + icon = { + Icon( + imageVector = NiaIcons.Add, + contentDescription = stringResource( + id = string.card_follow_button_content_desc, + ), + ) + }, + checkedIcon = { + Icon( + imageVector = NiaIcons.Check, + contentDescription = stringResource( + id = string.card_unfollow_button_content_desc, + ), + ) + }, ) - } - } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ), + modifier = modifier + .semantics(mergeDescendants = true) { /* no-op */ } + .clickable(enabled = true, onClick = onClick), + ) } @Composable diff --git a/feature/search/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt b/feature/search/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt index 53f00c0dc..d6c07221e 100644 --- a/feature/search/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt +++ b/feature/search/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt @@ -20,11 +20,14 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.hasScrollToNodeAction import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollToIndex import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID @@ -139,15 +142,18 @@ class SearchScreenTest { composeTestRule .onNodeWithText(topicsString) .assertIsDisplayed() - composeTestRule - .onNodeWithText(followableTopicTestData[0].topic.name) - .assertIsDisplayed() - composeTestRule - .onNodeWithText(followableTopicTestData[1].topic.name) - .assertIsDisplayed() - composeTestRule - .onNodeWithText(followableTopicTestData[2].topic.name) - .assertIsDisplayed() + + val scrollableNode = composeTestRule + .onAllNodes(hasScrollToNodeAction()) + .onFirst() + + followableTopicTestData.forEachIndexed { index, followableTopic -> + scrollableNode.performScrollToIndex(index) + + composeTestRule + .onNodeWithText(followableTopic.topic.name) + .assertIsDisplayed() + } composeTestRule .onAllNodesWithContentDescription(followButtonContentDesc) From 14e53e2bb7e1ba7b60f304d0712023e1788fe011 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 7 Jul 2023 14:48:35 +0100 Subject: [PATCH 20/94] Remove problematic API 23 device from FTL tests Change-Id: I85b3979ffda637d558f071ac10663f5578f85db7 --- kokoro/nightly.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kokoro/nightly.sh b/kokoro/nightly.sh index 94946020e..1423f4f44 100755 --- a/kokoro/nightly.sh +++ b/kokoro/nightly.sh @@ -21,12 +21,11 @@ set -e set -x # Run the normal build, but replace the default virtual devices with physical ones. -# hammerhead | Nexus 5 | API 23 | Phone # walleye | Pixel 2 | API 27 | Phone # gts4lltevzw | Galaxy Tab S4 | API 28 | Tablet # a10 | Samsung A10 | API 29 | Phone # redfin | Pixel 5e | API 30 | Phone # oriole | Pixel 6 | API 31 | Phone -bash $KOKORO_ARTIFACTS_DIR/git/nowinandroid/kokoro/build.sh "hammerhead,walleye,gts4lltevzw,a10,redfin,oriole" "23,27,28,29,30,31" +bash $KOKORO_ARTIFACTS_DIR/git/nowinandroid/kokoro/build.sh "walleye,gts4lltevzw,a10,redfin,oriole" "27,28,29,30,31" exit $? From 2731e9817b620598a7de0f8899612f0e771c9aa4 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 9 Jul 2023 10:44:40 +0200 Subject: [PATCH 21/94] Kotlin 1.9.0 - https://github.com/JetBrains/kotlin/releases/tag/v1.9.0 - https://github.com/google/ksp/releases/tag/1.9.0-1.0.11 - Compose compiler `1.5.0-dev-k1.9.0-6a60475e07f` --- gradle/libs.versions.toml | 6 +++--- settings.gradle.kts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20afadb36..be13af451 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidxActivity = "1.7.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" androidxComposeBom = "2023.06.01" -androidxComposeCompiler = "1.4.8" +androidxComposeCompiler = "1.5.0-dev-k1.9.0-6a60475e07f" androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxCore = "1.9.0" androidxCoreSplashscreen = "1.0.0" @@ -38,11 +38,11 @@ hilt = "2.46.1" hiltExt = "1.0.0" jacoco = "0.8.7" junit4 = "4.13.2" -kotlin = "1.8.22" +kotlin = "1.9.0" kotlinxCoroutines = "1.6.4" kotlinxDatetime = "0.4.0" kotlinxSerializationJson = "1.5.1" -ksp = "1.8.22-1.0.11" +ksp = "1.9.0-1.0.11" lint = "30.3.1" okhttp = "4.10.0" protobuf = "3.23.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index d0c477b3d..390416cfb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://androidx.dev/storage/compose-compiler/repository/") } } rootProject.name = "nowinandroid" From 8b7f6303c84317196eee96b8d986a55ed632bc1c Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 9 Jul 2023 10:06:25 +0100 Subject: [PATCH 22/94] Align `targetSdk` to 34 in `AndroidTestConventionPlugin.kt` Initial update was applied in https://github.com/android/nowinandroid/pull/814 --- .../convention/src/main/kotlin/AndroidTestConventionPlugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt index 286871010..e48d75757 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -31,10 +31,10 @@ class AndroidTestConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 31 + defaultConfig.targetSdk = 34 configureGradleManagedDevices(this) } } } -} \ No newline at end of file +} From e18f7cb3764cbaf30658ed2944230f10d2f2c258 Mon Sep 17 00:00:00 2001 From: Murat Yener Date: Mon, 10 Jul 2023 16:18:30 -0700 Subject: [PATCH 23/94] addressed comments --- .../apps/nowinandroid/foryou/ForYouActions.kt | 14 ++++++++++++++ .../nowinandroid/interests/InterestsActions.kt | 14 -------------- .../ScrollTopicListPowerMetricsBenchmark.kt | 5 +++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt index 5e4952fbb..8599bddd0 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt @@ -88,3 +88,17 @@ fun MacrobenchmarkScope.forYouScrollFeedDownUp() { val feedList = device.findObject(By.res("forYou:feed")) device.flingElementDownUp(feedList) } + +fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) { + when (isDark) { + true -> device.findObject(By.text("Dark")).click() + false -> device.findObject(By.text("Light")).click() + } + device.waitForIdle() + device.findObject(By.text("OK")).click() + + // Wait until the top app bar is visible on screen + device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000) + val topAppBar = device.findObject(By.res("niaTopAppBar")) + topAppBar.wait(Until.hasObject(By.text("Now in Android")), 2_000) +} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt index a6420a7fd..e94369ce2 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt @@ -48,17 +48,3 @@ fun MacrobenchmarkScope.interestsToggleBookmarked() { checkable.click() device.waitForIdle() } - -fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) { - when (isDark) { - true -> device.findObject(By.text("Dark")).click() - false -> device.findObject(By.text("Light")).click() - } - device.waitForIdle() - device.findObject(By.text("OK")).click() - - // Wait until interests are shown on screen - device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000) - val topAppBar = device.findObject(By.res("niaTopAppBar")) - topAppBar.wait(Until.hasObject(By.text("Now in Android")), 2_000) -} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt index 8ede75e4b..13c6f55e3 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import com.google.samples.apps.nowinandroid.allowNotifications import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp import com.google.samples.apps.nowinandroid.foryou.forYouSelectTopics import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent +import com.google.samples.apps.nowinandroid.foryou.setAppTheme import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -67,7 +68,7 @@ class ScrollTopicListPowerMetricsBenchmark { pressHome() startActivityAndWait() allowNotifications() - // Navigate to interests screen + // Navigate to Settings device.findObject(By.desc("Settings")).click() device.waitForIdle() setAppTheme(isDark) From 805914dadeb928ecbb090acd6a6f43ddf6267441 Mon Sep 17 00:00:00 2001 From: blackbracken Date: Thu, 13 Jul 2023 18:52:53 +0900 Subject: [PATCH 24/94] Call getNewsResourceIds instead of getNewsResources --- .../repository/OfflineFirstNewsRepository.kt | 5 +--- .../data/testdoubles/TestNewsResourceDao.kt | 27 +++++++++++++++++++ .../core/database/dao/NewsResourceDao.kt | 27 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt index b18bb9044..ce395ad1c 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt @@ -80,17 +80,14 @@ class OfflineFirstNewsRepository @Inject constructor( val hasOnboarded = userData.shouldHideOnboarding val followedTopicIds = userData.followedTopics - // TODO: Make this more efficient, there is no need to retrieve populated - // news resources when all that's needed are the ids val existingNewsResourceIdsThatHaveChanged = when { - hasOnboarded -> newsResourceDao.getNewsResources( + hasOnboarded -> newsResourceDao.getNewsResourceIds( useFilterTopicIds = true, filterTopicIds = followedTopicIds, useFilterNewsIds = true, filterNewsIds = changedIds.toSet(), ) .first() - .map { it.entity.id } .toSet() // No need to retrieve anything if notifications won't be sent else -> emptySet() diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt index d5d8932e7..6e5c45305 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt @@ -67,6 +67,33 @@ class TestNewsResourceDao : NewsResourceDao { result } + override fun getNewsResourceIds( + useFilterTopicIds: Boolean, + filterTopicIds: Set, + useFilterNewsIds: Boolean, + filterNewsIds: Set, + ): Flow> = + entitiesStateFlow + .map { newsResourceEntities -> + newsResourceEntities.map { entity -> + entity.asPopulatedNewsResource(topicCrossReferences) + } + } + .map { resources -> + var result = resources + if (useFilterTopicIds) { + result = result.filter { resource -> + resource.topics.any { it.id in filterTopicIds } + } + } + if (useFilterNewsIds) { + result = result.filter { resource -> + resource.entity.id in filterNewsIds + } + } + result.map { it.entity.id } + } + override suspend fun insertOrIgnoreNewsResources( entities: List, ): List { diff --git a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt index a05507a8b..a3e1e158b 100644 --- a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt +++ b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt @@ -65,6 +65,33 @@ interface NewsResourceDao { filterNewsIds: Set = emptySet(), ): Flow> + @Transaction + @Query( + value = """ + SELECT id FROM news_resources + WHERE + CASE WHEN :useFilterNewsIds + THEN id IN (:filterNewsIds) + ELSE 1 + END + AND + CASE WHEN :useFilterTopicIds + THEN id IN + ( + SELECT news_resource_id FROM news_resources_topics + WHERE topic_id IN (:filterTopicIds) + ) + ELSE 1 + END + ORDER BY publish_date DESC + """) + fun getNewsResourceIds( + useFilterTopicIds: Boolean = false, + filterTopicIds: Set = emptySet(), + useFilterNewsIds: Boolean = false, + filterNewsIds: Set = emptySet(), + ): Flow> + /** * Inserts [entities] into the db if they don't exist, and ignores those that do */ From 7f62b725a6e3cb0a607af62d06ea6583b463912b Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 13 Jul 2023 11:34:45 +0100 Subject: [PATCH 25/94] Bump version to code=7, name=0.1.1 Change-Id: I2d8fa940dc5e4c14e3c9df650bc1f1432a330f02 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a1450258..c3c0aca4f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,8 @@ plugins { android { defaultConfig { applicationId = "com.google.samples.apps.nowinandroid" - versionCode = 6 - versionName = "0.1.0" // X.Y.Z; X = Major, Y = minor, Z = Patch level + versionCode = 7 + versionName = "0.1.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level // Custom test runner to set up Hilt dependency graph testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" From aa66cbb596e1b76f288d9e39cf0fbe3eed732503 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Sun, 16 Jul 2023 22:33:37 +0200 Subject: [PATCH 26/94] allow notifications --- .../samples/apps/nowinandroid/startup/StartupBenchmark.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt index 8e396eda3..dd79b319f 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt @@ -27,6 +27,7 @@ import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import com.google.samples.apps.nowinandroid.PACKAGE_NAME +import com.google.samples.apps.nowinandroid.allowNotifications import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent import org.junit.Rule import org.junit.Test @@ -86,6 +87,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { }, ) { startActivityAndWait() + allowNotifications() // Waits until the content is ready to capture Time To Full Display forYouWaitForContent() } From e9a16e437def5feab1c94e46fadeed671e065604 Mon Sep 17 00:00:00 2001 From: blackbracken Date: Wed, 19 Jul 2023 02:45:29 +0900 Subject: [PATCH 27/94] add kdoc for getNewsResourceIds --- .../apps/nowinandroid/core/database/dao/NewsResourceDao.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt index a3e1e158b..4134f569b 100644 --- a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt +++ b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt @@ -65,6 +65,9 @@ interface NewsResourceDao { filterNewsIds: Set = emptySet(), ): Flow> + /** + * Fetches ids of news resources that match the query parameters + */ @Transaction @Query( value = """ From b570be2abbefc9176b726baec289be7618ebb7a6 Mon Sep 17 00:00:00 2001 From: blackbracken Date: Wed, 19 Jul 2023 02:53:29 +0900 Subject: [PATCH 28/94] reformat by spotless --- .../apps/nowinandroid/core/database/dao/NewsResourceDao.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt index 4134f569b..0ad1e4f7d 100644 --- a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt +++ b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt @@ -70,7 +70,7 @@ interface NewsResourceDao { */ @Transaction @Query( - value = """ + value = """ SELECT id FROM news_resources WHERE CASE WHEN :useFilterNewsIds @@ -87,7 +87,8 @@ interface NewsResourceDao { ELSE 1 END ORDER BY publish_date DESC - """) + """, + ) fun getNewsResourceIds( useFilterTopicIds: Boolean = false, filterTopicIds: Set = emptySet(), From f7ed38182d5fdd101b02279e9aa879b38751154d Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Thu, 25 May 2023 21:17:52 +0200 Subject: [PATCH 29/94] Add protobuf generated sources to the sourceSets And update to version 3.23.4. --- core/datastore/build.gradle.kts | 7 +++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index a9ec7a78f..d6ca7ebcd 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -52,6 +52,13 @@ protobuf { } } +androidComponents.beforeVariants { + android.sourceSets.register(it.name) { + java.srcDir(buildDir.resolve("generated/source/proto/${it.name}/java")) + kotlin.srcDir(buildDir.resolve("generated/source/proto/${it.name}/kotlin")) + } +} + dependencies { implementation(project(":core:common")) implementation(project(":core:model")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be13af451..bb356f006 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ kotlinxSerializationJson = "1.5.1" ksp = "1.9.0-1.0.11" lint = "30.3.1" okhttp = "4.10.0" -protobuf = "3.23.0" +protobuf = "3.23.4" protobufPlugin = "0.9.3" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "1.0.0" From ba82b06cc3cc1a169d8adead9584aea42e3781b7 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 9 Jul 2023 11:22:15 +0200 Subject: [PATCH 30/94] Fix Lint `RememberReturnType` issue with explicit type --- .../com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 2457af900..1560a74eb 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -183,7 +183,7 @@ class NiaAppStateTest { @Composable private fun rememberTestNavController(): TestNavHostController { val context = LocalContext.current - val navController = remember { + return remember { TestNavHostController(context).apply { navigatorProvider.addNavigator(ComposeNavigator()) graph = createGraph(startDestination = "a") { @@ -193,5 +193,4 @@ private fun rememberTestNavController(): TestNavHostController { } } } - return navController } From bc0a7e63a13bbaf2c0ace7d25e236ad2906cbc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Wed, 19 Jul 2023 09:18:51 +0200 Subject: [PATCH 31/94] Update Compose compiler version to 1.5.0 Change-Id: Ieddb8410a592bb38c5764b7288fd4828306d6743 --- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb356f006..2dd3f379d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidxActivity = "1.7.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" androidxComposeBom = "2023.06.01" -androidxComposeCompiler = "1.5.0-dev-k1.9.0-6a60475e07f" +androidxComposeCompiler = "1.5.0" androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxCore = "1.9.0" androidxCoreSplashscreen = "1.0.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index 390416cfb..d0c477b3d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,7 +28,6 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven("https://androidx.dev/storage/compose-compiler/repository/") } } rootProject.name = "nowinandroid" From c64fb895b4f3ff09481b33fc6dbc91f51472539d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Wed, 19 Jul 2023 09:19:51 +0200 Subject: [PATCH 32/94] Upgrade Hilt to 2.47 Change-Id: I344cb7542107f134b71c62b652211eba370762ec --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2dd3f379d..71b692851 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ firebasePerfPlugin = "1.4.2" gmsPlugin = "4.3.14" googleOss = "17.0.1" googleOssPlugin = "0.10.6" -hilt = "2.46.1" +hilt = "2.47" hiltExt = "1.0.0" jacoco = "0.8.7" junit4 = "4.13.2" From 05e1656ea44b0cdcd7eef726323e6ad980461de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Wed, 19 Jul 2023 09:23:14 +0200 Subject: [PATCH 33/94] Update lint to 31.0.2 Change-Id: I11a2cf41f1428931794cf26ebc655ccb0cda33ab --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71b692851..99248c8bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ kotlinxCoroutines = "1.6.4" kotlinxDatetime = "0.4.0" kotlinxSerializationJson = "1.5.1" ksp = "1.9.0-1.0.11" -lint = "30.3.1" +lint = "31.0.2" okhttp = "4.10.0" protobuf = "3.23.4" protobufPlugin = "0.9.3" From 3a0ce45c1b791c8507926380d211fa069f5a6a2e Mon Sep 17 00:00:00 2001 From: Don Turner Date: Wed, 19 Jul 2023 10:23:27 +0100 Subject: [PATCH 34/94] Use newer lint version Change-Id: Ifb8f9495a01b4916c3e1ec0cd6ff4bd47080df37 --- gradle.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle.properties b/gradle.properties index b57dc01ed..22e1dc72e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,3 +38,7 @@ android.nonTransitiveRClass=true # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false + +# Use newer lint version +# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html +android.experimental.lint.version=8.1.0-rc01 \ No newline at end of file From 5fa8c1d7263526e998cdcd5e14425dc6cffe77ac Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Wed, 19 Jul 2023 12:35:19 +0200 Subject: [PATCH 35/94] Update gradle.properties --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 22e1dc72e..de14513e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,6 +39,6 @@ android.nonTransitiveRClass=true android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false -# Use newer lint version +# Use newer lint version to support Kotlin 1.9 and corresponding kotlinx-metadata-jvm # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html android.experimental.lint.version=8.1.0-rc01 \ No newline at end of file From 31b4841cb222110815361bc33815ef680a6423a5 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 11 May 2023 09:31:34 +0100 Subject: [PATCH 36/94] Add scrollbars to app --- .../component/scrollbar/AppScrollbars.kt | 216 +++++++++++ .../scrollbar/LazyScrollbarUtilities.kt | 156 ++++++++ .../component/scrollbar/Scrollbar.kt | 340 ++++++++++++++++++ .../component/scrollbar/ScrollbarExt.kt | 104 ++++++ .../component/scrollbar/ThumbExt.kt | 74 ++++ .../feature/bookmarks/BookmarksScreen.kt | 97 +++-- .../feature/foryou/ForYouScreen.kt | 231 +++++++----- .../feature/interests/TabContent.kt | 69 +++- .../nowinandroid/feature/topic/TopicScreen.kt | 103 ++++-- 9 files changed, 1224 insertions(+), 166 deletions(-) create mode 100644 core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt create mode 100644 core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt create mode 100644 core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt create mode 100644 core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt create mode 100644 core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt new file mode 100644 index 000000000..bed9e6b44 --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.Orientation.Horizontal +import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Active +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Dormant +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Inactive +import kotlinx.coroutines.delay + +/** + * A [Scrollbar] that allows for fast scrolling of content. + * Its thumb disappears when the scrolling container is dormant. + * @param modifier a [Modifier] for the [Scrollbar] + * @param state the driving state for the [Scrollbar] + * @param scrollInProgress a flag indicating if the scrolling container for the scrollbar is + * currently scrolling + * @param orientation the orientation of the scrollbar + * @param onThumbMoved the fast scroll implementation + */ +@Composable +fun FastScrollbar( + modifier: Modifier = Modifier, + state: ScrollbarState, + scrollInProgress: Boolean, + orientation: Orientation, + onThumbMoved: (Float) -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + Scrollbar( + modifier = modifier, + orientation = orientation, + interactionSource = interactionSource, + state = state, + thumb = { + FastScrollbarThumb( + scrollInProgress = scrollInProgress, + interactionSource = interactionSource, + orientation = orientation, + ) + }, + onThumbMoved = onThumbMoved, + ) +} + +/** + * A simple [Scrollbar]. + * Its thumb disappears when the scrolling container is dormant. + * @param modifier a [Modifier] for the [Scrollbar] + * @param state the driving state for the [Scrollbar] + * @param scrollInProgress a flag indicating if the scrolling container for the scrollbar is + * currently scrolling + * @param orientation the orientation of the scrollbar + */ +@Composable +fun DecorativeScrollbar( + modifier: Modifier = Modifier, + state: ScrollbarState, + scrollInProgress: Boolean, + orientation: Orientation, +) { + val interactionSource = remember { MutableInteractionSource() } + Scrollbar( + modifier = modifier, + orientation = orientation, + interactionSource = interactionSource, + state = state, + thumb = { + DecorativeScrollbarThumb( + interactionSource = interactionSource, + scrollInProgress = scrollInProgress, + orientation = orientation, + ) + }, + ) +} + +/** + * A scrollbar thumb that is intended to also be a touch target for fast scrolling. + */ +@Composable +private fun FastScrollbarThumb( + scrollInProgress: Boolean, + interactionSource: InteractionSource, + orientation: Orientation, +) { + Box( + modifier = Modifier + .run { + when (orientation) { + Vertical -> width(12.dp).fillMaxHeight() + Horizontal -> height(12.dp).fillMaxWidth() + } + } + .background( + color = scrollbarThumbColor( + scrollInProgress = scrollInProgress, + interactionSource = interactionSource, + ), + shape = RoundedCornerShape(16.dp), + ), + ) +} + +/** + * A decorative scrollbar thumb for communicating a user's position in a list solely. + */ +@Composable +private fun DecorativeScrollbarThumb( + scrollInProgress: Boolean, + interactionSource: InteractionSource, + orientation: Orientation, +) { + Box( + modifier = Modifier + .run { + when (orientation) { + Vertical -> width(2.dp).fillMaxHeight() + Horizontal -> height(2.dp).fillMaxWidth() + } + } + .background( + color = scrollbarThumbColor( + scrollInProgress = scrollInProgress, + interactionSource = interactionSource, + ), + shape = RoundedCornerShape(16.dp), + ), + ) +} + +/** + * The color of the scrollbar thumb as a function of its interaction state. + * @param scrollInProgress if the scrolling container is currently scrolling + * @param interactionSource source of interactions in the scrolling container + */ +@Composable +private fun scrollbarThumbColor( + scrollInProgress: Boolean, + interactionSource: InteractionSource, +): Color { + var state by remember { mutableStateOf(Active) } + val pressed by interactionSource.collectIsPressedAsState() + val hovered by interactionSource.collectIsHoveredAsState() + val dragged by interactionSource.collectIsDraggedAsState() + val active = pressed || hovered || dragged || scrollInProgress + + val color by animateColorAsState( + targetValue = when (state) { + Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f) + Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) + Dormant -> Color.Transparent + }, + animationSpec = SpringSpec( + stiffness = Spring.StiffnessLow, + ), + label = "Scrollbar thumb color", + ) + LaunchedEffect(active) { + when (active) { + true -> state = Active + false -> { + state = Inactive + delay(2_000) + state = Dormant + } + } + } + + return color +} + +private enum class ThumbState { + Active, Inactive, Dormant +} diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt new file mode 100644 index 000000000..d45c5781a --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar + +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlin.math.abs +import kotlin.math.min + +/** + * Calculates the [ScrollbarState] for lazy layouts. + * @param itemsAvailable the total amount of items available to scroll in the layout. + * @param visibleItems a list of items currently visible in the layout. + * @param firstItemIndex a function for interpolating the first visible index in the lazy layout + * as scrolling progresses for smooth and linear scrollbar thumb progression. + * [itemsAvailable]. + * @param reverseLayout if the items in the backing lazy layout are laid out in reverse order. + * */ +@Composable +internal inline fun LazyState.scrollbarState( + itemsAvailable: Int, + crossinline visibleItems: LazyState.() -> List, + crossinline firstItemIndex: LazyState.(List) -> Float, + crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float, + crossinline reverseLayout: LazyState.() -> Boolean, +): ScrollbarState { + var state by remember { mutableStateOf(ScrollbarState.FULL) } + + LaunchedEffect( + key1 = this, + key2 = itemsAvailable, + ) { + snapshotFlow { + if (itemsAvailable == 0) return@snapshotFlow null + + val visibleItemsInfo = visibleItems(this@scrollbarState) + if (visibleItemsInfo.isEmpty()) return@snapshotFlow null + + // Add the item offset for interpolation between scroll indices + val firstIndex = min( + a = firstItemIndex(visibleItemsInfo), + b = itemsAvailable.toFloat(), + ) + if (firstIndex.isNaN()) return@snapshotFlow null + + val itemsVisible = visibleItemsInfo.sumOf { + itemPercentVisible(it).toDouble() + }.toFloat() + + val thumbTravelPercent = min( + a = firstIndex / itemsAvailable, + b = 1f, + ) + val thumbSizePercent = min( + a = itemsVisible / itemsAvailable, + b = 1f, + ) + + ScrollbarState( + thumbSizePercent = thumbSizePercent, + thumbTravelPercent = when { + reverseLayout() -> 1f - thumbTravelPercent + else -> thumbTravelPercent + }, + ) + } + .filterNotNull() + .distinctUntilChanged() + .collect { state = it } + } + return state +} + +/** + * Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar + * progression. + * @param visibleItems a list of items currently visible in the layout. + * @param itemSize a lookup function for the size of an item in the layout. + * @param offset a lookup function for the offset of an item relative to the start of the view port. + * @param nextItemOnMainAxis a lookup function for the next item on the main axis in the direction + * of the scroll. + * @param itemIndex a lookup function for index of an item in the layout relative to + * the total amount of items available. + * */ +internal inline fun LazyState.interpolateFirstItemIndex( + visibleItems: List, + crossinline itemSize: LazyState.(LazyStateItem) -> Int, + crossinline offset: LazyState.(LazyStateItem) -> Int, + crossinline nextItemOnMainAxis: LazyState.(LazyStateItem) -> LazyStateItem?, + crossinline itemIndex: (LazyStateItem) -> Int, +): Float { + if (visibleItems.isEmpty()) return 0f + + val firstItem = visibleItems.first() + val firstItemIndex = itemIndex(firstItem) + + if (firstItemIndex < 0) return Float.NaN + + val itemOffset = offset(firstItem).toFloat() + val offsetPercentage = abs(itemOffset) / itemSize(firstItem) + + val nextItem = nextItemOnMainAxis(firstItem) ?: return firstItemIndex + offsetPercentage + + val nextItemIndex = itemIndex(nextItem) + + return firstItemIndex + ((nextItemIndex - firstItemIndex) * offsetPercentage) +} + +/** + * Returns the percentage of an item that is currently visible in the view port. + * @param itemSize the size of the item + * @param itemStartOffset the start offset of the item relative to the view port start + * @param viewportStartOffset the start offset of the view port + * @param viewportEndOffset the end offset of the view port + */ +internal fun itemVisibilityPercentage( + itemSize: Int, + itemStartOffset: Int, + viewportStartOffset: Int, + viewportEndOffset: Int, +): Float { + if (itemSize == 0) return 0f + val itemEnd = itemStartOffset + itemSize + val startOffset = when { + itemStartOffset > viewportStartOffset -> 0 + else -> abs(abs(viewportStartOffset) - abs(itemStartOffset)) + } + val endOffset = when { + itemEnd < viewportEndOffset -> 0 + else -> abs(abs(itemEnd) - abs(viewportEndOffset)) + } + val size = itemSize.toFloat() + return (size - startOffset - endOffset) / size +} diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt new file mode 100644 index 000000000..4984946bc --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -0,0 +1,340 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import androidx.compose.ui.util.packFloats +import androidx.compose.ui.util.unpackFloat1 +import androidx.compose.ui.util.unpackFloat2 +import kotlinx.coroutines.delay +import kotlin.math.max +import kotlin.math.min + +/** + * Class definition for the core properties of a scroll bar + */ +@Immutable +@JvmInline +value class ScrollbarState internal constructor( + internal val packedValue: Long, +) { + companion object { + val FULL = ScrollbarState( + thumbSizePercent = 1f, + thumbTravelPercent = 0f, + ) + } +} + +/** + * Class definition for the core properties of a scroll bar track + */ +@Immutable +@JvmInline +private value class ScrollbarTrack( + val packedValue: Long, +) { + constructor( + max: Float, + min: Float, + ) : this(packFloats(max, min)) +} + +/** + * Creates a scrollbar state with the listed properties + * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size + * @param thumbTravelPercent the distance the thumb has traveled as a percentage of total track size + */ +fun ScrollbarState( + thumbSizePercent: Float, + thumbTravelPercent: Float, +) = ScrollbarState( + packFloats( + val1 = thumbSizePercent, + val2 = thumbTravelPercent, + ), +) + +/** + * Returns the thumb size of the scrollbar as a percentage of the total track size + */ +val ScrollbarState.thumbSizePercent + get() = unpackFloat1(packedValue) + +/** + * Returns the distance the thumb has traveled as a percentage of total track size + */ +val ScrollbarState.thumbTravelPercent + get() = unpackFloat2(packedValue) + +/** + * Returns the size of the scrollbar track in pixels + */ +private val ScrollbarTrack.size + get() = unpackFloat2(packedValue) - unpackFloat1(packedValue) + +/** + * Returns the position of the scrollbar thumb on the track as a percentage + */ +private fun ScrollbarTrack.thumbPosition( + dimension: Float, +): Float = max( + a = min( + a = dimension / size, + b = 1f, + ), + b = 0f, +) + +/** + * Returns the value of [offset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(offset: Offset) = when (this) { + Orientation.Horizontal -> offset.x + Orientation.Vertical -> offset.y +} + +/** + * Returns the value of [intSize] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intSize: IntSize) = when (this) { + Orientation.Horizontal -> intSize.width + Orientation.Vertical -> intSize.height +} + +/** + * Returns the value of [intOffset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { + Orientation.Horizontal -> intOffset.x + Orientation.Vertical -> intOffset.y +} + +/** + * A Composable for drawing a Scrollbar + * @param orientation the scroll direction of the scrollbar + * @param state the state describing the position of the scrollbar + * @param minThumbSize the minimum size of the scrollbar thumb + * @param interactionSource allows for observing the state of the scroll bar + * @param thumb a composable for drawing the scrollbar thumb + * @param onThumbMoved an function for reacting to scroll bar interactions, for example implementing + * a fast scroll + */ +@Composable +fun Scrollbar( + modifier: Modifier = Modifier, + orientation: Orientation, + state: ScrollbarState, + minThumbSize: Dp = 40.dp, + interactionSource: MutableInteractionSource? = null, + thumb: @Composable () -> Unit, + onThumbMoved: ((Float) -> Unit)? = null, +) { + val localDensity = LocalDensity.current + var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) } + var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } + var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } + + var track by remember { mutableStateOf(ScrollbarTrack(0)) } + val updatedState by rememberUpdatedState(state) + val updatedTrack by rememberUpdatedState(track) + + val thumbSizePercent = state.thumbSizePercent + val thumbTravelPercent = when { + interactionThumbTravelPercent.isNaN() -> state.thumbTravelPercent + else -> interactionThumbTravelPercent + } + val thumbSizePx = max( + a = thumbSizePercent * track.size, + b = with(localDensity) { minThumbSize.toPx() }, + ) + + val thumbSizeDp by animateDpAsState( + targetValue = with(localDensity) { thumbSizePx.toDp() }, + label = "thumb size", + ) + + val thumbTravelPx = min( + a = track.size * thumbTravelPercent, + b = track.size - thumbSizePx, + ) + + val draggableState = rememberDraggableState { delta -> + if (draggedOffset == Offset.Unspecified) return@rememberDraggableState + + draggedOffset = when (orientation) { + Orientation.Vertical -> draggedOffset.copy( + y = draggedOffset.y + delta, + ) + + Orientation.Horizontal -> draggedOffset.copy( + x = draggedOffset.x + delta, + ) + } + } + Box( + modifier = modifier + .run { + when (orientation) { + Orientation.Vertical -> fillMaxHeight() + Orientation.Horizontal -> fillMaxWidth() + } + } + .onGloballyPositioned { coordinates -> + val position = orientation.valueOf(coordinates.positionInRoot()) + track = ScrollbarTrack( + max = position, + min = position + orientation.valueOf(coordinates.size), + ) + } + // Process scrollbar presses + .pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + val initialPress = PressInteraction.Press(offset) + + interactionSource?.tryEmit(initialPress) + pressedOffset = offset + + interactionSource?.tryEmit( + if (tryAwaitRelease()) { + PressInteraction.Release(initialPress) + } else { + PressInteraction.Cancel(initialPress) + }, + ) + pressedOffset = Offset.Unspecified + }, + ) + } + // Process scrollbar drags + .draggable( + state = draggableState, + orientation = orientation, + interactionSource = interactionSource, + onDragStarted = { startedPosition: Offset -> + draggedOffset = startedPosition + }, + onDragStopped = { + draggedOffset = Offset.Unspecified + }, + ), + ) { + val offset = max( + a = with(localDensity) { thumbTravelPx.toDp() }, + b = 0.dp, + ) + Box( + modifier = Modifier + .align(Alignment.TopStart) + .run { + when (orientation) { + Orientation.Horizontal -> width(thumbSizeDp) + Orientation.Vertical -> height(thumbSizeDp) + } + } + .offset( + y = when (orientation) { + Orientation.Horizontal -> 0.dp + Orientation.Vertical -> offset + }, + x = when (orientation) { + Orientation.Horizontal -> offset + Orientation.Vertical -> 0.dp + }, + ), + ) { + thumb() + } + } + + if (onThumbMoved == null) return + + // Process presses + LaunchedEffect(pressedOffset) { + if (pressedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@LaunchedEffect + } + + var currentTravel = updatedState.thumbTravelPercent + val destinationTravel = updatedTrack.thumbPosition( + dimension = orientation.valueOf(pressedOffset), + ) + val isPositive = currentTravel < destinationTravel + // TODO: Come up with a better heuristic for jumps + val delta = if (isPositive) 0.1f else -0.1f + + while (currentTravel != destinationTravel) { + currentTravel = + if (isPositive) { + min(currentTravel + delta, destinationTravel) + } else { + max(currentTravel + delta, destinationTravel) + } + onThumbMoved(currentTravel) + interactionThumbTravelPercent = currentTravel + // TODO: Define this more thoroughly + delay(100) + } + } + + // Process drags + LaunchedEffect(draggedOffset) { + if (draggedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@LaunchedEffect + } + val currentTravel = updatedTrack.thumbPosition( + dimension = orientation.valueOf(draggedOffset), + ) + onThumbMoved(currentTravel) + interactionThumbTravelPercent = currentTravel + } +} diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt new file mode 100644 index 000000000..aea4cd661 --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyListState]. + * + * @param itemsAvailable the total amount of items available to scroll in the lazy list. + * @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable]. + */ +@Composable +fun LazyListState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, +): ScrollbarState = + scrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + firstItemIndex = { visibleItems -> + interpolateFirstItemIndex( + visibleItems = visibleItems, + itemSize = { it.size }, + offset = { it.offset }, + nextItemOnMainAxis = { first -> visibleItems.find { it != first } }, + itemIndex = itemIndex, + ) + }, + itemPercentVisible = itemPercentVisible@{ itemInfo -> + itemVisibilityPercentage( + itemSize = itemInfo.size, + itemStartOffset = itemInfo.offset, + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + }, + reverseLayout = { layoutInfo.reverseLayout }, + ) + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] + * + * @param itemsAvailable the total amount of items available to scroll in the grid. + * @param itemIndex a lookup function for index of an item in the grid relative to [itemsAvailable]. + */ +@Composable +fun LazyGridState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, +): ScrollbarState = + scrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + firstItemIndex = { visibleItems -> + interpolateFirstItemIndex( + visibleItems = visibleItems, + itemSize = { + layoutInfo.orientation.valueOf(it.size) + }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + when (layoutInfo.orientation) { + Orientation.Vertical -> visibleItems.find { + it != first && it.row != first.row + } + + Orientation.Horizontal -> visibleItems.find { + it != first && it.column != first.column + } + } + }, + itemIndex = itemIndex, + ) + }, + itemPercentVisible = itemPercentVisible@{ itemInfo -> + itemVisibilityPercentage( + itemSize = layoutInfo.orientation.valueOf(itemInfo.size), + itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + }, + reverseLayout = { layoutInfo.reverseLayout }, + ) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt new file mode 100644 index 000000000..f03e21c85 --- /dev/null +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue + +/** + * Remembers a function to react to [Scrollbar] thumb position movements for a [LazyListState] + * @param itemsAvailable the amount of items in the list. + */ +@Composable +fun LazyListState.rememberThumbInteractions( + itemsAvailable: Int, +): (Float) -> Unit = rememberThumbInteractions( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + +/** + * Remembers a function to react to [Scrollbar] thumb position movements for a [LazyGridState] + * @param itemsAvailable the amount of items in the grid. + */ +@Composable +fun LazyGridState.rememberThumbInteractions( + itemsAvailable: Int, +): (Float) -> Unit = rememberThumbInteractions( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + +/** + * Generic function to react to [Scrollbar] thumb interactions in a lazy layout. + * @param itemsAvailable the total amount of items available to scroll in the layout. + * @param scroll a function to be invoked when an index has been identified to scroll to. + */ +@Composable +private inline fun rememberThumbInteractions( + itemsAvailable: Int, + crossinline scroll: suspend (index: Int) -> Unit, +): (Float) -> Unit { + var percentage by remember { mutableStateOf(Float.NaN) } + val itemCount by rememberUpdatedState(itemsAvailable) + + LaunchedEffect(percentage) { + if (percentage.isNaN()) return@LaunchedEffect + val indexToFind = (itemCount * percentage).toInt() + scroll(indexToFind) + } + return remember { + { percentage = it } + } +} diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 0f15e29b0..08776b210 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -18,11 +18,13 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -57,6 +59,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource @@ -112,9 +117,13 @@ internal fun BookmarksScreen( if (shouldDisplayUndoBookmark) { val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) if (snackBarResult) { - undoBookmarkRemoval() - } else { - clearUndoState() + + undoBookmarkRemoval() + } + + else { + clearUndoState() + } } } @@ -130,18 +139,20 @@ internal fun BookmarksScreen( onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } - when (feedState) { - Loading -> LoadingState(modifier) - is Success -> if (feedState.feed.isNotEmpty()) { - BookmarksGrid( - feedState, - removeFromBookmarks, - onNewsResourceViewed, - onTopicClick, - modifier, - ) - } else { - EmptyState(modifier) + + when (feedState) { + Loading -> LoadingState(modifier) + is Success -> if (feedState.feed.isNotEmpty()) { + BookmarksGrid( + feedState, + removeFromBookmarks, + onNewsResourceViewed, + onTopicClick, + modifier, + ) + } else { + EmptyState(modifier) + } } @@ -169,25 +180,49 @@ private fun BookmarksGrid( ) { val scrollableState = rememberLazyGridState() TrackScrollJank(scrollableState = scrollableState, stateName = "bookmarks:grid") - LazyVerticalGrid( - columns = Adaptive(300.dp), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - state = scrollableState, + Box( modifier = modifier - .fillMaxSize() - .testTag("bookmarks:feed"), + .fillMaxSize(), ) { - newsFeed( - feedState = feedState, - onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) }, - onNewsResourceViewed = onNewsResourceViewed, - onTopicClick = onTopicClick, - ) - item(span = { GridItemSpan(maxLineSpan) }) { - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + LazyVerticalGrid( + columns = Adaptive(300.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + state = scrollableState, + modifier = Modifier + .fillMaxSize() + .testTag("bookmarks:feed"), + ) { + newsFeed( + feedState = feedState, + onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) }, + onNewsResourceViewed = onNewsResourceViewed, + onTopicClick = onTopicClick, + ) + item(span = { GridItemSpan(maxLineSpan) }) { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } } + val itemsAvailable = when (feedState) { + Loading -> 1 + is Success -> feedState.feed.size + } + val scrollbarState = scrollableState.scrollbarState( + itemsAvailable = itemsAvailable, + ) + FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 2.dp) + .align(Alignment.CenterEnd), + state = scrollbarState, + orientation = Orientation.Vertical, + scrollInProgress = scrollableState.isScrollInProgress, + onThumbMoved = scrollableState.rememberThumbInteractions( + itemsAvailable = itemsAvailable, + ), + ) } } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index eaa0c58fa..db33d9e93 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -33,6 +34,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -87,6 +89,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicA import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DecorativeScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource @@ -144,75 +150,96 @@ internal fun ForYouScreen( // This code should be called when the UI is ready for use and relates to Time To Full Display. ReportDrawnWhen { !isSyncing && !isOnboardingLoading && !isFeedLoading } + val itemsAvailable = feedItemsSize(feedState, onboardingUiState) + val state = rememberLazyGridState() + val scrollbarState = state.scrollbarState( + itemsAvailable = itemsAvailable, + ) TrackScrollJank(scrollableState = state, stateName = "forYou:feed") - LazyVerticalGrid( - columns = Adaptive(300.dp), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + Box( modifier = modifier - .fillMaxSize() - .testTag("forYou:feed"), - state = state, + .fillMaxSize(), ) { - onboarding( - onboardingUiState = onboardingUiState, - onTopicCheckedChanged = onTopicCheckedChanged, - saveFollowedTopics = saveFollowedTopics, - // Custom LayoutModifier to remove the enforced parent 16.dp contentPadding - // from the LazyVerticalGrid and enable edge-to-edge scrolling for this section - interestsItemModifier = Modifier.layout { measurable, constraints -> - val placeable = measurable.measure( - constraints.copy( - maxWidth = constraints.maxWidth + 32.dp.roundToPx(), - ), - ) - layout(placeable.width, placeable.height) { - placeable.place(0, 0) - } - }, - ) + LazyVerticalGrid( + columns = Adaptive(300.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .testTag("forYou:feed"), + state = state, + ) { + onboarding( + onboardingUiState = onboardingUiState, + onTopicCheckedChanged = onTopicCheckedChanged, + saveFollowedTopics = saveFollowedTopics, + // Custom LayoutModifier to remove the enforced parent 16.dp contentPadding + // from the LazyVerticalGrid and enable edge-to-edge scrolling for this section + interestsItemModifier = Modifier.layout { measurable, constraints -> + val placeable = measurable.measure( + constraints.copy( + maxWidth = constraints.maxWidth + 32.dp.roundToPx(), + ), + ) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + }, + ) - newsFeed( - feedState = feedState, - onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, - onNewsResourceViewed = onNewsResourceViewed, - onTopicClick = onTopicClick, - ) + newsFeed( + feedState = feedState, + onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, + onNewsResourceViewed = onNewsResourceViewed, + onTopicClick = onTopicClick, + ) - item(span = { GridItemSpan(maxLineSpan) }, contentType = "bottomSpacing") { - Column { - Spacer(modifier = Modifier.height(8.dp)) - // Add space for the content to clear the "offline" snackbar. - // TODO: Check that the Scaffold handles this correctly in NiaApp - // if (isOffline) Spacer(modifier = Modifier.height(48.dp)) - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + item(span = { GridItemSpan(maxLineSpan) }, contentType = "bottomSpacing") { + Column { + Spacer(modifier = Modifier.height(8.dp)) + // Add space for the content to clear the "offline" snackbar. + // TODO: Check that the Scaffold handles this correctly in NiaApp + // if (isOffline) Spacer(modifier = Modifier.height(48.dp)) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } } } - } - AnimatedVisibility( - visible = isSyncing || isFeedLoading || isOnboardingLoading, - enter = slideInVertically( - initialOffsetY = { fullHeight -> -fullHeight }, - ) + fadeIn(), - exit = slideOutVertically( - targetOffsetY = { fullHeight -> -fullHeight }, - ) + fadeOut(), - ) { - val loadingContentDescription = stringResource(id = R.string.for_you_loading) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), + AnimatedVisibility( + visible = isSyncing || isFeedLoading || isOnboardingLoading, + enter = slideInVertically( + initialOffsetY = { fullHeight -> -fullHeight }, + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { fullHeight -> -fullHeight }, + ) + fadeOut(), ) { - NiaOverlayLoadingWheel( + val loadingContentDescription = stringResource(id = R.string.for_you_loading) + Box( modifier = Modifier - .align(Alignment.Center), - contentDesc = loadingContentDescription, - ) + .fillMaxWidth() + .padding(top = 8.dp), + ) { + NiaOverlayLoadingWheel( + modifier = Modifier + .align(Alignment.Center), + contentDesc = loadingContentDescription, + ) + } } + FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 2.dp) + .align(Alignment.CenterEnd), + state = scrollbarState, + orientation = Orientation.Vertical, + scrollInProgress = state.isScrollInProgress, + onThumbMoved = state.rememberThumbInteractions( + itemsAvailable = itemsAvailable, + ), + ) } TrackScreenViewEvent(screenName = "ForYou") NotificationPermissionEffect() @@ -298,38 +325,52 @@ private fun TopicSelection( TrackScrollJank(scrollableState = lazyGridState, stateName = topicSelectionTestTag) - LazyHorizontalGrid( - state = lazyGridState, - rows = GridCells.Fixed(3), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(24.dp), + Box( modifier = modifier - // LazyHorizontalGrid has to be constrained in height. - // However, we can't set a fixed height because the horizontal grid contains - // vertical text that can be rescaled. - // When the fontScale is at most 1, we know that the horizontal grid will be at most - // 240dp tall, so this is an upper bound for when the font scale is at most 1. - // When the fontScale is greater than 1, the height required by the text inside the - // horizontal grid will increase by at most the same factor, so 240sp is a valid - // upper bound for how much space we need in that case. - // The maximum of these two bounds is therefore a valid upper bound in all cases. - .heightIn(max = max(240.dp, with(LocalDensity.current) { 240.sp.toDp() })) - .fillMaxWidth() - .testTag(topicSelectionTestTag), + .fillMaxWidth(), ) { - items( - items = onboardingUiState.topics, - key = { it.topic.id }, + LazyHorizontalGrid( + state = lazyGridState, + rows = GridCells.Fixed(3), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(24.dp), + modifier = Modifier + // LazyHorizontalGrid has to be constrained in height. + // However, we can't set a fixed height because the horizontal grid contains + // vertical text that can be rescaled. + // When the fontScale is at most 1, we know that the horizontal grid will be at most + // 240dp tall, so this is an upper bound for when the font scale is at most 1. + // When the fontScale is greater than 1, the height required by the text inside the + // horizontal grid will increase by at most the same factor, so 240sp is a valid + // upper bound for how much space we need in that case. + // The maximum of these two bounds is therefore a valid upper bound in all cases. + .heightIn(max = max(240.dp, with(LocalDensity.current) { 240.sp.toDp() })) + .fillMaxWidth() + .testTag(topicSelectionTestTag), ) { - SingleTopicButton( - name = it.topic.name, - topicId = it.topic.id, - imageUrl = it.topic.imageUrl, - isSelected = it.isFollowed, - onClick = onTopicCheckedChanged, - ) + items( + items = onboardingUiState.topics, + key = { it.topic.id }, + ) { + SingleTopicButton( + name = it.topic.name, + topicId = it.topic.id, + imageUrl = it.topic.imageUrl, + isSelected = it.isFollowed, + onClick = onTopicCheckedChanged, + ) + } } + DecorativeScrollbar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .align(Alignment.BottomStart), + state = lazyGridState.scrollbarState(itemsAvailable = onboardingUiState.topics.size), + orientation = Orientation.Horizontal, + scrollInProgress = lazyGridState.isScrollInProgress, + ) } } @@ -442,6 +483,26 @@ private fun DeepLinkEffect( } } +private fun feedItemsSize( + feedState: NewsFeedUiState, + onboardingUiState: OnboardingUiState, +): Int { + val feedSize = when (feedState) { + NewsFeedUiState.Loading -> 1 + is NewsFeedUiState.Success -> feedState.feed.size + } + val onboardingSize = when (onboardingUiState) { + OnboardingUiState.LoadFailed, + OnboardingUiState.NotShown, + -> 0 + + OnboardingUiState.Loading, + is OnboardingUiState.Shown, + -> 1 + } + return feedSize + onboardingSize +} + @DevicePreviews @Composable fun ForYouScreenPopulatedFeed( diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index d55cd9a38..0755f4e5a 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -16,17 +16,26 @@ package com.google.samples.apps.nowinandroid.feature.interests +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @Composable @@ -37,30 +46,52 @@ fun TopicsTabContent( modifier: Modifier = Modifier, withBottomSpacer: Boolean = true, ) { - LazyColumn( + Box( modifier = modifier - .padding(horizontal = 24.dp) - .testTag("interests:topics"), - contentPadding = PaddingValues(vertical = 16.dp), + .fillMaxWidth(), ) { - topics.forEach { followableTopic -> - val topicId = followableTopic.topic.id - item(key = topicId) { - InterestsItem( - name = followableTopic.topic.name, - following = followableTopic.isFollowed, - description = followableTopic.topic.shortDescription, - topicImageUrl = followableTopic.topic.imageUrl, - onClick = { onTopicClick(topicId) }, - onFollowButtonClick = { onFollowButtonClick(topicId, it) }, - ) + val scrollableState = rememberLazyListState() + LazyColumn( + modifier = Modifier + .padding(horizontal = 24.dp) + .testTag("interests:topics"), + contentPadding = PaddingValues(vertical = 16.dp), + state = scrollableState, + ) { + topics.forEach { followableTopic -> + val topicId = followableTopic.topic.id + item(key = topicId) { + InterestsItem( + name = followableTopic.topic.name, + following = followableTopic.isFollowed, + description = followableTopic.topic.shortDescription, + topicImageUrl = followableTopic.topic.imageUrl, + onClick = { onTopicClick(topicId) }, + onFollowButtonClick = { onFollowButtonClick(topicId, it) }, + ) + } } - } - if (withBottomSpacer) { - item { - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + if (withBottomSpacer) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } } } + val scrollbarState = scrollableState.scrollbarState( + itemsAvailable = topics.size, + ) + FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 2.dp) + .align(Alignment.CenterEnd), + state = scrollbarState, + orientation = Orientation.Vertical, + scrollInProgress = scrollableState.isScrollInProgress, + onThumbMoved = scrollableState.rememberThumbInteractions( + itemsAvailable = topics.size, + ), + ) } } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index b987a2752..e2a14c06f 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -17,16 +17,21 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -49,6 +54,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicA import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @@ -97,45 +105,78 @@ internal fun TopicScreen( ) { val state = rememberLazyListState() TrackScrollJank(scrollableState = state, stateName = "topic:screen") - LazyColumn( - state = state, + Box( modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, ) { - item { - Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) - } - when (topicUiState) { - TopicUiState.Loading -> item { - NiaLoadingWheel( - modifier = modifier, - contentDesc = stringResource(id = string.topic_loading), - ) + LazyColumn( + state = state, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) } + when (topicUiState) { + TopicUiState.Loading -> item { + NiaLoadingWheel( + modifier = modifier, + contentDesc = stringResource(id = string.topic_loading), + ) + } - TopicUiState.Error -> TODO() - is TopicUiState.Success -> { - item { - TopicToolbar( - onBackClick = onBackClick, - onFollowClick = onFollowClick, - uiState = topicUiState.followableTopic, + TopicUiState.Error -> TODO() + is TopicUiState.Success -> { + item { + TopicToolbar( + onBackClick = onBackClick, + onFollowClick = onFollowClick, + uiState = topicUiState.followableTopic, + ) + } + TopicBody( + name = topicUiState.followableTopic.topic.name, + description = topicUiState.followableTopic.topic.longDescription, + news = newsUiState, + imageUrl = topicUiState.followableTopic.topic.imageUrl, + onBookmarkChanged = onBookmarkChanged, + onNewsResourceViewed = onNewsResourceViewed, + onTopicClick = onTopicClick, ) } - TopicBody( - name = topicUiState.followableTopic.topic.name, - description = topicUiState.followableTopic.topic.longDescription, - news = newsUiState, - imageUrl = topicUiState.followableTopic.topic.imageUrl, - onBookmarkChanged = onBookmarkChanged, - onNewsResourceViewed = onNewsResourceViewed, - onTopicClick = onTopicClick, - ) + } + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) } } - item { - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) - } + val itemsAvailable = topicItemsSize(topicUiState, newsUiState) + val scrollbarState = state.scrollbarState( + itemsAvailable = itemsAvailable, + ) + FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .windowInsetsPadding(WindowInsets.systemBars) + .padding(horizontal = 2.dp) + .align(Alignment.CenterEnd), + state = scrollbarState, + orientation = Orientation.Vertical, + scrollInProgress = state.isScrollInProgress, + onThumbMoved = state.rememberThumbInteractions( + itemsAvailable = itemsAvailable, + ), + ) + } +} + +private fun topicItemsSize( + topicUiState: TopicUiState, + newsUiState: NewsUiState, +) = when (topicUiState) { + TopicUiState.Error -> 0 // Nothing + TopicUiState.Loading -> 1 // Loading bar + is TopicUiState.Success -> when (newsUiState) { + NewsUiState.Error -> 0 // Nothing + NewsUiState.Loading -> 1 // Loading bar + is NewsUiState.Success -> 2 + newsUiState.news.size // Toolbar, header } } From 795c5e32a9bcf3afc53f683763eb127083a6b813 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 11 May 2023 09:54:05 +0100 Subject: [PATCH 37/94] Add hoverable to scrollbar --- .../core/designsystem/component/scrollbar/Scrollbar.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 4984946bc..dfe0ce1f0 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box @@ -221,9 +222,10 @@ fun Scrollbar( Box( modifier = modifier .run { + val withHover = interactionSource?.let(::hoverable) ?: this when (orientation) { - Orientation.Vertical -> fillMaxHeight() - Orientation.Horizontal -> fillMaxWidth() + Orientation.Vertical -> withHover.fillMaxHeight() + Orientation.Horizontal -> withHover.fillMaxWidth() } } .onGloballyPositioned { coordinates -> From 3aec70504873b9ec3bd838823ae1c4f53c7e93cc Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 11 May 2023 11:25:48 +0100 Subject: [PATCH 38/94] Add padding to scrollbars --- .../apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt | 3 +++ .../samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt | 3 +++ .../samples/apps/nowinandroid/feature/interests/TabContent.kt | 3 +++ 3 files changed, 9 insertions(+) diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 08776b210..cb65984e6 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -30,7 +30,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells.Adaptive import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -214,6 +216,7 @@ private fun BookmarksGrid( FastScrollbar( modifier = Modifier .fillMaxHeight() + .windowInsetsPadding(WindowInsets.systemBars) .padding(horizontal = 2.dp) .align(Alignment.CenterEnd), state = scrollbarState, diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index db33d9e93..67551f4b0 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -42,9 +42,11 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells.Adaptive @@ -231,6 +233,7 @@ internal fun ForYouScreen( FastScrollbar( modifier = Modifier .fillMaxHeight() + .windowInsetsPadding(WindowInsets.systemBars) .padding(horizontal = 2.dp) .align(Alignment.CenterEnd), state = scrollbarState, diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index 0755f4e5a..85d7c20e8 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable @@ -84,6 +86,7 @@ fun TopicsTabContent( FastScrollbar( modifier = Modifier .fillMaxHeight() + .windowInsetsPadding(WindowInsets.systemBars) .padding(horizontal = 2.dp) .align(Alignment.CenterEnd), state = scrollbarState, From d6a265093d75440d0cce803cbc3e9d30c1f3877d Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 11 May 2023 13:58:44 +0100 Subject: [PATCH 39/94] Extract scrollbar values to constants --- .../core/designsystem/component/scrollbar/Scrollbar.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index dfe0ce1f0..3d33247d9 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -57,6 +57,9 @@ import kotlinx.coroutines.delay import kotlin.math.max import kotlin.math.min +private const val SCROLLBAR_PRESS_DELAY = 100L +private const val SCROLLBAR_PRESS_DELTA = 0.1f + /** * Class definition for the core properties of a scroll bar */ @@ -310,8 +313,7 @@ fun Scrollbar( dimension = orientation.valueOf(pressedOffset), ) val isPositive = currentTravel < destinationTravel - // TODO: Come up with a better heuristic for jumps - val delta = if (isPositive) 0.1f else -0.1f + val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f while (currentTravel != destinationTravel) { currentTravel = @@ -322,8 +324,7 @@ fun Scrollbar( } onThumbMoved(currentTravel) interactionThumbTravelPercent = currentTravel - // TODO: Define this more thoroughly - delay(100) + delay(SCROLLBAR_PRESS_DELAY) } } From bda31e9d153c574128cff19f472ac6b00aca4553 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Fri, 12 May 2023 10:33:49 +0100 Subject: [PATCH 40/94] Default scrollbars to dormant --- .../core/designsystem/component/scrollbar/AppScrollbars.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index bed9e6b44..2a8827c3d 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -180,7 +180,7 @@ private fun scrollbarThumbColor( scrollInProgress: Boolean, interactionSource: InteractionSource, ): Color { - var state by remember { mutableStateOf(Active) } + var state by remember { mutableStateOf(Dormant) } val pressed by interactionSource.collectIsPressedAsState() val hovered by interactionSource.collectIsHoveredAsState() val dragged by interactionSource.collectIsDraggedAsState() From 4858167b24ced98b26ec069c0f3bc6540b47a418 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 13 May 2023 12:21:34 +0100 Subject: [PATCH 41/94] Added clarifying comments to code and used better variable names --- .../component/scrollbar/AppScrollbars.kt | 6 +- .../scrollbar/LazyScrollbarUtilities.kt | 10 +- .../component/scrollbar/Scrollbar.kt | 122 ++++++++++-------- .../component/scrollbar/ScrollbarExt.kt | 4 +- 4 files changed, 75 insertions(+), 67 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index 2a8827c3d..2786a09fe 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -49,6 +49,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Inactive import kotlinx.coroutines.delay +private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L + /** * A [Scrollbar] that allows for fast scrolling of content. * Its thumb disappears when the scrolling container is dormant. @@ -80,7 +82,7 @@ fun FastScrollbar( orientation = orientation, ) }, - onThumbMoved = onThumbMoved, + onThumbDisplaced = onThumbMoved, ) } @@ -202,7 +204,7 @@ private fun scrollbarThumbColor( true -> state = Active false -> { state = Inactive - delay(2_000) + delay(INACTIVE_TO_DORMANT_COOL_DOWN) state = Dormant } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt index d45c5781a..c4ce8c22d 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -33,7 +33,7 @@ import kotlin.math.min * Calculates the [ScrollbarState] for lazy layouts. * @param itemsAvailable the total amount of items available to scroll in the layout. * @param visibleItems a list of items currently visible in the layout. - * @param firstItemIndex a function for interpolating the first visible index in the lazy layout + * @param firstVisibleItemIndex a function for interpolating the first visible index in the lazy layout * as scrolling progresses for smooth and linear scrollbar thumb progression. * [itemsAvailable]. * @param reverseLayout if the items in the backing lazy layout are laid out in reverse order. @@ -42,7 +42,7 @@ import kotlin.math.min internal inline fun LazyState.scrollbarState( itemsAvailable: Int, crossinline visibleItems: LazyState.() -> List, - crossinline firstItemIndex: LazyState.(List) -> Float, + crossinline firstVisibleItemIndex: LazyState.(List) -> Float, crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float, crossinline reverseLayout: LazyState.() -> Boolean, ): ScrollbarState { @@ -58,9 +58,8 @@ internal inline fun LazyState.scrol val visibleItemsInfo = visibleItems(this@scrollbarState) if (visibleItemsInfo.isEmpty()) return@snapshotFlow null - // Add the item offset for interpolation between scroll indices val firstIndex = min( - a = firstItemIndex(visibleItemsInfo), + a = firstVisibleItemIndex(visibleItemsInfo), b = itemsAvailable.toFloat(), ) if (firstIndex.isNaN()) return@snapshotFlow null @@ -77,10 +76,9 @@ internal inline fun LazyState.scrol a = itemsVisible / itemsAvailable, b = 1f, ) - ScrollbarState( thumbSizePercent = thumbSizePercent, - thumbTravelPercent = when { + thumbDisplacementPercent = when { reverseLayout() -> 1f - thumbTravelPercent else -> thumbTravelPercent }, diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 3d33247d9..5baf003e0 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -57,8 +57,8 @@ import kotlinx.coroutines.delay import kotlin.math.max import kotlin.math.min -private const val SCROLLBAR_PRESS_DELAY = 100L -private const val SCROLLBAR_PRESS_DELTA = 0.1f +private const val SCROLLBAR_PRESS_DELAY = 10L +private const val SCROLLBAR_PRESS_DELTA = 0.02f /** * Class definition for the core properties of a scroll bar @@ -71,7 +71,7 @@ value class ScrollbarState internal constructor( companion object { val FULL = ScrollbarState( thumbSizePercent = 1f, - thumbTravelPercent = 0f, + thumbDisplacementPercent = 0f, ) } } @@ -93,15 +93,16 @@ private value class ScrollbarTrack( /** * Creates a scrollbar state with the listed properties * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size - * @param thumbTravelPercent the distance the thumb has traveled as a percentage of total track size + * @param thumbDisplacementPercent the distance the thumb has traveled as a percentage of total + * track size */ fun ScrollbarState( thumbSizePercent: Float, - thumbTravelPercent: Float, + thumbDisplacementPercent: Float, ) = ScrollbarState( packFloats( val1 = thumbSizePercent, - val2 = thumbTravelPercent, + val2 = thumbDisplacementPercent, ), ) @@ -114,7 +115,7 @@ val ScrollbarState.thumbSizePercent /** * Returns the distance the thumb has traveled as a percentage of total track size */ -val ScrollbarState.thumbTravelPercent +val ScrollbarState.thumbDisplacementPercent get() = unpackFloat2(packedValue) /** @@ -167,8 +168,8 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { * @param minThumbSize the minimum size of the scrollbar thumb * @param interactionSource allows for observing the state of the scroll bar * @param thumb a composable for drawing the scrollbar thumb - * @param onThumbMoved an function for reacting to scroll bar interactions, for example implementing - * a fast scroll + * @param onThumbDisplaced an function for reacting to scroll bar displacements caused by direct + * interactions on the scrollbar thumb by the user, for example implementing a fast scroll */ @Composable fun Scrollbar( @@ -178,50 +179,47 @@ fun Scrollbar( minThumbSize: Dp = 40.dp, interactionSource: MutableInteractionSource? = null, thumb: @Composable () -> Unit, - onThumbMoved: ((Float) -> Unit)? = null, + onThumbDisplaced: ((Float) -> Unit)? = null, ) { val localDensity = LocalDensity.current - var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) } + + // Using Offset.Unspecified and Float.NaN instead of null + // to prevent unnecessary boxing of primitives var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } - var track by remember { mutableStateOf(ScrollbarTrack(0)) } - val updatedState by rememberUpdatedState(state) - val updatedTrack by rememberUpdatedState(track) + // Used to immediately show drag feedback in the UI while the scrolling implementation + // catches up + var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) } + + var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } - val thumbSizePercent = state.thumbSizePercent val thumbTravelPercent = when { - interactionThumbTravelPercent.isNaN() -> state.thumbTravelPercent + interactionThumbTravelPercent.isNaN() -> state.thumbDisplacementPercent else -> interactionThumbTravelPercent } val thumbSizePx = max( - a = thumbSizePercent * track.size, + a = state.thumbSizePercent * track.size, b = with(localDensity) { minThumbSize.toPx() }, ) - val thumbSizeDp by animateDpAsState( targetValue = with(localDensity) { thumbSizePx.toDp() }, - label = "thumb size", + label = "scrollbar thumb size", ) - - val thumbTravelPx = min( + val thumbDisplacementPx = min( a = track.size * thumbTravelPercent, b = track.size - thumbSizePx, ) - val draggableState = rememberDraggableState { delta -> if (draggedOffset == Offset.Unspecified) return@rememberDraggableState draggedOffset = when (orientation) { - Orientation.Vertical -> draggedOffset.copy( - y = draggedOffset.y + delta, - ) - - Orientation.Horizontal -> draggedOffset.copy( - x = draggedOffset.x + delta, - ) + Orientation.Vertical -> draggedOffset.copy(y = draggedOffset.y + delta) + Orientation.Horizontal -> draggedOffset.copy(x = draggedOffset.x + delta) } } + + // Scrollbar track container Box( modifier = modifier .run { @@ -232,10 +230,10 @@ fun Scrollbar( } } .onGloballyPositioned { coordinates -> - val position = orientation.valueOf(coordinates.positionInRoot()) + val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot()) track = ScrollbarTrack( - max = position, - min = position + orientation.valueOf(coordinates.size), + max = scrollbarStartCoordinate, + min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size), ) } // Process scrollbar presses @@ -243,17 +241,17 @@ fun Scrollbar( detectTapGestures( onPress = { offset -> val initialPress = PressInteraction.Press(offset) - interactionSource?.tryEmit(initialPress) + + // Start the press pressedOffset = offset interactionSource?.tryEmit( - if (tryAwaitRelease()) { - PressInteraction.Release(initialPress) - } else { - PressInteraction.Cancel(initialPress) - }, + if (tryAwaitRelease()) PressInteraction.Release(initialPress) + else PressInteraction.Cancel(initialPress), ) + + // End the press pressedOffset = Offset.Unspecified }, ) @@ -271,10 +269,11 @@ fun Scrollbar( }, ), ) { - val offset = max( - a = with(localDensity) { thumbTravelPx.toDp() }, + val scrollbarThumbDisplacement = max( + a = with(localDensity) { thumbDisplacementPx.toDp() }, b = 0.dp, ) + // Scrollbar thumb container Box( modifier = Modifier .align(Alignment.TopStart) @@ -287,10 +286,10 @@ fun Scrollbar( .offset( y = when (orientation) { Orientation.Horizontal -> 0.dp - Orientation.Vertical -> offset + Orientation.Vertical -> scrollbarThumbDisplacement }, x = when (orientation) { - Orientation.Horizontal -> offset + Orientation.Horizontal -> scrollbarThumbDisplacement Orientation.Vertical -> 0.dp }, ), @@ -299,31 +298,40 @@ fun Scrollbar( } } - if (onThumbMoved == null) return + if (onThumbDisplaced == null) return + + // State that will be read inside the effects that follow + // but will not cause re-triggering of them + val updatedState by rememberUpdatedState(state) // Process presses LaunchedEffect(pressedOffset) { + // Press ended, reset interactionThumbTravelPercent if (pressedOffset == Offset.Unspecified) { interactionThumbTravelPercent = Float.NaN return@LaunchedEffect } - var currentTravel = updatedState.thumbTravelPercent - val destinationTravel = updatedTrack.thumbPosition( + var currentThumbDisplacement = updatedState.thumbDisplacementPercent + val destinationThumbDisplacement = track.thumbPosition( dimension = orientation.valueOf(pressedOffset), ) - val isPositive = currentTravel < destinationTravel + val isPositive = currentThumbDisplacement < destinationThumbDisplacement val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f - while (currentTravel != destinationTravel) { - currentTravel = - if (isPositive) { - min(currentTravel + delta, destinationTravel) - } else { - max(currentTravel + delta, destinationTravel) - } - onThumbMoved(currentTravel) - interactionThumbTravelPercent = currentTravel + while (currentThumbDisplacement != destinationThumbDisplacement) { + currentThumbDisplacement = when { + isPositive -> min( + a = currentThumbDisplacement + delta, + b = destinationThumbDisplacement, + ) + else -> max( + a = currentThumbDisplacement + delta, + b = destinationThumbDisplacement, + ) + } + onThumbDisplaced(currentThumbDisplacement) + interactionThumbTravelPercent = currentThumbDisplacement delay(SCROLLBAR_PRESS_DELAY) } } @@ -334,10 +342,10 @@ fun Scrollbar( interactionThumbTravelPercent = Float.NaN return@LaunchedEffect } - val currentTravel = updatedTrack.thumbPosition( + val currentTravel = track.thumbPosition( dimension = orientation.valueOf(draggedOffset), ) - onThumbMoved(currentTravel) + onThumbDisplaced(currentTravel) interactionThumbTravelPercent = currentTravel } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt index aea4cd661..26f0bb2ae 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -37,7 +37,7 @@ fun LazyListState.scrollbarState( scrollbarState( itemsAvailable = itemsAvailable, visibleItems = { layoutInfo.visibleItemsInfo }, - firstItemIndex = { visibleItems -> + firstVisibleItemIndex = { visibleItems -> interpolateFirstItemIndex( visibleItems = visibleItems, itemSize = { it.size }, @@ -71,7 +71,7 @@ fun LazyGridState.scrollbarState( scrollbarState( itemsAvailable = itemsAvailable, visibleItems = { layoutInfo.visibleItemsInfo }, - firstItemIndex = { visibleItems -> + firstVisibleItemIndex = { visibleItems -> interpolateFirstItemIndex( visibleItems = visibleItems, itemSize = { From ce6eaa9d64415b84bd230850c65e02f1372da6ab Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 13 May 2023 13:19:43 +0100 Subject: [PATCH 42/94] Fix spotless --- .../core/designsystem/component/scrollbar/Scrollbar.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 5baf003e0..4e28d340e 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -247,8 +247,10 @@ fun Scrollbar( pressedOffset = offset interactionSource?.tryEmit( - if (tryAwaitRelease()) PressInteraction.Release(initialPress) - else PressInteraction.Cancel(initialPress), + when { + tryAwaitRelease() -> PressInteraction.Release(initialPress) + else -> PressInteraction.Cancel(initialPress) + }, ) // End the press From cb1d50e65ed95795673c2aa02af8d12325fdc4a4 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 13 May 2023 20:23:56 +0100 Subject: [PATCH 43/94] Even better names Change-Id: Ia699c3ce8fd1ce7a6e406b00c81dc196b487ed65 --- .../component/scrollbar/AppScrollbars.kt | 36 ++--- .../component/scrollbar/ThumbExt.kt | 18 +-- .../feature/bookmarks/BookmarksScreen.kt | 42 +++-- .../feature/foryou/ForYouScreen.kt | 10 +- .../feature/interests/TabContent.kt | 7 +- .../feature/search/SearchScreen.kt | 147 +++++++++++------- .../nowinandroid/feature/topic/TopicScreen.kt | 7 +- 7 files changed, 139 insertions(+), 128 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index 2786a09fe..373bcb7d9 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation.Horizontal import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState @@ -56,18 +57,15 @@ private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L * Its thumb disappears when the scrolling container is dormant. * @param modifier a [Modifier] for the [Scrollbar] * @param state the driving state for the [Scrollbar] - * @param scrollInProgress a flag indicating if the scrolling container for the scrollbar is - * currently scrolling * @param orientation the orientation of the scrollbar - * @param onThumbMoved the fast scroll implementation + * @param onThumbDisplaced the fast scroll implementation */ @Composable -fun FastScrollbar( +fun ScrollableState.FastScrollbar( modifier: Modifier = Modifier, state: ScrollbarState, - scrollInProgress: Boolean, orientation: Orientation, - onThumbMoved: (Float) -> Unit, + onThumbDisplaced: (Float) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } Scrollbar( @@ -77,12 +75,11 @@ fun FastScrollbar( state = state, thumb = { FastScrollbarThumb( - scrollInProgress = scrollInProgress, interactionSource = interactionSource, orientation = orientation, ) }, - onThumbDisplaced = onThumbMoved, + onThumbDisplaced = onThumbDisplaced, ) } @@ -91,15 +88,12 @@ fun FastScrollbar( * Its thumb disappears when the scrolling container is dormant. * @param modifier a [Modifier] for the [Scrollbar] * @param state the driving state for the [Scrollbar] - * @param scrollInProgress a flag indicating if the scrolling container for the scrollbar is - * currently scrolling * @param orientation the orientation of the scrollbar */ @Composable -fun DecorativeScrollbar( +fun ScrollableState.DecorativeScrollbar( modifier: Modifier = Modifier, state: ScrollbarState, - scrollInProgress: Boolean, orientation: Orientation, ) { val interactionSource = remember { MutableInteractionSource() } @@ -111,7 +105,6 @@ fun DecorativeScrollbar( thumb = { DecorativeScrollbarThumb( interactionSource = interactionSource, - scrollInProgress = scrollInProgress, orientation = orientation, ) }, @@ -122,8 +115,7 @@ fun DecorativeScrollbar( * A scrollbar thumb that is intended to also be a touch target for fast scrolling. */ @Composable -private fun FastScrollbarThumb( - scrollInProgress: Boolean, +private fun ScrollableState.FastScrollbarThumb( interactionSource: InteractionSource, orientation: Orientation, ) { @@ -137,7 +129,6 @@ private fun FastScrollbarThumb( } .background( color = scrollbarThumbColor( - scrollInProgress = scrollInProgress, interactionSource = interactionSource, ), shape = RoundedCornerShape(16.dp), @@ -149,8 +140,7 @@ private fun FastScrollbarThumb( * A decorative scrollbar thumb for communicating a user's position in a list solely. */ @Composable -private fun DecorativeScrollbarThumb( - scrollInProgress: Boolean, +private fun ScrollableState.DecorativeScrollbarThumb( interactionSource: InteractionSource, orientation: Orientation, ) { @@ -164,7 +154,6 @@ private fun DecorativeScrollbarThumb( } .background( color = scrollbarThumbColor( - scrollInProgress = scrollInProgress, interactionSource = interactionSource, ), shape = RoundedCornerShape(16.dp), @@ -174,19 +163,18 @@ private fun DecorativeScrollbarThumb( /** * The color of the scrollbar thumb as a function of its interaction state. - * @param scrollInProgress if the scrolling container is currently scrolling * @param interactionSource source of interactions in the scrolling container */ @Composable -private fun scrollbarThumbColor( - scrollInProgress: Boolean, +private fun ScrollableState.scrollbarThumbColor( interactionSource: InteractionSource, ): Color { var state by remember { mutableStateOf(Dormant) } val pressed by interactionSource.collectIsPressedAsState() val hovered by interactionSource.collectIsHoveredAsState() val dragged by interactionSource.collectIsDraggedAsState() - val active = pressed || hovered || dragged || scrollInProgress + val active = (canScrollForward || canScrollForward) && + (pressed || hovered || dragged || isScrollInProgress) val color by animateColorAsState( targetValue = when (state) { @@ -202,7 +190,7 @@ private fun scrollbarThumbColor( LaunchedEffect(active) { when (active) { true -> state = Active - false -> { + false -> if (state == Active) { state = Inactive delay(INACTIVE_TO_DORMANT_COOL_DOWN) state = Dormant diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt index f03e21c85..4ed966da4 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt @@ -27,36 +27,36 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue /** - * Remembers a function to react to [Scrollbar] thumb position movements for a [LazyListState] + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState] * @param itemsAvailable the amount of items in the list. */ @Composable -fun LazyListState.rememberThumbInteractions( +fun LazyListState.rememberFastScroller( itemsAvailable: Int, -): (Float) -> Unit = rememberThumbInteractions( +): (Float) -> Unit = rememberFastScroller( itemsAvailable = itemsAvailable, scroll = ::scrollToItem, ) /** - * Remembers a function to react to [Scrollbar] thumb position movements for a [LazyGridState] + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyGridState] * @param itemsAvailable the amount of items in the grid. */ @Composable -fun LazyGridState.rememberThumbInteractions( +fun LazyGridState.rememberFastScroller( itemsAvailable: Int, -): (Float) -> Unit = rememberThumbInteractions( +): (Float) -> Unit = rememberFastScroller( itemsAvailable = itemsAvailable, scroll = ::scrollToItem, ) /** - * Generic function to react to [Scrollbar] thumb interactions in a lazy layout. + * Generic function to react to [Scrollbar] thumb displacements in a lazy layout. * @param itemsAvailable the total amount of items available to scroll in the layout. * @param scroll a function to be invoked when an index has been identified to scroll to. */ @Composable -private inline fun rememberThumbInteractions( +private inline fun rememberFastScroller( itemsAvailable: Int, crossinline scroll: suspend (index: Int) -> Unit, ): (Float) -> Unit { @@ -69,6 +69,6 @@ private inline fun rememberThumbInteractions( scroll(indexToFind) } return remember { - { percentage = it } + { newPercentage -> percentage = newPercentage } } } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index cb65984e6..8360607a8 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -62,7 +63,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -120,12 +121,9 @@ internal fun BookmarksScreen( val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) if (snackBarResult) { - undoBookmarkRemoval() - } - - else { - clearUndoState() - + undoBookmarkRemoval() + } else { + clearUndoState() } } } @@ -142,19 +140,18 @@ internal fun BookmarksScreen( } - when (feedState) { - Loading -> LoadingState(modifier) - is Success -> if (feedState.feed.isNotEmpty()) { - BookmarksGrid( - feedState, - removeFromBookmarks, - onNewsResourceViewed, - onTopicClick, - modifier, - ) - } else { - EmptyState(modifier) - + when (feedState) { + Loading -> LoadingState(modifier) + is Success -> if (feedState.feed.isNotEmpty()) { + BookmarksGrid( + feedState, + removeFromBookmarks, + onNewsResourceViewed, + onTopicClick, + modifier, + ) + } else { + EmptyState(modifier) } } @@ -213,7 +210,7 @@ private fun BookmarksGrid( val scrollbarState = scrollableState.scrollbarState( itemsAvailable = itemsAvailable, ) - FastScrollbar( + scrollableState.FastScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) @@ -221,8 +218,7 @@ private fun BookmarksGrid( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - scrollInProgress = scrollableState.isScrollInProgress, - onThumbMoved = scrollableState.rememberThumbInteractions( + onThumbDisplaced = scrollableState.rememberFastScroller( itemsAvailable = itemsAvailable, ), ) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 67551f4b0..ffbda5de3 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -93,7 +93,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DecorativeScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -230,7 +230,7 @@ internal fun ForYouScreen( ) } } - FastScrollbar( + state.FastScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) @@ -238,8 +238,7 @@ internal fun ForYouScreen( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - scrollInProgress = state.isScrollInProgress, - onThumbMoved = state.rememberThumbInteractions( + onThumbDisplaced = state.rememberFastScroller( itemsAvailable = itemsAvailable, ), ) @@ -365,14 +364,13 @@ private fun TopicSelection( ) } } - DecorativeScrollbar( + lazyGridState.DecorativeScrollbar( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .align(Alignment.BottomStart), state = lazyGridState.scrollbarState(itemsAvailable = onboardingUiState.topics.size), orientation = Orientation.Horizontal, - scrollInProgress = lazyGridState.isScrollInProgress, ) } } diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index 85d7c20e8..acc84e895 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @@ -83,7 +83,7 @@ fun TopicsTabContent( val scrollbarState = scrollableState.scrollbarState( itemsAvailable = topics.size, ) - FastScrollbar( + scrollableState.FastScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) @@ -91,8 +91,7 @@ fun TopicsTabContent( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - scrollInProgress = scrollableState.isScrollInProgress, - onThumbMoved = scrollableState.rememberThumbInteractions( + onThumbDisplaced = scrollableState.rememberFastScroller( itemsAvailable = topics.size, ), ) diff --git a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index dc5ee45a8..3e34c3fdd 100644 --- a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -17,17 +17,22 @@ package com.google.samples.apps.nowinandroid.feature.search import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells.Adaptive @@ -75,12 +80,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading +import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.R.string import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.newsFeed @@ -289,81 +299,102 @@ private fun SearchResultBody( searchQuery: String = "", ) { val state = rememberLazyGridState() - LazyVerticalGrid( - columns = Adaptive(300.dp), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + Box( modifier = Modifier - .fillMaxSize() - .testTag("search:newsResources"), - state = state, + .fillMaxSize(), ) { - if (topics.isNotEmpty()) { - item( - span = { - GridItemSpan(maxLineSpan) - }, - ) { - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.topics)) - } + LazyVerticalGrid( + columns = Adaptive(300.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .fillMaxSize() + .testTag("search:newsResources"), + state = state, + ) { + if (topics.isNotEmpty()) { + item( + span = { + GridItemSpan(maxLineSpan) }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) + ) { + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(stringResource(id = searchR.string.topics)) + } + }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + topics.forEach { followableTopic -> + val topicId = followableTopic.topic.id + item( + key = "topic-$topicId", // Append a prefix to distinguish a key for news resources + span = { + GridItemSpan(maxLineSpan) + }, + ) { + InterestsItem( + name = followableTopic.topic.name, + following = followableTopic.isFollowed, + description = followableTopic.topic.shortDescription, + topicImageUrl = followableTopic.topic.imageUrl, + onClick = { + // Pass the current search query to ViewModel to save it as recent searches + onSearchTriggered(searchQuery) + onTopicClick(topicId) + }, + onFollowButtonClick = { onFollowButtonClick(topicId, it) }, + ) + } + } } - topics.forEach { followableTopic -> - val topicId = followableTopic.topic.id + + if (newsResources.isNotEmpty()) { item( - key = "topic-$topicId", // Append a prefix to distinguish a key for news resources span = { GridItemSpan(maxLineSpan) }, ) { - InterestsItem( - name = followableTopic.topic.name, - following = followableTopic.isFollowed, - description = followableTopic.topic.shortDescription, - topicImageUrl = followableTopic.topic.imageUrl, - onClick = { - // Pass the current search query to ViewModel to save it as recent searches - onSearchTriggered(searchQuery) - onTopicClick(topicId) + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(stringResource(id = searchR.string.updates)) + } }, - onFollowButtonClick = { onFollowButtonClick(topicId, it) }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) } - } - } - if (newsResources.isNotEmpty()) { - item( - span = { - GridItemSpan(maxLineSpan) - }, - ) { - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.updates)) - } + newsFeed( + feedState = Success(feed = newsResources), + onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, + onNewsResourceViewed = onNewsResourceViewed, + onTopicClick = onTopicClick, + onExpandedCardClick = { + onSearchTriggered(searchQuery) }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) } - - newsFeed( - feedState = NewsFeedUiState.Success(feed = newsResources), - onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, - onNewsResourceViewed = onNewsResourceViewed, - onTopicClick = onTopicClick, - onExpandedCardClick = { - onSearchTriggered(searchQuery) - }, - ) } + val itemsAvailable = topics.size + newsResources.size + val scrollbarState = state.scrollbarState( + itemsAvailable = itemsAvailable, + ) + state.FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .windowInsetsPadding(WindowInsets.systemBars) + .padding(horizontal = 2.dp) + .align(Alignment.CenterEnd), + state = scrollbarState, + orientation = Orientation.Vertical, + onThumbDisplaced = state.rememberFastScroller( + itemsAvailable = itemsAvailable, + ), + ) } } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index e2a14c06f..f81c5b51c 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -55,7 +55,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackg import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberThumbInteractions +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -151,7 +151,7 @@ internal fun TopicScreen( val scrollbarState = state.scrollbarState( itemsAvailable = itemsAvailable, ) - FastScrollbar( + state.FastScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) @@ -159,8 +159,7 @@ internal fun TopicScreen( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - scrollInProgress = state.isScrollInProgress, - onThumbMoved = state.rememberThumbInteractions( + onThumbDisplaced = state.rememberFastScroller( itemsAvailable = itemsAvailable, ), ) From bdf3ee3cb0b36d3431b6d7f7801729690a2c9d31 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 29 Jun 2023 15:00:53 +0100 Subject: [PATCH 44/94] PR feedback Change-Id: Ie22aa3c7fdf4c8ca8d8118837cfd279de8c6d80b --- .../apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt | 2 -- .../samples/apps/nowinandroid/feature/search/SearchScreen.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 8360607a8..da4d32469 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -120,7 +120,6 @@ internal fun BookmarksScreen( if (shouldDisplayUndoBookmark) { val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) if (snackBarResult) { - undoBookmarkRemoval() } else { clearUndoState() @@ -139,7 +138,6 @@ internal fun BookmarksScreen( onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } - when (feedState) { Loading -> LoadingState(modifier) is Success -> if (feedState.feed.isNotEmpty()) { diff --git a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index 3e34c3fdd..14b60e564 100644 --- a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -88,8 +88,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews -import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState -import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.R.string import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent From 2dba52266332a58ae6a539491716a2f356202e27 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 29 Jun 2023 15:03:28 +0100 Subject: [PATCH 45/94] Check if first item size == 0 Change-Id: I41acffad84d9db1cf407f77b04530643933f98bf --- .../component/scrollbar/LazyScrollbarUtilities.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt index c4ce8c22d..2c1df0c66 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -116,8 +116,11 @@ internal inline fun LazyState.inter if (firstItemIndex < 0) return Float.NaN + val firstItemSize = itemSize(firstItem) + if (firstItemSize == 0) return Float.NaN + val itemOffset = offset(firstItem).toFloat() - val offsetPercentage = abs(itemOffset) / itemSize(firstItem) + val offsetPercentage = abs(itemOffset) / firstItemSize val nextItem = nextItemOnMainAxis(firstItem) ?: return firstItemIndex + offsetPercentage From eeb49732657e8cd711c3bec252b4e413251252bb Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Wed, 5 Jul 2023 14:18:29 +0100 Subject: [PATCH 46/94] PR feedback Change-Id: I48492e3c121ff8b2ee6bbbac08aa1829f6a6467f --- .../component/scrollbar/AppScrollbars.kt | 17 ++++++----- .../scrollbar/LazyScrollbarUtilities.kt | 3 ++ .../component/scrollbar/Scrollbar.kt | 30 ++++++++++++------- .../feature/bookmarks/BookmarksScreen.kt | 4 +-- .../feature/foryou/ForYouScreen.kt | 4 +-- .../feature/interests/TabContent.kt | 4 +-- .../feature/search/SearchScreen.kt | 4 +-- .../nowinandroid/feature/topic/TopicScreen.kt | 4 +-- 8 files changed, 43 insertions(+), 27 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index 373bcb7d9..ca282aa7a 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -50,10 +50,13 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Inactive import kotlinx.coroutines.delay -private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L +/** + * The time period for showing the scrollbar thumb after interacting with it, before it fades away + */ +private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L /** - * A [Scrollbar] that allows for fast scrolling of content. + * A [Scrollbar] that allows for fast scrolling of content by dragging its thumb. * Its thumb disappears when the scrolling container is dormant. * @param modifier a [Modifier] for the [Scrollbar] * @param state the driving state for the [Scrollbar] @@ -61,7 +64,7 @@ private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L * @param onThumbDisplaced the fast scroll implementation */ @Composable -fun ScrollableState.FastScrollbar( +fun ScrollableState.DraggableScrollbar( modifier: Modifier = Modifier, state: ScrollbarState, orientation: Orientation, @@ -74,7 +77,7 @@ fun ScrollableState.FastScrollbar( interactionSource = interactionSource, state = state, thumb = { - FastScrollbarThumb( + DraggableScrollbarThumb( interactionSource = interactionSource, orientation = orientation, ) @@ -115,7 +118,7 @@ fun ScrollableState.DecorativeScrollbar( * A scrollbar thumb that is intended to also be a touch target for fast scrolling. */ @Composable -private fun ScrollableState.FastScrollbarThumb( +private fun ScrollableState.DraggableScrollbarThumb( interactionSource: InteractionSource, orientation: Orientation, ) { @@ -137,7 +140,7 @@ private fun ScrollableState.FastScrollbarThumb( } /** - * A decorative scrollbar thumb for communicating a user's position in a list solely. + * A decorative scrollbar thumb used solely for communicating a user's position in a list. */ @Composable private fun ScrollableState.DecorativeScrollbarThumb( @@ -192,7 +195,7 @@ private fun ScrollableState.scrollbarThumbColor( true -> state = Active false -> if (state == Active) { state = Inactive - delay(INACTIVE_TO_DORMANT_COOL_DOWN) + delay(SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS) state = Dormant } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt index 2c1df0c66..c7ef8fe91 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -101,6 +101,9 @@ internal inline fun LazyState.scrol * of the scroll. * @param itemIndex a lookup function for index of an item in the layout relative to * the total amount of items available. + * + * @return a [Float] in the range [firstItemPosition..nextItemPosition) where nextItemPosition + * is the index of the consecutive item along the major axis. * */ internal inline fun LazyState.interpolateFirstItemIndex( visibleItems: List, diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 4e28d340e..0571ff6d1 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Android Open Source Project + * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,8 +57,17 @@ import kotlinx.coroutines.delay import kotlin.math.max import kotlin.math.min -private const val SCROLLBAR_PRESS_DELAY = 10L -private const val SCROLLBAR_PRESS_DELTA = 0.02f +/** + * The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll + * instead of dragging the scrollbar thumb. + */ +private const val SCROLLBAR_PRESS_DELAY_MS = 10L + +/** + * The percentage displacement of the scrollbar when scrolled by long presses on the scrollbar + * track. + */ +private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f /** * Class definition for the core properties of a scroll bar @@ -91,10 +100,11 @@ private value class ScrollbarTrack( } /** - * Creates a scrollbar state with the listed properties + * Creates a [ScrollbarState] with the listed properties * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size * @param thumbDisplacementPercent the distance the thumb has traveled as a percentage of total - * track size + * track size. Refers to either the thumb width (for horizontal scrollbars) + * or height (for vertical scrollbars). */ fun ScrollbarState( thumbSizePercent: Float, @@ -162,7 +172,7 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { } /** - * A Composable for drawing a Scrollbar + * A Composable for drawing a scrollbar * @param orientation the scroll direction of the scrollbar * @param state the state describing the position of the scrollbar * @param minThumbSize the minimum size of the scrollbar thumb @@ -219,7 +229,7 @@ fun Scrollbar( } } - // Scrollbar track container + // scrollbar track container Box( modifier = modifier .run { @@ -275,7 +285,7 @@ fun Scrollbar( a = with(localDensity) { thumbDisplacementPx.toDp() }, b = 0.dp, ) - // Scrollbar thumb container + // scrollbar thumb container Box( modifier = Modifier .align(Alignment.TopStart) @@ -319,7 +329,7 @@ fun Scrollbar( dimension = orientation.valueOf(pressedOffset), ) val isPositive = currentThumbDisplacement < destinationThumbDisplacement - val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f + val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f while (currentThumbDisplacement != destinationThumbDisplacement) { currentThumbDisplacement = when { @@ -334,7 +344,7 @@ fun Scrollbar( } onThumbDisplaced(currentThumbDisplacement) interactionThumbTravelPercent = currentThumbDisplacement - delay(SCROLLBAR_PRESS_DELAY) + delay(SCROLLBAR_PRESS_DELAY_MS) } } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index da4d32469..d4363f12c 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -62,7 +62,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme @@ -208,7 +208,7 @@ private fun BookmarksGrid( val scrollbarState = scrollableState.scrollbarState( itemsAvailable = itemsAvailable, ) - scrollableState.FastScrollbar( + scrollableState.DraggableScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index ffbda5de3..dbcfe7eeb 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -92,7 +92,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButto import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DecorativeScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -230,7 +230,7 @@ internal fun ForYouScreen( ) } } - state.FastScrollbar( + state.DraggableScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index acc84e895..7ae652344 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @@ -83,7 +83,7 @@ fun TopicsTabContent( val scrollbarState = scrollableState.scrollbarState( itemsAvailable = topics.size, ) - scrollableState.FastScrollbar( + scrollableState.DraggableScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) diff --git a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index 14b60e564..08356d938 100644 --- a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -80,7 +80,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -381,7 +381,7 @@ private fun SearchResultBody( val scrollbarState = state.scrollbarState( itemsAvailable = itemsAvailable, ) - state.FastScrollbar( + state.DraggableScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index f81c5b51c..84975b4ea 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -54,7 +54,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicA import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.FastScrollbar +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -151,7 +151,7 @@ internal fun TopicScreen( val scrollbarState = state.scrollbarState( itemsAvailable = itemsAvailable, ) - state.FastScrollbar( + state.DraggableScrollbar( modifier = Modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) From 806726a7f0804948b26943357996ffce8364bdad Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Wed, 5 Jul 2023 14:21:06 +0100 Subject: [PATCH 47/94] PR feedback Change-Id: I6e9f4a2ba53e81d32d07e229eb848541801eca18 --- .../core/designsystem/component/scrollbar/Scrollbar.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 0571ff6d1..f4710f2c6 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -101,10 +101,11 @@ private value class ScrollbarTrack( /** * Creates a [ScrollbarState] with the listed properties - * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size + * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. + * Refers to either the thumb width (for horizontal scrollbars) + * or height (for vertical scrollbars). * @param thumbDisplacementPercent the distance the thumb has traveled as a percentage of total - * track size. Refers to either the thumb width (for horizontal scrollbars) - * or height (for vertical scrollbars). + * track size. */ fun ScrollbarState( thumbSizePercent: Float, From e13e84c155b26d12c87b922362054984eee418fa Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Wed, 5 Jul 2023 16:56:15 +0100 Subject: [PATCH 48/94] Replace usages of 'displacement' to 'moved' in parameter names Change-Id: I5b3da60041b046454e848d187f6dd2bfadf22b9a --- .../component/scrollbar/AppScrollbars.kt | 6 +-- .../scrollbar/LazyScrollbarUtilities.kt | 2 +- .../component/scrollbar/Scrollbar.kt | 52 +++++++++---------- .../component/scrollbar/ThumbExt.kt | 10 ++-- .../feature/bookmarks/BookmarksScreen.kt | 4 +- .../feature/foryou/ForYouScreen.kt | 4 +- .../feature/interests/TabContent.kt | 4 +- .../feature/search/SearchScreen.kt | 4 +- .../nowinandroid/feature/topic/TopicScreen.kt | 4 +- 9 files changed, 45 insertions(+), 45 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index ca282aa7a..fa913cb27 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -61,14 +61,14 @@ private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L * @param modifier a [Modifier] for the [Scrollbar] * @param state the driving state for the [Scrollbar] * @param orientation the orientation of the scrollbar - * @param onThumbDisplaced the fast scroll implementation + * @param onThumbMoved the fast scroll implementation */ @Composable fun ScrollableState.DraggableScrollbar( modifier: Modifier = Modifier, state: ScrollbarState, orientation: Orientation, - onThumbDisplaced: (Float) -> Unit, + onThumbMoved: (Float) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } Scrollbar( @@ -82,7 +82,7 @@ fun ScrollableState.DraggableScrollbar( orientation = orientation, ) }, - onThumbDisplaced = onThumbDisplaced, + onThumbMoved = onThumbMoved, ) } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt index c7ef8fe91..8c4063b15 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -78,7 +78,7 @@ internal inline fun LazyState.scrol ) ScrollbarState( thumbSizePercent = thumbSizePercent, - thumbDisplacementPercent = when { + thumbMovedPercent = when { reverseLayout() -> 1f - thumbTravelPercent else -> thumbTravelPercent }, diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index f4710f2c6..11997468c 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -80,7 +80,7 @@ value class ScrollbarState internal constructor( companion object { val FULL = ScrollbarState( thumbSizePercent = 1f, - thumbDisplacementPercent = 0f, + thumbMovedPercent = 0f, ) } } @@ -104,16 +104,16 @@ private value class ScrollbarTrack( * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. * Refers to either the thumb width (for horizontal scrollbars) * or height (for vertical scrollbars). - * @param thumbDisplacementPercent the distance the thumb has traveled as a percentage of total + * @param thumbMovedPercent the distance the thumb has traveled as a percentage of total * track size. */ fun ScrollbarState( thumbSizePercent: Float, - thumbDisplacementPercent: Float, + thumbMovedPercent: Float, ) = ScrollbarState( packFloats( val1 = thumbSizePercent, - val2 = thumbDisplacementPercent, + val2 = thumbMovedPercent, ), ) @@ -126,7 +126,7 @@ val ScrollbarState.thumbSizePercent /** * Returns the distance the thumb has traveled as a percentage of total track size */ -val ScrollbarState.thumbDisplacementPercent +val ScrollbarState.thumbMovedPercent get() = unpackFloat2(packedValue) /** @@ -179,7 +179,7 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { * @param minThumbSize the minimum size of the scrollbar thumb * @param interactionSource allows for observing the state of the scroll bar * @param thumb a composable for drawing the scrollbar thumb - * @param onThumbDisplaced an function for reacting to scroll bar displacements caused by direct + * @param onThumbMoved an function for reacting to scroll bar displacements caused by direct * interactions on the scrollbar thumb by the user, for example implementing a fast scroll */ @Composable @@ -190,7 +190,7 @@ fun Scrollbar( minThumbSize: Dp = 40.dp, interactionSource: MutableInteractionSource? = null, thumb: @Composable () -> Unit, - onThumbDisplaced: ((Float) -> Unit)? = null, + onThumbMoved: ((Float) -> Unit)? = null, ) { val localDensity = LocalDensity.current @@ -206,7 +206,7 @@ fun Scrollbar( var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } val thumbTravelPercent = when { - interactionThumbTravelPercent.isNaN() -> state.thumbDisplacementPercent + interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent else -> interactionThumbTravelPercent } val thumbSizePx = max( @@ -217,7 +217,7 @@ fun Scrollbar( targetValue = with(localDensity) { thumbSizePx.toDp() }, label = "scrollbar thumb size", ) - val thumbDisplacementPx = min( + val thumbMovedPx = min( a = track.size * thumbTravelPercent, b = track.size - thumbSizePx, ) @@ -282,8 +282,8 @@ fun Scrollbar( }, ), ) { - val scrollbarThumbDisplacement = max( - a = with(localDensity) { thumbDisplacementPx.toDp() }, + val scrollbarThumbMovedDp = max( + a = with(localDensity) { thumbMovedPx.toDp() }, b = 0.dp, ) // scrollbar thumb container @@ -299,10 +299,10 @@ fun Scrollbar( .offset( y = when (orientation) { Orientation.Horizontal -> 0.dp - Orientation.Vertical -> scrollbarThumbDisplacement + Orientation.Vertical -> scrollbarThumbMovedDp }, x = when (orientation) { - Orientation.Horizontal -> scrollbarThumbDisplacement + Orientation.Horizontal -> scrollbarThumbMovedDp Orientation.Vertical -> 0.dp }, ), @@ -311,7 +311,7 @@ fun Scrollbar( } } - if (onThumbDisplaced == null) return + if (onThumbMoved == null) return // State that will be read inside the effects that follow // but will not cause re-triggering of them @@ -325,26 +325,26 @@ fun Scrollbar( return@LaunchedEffect } - var currentThumbDisplacement = updatedState.thumbDisplacementPercent - val destinationThumbDisplacement = track.thumbPosition( + var currentThumbMovedPercent = updatedState.thumbMovedPercent + val destinationThumbMovedPercent = track.thumbPosition( dimension = orientation.valueOf(pressedOffset), ) - val isPositive = currentThumbDisplacement < destinationThumbDisplacement + val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f - while (currentThumbDisplacement != destinationThumbDisplacement) { - currentThumbDisplacement = when { + while (currentThumbMovedPercent != destinationThumbMovedPercent) { + currentThumbMovedPercent = when { isPositive -> min( - a = currentThumbDisplacement + delta, - b = destinationThumbDisplacement, + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, ) else -> max( - a = currentThumbDisplacement + delta, - b = destinationThumbDisplacement, + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, ) } - onThumbDisplaced(currentThumbDisplacement) - interactionThumbTravelPercent = currentThumbDisplacement + onThumbMoved(currentThumbMovedPercent) + interactionThumbTravelPercent = currentThumbMovedPercent delay(SCROLLBAR_PRESS_DELAY_MS) } } @@ -358,7 +358,7 @@ fun Scrollbar( val currentTravel = track.thumbPosition( dimension = orientation.valueOf(draggedOffset), ) - onThumbDisplaced(currentTravel) + onThumbMoved(currentTravel) interactionThumbTravelPercent = currentTravel } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt index 4ed966da4..4d187e269 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt @@ -31,9 +31,9 @@ import androidx.compose.runtime.setValue * @param itemsAvailable the amount of items in the list. */ @Composable -fun LazyListState.rememberFastScroller( +fun LazyListState.rememberDraggableScroller( itemsAvailable: Int, -): (Float) -> Unit = rememberFastScroller( +): (Float) -> Unit = rememberDraggableScroller( itemsAvailable = itemsAvailable, scroll = ::scrollToItem, ) @@ -43,9 +43,9 @@ fun LazyListState.rememberFastScroller( * @param itemsAvailable the amount of items in the grid. */ @Composable -fun LazyGridState.rememberFastScroller( +fun LazyGridState.rememberDraggableScroller( itemsAvailable: Int, -): (Float) -> Unit = rememberFastScroller( +): (Float) -> Unit = rememberDraggableScroller( itemsAvailable = itemsAvailable, scroll = ::scrollToItem, ) @@ -56,7 +56,7 @@ fun LazyGridState.rememberFastScroller( * @param scroll a function to be invoked when an index has been identified to scroll to. */ @Composable -private inline fun rememberFastScroller( +private inline fun rememberDraggableScroller( itemsAvailable: Int, crossinline scroll: suspend (index: Int) -> Unit, ): (Float) -> Unit { diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index d4363f12c..e46ada015 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -63,7 +63,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -216,7 +216,7 @@ private fun BookmarksGrid( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - onThumbDisplaced = scrollableState.rememberFastScroller( + onThumbMoved = scrollableState.rememberDraggableScroller( itemsAvailable = itemsAvailable, ), ) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index dbcfe7eeb..f536c3385 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -93,7 +93,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DecorativeScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -238,7 +238,7 @@ internal fun ForYouScreen( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - onThumbDisplaced = state.rememberFastScroller( + onThumbMoved = state.rememberDraggableScroller( itemsAvailable = itemsAvailable, ), ) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index 7ae652344..d865f5e1a 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @@ -91,7 +91,7 @@ fun TopicsTabContent( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - onThumbDisplaced = scrollableState.rememberFastScroller( + onThumbMoved = scrollableState.rememberDraggableScroller( itemsAvailable = topics.size, ), ) diff --git a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index 08356d938..fede7766b 100644 --- a/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -81,7 +81,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -389,7 +389,7 @@ private fun SearchResultBody( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - onThumbDisplaced = state.rememberFastScroller( + onThumbMoved = state.rememberDraggableScroller( itemsAvailable = itemsAvailable, ), ) diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 84975b4ea..075e7f881 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -55,7 +55,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackg import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar -import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberFastScroller +import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -159,7 +159,7 @@ internal fun TopicScreen( .align(Alignment.CenterEnd), state = scrollbarState, orientation = Orientation.Vertical, - onThumbDisplaced = state.rememberFastScroller( + onThumbMoved = state.rememberDraggableScroller( itemsAvailable = itemsAvailable, ), ) From 9babb50c58727f731c8a5dfdacf33e88a3ad2ca9 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 22 Jul 2023 00:17:45 +0100 Subject: [PATCH 49/94] Interact with scrollbar only if drag direction matches scrollbar orientation Change-Id: I8fb5a64098ea83458d25fd2b7363a6b5dd3fef2f --- .../component/scrollbar/Scrollbar.kt | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 11997468c..74d9e0467 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -18,10 +18,11 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollb import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box @@ -41,6 +42,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot @@ -53,7 +55,9 @@ import androidx.compose.ui.unit.max import androidx.compose.ui.util.packFloats import androidx.compose.ui.util.unpackFloat1 import androidx.compose.ui.util.unpackFloat2 +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout import kotlin.math.max import kotlin.math.min @@ -221,14 +225,6 @@ fun Scrollbar( a = track.size * thumbTravelPercent, b = track.size - thumbSizePx, ) - val draggableState = rememberDraggableState { delta -> - if (draggedOffset == Offset.Unspecified) return@rememberDraggableState - - draggedOffset = when (orientation) { - Orientation.Vertical -> draggedOffset.copy(y = draggedOffset.y + delta) - Orientation.Horizontal -> draggedOffset.copy(x = draggedOffset.x + delta) - } - } // scrollbar track container Box( @@ -251,36 +247,77 @@ fun Scrollbar( .pointerInput(Unit) { detectTapGestures( onPress = { offset -> - val initialPress = PressInteraction.Press(offset) - interactionSource?.tryEmit(initialPress) - - // Start the press - pressedOffset = offset + try { + // Wait for a long press before scrolling + withTimeout(viewConfiguration.longPressTimeoutMillis) { + tryAwaitRelease() + } + } catch (e: TimeoutCancellationException) { + // Start the press triggered scroll + val initialPress = PressInteraction.Press(offset) + interactionSource?.tryEmit(initialPress) - interactionSource?.tryEmit( - when { - tryAwaitRelease() -> PressInteraction.Release(initialPress) - else -> PressInteraction.Cancel(initialPress) - }, - ) + pressedOffset = offset + interactionSource?.tryEmit( + when { + tryAwaitRelease() -> PressInteraction.Release(initialPress) + else -> PressInteraction.Cancel(initialPress) + }, + ) - // End the press - pressedOffset = Offset.Unspecified + // End the press + pressedOffset = Offset.Unspecified + } }, ) } // Process scrollbar drags - .draggable( - state = draggableState, - orientation = orientation, - interactionSource = interactionSource, - onDragStarted = { startedPosition: Offset -> - draggedOffset = startedPosition - }, - onDragStopped = { + .pointerInput(Unit) { + var dragInteraction: DragInteraction.Start? = null + val onDragStart: (Offset) -> Unit = { offset -> + val start = DragInteraction.Start() + dragInteraction = start + interactionSource?.tryEmit(start) + draggedOffset = offset + } + val onDragEnd: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Stop(it)) } + draggedOffset = Offset.Unspecified + } + val onDragCancel: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Cancel(it)) } draggedOffset = Offset.Unspecified - }, - ), + } + val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = + onDrag@{ _, delta -> + if (draggedOffset == Offset.Unspecified) return@onDrag + draggedOffset = when (orientation) { + Orientation.Vertical -> draggedOffset.copy( + y = draggedOffset.y + delta, + ) + + Orientation.Horizontal -> draggedOffset.copy( + x = draggedOffset.x + delta, + ) + } + } + + when (orientation) { + Orientation.Horizontal -> detectHorizontalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onHorizontalDrag = onDrag, + ) + + Orientation.Vertical -> detectVerticalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onVerticalDrag = onDrag, + ) + } + }, ) { val scrollbarThumbMovedDp = max( a = with(localDensity) { thumbMovedPx.toDp() }, @@ -338,6 +375,7 @@ fun Scrollbar( a = currentThumbMovedPercent + delta, b = destinationThumbMovedPercent, ) + else -> max( a = currentThumbMovedPercent + delta, b = destinationThumbMovedPercent, From ee8b70003d69212b9dd60e075aae47e78982de5a Mon Sep 17 00:00:00 2001 From: Qamar Safadi Date: Sat, 22 Jul 2023 12:07:42 +0300 Subject: [PATCH 50/94] feat: Show loading progress for Image Component as a solve for //TODO b/228077205, --- .../component/DynamicAsyncImage.kt | 33 ++++++++++++++-- .../nowinandroid/core/ui/NewsResourceCard.kt | 38 +++++++++++++++---- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index e26a824af..abd783545 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -16,11 +16,19 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme /** @@ -34,11 +42,30 @@ fun DynamicAsyncImage( placeholder: Painter? = null, ) { val iconTint = LocalTintTheme.current.iconTint - AsyncImage( - placeholder = placeholder, + SubcomposeAsyncImage( + error = { + if (placeholder != null) { + Image( + painter = placeholder, + contentDescription = "placeholder image", + ) + } + }, model = imageUrl, contentDescription = contentDescription, colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, modifier = modifier, - ) + loading = { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + Modifier.size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + }, + + ) } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 009fb1249..aed1871fa 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -44,6 +46,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -58,6 +61,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -146,20 +151,37 @@ fun NewsResourceCardExpanded( fun NewsResourceHeaderImage( headerImageUrl: String?, ) { - AsyncImage( - placeholder = if (LocalInspectionMode.current) { - painterResource(DesignsystemR.drawable.ic_placeholder_default) - } else { - // TODO b/228077205, show specific loading image visual - null - }, + + SubcomposeAsyncImage( modifier = Modifier .fillMaxWidth() .height(180.dp), contentScale = ContentScale.Crop, model = headerImageUrl, // TODO b/226661685: Investigate using alt text of image to populate content description - contentDescription = null, // decorative image + contentDescription = null, // decorative image, + error = { + if (LocalInspectionMode.current) { + Image( + painter = + painterResource(DesignsystemR.drawable.ic_placeholder_default), + contentDescription = "placeholder image", + ) + } else { + null + } + }, + loading = { + Box( + modifier = Modifier.size(180.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + Modifier.size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + }, ) } From 3284eb68a3b7ef2a4e3411e3b868dfcdef42368a Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Sat, 22 Jul 2023 12:20:49 +0300 Subject: [PATCH 51/94] feat: Show loading progress for Image Component as a solve for //TODO b/228077205, --- .../component/DynamicAsyncImage.kt | 33 ++-------------- .../nowinandroid/core/ui/NewsResourceCard.kt | 38 ++++--------------- 2 files changed, 11 insertions(+), 60 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index abd783545..e26a824af 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -16,19 +16,11 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme /** @@ -42,30 +34,11 @@ fun DynamicAsyncImage( placeholder: Painter? = null, ) { val iconTint = LocalTintTheme.current.iconTint - SubcomposeAsyncImage( - error = { - if (placeholder != null) { - Image( - painter = placeholder, - contentDescription = "placeholder image", - ) - } - }, + AsyncImage( + placeholder = placeholder, model = imageUrl, contentDescription = contentDescription, colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, modifier = modifier, - loading = { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - Modifier.size(80.dp), - color = MaterialTheme.colorScheme.tertiary, - ) - } - }, - - ) + ) } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 0342bfcaf..c9a327881 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -17,7 +17,6 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -32,7 +31,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -46,7 +44,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -61,8 +58,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter -import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -152,37 +147,20 @@ fun NewsResourceCardExpanded( fun NewsResourceHeaderImage( headerImageUrl: String?, ) { - - SubcomposeAsyncImage( + AsyncImage( + placeholder = if (LocalInspectionMode.current) { + painterResource(DesignsystemR.drawable.ic_placeholder_default) + } else { + // TODO b/228077205, show specific loading image visual + null + }, modifier = Modifier .fillMaxWidth() .height(180.dp), contentScale = ContentScale.Crop, model = headerImageUrl, // TODO b/226661685: Investigate using alt text of image to populate content description - contentDescription = null, // decorative image, - error = { - if (LocalInspectionMode.current) { - Image( - painter = - painterResource(DesignsystemR.drawable.ic_placeholder_default), - contentDescription = "placeholder image", - ) - } else { - null - } - }, - loading = { - Box( - modifier = Modifier.size(180.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - Modifier.size(80.dp), - color = MaterialTheme.colorScheme.tertiary, - ) - } - }, + contentDescription = null, // decorative image ) } From 7a05f7db96b647f009795059c4ed63eab11fc9e7 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Sat, 22 Jul 2023 12:37:44 +0300 Subject: [PATCH 52/94] feat: Show loading progress for Image Component as a solve for //TODO b/228077205, --- .../component/DynamicAsyncImage.kt | 33 +++++++++++-- .../nowinandroid/core/ui/NewsResourceCard.kt | 46 +++++++++++++------ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index e26a824af..abd783545 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -16,11 +16,19 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme /** @@ -34,11 +42,30 @@ fun DynamicAsyncImage( placeholder: Painter? = null, ) { val iconTint = LocalTintTheme.current.iconTint - AsyncImage( - placeholder = placeholder, + SubcomposeAsyncImage( + error = { + if (placeholder != null) { + Image( + painter = placeholder, + contentDescription = "placeholder image", + ) + } + }, model = imageUrl, contentDescription = contentDescription, colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, modifier = modifier, - ) + loading = { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + Modifier.size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + }, + + ) } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index c9a327881..aed1871fa 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -44,6 +46,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -58,6 +61,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -70,7 +75,6 @@ import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import java.time.ZoneId import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.util.Locale import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR @@ -147,20 +151,37 @@ fun NewsResourceCardExpanded( fun NewsResourceHeaderImage( headerImageUrl: String?, ) { - AsyncImage( - placeholder = if (LocalInspectionMode.current) { - painterResource(DesignsystemR.drawable.ic_placeholder_default) - } else { - // TODO b/228077205, show specific loading image visual - null - }, + + SubcomposeAsyncImage( modifier = Modifier .fillMaxWidth() .height(180.dp), contentScale = ContentScale.Crop, model = headerImageUrl, // TODO b/226661685: Investigate using alt text of image to populate content description - contentDescription = null, // decorative image + contentDescription = null, // decorative image, + error = { + if (LocalInspectionMode.current) { + Image( + painter = + painterResource(DesignsystemR.drawable.ic_placeholder_default), + contentDescription = "placeholder image", + ) + } else { + null + } + }, + loading = { + Box( + modifier = Modifier.size(180.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + Modifier.size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + }, ) } @@ -231,11 +252,8 @@ fun dateFormatted(publishDate: Instant): String { } } - return DateTimeFormatter - .ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(Locale.getDefault()) - .withZone(zoneId) - .format(publishDate.toJavaInstant()) + return DateTimeFormatter.ofPattern("MMM d, yyyy") + .withZone(zoneId).format(publishDate.toJavaInstant()) } @Composable From a294adb6b9bced0ec9db1534e7ca60215d8859b6 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Tue, 25 Jul 2023 01:22:21 +0300 Subject: [PATCH 53/94] feat: fix format issue --- .../core/designsystem/component/DynamicAsyncImage.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index abd783545..97c1252ce 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -66,6 +66,5 @@ fun DynamicAsyncImage( ) } }, - - ) + ) } From 8a197800c5924be812865a6af44a9957ee39a194 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Tue, 25 Jul 2023 08:28:08 +0300 Subject: [PATCH 54/94] feat: fix format issue --- .../samples/apps/nowinandroid/core/ui/NewsResourceCard.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index aed1871fa..9c8adaa59 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -60,8 +59,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter import coil.compose.SubcomposeAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag @@ -151,7 +148,6 @@ fun NewsResourceCardExpanded( fun NewsResourceHeaderImage( headerImageUrl: String?, ) { - SubcomposeAsyncImage( modifier = Modifier .fillMaxWidth() From abdb7b8bf60acd1f87c171f4137952e56488184a Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 25 Jul 2023 20:17:52 +0100 Subject: [PATCH 55/94] Update AGP 8.1.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99248c8bf..dbb82b8fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.28.0" androidDesugarJdkLibs = "1.2.2" -androidGradlePlugin = "8.0.2" +androidGradlePlugin = "8.1.0" androidxActivity = "1.7.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" From 5467aad0541039b27f22c8a5ff83d7c9d420e3ef Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 25 Jul 2023 20:18:39 +0100 Subject: [PATCH 56/94] Remove AGP lint version override --- gradle.properties | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index de14513e6..b57dc01ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,3 @@ android.nonTransitiveRClass=true # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false - -# Use newer lint version to support Kotlin 1.9 and corresponding kotlinx-metadata-jvm -# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.1.0-rc01 \ No newline at end of file From 61f7eb8823e7dcf4ffbed6a98a8ecdb78b763586 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 25 Jul 2023 21:23:37 +0200 Subject: [PATCH 57/94] Update new `CommonExtension` signature --- .../com/google/samples/apps/nowinandroid/AndroidCompose.kt | 2 +- .../google/samples/apps/nowinandroid/GradleManagedDevices.kt | 2 +- .../com/google/samples/apps/nowinandroid/KotlinAndroid.kt | 2 +- .../kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index 9950352b1..7a592e916 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -27,7 +27,7 @@ import java.io.File * Configure Compose-specific options */ internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension<*, *, *, *>, + commonExtension: CommonExtension<*, *, *, *, *>, ) { commonExtension.apply { buildFeatures { diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt index 86e29be33..6aa896444 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt @@ -25,7 +25,7 @@ import org.gradle.kotlin.dsl.invoke * Configure project for Gradle managed devices */ internal fun configureGradleManagedDevices( - commonExtension: CommonExtension<*, *, *, *>, + commonExtension: CommonExtension<*, *, *, *, *>, ) { val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd") val pixel6 = DeviceConfig("Pixel 6", 31, "aosp") diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index 8c510c017..04ee4e56e 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile * Configure base Kotlin with Android options */ internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *>, + commonExtension: CommonExtension<*, *, *, *, *>, ) { commonExtension.apply { compileSdk = 34 diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt index ef55024e2..60d059ac0 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt @@ -20,7 +20,7 @@ enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: St } fun configureFlavors( - commonExtension: CommonExtension<*, *, *, *>, + commonExtension: CommonExtension<*, *, *, *, *>, flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {} ) { commonExtension.apply { From fb2bd63d68fe5a05c29ce606927f39e0c3dd61a3 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 25 Jul 2023 20:40:54 +0100 Subject: [PATCH 58/94] Update desugar_jdk_libs to version 2.0.3 Changelog: https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99248c8bf..9de0af514 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanist = "0.28.0" -androidDesugarJdkLibs = "1.2.2" +androidDesugarJdkLibs = "2.0.3" androidGradlePlugin = "8.0.2" androidxActivity = "1.7.0" androidxAppCompat = "1.5.1" From 97414a667ad499da4669ff7ff656303e6556b0e4 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 25 Jul 2023 20:43:38 +0100 Subject: [PATCH 59/94] Update room to version 2.5.2 Changelog: https://developer.android.com/jetpack/androidx/releases/room#2.5.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99248c8bf..5ba728063 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,7 +49,7 @@ protobuf = "3.23.4" protobufPlugin = "0.9.3" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "1.0.0" -room = "2.5.0" +room = "2.5.2" secrets = "2.0.1" turbine = "0.12.1" From 57025e75874625efcf80c6cc40bd0bd5c88f8736 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Wed, 26 Jul 2023 08:40:24 +0300 Subject: [PATCH 60/94] fix: review notes --- .../apps/nowinandroid/core/ui/NewsResourceCard.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 9c8adaa59..50f41d8a1 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -72,6 +72,7 @@ import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Locale import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR @@ -157,15 +158,11 @@ fun NewsResourceHeaderImage( // TODO b/226661685: Investigate using alt text of image to populate content description contentDescription = null, // decorative image, error = { - if (LocalInspectionMode.current) { Image( painter = painterResource(DesignsystemR.drawable.ic_placeholder_default), contentDescription = "placeholder image", ) - } else { - null - } }, loading = { Box( @@ -248,8 +245,12 @@ fun dateFormatted(publishDate: Instant): String { } } - return DateTimeFormatter.ofPattern("MMM d, yyyy") - .withZone(zoneId).format(publishDate.toJavaInstant()) + return DateTimeFormatter + .ofLocalizedDate(FormatStyle.MEDIUM) + .withLocale(Locale.getDefault()) + .withZone(zoneId) + .format(publishDate.toJavaInstant()) + } @Composable From 654944430d1670084ace031ba779f06833b58ba9 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 27 Jul 2023 17:57:55 +0100 Subject: [PATCH 61/94] Fix for Retrofit R8 issue, bump version to 0.1.2 Change-Id: Ia90d693731c95248211b12b1ce2e9dfab0ecf34c --- app/build.gradle.kts | 4 ++-- app/proguard-rules.pro | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3c0aca4f..38cb7efe2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,8 @@ plugins { android { defaultConfig { applicationId = "com.google.samples.apps.nowinandroid" - versionCode = 7 - versionName = "0.1.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level + versionCode = 8 + versionName = "0.1.2" // X.Y.Z; X = Major, Y = minor, Z = Patch level // Custom test runner to set up Hilt dependency graph testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index dcaf39ce7..9c7f3b935 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,3 +7,13 @@ -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE + +# Fix for Retrofit issue https://github.com/square/retrofit/issues/3751 +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation \ No newline at end of file From 9c1ec17a39103f27c642440132bd0414f9f00e37 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Thu, 27 Jul 2023 21:53:34 +0300 Subject: [PATCH 62/94] fix: modifier to Modifier --- .../core/designsystem/component/DynamicAsyncImage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index 97c1252ce..ba5a890fe 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -57,7 +57,7 @@ fun DynamicAsyncImage( modifier = modifier, loading = { Box( - modifier = modifier, + modifier = Modifier, contentAlignment = Alignment.Center, ) { CircularProgressIndicator( From f1a2993adc8aa3e085ae29604a8976e32a462074 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Thu, 27 Jul 2023 22:03:12 +0300 Subject: [PATCH 63/94] feat: remove add placeholder todo --- .../samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index eaa0c58fa..145a7dbbc 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -394,7 +394,6 @@ fun TopicIcon( modifier: Modifier = Modifier, ) { DynamicAsyncImage( - // TODO b/228077205, show loading image visual instead of static placeholder placeholder = painterResource(R.drawable.ic_icon_placeholder), imageUrl = imageUrl, contentDescription = null, // decorative From e2762125740a9bfba5fece69ef48b19bc7473018 Mon Sep 17 00:00:00 2001 From: qamarelsafadi Date: Thu, 27 Jul 2023 22:04:26 +0300 Subject: [PATCH 64/94] feat: remove check if placeholder null and put a default placeholder if its not passed. --- .../core/designsystem/component/DynamicAsyncImage.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index ba5a890fe..94e65a780 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -26,9 +26,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.compose.SubcomposeAsyncImage +import com.google.samples.apps.nowinandroid.core.designsystem.R import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme /** @@ -44,12 +46,10 @@ fun DynamicAsyncImage( val iconTint = LocalTintTheme.current.iconTint SubcomposeAsyncImage( error = { - if (placeholder != null) { - Image( - painter = placeholder, - contentDescription = "placeholder image", - ) - } + Image( + painter = placeholder ?: painterResource(R.drawable.ic_placeholder_default), + contentDescription = "placeholder image", + ) }, model = imageUrl, contentDescription = contentDescription, From 250850cf9b1138c75b7ccd013a822d35b32bc33e Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Thu, 27 Jul 2023 20:31:42 +0100 Subject: [PATCH 65/94] Remove hardcoded topic names in `NavigationTest.kt` --- .../apps/nowinandroid/ui/NavigationTest.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 036a2955c..0cc8f4d4f 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -33,13 +33,17 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.NoActivityResumedException import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.R +import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import javax.inject.Inject import kotlin.properties.ReadOnlyProperty import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR @@ -78,6 +82,9 @@ class NavigationTest { @get:Rule(order = 3) val composeTestRule = createAndroidComposeRule() + @Inject + lateinit var datasource: FakeNiaNetworkDataSource + private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = ReadOnlyProperty { _, _ -> activity.getString(resId) } @@ -92,6 +99,9 @@ class NavigationTest { private val brand by composeTestRule.stringResource(SettingsR.string.brand_android) private val ok by composeTestRule.stringResource(SettingsR.string.dismiss_dialog_button_text) + @Before + fun setup() = hiltRule.inject() + @Test fun firstScreen_isForYou() { composeTestRule.apply { @@ -251,11 +261,11 @@ class NavigationTest { } @Test - fun navigationBar_multipleBackStackInterests() { + fun navigationBar_multipleBackStackInterests() = runTest { + suspend fun randomTopicName() = datasource.getTopics(ids = null).random().name composeTestRule.apply { onNodeWithText(interests).performClick() - // TODO: Grab string from fake data - onNodeWithText("Android Studio & Tools").performClick() + onNodeWithText(randomTopicName()).performClick() // Switch tab onNodeWithText(forYou).performClick() @@ -264,7 +274,7 @@ class NavigationTest { onNodeWithText(interests).performClick() // Verify we're not in the list of interests - onNodeWithText("Android Auto").assertDoesNotExist() // TODO: Grab string from fake data + onNodeWithText(randomTopicName()).assertDoesNotExist() } } } From bf5fdb4cf8f2b41de690d94623cb21a12863d6d9 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Thu, 27 Jul 2023 20:36:42 +0100 Subject: [PATCH 66/94] Update NavigationTest.kt --- .../samples/apps/nowinandroid/ui/NavigationTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 0cc8f4d4f..a5241a9db 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -33,7 +33,7 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.NoActivityResumedException import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.R -import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource +import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule @@ -83,7 +83,7 @@ class NavigationTest { val composeTestRule = createAndroidComposeRule() @Inject - lateinit var datasource: FakeNiaNetworkDataSource + lateinit var datasource: NiaNetworkDataSource private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = ReadOnlyProperty { _, _ -> activity.getString(resId) } @@ -262,10 +262,10 @@ class NavigationTest { @Test fun navigationBar_multipleBackStackInterests() = runTest { - suspend fun randomTopicName() = datasource.getTopics(ids = null).random().name + val topics = datasource.getTopics(ids = null) composeTestRule.apply { onNodeWithText(interests).performClick() - onNodeWithText(randomTopicName()).performClick() + onNodeWithText(topics.random().name).performClick() // Switch tab onNodeWithText(forYou).performClick() @@ -274,7 +274,7 @@ class NavigationTest { onNodeWithText(interests).performClick() // Verify we're not in the list of interests - onNodeWithText(randomTopicName()).assertDoesNotExist() + onNodeWithText(topics.random().name).assertDoesNotExist() } } } From 0a1f333fc64ec75b8a794d587a7c9764f148d8bd Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Thu, 27 Jul 2023 20:58:30 +0100 Subject: [PATCH 67/94] Update NavigationTest.kt --- .../samples/apps/nowinandroid/ui/NavigationTest.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index a5241a9db..3d14eedd6 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -27,8 +27,10 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode import androidx.test.espresso.Espresso import androidx.test.espresso.NoActivityResumedException import com.google.samples.apps.nowinandroid.MainActivity @@ -262,10 +264,13 @@ class NavigationTest { @Test fun navigationBar_multipleBackStackInterests() = runTest { - val topics = datasource.getTopics(ids = null) composeTestRule.apply { onNodeWithText(interests).performClick() - onNodeWithText(topics.random().name).performClick() + + // Select a random topic + val topic = datasource.getTopics().random().name + onNodeWithTag("interests:topics").performScrollToNode(hasText(topic)) + onNodeWithText(topic).performClick() // Switch tab onNodeWithText(forYou).performClick() @@ -274,7 +279,7 @@ class NavigationTest { onNodeWithText(interests).performClick() // Verify we're not in the list of interests - onNodeWithText(topics.random().name).assertDoesNotExist() + onNodeWithTag("interests:topics").assertDoesNotExist() } } } From 995d7b643f78a76b33a4009f0980bd3f15b2e9f2 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Thu, 27 Jul 2023 21:13:26 +0100 Subject: [PATCH 68/94] Add missing full stop in `search_not_ready` string --- feature/search/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml index 62db1da1d..2a824653e 100644 --- a/feature/search/src/main/res/values/strings.xml +++ b/feature/search/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Search Clear search text Sorry, there is no content found for your search \"%1$s\" - Sorry, we are still processing the search index. Please come back later + Sorry, we are still processing the search index. Please come back later. Try another search or explorer Interests to browse topics @@ -26,4 +26,4 @@ Updates Recent searches Clear searches - \ No newline at end of file + From 2b218b97d959d79268bb0883a9e31ebcea18cb82 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 14 Apr 2023 11:39:56 -0700 Subject: [PATCH 69/94] Switch to new Activity.enableEdgeToEdge Change-Id: I9bbb7b2d116099524525619714ef18448e707c4f --- app/build.gradle.kts | 1 - .../samples/apps/nowinandroid/MainActivity.kt | 40 +++++++++++++++---- app/src/main/res/values-night/themes.xml | 5 +-- app/src/main/res/values-v23/themes.xml | 22 ---------- app/src/main/res/values-v27/themes.xml | 23 ----------- app/src/main/res/values/themes.xml | 13 +----- gradle/libs.versions.toml | 3 +- 7 files changed, 36 insertions(+), 71 deletions(-) delete mode 100644 app/src/main/res/values-v23/themes.xml delete mode 100644 app/src/main/res/values-v27/themes.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 38cb7efe2..df5cfa296 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,6 @@ dependencies { debugImplementation(libs.androidx.compose.ui.testManifest) debugImplementation(project(":ui-test-hilt-manifest")) - implementation(libs.accompanist.systemuicontroller) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index e107fd88c..7fe1bc674 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -19,7 +19,9 @@ package com.google.samples.apps.nowinandroid import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi @@ -31,13 +33,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats import androidx.profileinstaller.ProfileVerifier -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.MainActivityUiState.Success import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper @@ -108,16 +108,28 @@ class MainActivity : ComponentActivity() { } // Turn off the decor fitting system windows, which allows us to handle insets, - // including IME animations - WindowCompat.setDecorFitsSystemWindows(window, false) + // including IME animations, and go edge-to-edge + // This also sets up the initial system bar style based on the platform theme + enableEdgeToEdge() setContent { - val systemUiController = rememberSystemUiController() val darkTheme = shouldUseDarkTheme(uiState) - // Update the dark content of the system bars to match the theme - DisposableEffect(systemUiController, darkTheme) { - systemUiController.systemBarsDarkContentEnabled = !darkTheme + // Update the edge to edge configuration to match the theme + // This is the same parameters as the default enableEdgeToEdge call, but we manually + // resolve whether or not to show dark theme using uiState, since it can be different + // than the configuration's dark theme value based on the user preference. + DisposableEffect(darkTheme) { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + android.graphics.Color.TRANSPARENT, + android.graphics.Color.TRANSPARENT, + ) { darkTheme }, + navigationBarStyle = SystemBarStyle.auto( + lightScrim, + darkScrim, + ) { darkTheme }, + ) onDispose {} } @@ -224,3 +236,15 @@ private fun shouldUseDarkTheme( DarkThemeConfig.DARK -> true } } + +/** + * The default light scrim, as defined by androidx and the platform: + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598 + */ +private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF) + +/** + * The default dark scrim, as defined by androidx and the platform: + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598 + */ +private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b) diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index b014ff647..2cf4177e6 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -16,10 +16,7 @@ --> - + - diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index 969e51914..000000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 82456f53a..7cdd25527 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -18,19 +18,10 @@ - - - - +