Merge changes from topic "inavlidation-tracker-runtime" into androidx-main
* changes:
Converting `invalidation tracker` related files in `room-runtime` from Java to Kotlin.
Initial code check-in for renaming `invalidation tracker` related files in `room-runtime` from *.java to *.kt.
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index 307ed79..0426c01 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -44,6 +44,13 @@
// public properties are shared by micro + macro benchmarks
public val suppressedErrors: Set<String>
+ val enabledRules: Set<RuleType>
+ enum class RuleType {
+ Microbenchmark,
+ Macrobenchmark,
+ BaselineProfile
+ }
+
// internal properties are microbenchmark only
internal val outputEnable: Boolean
internal val startupMode: Boolean
@@ -106,6 +113,19 @@
.filter { it.isNotEmpty() }
.toSet()
+ enabledRules = arguments.getBenchmarkArgument(
+ key = "enabledRules",
+ defaultValue = RuleType.values().joinToString(separator = ",") { it.toString() }
+ )
+ .split(',')
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .map { arg ->
+ RuleType.values().find { arg.lowercase() == it.toString().lowercase() }
+ ?: throw IllegalArgumentException("Unable to parse enabledRules arg: $arg")
+ }
+ .toSet()
+
_profiler = arguments.getProfiler(outputEnable)
profilerSampleFrequency =
arguments.getBenchmarkArgument("profiling.sampleFrequency")?.ifBlank { null }
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index ef52341..f1a1b44 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -19,6 +19,7 @@
import android.Manifest
import android.util.Log
import androidx.annotation.RestrictTo
+import androidx.benchmark.Arguments
import androidx.benchmark.BenchmarkState
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
@@ -31,6 +32,7 @@
import java.io.File
import java.io.FileNotFoundException
import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -186,6 +188,7 @@
private fun applyInternal(base: Statement, description: Description) =
Statement {
applied = true
+ assumeTrue(Arguments.RuleType.Microbenchmark in Arguments.enabledRules)
var invokeMethodName = description.methodName
Log.d(TAG, "-- Running ${description.className}#$invokeMethodName --")
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
index 0488c29..63d2072 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
@@ -18,10 +18,12 @@
import android.Manifest
import androidx.annotation.RequiresApi
+import androidx.benchmark.Arguments
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.collectBaselineProfile
import androidx.test.rule.GrantPermissionRule
+import org.junit.Assume.assumeTrue
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -69,6 +71,7 @@
private fun applyInternal(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
+ assumeTrue(Arguments.RuleType.BaselineProfile in Arguments.enabledRules)
currentDescription = description
base.evaluate()
}
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
index 8c67069..f14b0bd9 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
@@ -18,12 +18,14 @@
import android.Manifest
import androidx.annotation.IntRange
+import androidx.benchmark.Arguments
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.Metric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.macrobenchmarkWithStartupMode
import androidx.test.rule.GrantPermissionRule
+import org.junit.Assume.assumeTrue
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -125,6 +127,7 @@
private fun applyInternal(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
+ assumeTrue(Arguments.RuleType.Macrobenchmark in Arguments.enabledRules)
currentDescription = description
base.evaluate()
}
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 0d83485..185d101 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -40,6 +40,7 @@
testImplementation(libs.truth)
testImplementation(project(":internal-testutils-gradle-plugin"))
testImplementation(gradleTestKit())
+ testImplementation(libs.checkmark)
}
SdkResourceGenerator.generateForHostTest(project)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
index 582361e..75a946a 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
@@ -18,6 +18,7 @@
import androidx.testutils.gradle.ProjectSetupRule
import java.io.File
+import net.saff.checkmark.Checkmark.Companion.check
import org.gradle.testkit.runner.GradleRunner
import org.junit.Assert
import org.junit.Test
@@ -75,12 +76,11 @@
File(setup.rootDir, "build.gradle").writeText(buildGradleText)
Assert.assertTrue(privateJar.path, privateJar.exists())
- val output = GradleRunner.create().withProjectDir(setup.rootDir)
- .withArguments("tasks", "--stacktrace").build().output
- Assert.assertTrue(
- output,
- output.contains("listAndroidXProperties - Lists AndroidX-specific properties")
- )
+ GradleRunner.create().withProjectDir(setup.rootDir)
+ .withArguments("tasks", "--stacktrace")
+ .build().output.check {
+ it.contains("listAndroidXProperties - Lists AndroidX-specific properties")
+ }
}
}
}
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/MavenUploadHelperTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/MavenUploadHelperTest.kt
index 4f91ae3..e9e0022 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/MavenUploadHelperTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/MavenUploadHelperTest.kt
@@ -80,14 +80,23 @@
</dependencies>
</project>"""
- // Expect that elements in <dependencies> are sorted alphabetically.
- val expected = """<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ /*
+ It should have the following comment, but is broken b/230396269
+
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
+ */
+ // Expect that elements in <dependencies> are sorted alphabetically.
+ val expected = """<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+
+
+
+
<modelVersion>4.0.0</modelVersion>
<groupId>androidx.collection</groupId>
<artifactId>collection-jvm</artifactId>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index ebef400..4376674 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -46,6 +46,7 @@
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.DocsType
+import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.file.DuplicatesStrategy
@@ -266,6 +267,10 @@
DocsType.DOCS_TYPE_ATTRIBUTE,
project.objects.named<DocsType>(DocsType.SOURCES)
)
+ it.attribute(
+ LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+ project.objects.named<LibraryElements>(LibraryElements.JAR)
+ )
}
}
docsSourcesConfiguration = project.configurations.create("docs-sources") {
diff --git a/buildSrc/remoteBuildCache.gradle b/buildSrc/remoteBuildCache.gradle
index 6a5e7ea..d40ee47 100644
--- a/buildSrc/remoteBuildCache.gradle
+++ b/buildSrc/remoteBuildCache.gradle
@@ -46,7 +46,7 @@
settings.buildCache {
remote(GcpBuildCache) {
projectId = "androidx-ge"
- bucketName = "androidx-gradle-build-cache"
+ bucketName = "androidx-gradle-remote-cache"
push = (BUILD_NUMBER != null && !BUILD_NUMBER.startsWith("P"))
}
}
diff --git a/busytown/impl/build-metalava-and-androidx.sh b/busytown/impl/build-metalava-and-androidx.sh
index 89d6fa4..92bdb80 100755
--- a/busytown/impl/build-metalava-and-androidx.sh
+++ b/busytown/impl/build-metalava-and-androidx.sh
@@ -34,7 +34,7 @@
gw="$METALAVA_DIR/gradlew -Dorg.gradle.jvmargs=-Xmx24g"
# Use androidx prebuilt since we don't have metalava prebuilts
-export ANDROID_HOME="$WORKING_DIR/../../prebuilts/fullsdk-linux/platforms/android-31/android.jar"
+export ANDROID_HOME="$WORKING_DIR/../../prebuilts/fullsdk-linux/"
function buildMetalava() {
METALAVA_BUILD_LOG="$OUT_DIR/metalava.log"
diff --git a/camera/OWNERS b/camera/OWNERS
index 8c678a7..c1605ae 100644
--- a/camera/OWNERS
+++ b/camera/OWNERS
@@ -3,6 +3,7 @@
trevormcguire@google.com
ericng@google.com
xizh@google.com
+kailianc@google.com
charcoalchen@google.com
scottnien@google.com
wenhungteng@google.com
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 57cebb0..c7b923a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -66,7 +66,10 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -78,6 +81,46 @@
private static final String TAG = "Camera2CapturePipeline";
+ private static final Set<AfState> AF_CONVERGED_STATE_SET =
+ Collections.unmodifiableSet(EnumSet.of(
+ AfState.PASSIVE_FOCUSED,
+ AfState.PASSIVE_NOT_FOCUSED,
+ AfState.LOCKED_FOCUSED,
+ AfState.LOCKED_NOT_FOCUSED
+ ));
+
+ private static final Set<AwbState> AWB_CONVERGED_STATE_SET =
+ Collections.unmodifiableSet(EnumSet.of(
+ AwbState.CONVERGED,
+ // Unknown means cannot get valid state from CaptureResult
+ AwbState.UNKNOWN
+ ));
+
+ private static final Set<AeState> AE_CONVERGED_STATE_SET =
+ Collections.unmodifiableSet(EnumSet.of(
+ AeState.CONVERGED,
+ AeState.FLASH_REQUIRED,
+ // Unknown means cannot get valid state from CaptureResult
+ AeState.UNKNOWN
+ ));
+
+ private static final Set<AeState> AE_TORCH_AS_FLASH_CONVERGED_STATE_SET;
+
+ static {
+ EnumSet<AeState> aeStateSet = EnumSet.copyOf(AE_CONVERGED_STATE_SET);
+
+ // Some devices always show FLASH_REQUIRED when the torch is opened, so it cannot be
+ // treated as the AE converge signal.
+ aeStateSet.remove(AeState.FLASH_REQUIRED);
+
+ // AeState.UNKNOWN means it doesn't have valid AE info. For this kind of device, we tend
+ // to wait for a few more seconds for the auto exposure update. So the UNKNOWN state
+ // should not be treated as the AE converge signal.
+ aeStateSet.remove(AeState.UNKNOWN);
+
+ AE_TORCH_AS_FLASH_CONVERGED_STATE_SET = Collections.unmodifiableSet(aeStateSet);
+ }
+
@NonNull
private final Camera2CameraControlImpl mCameraControl;
@@ -141,7 +184,7 @@
}
if (isTorchAsFlash(flashType)) {
- pipeline.addTask(new TorchTask(mCameraControl, flashMode));
+ pipeline.addTask(new TorchTask(mCameraControl, flashMode, mExecutor));
} else {
pipeline.addTask(new AePreCaptureTask(mCameraControl, flashMode, aeQuirk));
}
@@ -236,7 +279,8 @@
if (!mTasks.isEmpty()) {
ListenableFuture<TotalCaptureResult> getResult =
mPipelineSubTask.isCaptureResultNeeded() ? waitForResult(
- ResultListener.NO_TIMEOUT, null) : Futures.immediateFuture(null);
+ ResultListener.NO_TIMEOUT, mCameraControl, null) :
+ Futures.immediateFuture(null);
preCapture = FutureChain.from(getResult).transformAsync(captureResult -> {
if (isFlashRequired(flashMode, captureResult)) {
@@ -245,7 +289,8 @@
return mPipelineSubTask.preCapture(captureResult);
}, mExecutor).transformAsync(is3aConvergeRequired -> {
if (is3aConvergeRequired) {
- return waitForResult(mTimeout3A, this::is3AConverged);
+ return waitForResult(mTimeout3A, mCameraControl,
+ (result) -> is3AConverged(result, false));
}
return Futures.immediateFuture(null);
}, mExecutor);
@@ -357,47 +402,46 @@
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
configBuilder.addImplementationOptions(impBuilder.build());
}
+ }
- @ExecutedBy("mExecutor")
- @NonNull
- private ListenableFuture<TotalCaptureResult> waitForResult(long waitTimeout,
- @Nullable ResultListener.Checker checker) {
- ResultListener resultListener = new ResultListener(waitTimeout, checker);
- mCameraControl.addCaptureResultListener(resultListener);
- return resultListener.getFuture();
+ @ExecutedBy("mExecutor")
+ @NonNull
+ static ListenableFuture<TotalCaptureResult> waitForResult(long waitTimeout,
+ @NonNull Camera2CameraControlImpl cameraControl,
+ @Nullable ResultListener.Checker checker) {
+ ResultListener resultListener = new ResultListener(waitTimeout, checker);
+ cameraControl.addCaptureResultListener(resultListener);
+ return resultListener.getFuture();
+ }
+
+ static boolean is3AConverged(@Nullable TotalCaptureResult totalCaptureResult,
+ boolean isTorchAsFlash) {
+ if (totalCaptureResult == null) {
+ return false;
}
- private boolean is3AConverged(@Nullable TotalCaptureResult totalCaptureResult) {
- if (totalCaptureResult == null) {
- return false;
- }
+ Camera2CameraCaptureResult captureResult = new Camera2CameraCaptureResult(
+ totalCaptureResult);
- Camera2CameraCaptureResult captureResult = new Camera2CameraCaptureResult(
- totalCaptureResult);
+ // If afMode is OFF or UNKNOWN , no need for waiting.
+ // otherwise wait until af is locked or focused.
+ boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
+ || captureResult.getAfMode() == AfMode.UNKNOWN
+ || AF_CONVERGED_STATE_SET.contains(captureResult.getAfState());
- // If afMode is OFF or UNKNOWN , no need for waiting.
- // otherwise wait until af is locked or focused.
- boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
- || captureResult.getAfMode() == AfMode.UNKNOWN
- || captureResult.getAfState() == AfState.PASSIVE_FOCUSED
- || captureResult.getAfState() == AfState.PASSIVE_NOT_FOCUSED
- || captureResult.getAfState() == AfState.LOCKED_FOCUSED
- || captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED;
-
- // Unknown means cannot get valid state from CaptureResult
- boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
- || captureResult.getAeState() == AeState.FLASH_REQUIRED
- || captureResult.getAeState() == AeState.UNKNOWN;
-
- // Unknown means cannot get valid state from CaptureResult
- boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
- || captureResult.getAwbState() == AwbState.UNKNOWN;
-
- Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
- + " AF =" + captureResult.getAfState()
- + " AWB=" + captureResult.getAwbState());
- return isAfReady && isAeReady && isAwbReady;
+ boolean isAeReady;
+ if (isTorchAsFlash) {
+ isAeReady = AE_TORCH_AS_FLASH_CONVERGED_STATE_SET.contains(captureResult.getAeState());
+ } else {
+ isAeReady = AE_CONVERGED_STATE_SET.contains(captureResult.getAeState());
}
+
+ boolean isAwbReady = AWB_CONVERGED_STATE_SET.contains(captureResult.getAwbState());
+
+ Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
+ + " AF =" + captureResult.getAfState()
+ + " AWB=" + captureResult.getAwbState());
+ return isAfReady && isAeReady && isAwbReady;
}
interface PipelineTask {
@@ -490,14 +534,19 @@
* Task to open the Torch if flash is required.
*/
static class TorchTask implements PipelineTask {
+ private static final long CHECK_3A_WITH_TORCH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(2);
private final Camera2CameraControlImpl mCameraControl;
private final @FlashMode int mFlashMode;
private boolean mIsExecuted = false;
+ @CameraExecutor
+ private final Executor mExecutor;
- TorchTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode) {
+ TorchTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode,
+ @NonNull Executor executor) {
mCameraControl = cameraControl;
mFlashMode = flashMode;
+ mExecutor = executor;
}
@ExecutedBy("mExecutor")
@@ -515,8 +564,10 @@
mCameraControl.getTorchControl().enableTorchInternal(completer, true);
return "TorchOn";
});
- return FutureChain.from(future).transform(input -> true,
- CameraXExecutors.directExecutor());
+ return FutureChain.from(future).transformAsync(
+ input -> waitForResult(CHECK_3A_WITH_TORCH_TIMEOUT_IN_NS,
+ mCameraControl, (result) -> is3AConverged(result, true)),
+ mExecutor).transform(input -> false, CameraXExecutors.directExecutor());
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index b3dd1ee..4f523b4 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -18,6 +18,7 @@
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -54,6 +55,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
@@ -403,6 +405,12 @@
@Override
public void onCaptureSequenceAborted(int captureSequenceId) {
}
+
+ @Override
+ public void onCaptureCompleted(long timestamp, int captureSequenceId,
+ @NonNull Map<CaptureResult.Key, Object> result) {
+
+ }
});
break;
case ON_CAPTURE_SESSION_ENDED:
@@ -612,5 +620,11 @@
@Override
public void onCaptureSequenceAborted(int captureSequenceId) {
}
+
+ @Override
+ public void onCaptureCompleted(long timestamp, int captureSequenceId,
+ @NonNull Map<CaptureResult.Key, Object> result) {
+
+ }
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
index b153fd8..edc66c2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
@@ -85,6 +85,9 @@
if (CaptureSessionStuckQuirk.load(cameraCharacteristicsCompat)) {
quirks.add(new CaptureSessionStuckQuirk());
}
+ if (ImageCaptureWithFlashUnderexposureQuirk.load(cameraCharacteristicsCompat)) {
+ quirks.add(new ImageCaptureWithFlashUnderexposureQuirk());
+ }
return new Quirks(quirks);
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWithFlashUnderexposureQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWithFlashUnderexposureQuirk.java
new file mode 100644
index 0000000..afcbeb4
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWithFlashUnderexposureQuirk.java
@@ -0,0 +1,64 @@
+/*
+ * 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
+ *
+ * http://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 androidx.camera.camera2.internal.compat.quirk;
+
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A quirk to denote even when the camera uses flash ON/AUTO mode, but the captured image is
+ * still underexposed.
+ *
+ * <p>QuirkSummary
+ * Bug Id: 228800282
+ * Description: While the flash is in ON/AUTO mode and the camera fires the flash in a dark
+ * environment, the captured photos are underexposed after continuously capturing 2
+ * or more photos.
+ * Device(s): Samsung Galaxy A2 Core (sm-a260f), Samsung Galaxy J5 (sm-j530f), Samsung Galaxy
+ * J6 (sm-j600g), Samsung Galaxy J7 Neo (sm-j701f), Samsung Galaxy J7 Prime
+ * (sm-g610f)
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ImageCaptureWithFlashUnderexposureQuirk implements UseTorchAsFlashQuirk {
+
+ public static final String BUILD_BRAND = "SAMSUNG";
+
+ // List of devices with the issue. See b/228800282.
+ public static final List<String> BUILD_MODELS = Arrays.asList(
+ "sm-a260f", // Samsung Galaxy A2 Core
+ "sm-j530f", // Samsung Galaxy J5
+ "sm-j600g", // Samsung Galaxy J6
+ "sm-j701f", // Samsung Galaxy J7 Neo
+ "sm-g610f" // Samsung Galaxy J7 Prime
+ );
+
+ static boolean load(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
+ return BUILD_BRAND.equals(Build.BRAND.toUpperCase(Locale.US))
+ && BUILD_MODELS.contains(Build.MODEL.toLowerCase(Locale.US))
+ && cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == LENS_FACING_BACK;
+ }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBufferTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ArrayRingBufferTest.kt
similarity index 62%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBufferTest.kt
rename to camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ArrayRingBufferTest.kt
index d147aa7..61c2f9d 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBufferTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ArrayRingBufferTest.kt
@@ -13,18 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.camera.camera2.internal.util
-import androidx.camera.camera2.internal.util.RingBuffer.OnRemoveCallback
+package androidx.camera.core.internal.utils
+
import androidx.testutils.assertThrows
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.Mockito.any
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
@RunWith(JUnit4::class)
class ArrayRingBufferTest {
@@ -32,40 +30,42 @@
@Test
fun testEnqueue() {
val testBuffer: RingBuffer<Int> =
- androidx.camera.camera2.internal.util.ArrayRingBuffer(3)
+ ArrayRingBuffer(3)
testBuffer.enqueue(1)
testBuffer.enqueue(2)
testBuffer.enqueue(3)
testBuffer.enqueue(4)
- assertThat(testBuffer.dequeue()).isEqualTo(2)
+ Truth.assertThat(testBuffer.dequeue()).isEqualTo(2)
}
@Test
fun testDequeue_correctValueIsDequeued() {
@Suppress("UNCHECKED_CAST")
- val mockCallback: OnRemoveCallback<Int> = mock(
- OnRemoveCallback::class.java) as OnRemoveCallback<Int>
+ val mockCallback: RingBuffer.OnRemoveCallback<Int> = Mockito.mock(
+ RingBuffer.OnRemoveCallback::class.java
+ ) as RingBuffer.OnRemoveCallback<Int>
val testBuffer: RingBuffer<Int> =
- androidx.camera.camera2.internal.util.ArrayRingBuffer(
+ ArrayRingBuffer(
3,
mockCallback
)
testBuffer.enqueue(1)
testBuffer.enqueue(2)
testBuffer.enqueue(3)
- assertThat(testBuffer.dequeue()).isEqualTo(1)
- verify(mockCallback, times(0)).onRemove(any())
+ Truth.assertThat(testBuffer.dequeue()).isEqualTo(1)
+ Mockito.verify(mockCallback, Mockito.times(0)).onRemove(any())
}
@Test
fun testDequeue_OnRemoveCallbackCalledOnlyWhenDiscardingItemsDueToCapacity() {
@Suppress("UNCHECKED_CAST")
- val mockCallback: OnRemoveCallback<Int> = mock(
- OnRemoveCallback::class.java) as OnRemoveCallback<Int>
+ val mockCallback: RingBuffer.OnRemoveCallback<Int> = Mockito.mock(
+ RingBuffer.OnRemoveCallback::class.java
+ ) as RingBuffer.OnRemoveCallback<Int>
val testBuffer: RingBuffer<Int> =
- androidx.camera.camera2.internal.util.ArrayRingBuffer(
+ ArrayRingBuffer(
3,
mockCallback
)
@@ -73,15 +73,15 @@
testBuffer.enqueue(2)
testBuffer.enqueue(3)
testBuffer.enqueue(4)
- verify(mockCallback).onRemove(1)
- assertThat(testBuffer.dequeue()).isEqualTo(2)
- verify(mockCallback, times(1)).onRemove(any())
+ Mockito.verify(mockCallback).onRemove(1)
+ Truth.assertThat(testBuffer.dequeue()).isEqualTo(2)
+ Mockito.verify(mockCallback, Mockito.times(1)).onRemove(any())
}
@Test()
fun testDequeue_exceptionThrownWhenBufferEmpty() {
val testBuffer: RingBuffer<Int> =
- androidx.camera.camera2.internal.util.ArrayRingBuffer(5)
+ ArrayRingBuffer(5)
assertThrows(NoSuchElementException::class.java, testBuffer::dequeue)
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBufferTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ZslRingBufferTest.kt
similarity index 93%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBufferTest.kt
rename to camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ZslRingBufferTest.kt
index 4fcaff0..76d4471 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBufferTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/utils/ZslRingBufferTest.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.camera.camera2.internal.util
+package androidx.camera.core.internal.utils
import android.os.Build
import androidx.annotation.RequiresApi
@@ -24,7 +24,7 @@
import androidx.camera.core.impl.CameraCaptureMetaData.AwbState
import androidx.camera.core.impl.CameraCaptureResult
import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.camera2.internal.util.RingBuffer.OnRemoveCallback
+import androidx.camera.core.internal.utils.RingBuffer.OnRemoveCallback
import androidx.camera.testing.fakes.FakeImageProxy
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -55,7 +55,7 @@
fun enqueue_ensureOldFramesAreRemoved() {
@Suppress("UNCHECKED_CAST")
val onRemoveCallback = mock(OnRemoveCallback::class.java) as OnRemoveCallback<ImageProxy>
- val ringBuffer = androidx.camera.camera2.internal.util.ZslRingBuffer(
+ val ringBuffer = ZslRingBuffer(
2,
onRemoveCallback
)
@@ -75,7 +75,7 @@
fun enqueue_framesWithBad3AStatesNotQueued() {
@Suppress("UNCHECKED_CAST")
val onRemoveCallback = mock(OnRemoveCallback::class.java) as OnRemoveCallback<ImageProxy>
- val ringBuffer = androidx.camera.camera2.internal.util.ZslRingBuffer(
+ val ringBuffer = ZslRingBuffer(
2,
onRemoveCallback
)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index 99f8a46..bb86a51 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -16,6 +16,7 @@
package androidx.camera.core.impl;
+import android.hardware.camera2.CaptureResult;
import android.media.ImageReader;
import androidx.annotation.NonNull;
@@ -23,6 +24,8 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraInfo;
+import java.util.Map;
+
/**
* A processor for (1) transforming the surfaces used in Preview/ImageCapture/ImageAnalysis
* into final session configuration where intermediate {@link ImageReader}s could be created for
@@ -171,5 +174,30 @@
* @param captureSequenceId id of the current capture sequence
*/
void onCaptureSequenceAborted(int captureSequenceId);
+
+ /**
+ * Capture result callback that needs to be called when the process capture results are
+ * ready as part of frame post-processing.
+ *
+ * This callback will fire after {@link #onCaptureStarted}, {@link #onCaptureProcessStarted}
+ * and before {@link #onCaptureSequenceCompleted}. The callback is not expected to fire
+ * in case of capture failure {@link #onCaptureFailed} or capture abort
+ * {@link #onCaptureSequenceAborted}.
+ *
+ * @param timestamp The timestamp at start of capture. The same timestamp value
+ * passed to {@link #onCaptureStarted}.
+ * @param captureSequenceId the capture id of the request that generated the capture
+ * results. This is the return value of either
+ * {@link #startRepeating} or {@link #startCapture}.
+ * @param result Map containing the supported capture results. Do note
+ * that if results 'android.jpeg.quality' and
+ * 'android.jpeg.orientation' are present in the process
+ * capture input results, then the values must also be passed
+ * as part of this callback. Both Camera2 and CameraX guarantee
+ * that those two settings and results are always supported and
+ * applied by the corresponding framework.
+ */
+ void onCaptureCompleted(long timestamp, int captureSequenceId,
+ @NonNull Map<CaptureResult.Key, Object> result);
}
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBuffer.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ArrayRingBuffer.java
similarity index 94%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBuffer.java
rename to camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ArrayRingBuffer.java
index aca7060..c60acdc 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ArrayRingBuffer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ArrayRingBuffer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.camera2.internal.util;
+package androidx.camera.core.internal.utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,7 +30,7 @@
private final int mRingBufferCapacity;
private final ArrayDeque<T> mBuffer;
- @Nullable final OnRemoveCallback<T> mOnRemoveCallback;
+ @Nullable final OnRemoveCallback<T> mOnRemoveCallback;
public ArrayRingBuffer(int ringBufferCapacity) {
this(ringBufferCapacity, null);
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/RingBuffer.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/RingBuffer.java
similarity index 96%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/RingBuffer.java
rename to camera/camera-core/src/main/java/androidx/camera/core/internal/utils/RingBuffer.java
index 3fec45a..b767825 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/RingBuffer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/RingBuffer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.camera2.internal.util;
+package androidx.camera.core.internal.utils;
import androidx.annotation.NonNull;
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBuffer.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
similarity index 97%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBuffer.java
rename to camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
index 0b70d28..eb29d0a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/ZslRingBuffer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.camera2.internal.util;
+package androidx.camera.core.internal.utils;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index a364d4b..ccb0dac 100755
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -17,6 +17,8 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -98,4 +100,17 @@
public Range<Long> getEstimatedCaptureLatencyRange(@NonNull Size captureOutputSize) {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Nullable
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Nullable
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index d68d8eb..2d26639 100755
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -17,6 +17,8 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -98,4 +100,16 @@
public Range<Long> getEstimatedCaptureLatencyRange(@NonNull Size captureOutputSize) {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Nullable
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Nullable
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index b943e0a..66c5839d 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -17,6 +17,8 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -98,4 +100,17 @@
public Range<Long> getEstimatedCaptureLatencyRange(@NonNull Size captureOutputSize) {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Nullable
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Nullable
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index 90de15a..3eee146 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.impl;
+import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
@@ -23,12 +24,14 @@
import android.view.Surface;
import java.util.Map;
+import java.util.concurrent.Executor;
/**
* The interface for processing a set of {@link Image}s that have captured.
*
* @since 1.0
*/
+@SuppressLint("UnknownNullness")
public interface CaptureProcessorImpl extends ProcessorImpl {
/**
* Process a set images captured that were requested.
@@ -41,4 +44,23 @@
* invalid after this method completes, so no references to them should be kept.
*/
void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
+
+ /**
+ * Process a set images captured that were requested.
+ *
+ * <p> The result of the processing step should be written to the {@link Surface} that was
+ * received by {@link #onOutputSurface(Surface, int)}.
+ *
+ * @param results The map of {@link ImageFormat#YUV_420_888} format images and metadata
+ * to process. The {@link Image} that are contained within the map will
+ * become invalid after this method completes, so no references to them
+ * should be kept.
+ * @param resultCallback Capture result callback to be called once the capture result
+ * values of the processed image are ready.
+ * @param executor The executor to run the callback on. If null then the callback will
+ * run on any arbitrary executor.
+ * @since 1.3
+ */
+ void process(Map<Integer, Pair<Image, TotalCaptureResult>> results,
+ ProcessResultImpl resultCallback, Executor executor);
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 66f4a50..f1191dcf 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -17,6 +17,8 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -98,4 +100,16 @@
public Range<Long> getEstimatedCaptureLatencyRange(@NonNull Size captureOutputSize) {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Nullable
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Nullable
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
index 571c2e3..88bd105 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
@@ -16,14 +16,15 @@
package androidx.camera.extensions.impl;
+import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
-import androidx.annotation.Nullable;
-
import java.util.List;
/**
@@ -31,6 +32,7 @@
*
* @since 1.0
*/
+@SuppressLint("UnknownNullness")
public interface ImageCaptureExtenderImpl extends ExtenderStateListener {
/**
* Indicates whether the extension is supported on the device.
@@ -76,10 +78,10 @@
* returned list is not null, it will be used to find the best resolutions combination for
* the bound use cases.
*
- * @return the customized supported resolutions.
+ * @return the customized supported resolutions, or null to support all sizes retrieved from
+ * {@link android.hardware.camera2.params.StreamConfigurationMap}.
* @since 1.1
*/
- @Nullable
List<Pair<Integer, Size[]>> getSupportedResolutions();
/**
@@ -87,7 +89,7 @@
* resolution.
*
* <p>This includes the time spent processing the multi-frame capture request along with any
- * additional time for encoding of the processed buffer in the framework if necessary.
+ * additional time for encoding of the processed buffer in the framework if necessary.</p>
*
* @param captureOutputSize size of the capture output surface. If it is null or not in the
* supported output sizes, maximum capture output size is used for
@@ -96,6 +98,65 @@
* null if no capture latency info can be provided.
* @since 1.2
*/
- @Nullable
- Range<Long> getEstimatedCaptureLatencyRange(@Nullable Size captureOutputSize);
+ Range<Long> getEstimatedCaptureLatencyRange(Size captureOutputSize);
+
+ /**
+ * Return a list of orthogonal capture request keys.
+ *
+ * <p>Any keys included in the list will be configurable by clients of the extension and will
+ * affect the extension functionality.</p>
+ *
+ * <p>Do note that the list of keys applies to {@link PreviewExtenderImpl} as well.</p>
+ *
+ * <p>Also note that the keys {@link CaptureRequest#JPEG_QUALITY} and
+ * {@link CaptureRequest#JPEG_ORIENTATION} are always supported regardless being added in the
+ * list or not. To support common camera operations like zoom, tap-to-focus, flash and
+ * exposure compensation, we recommend supporting the following keys if possible.
+ * <pre>
+ * zoom: {@link CaptureRequest#CONTROL_ZOOM_RATIO}
+ * {@link CaptureRequest#SCALER_CROP_REGION}
+ * tap-to-focus:
+ * {@link CaptureRequest#CONTROL_AF_MODE}
+ * {@link CaptureRequest#CONTROL_AF_TRIGGER}
+ * {@link CaptureRequest#CONTROL_AF_REGIONS}
+ * {@link CaptureRequest#CONTROL_AE_REGIONS}
+ * {@link CaptureRequest#CONTROL_AWB_REGIONS}
+ * flash:
+ * {@link CaptureRequest#CONTROL_AE_MODE}
+ * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER}
+ * {@link CaptureRequest#FLASH_MODE}
+ * exposure compensation:
+ * {@link CaptureRequest#CONTROL_AE_EXPOSURE_COMPENSATION}
+ * </pre>
+ * On basic extensions that implement 1.2 or prior version, the above keys are all supported
+ * explicitly. When migrating from 1.2 or prior to 1.3, please note that both CameraX and
+ * Camera2 will honor the returned list and support only the keys contained in it. For
+ * example, if OEM decides to return only {@link CaptureRequest#CONTROL_ZOOM_RATIO} and
+ * {@link CaptureRequest#SCALER_CROP_REGION} in the 1.3 implementation, it means only zoom is
+ * supported for the app while tap-to-focus , flash and exposure compensation are not allowed.
+ *
+ * @return List of supported orthogonal capture keys, or an empty list if no capture settings
+ * are not supported.
+ * @since 1.3
+ */
+ List<CaptureRequest.Key> getAvailableCaptureRequestKeys();
+
+ /**
+ * Return a list of supported capture result keys.
+ *
+ * <p>Any keys included in this list must be available as part of the registered
+ * {@link ProcessResultImpl} callback. In case frame processing is not supported,
+ * then the Camera2/CameraX framework will use the list to filter and notify camera clients
+ * using the respective camera results.</p>
+ *
+ * <p>At the very minimum, it is expected that the result key list is a superset of the
+ * capture request keys.</p>
+ *
+ * <p>Do note that the list of keys applies to {@link PreviewExtenderImpl} as well.</p>
+ *
+ * @return List of supported capture result keys, or an empty list if capture results are not
+ * supported.
+ * @since 1.3
+ */
+ List<CaptureResult.Key> getAvailableCaptureResultKeys();
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 3b39cf1..c8ac978 100755
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -17,6 +17,8 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -98,4 +100,16 @@
public Range<Long> getEstimatedCaptureLatencyRange(@NonNull Size captureOutputSize) {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Nullable
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Nullable
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index e7ecaa1..f203eba 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,16 +16,20 @@
package androidx.camera.extensions.impl;
+import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
+import java.util.concurrent.Executor;
+
/**
* Processes a single {@link Image} and {@link TotalCaptureResult} to produce an output to a
* stream.
*
* @since 1.0
*/
+@SuppressLint("UnknownNullness")
public interface PreviewImageProcessorImpl extends ProcessorImpl {
/**
* Processes the requested image capture.
@@ -38,4 +42,23 @@
* @param result The metadata associated with the image to process.
*/
void process(Image image, TotalCaptureResult result);
+
+ /**
+ * Processes the requested image capture.
+ *
+ * <p> The result of the processing step should be written to the {@link android.view.Surface}
+ * that was received by {@link ProcessorImpl#onOutputSurface(android.view.Surface, int)}.
+ *
+ * @param image The {@link ImageFormat#YUV_420_888} format image to process. This will
+ * be invalid after the method completes so no reference to it should be
+ * kept.
+ * @param result The metadata associated with the image to process.
+ * @param resultCallback Capture result callback to be called once the capture result
+ * values of the processed image are ready.
+ * @param executor The executor to run the callback on. If null then the callback will
+ * run on any arbitrary executor.
+ * @since 1.3
+ */
+ void process(Image image, TotalCaptureResult result, ProcessResultImpl resultCallback,
+ Executor executor);
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java
new file mode 100644
index 0000000..d0e3605
--- /dev/null
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ProcessResultImpl.java
@@ -0,0 +1,45 @@
+/*
+ * 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
+ *
+ * http://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 androidx.camera.extensions.impl;
+
+import android.annotation.SuppressLint;
+import android.hardware.camera2.CaptureResult;
+import android.util.Pair;
+
+import java.util.List;
+
+/**
+ * Allows clients to receive information about the capture result values of processed frames.
+ *
+ * @since 1.3
+ */
+@SuppressLint("UnknownNullness")
+public interface ProcessResultImpl {
+ /**
+ * Capture result callback that needs to be called when the process capture results are
+ * ready as part of frame post-processing.
+ *
+ * @param shutterTimestamp The shutter time stamp of the processed frame.
+ * @param result Key value pairs for all supported capture results. Do note that
+ * if results 'android.jpeg.quality' and 'android.jpeg.orientation'
+ * are present in the process capture input results, then the values
+ * must also be passed as part of this callback. Both Camera2 and
+ * CameraX guarantee that those two settings and results are always
+ * supported and applied by the corresponding framework.
+ */
+ void onCaptureCompleted(long shutterTimestamp, List<Pair<CaptureResult.Key, Object>> result);
+}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
index 873c681..465bfe8 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -125,7 +127,7 @@
* Returns supported output sizes for Image Analysis (YUV_420_888 format).
*
* <p>OEM can optionally support a YUV surface for ImageAnalysis along with Preview/ImageCapture
- * output surfaces. If imageAnalysis YUV surface is not supported, OEM should return null or a
+ * output surfaces. If imageAnalysis YUV surface is not supported, OEM should return null or
* empty list.
*/
List<Size> getSupportedYuvAnalysisResolutions(String cameraId);
@@ -135,4 +137,52 @@
* required for starting a extension and cleanup.
*/
SessionProcessorImpl createSessionProcessor();
+
+ /**
+ * Returns a list of orthogonal capture request keys.
+ *
+ * <p>Any keys included in the list will be configurable by clients of the extension and will
+ * affect the extension functionality.</p>
+ *
+ * <p>Please note that the keys {@link CaptureRequest#JPEG_QUALITY} and
+ * {@link CaptureRequest#JPEG_ORIENTATION} are always supported regardless being added in the
+ * list or not. To support common camera operations like zoom, tap-to-focus, flash and
+ * exposure compensation, we recommend supporting the following keys if possible.
+ * <pre>
+ * zoom: {@link CaptureRequest#CONTROL_ZOOM_RATIO}
+ * {@link CaptureRequest#SCALER_CROP_REGION}
+ * tap-to-focus:
+ * {@link CaptureRequest#CONTROL_AF_MODE}
+ * {@link CaptureRequest#CONTROL_AF_TRIGGER}
+ * {@link CaptureRequest#CONTROL_AF_REGIONS}
+ * {@link CaptureRequest#CONTROL_AE_REGIONS}
+ * {@link CaptureRequest#CONTROL_AWB_REGIONS}
+ * flash:
+ * {@link CaptureRequest#CONTROL_AE_MODE}
+ * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER}
+ * {@link CaptureRequest#FLASH_MODE}
+ * exposure compensation:
+ * {@link CaptureRequest#CONTROL_AE_EXPOSURE_COMPENSATION}
+ * </pre>
+ *
+ * @return List of supported orthogonal capture keys, or an empty list if no capture settings
+ * are not supported.
+ * @since 1.3
+ */
+ List<CaptureRequest.Key> getAvailableCaptureRequestKeys();
+
+ /**
+ * Returns a list of supported capture result keys.
+ *
+ * <p>Any keys included in this list must be available as part of the registered
+ * {@link SessionProcessorImpl.CaptureCallback#onCaptureCompleted} callback.</p>
+ *
+ * <p>At the very minimum, it is expected that the result key list is a superset of the
+ * capture request keys.</p>
+ *
+ * @return List of supported capture result keys, or
+ * an empty list if capture results are not supported.
+ * @since 1.3
+ */
+ List<CaptureResult.Key> getAvailableCaptureResultKeys();
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
index 7753258..0d3bd4a 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/AutoAdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -76,4 +78,14 @@
public SessionProcessorImpl createSessionProcessor() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
index 91d8171b..1dec326 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BeautyAdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -76,4 +78,14 @@
public SessionProcessorImpl createSessionProcessor() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
index b05740d..bc41b4e 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -76,4 +78,14 @@
public SessionProcessorImpl createSessionProcessor() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
index bfd8e9a..06157dc3 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/HdrAdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -77,4 +79,14 @@
public SessionProcessorImpl createSessionProcessor() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
index fc05224..97da5c1 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/NightAdvancedExtenderImpl.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.util.Range;
import android.util.Size;
@@ -76,4 +78,14 @@
public SessionProcessorImpl createSessionProcessor() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public List<CaptureRequest.Key> getAvailableCaptureRequestKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public List<CaptureResult.Key> getAvailableCaptureResultKeys() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
index 75f51ce..fabfc2b 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
import android.view.Surface;
import java.util.Map;
@@ -123,6 +124,22 @@
void setParameters(Map<CaptureRequest.Key<?>, Object> parameters);
/**
+ * CameraX / Camera2 will call this interface in response to client requests involving
+ * the output preview surface. Typical examples include requests that include AF/AE triggers.
+ * Extensions can disregard any capture request keys that were not advertised in
+ * {@link AdvancedExtenderImpl#getAvailableCaptureRequestKeys}.
+ *
+ * @param triggers Capture request key value map.
+ * @param callback a callback to report the status.
+ * @return the id of the capture sequence.
+ *
+ * @throws IllegalArgumentException If there are no valid settings that can be applied
+ *
+ * @since 1.3
+ */
+ int startTrigger(Map<CaptureRequest.Key<?>, Object> triggers, CaptureCallback callback);
+
+ /**
* This will be invoked once after the {@link android.hardware.camera2.CameraCaptureSession}
* has been created. {@link RequestProcessorImpl} is passed for OEM to submit single
* requests or set repeating requests. This ExtensionRequestProcessor will be valid to use
@@ -235,5 +252,30 @@
* @param captureSequenceId id of the current capture sequence
*/
void onCaptureSequenceAborted(int captureSequenceId);
+
+ /**
+ * Capture result callback that needs to be called when the process capture results are
+ * ready as part of frame post-processing.
+ *
+ * This callback will fire after {@link #onCaptureStarted}, {@link #onCaptureProcessStarted}
+ * and before {@link #onCaptureSequenceCompleted}. The callback is not expected to fire
+ * in case of capture failure {@link #onCaptureFailed} or capture abort
+ * {@link #onCaptureSequenceAborted}.
+ *
+ * @param timestamp The timestamp at start of capture. The same timestamp value
+ * passed to {@link #onCaptureStarted}.
+ * @param captureSequenceId the capture id of the request that generated the capture
+ * results. This is the return value of either
+ * {@link #startRepeating} or {@link #startCapture}.
+ * @param result Map containing the supported capture results. Do note
+ * that if results 'android.jpeg.quality' and
+ * 'android.jpeg.orientation' are present in the process
+ * capture input results, then the values must also be passed
+ * as part of this callback. Both Camera2 and CameraX guarantee
+ * that those two settings and results are always supported and
+ * applied by the corresponding framework.
+ */
+ void onCaptureCompleted(long timestamp, int captureSequenceId,
+ Map<CaptureResult.Key, Object> result);
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
index a9dc65d..ac88af5 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
@@ -462,5 +462,11 @@
public void onCaptureSequenceAborted(int captureSequenceId) {
mCaptureCallback.onCaptureSequenceAborted(captureSequenceId);
}
+
+ @Override
+ public void onCaptureCompleted(long timestamp, int captureSequenceId,
+ Map<CaptureResult.Key, Object> result) {
+ mCaptureCallback.onCaptureCompleted(timestamp, captureSequenceId, result);
+ }
}
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index a921d98..4669d5f 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -59,7 +59,6 @@
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -456,7 +455,6 @@
file.delete()
}
- @FlakyTest(bugId = 229247066)
@Test
fun canReceiveRecordingStats() {
clearInvocations(videoRecordEventListener)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
index 85500eb..d469d68 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
@@ -285,28 +285,28 @@
fun pauseResumeEncoder_getChronologicalData() {
// Arrange.
fakeAudioLoop.start()
- val dataList = ArrayList<EncodedData>()
+ val inOrder = inOrder(encoderCallback)
// Act.
encoder.start()
- verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+ inOrder.verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
encoder.pause()
- verify(encoderCallback, timeout(5000L)).onEncodePaused()
-
- // Save all values before clear invocations
- val startCaptor = ArgumentCaptor.forClass(EncodedData::class.java)
- verify(encoderCallback, atLeastOnce()).onEncodedData(startCaptor.capture())
- dataList.addAll(startCaptor.allValues)
- clearInvocations(encoderCallback)
+ inOrder.verify(encoderCallback, timeout(5000L)).onEncodePaused()
encoder.start()
- val resumeCaptor = ArgumentCaptor.forClass(EncodedData::class.java)
- verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(resumeCaptor.capture())
- dataList.addAll(resumeCaptor.allValues)
+ inOrder.verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
// Assert.
- verifyDataInChronologicalOrder(dataList)
+ val captor = ArgumentCaptor.forClass(EncodedData::class.java)
+ verify(
+ encoderCallback,
+ Mockito.atLeast(/*start*/5 + /*resume*/5)
+ ).onEncodedData(captor.capture())
+ verifyDataInChronologicalOrder(captor.allValues)
+
+ // Cleanup.
+ encoder.stop()
}
@Test
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
index a64ab37..ccaaccc 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
@@ -63,7 +63,7 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.atLeast
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.inOrder
@@ -243,29 +243,23 @@
@Test
fun pauseResumeVideoEncoder_getChronologicalData() {
- val dataList = ArrayList<EncodedData>()
+ val inOrder = inOrder(videoEncoderCallback)
videoEncoder.start()
- verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+ inOrder.verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
videoEncoder.pause()
- verify(videoEncoderCallback, timeout(5000L)).onEncodePaused()
-
- // Save all values before clear invocations
- val startCaptor = ArgumentCaptor.forClass(EncodedData::class.java)
- verify(videoEncoderCallback, atLeastOnce()).onEncodedData(startCaptor.capture())
- dataList.addAll(startCaptor.allValues)
- clearInvocations(videoEncoderCallback)
+ inOrder.verify(videoEncoderCallback, timeout(5000L)).onEncodePaused()
videoEncoder.start()
- val resumeCaptor = ArgumentCaptor.forClass(EncodedData::class.java)
+ inOrder.verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+ val captor = ArgumentCaptor.forClass(EncodedData::class.java)
verify(
videoEncoderCallback,
- timeout(15000L).atLeast(5)
- ).onEncodedData(resumeCaptor.capture())
- dataList.addAll(resumeCaptor.allValues)
-
- verifyDataInChronologicalOrder(dataList)
+ atLeast(/*start*/5 + /*resume*/5)
+ ).onEncodedData(captor.capture())
+ verifyDataInChronologicalOrder(captor.allValues)
}
@Test
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index 2c377ad..0f504df 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -284,12 +284,13 @@
*/
@Override
public void start() {
+ final long startTriggerTimeUs = generatePresentationTimeUs();
mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
mLastDataStopTimestamp = null;
- final long startTimeUs = generatePresentationTimeUs();
+ final long startTimeUs = startTriggerTimeUs;
Logger.d(mTag, "Start on " + DebugUtils.readableUs(startTimeUs));
try {
if (mIsFlushedAfterEndOfStream) {
@@ -320,7 +321,7 @@
pauseRange != null && pauseRange.getUpper() == NO_LIMIT_LONG,
"There should be a \"pause\" before \"resume\"");
final long pauseTimeUs = pauseRange.getLower();
- final long resumeTimeUs = generatePresentationTimeUs();
+ final long resumeTimeUs = startTriggerTimeUs;
mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, resumeTimeUs));
// Do not update total paused duration here since current output buffer may
// still before the pause range.
@@ -397,6 +398,7 @@
*/
@Override
public void stop(long expectedStopTimeUs) {
+ final long stopTriggerTimeUs = generatePresentationTimeUs();
mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
@@ -414,7 +416,7 @@
}
long stopTimeUs;
if (expectedStopTimeUs == TIMESTAMP_ANY) {
- stopTimeUs = generatePresentationTimeUs();
+ stopTimeUs = stopTriggerTimeUs;
} else if (expectedStopTimeUs < startTimeUs) {
// If the recording is stopped immediately after started, it's possible
// that the expected stop time is less than the start time because the
@@ -422,7 +424,7 @@
// this case so that the recording can be stopped correctly.
Logger.w(mTag, "The expected stop time is less than the start time. Use "
+ "current time as stop time.");
- stopTimeUs = generatePresentationTimeUs();
+ stopTimeUs = stopTriggerTimeUs;
} else {
stopTimeUs = expectedStopTimeUs;
}
@@ -499,6 +501,7 @@
*/
@Override
public void pause() {
+ final long pauseTriggerTimeUs = generatePresentationTimeUs();
mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
@@ -513,7 +516,7 @@
break;
case STARTED:
// Create and insert a pause/resume range.
- final long pauseTimeUs = generatePresentationTimeUs();
+ final long pauseTimeUs = pauseTriggerTimeUs;
Logger.d(mTag, "Pause on " + DebugUtils.readableUs(pauseTimeUs));
mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, NO_LIMIT_LONG));
setState(PAUSED);
@@ -834,6 +837,18 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mEncoderExecutor")
+ long getAdjustedTimeUs(@NonNull MediaCodec.BufferInfo bufferInfo) {
+ long adjustedTimeUs;
+ if (mTotalPausedDurationUs > 0L) {
+ adjustedTimeUs = bufferInfo.presentationTimeUs - mTotalPausedDurationUs;
+ } else {
+ adjustedTimeUs = bufferInfo.presentationTimeUs;
+ }
+ return adjustedTimeUs;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mEncoderExecutor")
boolean isInPauseRange(long timeUs) {
for (Range<Long> range : mActivePauseResumeTimeRanges) {
if (range.contains(timeUs)) {
@@ -940,6 +955,7 @@
*/
private long mLastSentPresentationTimeUs = 0L;
private boolean mIsOutputBufferInPauseState = false;
+ private boolean mIsKeyFrameRequired = false;
MediaCodecCallback() {
if (mIsVideoEncoder
@@ -1014,14 +1030,18 @@
if (!mHasFirstData) {
mHasFirstData = true;
}
- if (mTotalPausedDurationUs > 0) {
- bufferInfo.presentationTimeUs -= mTotalPausedDurationUs;
+ long adjustedTimeUs = getAdjustedTimeUs(bufferInfo);
+ if (bufferInfo.presentationTimeUs != adjustedTimeUs) {
+ // If adjusted time <= last sent time, the buffer should have been
+ // detected and dropped in checkBufferInfo().
+ Preconditions.checkState(
+ adjustedTimeUs > mLastSentPresentationTimeUs);
+ bufferInfo.presentationTimeUs = adjustedTimeUs;
if (DEBUG) {
- Logger.d(mTag, "Reduce bufferInfo.presentationTimeUs to "
- + DebugUtils.readableUs(bufferInfo.presentationTimeUs));
+ Logger.d(mTag, "Adjust bufferInfo.presentationTimeUs to "
+ + DebugUtils.readableUs(adjustedTimeUs));
}
}
-
mLastSentPresentationTimeUs = bufferInfo.presentationTimeUs;
try {
EncodedDataImpl encodedData = new EncodedDataImpl(mediaCodec, index,
@@ -1153,12 +1173,28 @@
return true;
}
- if (!mHasFirstData && mIsVideoEncoder && !isKeyFrame(bufferInfo)) {
- Logger.d(mTag, "Drop buffer by first video frame is not key frame.");
- requestKeyFrameToMediaCodec();
+ // We should check if the adjusted time is valid. see b/189114207.
+ if (getAdjustedTimeUs(bufferInfo) <= mLastSentPresentationTimeUs) {
+ Logger.d(mTag, "Drop buffer by adjusted time is less than the last sent time.");
+ if (mIsVideoEncoder && isKeyFrame(bufferInfo)) {
+ mIsKeyFrameRequired = true;
+ }
return true;
}
+ if (!mHasFirstData && !mIsKeyFrameRequired && mIsVideoEncoder) {
+ mIsKeyFrameRequired = true;
+ }
+
+ if (mIsKeyFrameRequired) {
+ if (!isKeyFrame(bufferInfo)) {
+ Logger.d(mTag, "Drop buffer by not a key frame.");
+ requestKeyFrameToMediaCodec();
+ return true;
+ }
+ mIsKeyFrameRequired = false;
+ }
+
return false;
}
@@ -1213,23 +1249,10 @@
}
} else if (mIsOutputBufferInPauseState && !isInPauseRange) {
// From pause to resume
+ Logger.d(mTag, "Switch to resume state");
+ mIsOutputBufferInPauseState = false;
if (mIsVideoEncoder && !isKeyFrame(bufferInfo)) {
- // If a video frame is not a key frame, do not switch to resume state.
- // This is because a key frame is required to be the first encoded data
- // after resume, otherwise output video will have "shattered" transitioning
- // effect.
- Logger.d(mTag, "Not a key frame, don't switch to resume state.");
- requestKeyFrameToMediaCodec();
- } else {
- // It should check if the adjusted time is valid before switch to resume.
- // It may get invalid adjusted time, see b/189114207.
- long adjustedTimeUs = bufferInfo.presentationTimeUs - mTotalPausedDurationUs;
- if (adjustedTimeUs > mLastSentPresentationTimeUs) {
- Logger.d(mTag, "Switch to resume state");
- mIsOutputBufferInPauseState = false;
- } else {
- Logger.d(mTag, "Adjusted time by pause duration is invalid.");
- }
+ mIsKeyFrameRequired = true;
}
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 9d2016d..ae641a1 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -1139,6 +1139,9 @@
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
+ if (mCamera == null) {
+ return false;
+ }
// Since we are showing full camera preview we will be using
// DisplayOrientedMeteringPointFactory to map the view's (x, y) to a
// metering point.
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
index 99c9316..ab40d55 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
@@ -31,11 +31,11 @@
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.ViewActions.swipeLeft
-import androidx.test.espresso.action.ViewActions.swipeRight
+import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -103,18 +103,19 @@
}
}
- // The test makes sure the TextureView surface texture keeps the same after swipe out/in.
+ // The test makes sure the TextureView surface texture keeps the same after switch.
@Test
- fun testPreviewViewUpdateAfterSwipeOutIn() {
+ fun testPreviewViewUpdateAfterSwitch() {
launchActivity(lensFacing).use { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
- // swipe out CameraFragment and then swipe in to check Preview update
- onView(withId(R.id.viewPager)).perform(swipeLeft())
+ // Switch from CameraFragment to BlankFragment, and then switch back to check Preview
+ // update
+ onView(withText(ViewPagerActivity.BLANK_FRAGMENT_TAB_TITLE)).perform(click())
onView(withId(R.id.blank_textview)).check(matches(isDisplayed()))
- onView(withId(R.id.viewPager)).perform(swipeRight())
+ onView(withText(ViewPagerActivity.CAMERA_FRAGMENT_TAB_TITLE)).perform(click())
onView(withId(R.id.preview_textureview)).check(matches(isDisplayed()))
// Check if the surface texture of TextureView continues getting updates after
// detaching from window and then attaching to window.
@@ -123,25 +124,26 @@
}
@Test
- fun testPreviewViewUpdateAfterSwipeOutAndStop_ResumeAndSwipeIn() {
+ fun testPreviewViewUpdateAfterSwitchOutAndStop_ResumeAndSwitchBack() {
launchActivity(lensFacing).use { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
- // swipe out CameraFragment and then Stop and Resume ViewPagerActivity
- onView(withId(R.id.viewPager)).perform(swipeLeft())
+ // Switch from CameraFragment to BlankFragment, and then Stop and Resume
+ // ViewPagerActivity
+ onView(withText(ViewPagerActivity.BLANK_FRAGMENT_TAB_TITLE)).perform(click())
onView(withId(R.id.blank_textview)).check(matches(isDisplayed()))
scenario.moveToState(State.CREATED)
scenario.moveToState(State.RESUMED)
mDevice.waitForIdle(ACTION_IDLE_TIMEOUT)
- // After resume, swipe in CameraFragment to check Preview in stream state
- onView(withId(R.id.viewPager)).perform(swipeRight())
+ // After resume, switch back to CameraFragment, to check Preview in stream state
+ onView(withText(ViewPagerActivity.CAMERA_FRAGMENT_TAB_TITLE)).perform(click())
onView(withId(R.id.preview_textureview)).check(matches(isDisplayed()))
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
- // The test covers pause/resume and ViewPager2 swipe out/in behaviors. Hence, need to
+ // The test covers pause/resume and ViewPager switch behaviors. Hence, need to
// check if the surface texture of TextureView continues getting updates, or not.
assertSurfaceTextureFramesUpdate(scenario)
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivity.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivity.kt
index 92272b4..b0a0319 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivity.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivity.kt
@@ -22,6 +22,7 @@
import android.os.Build
import android.os.Bundle
import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.camera.integration.uiwidgets.databinding.ActivityViewpagerBinding
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -43,6 +44,12 @@
)
private const val TAG = " ViewPagerActivity"
private const val REQUEST_CODE_PERMISSIONS = 8
+
+ @VisibleForTesting
+ const val CAMERA_FRAGMENT_TAB_TITLE = "CAMERA_VIEW"
+
+ @VisibleForTesting
+ const val BLANK_FRAGMENT_TAB_TITLE = "BLANK_VIEW"
}
private lateinit var binding: ActivityViewpagerBinding
@@ -76,6 +83,7 @@
private fun setupAdapter() {
Log.d(TAG, "Setup ViewPagerAdapter. ")
binding.viewPager.adapter = ViewPagerAdapter(supportFragmentManager)
+ binding.tabLayout.setupWithViewPager(binding.viewPager)
}
override fun onRequestPermissionsResult(
@@ -117,5 +125,11 @@
1 -> TextViewFragment.newInstance()
else -> throw IllegalArgumentException()
}
+
+ override fun getPageTitle(position: Int) = when (position) {
+ 0 -> CAMERA_FRAGMENT_TAB_TITLE
+ 1 -> BLANK_FRAGMENT_TAB_TITLE
+ else -> throw IllegalArgumentException()
+ }
}
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/res/layout/activity_viewpager.xml b/camera/integration-tests/uiwidgetstestapp/src/main/res/layout/activity_viewpager.xml
index 4852348d..743b28c 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/res/layout/activity_viewpager.xml
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/res/layout/activity_viewpager.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +13,22 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<androidx.viewpager.widget.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/viewPager"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".viewpager.ViewPagerActivity" />
\ No newline at end of file
+ android:orientation="vertical">
+
+ <androidx.viewpager.widget.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/viewPager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ tools:context=".viewpager.ViewPagerActivity" />
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car/app/OWNERS b/car/app/OWNERS
index bbf5309..4964102 100644
--- a/car/app/OWNERS
+++ b/car/app/OWNERS
@@ -1,5 +1,6 @@
# Bug component: 460472
-shiufai@google.com
-limarafael@google.com
-babakbo@google.com
-jayyoo@google.com
+davedandeneau@google.com
+calcoholado@google.com
+
+# back up
+babakbo@google.com
\ No newline at end of file
diff --git a/car/app/app-automotive/api/OWNERS b/car/app/app-automotive/api/OWNERS
index 0d62452..816ecbd 100644
--- a/car/app/app-automotive/api/OWNERS
+++ b/car/app/app-automotive/api/OWNERS
@@ -1,10 +1,10 @@
set noparent
-shiufai@google.com
+davedandeneau@google.com
+calcoholado@google.com
sgurun@google.com
# back up
-limarafael@google.com
babakbo@google.com
# core team
diff --git a/car/app/app-projected/api/OWNERS b/car/app/app-projected/api/OWNERS
index 3c42cee..ab91e1e 100644
--- a/car/app/app-projected/api/OWNERS
+++ b/car/app/app-projected/api/OWNERS
@@ -1,11 +1,9 @@
set noparent
-shiufai@google.com
+davedandeneau@google.com
+calcoholado@google.com
sgurun@google.com
-# back up
-limarafael@google.com
-
# core team
alanv@google.com
aurimas@google.com
diff --git a/car/app/app-samples/showcase/common/build.gradle b/car/app/app-samples/showcase/common/build.gradle
index 4517db7..29fdc59 100644
--- a/car/app/app-samples/showcase/common/build.gradle
+++ b/car/app/app-samples/showcase/common/build.gradle
@@ -28,7 +28,7 @@
dependencies {
implementation(project(":car:app:app"))
- implementation("com.squareup.leakcanary:leakcanary-android:2.7")
+ implementation(libs.leakcanary)
implementation("androidx.core:core:1.7.0")
implementation(project(":annotation:annotation-experimental"))
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index 1c0b5d7..f578a5f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -127,7 +127,7 @@
<!-- FinishAppScreen -->
<string name="finish_app_msg">This will finish the app, and when you return it will pre-seed a permission screen</string>
<string name="finish_app_title">Finish App Demo</string>
- <string name="finish_app_demo_title">Pre-seed the Screen backstack on next run Demo</string>
+ <string name="finish_app_demo_title">Pre-seed the Permission Screen on next run Demo</string>
<!-- LoadingDemoScreen -->
<string name="loading_demo_title">Loading Demo</string>
@@ -164,8 +164,8 @@
<string name="arrived_address_msg">Google Bellevue Office\n1120 112th Ave NE</string>
<!-- RoutingDemoModels -->
- <string name="current_step_cue">Roy st 520</string>
- <string name="next_step_cue">I5 Aurora Ave N</string>
+ <string name="current_step_cue" translatable="false">Roy st 520</string>
+ <string name="next_step_cue" translatable="false">I5 Aurora Ave N</string>
<string name="travel_est_trip_text">Pick Up Alice</string>
<!-- NotificationDemoScreen -->
@@ -364,7 +364,7 @@
<string name="image_test_title">Image test</string>
<string name="image_test_text">Image changes are allowed</string>
<string name="additional_data_title">Additional Data</string>
- <string name="additional_data_text">Updates allows on back operations.</string>
+ <string name="additional_data_text">Updates allowed on back operations.</string>
<!-- StartScreen -->
<string name="misc_templates_demos_title">Misc Templates Demos</string>
diff --git a/car/app/app-testing/api/OWNERS b/car/app/app-testing/api/OWNERS
index fcf4b74..f067310 100644
--- a/car/app/app-testing/api/OWNERS
+++ b/car/app/app-testing/api/OWNERS
@@ -1,9 +1,7 @@
set noparent
-shiufai@google.com
-
-# back up
-limarafael@google.com
+davedandeneau@google.com
+calcoholado@google.com
# core team
alanv@google.com
diff --git a/car/app/app/api/OWNERS b/car/app/app/api/OWNERS
index 5e04c8a..8fffa15 100644
--- a/car/app/app/api/OWNERS
+++ b/car/app/app/api/OWNERS
@@ -1,11 +1,9 @@
set noparent
-shiufai@google.com
+calcoholado@google.com
+davedandeneau@google.com
sgurun@google.com
-# back up
-limarafael@google.com
-
# core team
alanv@google.com
aurimas@google.com
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index eedae0f..35aa266 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -43,7 +43,6 @@
macosX64()
macosArm64()
linuxX64()
- mingwX64()
}
sourceSets {
@@ -62,12 +61,12 @@
if (enableNative) {
nativeMain
- configure([linuxX64Main, macosX64Main, macosArm64Main, mingwX64Main]) {
+ configure([linuxX64Main, macosX64Main, macosArm64Main]) {
dependsOn nativeMain
}
nativeTest
- configure([linuxX64Test, macosX64Test, macosArm64Test, mingwX64Test]) {
+ configure([linuxX64Test, macosX64Test, macosArm64Test]) {
dependsOn nativeTest
}
}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
index 6230ddb..83dfc46 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
@@ -23,15 +23,12 @@
* CircularArray is a generic circular array data structure that provides O(1) random read, O(1)
* prepend and O(1) append. The CircularArray automatically grows its capacity when number of added
* items is over its capacity.
- */
-public class CircularArray<E>
-
-/**
- * Creates a circular array with capacity for at least [minCapacity] elements.
+ *
+ * @constructor Creates a circular array with capacity for at least [minCapacity] elements.
*
* @param minCapacity the minimum capacity, between 1 and 2^30 inclusive
*/
-@JvmOverloads public constructor(minCapacity: Int = 8) {
+public class CircularArray<E> @JvmOverloads public constructor(minCapacity: Int = 8) {
private var elements: Array<E?>
private var head = 0
private var tail = 0
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/SimpleArrayMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/SimpleArrayMap.kt
index d1ed31a..d0d2319 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/SimpleArrayMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/SimpleArrayMap.kt
@@ -44,20 +44,17 @@
private const val BASE_SIZE = 4
/**
- * Base implementation of [ArrayMap] that doesn't include any standard Java
- * container API interoperability. These features are generally heavier-weight ways
+ * Base implementation of [ArrayMap][androidx.collection.ArrayMap] that doesn't include any standard
+ * Java container API interoperability. These features are generally heavier-weight ways
* to interact with the container, so discouraged, but they can be useful to make it
* easier to use as a drop-in replacement for HashMap. If you don't need them, this
* class can be preferable since it doesn't bring in any of the implementation of those
* APIs, allowing that code to be stripped by ProGuard.
+ *
+ * @constructor Create a new [SimpleArrayMap] with a given initial capacity. The default capacity of
+ * an array map is 0, and will grow once items are added to it.
*/
-public open class SimpleArrayMap<K, V>
-
-/**
- * Create a new [SimpleArrayMap] with a given initial capacity. The default capacity of an array
- * map is 0, and will grow once items are added to it.
- */
-@JvmOverloads public constructor(capacity: Int = 0) {
+public open class SimpleArrayMap<K, V> @JvmOverloads public constructor(capacity: Int = 0) {
private var hashes: IntArray = when (capacity) {
0 -> EMPTY_INTS
else -> IntArray(capacity)
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
index 757301a..c30e713 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -30,7 +30,6 @@
* both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry
* object for each mapping.
*
- *
* Note that this container keeps its mappings in an array data structure, using a binary search to
* find keys. The implementation is not intended to be appropriate for data structures that may
* contain large numbers of items. It is generally slower than a traditional HashMap, since lookups
@@ -38,26 +37,22 @@
* For containers holding up to hundreds of items, the performance difference is not significant,
* less than 50%.
*
- *
* To help with performance, the container includes an optimization when removing keys: instead of
* compacting its array immediately, it leaves the removed entry marked as deleted. The entry can
* then be re-used for the same key, or compacted later in a single garbage collection step of all
* removed entries. This garbage collection will need to be performed at any time the array needs to
* be grown or the map size or entry values are retrieved.
*
- *
* It is possible to iterate over the items in this container using [keyAt] and [valueAt]. Iterating
* over the keys using [keyAt] with ascending values of the index will return the keys in ascending
* order, or the values corresponding to the keys in ascending order in the case of [valueAt].
+ *
+ * @constructor Creates a new [LongSparseArray] containing no mappings that will not require any
+ * additional memory allocation to store the specified number of mappings. If you supply an initial
+ * capacity of 0, the sparse array will be initialized with a light-weight representation not
+ * requiring any additional array allocations.
*/
public open class LongSparseArray<E>
-
-/**
- * Creates a new [LongSparseArray] containing no mappings that will not require any additional
- * memory allocation to store the specified number of mappings. If you supply an initial capacity
- * of 0, the sparse array will be initialized with a light-weight representation not requiring any
- * additional array allocations.
- */
@JvmOverloads public constructor(initialCapacity: Int = 10) : Cloneable {
private var garbage = false
private var keys: LongArray
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/LruCache.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/LruCache.kt
index 3d33871..68980b3 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/LruCache.kt
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/LruCache.kt
@@ -20,18 +20,17 @@
import kotlin.Long.Companion.MAX_VALUE
/**
- * Static library version of `android.util.LruCache`. Used to write apps
- * that run on API levels prior to 12. When running on API level 12 or above,
- * this implementation is still used; it does not try to switch to the
- * framework's implementation. See the framework SDK documentation for a class
- * overview.
+ * Static library version of `android.util.LruCache`. Used to write apps that run on API levels
+ * prior to 12. When running on API level 12 or above, this implementation is still used; it does
+ * not try to switch to the framework's implementation. See the framework SDK documentation for a
+ * class overview.
+ *
+ * @constructor Creates a new [LruCache]
+ * @param maxSize for caches that do not override [sizeOf], this is the maximum number of entries in
+ * the cache. For all other caches, this is the maximum sum of the sizes of the entries in this
+ * cache.
*/
public open class LruCache<K : Any, V : Any>
-/**
- * @param maxSize for caches that do not override [sizeOf], this is
- * the maximum number of entries in the cache. For all other caches,
- * this is the maximum sum of the sizes of the entries in this cache.
- */
public constructor(@IntRange(from = 1, to = MAX_VALUE) private var maxSize: Int) {
init {
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index ecf0cc1..0c9a27e 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -426,7 +426,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderImplKt {
}
public final class LazyListKt {
@@ -558,6 +558,9 @@
public final class LazyGridItemPlacementAnimatorKt {
}
+ public final class LazyGridItemProviderImplKt {
+ }
+
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
}
@@ -568,9 +571,6 @@
property public abstract int maxLineSpan;
}
- public final class LazyGridItemsProviderImplKt {
- }
-
public final class LazyGridKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 1334225..f8ec8fe 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -488,7 +488,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderImplKt {
}
public final class LazyListKt {
@@ -621,6 +621,9 @@
public final class LazyGridItemPlacementAnimatorKt {
}
+ public final class LazyGridItemProviderImplKt {
+ }
+
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
}
@@ -632,9 +635,6 @@
property public abstract int maxLineSpan;
}
- public final class LazyGridItemsProviderImplKt {
- }
-
public final class LazyGridKt {
}
@@ -715,18 +715,18 @@
public final class IntervalListKt {
}
- @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemsProvider {
- method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
+ @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
+ method @androidx.compose.runtime.Composable public void Item(int index);
method public Object? getContentType(int index);
- method public int getItemsCount();
+ method public int getItemCount();
method public Object getKey(int index);
method public java.util.Map<java.lang.Object,java.lang.Integer> getKeyToIndexMap();
- property public abstract int itemsCount;
+ property public abstract int itemCount;
property public abstract java.util.Map<java.lang.Object,java.lang.Integer> keyToIndexMap;
}
public final class LazyLayoutKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayout(androidx.compose.foundation.lazy.layout.LazyLayoutItemsProvider itemsProvider, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState? prefetchState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayout(androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider itemProvider, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState? prefetchState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
}
@androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public sealed interface LazyLayoutMeasureScope extends androidx.compose.ui.layout.MeasureScope {
@@ -758,7 +758,7 @@
}
@androidx.compose.foundation.ExperimentalFoundationApi public interface PinnableParent {
- method public androidx.compose.foundation.lazy.layout.PinnableParent.PinnedItemsHandle pinBeyondBoundsItems();
+ method public androidx.compose.foundation.lazy.layout.PinnableParent.PinnedItemsHandle pinItems();
}
@androidx.compose.foundation.ExperimentalFoundationApi public static interface PinnableParent.PinnedItemsHandle {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index ecf0cc1..0c9a27e 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -426,7 +426,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderImplKt {
}
public final class LazyListKt {
@@ -558,6 +558,9 @@
public final class LazyGridItemPlacementAnimatorKt {
}
+ public final class LazyGridItemProviderImplKt {
+ }
+
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
}
@@ -568,9 +571,6 @@
property public abstract int maxLineSpan;
}
- public final class LazyGridItemsProviderImplKt {
- }
-
public final class LazyGridKt {
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
new file mode 100644
index 0000000..9da2191
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
@@ -0,0 +1,233 @@
+/*
+ * 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
+ *
+ * http://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.
+ */
+@file:OptIn(ExperimentalTextApi::class)
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+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.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.TileMode
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun TextBrushDemo() {
+ LazyColumn {
+ item {
+ TagLine(tag = "Shader")
+ BrushDemo()
+ }
+ item {
+ TagLine(tag = "Brush Emojis")
+ BrushGraphicalEmoji()
+ }
+ item {
+ TagLine(tag = "SingleLine Span Brush")
+ SingleLineSpanBrush()
+ }
+ item {
+ TagLine(tag = "MultiLine Span Brush")
+ MultiLineSpanBrush()
+ }
+ item {
+ TagLine(tag = "Animated Brush")
+ AnimatedBrush()
+ }
+ item {
+ TagLine(tag = "Shadow and Brush")
+ ShadowAndBrush()
+ }
+ item {
+ TagLine(tag = "TextField")
+ TextFieldBrush()
+ }
+ }
+}
+
+@Composable
+fun BrushDemo() {
+ Text(
+ "Brush is awesome\nBrush is awesome\nBrush is awesome",
+ style = TextStyle(
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ tileMode = TileMode.Mirror
+ ),
+ fontSize = 30.sp
+ )
+ )
+}
+
+@Composable
+fun BrushGraphicalEmoji() {
+ Text(
+ "\uD83D\uDEF3\uD83D\uDD2E\uD83E\uDDED\uD83E\uDD5D\uD83E\uDD8C\uD83D\uDE0D",
+ style = TextStyle(
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ tileMode = TileMode.Mirror
+ )
+ ),
+ fontSize = 30.sp
+ )
+}
+
+@Composable
+fun SingleLineSpanBrush() {
+ val infiniteTransition = rememberInfiniteTransition()
+ val start by infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = 4000f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 2000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+ Text(
+ buildAnnotatedString {
+ append("Brush is awesome\n")
+ withStyle(
+ SpanStyle(
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ start = Offset(start, 0f),
+ tileMode = TileMode.Mirror
+ )
+ )
+ ) {
+ append("Brush is awesome")
+ }
+ append("\nBrush is awesome")
+ },
+ fontSize = 30.sp,
+ )
+}
+
+@Composable
+fun MultiLineSpanBrush() {
+ Text(
+ buildAnnotatedString {
+ append("Brush is aweso")
+ withStyle(
+ SpanStyle(
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ tileMode = TileMode.Mirror
+ )
+ )
+ ) {
+ append("me\nBrush is awesome\nCo")
+ }
+ append("mpose is awesome")
+ },
+ fontSize = 30.sp,
+ )
+}
+
+@Composable
+fun AnimatedBrush() {
+ val infiniteTransition = rememberInfiniteTransition()
+ val radius by infiniteTransition.animateFloat(
+ initialValue = 100f,
+ targetValue = 300f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+ Text(
+ text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum" +
+ " rhoncus est volutpat venenatis. Fusce semper, sapien ut venenatis" +
+ " pellentesque, lorem dui aliquam sapien, non pharetra diam neque " +
+ "id mi",
+ style = TextStyle(
+ brush = Brush.radialGradient(
+ *RainbowStops.zip(RainbowColors).toTypedArray(),
+ radius = radius,
+ tileMode = TileMode.Mirror
+ ),
+ fontSize = 30.sp
+ )
+ )
+}
+
+@Composable
+fun ShadowAndBrush() {
+ Text(
+ "Brush is awesome",
+ style = TextStyle(
+ shadow = Shadow(
+ offset = Offset(8f, 8f),
+ blurRadius = 4f,
+ color = Color.Black
+ ),
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ tileMode = TileMode.Mirror
+ )
+ ),
+ fontSize = 42.sp
+ )
+}
+
+@Composable
+fun TextFieldBrush() {
+ var text by remember { mutableStateOf("Brush is awesome") }
+ TextField(
+ value = text,
+ onValueChange = { text = it },
+ modifier = Modifier.fillMaxWidth(),
+ textStyle = TextStyle(
+ brush = Brush.linearGradient(
+ colors = RainbowColors,
+ tileMode = TileMode.Mirror
+ ),
+ fontSize = 30.sp
+ )
+ )
+}
+
+private val RainbowColors = listOf(
+ Color(0xff9c4f96),
+ Color(0xffff6355),
+ Color(0xfffba949),
+ Color(0xfffae442),
+ Color(0xff8bd448),
+ Color(0xff2aa8f2)
+)
+private val RainbowStops = listOf(0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
index 0b0adf0..023eb045 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
@@ -49,9 +49,8 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.style.LineHeightBehavior
-import androidx.compose.ui.text.style.LineHeightTrim
-import androidx.compose.ui.text.style.LineVerticalAlignment
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@@ -73,11 +72,11 @@
var lineHeightSp = remember { mutableStateOf(60f) }
var lineHeightEm = remember { mutableStateOf(1f) }
var lineHeightEnabled = remember { mutableStateOf(false) }
- val lineHeightBehaviorEnabled = remember { mutableStateOf(false) }
- var lineVerticalAlignment = remember {
- mutableStateOf(LineHeightBehavior.Default.alignment)
+ val lineHeightStyleEnabled = remember { mutableStateOf(false) }
+ var lineHeightAlignment = remember {
+ mutableStateOf(LineHeightStyle.Default.alignment)
}
- var lineHeightTrim = remember { mutableStateOf(LineHeightBehavior.Default.trim) }
+ var lineHeightTrim = remember { mutableStateOf(LineHeightStyle.Default.trim) }
val includeFontPadding = remember { mutableStateOf(false) }
val applyMaxLines = remember { mutableStateOf(false) }
val ellipsize = remember { mutableStateOf(false) }
@@ -89,19 +88,19 @@
LineHeightConfiguration(lineHeightSp, lineHeightEm, lineHeightEnabled)
StringConfiguration(useSizedSpan, singleLine, useTallScript)
FontPaddingAndMaxLinesConfiguration(includeFontPadding, applyMaxLines, ellipsize)
- LineHeightBehaviorConfiguration(
- lineHeightBehaviorEnabled,
+ LineHeightStyleConfiguration(
+ lineHeightStyleEnabled,
lineHeightTrim,
- lineVerticalAlignment
+ lineHeightAlignment
)
Spacer(Modifier.padding(16.dp))
TextWithLineHeight(
lineHeightEnabled.value,
lineHeightSp.value,
lineHeightEm.value,
- if (lineHeightBehaviorEnabled.value) {
- LineHeightBehavior(
- alignment = lineVerticalAlignment.value,
+ if (lineHeightStyleEnabled.value) {
+ LineHeightStyle(
+ alignment = lineHeightAlignment.value,
trim = lineHeightTrim.value
)
} else null,
@@ -173,22 +172,22 @@
@OptIn(ExperimentalTextApi::class)
@Composable
-private fun LineHeightBehaviorConfiguration(
- lineHeightBehaviorEnabled: MutableState<Boolean>,
- lineHeightTrim: MutableState<LineHeightTrim>,
- lineVerticalAlignment: MutableState<LineVerticalAlignment>
+private fun LineHeightStyleConfiguration(
+ lineHeightStyleEnabled: MutableState<Boolean>,
+ lineHeightTrim: MutableState<Trim>,
+ lineHeightAlignment: MutableState<LineHeightStyle.Alignment>
) {
Column(Modifier.horizontalScroll(rememberScrollState())) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
- checked = lineHeightBehaviorEnabled.value,
- onCheckedChange = { lineHeightBehaviorEnabled.value = it }
+ checked = lineHeightStyleEnabled.value,
+ onCheckedChange = { lineHeightStyleEnabled.value = it }
)
- Text("LineHeightBehavior", style = HintStyle)
+ Text("LineHeightStyle", style = HintStyle)
}
Column(Modifier.padding(horizontal = 16.dp)) {
- LineHeightTrimOptions(lineHeightTrim, lineHeightBehaviorEnabled.value)
- LineHeightAlignmentOptions(lineVerticalAlignment, lineHeightBehaviorEnabled.value)
+ LineHeightTrimOptions(lineHeightTrim, lineHeightStyleEnabled.value)
+ LineHeightAlignmentOptions(lineHeightAlignment, lineHeightStyleEnabled.value)
}
}
}
@@ -196,14 +195,14 @@
@OptIn(ExperimentalTextApi::class)
@Composable
private fun LineHeightAlignmentOptions(
- lineVerticalAlignment: MutableState<LineVerticalAlignment>,
+ lineHeightAlignment: MutableState<LineHeightStyle.Alignment>,
enabled: Boolean
) {
val options = listOf(
- LineVerticalAlignment.Proportional,
- LineVerticalAlignment.Top,
- LineVerticalAlignment.Center,
- LineVerticalAlignment.Bottom
+ LineHeightStyle.Alignment.Proportional,
+ LineHeightStyle.Alignment.Top,
+ LineHeightStyle.Alignment.Center,
+ LineHeightStyle.Alignment.Bottom
)
Row(
@@ -216,15 +215,15 @@
Modifier
.height(56.dp)
.selectable(
- selected = (option == lineVerticalAlignment.value),
- onClick = { lineVerticalAlignment.value = option },
+ selected = (option == lineHeightAlignment.value),
+ onClick = { lineHeightAlignment.value = option },
role = Role.RadioButton,
enabled = enabled
),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
- selected = (option == lineVerticalAlignment.value),
+ selected = (option == lineHeightAlignment.value),
onClick = null,
enabled = enabled
)
@@ -237,14 +236,14 @@
@OptIn(ExperimentalTextApi::class)
@Composable
private fun LineHeightTrimOptions(
- lineHeightTrim: MutableState<LineHeightTrim>,
+ lineHeightTrim: MutableState<Trim>,
enabled: Boolean
) {
val options = listOf(
- LineHeightTrim.Both,
- LineHeightTrim.None,
- LineHeightTrim.FirstLineTop,
- LineHeightTrim.LastLineBottom
+ Trim.Both,
+ Trim.None,
+ Trim.FirstLineTop,
+ Trim.LastLineBottom
)
Row(
@@ -367,7 +366,7 @@
lineHeightEnabled: Boolean,
lineHeightSp: Float,
lineHeightEm: Float,
- lineHeightBehavior: LineHeightBehavior?,
+ lineHeightStyle: LineHeightStyle?,
includeFontPadding: Boolean,
applyMaxLines: Boolean,
ellipsize: Boolean,
@@ -410,7 +409,7 @@
val style = TextStyle(
fontSize = FontSize,
color = TextMetricColors.Default.text,
- lineHeightBehavior = lineHeightBehavior,
+ lineHeightStyle = lineHeightStyle,
lineHeight = if (lineHeightEnabled) {
if (lineHeightSp > 0) lineHeightSp.sp else lineHeightEm.em
} else {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 217fdd6..e8303ed 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -22,24 +22,41 @@
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
+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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.ListItem
+import androidx.compose.material.Switch
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+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.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
@@ -65,8 +82,10 @@
import androidx.compose.ui.text.samples.TextOverflowVisibleMinHeightSample
import androidx.compose.ui.text.samples.TextStyleSample
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
@@ -624,4 +643,91 @@
inlineContent = mapOf(inlineContentId to inlineTextContent),
modifier = Modifier.fillMaxWidth()
)
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun EllipsizeDemo() {
+ var softWrap by remember { mutableStateOf(true) }
+ var ellipsis by remember { mutableStateOf(true) }
+ var withSpans by remember { mutableStateOf(false) }
+ val lineHeight = remember { mutableStateOf(16.sp) }
+ val heightRestriction = remember { mutableStateOf(45.dp) }
+
+ Column {
+ ListItem(
+ Modifier.selectable(softWrap) { softWrap = !softWrap },
+ trailing = { Switch(softWrap, null) }
+ ) {
+ Text("Soft wrap")
+ }
+ ListItem(
+ Modifier.selectable(ellipsis) { ellipsis = !ellipsis },
+ trailing = { Switch(ellipsis, null) }
+ ) {
+ Text("Ellipsis")
+ }
+ ListItem(
+ Modifier.selectable(withSpans) { withSpans = !withSpans },
+ trailing = { Switch(withSpans, null) },
+ secondaryText = { Text("Text with spans") }
+ ) {
+ Text("Spans")
+ }
+
+ Row(horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ IconButton({
+ heightRestriction.value = (heightRestriction.value + 5.dp).coerceAtMost(300.dp)
+ }) {
+ Icon(Icons.Default.KeyboardArrowUp, "Increase height")
+ }
+ Text("Max height ${heightRestriction.value}")
+ IconButton({
+ heightRestriction.value = (heightRestriction.value - 5.dp).coerceAtLeast(0.dp)
+ }) {
+ Icon(Icons.Default.KeyboardArrowDown, "Decrease height")
+ }
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ IconButton({
+ lineHeight.value = ((lineHeight.value.value + 2f)).coerceAtMost(100f).sp
+ }) {
+ Icon(Icons.Default.KeyboardArrowUp, "Increase line height")
+ }
+ Text("Line height ${lineHeight.value.value.toInt().sp}")
+ IconButton({
+ lineHeight.value = ((lineHeight.value.value - 2f)).coerceAtLeast(5f).sp
+ }) {
+ Icon(Icons.Default.KeyboardArrowDown, "Decrease line height")
+ }
+ }
+ }
+
+ val fontSize = 16.sp
+ val text = "This is a very-very " +
+ "long text that has a limited height and width to test how it's ellipsized." +
+ " This is a second sentence of the text."
+ val textWithSpans = buildAnnotatedString {
+ withStyle(SpanStyle(fontSize = fontSize / 2)) {
+ append("This is a very-very long text that has ")
+ }
+ withStyle(SpanStyle(fontSize = fontSize * 2)) {
+ append("a limited height")
+ }
+ append(" and width to test how it's ellipsized. This is a second sentence of the text.")
+ }
+ Text(
+ text = if (withSpans) textWithSpans else AnnotatedString(text),
+ fontSize = fontSize,
+ lineHeight = lineHeight.value,
+ modifier = Modifier
+ .background(Color.Magenta)
+ .width(200.dp)
+ .heightIn(max = heightRestriction.value),
+ softWrap = softWrap,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip
+ )
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 82ad134..f85c14f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -23,6 +23,8 @@
"Text",
listOf(
ComposableDemo("Static text") { TextDemo() },
+ ComposableDemo("Brush") { TextBrushDemo() },
+ ComposableDemo("Ellipsize") { EllipsizeDemo() },
ComposableDemo("Typeface") { TypefaceDemo() },
ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
ComposableDemo("All system font families") { SystemFontFamilyDemo() },
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index dd8c203..f42e6a8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -352,7 +352,7 @@
private fun Modifier.pinnableParent(onPin: () -> PinnedItemsHandle): Modifier {
return modifierLocalProvider(ModifierLocalPinnableParent) {
object : PinnableParent {
- override fun pinBeyondBoundsItems(): PinnedItemsHandle {
+ override fun pinItems(): PinnedItemsHandle {
return onPin()
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index 11375ac..f37a825 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -68,12 +68,12 @@
override fun placeChildren() {}
}
}
- val itemsProvider = itemProvider({ 0 }) { { } }
+ val itemProvider = itemProvider({ 0 }) { }
rule.setContent {
counter.value // just to trigger recomposition
LazyLayout(
- itemsProvider = itemsProvider,
+ itemProvider = itemProvider,
measurePolicy = policy,
// this will return a new object everytime causing LazyLayout recomposition
// without causing remeasure
@@ -93,11 +93,11 @@
@Test
fun measureAndPlaceTwoItems() {
- val itemsProvider = itemProvider({ 2 }) { index ->
- { Box(Modifier.fillMaxSize().testTag("$index")) }
+ val itemProvider = itemProvider({ 2 }) { index ->
+ Box(Modifier.fillMaxSize().testTag("$index"))
}
rule.setContent {
- LazyLayout(itemsProvider) {
+ LazyLayout(itemProvider) {
val item1 = measure(0, Constraints.fixed(50, 50))[0]
val item2 = measure(1, Constraints.fixed(20, 20))[0]
layout(100, 100) {
@@ -117,15 +117,13 @@
@Test
fun measureAndPlaceMultipleLayoutsInOneItem() {
- val itemsProvider = itemProvider({ 1 }) { index ->
- {
- Box(Modifier.fillMaxSize().testTag("${index}x0"))
- Box(Modifier.fillMaxSize().testTag("${index}x1"))
- }
+ val itemProvider = itemProvider({ 1 }) { index ->
+ Box(Modifier.fillMaxSize().testTag("${index}x0"))
+ Box(Modifier.fillMaxSize().testTag("${index}x1"))
}
rule.setContent {
- LazyLayout(itemsProvider) {
+ LazyLayout(itemProvider) {
val items = measure(0, Constraints.fixed(50, 50))
layout(100, 100) {
items[0].place(0, 0)
@@ -143,16 +141,16 @@
}
@Test
- fun updatingItemsProvider() {
- var itemsProvider by mutableStateOf(itemProvider({ 1 }) { index ->
- { Box(Modifier.fillMaxSize().testTag("$index")) }
+ fun updatingitemProvider() {
+ var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
+ Box(Modifier.fillMaxSize().testTag("$index"))
})
rule.setContent {
- LazyLayout(itemsProvider) {
+ LazyLayout(itemProvider) {
val constraints = Constraints.fixed(100, 100)
val items = mutableListOf<Placeable>()
- repeat(itemsProvider.itemsCount) { index ->
+ repeat(itemProvider.itemCount) { index ->
items.addAll(measure(index, constraints))
}
layout(100, 100) {
@@ -167,8 +165,8 @@
rule.onNodeWithTag("1").assertDoesNotExist()
rule.runOnIdle {
- itemsProvider = itemProvider({ 2 }) { index ->
- { Box(Modifier.fillMaxSize().testTag("$index")) }
+ itemProvider = itemProvider({ 2 }) { index ->
+ Box(Modifier.fillMaxSize().testTag("$index"))
}
}
@@ -177,17 +175,17 @@
}
@Test
- fun stateBasedItemsProvider() {
- var itemsCount by mutableStateOf(1)
- val itemsProvider = itemProvider({ itemsCount }) { index ->
- { Box(Modifier.fillMaxSize().testTag("$index")) }
+ fun stateBaseditemProvider() {
+ var itemCount by mutableStateOf(1)
+ val itemProvider = itemProvider({ itemCount }) { index ->
+ Box(Modifier.fillMaxSize().testTag("$index"))
}
rule.setContent {
- LazyLayout(itemsProvider) {
+ LazyLayout(itemProvider) {
val constraints = Constraints.fixed(100, 100)
val items = mutableListOf<Placeable>()
- repeat(itemsProvider.itemsCount) { index ->
+ repeat(itemProvider.itemCount) { index ->
items.addAll(measure(index, constraints))
}
layout(100, 100) {
@@ -202,7 +200,7 @@
rule.onNodeWithTag("1").assertDoesNotExist()
rule.runOnIdle {
- itemsCount = 2
+ itemCount = 2
}
rule.onNodeWithTag("0").assertIsDisplayed()
@@ -229,13 +227,13 @@
placeable.place(0, 0)
}
}
- val itemsProvider = itemProvider({ 1 }) { index ->
- { Box(Modifier.fillMaxSize().testTag("$index").then(modifier)) }
+ val itemProvider = itemProvider({ 1 }) { index ->
+ Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
}
var needToCompose by mutableStateOf(false)
val prefetchState = LazyLayoutPrefetchState()
rule.setContent {
- LazyLayout(itemsProvider, prefetchState = prefetchState) {
+ LazyLayout(itemProvider, prefetchState = prefetchState) {
val item = if (needToCompose) {
measure(0, constraints)[0]
} else null
@@ -270,20 +268,18 @@
@Test
fun cancelPrefetchedItem() {
var composed = false
- val itemsProvider = itemProvider({ 1 }) {
- {
- Box(Modifier.fillMaxSize())
- DisposableEffect(Unit) {
- composed = true
- onDispose {
- composed = false
- }
+ val itemProvider = itemProvider({ 1 }) {
+ Box(Modifier.fillMaxSize())
+ DisposableEffect(Unit) {
+ composed = true
+ onDispose {
+ composed = false
}
}
}
val prefetchState = LazyLayoutPrefetchState()
rule.setContent {
- LazyLayout(itemsProvider, prefetchState = prefetchState) {
+ LazyLayout(itemProvider, prefetchState = prefetchState) {
layout(100, 100) {}
}
}
@@ -307,19 +303,17 @@
fun keptForReuseItemIsDisposedWhenCanceled() {
val needChild = mutableStateOf(true)
var composed = true
- val itemsProvider = itemProvider({ 1 }) {
- {
- DisposableEffect(Unit) {
- composed = true
- onDispose {
- composed = false
- }
+ val itemProvider = itemProvider({ 1 }) {
+ DisposableEffect(Unit) {
+ composed = true
+ onDispose {
+ composed = false
}
}
}
rule.setContent {
- LazyLayout(itemsProvider) { constraints ->
+ LazyLayout(itemProvider) { constraints ->
if (needChild.value) {
measure(0, constraints)
}
@@ -348,12 +342,12 @@
placeable.place(0, 0)
}
}.fillMaxSize()
- val itemsProvider = itemProvider({ 2 }) {
- { Box(modifier) }
+ val itemProvider = itemProvider({ 2 }) {
+ Box(modifier)
}
rule.setContent {
- LazyLayout(itemsProvider) { constraints ->
+ LazyLayout(itemProvider) { constraints ->
val node = if (indexToCompose != null) {
measure(indexToCompose!!, constraints).first()
} else {
@@ -382,16 +376,17 @@
}
private fun itemProvider(
- itemsCount: () -> Int,
- content: (Int) -> @Composable () -> Unit
- ): LazyLayoutItemsProvider {
- return object : LazyLayoutItemsProvider {
- override fun getContent(index: Int): @Composable () -> Unit {
- return content(index)
+ itemCount: () -> Int,
+ itemContent: @Composable (Int) -> Unit
+ ): LazyLayoutItemProvider {
+ return object : LazyLayoutItemProvider {
+ @Composable
+ override fun Item(index: Int) {
+ itemContent(index)
}
- override val itemsCount: Int
- get() = itemsCount()
+ override val itemCount: Int
+ get() = itemCount()
override fun getKey(index: Int) = index
override val keyToIndexMap: Map<Any, Int> = emptyMap()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/PinnableParentTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/PinnableParentTest.kt
index 3b56f05..8fd7f13 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/PinnableParentTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/PinnableParentTest.kt
@@ -71,7 +71,7 @@
}
private class TestPinnableParent : PinnableParent {
- override fun pinBeyondBoundsItems(): PinnedItemsHandle {
+ override fun pinItems(): PinnedItemsHandle {
TODO("Not yet implemented")
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 9cb0e46..b9c01ed 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -19,15 +19,19 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -36,26 +40,22 @@
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TextInputService
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.eq
-import com.nhaarman.mockitokotlin2.inOrder
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-@LargeTest
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
@RunWith(AndroidJUnit4::class)
class CoreTextFieldInputServiceIntegrationTest {
@get:Rule
val rule = createComposeRule()
- private val platformTextInputService = mock<PlatformTextInputService>()
+ private lateinit var focusManager: FocusManager
+ private val platformTextInputService = FakePlatformTextInputService()
private val textInputService = TextInputService(platformTextInputService)
@Test
@@ -88,12 +88,9 @@
rule.runOnIdle {
assertThat(focused).isTrue()
- verify(platformTextInputService, times(1)).startInput(
- eq(value),
- eq(imeOptions),
- any(), // onEditCommand
- any() // onImeActionPerformed
- )
+ assertThat(platformTextInputService.inputStarted).isTrue()
+ assertThat(platformTextInputService.lastInputValue).isEqualTo(value)
+ assertThat(platformTextInputService.lastInputImeOptions).isEqualTo(imeOptions)
}
}
@@ -127,22 +124,168 @@
}
rule.runOnIdle {
- inOrder(platformTextInputService) {
- verify(platformTextInputService).startInput(any(), any(), any(), any())
- // On Android, this stopInput should no-op because of the immediately-following call
- // to startInput. See b/187746439.
- verify(platformTextInputService).stopInput()
- verify(platformTextInputService).startInput(any(), any(), any(), any())
+ assertThat(platformTextInputService.startInputCalls).isEqualTo(2)
+ assertThat(platformTextInputService.stopInputCalls).isEqualTo(1)
+ assertThat(platformTextInputService.inputStarted).isTrue()
+ }
+ }
+
+ @Test
+ fun keyboardShownOnInitialClick() {
+ // Arrange.
+ setContent {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.testTag("TextField1")
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag("TextField1").performClick()
+
+ // Assert.
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+ }
+
+ @Test
+ fun keyboardShownOnInitialFocus() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ setContent {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+ }
+
+ @Test
+ fun keyboardHiddenWhenFocusIsLost() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ setContent {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ // Request focus and wait for keyboard.
+ rule.runOnIdle { focusRequester.requestFocus() }
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isFalse() }
+ }
+
+ @Test
+ fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
+ // Arrange.
+ setContent {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.testTag("TextField1")
+ )
+ }
+ rule.onNodeWithTag("TextField1").performClick()
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+
+ // Act.
+ rule.runOnIdle { platformTextInputService.keyboardShown = false }
+ rule.onNodeWithTag("TextField1").performClick()
+
+ // Assert.
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+ }
+
+ @Test
+ fun keyboardStaysVisibleWhenMovingFromOneTextFieldToAnother() {
+ // Arrange.
+ val (focusRequester1, focusRequester2) = FocusRequester.createRefs()
+ setContent {
+ Column {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester1)
+ )
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester2)
+ )
}
}
+ rule.runOnIdle { focusRequester1.requestFocus() }
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
+
+ // Act.
+ rule.runOnIdle { focusRequester2.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(platformTextInputService.keyboardShown).isTrue() }
}
private fun setContent(content: @Composable () -> Unit) {
rule.setContent {
+ focusManager = LocalFocusManager.current
CompositionLocalProvider(
LocalTextInputService provides textInputService,
content = content
)
}
}
+
+ private class FakePlatformTextInputService : PlatformTextInputService {
+ var startInputCalls = 0
+ var stopInputCalls = 0
+ var inputStarted = false
+ var keyboardShown = false
+
+ var lastInputValue: TextFieldValue? = null
+ var lastInputImeOptions: ImeOptions? = null
+
+ override fun startInput(
+ value: TextFieldValue,
+ imeOptions: ImeOptions,
+ onEditCommand: (List<EditCommand>) -> Unit,
+ onImeActionPerformed: (ImeAction) -> Unit
+ ) {
+ startInputCalls++
+ inputStarted = true
+ keyboardShown = true
+ lastInputValue = value
+ lastInputImeOptions = imeOptions
+ }
+
+ override fun stopInput() {
+ stopInputCalls++
+ inputStarted = false
+ keyboardShown = false
+ }
+
+ override fun showSoftwareKeyboard() {
+ keyboardShown = true
+ }
+
+ override fun hideSoftwareKeyboard() {
+ keyboardShown = false
+ }
+
+ override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
+ // Tests don't care.
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
deleted file mode 100644
index 0b02623..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2020 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
- *
- * http://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 androidx.compose.foundation.text
-
-import android.os.Build
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class CoreTextFieldSoftKeyboardTest {
- @get:Rule
- val rule = createComposeRule()
-
- private lateinit var focusManager: FocusManager
- private val timeout = 15_000L
- private val keyboardHelper = KeyboardHelper(rule, timeout)
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
- @Test
- fun keyboardShownOnInitialClick() {
- // Arrange.
- rule.setContentForTest {
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.testTag("TextField1")
- )
- }
-
- // Act.
- rule.onNodeWithTag("TextField1").performClick()
-
- // Assert.
- keyboardHelper.waitForKeyboardVisibility(visible = true)
- }
-
- @FlakyTest(bugId = 228258574)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
- @Test
- fun keyboardShownOnInitialFocus() {
- // Arrange.
- val focusRequester = FocusRequester()
- rule.setContentForTest {
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.focusRequester(focusRequester)
- )
- }
-
- // Act.
- rule.runOnIdle { focusRequester.requestFocus() }
-
- // Assert.
- keyboardHelper.waitForKeyboardVisibility(visible = true)
- }
-
- @FlakyTest(bugId = 229247491)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
- @Test
- fun keyboardHiddenWhenFocusIsLost() {
- // Arrange.
- val focusRequester = FocusRequester()
- rule.setContentForTest {
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.focusRequester(focusRequester)
- )
- }
- // Request focus and wait for keyboard.
- rule.runOnIdle { focusRequester.requestFocus() }
- keyboardHelper.waitForKeyboardVisibility(visible = true)
-
- // Act.
- rule.runOnIdle { focusManager.clearFocus() }
-
- // Assert.
- keyboardHelper.waitForKeyboardVisibility(visible = false)
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
- @Test
- fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
- // Arrange.
- rule.setContentForTest {
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.testTag("TextField1")
- )
- }
- rule.onNodeWithTag("TextField1").performClick()
- keyboardHelper.waitForKeyboardVisibility(visible = true)
-
- // Act.
- rule.runOnIdle { keyboardHelper.hideKeyboard() }
- keyboardHelper.waitForKeyboardVisibility(visible = false)
- rule.onNodeWithTag("TextField1").performClick()
-
- // Assert.
- keyboardHelper.waitForKeyboardVisibility(visible = true)
- }
-
- @OptIn(ExperimentalComposeUiApi::class)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
- @Test
- fun keyboardStaysVisibleWhenMovingFromOneTextFieldToAnother() {
- // Arrange.
- val (focusRequester1, focusRequester2) = FocusRequester.createRefs()
- rule.setContentForTest {
- Column {
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.focusRequester(focusRequester1)
- )
- CoreTextField(
- value = TextFieldValue("Hello"),
- onValueChange = {},
- modifier = Modifier.focusRequester(focusRequester2)
- )
- }
- }
- rule.runOnIdle { focusRequester1.requestFocus() }
- keyboardHelper.waitForKeyboardVisibility(visible = true)
-
- // Act.
- rule.runOnIdle { focusRequester2.requestFocus() }
-
- // Assert.
- keyboardHelper.waitForKeyboardVisibility(visible = false)
- }
-
- private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
- setContent {
- keyboardHelper.view = LocalView.current
- focusManager = LocalFocusManager.current
- composable()
- }
- // We experienced some flakiness in tests if the keyboard was visible at the start of the
- // test. So we make sure that the keyboard is hidden at the start of every test.
- keyboardHelper.hideKeyboardIfShown()
- }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
index c5293a8..1e699be 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
@@ -34,6 +34,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlin.math.roundToInt
@OptIn(InternalFoundationTextApi::class)
@RunWith(AndroidJUnit4::class)
@@ -159,6 +160,29 @@
assertThat(layoutResult.lineCount).isEqualTo(1)
assertThat(layoutResult.isLineEllipsized(0)).isTrue()
}
+
+ @Test
+ fun TextLayoutResult_layoutWithLimitedHeight_withEllipsis() {
+ val fontSize = 20f
+ val text = AnnotatedString(text = "Hello World! Hello World! Hello World! Hello World!")
+ val textDelegate = TextDelegate(
+ text = text,
+ style = TextStyle(fontSize = fontSize.sp),
+ overflow = TextOverflow.Ellipsis,
+ density = density,
+ fontFamilyResolver = fontFamilyResolver
+ )
+ textDelegate.layoutIntrinsics(LayoutDirection.Ltr)
+
+ val constraints = Constraints(
+ maxWidth = textDelegate.maxIntrinsicWidth / 4,
+ maxHeight = (fontSize * 2.7).roundToInt() // fully fits at most 2 lines
+ )
+ val layoutResult = textDelegate.layout(constraints, LayoutDirection.Ltr)
+
+ assertThat(layoutResult.lineCount).isEqualTo(2)
+ assertThat(layoutResult.isLineEllipsized(1)).isTrue()
+ }
}
private fun TextLayoutResult.toBitmap() = Bitmap.createBitmap(
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
index 8a959c8..be9c1fa 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
@@ -157,8 +157,8 @@
var scheduleForNextFrame = false
while (prefetchRequests.isNotEmpty() && !scheduleForNextFrame) {
val request = prefetchRequests[0]
- val itemsProvider = itemContentFactory.itemsProvider()
- if (request.canceled || request.index !in 0 until itemsProvider.itemsCount) {
+ val itemProvider = itemContentFactory.itemProvider()
+ if (request.canceled || request.index !in 0 until itemProvider.itemCount) {
prefetchRequests.removeAt(0)
} else if (request.precomposeHandle == null) {
trace("compose:lazylist:prefetch:compose") {
@@ -166,7 +166,7 @@
// check if there is enough time left in this frame. otherwise, we schedule
// a next frame callback in which we will post the message in the handler again.
if (enoughTimeLeft(beforeTimeNs, nextFrameNs, averagePrecomposeTimeNs)) {
- val key = itemsProvider.getKey(request.index)
+ val key = itemProvider.getKey(request.index)
val content = itemContentFactory.getContent(request.index, key)
request.precomposeHandle = subcomposeLayoutState.precompose(key, content)
averagePrecomposeTimeNs = calculateAverageTime(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 687d317..38f932d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -145,7 +145,7 @@
// synchronously and suspend after the items are pinned.
scope.launch(start = CoroutineStart.UNDISPATCHED) {
try {
- pinnedItemsHandle = pinnableParent?.pinBeyondBoundsItems()
+ pinnedItemsHandle = pinnableParent?.pinItems()
bringIntoViewRequester.bringIntoView()
} finally {
pinnedItemsHandle?.unpin()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
index 321cb06..61ef514 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
@@ -21,22 +21,22 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
-internal data class LazyItemScopeImpl(
- val density: Density,
- val constraints: Constraints
-) : LazyItemScope {
- private val maxWidth: Dp = with(density) { constraints.maxWidth.toDp() }
- private val maxHeight: Dp = with(density) { constraints.maxHeight.toDp() }
+internal class LazyItemScopeImpl : LazyItemScope {
+
+ var maxWidth: Dp by mutableStateOf(Dp.Unspecified)
+ var maxHeight: Dp by mutableStateOf(Dp.Unspecified)
override fun Modifier.fillParentMaxSize(fraction: Float) = size(
maxWidth * fraction,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index aa24c83..071d1b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -37,10 +37,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
@@ -77,9 +75,7 @@
) {
val overScrollController = rememberOverScrollController()
- val itemScope: Ref<LazyItemScopeImpl> = remember { Ref() }
-
- val itemsProvider = rememberItemsProvider(state, content, itemScope)
+ val itemProvider = rememberItemProvider(state, content)
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
@@ -88,8 +84,7 @@
state.placementAnimator = placementAnimator
val measurePolicy = rememberLazyListMeasurePolicy(
- itemsProvider,
- itemScope,
+ itemProvider,
state,
overScrollController,
contentPadding,
@@ -102,13 +97,13 @@
placementAnimator
)
- ScrollPositionUpdater(itemsProvider, state)
+ ScrollPositionUpdater(itemProvider, state)
LazyLayout(
modifier = modifier
.then(state.remeasurementModifier)
.lazyListSemantics(
- itemsProvider = itemsProvider,
+ itemProvider = itemProvider,
state = state,
coroutineScope = scope,
isVertical = isVertical,
@@ -137,7 +132,7 @@
),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
- itemsProvider = itemsProvider
+ itemProvider = itemProvider
)
}
@@ -145,11 +140,11 @@
@ExperimentalFoundationApi
@Composable
private fun ScrollPositionUpdater(
- itemsProvider: LazyListItemsProvider,
+ itemProvider: LazyListItemProvider,
state: LazyListState
) {
- if (itemsProvider.itemsCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ if (itemProvider.itemCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
}
@@ -157,9 +152,7 @@
@Composable
private fun rememberLazyListMeasurePolicy(
/** Items provider of the list. */
- itemsProvider: LazyListItemsProvider,
- /** Value holder for the item scope used to compose items. */
- itemScope: Ref<LazyItemScopeImpl>,
+ itemProvider: LazyListItemProvider,
/** The state of the list. */
state: LazyListState,
/** The overscroll controller. */
@@ -213,13 +206,14 @@
val contentConstraints =
containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
// Update the state's cached Density
state.density = this
- // this will update the scope object if the constrains have been changed
- itemScope.update(this, contentConstraints)
+ // this will update the scope used by the item composables
+ itemProvider.itemScope.maxWidth = contentConstraints.maxWidth.toDp()
+ itemProvider.itemScope.maxHeight = contentConstraints.maxHeight.toDp()
val spaceBetweenItemsDp = if (isVertical) {
requireNotNull(verticalArrangement).spacing
@@ -228,12 +222,12 @@
}
val spaceBetweenItems = spaceBetweenItemsDp.roundToPx()
- val itemsCount = itemsProvider.itemsCount
+ val itemsCount = itemProvider.itemCount
- val itemProvider = LazyMeasuredItemProvider(
+ val measuredItemProvider = LazyMeasuredItemProvider(
contentConstraints,
isVertical,
- itemsProvider,
+ itemProvider,
this
) { index, key, placeables ->
// we add spaceBetweenItems as an extra spacing for all items apart from the last one so
@@ -255,7 +249,7 @@
placementAnimator = placementAnimator
)
}
- state.premeasureConstraints = itemProvider.childConstraints
+ state.premeasureConstraints = measuredItemProvider.childConstraints
// can be negative if the content padding is larger than the max size from constraints
val mainAxisAvailableSize = if (isVertical) {
@@ -266,7 +260,7 @@
measureLazyList(
itemsCount = itemsCount,
- itemProvider = itemProvider,
+ itemProvider = measuredItemProvider,
mainAxisAvailableSize = mainAxisAvailableSize,
beforeContentPadding = beforeContentPadding,
afterContentPadding = afterContentPadding,
@@ -275,7 +269,7 @@
scrollToBeConsumed = state.scrollToBeConsumed,
constraints = contentConstraints,
isVertical = isVertical,
- headerIndexes = itemsProvider.headerIndexes,
+ headerIndexes = itemProvider.headerIndexes,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
reverseLayout = reverseLayout,
@@ -303,13 +297,6 @@
}
}
-private fun Ref<LazyItemScopeImpl>.update(density: Density, constraints: Constraints) {
- val value = value
- if (value == null || value.density != density || value.constraints != constraints) {
- this.value = LazyItemScopeImpl(density, constraints)
- }
-}
-
private fun refreshOverScrollInfo(
overScrollController: OverScrollController,
result: LazyListMeasureResult,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
similarity index 78%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 2bf7e7a..75d380a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -17,10 +17,12 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemsProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
@ExperimentalFoundationApi
-internal interface LazyListItemsProvider : LazyLayoutItemsProvider {
+internal interface LazyListItemProvider : LazyLayoutItemProvider {
/** The list of indexes of the sticky header items */
val headerIndexes: List<Int>
+ /** The scope used by the item content lambdas */
+ val itemScope: LazyItemScopeImpl
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
similarity index 91%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProviderImpl.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
index 0bb8815..6077018 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProviderImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
@@ -30,15 +30,13 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.node.Ref
@ExperimentalFoundationApi
@Composable
-internal fun rememberItemsProvider(
+internal fun rememberItemProvider(
state: LazyListState,
- content: LazyListScope.() -> Unit,
- itemScope: Ref<LazyItemScopeImpl>
-): LazyListItemsProvider {
+ content: LazyListScope.() -> Unit
+): LazyListItemProvider {
val latestContent = rememberUpdatedState(content)
val nearestItemsRangeState = remember(state) {
mutableStateOf(
@@ -52,11 +50,10 @@
.collect { nearestItemsRangeState.value = it }
}
return remember(nearestItemsRangeState) {
- LazyListItemsProviderImpl(
+ LazyListItemProviderImpl(
derivedStateOf {
val listScope = LazyListScopeImpl().apply(latestContent.value)
LazyListItemsSnapshot(
- itemScope,
listScope.intervals,
listScope.headerIndexes,
nearestItemsRangeState.value
@@ -68,7 +65,6 @@
@ExperimentalFoundationApi
internal class LazyListItemsSnapshot(
- private val itemScope: Ref<LazyItemScopeImpl>,
private val list: IntervalList<LazyListIntervalContent>,
val headerIndexes: List<Int>,
nearestItemsRange: IntRange
@@ -96,10 +92,11 @@
return key ?: getDefaultLazyLayoutKey(index)
}
- fun getContent(index: Int): @Composable () -> Unit {
+ @Composable
+ fun Item(scope: LazyItemScope, index: Int) {
val interval = getIntervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
- return interval.content.content.invoke(itemScope.value!!, localIntervalIndex)
+ interval.content.item.invoke(scope, localIntervalIndex)
}
val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, list)
@@ -112,17 +109,22 @@
}
@ExperimentalFoundationApi
-internal class LazyListItemsProviderImpl(
+internal class LazyListItemProviderImpl(
private val itemsSnapshot: State<LazyListItemsSnapshot>
-) : LazyListItemsProvider {
+) : LazyListItemProvider {
+
+ override val itemScope = LazyItemScopeImpl()
override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
- override val itemsCount get() = itemsSnapshot.value.itemsCount
+ override val itemCount get() = itemsSnapshot.value.itemsCount
override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
- override fun getContent(index: Int) = itemsSnapshot.value.getContent(index)
+ @Composable
+ override fun Item(index: Int) {
+ itemsSnapshot.value.Item(itemScope, index)
+ }
override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
index d5c9a51..1999746 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
@@ -40,7 +40,7 @@
LazyListIntervalContent(
key = key,
type = contentType,
- content = { index -> @Composable { itemContent(index) } }
+ item = itemContent
)
)
}
@@ -51,7 +51,7 @@
LazyListIntervalContent(
key = if (key != null) { _: Int -> key } else null,
type = { contentType },
- content = { @Composable { content() } }
+ item = { content() }
)
)
}
@@ -74,5 +74,5 @@
internal class LazyListIntervalContent(
val key: ((index: Int) -> Any)?,
val type: ((index: Int) -> Any?),
- val content: LazyItemScope.(index: Int) -> @Composable () -> Unit
+ val item: @Composable LazyItemScope.(index: Int) -> Unit
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
index bb5b8df..7c84ae0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
@@ -94,8 +94,8 @@
* as the first visible one even given that its index has been changed.
*/
@ExperimentalFoundationApi
- fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyListItemsProvider) {
- update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemsProvider), scrollOffset)
+ fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+ update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
}
private fun update(index: DataIndex, scrollOffset: Int) {
@@ -119,19 +119,19 @@
private fun findLazyListIndexByKey(
key: Any?,
lastKnownIndex: DataIndex,
- itemsProvider: LazyListItemsProvider
+ itemProvider: LazyListItemProvider
): DataIndex {
if (key == null) {
// there were no real item during the previous measure
return lastKnownIndex
}
- if (lastKnownIndex.value < itemsProvider.itemsCount &&
- key == itemsProvider.getKey(lastKnownIndex.value)
+ if (lastKnownIndex.value < itemProvider.itemCount &&
+ key == itemProvider.getKey(lastKnownIndex.value)
) {
// this item is still at the same index
return lastKnownIndex
}
- val newIndex = itemsProvider.keyToIndexMap[key]
+ val newIndex = itemProvider.keyToIndexMap[key]
if (newIndex != null) {
return DataIndex(newIndex)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 4d25eca..d2712e8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -377,8 +377,8 @@
* items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- internal fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyListItemsProvider) {
- scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+ scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
companion object {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
index cbc9c79..facb234 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
@@ -28,7 +28,7 @@
internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
constraints: Constraints,
isVertical: Boolean,
- private val itemsProvider: LazyListItemsProvider,
+ private val itemProvider: LazyListItemProvider,
private val measureScope: LazyLayoutMeasureScope,
private val measuredItemFactory: MeasuredItemFactory
) {
@@ -43,7 +43,7 @@
* correct constraints and wrapped into [LazyMeasuredItem].
*/
fun getAndMeasure(index: DataIndex): LazyMeasuredItem {
- val key = itemsProvider.getKey(index.value)
+ val key = itemProvider.getKey(index.value)
val placeables = measureScope.measure(index.value, childConstraints)
return measuredItemFactory.createItem(index, key, placeables)
}
@@ -52,7 +52,7 @@
* Contains the mapping between the key and the index. It could contain not all the items of
* the list as an optimization.
**/
- val keyToIndexMap: Map<Any, Int> get() = itemsProvider.keyToIndexMap
+ val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
}
// This interface allows to avoid autoboxing on index param
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
index 62bf48d..d785a83 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
@@ -38,7 +38,7 @@
@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
@Composable
internal fun Modifier.lazyListSemantics(
- itemsProvider: LazyListItemsProvider,
+ itemProvider: LazyListItemProvider,
state: LazyListState,
coroutineScope: CoroutineScope,
isVertical: Boolean,
@@ -46,16 +46,16 @@
userScrollEnabled: Boolean
) = this.then(
remember(
- itemsProvider,
+ itemProvider,
state,
isVertical,
reverseScrolling,
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemsProvider::getKey
+ val key = itemProvider::getKey
var result = -1
- for (index in 0 until itemsProvider.itemsCount) {
+ for (index in 0 until itemProvider.itemCount) {
if (key(index) == needle) {
result = index
break
@@ -76,7 +76,7 @@
if (state.canScrollForward) {
// If we can scroll further, we don't know the end yet,
// but it's upper bounded by #items + 1
- itemsProvider.itemsCount + 1f
+ itemProvider.itemCount + 1f
} else {
// If we can't scroll further, the current value is the max
state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 9a88f0c..8b27ce8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -78,12 +78,10 @@
) {
val overScrollController = rememberOverScrollController()
- val itemScope = remember { LazyGridItemScopeImpl() }
+ val itemProvider = rememberItemProvider(state, content)
- val itemsProvider = rememberItemsProvider(state, content, itemScope)
-
- val spanLayoutProvider = remember(itemsProvider) {
- derivedStateOf { LazyGridSpanLayoutProvider(itemsProvider) }
+ val spanLayoutProvider = remember(itemProvider) {
+ derivedStateOf { LazyGridSpanLayoutProvider(itemProvider) }
}
val scope = rememberCoroutineScope()
@@ -93,7 +91,7 @@
state.placementAnimator = placementAnimator
val measurePolicy = rememberLazyGridMeasurePolicy(
- itemsProvider,
+ itemProvider,
state,
overScrollController,
spanLayoutProvider,
@@ -108,13 +106,13 @@
state.isVertical = isVertical
- ScrollPositionUpdater(itemsProvider, state)
+ ScrollPositionUpdater(itemProvider, state)
LazyLayout(
modifier = modifier
.then(state.remeasurementModifier)
.lazyGridSemantics(
- itemsProvider = itemsProvider,
+ itemProvider = itemProvider,
state = state,
coroutineScope = scope,
isVertical = isVertical,
@@ -143,7 +141,7 @@
),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
- itemsProvider = itemsProvider
+ itemProvider = itemProvider
)
}
@@ -151,11 +149,11 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ScrollPositionUpdater(
- itemsProvider: LazyGridItemsProvider,
+ itemProvider: LazyGridItemProvider,
state: LazyGridState
) {
- if (itemsProvider.itemsCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ if (itemProvider.itemCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
}
@@ -163,7 +161,7 @@
@Composable
private fun rememberLazyGridMeasurePolicy(
/** Items provider of the list. */
- itemsProvider: LazyGridItemsProvider,
+ itemProvider: LazyGridItemProvider,
/** The state of the list. */
state: LazyGridState,
/** The overscroll controller. */
@@ -215,7 +213,7 @@
val afterContentPadding = totalMainAxisPadding - beforeContentPadding
val contentConstraints = constraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
val spanLayoutProvider = stateOfSpanLayoutProvider.value
val resolvedSlotSizesSums = slotSizesSums(constraints)
@@ -238,10 +236,10 @@
}
val spaceBetweenSlots = spaceBetweenSlotsDp.roundToPx()
- val itemsCount = itemsProvider.itemsCount
+ val itemsCount = itemProvider.itemCount
val measuredItemProvider = LazyMeasuredItemProvider(
- itemsProvider,
+ itemProvider,
this,
spaceBetweenLines
) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
similarity index 87%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProvider.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index d5ba6bf..d2f12c5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,10 +17,10 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemsProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
@ExperimentalFoundationApi
-internal interface LazyGridItemsProvider : LazyLayoutItemsProvider {
+internal interface LazyGridItemProvider : LazyLayoutItemProvider {
/** Returns the span for the given [index] */
fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
similarity index 92%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
index f408b08..cbd7cd0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
@@ -33,11 +33,10 @@
@ExperimentalFoundationApi
@Composable
-internal fun rememberItemsProvider(
+internal fun rememberItemProvider(
state: LazyGridState,
content: LazyGridScope.() -> Unit,
- itemScope: LazyGridItemScope
-): LazyGridItemsProvider {
+): LazyGridItemProvider {
val latestContent = rememberUpdatedState(content)
val nearestItemsRangeState = remember(state) {
mutableStateOf(
@@ -51,11 +50,10 @@
.collect { nearestItemsRangeState.value = it }
}
return remember(nearestItemsRangeState) {
- LazyGridItemsProviderImpl(
+ LazyGridItemProviderImpl(
derivedStateOf {
val listScope = LazyGridScopeImpl().apply(latestContent.value)
LazyGridItemsSnapshot(
- itemScope,
listScope.intervals,
listScope.hasCustomSpans,
nearestItemsRangeState.value
@@ -67,7 +65,6 @@
@ExperimentalFoundationApi
internal class LazyGridItemsSnapshot(
- private val itemScope: LazyGridItemScope,
private val intervals: IntervalList<LazyGridIntervalContent>,
val hasCustomSpans: Boolean,
nearestItemsRange: IntRange
@@ -93,10 +90,11 @@
return interval.content.span.invoke(this, localIntervalIndex)
}
- fun getContent(index: Int): @Composable () -> Unit {
+ @Composable
+ fun Item(index: Int) {
val interval = getIntervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
- return interval.content.content.invoke(itemScope, localIntervalIndex)
+ interval.content.item.invoke(LazyGridItemScopeImpl, localIntervalIndex)
}
val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
@@ -117,11 +115,11 @@
}
@ExperimentalFoundationApi
-internal class LazyGridItemsProviderImpl(
+internal class LazyGridItemProviderImpl(
private val itemsSnapshot: State<LazyGridItemsSnapshot>
-) : LazyGridItemsProvider {
+) : LazyGridItemProvider {
- override val itemsCount get() = itemsSnapshot.value.itemsCount
+ override val itemCount get() = itemsSnapshot.value.itemsCount
override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
@@ -130,7 +128,10 @@
override val hasCustomSpans: Boolean get() = itemsSnapshot.value.hasCustomSpans
- override fun getContent(index: Int) = itemsSnapshot.value.getContent(index)
+ @Composable
+ override fun Item(index: Int) {
+ itemsSnapshot.value.Item(index)
+ }
override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
index d42ef7d..6851f68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.unit.IntOffset
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridItemScopeImpl : LazyGridItemScope {
+internal object LazyGridItemScopeImpl : LazyGridItemScope {
@ExperimentalFoundationApi
override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
index 4960fa8..c006c69 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
@@ -39,7 +39,7 @@
key = key?.let { { key } },
span = span?.let { { span() } } ?: DefaultSpan,
type = { contentType },
- content = { { content() } }
+ item = { content() }
)
)
if (span != null) hasCustomSpans = true
@@ -58,7 +58,7 @@
key = key,
span = span ?: DefaultSpan,
type = contentType,
- content = { { itemContent(it) } }
+ item = itemContent
)
)
if (span != null) hasCustomSpans = true
@@ -70,5 +70,5 @@
val key: ((index: Int) -> Any)?,
val span: LazyGridItemSpanScope.(Int) -> GridItemSpan,
val type: ((index: Int) -> Any?),
- val content: LazyGridItemScope.(Int) -> (@Composable () -> Unit)
+ val item: @Composable LazyGridItemScope.(Int) -> Unit
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
index 83a321e..6f886b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -94,8 +94,8 @@
* there were items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyGridItemsProvider) {
- update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemsProvider), scrollOffset)
+ fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+ update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
}
private fun update(index: ItemIndex, scrollOffset: Int) {
@@ -118,19 +118,19 @@
private fun findLazyGridIndexByKey(
key: Any?,
lastKnownIndex: ItemIndex,
- itemsProvider: LazyGridItemsProvider
+ itemProvider: LazyGridItemProvider
): ItemIndex {
if (key == null) {
// there were no real item during the previous measure
return lastKnownIndex
}
- if (lastKnownIndex.value < itemsProvider.itemsCount &&
- key == itemsProvider.getKey(lastKnownIndex.value)
+ if (lastKnownIndex.value < itemProvider.itemCount &&
+ key == itemProvider.getKey(lastKnownIndex.value)
) {
// this item is still at the same index
return lastKnownIndex
}
- val newIndex = itemsProvider.keyToIndexMap[key]
+ val newIndex = itemProvider.keyToIndexMap[key]
if (newIndex != null) {
return ItemIndex(newIndex)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt
index 84bf0c1..cad9575 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt
@@ -47,12 +47,19 @@
/**
* The max current line (horizontal for vertical grids) the item can occupy, such that
* it will be positioned on the current line.
+ *
+ * For example if [LazyVerticalGrid] has 3 columns this value will be 3 for the first cell in
+ * the line, 2 for the second cell, and 1 for the last one. If you return a span count larger
+ * than [maxCurrentLineSpan] this means we can't fit this cell into the current line, so the
+ * cell will be positioned on the next line.
*/
val maxCurrentLineSpan: Int
/**
* The max line span (horizontal for vertical grids) an item can occupy. This will be the
* number of columns in vertical grids or the number of rows in horizontal grids.
+ *
+ * For example if [LazyVerticalGrid] has 3 columns this value will be 3 for each cell.
*/
val maxLineSpan: Int
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index d995d78..6c0f594 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -21,7 +21,7 @@
import kotlin.math.sqrt
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsProvider: LazyGridItemsProvider) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
class LineConfiguration(val firstItemIndex: Int, val spans: List<GridItemSpan>)
/** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -60,7 +60,7 @@
List(currentSlotsPerLine) { GridItemSpan(1) }.also { previousDefaultSpans = it }
}
- val totalSize get() = itemsProvider.itemsCount
+ val totalSize get() = itemProvider.itemCount
/** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
var slotsPerLine = 0
@@ -72,7 +72,7 @@
}
fun getLineConfiguration(lineIndex: Int): LineConfiguration {
- if (!itemsProvider.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
// Quick return when all spans are 1x1 - in this case we can easily calculate positions.
val firstItemIndex = lineIndex * slotsPerLine
return LineConfiguration(
@@ -172,7 +172,7 @@
return LineIndex(0)
}
require(itemIndex < totalSize)
- if (!itemsProvider.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
return LineIndex(itemIndex / slotsPerLine)
}
@@ -210,7 +210,7 @@
return LineIndex(currentLine)
}
- private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsProvider) {
+ private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
with(LazyGridItemSpanScopeImpl) {
maxCurrentLineSpan = maxSpan
maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index 82f5d31..3286ee5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -399,8 +399,8 @@
* items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- internal fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyGridItemsProvider) {
- scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+ scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
companion object {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
index c6844974..b6160a9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -26,7 +26,7 @@
*/
@OptIn(ExperimentalFoundationApi::class)
internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
- private val itemsProvider: LazyGridItemsProvider,
+ private val itemProvider: LazyGridItemProvider,
private val measureScope: LazyLayoutMeasureScope,
private val defaultMainAxisSpacing: Int,
private val measuredItemFactory: MeasuredItemFactory
@@ -40,7 +40,7 @@
mainAxisSpacing: Int = defaultMainAxisSpacing,
constraints: Constraints
): LazyMeasuredItem {
- val key = itemsProvider.getKey(index.value)
+ val key = itemProvider.getKey(index.value)
val placeables = measureScope.measure(index.value, constraints)
val crossAxisSize = if (constraints.hasFixedWidth) {
constraints.minWidth
@@ -61,7 +61,7 @@
* Contains the mapping between the key and the index. It could contain not all the items of
* the list as an optimization.
**/
- val keyToIndexMap: Map<Any, Int> get() = itemsProvider.keyToIndexMap
+ val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
}
// This interface allows to avoid autoboxing on index param
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 19bf588..54cafcd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -38,7 +38,7 @@
@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
@Composable
internal fun Modifier.lazyGridSemantics(
- itemsProvider: LazyGridItemsProvider,
+ itemProvider: LazyGridItemProvider,
state: LazyGridState,
coroutineScope: CoroutineScope,
isVertical: Boolean,
@@ -46,16 +46,16 @@
userScrollEnabled: Boolean
) = this.then(
remember(
- itemsProvider,
+ itemProvider,
state,
isVertical,
reverseScrolling,
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemsProvider::getKey
+ val key = itemProvider::getKey
var result = -1
- for (index in 0 until itemsProvider.itemsCount) {
+ for (index in 0 until itemProvider.itemCount) {
if (key(index) == needle) {
result = index
break
@@ -76,7 +76,7 @@
if (state.canScrollForward) {
// If we can scroll further, we don't know the end yet,
// but it's upper bounded by #items + 1
- itemsProvider.itemsCount + 1f
+ itemProvider.itemCount + 1f
} else {
// If we can't scroll further, the current value is the max
state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 7b68e1a..cc178f3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -32,7 +32,7 @@
* A layout that only composes and lays out currently needed items. Can be used to build
* efficient scrollable layouts.
*
- * @param itemsProvider provides all the needed info about the items which could be used to
+ * @param itemProvider provides all the needed info about the items which could be used to
* compose and measure items as part of [measurePolicy].
* @param modifier to apply on the layout
* @param prefetchState allows to schedule items for prefetching
@@ -41,16 +41,16 @@
@ExperimentalFoundationApi
@Composable
fun LazyLayout(
- itemsProvider: LazyLayoutItemsProvider,
+ itemProvider: LazyLayoutItemProvider,
modifier: Modifier = Modifier,
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
) {
- val currentItemsProvider = rememberUpdatedState(itemsProvider)
+ val currentItemProvider = rememberUpdatedState(itemProvider)
val saveableStateHolder = rememberSaveableStateHolder()
val itemContentFactory = remember {
- LazyLayoutItemContentFactory(saveableStateHolder) { currentItemsProvider.value }
+ LazyLayoutItemContentFactory(saveableStateHolder) { currentItemProvider.value }
}
val subcomposeLayoutState = remember {
SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 8a6aef7..b2dddac 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -28,19 +28,19 @@
/**
* This class:
- * 1) Caches the lambdas being produced by [itemsProvider]. This allows us to perform less
+ * 1) Caches the lambdas being produced by [itemProvider]. This allows us to perform less
* recompositions as the compose runtime can skip the whole composition if we subcompose with the
* same instance of the content lambda.
* 2) Updates the mapping between keys and indexes when we have a new factory
- * 3) Adds state restoration on top of the composable returned by [itemsProvider] with help of
+ * 3) Adds state restoration on top of the composable returned by [itemProvider] with help of
* [saveableStateHolder].
*/
@ExperimentalFoundationApi
internal class LazyLayoutItemContentFactory(
private val saveableStateHolder: SaveableStateHolder,
- val itemsProvider: () -> LazyLayoutItemsProvider,
+ val itemProvider: () -> LazyLayoutItemProvider,
) {
- /** Contains the cached lambdas produced by the [itemsProvider]. */
+ /** Contains the cached lambdas produced by the [itemProvider]. */
private val lambdasCache = mutableMapOf<Any, CachedItemContent>()
/** Density used to obtain the cached lambdas. */
@@ -70,7 +70,7 @@
return if (cachedContent != null) {
cachedContent.type
} else {
- val itemProvider = itemsProvider()
+ val itemProvider = itemProvider()
val index = itemProvider.keyToIndexMap[key]
if (index != null) {
itemProvider.getContentType(index)
@@ -85,7 +85,7 @@
*/
fun getContent(index: Int, key: Any): @Composable () -> Unit {
val cached = lambdasCache[key]
- val type = itemsProvider().getContentType(index)
+ val type = itemProvider().getContentType(index)
return if (cached != null && cached.lastKnownIndex == index && cached.type == type) {
cached.content
} else {
@@ -108,15 +108,16 @@
get() = _content ?: createContentLambda().also { _content = it }
private fun createContentLambda() = @Composable {
- val itemsProvider = itemsProvider()
- val index = itemsProvider.keyToIndexMap[key]?.also {
+ val itemProvider = itemProvider()
+ val index = itemProvider.keyToIndexMap[key]?.also {
lastKnownIndex = it
} ?: lastKnownIndex
- if (index < itemsProvider.itemsCount) {
- val key = itemsProvider.getKey(index)
+ if (index < itemProvider.itemCount) {
+ val key = itemProvider.getKey(index)
if (key == this.key) {
- val content = itemsProvider.getContent(index)
- saveableStateHolder.SaveableStateProvider(key, content)
+ saveableStateHolder.SaveableStateProvider(key) {
+ itemProvider.Item(index)
+ }
}
}
DisposableEffect(key) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
similarity index 87%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
index 79da266..370b7ee 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
@@ -26,18 +26,23 @@
*/
@Stable
@ExperimentalFoundationApi
-interface LazyLayoutItemsProvider {
+interface LazyLayoutItemProvider {
- /** The total number of items in the lazy layout (visible or not). */
- val itemsCount: Int
+ /**
+ * The total number of items in the lazy layout (visible or not).
+ */
+ val itemCount: Int
- /** Returns the content lambda for the given index and scope object */
- fun getContent(index: Int): @Composable () -> Unit
+ /**
+ * The item for the given [index].
+ */
+ @Composable
+ fun Item(index: Int)
/**
* Returns the content type for the item on this index. It is used to improve the item
* compositions reusing efficiency.
- **/
+ */
fun getContentType(index: Int): Any?
/**
@@ -50,7 +55,7 @@
/**
* Contains the mapping between the key and the index. It could contain not all the items of
* the list as an optimization.
- **/
+ */
val keyToIndexMap: Map<Any, Int>
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
index 448b51b..ff8316f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
@@ -47,9 +47,9 @@
/**
* Subcompose and measure the item of lazy layout.
*
- * @param index the item index. Should be no larger that [LazyLayoutItemsProvider.itemsCount].
+ * @param index the item index. Should be no larger that [LazyLayoutItemProvider.itemCount].
* @param constraints [Constraints] to measure the children emitted into an item content
- * composable specified via [LazyLayoutItemsProvider.getContent].
+ * composable specified via [LazyLayoutItemProvider.Item].
*
* @return Array of [Placeable]s. Note that if you emitted multiple children into the item
* composable you will receive multiple placeables, each of them will be measured with
@@ -113,7 +113,7 @@
return if (cachedPlaceable != null) {
cachedPlaceable
} else {
- val key = itemContentFactory.itemsProvider().getKey(index)
+ val key = itemContentFactory.itemProvider().getKey(index)
val itemContent = itemContentFactory.getContent(index, key)
val measurables = subcomposeMeasureScope.subcompose(key, itemContent)
Array(measurables.size) { i ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PinnableParent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PinnableParent.kt
index bcf7be4..aa522ff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PinnableParent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PinnableParent.kt
@@ -35,7 +35,7 @@
/**
* Parent modifiers that implement this interface should retain its current children when it
- * receives a call to [pinBeyondBoundsItems] and return a [PinnedItemsHandle]. It should hold on to
+ * receives a call to [pinItems] and return a [PinnedItemsHandle]. It should hold on to
* thesechildren until it receives a call to [PinnedItemsHandle.unpin].
*/
@ExperimentalFoundationApi
@@ -43,10 +43,10 @@
/**
* Pin the currently composed items.
*/
- fun pinBeyondBoundsItems(): PinnedItemsHandle
+ fun pinItems(): PinnedItemsHandle
/**
- * This is an object returned by [PinnableParent.pinBeyondBoundsItems] when it pins its
+ * This is an object returned by [PinnableParent.pinItems] when it pins its
* currently composed items. It provides an [unpin] function to release the pinned children.
*/
@ExperimentalFoundationApi
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index a907370..9c4339a 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -115,7 +115,7 @@
* signature.
*/
private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
- // Strip any mangled part of the name in case of inline classes
+ // Strip any mangled part of the name in case of value / inline classes
val expectedName = method.name.substringBefore("-")
val expectedSignature = ClassUtil.getAsmMethodSignature(method)
// Since Kotlin 1.6 PSI updates, in some cases what used to be `void` return types are converted
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 04c1c14..b5ba87a 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -582,7 +582,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError);
}
- public final class TextFieldDefaults {
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
method public float getFocusedBorderThickness();
method public float getMinHeight();
method public float getMinWidth();
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index c9468ea..b10467c 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -826,8 +826,8 @@
method @androidx.compose.runtime.Composable public default androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
- public final class TextFieldDefaults {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public void BorderStroke(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public void BorderBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> border);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
method public float getFocusedBorderThickness();
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 04c1c14..b5ba87a 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -582,7 +582,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError);
}
- public final class TextFieldDefaults {
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
method public float getFocusedBorderThickness();
method public float getMinHeight();
method public float getMinWidth();
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
index 2f2f2b2..f94d0ef 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
@@ -436,7 +436,7 @@
),
// update border thickness and shape
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled = enabled,
isError = false,
colors = colors,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
index 6fb3cd5..d30aa64 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
@@ -364,7 +364,7 @@
interactionSource = interactionSource,
singleLine = singleLine,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled = true,
isError = false,
colors = colors,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 01263aa..875876b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -195,7 +195,7 @@
interactionSource = interactionSource,
colors = colors,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
@@ -339,7 +339,7 @@
interactionSource = interactionSource,
colors = colors,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index d3fd4fa..bb248fc 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -894,6 +894,7 @@
dragBy(0f)
}
}
+ gestureEndAction.value.invoke(0f)
}
)
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
index c8002a1..975361d 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
@@ -186,6 +186,7 @@
/**
* Contains the default values used by [TextField] and [OutlinedTextField].
*/
+@Immutable
object TextFieldDefaults {
/**
* The default min width applied for a [TextField] and [OutlinedTextField].
@@ -305,7 +306,7 @@
*/
@ExperimentalMaterialApi
@Composable
- fun BorderStroke(
+ fun BorderBox(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource,
@@ -645,7 +646,7 @@
colors: TextFieldColors = outlinedTextFieldColors(),
contentPadding: PaddingValues = outlinedTextFieldPadding(),
border: @Composable () -> Unit = {
- BorderStroke(enabled, isError, interactionSource, colors)
+ BorderBox(enabled, isError, interactionSource, colors)
}
) {
CommonDecorationBox(
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index dd6207d..4a22954 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -520,7 +520,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
- public final class TextFieldDefaults {
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
method public float getFocusedBorderThickness();
method public float getMinHeight();
method public float getMinWidth();
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 4b0918f..384c98b 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -586,6 +586,7 @@
}
public final class SliderKt {
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> values, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SliderColors colors);
}
@@ -719,8 +720,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
- public final class TextFieldDefaults {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void BorderStroke(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void BorderBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void OutlinedTextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> border);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void TextFieldDecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
method public float getFocusedBorderThickness();
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index dd6207d..4a22954 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -520,7 +520,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
- public final class TextFieldDefaults {
+ @androidx.compose.runtime.Immutable public final class TextFieldDefaults {
method public float getFocusedBorderThickness();
method public float getMinHeight();
method public float getMinWidth();
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index bbdef54..1635b2f 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -85,6 +85,7 @@
import androidx.compose.material3.samples.PinnedSmallTopAppBar
import androidx.compose.material3.samples.RadioButtonSample
import androidx.compose.material3.samples.RadioGroupSample
+import androidx.compose.material3.samples.RangeSliderSample
import androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
import androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
@@ -98,6 +99,7 @@
import androidx.compose.material3.samples.SimpleTextFieldSample
import androidx.compose.material3.samples.SliderSample
import androidx.compose.material3.samples.SmallFloatingActionButtonSample
+import androidx.compose.material3.samples.StepRangeSliderSample
import androidx.compose.material3.samples.StepsSliderSample
import androidx.compose.material3.samples.SuggestionChipSample
import androidx.compose.material3.samples.SwitchSample
@@ -602,6 +604,20 @@
) {
StepsSliderSample()
},
+ Example(
+ name = ::RangeSliderSample.name,
+ description = SlidersExampleDescription,
+ sourceUrl = SlidersExampleSourceUrl
+ ) {
+ RangeSliderSample()
+ },
+ Example(
+ name = ::StepRangeSliderSample.name,
+ description = SlidersExampleDescription,
+ sourceUrl = SlidersExampleSourceUrl
+ ) {
+ StepRangeSliderSample()
+ },
)
private const val SnackbarsExampleDescription = "Snackbars examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSample.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSample.kt
index c3d3d80..e4f3525 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSample.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSample.kt
@@ -18,6 +18,8 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -53,4 +55,43 @@
steps = 4
)
}
-}
\ No newline at end of file
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun RangeSliderSample() {
+ var sliderPosition by remember { mutableStateOf(0f..100f) }
+ Column {
+ Text(text = sliderPosition.toString())
+ RangeSlider(
+ values = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..100f,
+ onValueChangeFinished = {
+ // launch some business logic update with the state you hold
+ // viewModel.updateSelectedSliderValue(sliderPosition)
+ },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun StepRangeSliderSample() {
+ var sliderPosition by remember { mutableStateOf(0f..100f) }
+ Column {
+ Text(text = sliderPosition.toString())
+ RangeSlider(
+ steps = 5,
+ values = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..100f,
+ onValueChangeFinished = {
+ // launch some business logic update with the state you hold
+ // viewModel.updateSelectedSliderValue(sliderPosition)
+ },
+ )
+ }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
index ec9f8e4..78a0b68 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
@@ -452,7 +452,7 @@
),
// update border thickness and shape
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled = enabled,
isError = false,
colors = colors,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
index 81410b7..f374513 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
@@ -89,6 +89,28 @@
}
@Test
+ fun sliderTest_middle_dark() {
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f) }
+ Slider(position, { position = it })
+ }
+ }
+ assertSliderAgainstGolden("slider_middle_dark")
+ }
+
+ @Test
+ fun sliderTest_middle_dark_disabled() {
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f) }
+ Slider(position, enabled = false, onValueChange = { position = it })
+ }
+ }
+ assertSliderAgainstGolden("slider_middle_dark_disabled")
+ }
+
+ @Test
fun sliderTest_end() {
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
@@ -111,6 +133,17 @@
}
@Test
+ fun sliderTest_middle_steps_dark() {
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f) }
+ Slider(position, { position = it }, steps = 5)
+ }
+ }
+ assertSliderAgainstGolden("slider_middle_steps_dark")
+ }
+
+ @Test
fun sliderTest_middle_steps_disabled() {
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
@@ -169,6 +202,105 @@
assertSliderAgainstGolden("slider_customColors_disabled")
}
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_middle_steps_disabled() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f..1f) }
+ RangeSlider(position, { position = it }, steps = 5, enabled = false)
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_middle_steps_disabled")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_middle_steps_enabled() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f..1f) }
+ RangeSlider(position, { position = it }, steps = 5)
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_middle_steps_enabled")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_middle_steps_dark_enabled() {
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f..1f) }
+ RangeSlider(position, { position = it }, steps = 5)
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_middle_steps_dark_enabled")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_middle_steps_dark_disabled() {
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f..1f) }
+ RangeSlider(
+ position,
+ enabled = false,
+ onValueChange = { position = it },
+ steps = 5)
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_middle_steps_dark_disabled")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_overlapingThumbs() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0.5f..0.51f) }
+ RangeSlider(position, { position = it })
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_overlapingThumbs")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_fullRange() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(0f..1f) }
+ RangeSlider(position, { position = it })
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_fullRange")
+ }
+
+ @Test
+ @ExperimentalMaterial3Api
+ fun rangeSliderTest_steps_customColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(30f..70f) }
+ RangeSlider(
+ values = position,
+ valueRange = 0f..100f,
+ onValueChange = { position = it }, steps = 9,
+ colors = SliderDefaults.colors(
+ thumbColor = Color.Blue,
+ activeTrackColor = Color.Red,
+ inactiveTrackColor = Color.Yellow,
+ activeTickColor = Color.Magenta,
+ inactiveTickColor = Color.Cyan
+ )
+ )
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_steps_customColors")
+ }
+
private fun assertSliderAgainstGolden(goldenName: String) {
rule.onNodeWithTag(wrapperTestTag)
.captureToImage()
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
index f3225a5..4ed02e4 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -16,14 +16,19 @@
package androidx.compose.material3
-import android.view.ViewConfiguration
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
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.height
import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.layout.width
import androidx.compose.material3.tokens.SliderTokens
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -32,6 +37,10 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
@@ -44,6 +53,7 @@
import androidx.compose.ui.test.assertRangeInfoEquals
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.click
+import androidx.compose.ui.test.isFocusable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performSemanticsAction
@@ -207,6 +217,41 @@
}
@Test
+ fun slider_drag_out_of_bounds() {
+ val state = mutableStateOf(0f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ Slider(
+ modifier = Modifier.testTag(tag),
+ value = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(width.toFloat() + 100f, 0f))
+ up()
+ expected = calculateFraction(left, right, centerX + 100 - slop)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @Test
fun slider_tap() {
val state = mutableStateOf(0f)
@@ -236,14 +281,24 @@
}
/**
- * Guarantee slider doesn't move while we wait to see if the gesture is scrolling.
+ * Guarantee slider doesn't move as we scroll, tapping still works
*/
@Test
- fun slider_tap_scrollableContainer() {
- rule.mainClock.autoAdvance = false
+ fun slider_scrollableContainer() {
val state = mutableStateOf(0f)
+ val offset = mutableStateOf(0f)
+
rule.setContent {
- Box(Modifier.verticalScroll(rememberScrollState())) {
+ Column(
+ modifier = Modifier
+ .height(2000.dp)
+ .scrollable(
+ orientation = Orientation.Vertical,
+ state = rememberScrollableState { delta ->
+ offset.value += delta
+ delta
+ })
+ ) {
Slider(
modifier = Modifier.testTag(tag),
value = state.value,
@@ -252,18 +307,30 @@
}
}
- var expected = 0f
- rule.onNodeWithTag(tag)
+ rule.runOnIdle {
+ Truth.assertThat(offset.value).isEqualTo(0f)
+ }
+
+ // Just scroll
+ rule.onNodeWithTag(tag, useUnmergedTree = true)
.performTouchInput {
- down(Offset(centerX + 50, centerY))
- expected = calculateFraction(left, right, centerX + 50)
+ down(Offset(centerX, centerY))
+ moveBy(Offset(0f, 500f))
+ up()
}
rule.runOnIdle {
+ Truth.assertThat(offset.value).isGreaterThan(0f)
Truth.assertThat(state.value).isEqualTo(0f)
}
- rule.mainClock.advanceTimeBy(ViewConfiguration.getTapTimeout().toLong())
+ // Tap
+ var expected = 0f
+ rule.onNodeWithTag(tag, useUnmergedTree = true)
+ .performTouchInput {
+ click(Offset(centerX, centerY))
+ expected = calculateFraction(left, right, centerX)
+ }
rule.runOnIdle {
Truth.assertThat(state.value).isWithin(0.001f).of(expected)
@@ -332,7 +399,7 @@
expected = calculateFraction(left, right, centerX - 100 + slop)
}
rule.runOnIdle {
- Truth.assertThat(state.value).isWithin(0.001f).of(expected)
+ Truth.assertThat(state.value).isWithin(0.002f).of(expected)
}
}
@@ -363,44 +430,17 @@
expected = calculateFraction(left, right, centerX - 50)
}
rule.runOnIdle {
- Truth.assertThat(state.value).isWithin(0.001f).of(expected)
+ Truth.assertThat(state.value).isWithin(0.002f).of(expected)
}
}
- @Test
- fun rangeSlider_tap_rtl() {
- val state = mutableStateOf(0f)
-
- rule.setMaterialContent(lightColorScheme()) {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Slider(
- modifier = Modifier.testTag(tag),
- value = state.value,
- onValueChange = { state.value = it }
- )
- }
- }
-
- rule.runOnUiThread {
- Truth.assertThat(state.value).isEqualTo(0f)
- }
-
- var expected = 0f
-
- rule.onNodeWithTag(tag)
- .performTouchInput {
- down(Offset(centerX + 50, centerY))
- up()
- expected = calculateFraction(left, right, centerX - 50)
- }
- rule.runOnIdle {
- Truth.assertThat(state.value).isWithin(0.001f).of(expected)
- }
+ private fun calculateFraction(left: Float, right: Float, pos: Float) = with(rule.density) {
+ val offset = (ThumbWidth / 2).toPx()
+ val start = left + offset
+ val end = right - offset
+ ((pos - start) / (end - start)).coerceIn(0f, 1f)
}
- private fun calculateFraction(a: Float, b: Float, pos: Float) =
- ((pos - a) / (b - a)).coerceIn(0f, 1f)
-
@Test
fun slider_sizes() {
val state = mutableStateOf(0f)
@@ -513,11 +553,14 @@
}
rule.onNodeWithTag(tag)
- .performTouchInput { down(center) }
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(100f, 0f))
+ }
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(1)
- Truth.assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
+ Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
}
// Dispose
@@ -527,10 +570,486 @@
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(2)
- Truth.assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
- Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Cancel::class.java)
- Truth.assertThat((interactions[1] as PressInteraction.Cancel).press)
+ Truth.assertThat(interactions.first()).isInstanceOf(DragInteraction.Start::class.java)
+ Truth.assertThat(interactions[1]).isInstanceOf(DragInteraction.Cancel::class.java)
+ Truth.assertThat((interactions[1] as DragInteraction.Cancel).start)
.isEqualTo(interactions[0])
}
}
+
+ @Test
+ fun slider_onValueChangedFinish_afterTap() {
+ var changedFlag = false
+ rule.setContent {
+ Slider(
+ modifier = Modifier.testTag(tag),
+ value = 0.0f,
+ onValueChangeFinished = { changedFlag = true },
+ onValueChange = {}
+ )
+ }
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ click(center)
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(changedFlag).isTrue()
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_dragThumb() {
+ val state = mutableStateOf(0f..1f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f..1f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(slop, 0f))
+ moveBy(Offset(100f, 0f))
+ expected = calculateFraction(left, right, centerX + 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isEqualTo(0f)
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_drag_out_of_bounds() {
+ val state = mutableStateOf(0f..1f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f..1f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(slop, 0f))
+ moveBy(Offset(width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(width.toFloat() + 100f, 0f))
+ up()
+ expected = calculateFraction(left, right, centerX + 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isEqualTo(0f)
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_drag_overlap_thumbs() {
+ val state = mutableStateOf(0.5f..1f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(centerRight)
+ moveBy(Offset(-slop, 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ up()
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value).isEqualTo(0.5f..0.5f)
+ }
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(-slop, 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ up()
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value).isEqualTo(0.0f..0.5f)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_tap() {
+ val state = mutableStateOf(0f..1f)
+
+ rule.setMaterialContent(lightColorScheme()) {
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f..1f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(Offset(centerX + 50, centerY))
+ up()
+ expected = calculateFraction(left, right, centerX + 50)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ Truth.assertThat(state.value.start).isEqualTo(0f)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_tap_rangeChange() {
+ val state = mutableStateOf(0f..25f)
+ val rangeEnd = mutableStateOf(.25f)
+
+ rule.setMaterialContent(lightColorScheme()) {
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it },
+ valueRange = 0f..rangeEnd.value
+ )
+ }
+ // change to 1 since [calculateFraction] coerces between 0..1
+ rule.runOnUiThread {
+ rangeEnd.value = 1f
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(Offset(centerX + 50, centerY))
+ up()
+ expected = calculateFraction(left, right, centerX + 50)
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_drag_rtl() {
+ val state = mutableStateOf(0f..1f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f..1f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(slop, 0f))
+ moveBy(Offset(100f, 0f))
+ up()
+ // subtract here as we're in rtl and going in the opposite direction
+ expected = calculateFraction(left, right, centerX - 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isEqualTo(0f)
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_drag_out_of_bounds_rtl() {
+ val state = mutableStateOf(0f..1f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0f..1f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(slop, 0f))
+ moveBy(Offset(width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(-width.toFloat(), 0f))
+ moveBy(Offset(width.toFloat() + 100f, 0f))
+ up()
+ // subtract here as we're in rtl and going in the opposite direction
+ expected = calculateFraction(left, right, centerX - 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isEqualTo(0f)
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_closeThumbs_dragRight() {
+ val state = mutableStateOf(0.5f..0.5f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0.5f..0.5f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(slop, 0f))
+ moveBy(Offset(100f, 0f))
+ up()
+ // subtract here as we're in rtl and going in the opposite direction
+ expected = calculateFraction(left, right, centerX + 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isEqualTo(0.5f)
+ Truth.assertThat(state.value.endInclusive).isWithin(0.001f).of(expected)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_closeThumbs_dragLeft() {
+ val state = mutableStateOf(0.5f..0.5f)
+ var slop = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ slop = LocalViewConfiguration.current.touchSlop
+ RangeSlider(
+ modifier = Modifier.testTag(tag),
+ values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.runOnUiThread {
+ Truth.assertThat(state.value).isEqualTo(0.5f..0.5f)
+ }
+
+ var expected = 0f
+
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(-slop - 1, 0f))
+ moveBy(Offset(-100f, 0f))
+ up()
+ // subtract here as we're in rtl and going in the opposite direction
+ expected = calculateFraction(left, right, centerX - 100)
+ }
+ rule.runOnIdle {
+ Truth.assertThat(state.value.start).isWithin(0.001f).of(expected)
+ Truth.assertThat(state.value.endInclusive).isEqualTo(0.5f)
+ }
+ }
+
+ /**
+ * Regression test for bug: 210289161 where RangeSlider was ignoring some modifiers like weight.
+ */
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_weightModifier() {
+ var sliderBounds = Rect(0f, 0f, 0f, 0f)
+ rule.setMaterialContent(lightColorScheme()) {
+ with(LocalDensity.current) {
+ Row(Modifier.width(500.toDp())) {
+ Spacer(Modifier.requiredSize(100.toDp()))
+ RangeSlider(
+ values = 0f..0.5f,
+ onValueChange = {},
+ modifier = Modifier.testTag(tag).weight(1f).onGloballyPositioned {
+ sliderBounds = it.boundsInParent()
+ }
+ )
+ Spacer(Modifier.requiredSize(100.toDp()))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(sliderBounds.left).isEqualTo(100)
+ Truth.assertThat(sliderBounds.right).isEqualTo(400)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_semantics_continuous() {
+ val state = mutableStateOf(0f..1f)
+
+ rule.setMaterialContent(lightColorScheme()) {
+ RangeSlider(
+ modifier = Modifier.testTag(tag), values = state.value,
+ onValueChange = { state.value = it }
+ )
+ }
+
+ rule.onAllNodes(isFocusable(), true)[0]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f, 0))
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+ rule.onAllNodes(isFocusable(), true)[1]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f, 0))
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
+
+ rule.runOnUiThread {
+ state.value = 0.5f..0.75f
+ }
+
+ rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ 0.5f,
+ 0f..0.75f,
+ 0
+ )
+ )
+
+ rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ 0.75f,
+ 0.5f..1f,
+ 0
+ )
+ )
+
+ rule.onAllNodes(isFocusable(), true)[0]
+ .performSemanticsAction(SemanticsActions.SetProgress) { it(0.6f) }
+
+ rule.onAllNodes(isFocusable(), true)[1]
+ .performSemanticsAction(SemanticsActions.SetProgress) { it(0.8f) }
+
+ rule.onAllNodes(isFocusable(), true)[0]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(0.6f, 0f..0.8f, 0))
+
+ rule.onAllNodes(isFocusable(), true)[1]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(0.8f, 0.6f..1f, 0))
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun rangeSlider_semantics_stepped() {
+ val state = mutableStateOf(0f..20f)
+ // Slider with [0,5,10,15,20] possible values
+ rule.setMaterialContent(lightColorScheme()) {
+ RangeSlider(
+ modifier = Modifier.testTag(tag), values = state.value,
+ steps = 3,
+ valueRange = 0f..20f,
+ onValueChange = { state.value = it },
+ )
+ }
+
+ rule.runOnUiThread {
+ state.value = 5f..10f
+ }
+
+ rule.onAllNodes(isFocusable(), true)[0].assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ 5f,
+ 0f..10f,
+ 3
+ )
+ )
+
+ rule.onAllNodes(isFocusable(), true)[1].assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ 10f,
+ 5f..20f,
+ 3,
+ )
+ )
+
+ rule.onAllNodes(isFocusable(), true)[0]
+ .performSemanticsAction(SemanticsActions.SetProgress) { it(10f) }
+
+ rule.onAllNodes(isFocusable(), true)[1]
+ .performSemanticsAction(SemanticsActions.SetProgress) { it(15f) }
+
+ rule.onAllNodes(isFocusable(), true)[0]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(10f, 0f..15f, 3))
+
+ rule.onAllNodes(isFocusable(), true)[1]
+ .assertRangeInfoEquals(ProgressBarRangeInfo(15f, 10f..20f, 3))
+ }
}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
index ce37822..7635871 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
@@ -356,7 +356,7 @@
interactionSource = interactionSource,
singleLine = singleLine,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled = true,
isError = false,
colors = colors,
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index 0da6f99..1fbe30c 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -31,6 +31,8 @@
Strings.CloseSheet -> resources.getString(R.string.close_sheet)
Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message)
Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu)
+ Strings.SliderRangeStart -> resources.getString(R.string.range_start)
+ Strings.SliderRangeEnd -> resources.getString(R.string.range_end)
else -> ""
}
}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 3c4ce14..4d10c95 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -196,7 +196,7 @@
interactionSource = interactionSource,
colors = colors,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
@@ -340,7 +340,7 @@
interactionSource = interactionSource,
colors = colors,
border = {
- TextFieldDefaults.BorderStroke(
+ TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index c525975..a99ed4b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -22,12 +22,16 @@
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.DragScope
import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.GestureCancellationException
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.DragInteraction
@@ -35,6 +39,7 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -61,6 +66,7 @@
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.shadow
@@ -70,30 +76,39 @@
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.max
+import androidx.compose.ui.util.lerp
import kotlin.math.abs
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
- * <a href="https://material.io/components/sliders" class="external" target="_blank">Material Design slider</a>.
+ * Material Design slider
*
* Sliders allow users to make selections from a range of values.
*
* Sliders reflect a range of values along a bar, from which users may select a single value.
* They are ideal for adjusting settings such as volume, brightness, or applying image filters.
*
- * ![Sliders image](https://developer.android.com/images/reference/androidx/compose/material/sliders.png)
+ * ![Sliders image](https://developer.android.com/images/reference/androidx/compose/material3/sliders.png)
*
* Use continuous sliders to allow users to make meaningful selections that don’t
* require a specific value:
@@ -125,6 +140,7 @@
* @param colors [SliderColors] that will be used to determine the color of the Slider parts in
* different state. See [SliderDefaults.colors] to customize.
*/
+// TODO(b/229979132): Add m.io link
@Composable
fun Slider(
value: Float,
@@ -154,8 +170,15 @@
.focusable(enabled, interactionSource)
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- val maxPx = constraints.maxWidth.toFloat()
- val minPx = 0f
+ val widthPx = constraints.maxWidth.toFloat()
+ val maxPx: Float
+ val minPx: Float
+ val thumbRadius = ThumbDiameter / 2
+
+ with(LocalDensity.current) {
+ maxPx = widthPx - thumbRadius.toPx()
+ minPx = thumbRadius.toPx()
+ }
fun scaleToUserValue(offset: Float) =
scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
@@ -165,14 +188,18 @@
val scope = rememberCoroutineScope()
val rawOffset = remember { mutableStateOf(scaleToOffset(value)) }
+ val pressOffset = remember { mutableStateOf(0f) }
+
val draggableState = remember(minPx, maxPx, valueRange) {
SliderDraggableState {
- rawOffset.value = (rawOffset.value + it).coerceIn(minPx, maxPx)
- onValueChangeState.value.invoke(scaleToUserValue(rawOffset.value))
+ rawOffset.value = (rawOffset.value + it + pressOffset.value)
+ pressOffset.value = 0f
+ val offsetInTrack = rawOffset.value.coerceIn(minPx, maxPx)
+ onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack))
}
}
- CorrectValueSideEffect(::scaleToOffset, valueRange, rawOffset, value)
+ CorrectValueSideEffect(::scaleToOffset, valueRange, minPx..maxPx, rawOffset, value)
val gestureEndAction = rememberUpdatedState<(Float) -> Unit> { velocity: Float ->
val current = rawOffset.value
@@ -188,8 +215,15 @@
}
}
- val press = Modifier.sliderPressModifier(
- draggableState, interactionSource, maxPx, isRtl, rawOffset, gestureEndAction, enabled
+ val press = Modifier.sliderTapModifier(
+ draggableState,
+ interactionSource,
+ widthPx,
+ isRtl,
+ rawOffset,
+ gestureEndAction,
+ pressOffset,
+ enabled
)
val drag = Modifier.draggable(
@@ -209,7 +243,7 @@
fraction,
tickFractions,
colors,
- maxPx,
+ maxPx - minPx,
interactionSource,
modifier = press.then(drag)
)
@@ -217,6 +251,256 @@
}
/**
+ * Material Design Range slider
+ *
+ * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
+ *
+ * The two values are still bounded by the value range but they also cannot cross each other.
+ *
+ * Use continuous Range Sliders to allow users to make meaningful selections that don’t
+ * require a specific values:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material3.samples.StepRangeSliderSample
+ *
+ * @param values current values of the RangeSlider. If either value is outside of [valueRange]
+ * provided, it will be coerced to this range.
+ * @param onValueChange lambda in which values should be updated
+ * @param modifier modifiers for the Range Slider layout
+ * @param enabled whether or not component is enabled and can we interacted with or not
+ * @param valueRange range of values that Range Slider values can take. Passed [values] will be
+ * coerced to this range
+ * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
+ * between across the whole value range. If 0, range slider will behave as a continuous slider and
+ * allow to choose any values from the range specified. Must not be negative.
+ * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
+ * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
+ * know when the user has completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to determine the color of the Range Slider
+ * parts in different state. See [SliderDefaults.colors] to customize.
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun RangeSlider(
+ values: ClosedFloatingPointRange<Float>,
+ onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+ /*@IntRange(from = 0)*/
+ steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ colors: SliderColors = SliderDefaults.colors()
+) {
+ val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+
+ require(steps >= 0) { "steps should be >= 0" }
+ val onValueChangeState = rememberUpdatedState(onValueChange)
+ val tickFractions = remember(steps) {
+ stepsToTickFractions(steps)
+ }
+
+ BoxWithConstraints(
+ modifier = modifier
+ .minimumTouchTargetSize()
+ .requiredSizeIn(minWidth = ThumbWidth * 2, minHeight = ThumbHeight * 2)
+ ) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val widthPx = constraints.maxWidth.toFloat()
+ val maxPx: Float
+ val minPx: Float
+
+ with(LocalDensity.current) {
+ maxPx = widthPx - ThumbWidth.toPx() / 2
+ minPx = ThumbWidth.toPx() / 2
+ }
+
+ fun scaleToUserValue(offset: ClosedFloatingPointRange<Float>) =
+ scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
+
+ fun scaleToOffset(userValue: Float) =
+ scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
+
+ val rawOffsetStart = remember { mutableStateOf(scaleToOffset(values.start)) }
+ val rawOffsetEnd = remember { mutableStateOf(scaleToOffset(values.endInclusive)) }
+
+ CorrectValueSideEffect(
+ ::scaleToOffset,
+ valueRange,
+ minPx..maxPx,
+ rawOffsetStart,
+ values.start
+ )
+ CorrectValueSideEffect(
+ ::scaleToOffset,
+ valueRange,
+ minPx..maxPx,
+ rawOffsetEnd,
+ values.endInclusive
+ )
+
+ val scope = rememberCoroutineScope()
+ val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> { isStart ->
+ val current = (if (isStart) rawOffsetStart else rawOffsetEnd).value
+ // target is a closest anchor to the `current`, if exists
+ val target = snapValueToTick(current, tickFractions, minPx, maxPx)
+ if (current == target) {
+ onValueChangeFinished?.invoke()
+ return@rememberUpdatedState
+ }
+
+ scope.launch {
+ Animatable(initialValue = current).animateTo(
+ target, SliderToTickAnimation,
+ 0f
+ ) {
+ (if (isStart) rawOffsetStart else rawOffsetEnd).value = this.value
+ onValueChangeState.value.invoke(
+ scaleToUserValue(rawOffsetStart.value..rawOffsetEnd.value)
+ )
+ }
+
+ onValueChangeFinished?.invoke()
+ }
+ }
+
+ val onDrag = rememberUpdatedState<(Boolean, Float) -> Unit> { isStart, offset ->
+ val offsetRange = if (isStart) {
+ rawOffsetStart.value = (rawOffsetStart.value + offset)
+ rawOffsetEnd.value = scaleToOffset(values.endInclusive)
+ val offsetEnd = rawOffsetEnd.value
+ val offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
+ offsetStart..offsetEnd
+ } else {
+ rawOffsetEnd.value = (rawOffsetEnd.value + offset)
+ rawOffsetStart.value = scaleToOffset(values.start)
+ val offsetStart = rawOffsetStart.value
+ val offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
+ offsetStart..offsetEnd
+ }
+
+ onValueChangeState.value.invoke(scaleToUserValue(offsetRange))
+ }
+
+ val pressDrag = Modifier.rangeSliderPressDragModifier(
+ startInteractionSource,
+ endInteractionSource,
+ rawOffsetStart,
+ rawOffsetEnd,
+ enabled,
+ isRtl,
+ widthPx,
+ valueRange,
+ gestureEndAction,
+ onDrag,
+ )
+
+ // The positions of the thumbs are dependant on each other.
+ val coercedStart = values.start.coerceIn(valueRange.start, values.endInclusive)
+ val coercedEnd = values.endInclusive.coerceIn(values.start, valueRange.endInclusive)
+ val fractionStart = calcFraction(valueRange.start, valueRange.endInclusive, coercedStart)
+ val fractionEnd = calcFraction(valueRange.start, valueRange.endInclusive, coercedEnd)
+ val startThumbSemantics = Modifier.sliderSemantics(
+ coercedStart,
+ tickFractions,
+ enabled,
+ { value -> onValueChangeState.value.invoke(value..coercedEnd) },
+ valueRange.start..coercedEnd,
+ steps
+ )
+ val endThumbSemantics = Modifier.sliderSemantics(
+ coercedEnd,
+ tickFractions,
+ enabled,
+ { value -> onValueChangeState.value.invoke(coercedStart..value) },
+ coercedStart..valueRange.endInclusive,
+ steps
+ )
+
+ RangeSliderImpl(
+ enabled,
+ fractionStart,
+ fractionEnd,
+ tickFractions,
+ colors,
+ maxPx - minPx,
+ startInteractionSource,
+ endInteractionSource,
+ modifier = pressDrag,
+ startThumbSemantics,
+ endThumbSemantics
+ )
+ }
+}
+
+@Composable
+private fun RangeSliderImpl(
+ enabled: Boolean,
+ positionFractionStart: Float,
+ positionFractionEnd: Float,
+ tickFractions: List<Float>,
+ colors: SliderColors,
+ width: Float,
+ startInteractionSource: MutableInteractionSource,
+ endInteractionSource: MutableInteractionSource,
+ modifier: Modifier,
+ startThumbSemantics: Modifier,
+ endThumbSemantics: Modifier
+) {
+ val startContentDescription = getString(Strings.SliderRangeStart)
+ val endContentDescription = getString(Strings.SliderRangeEnd)
+ Box(modifier.then(DefaultSliderConstraints)) {
+ val trackStrokeWidth: Float
+ val widthDp: Dp
+ with(LocalDensity.current) {
+ trackStrokeWidth = TrackHeight.toPx()
+ widthDp = width.toDp()
+ }
+
+ val offsetStart = widthDp * positionFractionStart
+ val offsetEnd = widthDp * positionFractionEnd
+ Track(
+ Modifier.align(Alignment.CenterStart).fillMaxSize(),
+ colors,
+ enabled,
+ positionFractionStart,
+ positionFractionEnd,
+ tickFractions,
+ ThumbWidth,
+ trackStrokeWidth
+ )
+
+ SliderThumb(
+ Modifier
+ .semantics(mergeDescendants = true) { contentDescription = startContentDescription }
+ .focusable(true, startInteractionSource)
+ .then(startThumbSemantics),
+ offsetStart,
+ startInteractionSource,
+ colors,
+ enabled,
+ ThumbSize
+ )
+ SliderThumb(
+ Modifier
+ .semantics(mergeDescendants = true) { contentDescription = endContentDescription }
+ .focusable(true, endInteractionSource)
+ .then(endThumbSemantics),
+ offsetEnd,
+ endInteractionSource,
+ colors,
+ enabled,
+ ThumbSize
+ )
+ }
+}
+
+/**
* Object to hold defaults used by [Slider]
*/
object SliderDefaults {
@@ -345,44 +629,37 @@
) {
Box(modifier.then(DefaultSliderConstraints)) {
val trackStrokeWidth: Float
- val thumbPx: Float
val widthDp: Dp
with(LocalDensity.current) {
trackStrokeWidth = SliderTokens.ActiveTrackHeight.toPx()
- thumbPx = SliderTokens.HandleWidth.toPx() / 2
widthDp = width.toDp()
}
- val thumbWidth = SliderTokens.HandleWidth
- val thumbHeight = SliderTokens.HandleHeight
- val offset = (widthDp - thumbWidth) * positionFraction
- val center = Modifier.align(Alignment.CenterStart)
-
+ val offset = widthDp * positionFraction
Track(
- center.fillMaxSize(),
+ Modifier.fillMaxSize(),
colors,
enabled,
0f,
positionFraction,
tickFractions,
- thumbPx,
+ ThumbWidth,
trackStrokeWidth
)
- SliderThumb(center, offset, interactionSource, colors, enabled, thumbWidth, thumbHeight)
+ SliderThumb(Modifier, offset, interactionSource, colors, enabled, ThumbSize)
}
}
@Composable
-private fun SliderThumb(
+private fun BoxScope.SliderThumb(
modifier: Modifier,
offset: Dp,
interactionSource: MutableInteractionSource,
colors: SliderColors,
enabled: Boolean,
- thumbHeight: Dp,
- thumbWidth: Dp
+ thumbSize: DpSize
) {
- Box(modifier.padding(start = offset)) {
+ Box(Modifier.padding(start = offset).align(Alignment.CenterStart)) {
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
@@ -397,27 +674,25 @@
}
}
- val thumbRippleRadius = max(thumbWidth, thumbHeight) + ThumbRippleMargin
-
val elevation = if (interactions.isNotEmpty()) {
ThumbPressedElevation
} else {
ThumbDefaultElevation
}
+ val shape = SliderTokens.HandleShape.toShape()
Spacer(
- Modifier
- .size(thumbWidth, thumbHeight)
+ modifier
+ .size(thumbSize)
.indication(
interactionSource = interactionSource,
- indication = rememberRipple(bounded = false, radius = thumbRippleRadius)
+ indication = rememberRipple(
+ bounded = false,
+ radius = SliderTokens.StateLayerSize / 2
+ )
)
.hoverable(interactionSource = interactionSource)
- .shadow(
- if (enabled) elevation else 0.dp,
- SliderTokens.HandleShape.toShape(),
- clip = false,
- )
- .background(colors.thumbColor(enabled).value, SliderTokens.HandleShape.toShape())
+ .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
+ .background(colors.thumbColor(enabled).value, shape)
)
}
}
@@ -430,17 +705,20 @@
positionFractionStart: Float,
positionFractionEnd: Float,
tickFractions: List<Float>,
- thumbPx: Float,
+ thumbWidth: Dp,
trackStrokeWidth: Float
) {
+ val thumbRadiusPx = with(LocalDensity.current) {
+ thumbWidth.toPx() / 2
+ }
val inactiveTrackColor = colors.trackColor(enabled, active = false)
val activeTrackColor = colors.trackColor(enabled, active = true)
val inactiveTickColor = colors.tickColor(enabled, active = false)
val activeTickColor = colors.tickColor(enabled, active = true)
Canvas(modifier) {
val isRtl = layoutDirection == LayoutDirection.Rtl
- val sliderLeft = Offset(thumbPx, center.y)
- val sliderRight = Offset(size.width - thumbPx, center.y)
+ val sliderLeft = Offset(thumbRadiusPx, center.y)
+ val sliderRight = Offset(size.width - thumbRadiusPx, center.y)
val sliderStart = if (isRtl) sliderRight else sliderLeft
val sliderEnd = if (isRtl) sliderLeft else sliderRight
drawLine(
@@ -490,18 +768,32 @@
): Float {
// target is a closest anchor to the `current`, if exists
return tickFractions
- .minByOrNull { abs(androidx.compose.ui.util.lerp(minPx, maxPx, it) - current) }
- ?.run { androidx.compose.ui.util.lerp(minPx, maxPx, this) }
+ .minByOrNull { abs(lerp(minPx, maxPx, it) - current) }
+ ?.run { lerp(minPx, maxPx, this) }
?: current
}
+@OptIn(ExperimentalComposeUiApi::class)
+private suspend fun AwaitPointerEventScope.awaitSlop(
+ id: PointerId,
+ type: PointerType
+): Pair<PointerInputChange, Float>? {
+ var initialDelta = 0f
+ val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
+ pointerInput.consume()
+ initialDelta = offset
+ }
+ val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
+ return if (afterSlopResult != null) afterSlopResult to initialDelta else null
+}
+
private fun stepsToTickFractions(steps: Int): List<Float> {
return if (steps == 0) emptyList() else List(steps + 2) { it.toFloat() / (steps + 1) }
}
// Scale x1 from a1..b1 range to a2..b2 range
private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
- androidx.compose.ui.util.lerp(a2, b2, calcFraction(a1, b1, x1))
+ lerp(a2, b2, calcFraction(a1, b1, x1))
// Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range
private fun scale(a1: Float, b1: Float, x: ClosedFloatingPointRange<Float>, a2: Float, b2: Float) =
@@ -515,14 +807,18 @@
private fun CorrectValueSideEffect(
scaleToOffset: (Float) -> Float,
valueRange: ClosedFloatingPointRange<Float>,
+ trackRange: ClosedFloatingPointRange<Float>,
valueState: MutableState<Float>,
value: Float
) {
SideEffect {
val error = (valueRange.endInclusive - valueRange.start) / 1000
val newOffset = scaleToOffset(value)
- if (abs(newOffset - valueState.value) > error)
- valueState.value = newOffset
+ if (abs(newOffset - valueState.value) > error) {
+ if (valueState.value in trackRange) {
+ valueState.value = newOffset
+ }
+ }
}
}
@@ -543,7 +839,7 @@
val resolvedValue = if (steps > 0) {
tickFractions
.map {
- androidx.compose.ui.util.lerp(
+ lerp(
valueRange.start,
valueRange.endInclusive,
it
@@ -566,47 +862,54 @@
}.progressSemantics(value, valueRange, steps)
}
-private fun Modifier.sliderPressModifier(
+private fun Modifier.sliderTapModifier(
draggableState: DraggableState,
interactionSource: MutableInteractionSource,
maxPx: Float,
isRtl: Boolean,
rawOffset: State<Float>,
gestureEndAction: State<(Float) -> Unit>,
+ pressOffset: MutableState<Float>,
enabled: Boolean
) = composed(
factory = {
if (enabled) {
- LaunchedEffect(draggableState, interactionSource, maxPx, isRtl) {
- // TODO (b/219761251): Use ModifierLocalScrollableContainer once it's public,
- // interaction sources should not be used for business logic
- interactionSource.interactions.collect { interaction ->
- when (interaction) {
- is PressInteraction.Press -> {
+ val scope = rememberCoroutineScope()
+ pointerInput(draggableState, interactionSource, maxPx, isRtl) {
+ detectTapGestures(
+ onPress = { pos ->
+ val to = if (isRtl) maxPx - pos.x else pos.x
+ pressOffset.value = to - rawOffset.value
+ try {
+ awaitRelease()
+ } catch (_: GestureCancellationException) {
+ pressOffset.value = 0f
+ }
+ },
+ onTap = {
+ scope.launch {
draggableState.drag(MutatePriority.UserInput) {
- val x = interaction.pressPosition.x
- val to = if (isRtl) maxPx - x else x
- dragBy(to - rawOffset.value)
+ // just trigger animation, press offset will be applied
+ dragBy(0f)
}
}
+ gestureEndAction.value.invoke(0f)
}
- }
- }
- Modifier.clickable(interactionSource = interactionSource, null) {
- gestureEndAction.value.invoke(0f)
+ )
}
} else {
this
}
},
inspectorInfo = debugInspectorInfo {
- name = "sliderPressModifier"
+ name = "sliderTapModifier"
properties["draggableState"] = draggableState
properties["interactionSource"] = interactionSource
properties["maxPx"] = maxPx
properties["isRtl"] = isRtl
properties["rawOffset"] = rawOffset
properties["gestureEndAction"] = gestureEndAction
+ properties["pressOffset"] = pressOffset
properties["enabled"] = enabled
})
@@ -625,6 +928,118 @@
}
}
+private fun Modifier.rangeSliderPressDragModifier(
+ startInteractionSource: MutableInteractionSource,
+ endInteractionSource: MutableInteractionSource,
+ rawOffsetStart: State<Float>,
+ rawOffsetEnd: State<Float>,
+ enabled: Boolean,
+ isRtl: Boolean,
+ maxPx: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ gestureEndAction: State<(Boolean) -> Unit>,
+ onDrag: State<(Boolean, Float) -> Unit>,
+): Modifier =
+ if (enabled) {
+ pointerInput(startInteractionSource, endInteractionSource, maxPx, isRtl, valueRange) {
+ val rangeSliderLogic = RangeSliderLogic(
+ startInteractionSource,
+ endInteractionSource,
+ rawOffsetStart,
+ rawOffsetEnd,
+ onDrag
+ )
+ coroutineScope {
+ forEachGesture {
+ awaitPointerEventScope {
+ val event = awaitFirstDown(requireUnconsumed = false)
+ val interaction = DragInteraction.Start()
+ var posX = if (isRtl) maxPx - event.position.x else event.position.x
+ val compare = rangeSliderLogic.compareOffsets(posX)
+ var draggingStart = if (compare != 0) {
+ compare < 0
+ } else {
+ rawOffsetStart.value > posX
+ }
+
+ awaitSlop(event.id, event.type)?.let {
+ val slop = viewConfiguration.pointerSlop(event.type)
+ val shouldUpdateCapturedThumb = abs(rawOffsetEnd.value - posX) < slop &&
+ abs(rawOffsetStart.value - posX) < slop
+ if (shouldUpdateCapturedThumb) {
+ val dir = it.second
+ draggingStart = if (isRtl) dir >= 0f else dir < 0f
+ posX += it.first.positionChange().x
+ }
+ }
+
+ rangeSliderLogic.captureThumb(
+ draggingStart,
+ posX,
+ interaction,
+ this@coroutineScope
+ )
+
+ val finishInteraction = try {
+ val success = horizontalDrag(pointerId = event.id) {
+ val deltaX = it.positionChange().x
+ onDrag.value.invoke(draggingStart, if (isRtl) -deltaX else deltaX)
+ }
+ if (success) {
+ DragInteraction.Stop(interaction)
+ } else {
+ DragInteraction.Cancel(interaction)
+ }
+ } catch (e: CancellationException) {
+ DragInteraction.Cancel(interaction)
+ }
+
+ gestureEndAction.value.invoke(draggingStart)
+ launch {
+ rangeSliderLogic
+ .activeInteraction(draggingStart)
+ .emit(finishInteraction)
+ }
+ }
+ }
+ }
+ }
+ } else {
+ this
+ }
+
+private class RangeSliderLogic(
+ val startInteractionSource: MutableInteractionSource,
+ val endInteractionSource: MutableInteractionSource,
+ val rawOffsetStart: State<Float>,
+ val rawOffsetEnd: State<Float>,
+ val onDrag: State<(Boolean, Float) -> Unit>,
+) {
+ fun activeInteraction(draggingStart: Boolean): MutableInteractionSource =
+ if (draggingStart) startInteractionSource else endInteractionSource
+
+ fun compareOffsets(eventX: Float): Int {
+ val diffStart = abs(rawOffsetStart.value - eventX)
+ val diffEnd = abs(rawOffsetEnd.value - eventX)
+ return diffStart.compareTo(diffEnd)
+ }
+
+ fun captureThumb(
+ draggingStart: Boolean,
+ posX: Float,
+ interaction: Interaction,
+ scope: CoroutineScope
+ ) {
+ onDrag.value.invoke(
+ draggingStart,
+ posX - if (draggingStart) rawOffsetStart.value else rawOffsetEnd.value
+ )
+ scope.launch {
+ activeInteraction(draggingStart).emit(interaction)
+ }
+ }
+}
+
@Immutable
private class DefaultSliderColors(
private val thumbColor: Color,
@@ -702,11 +1117,14 @@
}
// Internal to be referred to in tests
-internal val ThumbRippleMargin = 4.dp
+internal val ThumbWidth = SliderTokens.HandleWidth
+private val ThumbHeight = SliderTokens.HandleHeight
+private val ThumbSize = DpSize(ThumbWidth, ThumbHeight)
private val ThumbDefaultElevation = 1.dp
private val ThumbPressedElevation = 6.dp
// Internal to be referred to in tests
+internal val TrackHeight = 4.dp
private val SliderHeight = 48.dp
private val SliderMinWidth = 144.dp // TODO: clarify min width
private val DefaultSliderConstraints =
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index 8680682..25731d5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -28,6 +28,8 @@
val CloseSheet = Strings(2)
val DefaultErrorMessage = Strings(3)
val ExposedDropdownMenu = Strings(4)
+ val SliderRangeStart = Strings(5)
+ val SliderRangeEnd = Strings(6)
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 97dd92b..c8910bc 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -154,6 +154,7 @@
/**
* Contains the default values used by [TextField] and [OutlinedTextField].
*/
+@Immutable
object TextFieldDefaults {
/**
* The default min width applied for a [TextField] and [OutlinedTextField].
@@ -239,7 +240,7 @@
*/
@ExperimentalMaterial3Api
@Composable
- fun BorderStroke(
+ fun BorderBox(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource,
@@ -593,7 +594,7 @@
colors: TextFieldColors = outlinedTextFieldColors(),
contentPadding: PaddingValues = outlinedTextFieldPadding(),
border: @Composable () -> Unit = {
- BorderStroke(enabled, isError, interactionSource, colors)
+ BorderBox(enabled, isError, interactionSource, colors)
}
) {
CommonDecorationBox(
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index 8acb9f1..3e75629 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -25,6 +25,8 @@
Strings.CloseDrawer -> "Close navigation menu"
Strings.CloseSheet -> "Close sheet"
Strings.DefaultErrorMessage -> "Invalid input"
+ Strings.SliderRangeStart -> "Range Start"
+ Strings.SliderRangeEnd -> "Range End"
else -> ""
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
index 752ff77..9b7e52f 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
@@ -924,7 +924,7 @@
}
// Temporary handling of TextStyle: remove when TextStyle implements InspectableValue
- // Hide: paragraphStyle, spanStyle, platformStyle, lineHeightBehavior
+ // Hide: paragraphStyle, spanStyle, platformStyle, lineHeightStyle
private fun createFromTextStyle(name: String, value: TextStyle): NodeParameter? {
val parameter =
NodeParameter(name, ParameterType.String, TextStyle::class.java.simpleName)
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index 3f7fa36..f811a58 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -752,6 +752,159 @@
}
@Test
+ fun noErrors_inlineAndValueClasses() {
+ val inlineAndValueClassStub = compiledStub(
+ filename = "InlineAndValueClassStub.kt",
+ filepath = "androidx/compose/ui/foo",
+ checksum = 0x16c1f1c4,
+ """
+ package androidx.compose.ui.foo
+
+ import androidx.compose.ui.Modifier
+
+ inline class Inline(val value: Float)
+
+ @JvmInline
+ value class Value(val value: Float)
+
+ private object TestModifier : Modifier.Element
+
+ fun Modifier.inline(inline: Inline = Inline(1f)): Modifier {
+ return this.then(TestModifier)
+ }
+
+ fun Modifier.value(value: Value = Value(1f)): Modifier {
+ return this.then(TestModifier)
+ }
+ """,
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAAAGNgYGBmYGBgBGI2BijgsuEST8xLKcrPTKnQS87PLcgvTtUr
+ zdRLy88XkvTMy8nMS3XMSwlLzClNdc5JLC4OLilN8i4RYgtJLS7xLlFi0GIA
+ APJuU+dWAAAA
+ """,
+ """
+ androidx/compose/ui/foo/Inline.class:
+ H4sIAAAAAAAAAIVU3VMbVRT/3ZuvzbLAkvIVrJVCLQkfDdBqVVpaQGKDoShU
+ LMWvJVlgIdnF7IbhkfFF/wIffPSZ6eiMAmNHB+mbf5PjeO7dDWDI1Jlk7z3n
+ no/f+Z1z71///PYHgDvYZLhm2MWKYxX3MgWnvOO4ZqZqZdYdJ5OzS5ZtxsAY
+ 9C1j18iUDHsjs7C2ZRa8GEIMyobpLRulqskQSqWzDJFdX2JZDTEocXDEGcLe
+ puUy9OZfnWiCodlzlryKZW+MWOWdEkNHKpvOn6f2z8iuq143XbVKRbMSQytD
+ 9J5lW96kxLSsoQ0JFTquUHQ/T0qCvK+gg2yNnR3TLjKMpC7nuZw6SDOhoQvd
+ ImqS4WojjBcNXxOGV4XhzKsNrwnDN4jWGgsM7akG9Wu4jj5h20/cGpWNUQ3N
+ aFGJ7JtU5Kbhbs44RTOgMEzwcgyt51GyJceg/g1SopqthmGkVQxhRLKW05AS
+ MscoQ5P5ddUouUG4zlQ2Xz8LE+lnDGrVXnP2pJVGcxUV3m8xxCTZC+uilP/w
+ JFFQKXfxjkj8Ls3htuNRfzJbu+WMZXtmxTZKNBqiZtcquDFQ4xWjYs4KPAwD
+ qctAGkHTcB+TKu7hAc2n422aFYbEZUOaBb9QMXWN44zjtqhq1id1WUVYTJVe
+ cGzXq1QLnlO5wDndBqVGCEO/KP5/xl+M6gciwTxdoF0G7QLx1IdIKpsVRPOd
+ MfEZp17kLzA2t1s+u0ZttYN50zOKhmeQjpd3Q3TjmfhExQeUZZv0e5aQKAEv
+ UuDfT/YHVd7NVa6f7Kv043pM5UqE1iZaw7QqtIZobaGVK6ffPuw+2R/no2w6
+ kYjqvIePhl4es5P90x+jYSWsR+Z6dIWU8XFFV3vC3WyUPXr5fUieNunanK43
+ 02kL6ZjUteo66dpIlzjTXdHbF9v80CQrBKsnrET12Ol3jPu5vuGETEmKIogY
+ Kq3HJ2PKLsoHaqZkuO6SV127te1Rf8TM053Ik8XjannNrDwx1kqmmAunYJSW
+ jYol5EDZvOQZhe15YyeQ1SWnWimYWUsIycWq7Vllc9lyLTqdsm3HMzyLRgJj
+ 1Mqw5DkhHkPaiQ5HECXNGkkZ0QFaI4O/QH1OG46CaI5URlGUDtIATbQD4uKa
+ B853yVqcJV9AXzlCe6LzED19h3hdTx+i9xA3fpKZz4Mk8abEwMTjEQS5GSBQ
+ BIJjDNT7KGeJ6TkIfPprqPuOceugziFylmRYltcgyVi9z3kSul+Bz1OqTkxl
+ /9Cf4D8gEjoYOgE/xNuzlPW9G/Q/xkOpDx9I4kz6xsDjf6OLy9idpBRgfTxi
+ N4VpiWQG7wdZRIOEVVwgGzpG9hya7x4PoImddNe5uKOB+2Tgrg4e4dFg/69Q
+ f27YRD+WehZLldPAKGYOc0Gs3oAk3ve8jh7uz46exIfIB9YDRI44i78AX+k7
+ wuP6xsWxIJ3axEtc37jauLEGI5bER/g4cLgT1KcJzvt9zusZ0rAYEKxhSVbF
+ sS5RG9ig1abdE1o/oQzLqwjl8GkOT3NYwTPaYjWHz/D5KpiLL/DlKjpcaC6+
+ chGT35yLORcRF1EXM1Iz5WLcxW0Xw1JMuUi7uC73zS5a/gWm4bi81wgAAA==
+ """,
+ """
+ androidx/compose/ui/foo/InlineAndValueClassStubKt.class:
+ H4sIAAAAAAAAAIVU31PbRhD+Tv4lZEMMJCQQIElxE+zEkSH0p9sk1IlTJcbt
+ xAwvPHTOsoDDssRIJ0/60mH6X/Sx/Qs6faJ96HjoW/+oTlayQ2qC8YP2dve+
+ 29v9dk///vfX3wA28JJhjTstzxWtN7rpdo5c39IDoe+5rm44tnCsTae1w+3A
+ qtjc9xsyaL6SKTCG7CHvct3mzr7+XfPQMskbY5gS0aFiacP5Yeuly/BktXZR
+ /C23JfaE5ZWr+cv3GVZqrrevH1qy6XHh+Dp3HFdyKVzS666sB7ZNqExOHgg/
+ 179ehcqw3HYlGfpht6MLR1qew22qSXoURJh+ChrDNfPAMtuDKN9zj3csAjLc
+ W62dL6/8P08jDLJfzu9kkMGkhjSmGHKjeNy2fPmunhSyDKpRb2xv1ivPGe5e
+ WP35U+UMZjA7gWlcZVi8jK8U5hji8sByGB6PoX4M8xncwHwa17FAXI5rUrJP
+ PAOrMswND0GuZe3xwJYMr8cNg/Eh7WPnY/ny+U3hDg2rSdMivcCUrlcUnSOb
+ SFqt5qsZrCCn4SN8nEECSQ0K7jFMdsOBL64VxSteNRnS/dmKvCoKDIlIpfEZ
+ Ar6vc2lUStFLSuGhBj28Mt+/co1hujaY1i1L8haXnApTOt0YPVIWimQoQPS2
+ Q0WhzTci1Eqktej8j73jq1rvWFNuKP1PjUVr73jhTpaEUmIF+tZVVembyj8n
+ rHdMgp3+loyrsWycgIkhXGglh2GpbPz0ZyVN4ee1hHr663KJhQmsM9weOcj9
+ NlA9t0ZCIloIsTDil/OwTaTGK26LOL9SI0Q96DQtb5s3bfLM1FyT2zvcE6E9
+ cN58HThSdCzD6QpfkGvz/W+D3ur53bO3PwSbbEhutrf40SCo1nADz7SqIjTm
+ BzF2PoiPNWpqPGwYyflwsBDDN2S9IL9C62xhZuIEVwp/4FoP1//ETQW/h21F
+ JWw1tTmJKTwjfa4Ph4rFKNwslrBM+88HuBStVfomlIGB7ARu4TZZ4X3rgzym
+ F+M//QI1c4K7zworJ1jt3/aCZAwsfXYtMEn55i/K9/6YfGeG8i2c5ftgfL7F
+ S/ItRfmuj8x3mtzfRpubMGitkPcRMb6xi5iBTwx8auAzfG7gC3xpoIyvdsF8
+ fI3Hu1B9LPlY9PHER8JH0scDH0995N8CsSzNZB0HAAA=
+ """,
+ """
+ androidx/compose/ui/foo/TestModifier.class:
+ H4sIAAAAAAAAAKVUW0/UQBg9Mwu7pRa5eOGmeGFFLkqB+AaSIGLSZFmNkE0M
+ T7PbAQfaDmmnhEfiT/EXSHzAaGI2+uaPMn4tCwlykcSH+W5zzrffnJnur99f
+ vwN4hmmGsoj8WCt/z23ocEcn0k2Vu6G1uyYTs6J9taFkXAJj6N4Su8INRLTp
+ vq5vyYYpoXAB/5hXXg5kKCNCtjMU51WkzAJDYWy85qAEy0YbOhjazHuVMIxW
+ rjLKXI6XEcPC2LmEE+D45dsMIxUdb7pb0tRjoaLEFVGkjTBKU1zVppoGAaFm
+ rnLA8ku5IdLAeOFOkJTQzeBfPt0xce6/zuCgE702enCDoV2TLDHD8L+Obc03
+ gvwmbPBMfsurrq4tVpeWHQzA6aDiIENPZVsbgrkr0ghfGEFEHu4W6NmwzBQz
+ Awa2TfU9lWX0mLg/Q7I2922b9/N8Nfetnx94f3N/lk+zFyWL//hY5N08g85e
+ dOVnNGIY9CIaRi5Gfk0EqVwKRJKsmrQ+tW0Yht6mkVGh9KJdlah6QLCTi6TX
+ sqR9ydBVIX41DesyXhOEYeit6IYIaiJWWd4qlv/u9UbEIpRGxqea2qs6jRvy
+ lco4Ay1O7cyvY5rUbCOZirQGMnnJj2fyke8iX6B9+hAom6DMzQQl3z5xCPuA
+ Ao7JFhi4hidknSMAZU6ufyeuU5OM/JzQnHzHRBPFyS+4+encBrePQK0GWXSL
+ aqenekqrxFqJhb6TAftyMrX6Bv7uEP2fMXSQFzimcjtGZ8j+WBju0JB311Hw
+ MOzhnof7eEAhHnoYQXkdLMEjjNJ+AifB4wTWH5GWtQCVBAAA
+ """,
+ """
+ androidx/compose/ui/foo/Value.class:
+ H4sIAAAAAAAAAH1UW1MjRRT+unObDAMMWW6Jew3rEm4bYFdXZRcXkLhBWBRW
+ XBZvk2SAgWQmZiYpHilf9Bf44KPP1JZWKVBuaSH75m+yLE93hoshRVXS3ef0
+ uXznO6fn739//xPAfWwyXDPsQsWxCjvpvFMqO66ZrlrpdcdJrxjFqhkBY9C3
+ jJqRLhr2Rnoxt2XmvQgCDMqG6UkbhkBqIMMQqtUlltEQgRIFR5Qh6G1aLsON
+ +UvzTDC0es6yV7HsjRGrVC4ydKUyA/Nnmet3ZNfTqJuuWsWCWYmgnSH80LIt
+ b1JCWtHQgZgKHVcYNJkmJSE+UtBFpka5bNoFhpHUxTQXM/tZJjT0oFcEjTNc
+ bQbxvOEbwvCqMJy53PC6MLxBpJ6QwNCZalK+hltICts+YtaobIxqaEWbSlTf
+ IQY3DXdzximYPoNBgpdlaD+Lkik6BnVvkBKd2GoYxoCKIYxI0rIaUkLmGGVo
+ Mb+pGkXXD9edysw3TsLEwAsGtWrnnB1ppdFQhYX3WwwRSfbiuijlfzxJFFTK
+ A7wjEr/LcH3b8YqWnd6qldKW7ZkV2yims7ao2bXybgTUd8WomLMCD0N/6iKQ
+ ZtA0PMKkiod4n6bT8TbNCkPsoiHNQr1QMXTN44zjnqhqtk7qioqgGCo979iu
+ V6nmPadyjnN6C8oJIQxJUfzlwy8G9UMRf4FeT42G9Rzv1IZQKpMRPPPymFjG
+ qRXz5wibq5WyNgniEXWcXCyYnlEwPIN0vFQL0GtnYgmLBZRlm/Q7lpAoAS9Q
+ 4D+OdgdV3stVrh/tqvTjekTlSoj2FtqDtCu0B2hvo50rx9897j3aHeejbDoW
+ C+s8wUcDrw/Z0e7xT+GgEtRDcwldIWV0XNHVRLCXjbInr38IyNsWXZvT9Va6
+ bSMdk7p2XSddB+lip7oreudSRz00yQrBSgSVsB45/p7xeq5vOSFT4qIIIoZK
+ S9TJmLILktyZouG6y141d3fbo/aIkacnMU8WT6ulnFl5ZuSKphgLJ28UV4yK
+ JWRf2brsGfntBaPsy+qyU63kzYwlhPhS1faskrliuRbdTtm24xmeRROBMWpl
+ UPIcE19COokOhxAmTY6ktOgA7aHBX6G+pANHXjRHKqMoSAdpgBY6CR29ct/5
+ AVmLu/gr6KsH6Ix17yOR3Mc1fWAfN/dx+2eZ+SxIHG9KDEx8O/wgd3wEikBw
+ iP5GH+U0MX0NfJ++E9TJQ9zda3AInSYZluU1STLW6HOWhJ6X7/OcqhNT2Tf0
+ F/iPCAX2ho7A9/H2LGV97zb9D/FY6oN7kjiT1gh49B/0cBm7m5QCbB2POE1h
+ WiKZwQd+FtEgYRUVyIYOkTmDVneP+tDESbrrXLxR333Sd1cHD/BksO83qL80
+ bWI9lnoaS5XTwOg2izk/1k2fJJ582UAPr8+OHsdHmPet+4kcGf8V+GryAE8b
+ GxfFonTqEB/ixsadjBtrMmJxfIxPfIf7fn2a4LyvznkjQxqWfII1LMuqONYl
+ agMbtNt0ekb7p5RhZQ2BLD7L4nkWq3hBR6xl8Tm+WANz8SW+WkOXC83F1y4i
+ cs26mHMRchF2MSM1Uy7GXdxzMSzFlIsBF7fkudVF239hftWt0wgAAA==
+ """
+ )
+
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.ui.foo
+
+ import androidx.compose.ui.Modifier
+
+ fun Modifier.inlineModifier(): Modifier = inline()
+
+ fun Modifier.valueModifier(): Modifier = value()
+ """
+ ),
+ Stubs.Modifier,
+ inlineAndValueClassStub
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
fun noErrors() {
lint().files(
kotlin(
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 5b8a3fa..23a9c3a9 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -537,7 +537,7 @@
package androidx.compose.ui.text.android.style {
- public final class LineHeightBehaviorSpanKt {
+ public final class LineHeightStyleSpanKt {
}
public final class PlaceholderSpanKt {
@@ -1260,6 +1260,9 @@
property public final int Rtl;
}
+ public final class TextDrawStyleKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextGeometricTransform {
ctor public TextGeometricTransform(optional float scaleX, optional float skewX);
method public androidx.compose.ui.text.style.TextGeometricTransform copy(optional float scaleX, optional float skewX);
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 4c04cc8..25beabf 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -131,6 +131,7 @@
method public long getWordBoundary(int offset);
method public boolean isLineEllipsized(int lineIndex);
method public void paint(androidx.compose.ui.graphics.Canvas canvas, optional long color, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextDecoration? decoration);
+ method @androidx.compose.ui.text.ExperimentalTextApi public void paint(androidx.compose.ui.graphics.Canvas canvas, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextDecoration? decoration);
property public final boolean didExceedMaxLines;
property public final float firstBaseline;
property public final float height;
@@ -194,6 +195,7 @@
method public long getWordBoundary(int offset);
method public boolean isLineEllipsized(int lineIndex);
method public void paint(androidx.compose.ui.graphics.Canvas canvas, optional long color, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextDecoration? textDecoration);
+ method @androidx.compose.ui.text.ExperimentalTextApi public default void paint(androidx.compose.ui.graphics.Canvas canvas, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextDecoration? textDecoration);
property public abstract boolean didExceedMaxLines;
property public abstract float firstBaseline;
property public abstract float height;
@@ -228,13 +230,13 @@
}
@androidx.compose.runtime.Immutable public final class ParagraphStyle {
- ctor @androidx.compose.ui.text.ExperimentalTextApi public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior);
+ ctor @androidx.compose.ui.text.ExperimentalTextApi public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
- method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior);
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
method public operator boolean equals(Object? other);
method public long getLineHeight();
- method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineHeightBehavior? getLineHeightBehavior();
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
method public androidx.compose.ui.text.style.TextDirection? getTextDirection();
@@ -242,7 +244,7 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
property public final long lineHeight;
- property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
property public final androidx.compose.ui.text.style.TextAlign? textAlign;
property public final androidx.compose.ui.text.style.TextDirection? textDirection;
@@ -323,13 +325,16 @@
}
@androidx.compose.runtime.Immutable public final class SpanStyle {
- ctor @androidx.compose.ui.text.ExperimentalTextApi public SpanStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.PlatformSpanStyle? platformStyle);
ctor public SpanStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow);
+ ctor @androidx.compose.ui.text.ExperimentalTextApi public SpanStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.PlatformSpanStyle? platformStyle);
+ ctor @androidx.compose.ui.text.ExperimentalTextApi public SpanStyle(androidx.compose.ui.graphics.Brush? brush, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.PlatformSpanStyle? platformStyle);
method public androidx.compose.ui.text.SpanStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow);
method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.SpanStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.PlatformSpanStyle? platformStyle);
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.SpanStyle copy(androidx.compose.ui.graphics.Brush? brush, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.PlatformSpanStyle? platformStyle);
method public operator boolean equals(Object? other);
method public long getBackground();
method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.graphics.Brush? getBrush();
method public long getColor();
method public androidx.compose.ui.text.font.FontFamily? getFontFamily();
method public String? getFontFeatureSettings();
@@ -347,6 +352,7 @@
method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.SpanStyle plus(androidx.compose.ui.text.SpanStyle other);
property public final long background;
property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
property public final long color;
property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
property public final String? fontFeatureSettings;
@@ -490,12 +496,15 @@
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior);
ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+ ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+ ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
- method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior);
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
method public long getBackground();
method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.graphics.Brush? getBrush();
method public long getColor();
method public androidx.compose.ui.text.font.FontFamily? getFontFamily();
method public String? getFontFeatureSettings();
@@ -505,7 +514,7 @@
method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
method public long getLetterSpacing();
method public long getLineHeight();
- method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineHeightBehavior? getLineHeightBehavior();
+ method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.PlatformTextStyle? getPlatformStyle();
method public androidx.compose.ui.graphics.Shadow? getShadow();
@@ -525,6 +534,7 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.SpanStyle toSpanStyle();
property public final long background;
property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
property public final long color;
property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
property public final String? fontFeatureSettings;
@@ -534,7 +544,7 @@
property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
property public final long letterSpacing;
property public final long lineHeight;
- property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineHeightBehavior? lineHeightBehavior;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
property public final androidx.compose.ui.text.intl.LocaleList? localeList;
property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.PlatformTextStyle? platformStyle;
property public final androidx.compose.ui.graphics.Shadow? shadow;
@@ -600,7 +610,7 @@
package androidx.compose.ui.text.android.style {
- public final class LineHeightBehaviorSpanKt {
+ public final class LineHeightStyleSpanKt {
}
public final class PlaceholderSpanKt {
@@ -1280,40 +1290,20 @@
method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
}
- @androidx.compose.ui.text.ExperimentalTextApi public final class LineHeightBehavior {
- ctor public LineHeightBehavior(optional int alignment, optional int trim);
+ @androidx.compose.ui.text.ExperimentalTextApi public final class LineHeightStyle {
+ ctor public LineHeightStyle(int alignment, int trim);
method public int getAlignment();
method public int getTrim();
property public final int alignment;
property public final int trim;
- field public static final androidx.compose.ui.text.style.LineHeightBehavior.Companion Companion;
+ field public static final androidx.compose.ui.text.style.LineHeightStyle.Companion Companion;
}
- public static final class LineHeightBehavior.Companion {
- method public androidx.compose.ui.text.style.LineHeightBehavior getDefault();
- property public final androidx.compose.ui.text.style.LineHeightBehavior Default;
+ @androidx.compose.ui.text.ExperimentalTextApi @kotlin.jvm.JvmInline public static final value class LineHeightStyle.Alignment {
+ field public static final androidx.compose.ui.text.style.LineHeightStyle.Alignment.Companion Companion;
}
- @androidx.compose.ui.text.ExperimentalTextApi @kotlin.jvm.JvmInline public final value class LineHeightTrim {
- field public static final androidx.compose.ui.text.style.LineHeightTrim.Companion Companion;
- }
-
- public static final class LineHeightTrim.Companion {
- method public int getBoth();
- method public int getFirstLineTop();
- method public int getLastLineBottom();
- method public int getNone();
- property public final int Both;
- property public final int FirstLineTop;
- property public final int LastLineBottom;
- property public final int None;
- }
-
- @androidx.compose.ui.text.ExperimentalTextApi @kotlin.jvm.JvmInline public final value class LineVerticalAlignment {
- field public static final androidx.compose.ui.text.style.LineVerticalAlignment.Companion Companion;
- }
-
- public static final class LineVerticalAlignment.Companion {
+ public static final class LineHeightStyle.Alignment.Companion {
method public int getBottom();
method public int getCenter();
method public int getProportional();
@@ -1324,6 +1314,26 @@
property public final int Top;
}
+ public static final class LineHeightStyle.Companion {
+ method public androidx.compose.ui.text.style.LineHeightStyle getDefault();
+ property public final androidx.compose.ui.text.style.LineHeightStyle Default;
+ }
+
+ @androidx.compose.ui.text.ExperimentalTextApi @kotlin.jvm.JvmInline public static final value class LineHeightStyle.Trim {
+ field public static final androidx.compose.ui.text.style.LineHeightStyle.Trim.Companion Companion;
+ }
+
+ public static final class LineHeightStyle.Trim.Companion {
+ method public int getBoth();
+ method public int getFirstLineTop();
+ method public int getLastLineBottom();
+ method public int getNone();
+ property public final int Both;
+ property public final int FirstLineTop;
+ property public final int LastLineBottom;
+ property public final int None;
+ }
+
public enum ResolvedTextDirection {
enum_constant public static final androidx.compose.ui.text.style.ResolvedTextDirection Ltr;
enum_constant public static final androidx.compose.ui.text.style.ResolvedTextDirection Rtl;
@@ -1385,6 +1395,9 @@
property public final int Rtl;
}
+ public final class TextDrawStyleKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextGeometricTransform {
ctor public TextGeometricTransform(optional float scaleX, optional float skewX);
method public androidx.compose.ui.text.style.TextGeometricTransform copy(optional float scaleX, optional float skewX);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 5b8a3fa..23a9c3a9 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -537,7 +537,7 @@
package androidx.compose.ui.text.android.style {
- public final class LineHeightBehaviorSpanKt {
+ public final class LineHeightStyleSpanKt {
}
public final class PlaceholderSpanKt {
@@ -1260,6 +1260,9 @@
property public final int Rtl;
}
+ public final class TextDrawStyleKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextGeometricTransform {
ctor public TextGeometricTransform(optional float scaleX, optional float skewX);
method public androidx.compose.ui.text.style.TextGeometricTransform copy(optional float scaleX, optional float skewX);
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
index 4aaf612..ea15b16 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
@@ -34,9 +34,9 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import androidx.compose.ui.text.matchers.assertThat
-import androidx.compose.ui.text.style.LineHeightBehavior
-import androidx.compose.ui.text.style.LineHeightTrim
-import androidx.compose.ui.text.style.LineVerticalAlignment
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.unit.Constraints
import org.junit.Test
import org.junit.runner.RunWith
@@ -191,9 +191,9 @@
platformStyle = @Suppress("DEPRECATION") PlatformTextStyle(
includeFontPadding = false
),
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Proportional,
- trim = LineHeightTrim.None
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Proportional,
+ trim = Trim.None
)
),
)
@@ -220,9 +220,9 @@
text,
style = TextStyle(
lineHeight = lineHeight,
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Proportional,
- trim = LineHeightTrim.None
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Proportional,
+ trim = Trim.None
),
platformStyle = @Suppress("DEPRECATION") PlatformTextStyle(
includeFontPadding = false
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightBehaviorTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
similarity index 88%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightBehaviorTest.kt
rename to compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
index e7ce825..6b4b6d0 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightBehaviorTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
@@ -20,9 +20,9 @@
import androidx.compose.ui.text.android.style.lineHeight
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.platform.AndroidParagraph
-import androidx.compose.ui.text.style.LineHeightBehavior
-import androidx.compose.ui.text.style.LineHeightTrim
-import androidx.compose.ui.text.style.LineVerticalAlignment
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -38,7 +38,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
@OptIn(ExperimentalTextApi::class)
-class ParagraphIntegrationLineHeightBehaviorTest {
+class ParagraphIntegrationLineHeightStyleTest {
private val fontFamilyMeasureFont = FontTestData.BASIC_MEASURE_FONT.toFontFamily()
private val context = InstrumentationRegistry.getInstrumentation().context
private val defaultDensity = Density(density = 1f)
@@ -52,8 +52,8 @@
@Test
fun singleLine_even_trim_None() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -71,8 +71,8 @@
@Test
fun singleLine_even_trim_LastLineBottom() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -90,8 +90,8 @@
@Test
fun singleLine_even_trim_FirstLineTop() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -109,8 +109,8 @@
@Test
fun singleLine_even_trim_Both() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -129,8 +129,8 @@
@Test
fun singleLine_top_trim_None() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -148,8 +148,8 @@
@Test
fun singleLine_top_trim_LastLineBottom() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -165,8 +165,8 @@
@Test
fun singleLine_top_trim_FirstLineTop() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -184,8 +184,8 @@
@Test
fun singleLine_top_trim_Both() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -204,8 +204,8 @@
@Test
fun singleLine_bottom_trim_None() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -223,8 +223,8 @@
@Test
fun singleLine_bottom_trim_LastLineBottom() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -240,8 +240,8 @@
@Test
fun singleLine_bottom_trim_FirstLineTop() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -259,8 +259,8 @@
@Test
fun singleLine_bottom_trim_Both() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -279,8 +279,8 @@
@Test
fun singleLine_proportional_trim_None() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -299,8 +299,8 @@
@Test
fun singleLine_proportional_trim_LastLineBottom() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -319,8 +319,8 @@
@Test
fun singleLine_proportional_trim_FirstLineTop() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -339,8 +339,8 @@
@Test
fun singleLine_proportional_trim_Both() {
val paragraph = singleLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -359,8 +359,8 @@
@Test
fun multiLine_even_trim_None() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -383,8 +383,8 @@
@Test
fun multiLine_even_trim_LastLineBottom() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -413,8 +413,8 @@
@Test
fun multiLine_even_trim_FirstLineTop() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -443,8 +443,8 @@
@Test
fun multiLine_even_trim_Both() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Center
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Center
)
val defaultFontMetrics = defaultFontMetrics()
@@ -475,8 +475,8 @@
@Test
fun multiLine_top_trim_None() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -505,8 +505,8 @@
@Test
fun multiLine_top_trim_LastLineBottom() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -535,8 +535,8 @@
@Test
fun multiLine_top_trim_FirstLineTop() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -565,8 +565,8 @@
@Test
fun multiLine_top_trim_Both() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Top
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Top
)
val defaultFontMetrics = defaultFontMetrics()
@@ -597,8 +597,8 @@
@Test
fun multiLine_bottom_trim_None() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -627,8 +627,8 @@
@Test
fun multiLine_bottom_trim_LastLineBottom() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -657,8 +657,8 @@
@Test
fun multiLine_bottom_trim_FirstLineTop() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -687,8 +687,8 @@
@Test
fun multiLine_bottom_trim_Both() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Bottom
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Bottom
)
val defaultFontMetrics = defaultFontMetrics()
@@ -719,8 +719,8 @@
@Test
fun multiLine_proportional_trim_None() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.None,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.None,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -750,8 +750,8 @@
@Test
fun multiLine_proportional_trim_LastLineBottom() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.LastLineBottom,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.LastLineBottom,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -781,8 +781,8 @@
@Test
fun multiLine_proportional_trim_FirstLineTop() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.FirstLineTop,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.FirstLineTop,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -812,8 +812,8 @@
@Test
fun multiLine_proportional_trim_Both() {
val paragraph = multiLineParagraph(
- lineHeightTrim = LineHeightTrim.Both,
- distribution = LineVerticalAlignment.Proportional
+ lineHeightTrim = Trim.Both,
+ lineHeightAlignment = Alignment.Proportional
)
val defaultFontMetrics = defaultFontMetrics()
@@ -841,14 +841,14 @@
}
private fun singleLineParagraph(
- lineHeightTrim: LineHeightTrim,
- distribution: LineVerticalAlignment,
+ lineHeightTrim: Trim,
+ lineHeightAlignment: Alignment,
): AndroidParagraph {
val text = "AAA"
val textStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
+ lineHeightStyle = LineHeightStyle(
trim = lineHeightTrim,
- alignment = distribution
+ alignment = lineHeightAlignment
)
)
@@ -865,17 +865,17 @@
@Suppress("DEPRECATION")
private fun multiLineParagraph(
- lineHeightTrim: LineHeightTrim,
- distribution: LineVerticalAlignment,
+ lineHeightTrim: Trim,
+ lineHeightAlignment: Alignment,
): AndroidParagraph {
val lineCount = 3
val word = "AAA"
val text = "AAA".repeat(lineCount)
val textStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
+ lineHeightStyle = LineHeightStyle(
trim = lineHeightTrim,
- alignment = distribution
+ alignment = lineHeightAlignment
),
platformStyle = @Suppress("DEPRECATION") PlatformTextStyle(
includeFontPadding = false
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index cf1cde7..d6bdd0df 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -17,6 +17,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
@@ -24,6 +25,7 @@
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathOperation
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.text.FontTestData.Companion.BASIC_KERN_FONT
import androidx.compose.ui.text.FontTestData.Companion.BASIC_MEASURE_FONT
@@ -3547,6 +3549,36 @@
}
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testDefaultSpanStyle_setBrush() {
+ with(defaultDensity) {
+ val text = "abc"
+ // FontSize doesn't matter here, but it should be big enough for bitmap comparison.
+ val fontSize = 100.sp
+ val fontSizeInPx = fontSize.toPx()
+ val paragraphWidth = fontSizeInPx * text.length
+
+ val paragraphWithoutBrush = simpleParagraph(
+ text = text,
+ style = TextStyle(fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ val paragraphWithBrush = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraphWithBrush.bitmap())
+ .isNotEqualToBitmap(paragraphWithoutBrush.bitmap())
+ }
+ }
+
@Test
fun testGetPathForRange_singleLine() {
with(defaultDensity) {
@@ -4077,6 +4109,113 @@
)
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testSolidBrushColorIsSameAsColor() {
+ with(defaultDensity) {
+ val text = "abc"
+ // FontSize doesn't matter here, but it should be big enough for bitmap comparison.
+ val fontSize = 100.sp
+ val fontSizeInPx = fontSize.toPx()
+ val paragraphWidth = fontSizeInPx * text.length
+
+ val paragraphWithColor = simpleParagraph(
+ text = text,
+ style = TextStyle(color = Color.Red, fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ val paragraphWithSolidColor = simpleParagraph(
+ text = text,
+ style = TextStyle(brush = SolidColor(Color.Red), fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraphWithColor.bitmap())
+ .isEqualToBitmap(paragraphWithSolidColor.bitmap())
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testSpanBrush_overridesDefaultBrush() {
+ with(defaultDensity) {
+ val text = "abc"
+ // FontSize doesn't matter here, but it should be big enough for bitmap comparison.
+ val fontSize = 100.sp
+ val fontSizeInPx = fontSize.toPx()
+ val paragraphWidth = fontSizeInPx * text.length
+
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ),
+ width = paragraphWidth
+ )
+
+ val paragraphWithSpan = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ),
+ spanStyles = listOf(
+ AnnotatedString.Range(
+ item = SpanStyle(
+ brush = Brush.linearGradient(listOf(Color.Yellow, Color.Green))
+ ),
+ start = 0,
+ end = text.length
+ )
+ ),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraph.bitmap())
+ .isNotEqualToBitmap(paragraphWithSpan.bitmap())
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testBrush_notEffectedBy_TextDirection() {
+ with(defaultDensity) {
+ val ltrText = "aaa"
+ val rtlText = "\u05D0\u05D0\u05D0"
+ // FontSize doesn't matter here, but it should be big enough for bitmap comparison.
+ val fontSize = 100.sp
+ val fontSizeInPx = fontSize.toPx()
+ val paragraphWidth = fontSizeInPx * ltrText.length
+
+ val ltrParagraph = simpleParagraph(
+ text = ltrText,
+ style = TextStyle(
+ fontSize = fontSize,
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ),
+ width = paragraphWidth
+ )
+
+ val rtlParagraph = simpleParagraph(
+ text = rtlText,
+ style = TextStyle(
+ fontSize = fontSize,
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ),
+ width = paragraphWidth
+ )
+
+ assertThat(ltrParagraph.bitmap())
+ .isNotEqualToBitmap(rtlParagraph.bitmap())
+
+ // Color on the same pixel should be the same since they used the same brush.
+ assertThat(ltrParagraph.bitmap().getPixel(50, 50))
+ .isEqualTo(rtlParagraph.bitmap().getPixel(50, 50))
+ }
+ }
+
private fun simpleParagraph(
text: String = "",
style: TextStyle? = null,
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
index 55bd1cb..0dfb538 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.text
+import androidx.compose.ui.text.android.style.ceilToInt
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -503,12 +504,38 @@
assertThat(placeholderRects[1]).isNull()
}
+ @Test
+ fun placeHolderRects_withLimitedHeight_ellipsized() {
+ val text = "ABC"
+ val fontSize = 20f
+
+ val placeholder = Placeholder(1.em, 1.em, PlaceholderVerticalAlign.TextCenter)
+ val placeholders = listOf(
+ AnnotatedString.Range(placeholder, 0, 1),
+ AnnotatedString.Range(placeholder, 2, 3)
+ )
+ val paragraph = simpleParagraph(
+ text = text,
+ placeholders = placeholders,
+ fontSize = fontSize.sp,
+ width = 2 * fontSize,
+ height = 1.3f * fontSize,
+ ellipsis = true
+ )
+ val placeholderRects = paragraph.placeholderRects
+ assertThat(placeholderRects.size).isEqualTo(placeholders.size)
+ assertThat(placeholderRects[0]).isNotNull()
+ // The second placeholder should be ellipsized.
+ assertThat(placeholderRects[1]).isNull()
+ }
+
private fun simpleParagraph(
text: String = "",
fontSize: TextUnit = TextUnit.Unspecified,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
width: Float = Float.MAX_VALUE,
+ height: Float = Float.MAX_VALUE,
maxLines: Int = Int.MAX_VALUE,
ellipsis: Boolean = false
): Paragraph {
@@ -522,7 +549,7 @@
placeholders = placeholders,
maxLines = maxLines,
ellipsis = ellipsis,
- constraints = Constraints(maxWidth = width.ceilToInt()),
+ constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphTest.kt
index cb51bd5..c2d19bd 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphTest.kt
@@ -11,13 +11,17 @@
import android.text.style.RelativeSizeSpan
import android.text.style.ScaleXSpan
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.FontTestData.Companion.BASIC_MEASURE_FONT
+import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.UncachedFontFamilyResolver
@@ -30,6 +34,7 @@
import androidx.compose.ui.text.android.style.ShadowSpan
import androidx.compose.ui.text.android.style.SkewXSpan
import androidx.compose.ui.text.android.style.TextDecorationSpan
+import androidx.compose.ui.text.ceilToInt
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -38,9 +43,8 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.platform.style.ShaderBrushSpan
import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.PlatformTextStyle
-import androidx.compose.ui.text.ceilToInt
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextGeometricTransform
@@ -55,10 +59,10 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.math.ceil
import kotlin.math.roundToInt
+import org.junit.Test
+import org.junit.runner.RunWith
@OptIn(InternalPlatformTextApi::class)
@RunWith(AndroidJUnit4::class)
@@ -153,6 +157,80 @@
assertThat(paragraph.charSequence).hasSpanOnTop(ForegroundColorSpan::class, 0, "abc".length)
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testAnnotatedString_setBrushOnWholeText() {
+ val text = "abcde"
+ val brush = Brush.linearGradient(listOf(Color.Black, Color.White))
+ val spanStyle = SpanStyle(brush = brush)
+
+ val paragraph = simpleParagraph(
+ text = text,
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
+ width = 100.0f
+ )
+
+ assertThat(paragraph.charSequence).hasSpan(ShaderBrushSpan::class, 0, text.length) {
+ it.shaderBrush == brush
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testAnnotatedString_setSolidColorBrushOnWholeText() {
+ val text = "abcde"
+ val brush = SolidColor(Color.Red)
+ val spanStyle = SpanStyle(brush = brush)
+
+ val paragraph = simpleParagraph(
+ text = text,
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
+ width = 100.0f
+ )
+
+ assertThat(paragraph.charSequence).hasSpan(ForegroundColorSpan::class, 0, text.length)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testAnnotatedString_setBrushOnPartOfText() {
+ val text = "abcde"
+ val brush = Brush.linearGradient(listOf(Color.Black, Color.White))
+ val spanStyle = SpanStyle(brush = brush)
+
+ val paragraph = simpleParagraph(
+ text = text,
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, "abc".length)),
+ width = 100.0f
+ )
+
+ assertThat(paragraph.charSequence).hasSpan(ShaderBrushSpan::class, 0, "abc".length) {
+ it.shaderBrush == brush
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun testAnnotatedString_brushSpanReceivesSize() {
+ with(defaultDensity) {
+ val text = "abcde"
+ val brush = Brush.linearGradient(listOf(Color.Black, Color.White))
+ val spanStyle = SpanStyle(brush = brush)
+ val fontSize = 10.sp
+
+ val paragraph = simpleParagraph(
+ text = text,
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, "abc".length)),
+ width = 100.0f,
+ style = TextStyle(fontSize = fontSize, fontFamily = basicFontFamily)
+ )
+
+ assertThat(paragraph.charSequence).hasSpan(ShaderBrushSpan::class, 0, "abc".length) {
+ it.size == Size(100.0f, fontSize.toPx())
+ }
+ }
+ }
+
@Test
fun testStyle_setTextDecorationOnWholeText_withLineThrough() {
val text = "abcde"
@@ -904,6 +982,139 @@
}
@Test
+ fun testEllipsis_withLimitedHeightFitAllLines_doesNotEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ ellipsis = true,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 6 * fontSize.toPx(),
+ )
+
+ for (i in 0 until paragraph.lineCount) {
+ assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun testEllipsis_withLimitedHeight_doesEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ ellipsis = true,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 2.2f * fontSize.toPx(), // fits 2 lines
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(2)
+ assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ }
+ }
+
+ @Test
+ fun testEllipsis_withLimitedHeight_ellipsisFalse_doesNotEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ ellipsis = false,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 2.2f * fontSize.toPx(), // fits 2 lines
+ )
+
+ for (i in 0 until paragraph.lineCount) {
+ assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun testEllipsis_withMaxLinesMoreThanTextLines_andLimitedHeight_doesEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ ellipsis = true,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 2.2f * fontSize.toPx(), // fits 2 lines
+ maxLines = 5
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(2)
+ assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ }
+ }
+
+ @Test
+ fun testEllipsis_withMaxLines_andLimitedHeight_doesEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ ellipsis = true,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 4 * fontSize.toPx(),
+ maxLines = 2
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(2)
+ assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ }
+ }
+
+ @Test
+ fun testEllipsis_withSpans_withLimitedHeight_doesEllipsis() {
+ with(defaultDensity) {
+ val text = "This is a text"
+ val fontSize = 30.sp
+ val paragraph = simpleParagraph(
+ text = text,
+ spanStyles = listOf(
+ AnnotatedString.Range(SpanStyle(fontSize = fontSize * 2), 0, 2)
+ ),
+ ellipsis = true,
+ style = TextStyle(
+ fontFamily = basicFontFamily,
+ fontSize = fontSize
+ ),
+ width = 4 * fontSize.toPx(),
+ height = 2.2f * fontSize.toPx() // fits 2 lines
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(1)
+ assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ }
+ }
+
+ @Test
fun testSpanStyle_fontSize_appliedOnTextPaint() {
with(defaultDensity) {
val fontSize = 100.sp
@@ -1345,6 +1556,7 @@
ellipsis: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
width: Float,
+ height: Float = Float.POSITIVE_INFINITY,
style: TextStyle? = null,
fontFamilyResolver: FontFamily.Resolver = UncachedFontFamilyResolver(context)
): AndroidParagraph {
@@ -1358,7 +1570,10 @@
).merge(style),
maxLines = maxLines,
ellipsis = ellipsis,
- constraints = Constraints(maxWidth = width.ceilToInt()),
+ constraints = Constraints(
+ maxWidth = width.ceilToInt(),
+ maxHeight = height.ceilToInt()
+ ),
density = Density(density = 1f),
fontFamilyResolver = fontFamilyResolver
)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
index 8c21480..e990133 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt
@@ -18,16 +18,21 @@
import android.graphics.Paint
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shader
+import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.style.TextDecoration
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -143,6 +148,87 @@
assertThat(textPaint.color).isEqualTo(Color.Transparent.toArgb())
}
+ @Test
+ fun setShaderBrush_with_specified_size() {
+ var calls = 0
+ val brush = object : ShaderBrush() {
+ val brush = linearGradient(listOf(Color.Red, Color.Blue))
+ override fun createShader(size: Size): Shader {
+ calls++
+ return (brush as ShaderBrush).createShader(size)
+ }
+ }
+
+ val size = Size(10f, 10f)
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNotNull()
+ assertThat(calls).isEqualTo(1)
+ }
+
+ @Test
+ fun setShaderBrush_with_unspecified_size() {
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ val size = Size.Unspecified
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNull()
+ }
+
+ @Test
+ fun setColorBrush_with_specified_size() {
+ val brush = SolidColor(Color.Red)
+ val size = Size(10f, 10f)
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNull()
+ assertThat(textPaint.color).isEqualTo(Color.Red.toArgb())
+ }
+
+ @Test
+ fun setColorBrush_with_unspecified_size() {
+ val brush = SolidColor(Color.Red)
+ val size = Size.Unspecified
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNull()
+ assertThat(textPaint.color).isEqualTo(Color.Red.toArgb())
+ }
+
+ @Test
+ fun setUnspecifiedBrush_with_specified_size() {
+ val brush = SolidColor(Color.Unspecified)
+ val size = Size(10f, 10f)
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNull()
+ assertThat(textPaint.color).isNotEqualTo(Color.Unspecified.toArgb())
+
+ textPaint.setBrush(SolidColor(Color.Red), size)
+
+ assertThat(textPaint.shader).isNull()
+ assertThat(textPaint.color).isEqualTo(Color.Red.toArgb())
+ }
+
+ @Test
+ fun setNullBrush_with_specified_size() {
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ val size = Size(10f, 10f)
+ val textPaint = defaultTextPaint
+ textPaint.setBrush(brush, size)
+
+ assertThat(textPaint.shader).isNotNull()
+
+ textPaint.setBrush(null, size)
+
+ assertThat(textPaint.shader).isNull()
+ }
+
@SdkSuppress(minSdkVersion = 29)
@Test
fun shadow_default_values() {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
index 4058bcb..a8ded10 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/SpannableExtensionsTest.kt
@@ -16,12 +16,24 @@
package androidx.compose.ui.text.platform
+import android.graphics.Typeface
+import android.text.SpannableStringBuilder
+import android.text.style.ForegroundColorSpan
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.matchers.assertThat
import androidx.compose.ui.text.platform.extensions.flattenFontStylesAndApply
+import androidx.compose.ui.text.platform.extensions.setSpanStyles
+import androidx.compose.ui.text.platform.style.ShaderBrushSpan
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -420,4 +432,87 @@
verifyNoMoreInteractions()
}
}
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun shaderBrush_shouldAdd_shaderBrushSpan_whenApplied() {
+ val text = "abcde abcde"
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ val spanStyle = SpanStyle(brush = brush)
+ val spannable = SpannableStringBuilder().apply { append(text) }
+ spannable.setSpanStyles(
+ contextTextStyle = TextStyle(),
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
+ density = Density(1f, 1f),
+ resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
+ )
+
+ assertThat(spannable).hasSpan(ShaderBrushSpan::class, 0, text.length) {
+ it.shaderBrush == brush
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun solidColorBrush_shouldAdd_ForegroundColorSpan_whenApplied() {
+ val text = "abcde abcde"
+ val spanStyle = SpanStyle(brush = SolidColor(Color.Red))
+ val spannable = SpannableStringBuilder().apply { append(text) }
+ spannable.setSpanStyles(
+ contextTextStyle = TextStyle(),
+ spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
+ density = Density(1f, 1f),
+ resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
+ )
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun whenColorAndShaderBrushSpansCollide_bothShouldApply() {
+ val text = "abcde abcde"
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ val brushStyle = SpanStyle(brush = brush)
+ val colorStyle = SpanStyle(color = Color.Red)
+ val spannable = SpannableStringBuilder().apply { append(text) }
+ spannable.setSpanStyles(
+ contextTextStyle = TextStyle(),
+ spanStyles = listOf(
+ AnnotatedString.Range(brushStyle, 0, text.length),
+ AnnotatedString.Range(colorStyle, 0, text.length)
+ ),
+ density = Density(1f, 1f),
+ resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
+ )
+
+ assertThat(spannable).hasSpan(ShaderBrushSpan::class, 0, text.length) {
+ it.shaderBrush == brush
+ }
+ assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun whenColorAndSolidColorBrushSpansCollide_bothShouldApply() {
+ val text = "abcde abcde"
+ val brush = SolidColor(Color.Blue)
+ val brushStyle = SpanStyle(brush = brush)
+ val colorStyle = SpanStyle(color = Color.Red)
+ val spannable = SpannableStringBuilder().apply { append(text) }
+ spannable.setSpanStyles(
+ contextTextStyle = TextStyle(),
+ spanStyles = listOf(
+ AnnotatedString.Range(brushStyle, 0, text.length),
+ AnnotatedString.Range(colorStyle, 0, text.length)
+ ),
+ density = Density(1f, 1f),
+ resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
+ )
+
+ assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length) {
+ it.foregroundColor == Color.Blue.toArgb()
+ }
+ assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length) {
+ it.foregroundColor == Color.Red.toArgb()
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
index 1d568de..fc6163b 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
@@ -21,6 +21,8 @@
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@@ -50,11 +52,13 @@
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.platform.style.ShaderBrushSpan
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Density
import java.util.Locale as JavaLocale
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.ceilToInt
import androidx.compose.ui.unit.Constraints
@@ -117,20 +121,34 @@
null
}
- layout = TextLayout(
- charSequence = paragraphIntrinsics.charSequence,
- width = width,
- textPaint = textPaint,
- ellipsize = ellipsize,
+ val firstLayout = constructTextLayout(
alignment = alignment,
- textDirectionHeuristic = paragraphIntrinsics.textDirectionHeuristic,
- lineSpacingMultiplier = DEFAULT_LINESPACING_MULTIPLIER,
- maxLines = maxLines,
justificationMode = justificationMode,
- layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics,
- includePadding = style.isIncludeFontPaddingEnabled(),
- fallbackLineSpacing = true
+ ellipsize = ellipsize,
+ maxLines = maxLines
)
+
+ // Ellipsize if there's not enough vertical space to fit all lines
+ if (ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
+ val calculatedMaxLines =
+ firstLayout.numberOfLinesThatFitMaxHeight(constraints.maxHeight)
+ layout = if (calculatedMaxLines > 0 && calculatedMaxLines != maxLines) {
+ constructTextLayout(
+ alignment = alignment,
+ justificationMode = justificationMode,
+ ellipsize = ellipsize,
+ maxLines = calculatedMaxLines
+ )
+ } else {
+ firstLayout
+ }
+ } else {
+ layout = firstLayout
+ }
+
+ layout.getShaderBrushSpans().forEach { shaderBrushSpan ->
+ shaderBrushSpan.size = Size(width, height)
+ }
}
override val width: Float
@@ -379,15 +397,26 @@
internal fun isEllipsisApplied(lineIndex: Int): Boolean =
layout.isEllipsisApplied(lineIndex)
+ private fun TextLayout.getShaderBrushSpans(): Array<ShaderBrushSpan> {
+ if (text !is Spanned) return emptyArray()
+ val brushSpans = (text as Spanned).getSpans(
+ 0, text.length, ShaderBrushSpan::class.java
+ )
+ if (brushSpans.isEmpty()) return emptyArray()
+ return brushSpans
+ }
+
override fun paint(
canvas: Canvas,
color: Color,
shadow: Shadow?,
textDecoration: TextDecoration?
) {
- textPaint.setColor(color)
- textPaint.setShadow(shadow)
- textPaint.setTextDecoration(textDecoration)
+ with(textPaint) {
+ setColor(color)
+ setShadow(shadow)
+ setTextDecoration(textDecoration)
+ }
val nativeCanvas = canvas.nativeCanvas
if (didExceedMaxLines) {
@@ -399,6 +428,51 @@
nativeCanvas.restore()
}
}
+
+ @OptIn(ExperimentalTextApi::class)
+ override fun paint(
+ canvas: Canvas,
+ brush: Brush,
+ shadow: Shadow?,
+ textDecoration: TextDecoration?
+ ) {
+ with(textPaint) {
+ setBrush(brush, Size(width, height))
+ setShadow(shadow)
+ setTextDecoration(textDecoration)
+ }
+
+ val nativeCanvas = canvas.nativeCanvas
+ if (didExceedMaxLines) {
+ nativeCanvas.save()
+ nativeCanvas.clipRect(0f, 0f, width, height)
+ }
+ layout.paint(nativeCanvas)
+ if (didExceedMaxLines) {
+ nativeCanvas.restore()
+ }
+ }
+
+ private fun constructTextLayout(
+ alignment: Int,
+ justificationMode: Int,
+ ellipsize: TextUtils.TruncateAt?,
+ maxLines: Int
+ ) =
+ TextLayout(
+ charSequence = paragraphIntrinsics.charSequence,
+ width = width,
+ textPaint = textPaint,
+ ellipsize = ellipsize,
+ alignment = alignment,
+ textDirectionHeuristic = paragraphIntrinsics.textDirectionHeuristic,
+ lineSpacingMultiplier = DEFAULT_LINESPACING_MULTIPLIER,
+ maxLines = maxLines,
+ justificationMode = justificationMode,
+ layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics,
+ includePadding = paragraphIntrinsics.style.isIncludeFontPaddingEnabled(),
+ fallbackLineSpacing = true
+ )
}
/**
@@ -414,11 +488,21 @@
else -> DEFAULT_ALIGNMENT
}
+@OptIn(InternalPlatformTextApi::class)
+private fun TextLayout.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int {
+ for (lineIndex in 0 until lineCount) {
+ if (getLineBottom(lineIndex) > maxHeight) return lineIndex
+ }
+ return lineCount
+}
+
@Suppress("DEPRECATION")
@Deprecated(
"Font.ResourceLoader is deprecated, instead pass FontFamily.Resolver",
- replaceWith = ReplaceWith("ActualParagraph(text, style, spanStyles, placeholders, " +
- "maxLines, ellipsis, width, density, fontFamilyResolver)"),
+ replaceWith = ReplaceWith(
+ "ActualParagraph(text, style, spanStyles, placeholders, " +
+ "maxLines, ellipsis, width, density, fontFamilyResolver)"
+ ),
)
internal actual fun ActualParagraph(
text: String,
@@ -441,7 +525,7 @@
),
maxLines,
ellipsis,
- Constraints(maxWidth = width.ceilToInt())
+ Constraints(maxWidth = width.ceilToInt())
)
internal actual fun ActualParagraph(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
index 785a35d..60832dd 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
@@ -33,7 +33,7 @@
import androidx.compose.ui.text.platform.extensions.setPlaceholders
import androidx.compose.ui.text.platform.extensions.setSpanStyles
import androidx.compose.ui.text.platform.extensions.setTextIndent
-import androidx.compose.ui.text.style.LineHeightBehavior
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.isUnspecified
@@ -59,7 +59,7 @@
val spannableString = SpannableString(text)
if (contextTextStyle.isIncludeFontPaddingEnabled() &&
- contextTextStyle.lineHeightBehavior == null
+ contextTextStyle.lineHeightStyle == null
) {
// keep the existing line height behavior for includeFontPadding=true
spannableString.setLineHeight(
@@ -68,10 +68,10 @@
density = density
)
} else {
- val lineHeightBehavior = contextTextStyle.lineHeightBehavior ?: LineHeightBehavior.Default
+ val lineHeightStyle = contextTextStyle.lineHeightStyle ?: LineHeightStyle.Default
spannableString.setLineHeight(
lineHeight = contextTextStyle.lineHeight,
- lineHeightBehavior = lineHeightBehavior,
+ lineHeightStyle = lineHeightStyle,
contextFontSize = contextFontSize,
density = density,
)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
index 41df48c..32fd676 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
@@ -17,8 +17,13 @@
package androidx.compose.ui.text.platform
import android.text.TextPaint
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.style.TextDecoration
@@ -29,6 +34,8 @@
}
private var textDecoration: TextDecoration = TextDecoration.None
private var shadow: Shadow = Shadow.None
+ private var brush: Brush? = null
+ private var brushSize: Size? = null
fun setTextDecoration(textDecoration: TextDecoration?) {
val tmpTextDecoration = textDecoration ?: TextDecoration.None
@@ -64,4 +71,26 @@
}
}
}
+
+ fun setBrush(brush: Brush?, size: Size) {
+ if (brush == null) {
+ this.shader = null
+ return
+ }
+ if (this.brush != brush || this.brushSize != size) {
+ this.brush = brush
+ this.brushSize = size
+ when (brush) {
+ is SolidColor -> {
+ this.shader = null
+ setColor(brush.value)
+ }
+ is ShaderBrush -> {
+ if (size.isSpecified) {
+ shader = brush.createShader(size)
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index 349ab1e..5e0fe99 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -28,8 +28,11 @@
import android.text.style.MetricAffectingSpan
import android.text.style.RelativeSizeSpan
import android.text.style.ScaleXSpan
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.AnnotatedString
@@ -41,7 +44,7 @@
import androidx.compose.ui.text.android.style.FontFeatureSpan
import androidx.compose.ui.text.android.style.LetterSpacingSpanEm
import androidx.compose.ui.text.android.style.LetterSpacingSpanPx
-import androidx.compose.ui.text.android.style.LineHeightBehaviorSpan
+import androidx.compose.ui.text.android.style.LineHeightStyleSpan
import androidx.compose.ui.text.android.style.LineHeightSpan
import androidx.compose.ui.text.android.style.ShadowSpan
import androidx.compose.ui.text.android.style.SkewXSpan
@@ -56,8 +59,9 @@
import androidx.compose.ui.text.intersect
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.intl.LocaleList
+import androidx.compose.ui.text.platform.style.ShaderBrushSpan
import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.LineHeightBehavior
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
@@ -118,18 +122,18 @@
lineHeight: TextUnit,
contextFontSize: Float,
density: Density,
- lineHeightBehavior: LineHeightBehavior
+ lineHeightStyle: LineHeightStyle
) {
val resolvedLineHeight = resolveLineHeightInPx(lineHeight, contextFontSize, density)
if (!resolvedLineHeight.isNaN()) {
setSpan(
- span = LineHeightBehaviorSpan(
+ span = LineHeightStyleSpan(
lineHeight = resolvedLineHeight,
startIndex = 0,
endIndex = length,
- trimFirstLineTop = lineHeightBehavior.trim.isTrimFirstLineTop(),
- trimLastLineBottom = lineHeightBehavior.trim.isTrimLastLineBottom(),
- topPercentage = lineHeightBehavior.alignment.topPercentage
+ trimFirstLineTop = lineHeightStyle.trim.isTrimFirstLineTop(),
+ trimLastLineBottom = lineHeightStyle.trim.isTrimLastLineBottom(),
+ topPercentage = lineHeightStyle.alignment.topPercentage
),
start = 0,
end = length
@@ -213,6 +217,8 @@
setColor(style.color, start, end)
+ setBrush(style.brush, start, end)
+
setTextDecoration(style.textDecoration, start, end)
setFontSize(style.fontSize, density, start, end)
@@ -476,6 +482,23 @@
}
}
+private fun Spannable.setBrush(
+ brush: Brush?,
+ start: Int,
+ end: Int
+) {
+ brush?.let {
+ when (brush) {
+ is SolidColor -> {
+ setColor(brush.value, start, end)
+ }
+ is ShaderBrush -> {
+ setSpan(ShaderBrushSpan(brush), start, end)
+ }
+ }
+ }
+}
+
/**
* Returns true if there is any font settings on this [TextStyle].
* @see hasFontAttributes
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
index 25f9b3f..ff25df9 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
@@ -18,6 +18,7 @@
import android.graphics.Typeface
import android.os.Build
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
@@ -89,8 +90,13 @@
textSkewX += style.textGeometricTransform.skewX
}
- // these parameters are also updated by the Paragraph.draw
+ // these parameters are also updated by the Paragraph.paint
+
setColor(style.color)
+ // setBrush draws the text with given Brush. ShaderBrush requires Size to
+ // create a Shader. However, Size is unavailable at this stage of the layout.
+ // Paragraph.paint will receive a proper Size after layout is completed.
+ setBrush(style.brush, Size.Unspecified)
setShadow(style.shadow)
setTextDecoration(style.textDecoration)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/style/ShaderBrushSpan.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/style/ShaderBrushSpan.android.kt
new file mode 100644
index 0000000..515666d
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/style/ShaderBrushSpan.android.kt
@@ -0,0 +1,38 @@
+/*
+ * 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
+ *
+ * http://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 androidx.compose.ui.text.platform.style
+
+import android.text.TextPaint
+import android.text.style.CharacterStyle
+import android.text.style.UpdateAppearance
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.ShaderBrush
+
+/**
+ * A span that applies [ShaderBrush] to TextPaint after receiving a specified size
+ */
+internal class ShaderBrushSpan(
+ val shaderBrush: ShaderBrush
+) : CharacterStyle(), UpdateAppearance {
+ var size: Size? = null
+
+ override fun updateDrawState(textPaint: TextPaint?) {
+ if (textPaint != null) {
+ size?.let { textPaint.shader = shaderBrush.createShader(it) }
+ }
+ }
+}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index 8aade8e..3b4d9ac 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@@ -391,6 +392,22 @@
canvas.restore()
}
+ /** Paint the paragraphs to canvas. */
+ @ExperimentalTextApi
+ fun paint(
+ canvas: Canvas,
+ brush: Brush,
+ shadow: Shadow? = null,
+ decoration: TextDecoration? = null
+ ) {
+ canvas.save()
+ paragraphInfoList.fastForEach {
+ it.paragraph.paint(canvas, brush, shadow, decoration)
+ canvas.translate(0f, it.paragraph.height)
+ }
+ canvas.restore()
+ }
+
/** Returns path that enclose the given text range. */
fun getPathForRange(start: Int, end: Int): Path {
require(start in 0..end && end <= annotatedString.text.length) {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index e6159f7..7026c35 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -17,6 +17,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@@ -239,6 +240,23 @@
shadow: Shadow? = null,
textDecoration: TextDecoration? = null
)
+
+ /**
+ * Paint the paragraph to canvas, and also overrides some paint settings.
+ *
+ * If not overridden, this function @throws [UnsupportedOperationException].
+ */
+ @ExperimentalTextApi
+ fun paint(
+ canvas: Canvas,
+ brush: Brush,
+ shadow: Shadow? = null,
+ textDecoration: TextDecoration? = null
+ ) {
+ throw UnsupportedOperationException(
+ "Using brush for painting the paragraph is a separate functionality that " +
+ "is not supported on this platform")
+ }
}
/**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
index 378b019..c9a37c1 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
@@ -18,7 +18,7 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import androidx.compose.ui.text.style.LineHeightBehavior
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextIndent
@@ -45,10 +45,10 @@
* @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
* @param textIndent The indentation of the paragraph.
* @param platformStyle Platform specific [ParagraphStyle] parameters.
- * @param lineHeightBehavior the configuration for line height such as vertical alignment of the
+ * @param lineHeightStyle the configuration for line height such as vertical alignment of the
* line, whether to apply additional space as a result of line height to top of first line top and
* bottom of last line. The configuration is applied only when a [lineHeight] is defined.
- * When null, [LineHeightBehavior.Default] is used.
+ * When null, [LineHeightStyle.Default] is used.
*
* @see Paragraph
* @see AnnotatedString
@@ -64,7 +64,7 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalTextApi val platformStyle: PlatformParagraphStyle? = null,
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ExperimentalTextApi val lineHeightBehavior: LineHeightBehavior? = null
+ @get:ExperimentalTextApi val lineHeightStyle: LineHeightStyle? = null
) {
/**
@@ -100,7 +100,7 @@
lineHeight = lineHeight,
textIndent = textIndent,
platformStyle = null,
- lineHeightBehavior = null
+ lineHeightStyle = null
)
init {
@@ -133,7 +133,7 @@
textAlign = other.textAlign ?: this.textAlign,
textDirection = other.textDirection ?: this.textDirection,
platformStyle = mergePlatformStyle(other.platformStyle),
- lineHeightBehavior = other.lineHeightBehavior ?: this.lineHeightBehavior
+ lineHeightStyle = other.lineHeightStyle ?: this.lineHeightStyle
)
}
@@ -163,7 +163,7 @@
lineHeight = lineHeight,
textIndent = textIndent,
platformStyle = this.platformStyle,
- lineHeightBehavior = this.lineHeightBehavior
+ lineHeightStyle = this.lineHeightStyle
)
}
@@ -174,7 +174,7 @@
lineHeight: TextUnit = this.lineHeight,
textIndent: TextIndent? = this.textIndent,
platformStyle: PlatformParagraphStyle? = this.platformStyle,
- lineHeightBehavior: LineHeightBehavior? = this.lineHeightBehavior
+ lineHeightStyle: LineHeightStyle? = this.lineHeightStyle
): ParagraphStyle {
return ParagraphStyle(
textAlign = textAlign,
@@ -182,7 +182,7 @@
lineHeight = lineHeight,
textIndent = textIndent,
platformStyle = platformStyle,
- lineHeightBehavior = lineHeightBehavior
+ lineHeightStyle = lineHeightStyle
)
}
@@ -196,7 +196,7 @@
if (lineHeight != other.lineHeight) return false
if (textIndent != other.textIndent) return false
if (platformStyle != other.platformStyle) return false
- if (lineHeightBehavior != other.lineHeightBehavior) return false
+ if (lineHeightStyle != other.lineHeightStyle) return false
return true
}
@@ -208,7 +208,7 @@
result = 31 * result + lineHeight.hashCode()
result = 31 * result + (textIndent?.hashCode() ?: 0)
result = 31 * result + (platformStyle?.hashCode() ?: 0)
- result = 31 * result + (lineHeightBehavior?.hashCode() ?: 0)
+ result = 31 * result + (lineHeightStyle?.hashCode() ?: 0)
return result
}
@@ -220,7 +220,7 @@
"lineHeight=$lineHeight, " +
"textIndent=$textIndent, " +
"platformStyle=$platformStyle, " +
- "lineHeightBehavior=$lineHeightBehavior" +
+ "lineHeightStyle=$lineHeightStyle" +
")"
}
}
@@ -255,9 +255,9 @@
fraction
),
platformStyle = lerpPlatformStyle(start.platformStyle, stop.platformStyle, fraction),
- lineHeightBehavior = lerpDiscrete(
- start.lineHeightBehavior,
- stop.lineHeightBehavior,
+ lineHeightStyle = lerpDiscrete(
+ start.lineHeightStyle,
+ stop.lineHeightStyle,
fraction
)
)
@@ -285,5 +285,5 @@
lineHeight = if (style.lineHeight.isUnspecified) DefaultLineHeight else style.lineHeight,
textIndent = style.textIndent ?: TextIndent.None,
platformStyle = style.platformStyle,
- lineHeightBehavior = style.lineHeightBehavior
+ lineHeightStyle = style.lineHeightStyle
)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/SpanStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/SpanStyle.kt
index 52153b3..e4434e3 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/SpanStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/SpanStyle.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.lerp
@@ -30,6 +31,7 @@
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextDrawStyle
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.lerp
import androidx.compose.ui.unit.TextUnit
@@ -54,7 +56,6 @@
*
* @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderSample
*
- * @param color The text color.
* @param fontSize The size of glyphs (in logical pixels) to use when painting the text. This
* may be [TextUnit.Unspecified] for inheriting from another [SpanStyle].
* @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
@@ -79,8 +80,9 @@
* @see ParagraphStyle
*/
@Immutable
-class SpanStyle @ExperimentalTextApi constructor(
- val color: Color = Color.Unspecified,
+class SpanStyle @OptIn(ExperimentalTextApi::class) internal constructor(
+ // The fill to draw text, a unified representation of Color and Brush.
+ internal val textDrawStyle: TextDrawStyle,
val fontSize: TextUnit = TextUnit.Unspecified,
val fontWeight: FontWeight? = null,
val fontStyle: FontStyle? = null,
@@ -147,7 +149,7 @@
textDecoration: TextDecoration? = null,
shadow: Shadow? = null
) : this(
- color = color,
+ textDrawStyle = TextDrawStyle.from(color),
fontSize = fontSize,
fontWeight = fontWeight,
fontStyle = fontStyle,
@@ -165,6 +167,156 @@
)
/**
+ * Styling configuration for a text span. This configuration only allows character level styling,
+ * in order to set paragraph level styling such as line height, or text alignment please see
+ * [ParagraphStyle].
+ *
+ * @sample androidx.compose.ui.text.samples.SpanStyleSample
+ *
+ * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderSample
+ *
+ * @param color The color to draw the text.
+ * @param fontSize The size of glyphs (in logical pixels) to use when painting the text. This
+ * may be [TextUnit.Unspecified] for inheriting from another [SpanStyle].
+ * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
+ * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+ * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
+ * or style cannot be found in the provided font family.
+ * @param fontFamily The font family to be used when rendering the text.
+ * @param fontFeatureSettings The advanced typography settings provided by font. The format is
+ * the same as the CSS font-feature-settings attribute:
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
+ * @param letterSpacing The amount of space (in em) to add between each letter.
+ * @param baselineShift The amount by which the text is shifted up from the current baseline.
+ * @param textGeometricTransform The geometric transformation applied the text.
+ * @param localeList The locale list used to select region-specific glyphs.
+ * @param background The background color for the text.
+ * @param textDecoration The decorations to paint on the text (e.g., an underline).
+ * @param shadow The shadow effect applied on the text.
+ * @param platformStyle Platform specific [SpanStyle] parameters.
+ *
+ * @see AnnotatedString
+ * @see TextStyle
+ * @see ParagraphStyle
+ */
+ @ExperimentalTextApi
+ constructor(
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontWeight: FontWeight? = null,
+ fontStyle: FontStyle? = null,
+ fontSynthesis: FontSynthesis? = null,
+ fontFamily: FontFamily? = null,
+ fontFeatureSettings: String? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ baselineShift: BaselineShift? = null,
+ textGeometricTransform: TextGeometricTransform? = null,
+ localeList: LocaleList? = null,
+ background: Color = Color.Unspecified,
+ textDecoration: TextDecoration? = null,
+ shadow: Shadow? = null,
+ platformStyle: PlatformSpanStyle? = null
+ ) : this(
+ textDrawStyle = TextDrawStyle.from(color),
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle
+ )
+
+ /**
+ * Styling configuration for a text span. This configuration only allows character level styling,
+ * in order to set paragraph level styling such as line height, or text alignment please see
+ * [ParagraphStyle].
+ *
+ * @sample androidx.compose.ui.text.samples.SpanStyleSample
+ *
+ * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderSample
+ *
+ * @param brush The brush to use when painting the text. If brush is given as null, it will be
+ * treated as unspecified. It is equivalent to calling the alternative color constructor with
+ * [Color.Unspecified]
+ * @param fontSize The size of glyphs (in logical pixels) to use when painting the text. This
+ * may be [TextUnit.Unspecified] for inheriting from another [SpanStyle].
+ * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
+ * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+ * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
+ * or style cannot be found in the provided font family.
+ * @param fontFamily The font family to be used when rendering the text.
+ * @param fontFeatureSettings The advanced typography settings provided by font. The format is
+ * the same as the CSS font-feature-settings attribute:
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
+ * @param letterSpacing The amount of space (in em) to add between each letter.
+ * @param baselineShift The amount by which the text is shifted up from the current baseline.
+ * @param textGeometricTransform The geometric transformation applied the text.
+ * @param localeList The locale list used to select region-specific glyphs.
+ * @param background The background color for the text.
+ * @param textDecoration The decorations to paint on the text (e.g., an underline).
+ * @param shadow The shadow effect applied on the text.
+ * @param platformStyle Platform specific [SpanStyle] parameters.
+ *
+ * @see AnnotatedString
+ * @see TextStyle
+ * @see ParagraphStyle
+ */
+ @ExperimentalTextApi
+ constructor(
+ brush: Brush?,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontWeight: FontWeight? = null,
+ fontStyle: FontStyle? = null,
+ fontSynthesis: FontSynthesis? = null,
+ fontFamily: FontFamily? = null,
+ fontFeatureSettings: String? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ baselineShift: BaselineShift? = null,
+ textGeometricTransform: TextGeometricTransform? = null,
+ localeList: LocaleList? = null,
+ background: Color = Color.Unspecified,
+ textDecoration: TextDecoration? = null,
+ shadow: Shadow? = null,
+ platformStyle: PlatformSpanStyle? = null
+ ) : this(
+ textDrawStyle = TextDrawStyle.from(brush),
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle
+ )
+
+ /**
+ * Color to draw text.
+ */
+ val color: Color get() = this.textDrawStyle.color
+
+ /**
+ * Brush to draw text. If not null, overrides [color].
+ */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalTextApi
+ val brush: Brush? get() = this.textDrawStyle.brush
+
+ /**
* Returns a new span style that is a combination of this style and the given [other] style.
*
* [other] span style's null or inherit properties are replaced with the non-null properties of
@@ -179,7 +331,7 @@
if (other == null) return this
return SpanStyle(
- color = other.color.takeOrElse { this.color },
+ textDrawStyle = textDrawStyle.merge(other.textDrawStyle),
fontFamily = other.fontFamily ?: this.fontFamily,
fontSize = if (!other.fontSize.isUnspecified) other.fontSize else this.fontSize,
fontWeight = other.fontWeight ?: this.fontWeight,
@@ -232,7 +384,7 @@
shadow: Shadow? = this.shadow
): SpanStyle {
return SpanStyle(
- color = color,
+ textDrawStyle = if (color == this.color) textDrawStyle else TextDrawStyle.from(color),
fontSize = fontSize,
fontWeight = fontWeight,
fontStyle = fontStyle,
@@ -246,7 +398,7 @@
background = background,
textDecoration = textDecoration,
shadow = shadow,
- platformStyle = platformStyle
+ platformStyle = this.platformStyle
)
}
@@ -269,7 +421,44 @@
platformStyle: PlatformSpanStyle? = this.platformStyle
): SpanStyle {
return SpanStyle(
- color = color,
+ textDrawStyle = if (color == this.color) textDrawStyle else TextDrawStyle.from(color),
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle
+ )
+ }
+
+ @ExperimentalTextApi
+ fun copy(
+ brush: Brush?,
+ fontSize: TextUnit = this.fontSize,
+ fontWeight: FontWeight? = this.fontWeight,
+ fontStyle: FontStyle? = this.fontStyle,
+ fontSynthesis: FontSynthesis? = this.fontSynthesis,
+ fontFamily: FontFamily? = this.fontFamily,
+ fontFeatureSettings: String? = this.fontFeatureSettings,
+ letterSpacing: TextUnit = this.letterSpacing,
+ baselineShift: BaselineShift? = this.baselineShift,
+ textGeometricTransform: TextGeometricTransform? = this.textGeometricTransform,
+ localeList: LocaleList? = this.localeList,
+ background: Color = this.background,
+ textDecoration: TextDecoration? = this.textDecoration,
+ shadow: Shadow? = this.shadow,
+ platformStyle: PlatformSpanStyle? = this.platformStyle
+ ): SpanStyle {
+ return SpanStyle(
+ textDrawStyle = TextDrawStyle.from(brush),
fontSize = fontSize,
fontWeight = fontWeight,
fontStyle = fontStyle,
@@ -313,7 +502,7 @@
}
private fun hasSameNonLayoutAttributes(other: SpanStyle): Boolean {
- if (color != other.color) return false
+ if (textDrawStyle != other.textDrawStyle) return false
if (textDecoration != other.textDecoration) return false
if (shadow != other.shadow) return false
return true
@@ -322,6 +511,7 @@
@OptIn(ExperimentalTextApi::class)
override fun hashCode(): Int {
var result = color.hashCode()
+ result = 31 * result + brush.hashCode()
result = 31 * result + fontSize.hashCode()
result = 31 * result + (fontWeight?.hashCode() ?: 0)
result = 31 * result + (fontStyle?.hashCode() ?: 0)
@@ -343,6 +533,7 @@
override fun toString(): String {
return "SpanStyle(" +
"color=$color, " +
+ "brush=$brush, " +
"fontSize=$fontSize, " +
"fontWeight=$fontWeight, " +
"fontStyle=$fontStyle, " +
@@ -392,7 +583,7 @@
@OptIn(ExperimentalTextApi::class)
fun lerp(start: SpanStyle, stop: SpanStyle, fraction: Float): SpanStyle {
return SpanStyle(
- color = lerp(start.color, stop.color, fraction),
+ textDrawStyle = lerp(start.textDrawStyle, stop.textDrawStyle, fraction),
fontFamily = lerpDiscrete(
start.fontFamily,
stop.fontFamily,
@@ -468,7 +659,7 @@
@OptIn(ExperimentalTextApi::class)
internal fun resolveSpanStyleDefaults(style: SpanStyle) = SpanStyle(
- color = style.color.takeOrElse { DefaultColor },
+ textDrawStyle = style.textDrawStyle.takeOrElse { TextDrawStyle.from(DefaultColor) },
fontSize = if (style.fontSize.isUnspecified) DefaultFontSize else style.fontSize,
fontWeight = style.fontWeight ?: FontWeight.Normal,
fontStyle = style.fontStyle ?: FontStyle.Normal,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
index 5fcc181..16239dc 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
@@ -29,6 +29,7 @@
* @param canvas a canvas to be drawn
* @param textLayoutResult a result of text layout
*/
+ @OptIn(ExperimentalTextApi::class)
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
val needClipping = textLayoutResult.hasVisualOverflow &&
textLayoutResult.layoutInput.overflow == TextOverflow.Clip
@@ -40,12 +41,22 @@
canvas.clipRect(bounds)
}
try {
- textLayoutResult.multiParagraph.paint(
- canvas,
- textLayoutResult.layoutInput.style.color,
- textLayoutResult.layoutInput.style.shadow,
- textLayoutResult.layoutInput.style.textDecoration
- )
+ val brush = textLayoutResult.layoutInput.style.brush
+ if (brush != null) {
+ textLayoutResult.multiParagraph.paint(
+ canvas,
+ brush,
+ textLayoutResult.layoutInput.style.shadow,
+ textLayoutResult.layoutInput.style.textDecoration
+ )
+ } else {
+ textLayoutResult.multiParagraph.paint(
+ canvas,
+ textLayoutResult.layoutInput.style.color,
+ textLayoutResult.layoutInput.style.shadow,
+ textLayoutResult.layoutInput.style.textDecoration
+ )
+ }
} finally {
if (needClipping) {
canvas.restore()
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
index 9d2d6dc..c28f040 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.font.FontFamily
@@ -26,10 +27,11 @@
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.LineHeightBehavior
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextDrawStyle
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.LayoutDirection
@@ -97,13 +99,8 @@
* [LayoutDirection] as the primary signal.
* @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
* @param textIndent The indentation of the paragraph.
- * @param platformStyle Platform specific [TextStyle] parameters.
- * @param lineHeightBehavior the configuration for line height such as vertical alignment of the
- * line, whether to apply additional space as a result of line height to top of first line top
- * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
- * When null, [LineHeightBehavior.Default] is used.
*/
- @ExperimentalTextApi
+ @OptIn(ExperimentalTextApi::class)
constructor(
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
@@ -122,9 +119,7 @@
textAlign: TextAlign? = null,
textDirection: TextDirection? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
- textIndent: TextIndent? = null,
- platformStyle: PlatformTextStyle? = null,
- lineHeightBehavior: LineHeightBehavior? = null
+ textIndent: TextIndent? = null
) : this(
SpanStyle(
color = color,
@@ -141,17 +136,17 @@
background = background,
textDecoration = textDecoration,
shadow = shadow,
- platformStyle = platformStyle?.spanStyle
+ platformStyle = null
),
ParagraphStyle(
textAlign = textAlign,
textDirection = textDirection,
lineHeight = lineHeight,
textIndent = textIndent,
- platformStyle = platformStyle?.paragraphStyle,
- lineHeightBehavior = lineHeightBehavior
+ platformStyle = null,
+ lineHeightStyle = null
),
- platformStyle = platformStyle
+ platformStyle = null
)
/**
@@ -183,8 +178,13 @@
* [LayoutDirection] as the primary signal.
* @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
* @param textIndent The indentation of the paragraph.
+ * @param platformStyle Platform specific [TextStyle] parameters.
+ * @param lineHeightStyle the configuration for line height such as vertical alignment of the
+ * line, whether to apply additional space as a result of line height to top of first line top
+ * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+ * When null, [LineHeightStyle.Default] is used.
*/
- @OptIn(ExperimentalTextApi::class)
+ @ExperimentalTextApi
constructor(
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
@@ -203,28 +203,123 @@
textAlign: TextAlign? = null,
textDirection: TextDirection? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
- textIndent: TextIndent? = null
+ textIndent: TextIndent? = null,
+ platformStyle: PlatformTextStyle? = null,
+ lineHeightStyle: LineHeightStyle? = null
) : this(
- color = color,
- fontSize = fontSize,
- fontWeight = fontWeight,
- fontStyle = fontStyle,
- fontSynthesis = fontSynthesis,
- fontFamily = fontFamily,
- fontFeatureSettings = fontFeatureSettings,
- letterSpacing = letterSpacing,
- baselineShift = baselineShift,
- textGeometricTransform = textGeometricTransform,
- localeList = localeList,
- background = background,
- textDecoration = textDecoration,
- shadow = shadow,
- textAlign = textAlign,
- textDirection = textDirection,
- lineHeight = lineHeight,
- textIndent = textIndent,
- platformStyle = null,
- lineHeightBehavior = null
+ SpanStyle(
+ color = color,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle?.spanStyle
+ ),
+ ParagraphStyle(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ lineHeight = lineHeight,
+ textIndent = textIndent,
+ platformStyle = platformStyle?.paragraphStyle,
+ lineHeightStyle = lineHeightStyle
+ ),
+ platformStyle = platformStyle
+ )
+
+ /**
+ * Styling configuration for a `Text`.
+ *
+ * @sample androidx.compose.ui.text.samples.TextStyleSample
+ *
+ * @param brush The brush to use when painting the text. If brush is given as null, it will be
+ * treated as unspecified. It is equivalent to calling the alternative color constructor with
+ * [Color.Unspecified]
+ * @param fontSize The size of glyphs to use when painting the text. This
+ * may be [TextUnit.Unspecified] for inheriting from another [TextStyle].
+ * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
+ * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+ * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
+ * or style cannot be found in the provided font family.
+ * @param fontFamily The font family to be used when rendering the text.
+ * @param fontFeatureSettings The advanced typography settings provided by font. The format is
+ * the same as the CSS font-feature-settings attribute:
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
+ * @param letterSpacing The amount of space to add between each letter.
+ * @param baselineShift The amount by which the text is shifted up from the current baseline.
+ * @param textGeometricTransform The geometric transformation applied the text.
+ * @param localeList The locale list used to select region-specific glyphs.
+ * @param background The background color for the text.
+ * @param textDecoration The decorations to paint on the text (e.g., an underline).
+ * @param shadow The shadow effect applied on the text.
+ * @param textAlign The alignment of the text within the lines of the paragraph.
+ * @param textDirection The algorithm to be used to resolve the final text and paragraph
+ * direction: Left To Right or Right To Left. If no value is provided the system will use the
+ * [LayoutDirection] as the primary signal.
+ * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+ * @param textIndent The indentation of the paragraph.
+ * @param platformStyle Platform specific [TextStyle] parameters.
+ * @param lineHeightStyle the configuration for line height such as vertical alignment of the
+ * line, whether to apply additional space as a result of line height to top of first line top
+ * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+ */
+ @ExperimentalTextApi
+ constructor(
+ brush: Brush?,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontWeight: FontWeight? = null,
+ fontStyle: FontStyle? = null,
+ fontSynthesis: FontSynthesis? = null,
+ fontFamily: FontFamily? = null,
+ fontFeatureSettings: String? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ baselineShift: BaselineShift? = null,
+ textGeometricTransform: TextGeometricTransform? = null,
+ localeList: LocaleList? = null,
+ background: Color = Color.Unspecified,
+ textDecoration: TextDecoration? = null,
+ shadow: Shadow? = null,
+ textAlign: TextAlign? = null,
+ textDirection: TextDirection? = null,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ textIndent: TextIndent? = null,
+ platformStyle: PlatformTextStyle? = null,
+ lineHeightStyle: LineHeightStyle? = null
+ ) : this(
+ SpanStyle(
+ brush = brush,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle?.spanStyle
+ ),
+ ParagraphStyle(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ lineHeight = lineHeight,
+ textIndent = textIndent,
+ platformStyle = platformStyle?.paragraphStyle,
+ lineHeightStyle = lineHeightStyle
+ ),
+ platformStyle = platformStyle
)
@Stable
@@ -317,26 +412,36 @@
textIndent: TextIndent? = this.paragraphStyle.textIndent
): TextStyle {
return TextStyle(
- color = color,
- fontSize = fontSize,
- fontWeight = fontWeight,
- fontStyle = fontStyle,
- fontSynthesis = fontSynthesis,
- fontFamily = fontFamily,
- fontFeatureSettings = fontFeatureSettings,
- letterSpacing = letterSpacing,
- baselineShift = baselineShift,
- textGeometricTransform = textGeometricTransform,
- localeList = localeList,
- background = background,
- textDecoration = textDecoration,
- shadow = shadow,
- textAlign = textAlign,
- textDirection = textDirection,
- lineHeight = lineHeight,
- textIndent = textIndent,
- platformStyle = this.platformStyle,
- lineHeightBehavior = this.lineHeightBehavior
+ spanStyle = SpanStyle(
+ textDrawStyle = if (color == this.spanStyle.color) {
+ spanStyle.textDrawStyle
+ } else {
+ TextDrawStyle.from(color)
+ },
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = this.spanStyle.platformStyle
+ ),
+ paragraphStyle = ParagraphStyle(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ lineHeight = lineHeight,
+ textIndent = textIndent,
+ platformStyle = this.paragraphStyle.platformStyle,
+ lineHeightStyle = this.lineHeightStyle
+ ),
+ platformStyle = this.platformStyle
)
}
@@ -361,32 +466,102 @@
lineHeight: TextUnit = this.paragraphStyle.lineHeight,
textIndent: TextIndent? = this.paragraphStyle.textIndent,
platformStyle: PlatformTextStyle? = this.platformStyle,
- lineHeightBehavior: LineHeightBehavior? = this.paragraphStyle.lineHeightBehavior
+ lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle
): TextStyle {
return TextStyle(
- color = color,
- fontSize = fontSize,
- fontWeight = fontWeight,
- fontStyle = fontStyle,
- fontSynthesis = fontSynthesis,
- fontFamily = fontFamily,
- fontFeatureSettings = fontFeatureSettings,
- letterSpacing = letterSpacing,
- baselineShift = baselineShift,
- textGeometricTransform = textGeometricTransform,
- localeList = localeList,
- background = background,
- textDecoration = textDecoration,
- shadow = shadow,
- textAlign = textAlign,
- textDirection = textDirection,
- lineHeight = lineHeight,
- textIndent = textIndent,
- platformStyle = platformStyle,
- lineHeightBehavior = lineHeightBehavior
+ spanStyle = SpanStyle(
+ textDrawStyle = if (color == this.spanStyle.color) {
+ spanStyle.textDrawStyle
+ } else {
+ TextDrawStyle.from(color)
+ },
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle?.spanStyle
+ ),
+ paragraphStyle = ParagraphStyle(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ lineHeight = lineHeight,
+ textIndent = textIndent,
+ platformStyle = platformStyle?.paragraphStyle,
+ lineHeightStyle = lineHeightStyle
+ ),
+ platformStyle = platformStyle
)
}
+ @ExperimentalTextApi
+ fun copy(
+ brush: Brush?,
+ fontSize: TextUnit = this.spanStyle.fontSize,
+ fontWeight: FontWeight? = this.spanStyle.fontWeight,
+ fontStyle: FontStyle? = this.spanStyle.fontStyle,
+ fontSynthesis: FontSynthesis? = this.spanStyle.fontSynthesis,
+ fontFamily: FontFamily? = this.spanStyle.fontFamily,
+ fontFeatureSettings: String? = this.spanStyle.fontFeatureSettings,
+ letterSpacing: TextUnit = this.spanStyle.letterSpacing,
+ baselineShift: BaselineShift? = this.spanStyle.baselineShift,
+ textGeometricTransform: TextGeometricTransform? = this.spanStyle.textGeometricTransform,
+ localeList: LocaleList? = this.spanStyle.localeList,
+ background: Color = this.spanStyle.background,
+ textDecoration: TextDecoration? = this.spanStyle.textDecoration,
+ shadow: Shadow? = this.spanStyle.shadow,
+ textAlign: TextAlign? = this.paragraphStyle.textAlign,
+ textDirection: TextDirection? = this.paragraphStyle.textDirection,
+ lineHeight: TextUnit = this.paragraphStyle.lineHeight,
+ textIndent: TextIndent? = this.paragraphStyle.textIndent,
+ platformStyle: PlatformTextStyle? = this.platformStyle,
+ lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle
+ ): TextStyle {
+ return TextStyle(
+ spanStyle = SpanStyle(
+ brush = brush,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = textDecoration,
+ shadow = shadow,
+ platformStyle = platformStyle?.spanStyle
+ ),
+ paragraphStyle = ParagraphStyle(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ lineHeight = lineHeight,
+ textIndent = textIndent,
+ platformStyle = platformStyle?.paragraphStyle,
+ lineHeightStyle = lineHeightStyle
+ ),
+ platformStyle = platformStyle
+ )
+ }
+
+ /**
+ * The brush to use when drawing text. If not null, overrides [color].
+ */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalTextApi
+ val brush: Brush? get() = this.spanStyle.brush
+
/**
* The text color.
*/
@@ -490,12 +665,12 @@
*
* The configuration is applied only when a [lineHeight] is defined.
*
- * When null, [LineHeightBehavior.Default] is used.
+ * When null, [LineHeightStyle.Default] is used.
*/
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@ExperimentalTextApi
@get:ExperimentalTextApi
- val lineHeightBehavior: LineHeightBehavior? get() = this.paragraphStyle.lineHeightBehavior
+ val lineHeightStyle: LineHeightStyle? get() = this.paragraphStyle.lineHeightStyle
@OptIn(ExperimentalTextApi::class)
override fun equals(other: Any?): Boolean {
@@ -540,6 +715,7 @@
override fun toString(): String {
return "TextStyle(" +
"color=$color, " +
+ "brush=$brush, " +
"fontSize=$fontSize, " +
"fontWeight=$fontWeight, " +
"fontStyle=$fontStyle, " +
@@ -557,7 +733,7 @@
"lineHeight=$lineHeight, " +
"textIndent=$textIndent, " +
"platformStyle=$platformStyle" +
- "lineHeightBehavior=$lineHeightBehavior" +
+ "lineHeightStyle=$lineHeightStyle" +
")"
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightBehavior.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightBehavior.kt
deleted file mode 100644
index 9f46256..0000000
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightBehavior.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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
- *
- * http://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 androidx.compose.ui.text.style
-
-import androidx.compose.ui.text.PlatformParagraphStyle
-import androidx.compose.ui.text.ExperimentalTextApi
-
-/**
- * The configuration for line height such as alignment of the line in the provided line height,
- * whether to apply additional space as a result of line height to top of first line top and
- * bottom of last line.
- *
- * The configuration is applied only when a line height is defined on the text.
- *
- * [trim] feature is available only when [PlatformParagraphStyle.includeFontPadding] is false.
- *
- * Please check [LineHeightTrim] and [LineVerticalAlignment] for more description.
- *
- * @param alignment defines how to align the line in the space provided by the line height.
- * @param trim defines whether the space that would be added to the top of first line, and
- * bottom of the last line should be trimmed or not. This feature is available only when
- * [PlatformParagraphStyle.includeFontPadding] is false.
- */
-@ExperimentalTextApi
-class LineHeightBehavior(
- val alignment: LineVerticalAlignment = LineVerticalAlignment.Proportional,
- val trim: LineHeightTrim = LineHeightTrim.Both
-) {
- companion object {
- /**
- * The default configuration for LineHeightBehavior:
- * - alignment = LineVerticalAlignment.Proportional
- * - trim = LineHeightTrim.Both
- */
- val Default = LineHeightBehavior(
- alignment = LineVerticalAlignment.Proportional,
- trim = LineHeightTrim.Both
- )
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is LineHeightBehavior) return false
-
- if (alignment != other.alignment) return false
- if (trim != other.trim) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = alignment.hashCode()
- result = 31 * result + trim.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "LineHeightBehavior(" +
- "alignment=$alignment, " +
- "trim=$trim" +
- ")"
- }
-}
-
-/**
- * Defines how to align the line in the space provided by the line height.
- */
-@kotlin.jvm.JvmInline
-@ExperimentalTextApi
-value class LineVerticalAlignment private constructor(internal val topPercentage: Int) {
-
- init {
- check(topPercentage in 0..100 || topPercentage == -1) {
- "topRatio should be in [0..100] range or -1"
- }
- }
-
- override fun toString(): String {
- return when (topPercentage) {
- Top.topPercentage -> "LineVerticalAlignment.Top"
- Center.topPercentage -> "LineVerticalAlignment.Center"
- Proportional.topPercentage -> "LineVerticalAlignment.Proportional"
- Bottom.topPercentage -> "LineVerticalAlignment.Bottom"
- else -> "LineVerticalAlignment(topPercentage = $topPercentage)"
- }
- }
-
- companion object {
- /**
- * Align the line to the top of the space reserved for that line. This means that all extra
- * space as a result of line height is applied to the bottom of the line. When the provided
- * line height value is smaller than the actual line height, the line will still be aligned
- * to the top, therefore the required difference will be subtracted from the bottom of the
- * line.
- *
- * For example, when line height is 3.em, the lines are aligned to the top of 3.em
- * height:
- * <pre>
- * +--------+
- * | Line1 |
- * | |
- * | |
- * |--------|
- * | Line2 |
- * | |
- * | |
- * +--------+
- * </pre>
- */
- val Top = LineVerticalAlignment(topPercentage = 0)
-
- /**
- * Align the line to the center of the space reserved for the line. This configuration
- * distributes additional space evenly between top and bottom of the line.
- *
- * For example, when line height is 3.em, the lines are aligned to the center of 3.em
- * height:
- * <pre>
- * +--------+
- * | |
- * | Line1 |
- * | |
- * |--------|
- * | |
- * | Line2 |
- * | |
- * +--------+
- * </pre>
- */
- val Center = LineVerticalAlignment(topPercentage = 50)
-
- /**
- * Align the line proportional to the ascent and descent values of the line. For example
- * if ascent is 8 units of length, and descent is 2 units; an additional space of 10 units
- * will be distributed as 8 units to top, and 2 units to the bottom of the line. This is
- * the default behavior.
- */
- val Proportional = LineVerticalAlignment(topPercentage = -1)
-
- /**
- * Align the line to the bottom of the space reserved for that line. This means that all
- * extra space as a result of line height is applied to the top of the line. When the
- * provided line height value is smaller than the actual line height, the line will still
- * be aligned to the bottom, therefore the required difference will be subtracted from the
- * top of the line.
- *
- * For example, when line height is 3.em, the lines are aligned to the bottom of 3.em
- * height:
- * <pre>
- * +--------+
- * | |
- * | |
- * | Line1 |
- * |--------|
- * | |
- * | |
- * | Line2 |
- * +--------+
- * </pre>
- */
- val Bottom = LineVerticalAlignment(topPercentage = 100)
- }
-}
-
-/**
- * Defines whether the space that would be added to the top of first line, and bottom of the
- * last line should be trimmed or not. This feature is available only when
- * [PlatformParagraphStyle.includeFontPadding] is false.
- */
-@kotlin.jvm.JvmInline
-@ExperimentalTextApi
-value class LineHeightTrim private constructor(private val value: Int) {
-
- override fun toString(): String {
- return when (value) {
- FirstLineTop.value -> "LineHeightTrim.FirstLineTop"
- LastLineBottom.value -> "LineHeightTrim.LastLineBottom"
- Both.value -> "LineHeightTrim.Both"
- None.value -> "LineHeightTrim.None"
- else -> "Invalid"
- }
- }
-
- companion object {
- private const val FlagTrimTop = 0x00000001
- private const val FlagTrimBottom = 0x00000010
-
- /**
- * Trim the space that would be added to the top of the first line as a result of the
- * line height. Single line text is both the first and last line. This feature is
- * available only when [PlatformParagraphStyle.includeFontPadding] is false.
- *
- * For example, when line height is 3.em, and [LineVerticalAlignment] is
- * [LineVerticalAlignment.Center], the first line has 2.em height and the height from
- * first line baseline to second line baseline is still 3.em:
- * <pre>
- * +--------+
- * | Line1 |
- * | |
- * |--------|
- * | |
- * | Line2 |
- * | |
- * +--------+
- * </pre>
- */
- val FirstLineTop = LineHeightTrim(FlagTrimTop)
-
- /**
- * Trim the space that would be added to the bottom of the last line as a result of the
- * line height. Single line text is both the first and last line. This feature is
- * available only when [PlatformParagraphStyle.includeFontPadding] is false.
- *
- * For example, when line height is 3.em, and [LineVerticalAlignment] is
- * [LineVerticalAlignment.Center], the last line has 2.em height and the height from
- * first line baseline to second line baseline is still 3.em:
- * <pre>
- * +--------+
- * | |
- * | Line1 |
- * | |
- * |--------|
- * | |
- * | Line2 |
- * +--------+
- * </pre>
- */
- val LastLineBottom = LineHeightTrim(FlagTrimBottom)
-
- /**
- * Trim the space that would be added to the top of the first line and bottom of the last
- * line as a result of the line height. This feature is available only when
- * [PlatformParagraphStyle.includeFontPadding] is false.
- *
- * For example, when line height is 3.em, and [LineVerticalAlignment] is
- * [LineVerticalAlignment.Center], the first and last line has 2.em height and the height
- * from first line baseline to second line baseline is still 3.em:
- * <pre>
- * +--------+
- * | Line1 |
- * | |
- * |--------|
- * | |
- * | Line2 |
- * +--------+
- * </pre>
- */
- val Both = LineHeightTrim(FlagTrimTop or FlagTrimBottom)
-
- /**
- * Do not trim first line top or last line bottom.
- *
- * For example, when line height is 3.em, and [LineVerticalAlignment] is
- * [LineVerticalAlignment.Center], the first line height, last line height and the height
- * from first line baseline to second line baseline are 3.em:
- * <pre>
- * +--------+
- * | |
- * | Line1 |
- * | |
- * |--------|
- * | |
- * | Line2 |
- * | |
- * +--------+
- * </pre>
- */
- val None = LineHeightTrim(0)
- }
-
- internal fun isTrimFirstLineTop(): Boolean {
- return value and FlagTrimTop > 0
- }
-
- internal fun isTrimLastLineBottom(): Boolean {
- return value and FlagTrimBottom > 0
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
new file mode 100644
index 0000000..e7664d2
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
@@ -0,0 +1,292 @@
+/*
+ * 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
+ *
+ * http://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 androidx.compose.ui.text.style
+
+import androidx.compose.ui.text.PlatformParagraphStyle
+import androidx.compose.ui.text.ExperimentalTextApi
+
+/**
+ * The configuration for line height such as alignment of the line in the provided line height,
+ * whether to apply additional space as a result of line height to top of first line top and
+ * bottom of last line.
+ *
+ * The configuration is applied only when a line height is defined on the text.
+ *
+ * [trim] feature is available only when [PlatformParagraphStyle.includeFontPadding] is false.
+ *
+ * Please check [Trim] and [Alignment] for more description.
+ *
+ * @param alignment defines how to align the line in the space provided by the line height.
+ * @param trim defines whether the space that would be added to the top of first line, and
+ * bottom of the last line should be trimmed or not. This feature is available only when
+ * [PlatformParagraphStyle.includeFontPadding] is false.
+ */
+@ExperimentalTextApi
+class LineHeightStyle(
+ val alignment: Alignment,
+ val trim: Trim
+) {
+ companion object {
+ /**
+ * The default configuration for [LineHeightStyle]:
+ * - alignment = [Alignment.Proportional]
+ * - trim = [Trim.Both]
+ */
+ val Default = LineHeightStyle(
+ alignment = Alignment.Proportional,
+ trim = Trim.Both
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LineHeightStyle) return false
+
+ if (alignment != other.alignment) return false
+ if (trim != other.trim) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = alignment.hashCode()
+ result = 31 * result + trim.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "LineHeightStyle(" +
+ "alignment=$alignment, " +
+ "trim=$trim" +
+ ")"
+ }
+
+ /**
+ * Defines whether the space that would be added to the top of first line, and bottom of the
+ * last line should be trimmed or not. This feature is available only when
+ * [PlatformParagraphStyle.includeFontPadding] is false.
+ */
+ @kotlin.jvm.JvmInline
+ @ExperimentalTextApi
+ value class Trim private constructor(private val value: Int) {
+
+ override fun toString(): String {
+ return when (value) {
+ FirstLineTop.value -> "LineHeightStyle.Trim.FirstLineTop"
+ LastLineBottom.value -> "LineHeightStyle.Trim.LastLineBottom"
+ Both.value -> "LineHeightStyle.Trim.Both"
+ None.value -> "LineHeightStyle.Trim.None"
+ else -> "Invalid"
+ }
+ }
+
+ companion object {
+ private const val FlagTrimTop = 0x00000001
+ private const val FlagTrimBottom = 0x00000010
+
+ /**
+ * Trim the space that would be added to the top of the first line as a result of the
+ * line height. Single line text is both the first and last line. This feature is
+ * available only when [PlatformParagraphStyle.includeFontPadding] is false.
+ *
+ * For example, when line height is 3.em, and [Alignment] is
+ * [Alignment.Center], the first line has 2.em height and the height from
+ * first line baseline to second line baseline is still 3.em:
+ * <pre>
+ * +--------+
+ * | Line1 |
+ * | |
+ * |--------|
+ * | |
+ * | Line2 |
+ * | |
+ * +--------+
+ * </pre>
+ */
+ val FirstLineTop = Trim(FlagTrimTop)
+
+ /**
+ * Trim the space that would be added to the bottom of the last line as a result of the
+ * line height. Single line text is both the first and last line. This feature is
+ * available only when [PlatformParagraphStyle.includeFontPadding] is false.
+ *
+ * For example, when line height is 3.em, and [Alignment] is
+ * [Alignment.Center], the last line has 2.em height and the height from
+ * first line baseline to second line baseline is still 3.em:
+ * <pre>
+ * +--------+
+ * | |
+ * | Line1 |
+ * | |
+ * |--------|
+ * | |
+ * | Line2 |
+ * +--------+
+ * </pre>
+ */
+ val LastLineBottom = Trim(FlagTrimBottom)
+
+ /**
+ * Trim the space that would be added to the top of the first line and bottom of the last
+ * line as a result of the line height. This feature is available only when
+ * [PlatformParagraphStyle.includeFontPadding] is false.
+ *
+ * For example, when line height is 3.em, and [Alignment] is
+ * [Alignment.Center], the first and last line has 2.em height and the height
+ * from first line baseline to second line baseline is still 3.em:
+ * <pre>
+ * +--------+
+ * | Line1 |
+ * | |
+ * |--------|
+ * | |
+ * | Line2 |
+ * +--------+
+ * </pre>
+ */
+ val Both = Trim(FlagTrimTop or FlagTrimBottom)
+
+ /**
+ * Do not trim first line top or last line bottom.
+ *
+ * For example, when line height is 3.em, and [Alignment] is
+ * [Alignment.Center], the first line height, last line height and the height
+ * from first line baseline to second line baseline are 3.em:
+ * <pre>
+ * +--------+
+ * | |
+ * | Line1 |
+ * | |
+ * |--------|
+ * | |
+ * | Line2 |
+ * | |
+ * +--------+
+ * </pre>
+ */
+ val None = Trim(0)
+ }
+
+ internal fun isTrimFirstLineTop(): Boolean {
+ return value and FlagTrimTop > 0
+ }
+
+ internal fun isTrimLastLineBottom(): Boolean {
+ return value and FlagTrimBottom > 0
+ }
+ }
+
+ /**
+ * Defines how to align the line in the space provided by the line height.
+ */
+ @kotlin.jvm.JvmInline
+ @ExperimentalTextApi
+ value class Alignment private constructor(internal val topPercentage: Int) {
+
+ init {
+ check(topPercentage in 0..100 || topPercentage == -1) {
+ "topRatio should be in [0..100] range or -1"
+ }
+ }
+
+ override fun toString(): String {
+ return when (topPercentage) {
+ Top.topPercentage -> "LineHeightStyle.Alignment.Top"
+ Center.topPercentage -> "LineHeightStyle.Alignment.Center"
+ Proportional.topPercentage -> "LineHeightStyle.Alignment.Proportional"
+ Bottom.topPercentage -> "LineHeightStyle.Alignment.Bottom"
+ else -> "LineHeightStyle.Alignment(topPercentage = $topPercentage)"
+ }
+ }
+
+ companion object {
+ /**
+ * Align the line to the top of the space reserved for that line. This means that all extra
+ * space as a result of line height is applied to the bottom of the line. When the provided
+ * line height value is smaller than the actual line height, the line will still be aligned
+ * to the top, therefore the required difference will be subtracted from the bottom of the
+ * line.
+ *
+ * For example, when line height is 3.em, the lines are aligned to the top of 3.em
+ * height:
+ * <pre>
+ * +--------+
+ * | Line1 |
+ * | |
+ * | |
+ * |--------|
+ * | Line2 |
+ * | |
+ * | |
+ * +--------+
+ * </pre>
+ */
+ val Top = Alignment(topPercentage = 0)
+
+ /**
+ * Align the line to the center of the space reserved for the line. This configuration
+ * distributes additional space evenly between top and bottom of the line.
+ *
+ * For example, when line height is 3.em, the lines are aligned to the center of 3.em
+ * height:
+ * <pre>
+ * +--------+
+ * | |
+ * | Line1 |
+ * | |
+ * |--------|
+ * | |
+ * | Line2 |
+ * | |
+ * +--------+
+ * </pre>
+ */
+ val Center = Alignment(topPercentage = 50)
+
+ /**
+ * Align the line proportional to the ascent and descent values of the line. For example
+ * if ascent is 8 units of length, and descent is 2 units; an additional space of 10 units
+ * will be distributed as 8 units to top, and 2 units to the bottom of the line. This is
+ * the default behavior.
+ */
+ val Proportional = Alignment(topPercentage = -1)
+
+ /**
+ * Align the line to the bottom of the space reserved for that line. This means that all
+ * extra space as a result of line height is applied to the top of the line. When the
+ * provided line height value is smaller than the actual line height, the line will still
+ * be aligned to the bottom, therefore the required difference will be subtracted from the
+ * top of the line.
+ *
+ * For example, when line height is 3.em, the lines are aligned to the bottom of 3.em
+ * height:
+ * <pre>
+ * +--------+
+ * | |
+ * | |
+ * | Line1 |
+ * |--------|
+ * | |
+ * | |
+ * | Line2 |
+ * +--------+
+ * </pre>
+ */
+ val Bottom = Alignment(topPercentage = 100)
+ }
+ }
+}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextDrawStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextDrawStyle.kt
new file mode 100644
index 0000000..c915576
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextDrawStyle.kt
@@ -0,0 +1,113 @@
+/*
+ * 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
+ *
+ * http://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 androidx.compose.ui.text.style
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.graphics.lerp as lerpColor
+import androidx.compose.ui.text.lerpDiscrete
+
+/**
+ * An internal interface to represent possible ways to draw Text e.g. color, brush. This interface
+ * aims to unify unspecified versions of complementary drawing styles. There are some guarantees
+ * as following;
+ *
+ * - If [color] is not [Color.Unspecified], brush is null.
+ * - If [brush] is not null, color is [Color.Unspecified].
+ * - Both [color] can be [Color.Unspecified] and [brush] null, indicating that nothing is specified.
+ * - [SolidColor] brushes are stored as regular [Color].
+ */
+internal interface TextDrawStyle {
+ val color: Color
+
+ val brush: Brush?
+
+ fun merge(other: TextDrawStyle): TextDrawStyle {
+ // This control prevents Color or Unspecified TextDrawStyle to override an existing Brush.
+ // It is a temporary measure to prevent Material Text composables to remove given Brush
+ // from a TextStyle.
+ // TODO(halilibo): Just return other.takeOrElse { this } when Brush is stable.
+ return when {
+ other.brush != null -> other
+ brush != null -> this
+ else -> other.takeOrElse { this }
+ }
+ }
+
+ fun takeOrElse(other: () -> TextDrawStyle): TextDrawStyle {
+ return if (this != Unspecified) this else other()
+ }
+
+ object Unspecified : TextDrawStyle {
+ override val color: Color
+ get() = Color.Unspecified
+
+ override val brush: Brush?
+ get() = null
+ }
+
+ companion object {
+ fun from(color: Color): TextDrawStyle {
+ return if (color.isSpecified) ColorStyle(color) else Unspecified
+ }
+
+ fun from(brush: Brush?): TextDrawStyle {
+ return when (brush) {
+ null -> Unspecified
+ is SolidColor -> from(brush.value)
+ is ShaderBrush -> BrushStyle(brush)
+ }
+ }
+ }
+}
+
+private data class ColorStyle(private val value: Color) : TextDrawStyle {
+ init {
+ require(value.isSpecified) {
+ "ColorStyle value must be specified, use TextDrawStyle.Unspecified instead."
+ }
+ }
+
+ override val color: Color
+ get() = value
+
+ override val brush: Brush?
+ get() = null
+}
+
+private data class BrushStyle(private val value: ShaderBrush) : TextDrawStyle {
+ override val color: Color
+ get() = Color.Unspecified
+
+ override val brush: Brush
+ get() = value
+}
+
+/**
+ * If both TextDrawStyles do not represent a Brush, lerp the color values. Otherwise, lerp
+ * start to end discretely.
+ */
+internal fun lerp(start: TextDrawStyle, stop: TextDrawStyle, fraction: Float): TextDrawStyle {
+ return if ((start !is BrushStyle && stop !is BrushStyle)) {
+ TextDrawStyle.from(lerpColor(start.color, stop.color, fraction))
+ } else {
+ lerpDiscrete(start, stop, fraction)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
index 147df0e..2e43f89 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
@@ -400,6 +400,7 @@
}
}
+ // TODO(b/229518449): Implement an alternative to paint function that takes a brush.
override fun paint(
canvas: Canvas,
color: Color,
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
index 31f02e7..23888e3 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
@@ -16,8 +16,9 @@
package androidx.compose.ui.text
-import androidx.compose.ui.text.style.LineHeightBehavior
-import androidx.compose.ui.text.style.LineVerticalAlignment
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextIndent
@@ -321,64 +322,64 @@
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lerp with null lineHeightBehaviors has null lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = null)
+ fun `lerp with null lineHeightStyles has null lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.5f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lerp with non-null start, null end, closer to start has non-null lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = ParagraphStyle(lineHeightBehavior = null)
+ fun `lerp with non-null start, null end, closer to start has non-null lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = ParagraphStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
- assertThat(lerpedStyle.lineHeightBehavior).isSameInstanceAs(style.lineHeightBehavior)
+ assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lerp with non-null start, null end, closer to end has null lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = ParagraphStyle(lineHeightBehavior = null)
+ fun `lerp with non-null start, null end, closer to end has null lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = ParagraphStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lerp with null start, non-null end, closer to start has null lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `lerp with null start, non-null end, closer to start has null lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lerp with null start, non-null end, closer to end has non-null lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `lerp with null start, non-null end, closer to end has non-null lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
- assertThat(lerpedStyle.lineHeightBehavior).isSameInstanceAs(otherStyle.lineHeightBehavior)
+ assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
fun `equals return false for different line height behavior`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style == otherStyle).isFalse()
}
@@ -386,16 +387,8 @@
@OptIn(ExperimentalTextApi::class)
@Test
fun `equals return true for same line height behavior`() {
- val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
- val otherStyle = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
+ val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style == otherStyle).isTrue()
}
@@ -403,16 +396,8 @@
@OptIn(ExperimentalTextApi::class)
@Test
fun `hashCode is same for same line height behavior`() {
- val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
- val otherStyle = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
+ val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
}
@@ -421,13 +406,15 @@
@Test
fun `hashCode is different for different line height behavior`() {
val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
)
)
val otherStyle = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
)
)
@@ -436,90 +423,95 @@
@OptIn(ExperimentalTextApi::class)
@Test
- fun `copy with lineHeightBehavior returns new lineHeightBehavior`() {
+ fun `copy with lineHeightStyle returns new lineHeightStyle`() {
val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
)
)
- val newLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ val newLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
)
- val newStyle = style.copy(lineHeightBehavior = newLineHeightBehavior)
+ val newStyle = style.copy(lineHeightStyle = newLineHeightStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(newLineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(newLineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `copy without lineHeightBehavior uses existing lineHeightBehavior`() {
+ fun `copy without lineHeightStyle uses existing lineHeightStyle`() {
val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.Both
)
)
val newStyle = style.copy()
- assertThat(newStyle.lineHeightBehavior).isEqualTo(style.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with null lineHeightBehavior uses other's lineHeightBehavior`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `merge with null lineHeightStyle uses other's lineHeightStyle`() {
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(otherStyle.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with non-null lineHeightBehavior, returns original`() {
- val style = ParagraphStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = ParagraphStyle(lineHeightBehavior = null)
+ fun `merge with non-null lineHeightStyle, returns original`() {
+ val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = ParagraphStyle(lineHeightStyle = null)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(style.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with both null lineHeightBehavior returns null`() {
- val style = ParagraphStyle(lineHeightBehavior = null)
- val otherStyle = ParagraphStyle(lineHeightBehavior = null)
+ fun `merge with both null lineHeightStyle returns null`() {
+ val style = ParagraphStyle(lineHeightStyle = null)
+ val otherStyle = ParagraphStyle(lineHeightStyle = null)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isNull()
+ assertThat(newStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with both non-null lineHeightBehavior returns other's lineHeightBehavior`() {
+ fun `merge with both non-null lineHeightStyle returns other's lineHeightStyle`() {
val style = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
)
)
val otherStyle = ParagraphStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.Both
)
)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(otherStyle.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `constructor without lineHeightBehavior sets lineHeightBehavior to null`() {
+ fun `constructor without lineHeightStyle sets lineHeightStyle to null`() {
val style = ParagraphStyle(textAlign = TextAlign.Start)
- assertThat(style.lineHeightBehavior).isNull()
+ assertThat(style.lineHeightStyle).isNull()
}
}
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt
index 35f5344..df9a01b 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/SpanStyleTest.kt
@@ -16,7 +16,9 @@
package androidx.compose.ui.text
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -43,6 +45,7 @@
fun `constructor with default values`() {
val style = SpanStyle()
+ assertThat(style.brush).isNull()
assertThat(style.color).isEqualTo(Color.Unspecified)
assertThat(style.fontSize.isUnspecified).isTrue()
assertThat(style.fontWeight).isNull()
@@ -54,6 +57,36 @@
assertThat(style.fontFamily).isNull()
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with customized brush`() {
+ val brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Red))
+
+ val style = SpanStyle(brush = brush)
+
+ assertThat(style.brush).isEqualTo(brush)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with gradient brush has unspecified color`() {
+ val brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Red))
+
+ val style = SpanStyle(brush = brush)
+
+ assertThat(style.color).isEqualTo(Color.Unspecified)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with SolidColor converts to regular color`() {
+ val brush = SolidColor(Color.Red)
+
+ val style = SpanStyle(brush = brush)
+
+ assertThat(style.color).isEqualTo(Color.Red)
+ }
+
@Test
fun `constructor with customized color`() {
val color = Color.Red
@@ -392,6 +425,34 @@
assertThat(mergedStyle.platformStyle).isNull()
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `merge with brush has other brush and no color`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+
+ val style = SpanStyle(color = Color.Red)
+ val otherStyle = SpanStyle(brush = brush)
+
+ val mergedStyle = style.merge(otherStyle)
+
+ assertThat(mergedStyle.color).isEqualTo(Color.Unspecified)
+ assertThat(mergedStyle.brush).isEqualTo(brush)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `merge with unspecified brush has original brush`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+
+ val style = SpanStyle(brush = brush)
+ val otherStyle = SpanStyle()
+
+ val mergedStyle = style.merge(otherStyle)
+
+ assertThat(mergedStyle.color).isEqualTo(Color.Unspecified)
+ assertThat(mergedStyle.brush).isEqualTo(brush)
+ }
+
@Test
fun `plus operator merges`() {
val style = SpanStyle(
@@ -435,7 +496,7 @@
}
@Test
- fun `lerp color with a is set, and b is Unset`() {
+ fun `when lerp from Specified to Unspecified color, uses Color lerp logic`() {
val t = 0.3f
val color1 = Color.Red
val color2 = Color.Unspecified
@@ -739,4 +800,43 @@
assertThat(lerpedStyle.platformStyle).isNull()
}
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b specified and t is smaller than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = SpanStyle(brush = brush)
+ val style2 = SpanStyle(color = Color.Red)
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.4f)
+
+ assertThat(newStyle.brush).isEqualTo(brush)
+ assertThat(newStyle.color).isEqualTo(Color.Unspecified)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b specified and t is larger than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = SpanStyle(brush = brush)
+ val style2 = SpanStyle(color = Color.Red)
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.6f)
+
+ assertThat(newStyle.brush).isEqualTo(null)
+ assertThat(newStyle.color).isEqualTo(Color.Red)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b not specified and t is larger than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = SpanStyle(brush = brush)
+ val style2 = SpanStyle()
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.6f)
+
+ assertThat(newStyle.brush).isNull()
+ assertThat(newStyle.color).isEqualTo(Color.Unspecified)
+ }
}
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
index 78a0d34..32c238d 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
@@ -17,10 +17,6 @@
package androidx.compose.ui.text
import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assert_
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
@@ -28,81 +24,99 @@
import kotlin.reflect.KType
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class TextSpanParagraphStyleTest {
@Test
fun spanStyle_constructor_is_covered_by_TextStyle() {
- val spanStyleParameters = constructorParams(SpanStyle::class)
+ val spanStyleParameters = constructorParams(SpanStyle::class).toMutableSet().filter {
+ it.name != "platformStyle" && it.name != "textDrawStyle"
+ }
+ val textStyleParameters = mutableSetOf<Parameter>()
+ // In case of multiple constructors, all constructors together should cover
+ // all SpanStyle parameters.
for (constructor in TextStyle::class.constructors) {
- val textStyleParameters = constructorParams(constructor)
// for every SpanStyle parameter, expecting that parameter to be in TextStyle
// this guards that if a parameter is added to SpanStyle, it should be added
// to TextStyle
- if (textStyleParameters.containsAll(spanStyleParameters)) return
+ textStyleParameters += constructorParams(constructor).toSet().filter {
+ it.name != "platformStyle"
+ }
}
- assert_().fail()
+ assertThat(textStyleParameters).containsAtLeastElementsIn(spanStyleParameters)
}
@Test
fun spanStyle_properties_is_covered_by_TextStyle() {
- val spanStyleProperties = memberProperties(SpanStyle::class)
- val textStyleProperties = memberProperties(TextStyle::class)
+ val spanStyleProperties = memberProperties(SpanStyle::class).filter {
+ it.name != "platformStyle" && it.name != "textDrawStyle"
+ }
+ val textStyleProperties = memberProperties(TextStyle::class).filter {
+ it.name != "platformStyle"
+ }
assertThat(textStyleProperties).containsAtLeastElementsIn(spanStyleProperties)
}
@Test
fun paragraphStyle_is_covered_by_TextStyle() {
- val paragraphStyleParameters = constructorParams(ParagraphStyle::class)
-
- for (constructor in TextStyle::class.constructors) {
- val textStyleParameters = constructorParams(constructor)
- // for every ParagraphStyle parameter, expecting that parameter to be in TextStyle
- // this guards that if a parameter is added to ParagraphStyle, it should be added
- // to TextStyle
- if (textStyleParameters.containsAll(paragraphStyleParameters)) return
+ val paragraphStyleProperties = memberProperties(ParagraphStyle::class).filter {
+ it.name != "platformStyle"
}
-
- assert_().fail()
+ val textStyleProperties = memberProperties(TextStyle::class).filter {
+ it.name != "platformStyle"
+ }
+ assertThat(textStyleProperties).containsAtLeastElementsIn(paragraphStyleProperties)
}
@Test
fun paragraphStyle_properties_is_covered_by_TextStyle() {
- val paragraphStyleProperties = memberProperties(ParagraphStyle::class)
- val textStyleProperties = memberProperties(TextStyle::class)
+ val paragraphStyleProperties = memberProperties(ParagraphStyle::class).filter {
+ it.name != "platformStyle"
+ }
+ val textStyleProperties = memberProperties(TextStyle::class).filter {
+ it.name != "platformStyle"
+ }
assertThat(textStyleProperties).containsAtLeastElementsIn(paragraphStyleProperties)
}
@Test
fun textStyle_covered_by_ParagraphStyle_and_SpanStyle() {
- val spanStyleParameters = constructorParams(SpanStyle::class)
- val paragraphStyleParameters = constructorParams(ParagraphStyle::class)
- val allParameters = spanStyleParameters + paragraphStyleParameters
+ val spanStyleParameters = allConstructorParams(SpanStyle::class).filter {
+ it.name != "platformStyle" && it.name != "textDrawStyle"
+ }
+ val paragraphStyleParameters = allConstructorParams(ParagraphStyle::class).filter {
+ it.name != "platformStyle"
+ }
+ val allParameters = (spanStyleParameters + paragraphStyleParameters).toMutableSet()
- for (constructor in TextStyle::class.constructors) {
- val textStyleParameters = constructorParams(constructor)
- // for every TextStyle parameter, expecting that parameter to be in either ParagraphStyle
- // or SpanStyle
- // this guards that if a parameter is added to TextStyle, it should be added
- // to one of SpanStyle or ParagraphStyle
- if (allParameters.containsAll(textStyleParameters) &&
- textStyleParameters.containsAll(allParameters)
- ) return
+ val textStyleParameters = allConstructorParams(TextStyle::class).filter {
+ it.name != "platformStyle" && it.name != "spanStyle" && it.name != "paragraphStyle"
}
- assert_().fail()
+ // for every TextStyle parameter, expecting that parameter to be in either ParagraphStyle
+ // or SpanStyle
+ // this guards that if a parameter is added to TextStyle, it should be added
+ // to one of SpanStyle or ParagraphStyle
+ assertThat(allParameters).containsAtLeastElementsIn(textStyleParameters)
}
@Test
fun testStyle_properties_is_covered_by_ParagraphStyle_and_SpanStyle() {
- val spanStyleProperties = memberProperties(SpanStyle::class)
- val paragraphStyleProperties = memberProperties(ParagraphStyle::class)
+ val spanStyleProperties = memberProperties(SpanStyle::class).filter {
+ it.name != "platformStyle" && it.name != "textDrawStyle"
+ }
+ val paragraphStyleProperties = memberProperties(ParagraphStyle::class).filter {
+ it.name != "platformStyle"
+ }
val allProperties = spanStyleProperties + paragraphStyleProperties
val textStyleProperties = memberProperties(TextStyle::class).filter {
- it.name != "spanStyle" && it.name != "paragraphStyle"
+ it.name != "spanStyle" && it.name != "paragraphStyle" && it.name != "platformStyle"
}
assertThat(allProperties).containsAtLeastElementsIn(textStyleProperties)
}
@@ -111,18 +125,16 @@
return clazz.primaryConstructor?.let { constructorParams(it) } ?: listOf()
}
+ private fun <T : Any> allConstructorParams(clazz: KClass<T>): Set<Parameter> {
+ return clazz.constructors.flatMap { ctor -> constructorParams(ctor) }.toSet()
+ }
+
private fun <T : Any> constructorParams(constructor: KFunction<T>): List<Parameter> {
- return constructor.parameters.map { Parameter(it) }.filter {
- // types of platformStyle is different for each of TextStyle/ParagraphStyle/SpanStyle
- "platformStyle" != it.name
- }
+ return constructor.parameters.map { Parameter(it) }
}
private fun <T : Any> memberProperties(clazz: KClass<T>): Collection<Property> {
- return clazz.memberProperties.map { Property(it) }.filter {
- // types of platformStyle is different for each of TextStyle/ParagraphStyle/SpanStyle
- "platformStyle" != it.name
- }
+ return clazz.memberProperties.map { Property(it) }
}
private data class Parameter(
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
index d8f781b..14649a7 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
@@ -16,15 +16,18 @@
package androidx.compose.ui.text
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.LineVerticalAlignment
-import androidx.compose.ui.text.style.LineHeightBehavior
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
@@ -71,6 +74,45 @@
).isTrue()
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun returns_true_for_color_to_brush_change() {
+ val style = TextStyle(color = Color.Red)
+ assertThat(
+ style.hasSameLayoutAffectingAttributes(TextStyle(brush = SolidColor(Color.Green)))
+ ).isTrue()
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun returns_true_for_brush_to_color_change() {
+ val style = TextStyle(brush = SolidColor(Color.Green))
+ assertThat(
+ style.hasSameLayoutAffectingAttributes(TextStyle(color = Color.Red))
+ ).isTrue()
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun returns_true_for_brush_solid_color_change() {
+ val style = TextStyle(brush = SolidColor(Color.Red))
+ style.copy()
+ assertThat(
+ style.hasSameLayoutAffectingAttributes(TextStyle(brush = SolidColor(Color.Green)))
+ ).isTrue()
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun returns_true_for_brush_shader_change() {
+ val style = TextStyle(brush = Brush.linearGradient(listOf(Color.Black, Color.White)))
+ assertThat(
+ style.hasSameLayoutAffectingAttributes(TextStyle(
+ brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ ))
+ ).isTrue()
+ }
+
@Test
fun returns_true_for_shadow_change() {
val style = TextStyle(shadow = Shadow(color = Color.Red))
@@ -257,17 +299,19 @@
@OptIn(ExperimentalTextApi::class)
@Test
- fun returns_false_for_lineHeightBehavior_change() {
+ fun returns_false_for_lineHeightStyle_change() {
val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
)
)
assertThat(
style.hasSameLayoutAffectingAttributes(
TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.Both
)
)
)
@@ -276,11 +320,12 @@
@Test
fun should_be_updated_when_a_new_attribute_is_added_to_TextStyle() {
- // TextLayoutHelper TextStyle.caReuseLayout is very easy to forget to update when TextStyle
- // changes. Adding this test to fail so that when a new attribute is added to TextStyle
- // it will remind us that we need to update the function.
+ // TextLayoutHelper TextStyle.hasSameLayoutAffectingAttributes is very easy to forget
+ // to update when TextStyle changes. Adding this test to fail so that when a new attribute
+ // is added to TextStyle it will remind us that we need to update the function.
val knownProperties = listOf(
getProperty("color"),
+ getProperty("brush"),
getProperty("shadow"),
getProperty("textDecoration"),
getProperty("fontSize"),
@@ -304,7 +349,7 @@
// ui-text/../androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
getProperty("paragraphStyle"),
getProperty("spanStyle"),
- getProperty("lineHeightBehavior")
+ getProperty("lineHeightStyle")
)
val textStyleProperties = TextStyle::class.memberProperties.map { Property(it) }
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
index b1011c3..0e5d80f 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.text
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.font.FontFamily
@@ -51,6 +52,7 @@
fun test_default_values() {
// We explicitly expect the default values since we do not want to change these values.
resolveDefaults(TextStyle(), LayoutDirection.Ltr).also {
+ assertThat(it.brush).isNull()
assertThat(it.color).isEqualTo(DefaultColor)
assertThat(it.fontSize).isEqualTo(DefaultFontSize)
assertThat(it.fontWeight).isEqualTo(FontWeight.Normal)
@@ -72,6 +74,33 @@
assertThat(it.platformStyle).isNull()
}
}
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun test_use_provided_values_brush() {
+ val brush = Brush.linearGradient(listOf(Color.White, Color.Black))
+
+ assertThat(
+ resolveDefaults(
+ TextStyle(brush = brush),
+ direction = LayoutDirection.Ltr
+ ).brush
+ ).isEqualTo(brush)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun test_use_provided_values_shader_brush_color_unspecified() {
+ val brush = Brush.linearGradient(listOf(Color.White, Color.Black))
+
+ assertThat(
+ resolveDefaults(
+ TextStyle(brush = brush),
+ direction = LayoutDirection.Ltr
+ ).color
+ ).isEqualTo(Color.Unspecified)
+ }
+
@Test
fun test_use_provided_values_color() {
assertThat(
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
index e86dd17..25b6199 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
@@ -17,8 +17,10 @@
package androidx.compose.ui.text
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -27,9 +29,9 @@
import androidx.compose.ui.text.font.lerp
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.LineHeightBehavior
-import androidx.compose.ui.text.style.LineHeightTrim
-import androidx.compose.ui.text.style.LineVerticalAlignment
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
@@ -53,6 +55,7 @@
fun `constructor with default values`() {
val style = TextStyle()
+ assertThat(style.brush).isNull()
assertThat(style.color).isEqualTo(Color.Unspecified)
assertThat(style.fontSize.isUnspecified).isTrue()
assertThat(style.fontWeight).isNull()
@@ -65,6 +68,77 @@
assertThat(style.platformStyle).isNull()
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with customized brush`() {
+ val brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Red))
+
+ val style = TextStyle(brush = brush)
+
+ assertThat(style.brush).isEqualTo(brush)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with gradient brush has unspecified color`() {
+ val brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Red))
+
+ val style = TextStyle(brush = brush)
+
+ assertThat(style.color).isEqualTo(Color.Unspecified)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `constructor with SolidColor converts to regular color`() {
+ val brush = SolidColor(Color.Red)
+
+ val style = TextStyle(brush = brush)
+
+ assertThat(style.color).isEqualTo(Color.Red)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `empty copy with existing brush should not remove brush`() {
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+
+ val style = TextStyle(brush = brush)
+
+ assertThat(style.copy().brush).isEqualTo(brush)
+ }
+
+ @Test
+ fun `empty copy with existing color should not remove color`() {
+ val style = TextStyle(color = Color.Red)
+
+ assertThat(style.copy().color).isEqualTo(Color.Red)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `brush copy with existing color should remove color`() {
+ val style = TextStyle(color = Color.Red)
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+
+ with(style.copy(brush = brush)) {
+ assertThat(this.color).isEqualTo(Color.Unspecified)
+ assertThat(this.brush).isEqualTo(brush)
+ }
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `color copy with existing brush should remove brush`() {
+ val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+ val style = TextStyle(brush = brush)
+
+ with(style.copy(color = Color.Red)) {
+ assertThat(this.color).isEqualTo(Color.Red)
+ assertThat(this.brush).isNull()
+ }
+ }
+
@Test
fun `constructor with customized color`() {
val color = Color.Red
@@ -515,6 +589,34 @@
assertThat(mergedStyle.platformStyle).isNull()
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `merge with brush has other brush and no color`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+
+ val style = TextStyle(color = Color.Red)
+ val otherStyle = TextStyle(brush = brush)
+
+ val mergedStyle = style.merge(otherStyle)
+
+ assertThat(mergedStyle.color).isEqualTo(Color.Unspecified)
+ assertThat(mergedStyle.brush).isEqualTo(brush)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `merge with unspecified brush has original brush`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+
+ val style = TextStyle(brush = brush)
+ val otherStyle = TextStyle()
+
+ val mergedStyle = style.merge(otherStyle)
+
+ assertThat(mergedStyle.color).isEqualTo(Color.Unspecified)
+ assertThat(mergedStyle.brush).isEqualTo(brush)
+ }
+
@Test
fun `plus operator merges other TextStyle`() {
val style = TextStyle(
@@ -1017,6 +1119,45 @@
assertThat(newStyle.platformStyle).isEqualTo(style.platformStyle)
}
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b specified and t is smaller than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = TextStyle(brush = brush)
+ val style2 = TextStyle(color = Color.Red)
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.4f)
+
+ assertThat(newStyle.brush).isEqualTo(brush)
+ assertThat(newStyle.color).isEqualTo(Color.Unspecified)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b specified and t is larger than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = TextStyle(brush = brush)
+ val style2 = TextStyle(color = Color.Red)
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.6f)
+
+ assertThat(newStyle.brush).isEqualTo(null)
+ assertThat(newStyle.color).isEqualTo(Color.Red)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun `lerp brush with a specified, b not specified and t is larger than half`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val style1 = TextStyle(brush = brush)
+ val style2 = TextStyle()
+
+ val newStyle = lerp(start = style1, stop = style2, fraction = 0.6f)
+
+ assertThat(newStyle.brush).isNull()
+ assertThat(newStyle.color).isEqualTo(Color.Unspecified)
+ }
+
@Test
fun `toSpanStyle return attributes with correct values`() {
val color = Color.Red
@@ -1073,14 +1214,69 @@
@OptIn(ExperimentalTextApi::class)
@Test
+ fun `toSpanStyle return attributes with correct values for brush`() {
+ val brush = Brush.linearGradient(listOf(Color.Blue, Color.Red))
+ val fontSize = 56.sp
+ val fontWeight = FontWeight.Bold
+ val fontStyle = FontStyle.Italic
+ val fontSynthesis = FontSynthesis.All
+ val fontFamily = FontFamily.Default
+ val fontFeatureSettings = "font feature settings"
+ val letterSpacing = 0.2.sp
+ val baselineShift = BaselineShift.Subscript
+ val textGeometricTransform = TextGeometricTransform(scaleX = 0.5f, skewX = 0.6f)
+ val localeList = LocaleList("tr-TR")
+ val background = Color.Yellow
+ val decoration = TextDecoration.Underline
+ val shadow = Shadow(color = Color.Green, offset = Offset(2f, 4f))
+
+ val style = TextStyle(
+ brush = brush,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = decoration,
+ shadow = shadow
+ )
+
+ assertThat(style.toSpanStyle()).isEqualTo(
+ SpanStyle(
+ brush = brush,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis,
+ fontFamily = fontFamily,
+ fontFeatureSettings = fontFeatureSettings,
+ letterSpacing = letterSpacing,
+ baselineShift = baselineShift,
+ textGeometricTransform = textGeometricTransform,
+ localeList = localeList,
+ background = background,
+ textDecoration = decoration,
+ shadow = shadow
+ )
+ )
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
fun `toParagraphStyle return attributes with correct values`() {
val textAlign = TextAlign.Justify
val textDirection = TextDirection.Rtl
val lineHeight = 100.sp
val textIndent = TextIndent(firstLine = 20.sp, restLine = 40.sp)
- val lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center,
- trim = LineHeightTrim.None
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
)
val style = TextStyle(
@@ -1088,7 +1284,7 @@
textDirection = textDirection,
lineHeight = lineHeight,
textIndent = textIndent,
- lineHeightBehavior = lineHeightBehavior
+ lineHeightStyle = lineHeightStyle
)
assertThat(style.toParagraphStyle()).isEqualTo(
@@ -1097,7 +1293,7 @@
textDirection = textDirection,
lineHeight = lineHeight,
textIndent = textIndent,
- lineHeightBehavior = lineHeightBehavior
+ lineHeightStyle = lineHeightStyle
)
)
}
@@ -1109,64 +1305,64 @@
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lineHeightBehavior lerp with null lineHeightBehaviors has null lineHeightBehavior`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = null)
+ fun `lineHeightStyle lerp with null lineHeightStyles has null lineHeightStyle`() {
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.5f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lineHeightBehavior lerp with non-null start, null end, closer to start has non-null`() {
- val style = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = TextStyle(lineHeightBehavior = null)
+ fun `lineHeightStyle lerp with non-null start, null end, closer to start has non-null`() {
+ val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = TextStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
- assertThat(lerpedStyle.lineHeightBehavior).isSameInstanceAs(style.lineHeightBehavior)
+ assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lineHeightBehavior lerp with non-null start, null end, closer to end has null`() {
- val style = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = TextStyle(lineHeightBehavior = null)
+ fun `lineHeightStyle lerp with non-null start, null end, closer to end has null`() {
+ val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = TextStyle(lineHeightStyle = null)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lineHeightBehavior lerp with null start, non-null end, closer to start has null`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `lineHeightStyle lerp with null start, non-null end, closer to start has null`() {
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
- assertThat(lerpedStyle.lineHeightBehavior).isNull()
+ assertThat(lerpedStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `lineHeightBehavior lerp with null start, non-null end, closer to end has non-null`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `lineHeightStyle lerp with null start, non-null end, closer to end has non-null`() {
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
- assertThat(lerpedStyle.lineHeightBehavior).isSameInstanceAs(otherStyle.lineHeightBehavior)
+ assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
fun `equals return false for different line height behavior`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style == otherStyle).isFalse()
}
@@ -1174,16 +1370,8 @@
@OptIn(ExperimentalTextApi::class)
@Test
fun `equals return true for same line height behavior`() {
- val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
- val otherStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
+ val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style == otherStyle).isTrue()
}
@@ -1191,16 +1379,8 @@
@OptIn(ExperimentalTextApi::class)
@Test
fun `hashCode is same for same line height behavior`() {
- val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
- val otherStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- )
+ val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
}
@@ -1209,13 +1389,15 @@
@Test
fun `hashCode is different for different line height behavior`() {
val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
)
)
val otherStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
)
)
@@ -1224,91 +1406,96 @@
@OptIn(ExperimentalTextApi::class)
@Test
- fun `copy with lineHeightBehavior returns new lineHeightBehavior`() {
+ fun `copy with lineHeightStyle returns new lineHeightStyle`() {
val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
)
)
- val newLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ val newLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
)
- val newStyle = style.copy(lineHeightBehavior = newLineHeightBehavior)
+ val newStyle = style.copy(lineHeightStyle = newLineHeightStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(newLineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(newLineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `copy without lineHeightBehavior uses existing lineHeightBehavior`() {
+ fun `copy without lineHeightStyle uses existing lineHeightStyle`() {
val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
)
)
val newStyle = style.copy()
- assertThat(newStyle.lineHeightBehavior).isEqualTo(style.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with null lineHeightBehavior uses other's lineHeightBehavior`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
+ fun `merge with null lineHeightStyle uses other's lineHeightStyle`() {
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = LineHeightStyle.Default)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(otherStyle.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with non-null lineHeightBehavior, returns original`() {
- val style = TextStyle(lineHeightBehavior = LineHeightBehavior.Default)
- val otherStyle = TextStyle(lineHeightBehavior = null)
+ fun `merge with non-null lineHeightStyle, returns original`() {
+ val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
+ val otherStyle = TextStyle(lineHeightStyle = null)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(style.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with both null lineHeightBehavior returns null`() {
- val style = TextStyle(lineHeightBehavior = null)
- val otherStyle = TextStyle(lineHeightBehavior = null)
+ fun `merge with both null lineHeightStyle returns null`() {
+ val style = TextStyle(lineHeightStyle = null)
+ val otherStyle = TextStyle(lineHeightStyle = null)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isNull()
+ assertThat(newStyle.lineHeightStyle).isNull()
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `merge with both non-null lineHeightBehavior returns other's lineHeightBehavior`() {
+ fun `merge with both non-null lineHeightStyle returns other's lineHeightStyle`() {
val style = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
)
)
val otherStyle = TextStyle(
- lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
+ lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.Both
)
)
val newStyle = style.merge(otherStyle)
- assertThat(newStyle.lineHeightBehavior).isEqualTo(otherStyle.lineHeightBehavior)
+ assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
}
@OptIn(ExperimentalTextApi::class)
@Test
- fun `constructor without lineHeightBehavior sets lineHeightBehavior to null`() {
+ fun `constructor without lineHeightStyle sets lineHeightStyle to null`() {
val style = TextStyle(textAlign = TextAlign.Start)
- assertThat(style.lineHeightBehavior).isNull()
+ assertThat(style.lineHeightStyle).isNull()
}
@Test
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightBehaviorTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightBehaviorTest.kt
deleted file mode 100644
index a3a2d84..0000000
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightBehaviorTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2019 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
- *
- * http://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.
- */
-@file:OptIn(ExperimentalTextApi::class)
-
-package androidx.compose.ui.text.style
-
-import androidx.compose.ui.text.ExperimentalTextApi
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class LineHeightBehaviorTest {
-
- @Test
- fun equals_returns_false_for_different_distribution() {
- val lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
- )
- assertThat(lineHeightBehavior.equals(otherLineHeightBehavior)).isFalse()
- }
-
- @Test
- fun equals_returns_false_for_different_trim() {
- val lineHeightBehavior = LineHeightBehavior(
- trim = LineHeightTrim.None
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- trim = LineHeightTrim.Both
- )
- assertThat(lineHeightBehavior.equals(otherLineHeightBehavior)).isFalse()
- }
-
- @Test
- fun equals_returns_true_for_same_attributes() {
- val lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center,
- trim = LineHeightTrim.FirstLineTop
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center,
- trim = LineHeightTrim.FirstLineTop
- )
- assertThat(lineHeightBehavior.equals(otherLineHeightBehavior)).isTrue()
- }
-
- @Test
- fun hashCode_is_different_for_different_distribution() {
- val lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Bottom
- )
- assertThat(lineHeightBehavior.hashCode()).isNotEqualTo(otherLineHeightBehavior.hashCode())
- }
-
- @Test
- fun hashCode_is_different_for_different_trim() {
- val lineHeightBehavior = LineHeightBehavior(
- trim = LineHeightTrim.None
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- trim = LineHeightTrim.Both
- )
- assertThat(lineHeightBehavior.hashCode()).isNotEqualTo(otherLineHeightBehavior.hashCode())
- }
-
- @Test
- fun hashCode_is_same_for_same_attributes() {
- val lineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center,
- trim = LineHeightTrim.Both
- )
- val otherLineHeightBehavior = LineHeightBehavior(
- alignment = LineVerticalAlignment.Center,
- trim = LineHeightTrim.Both
- )
- assertThat(lineHeightBehavior.hashCode()).isEqualTo(otherLineHeightBehavior.hashCode())
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightStyleTest.kt
new file mode 100644
index 0000000..46c7cfd
--- /dev/null
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/LineHeightStyleTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 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
+ *
+ * http://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.
+ */
+@file:OptIn(ExperimentalTextApi::class)
+
+package androidx.compose.ui.text.style
+
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.LineHeightStyle.Alignment
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class LineHeightStyleTest {
+
+ @Test
+ fun equals_returns_false_for_different_alignment() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.None
+ )
+ assertThat(lineHeightStyle.equals(otherLineHeightStyle)).isFalse()
+ }
+
+ @Test
+ fun equals_returns_false_for_different_trim() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
+ )
+ assertThat(lineHeightStyle.equals(otherLineHeightStyle)).isFalse()
+ }
+
+ @Test
+ fun equals_returns_true_for_same_attributes() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.FirstLineTop
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.FirstLineTop
+ )
+ assertThat(lineHeightStyle.equals(otherLineHeightStyle)).isTrue()
+ }
+
+ @Test
+ fun hashCode_is_different_for_different_alignment() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Bottom,
+ trim = Trim.Both
+ )
+ assertThat(lineHeightStyle.hashCode()).isNotEqualTo(otherLineHeightStyle.hashCode())
+ }
+
+ @Test
+ fun hashCode_is_different_for_different_trim() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.None
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
+ )
+ assertThat(lineHeightStyle.hashCode()).isNotEqualTo(otherLineHeightStyle.hashCode())
+ }
+
+ @Test
+ fun hashCode_is_same_for_same_attributes() {
+ val lineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
+ )
+ val otherLineHeightStyle = LineHeightStyle(
+ alignment = Alignment.Center,
+ trim = Trim.Both
+ )
+ assertThat(lineHeightStyle.hashCode()).isEqualTo(otherLineHeightStyle.hashCode())
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextDrawStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextDrawStyleTest.kt
new file mode 100644
index 0000000..86e5deb
--- /dev/null
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/style/TextDrawStyleTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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
+ *
+ * http://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 androidx.compose.ui.text.style
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.text.lerpDiscrete
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TextDrawStyleTest {
+
+ private val defaultBrush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+
+ @Test
+ fun `color is not equal to unspecified`() {
+ val color = TextDrawStyle.from(Color.Red)
+ assertThat(color == TextDrawStyle.Unspecified).isFalse()
+ }
+
+ @Test
+ fun `brush is not equal to color`() {
+ val color = TextDrawStyle.from(Color.Red)
+ val brush = TextDrawStyle.from(defaultBrush)
+ assertThat(color == brush).isFalse()
+ }
+
+ @Test
+ fun `different colors are not equal`() {
+ val color = TextDrawStyle.from(Color.Red)
+ val otherColor = TextDrawStyle.from(Color.Blue)
+ assertThat(color == otherColor).isFalse()
+ }
+
+ @Test
+ fun `same colors should be equal`() {
+ val color = TextDrawStyle.from(Color.Red)
+ val otherColor = TextDrawStyle.from(Color.Red)
+ assertThat(color == otherColor).isTrue()
+ }
+
+ @Test
+ fun `unspecified color initiates an Unspecified TextDrawStyle`() {
+ val unspecified = TextDrawStyle.from(Color.Unspecified)
+ assertThat(unspecified).isEqualTo(TextDrawStyle.Unspecified)
+ assertThat(unspecified.color).isEqualTo(Color.Unspecified)
+ assertThat(unspecified.brush).isNull()
+ }
+
+ @Test
+ fun `specified color initiates only color`() {
+ val specified = TextDrawStyle.from(Color.Red)
+ assertThat(specified.color).isEqualTo(Color.Red)
+ assertThat(specified.brush).isNull()
+ }
+
+ @Test
+ fun `SolidColor is converted to color`() {
+ val specified = TextDrawStyle.from(SolidColor(Color.Red))
+ assertThat(specified.color).isEqualTo(Color.Red)
+ assertThat(specified.brush).isNull()
+ }
+
+ @Test
+ fun `ShaderBrush initiates a brush`() {
+ val specified = TextDrawStyle.from(defaultBrush)
+ assertThat(specified.color).isEqualTo(Color.Unspecified)
+ assertThat(specified.brush).isEqualTo(defaultBrush)
+ }
+
+ @Test
+ fun `merging unspecified with anything returns anything`() {
+ val current = TextDrawStyle.Unspecified
+
+ val other = TextDrawStyle.from(Color.Red)
+ assertThat(current.merge(other).color).isEqualTo(Color.Red)
+ assertThat(current.merge(other).brush).isNull()
+
+ val other2 = TextDrawStyle.from(defaultBrush)
+ assertThat(current.merge(other2).color).isEqualTo(Color.Unspecified)
+ assertThat(current.merge(other2).brush).isEqualTo(defaultBrush)
+ }
+
+ // TODO(halilibo): Update when Brush is stable.
+ @Test
+ fun `merging brush with color returns brush`() {
+ val current = TextDrawStyle.from(defaultBrush)
+
+ val other = TextDrawStyle.from(Color.Red)
+ assertThat(current.merge(other).color).isEqualTo(Color.Unspecified)
+ assertThat(current.merge(other).brush).isEqualTo(defaultBrush)
+ }
+
+ @Test
+ fun `merging color with brush returns brush`() {
+ val current = TextDrawStyle.from(Color.Red)
+
+ val other = TextDrawStyle.from(defaultBrush)
+ assertThat(current.merge(other).brush).isEqualTo(defaultBrush)
+ }
+
+ @Test
+ fun `merging color with color returns color`() {
+ val current = TextDrawStyle.from(Color.Blue)
+
+ val other = TextDrawStyle.from(Color.Red)
+ assertThat(current.merge(other).color).isEqualTo(Color.Red)
+ }
+
+ @Test
+ fun `merging brush with brush returns brush`() {
+ val current = TextDrawStyle.from(defaultBrush)
+
+ val newBrush = Brush.linearGradient(listOf(Color.White, Color.Black))
+ val other = TextDrawStyle.from(newBrush)
+ assertThat(current.merge(other).brush).isEqualTo(newBrush)
+ }
+
+ @Test
+ fun `lerps colors if both ends are not brush`() {
+ val start = TextDrawStyle.Unspecified
+ val stop = TextDrawStyle.from(Color.Red)
+
+ assertThat(lerp(start, stop, fraction = 0.4f)).isEqualTo(
+ TextDrawStyle.from(lerp(Color.Unspecified, Color.Red, 0.4f))
+ )
+ }
+
+ @Test
+ fun `lerps discrete if at least one end is brush`() {
+ val start = TextDrawStyle.from(defaultBrush)
+ val stop = TextDrawStyle.from(Color.Red)
+
+ assertThat(lerp(start, stop, fraction = 0.4f)).isEqualTo(
+ lerpDiscrete(start, stop, 0.4f)
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index f203b26..e59a90d 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1758,14 +1758,19 @@
}
public interface BeyondBoundsLayout {
- method public <T> T? searchBeyondBounds(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayoutScope,? extends T> block);
+ method public <T> T? layout(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope,? extends T> block);
}
- @kotlin.jvm.JvmInline public final value class BeyondBoundsLayoutDirection {
- field public static final androidx.compose.ui.layout.BeyondBoundsLayoutDirection.Companion Companion;
+ public static interface BeyondBoundsLayout.BeyondBoundsScope {
+ method public boolean getHasMoreContent();
+ property public abstract boolean hasMoreContent;
}
- public static final class BeyondBoundsLayoutDirection.Companion {
+ @kotlin.jvm.JvmInline public static final value class BeyondBoundsLayout.LayoutDirection {
+ field public static final androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion Companion;
+ }
+
+ public static final class BeyondBoundsLayout.LayoutDirection.Companion {
method public int getAbove();
method public int getAfter();
method public int getBefore();
@@ -1785,11 +1790,6 @@
property public static final androidx.compose.ui.modifier.ProvidableModifierLocal<androidx.compose.ui.layout.BeyondBoundsLayout> ModifierLocalBeyondBoundsLayout;
}
- public interface BeyondBoundsLayoutScope {
- method public boolean getHasMoreContent();
- property public abstract boolean hasMoreContent;
- }
-
@androidx.compose.runtime.Stable public interface ContentScale {
method public long computeScaleFactor(long srcSize, long dstSize);
field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 8cd62e2..5f6ea21 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1894,14 +1894,19 @@
}
public interface BeyondBoundsLayout {
- method public <T> T? searchBeyondBounds(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayoutScope,? extends T> block);
+ method public <T> T? layout(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope,? extends T> block);
}
- @kotlin.jvm.JvmInline public final value class BeyondBoundsLayoutDirection {
- field public static final androidx.compose.ui.layout.BeyondBoundsLayoutDirection.Companion Companion;
+ public static interface BeyondBoundsLayout.BeyondBoundsScope {
+ method public boolean getHasMoreContent();
+ property public abstract boolean hasMoreContent;
}
- public static final class BeyondBoundsLayoutDirection.Companion {
+ @kotlin.jvm.JvmInline public static final value class BeyondBoundsLayout.LayoutDirection {
+ field public static final androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion Companion;
+ }
+
+ public static final class BeyondBoundsLayout.LayoutDirection.Companion {
method public int getAbove();
method public int getAfter();
method public int getBefore();
@@ -1921,11 +1926,6 @@
property public static final androidx.compose.ui.modifier.ProvidableModifierLocal<androidx.compose.ui.layout.BeyondBoundsLayout> ModifierLocalBeyondBoundsLayout;
}
- public interface BeyondBoundsLayoutScope {
- method public boolean getHasMoreContent();
- property public abstract boolean hasMoreContent;
- }
-
@androidx.compose.runtime.Stable public interface ContentScale {
method public long computeScaleFactor(long srcSize, long dstSize);
field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 1b85c50..96041ab 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1758,14 +1758,19 @@
}
public interface BeyondBoundsLayout {
- method public <T> T? searchBeyondBounds(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayoutScope,? extends T> block);
+ method public <T> T? layout(int direction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope,? extends T> block);
}
- @kotlin.jvm.JvmInline public final value class BeyondBoundsLayoutDirection {
- field public static final androidx.compose.ui.layout.BeyondBoundsLayoutDirection.Companion Companion;
+ public static interface BeyondBoundsLayout.BeyondBoundsScope {
+ method public boolean getHasMoreContent();
+ property public abstract boolean hasMoreContent;
}
- public static final class BeyondBoundsLayoutDirection.Companion {
+ @kotlin.jvm.JvmInline public static final value class BeyondBoundsLayout.LayoutDirection {
+ field public static final androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion Companion;
+ }
+
+ public static final class BeyondBoundsLayout.LayoutDirection.Companion {
method public int getAbove();
method public int getAfter();
method public int getBefore();
@@ -1785,11 +1790,6 @@
property public static final androidx.compose.ui.modifier.ProvidableModifierLocal<androidx.compose.ui.layout.BeyondBoundsLayout> ModifierLocalBeyondBoundsLayout;
}
- public interface BeyondBoundsLayoutScope {
- method public boolean getHasMoreContent();
- property public abstract boolean hasMoreContent;
- }
-
@androidx.compose.runtime.Stable public interface ContentScale {
method public long computeScaleFactor(long srcSize, long dstSize);
field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt
new file mode 100644
index 0000000..0ceb743
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusSearchNonPlacedItemsTest.kt
@@ -0,0 +1,550 @@
+/*
+ * 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
+ *
+ * http://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 androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FocusSearchNonPlacedItemsTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private lateinit var focusManager: FocusManager
+ private val initialFocus: FocusRequester = FocusRequester()
+
+ @Test
+ fun moveFocusPrevious_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Previous)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusNext_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusLeft_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Left)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusRight_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Right)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusUp_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutVertically(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Up)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusDown_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutVertically(parent, unplacedIndices = listOf(1)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Down)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusIn_skipsUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(0), initialFocus) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ @OptIn(ExperimentalComposeUiApi::class)
+ focusManager.moveFocus(FocusDirection.In)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusPrevious_skipsFirstUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(0)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10, initialFocus)
+ FocusableBox(item3, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Previous)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isTrue()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusNext_skipsLastUnplacedItem() {
+ // Arrange.
+ val (parent, item1, item2, item3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(2)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10, initialFocus)
+ FocusableBox(item3, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isTrue()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusPrevious_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 10, 10, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Previous)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusNext_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusLeft_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Left)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusRight_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutHorizontally(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Right)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isTrue()
+ }
+ }
+
+ @Test
+ fun moveFocusUp_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutVertically(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10, initialFocus)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Up)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isTrue()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isFalse()
+ }
+ }
+
+ @Test
+ fun moveFocusDown_skipsMultipleUnplacedItems() {
+ // Arrange.
+ val (parent, item1, item2, item3, item4) = List(5) { mutableStateOf(false) }
+ rule.setContentForTest {
+ LayoutVertically(parent, unplacedIndices = listOf(1, 2)) {
+ FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+ FocusableBox(item2, 10, 10, 10, 10)
+ FocusableBox(item3, 20, 20, 10, 10)
+ FocusableBox(item4, 20, 20, 10, 10)
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Down)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parent.value).isFalse()
+ assertThat(item1.value).isFalse()
+ assertThat(item2.value).isFalse()
+ assertThat(item3.value).isFalse()
+ assertThat(item4.value).isTrue()
+ }
+ }
+
+ @Composable
+ internal fun FocusableBox(
+ isFocused: MutableState<Boolean>,
+ x: Dp,
+ y: Dp,
+ width: Dp,
+ height: Dp,
+ focusRequester: FocusRequester? = null,
+ content: @Composable BoxScope.() -> Unit = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .offset(x, y)
+ .size(width, height)
+ .focusRequester(focusRequester ?: remember { FocusRequester() })
+ .onFocusChanged { isFocused.value = it.isFocused }
+ .focusTarget(),
+ content = content
+ )
+ }
+
+ @Composable
+ fun LayoutHorizontally(
+ isFocused: MutableState<Boolean>,
+ unplacedIndices: List<Int>,
+ focusRequester: FocusRequester? = null,
+ content: @Composable () -> Unit,
+ ) {
+ Layout(
+ content = content,
+ modifier = Modifier
+ .focusRequester(focusRequester ?: remember { FocusRequester() })
+ .onFocusChanged { isFocused.value = it.isFocused }
+ .focusTarget()
+ ) { measurables, constraints ->
+ var width = 0
+ var height = 0
+ val placeables = measurables.map {
+ it.measure(constraints).run {
+ val offset = IntOffset(width, height)
+ width += this.width
+ height = maxOf(height, this.height)
+ Pair(this, offset)
+ }
+ }
+
+ layout(width, height) {
+ placeables.forEachIndexed { index, placeable ->
+ if (!unplacedIndices.contains(index)) {
+ placeable.first.placeRelative(placeable.second)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun LayoutVertically(
+ isFocused: MutableState<Boolean>,
+ unplacedIndices: List<Int>,
+ focusRequester: FocusRequester? = null,
+ content: @Composable () -> Unit
+ ) {
+ Layout(
+ content = content,
+ modifier = Modifier
+ .focusRequester(focusRequester ?: remember { FocusRequester() })
+ .onFocusChanged { isFocused.value = it.isFocused }
+ .focusTarget()
+ ) { measurables, constraints ->
+ var width = 0
+ var height = 0
+ val placeables = measurables.map {
+ it.measure(constraints).run {
+ val offset = IntOffset(width, height)
+ width = maxOf(width, this.width)
+ height += this.height
+ Pair(this, offset)
+ }
+ }
+
+ layout(width, height) {
+ placeables.forEachIndexed { index, placeable ->
+ if (!unplacedIndices.contains(index)) {
+ placeable.first.placeRelative(placeable.second)
+ }
+ }
+ }
+ }
+ }
+
+ private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
+ setContent {
+ focusManager = LocalFocusManager.current
+ composable()
+ }
+ rule.runOnIdle { initialFocus.requestFocus() }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
index ca0e3ac..1f7c53e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/BeyondBoundsLayoutTest.kt
@@ -19,7 +19,9 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.test.junit4.createComposeRule
@@ -58,7 +60,7 @@
// Act.
val returnValue = rule.runOnIdle {
- parent!!.searchBeyondBounds(After) {
+ parent!!.layout(After) {
blockInvoked = true
OperationResult
}
@@ -87,7 +89,7 @@
// Act.
val returnValue = rule.runOnIdle {
assertThat(parent).isNotNull()
- parent?.searchBeyondBounds<Int>(After) {
+ parent?.layout<Int>(After) {
blockInvokeCount++
// Always return null, to continue searching and indicate that
// we didn't find the item we were looking for.
@@ -119,7 +121,7 @@
// Act.
val returnValue = rule.runOnIdle {
assertThat(parent).isNotNull()
- parent?.searchBeyondBounds(After) {
+ parent?.layout(After) {
val returnValue = if (hasMoreContent) null else OperationResult
callMap[++iterationCount] = returnValue
returnValue
@@ -156,7 +158,7 @@
// Act.
val returnValue = rule.runOnIdle {
assertThat(parent).isNotNull()
- parent?.searchBeyondBounds(After) {
+ parent?.layout(After) {
// After the first item was added, we were able to perform our operation.
OperationResult
}
@@ -184,7 +186,7 @@
val returnValue = rule.runOnIdle {
assertThat(parent).isNotNull()
var iterationCount = 0
- parent?.searchBeyondBounds(After) {
+ parent?.layout(After) {
if (iterationCount++ < 3) null else OperationResult
}
}
@@ -214,12 +216,12 @@
// Act.
rule.runOnIdle {
assertThat(parent).isNotNull()
- returnValue1 = parent?.searchBeyondBounds<Int>(After) {
+ returnValue1 = parent?.layout<Int>(After) {
block1InvokeCount++
// Always return null, to indicate that we didn't find the item we were looking for.
null
}
- returnValue2 = parent?.searchBeyondBounds<Int>(After) {
+ returnValue2 = parent?.layout<Int>(After) {
block2InvokeCount++
// Always return null, to indicate that we didn't find the item we were looking for.
null
@@ -255,13 +257,13 @@
// Act.
rule.runOnIdle {
assertThat(parent).isNotNull()
- returnValue1 = parent?.searchBeyondBounds<Int>(direction) {
+ returnValue1 = parent?.layout<Int>(direction) {
block1InvokeCount++
if (!hasMoreContent) {
// Re-entrant call.
returnValue2 =
- parent?.searchBeyondBounds<Int>(direction) {
+ parent?.layout<Int>(direction) {
block2InvokeCount++
// Always return null, to indicate that we didn't find the item we were looking for.
null
@@ -283,9 +285,9 @@
private fun Modifier.parentWithoutNonVisibleItems(): Modifier {
return this.modifierLocalProvider(ModifierLocalBeyondBoundsLayout) {
object : BeyondBoundsLayout {
- override fun <T> searchBeyondBounds(
- direction: BeyondBoundsLayoutDirection,
- block: BeyondBoundsLayoutScope.() -> T?
+ override fun <T> layout(
+ direction: LayoutDirection,
+ block: BeyondBoundsScope.() -> T?
): T? = null
}
}
@@ -294,20 +296,23 @@
private fun Modifier.parentWithFiveNonVisibleItems(): Modifier {
return this.modifierLocalProvider(ModifierLocalBeyondBoundsLayout) {
object : BeyondBoundsLayout {
- override fun <T> searchBeyondBounds(
- direction: BeyondBoundsLayoutDirection,
- block: BeyondBoundsLayoutScope.() -> T?
+ override fun <T> layout(
+ direction: LayoutDirection,
+ block: BeyondBoundsScope.() -> T?
): T? {
var count = 5
var result: T? = null
while (count-- > 0 && result == null) {
- result = block.invoke(BeyondBoundsScope(hasMoreContent = count > 0))
+ result = block.invoke(
+ object : BeyondBoundsScope {
+ override val hasMoreContent: Boolean
+ get() = count > 0
+ }
+ )
}
return result
}
}
}
}
-
- private class BeyondBoundsScope(override val hasMoreContent: Boolean) : BeyondBoundsLayoutScope
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 4f17d628..634dcf9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -54,6 +54,7 @@
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
@@ -1903,6 +1904,101 @@
)
}
+ @Test
+ fun reusingWithNestedSubcomposeLayoutInside() {
+ val slotState = mutableStateOf(0)
+
+ rule.setContent {
+ SubcomposeLayout(
+ remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
+ ) { constraints ->
+ val slot = slotState.value
+ val child = subcompose(slot) {
+ ReusableContent(slot) {
+ Box {
+ SubcomposeLayout(Modifier.testTag("$slot")) { constraints ->
+ val placeable = subcompose(0) {
+ Box(modifier = Modifier.size(10.dp))
+ }.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+ }
+ }.first().measure(constraints)
+ layout(child.width, child.height) {
+ child.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ slotState.value = 1
+ }
+
+ rule.runOnIdle {
+ slotState.value = 2
+ }
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ rule.onNodeWithTag("0").assertDoesNotExist()
+ }
+
+ @Test
+ fun disposingPrecomposedItemInTheNestedSubcomposeLayout() {
+ var needSlot by mutableStateOf(true)
+ val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
+
+ rule.setContent {
+ SubcomposeLayout(
+ remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
+ ) { constraints ->
+ val child = if (needSlot) {
+ subcompose(0) {
+ Box {
+ SubcomposeLayout(state = state, Modifier.testTag("0")) { constraints ->
+ if (needSlot) {
+ val placeable = subcompose(0) {
+ Box(modifier = Modifier.size(10.dp))
+ }.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ } else {
+ layout(100, 100) { }
+ }
+ }
+ }
+ }.first().measure(constraints)
+ } else {
+ null
+ }
+ layout(100, 100) {
+ child?.place(0, 0)
+ }
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ state.precompose(1) {
+ Box(modifier = Modifier.size(10.dp).testTag("1"))
+ }
+ }
+
+ rule.runOnIdle {
+ needSlot = false
+ }
+
+ rule.runOnIdle {
+ handle.dispose()
+ }
+
+ rule.onNodeWithTag("1").assertDoesNotExist()
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ }
+
private fun composeItems(
state: SubcomposeLayoutState,
items: MutableState<List<Int>>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
index 8661f63..4ff5414 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
@@ -54,8 +54,10 @@
private var parent: FocusEventModifierLocal? = null
private val children = mutableVectorOf<FocusEventModifierLocal>()
- // This is the list of modifiers that contribute to the focus event's state.
- // When there are multiple, all FocusModifier states must be considered when notifying an event.
+ /**
+ * This is the list of modifiers that contribute to the focus event's state.
+ * When there are multiple, all FocusModifier states must be considered when notifying an event.
+ */
private val focusModifiers = mutableVectorOf<FocusModifier>()
override val key: ProvidableModifierLocal<FocusEventModifierLocal?>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
index ba5cb16..4dd57ea 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
@@ -55,26 +55,30 @@
val isCaptured: Boolean
}
-// Different states of the focus system. These are the states used by the Focus Nodes.
+/** Different states of the focus system. These are the states used by the Focus Nodes. */
internal enum class FocusStateImpl : FocusState {
- // The focusable component is currently active (i.e. it receives key events).
+ /** The focusable component is currently active (i.e. it receives key events). */
Active,
- // One of the descendants of the focusable component is Active.
+ /** One of the descendants of the focusable component is Active. */
ActiveParent,
- // The focusable component is currently active (has focus), and is in a state where
- // it does not want to give up focus. (Eg. a text field with an invalid phone number).
+ /**
+ * The focusable component is currently active (has focus), and is in a state where
+ * it does not want to give up focus. (Eg. a text field with an invalid phone number).
+ */
Captured,
- // The focusable component is not currently focusable. (eg. A disabled button).
+ /** The focusable component is not currently focusable. (eg. A disabled button). */
Deactivated,
- // One of the descendants of this deactivated component is Active.
+ /** One of the descendants of this deactivated component is Active. */
DeactivatedParent,
- // The focusable component does not receive any key events. (ie it is not active, nor are any
- // of its descendants active).
+ /**
+ * The focusable component does not receive any key events. (ie it is not active, nor are any
+ * of its descendants active).
+ */
Inactive;
override val isFocused: Boolean
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index 424c30b..4891047 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -226,6 +226,13 @@
}
/**
+ * Whether this node should be considered when searching for the next item during a traversal.
+ */
+internal val FocusModifier.isEligibleForFocusSearch: Boolean
+ get() = layoutNodeWrapper?.layoutNode?.isPlaced == true &&
+ layoutNodeWrapper?.layoutNode?.isAttached == true
+
+/**
* Returns [one] if it comes after [two] in the modifier chain or [two] if it comes after [one].
*/
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
index 781df18..cd15209 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
@@ -98,10 +98,10 @@
when (direction) {
Next -> children.forEachItemAfter(focusedItem) { child ->
- if (child.forwardFocusSearch(onFound)) return true
+ if (child.isEligibleForFocusSearch && child.forwardFocusSearch(onFound)) return true
}
Previous -> children.forEachItemBefore(focusedItem) { child ->
- if (child.backwardFocusSearch(onFound)) return true
+ if (child.isEligibleForFocusSearch && child.backwardFocusSearch(onFound)) return true
}
else -> error(InvalidFocusDirection)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index e614b45..9d53cf7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -134,10 +134,12 @@
var searchResult: FocusModifier? = null
forEach { candidateNode ->
- val candidateRect = candidateNode.focusRect()
- if (isBetterCandidate(candidateRect, bestCandidate, focusRect, direction)) {
- bestCandidate = candidateRect
- searchResult = candidateNode
+ if (candidateNode.isEligibleForFocusSearch) {
+ val candidateRect = candidateNode.focusRect()
+ if (isBetterCandidate(candidateRect, bestCandidate, focusRect, direction)) {
+ bestCandidate = candidateRect
+ searchResult = candidateNode
+ }
}
}
return searchResult
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 0e0aeb2..d2d9da5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -556,12 +556,12 @@
}
/**
- * Make a deep copy of the [PointerInputChange]
+ * Make a shallow copy of the [PointerInputChange]
*
* **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
- * performs deep copy of the [PointerInputChange]. Any [consume] call between any of the copies
- * will consume any other copy automatically. Therefore, copy with the new [isConsumed] is
- * not possible. Consider creating a new [PointerInputChange]
+ * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
+ * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
+ * is not possible. Consider creating a new [PointerInputChange]
*/
@Suppress("DEPRECATION")
fun copy(
@@ -593,7 +593,8 @@
@Suppress("DEPRECATION")
@Deprecated(
"Partial consumption has been deprecated. Use copy() instead without `consumed` " +
- "parameter to create a deep copy or a constructor to create a new PointerInputChange",
+ "parameter to create a shallow copy or a constructor to create a new " +
+ "PointerInputChange",
replaceWith = ReplaceWith(
"copy(id, currentTime, currentPosition, currentPressed, previousTime, " +
"previousPosition, previousPressed, type, scrollDelta)"
@@ -627,12 +628,12 @@
}
/**
- * Make a deep copy of the [PointerInputChange]
+ * Make a shallow copy of the [PointerInputChange]
*
* **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
- * performs deep copy of the [PointerInputChange]. Any [consume] call between any of the copies
- * will consume any other copy automatically. Therefore, copy with the new [isConsumed] is
- * not possible. Consider creating a new [PointerInputChange].
+ * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
+ * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
+ * is not possible. Consider creating a new [PointerInputChange].
*/
@ExperimentalComposeUiApi
@Suppress("DEPRECATION")
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/BeyondBoundsLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/BeyondBoundsLayout.kt
index 7eb9ad8..ce50b33 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/BeyondBoundsLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/BeyondBoundsLayout.kt
@@ -33,14 +33,14 @@
* A [BeyondBoundsLayout] instance can be obtained by consuming the
* [BeyondBoundsLayout modifier local][ModifierLocalBeyondBoundsLayout].
* It can be used to send a request to layout more items in a particular
- * [direction][BeyondBoundsLayoutDirection]. This can be useful when composition or layout
- * is determined lazily, as with a LazyColumn. The request is received by any parent up
- * the hierarchy that provides this modifier local.
+ * [direction][LayoutDirection]. This can be useful when composition or layout is determined lazily,
+ * as with a LazyColumn. The request is received by any parent up the hierarchy that provides this
+ * modifier local.
*/
interface BeyondBoundsLayout {
/**
* Send a request to layout more items in the specified
- * [direction][BeyondBoundsLayoutDirection]. The request is received by a parent up the
+ * [direction][LayoutDirection]. The request is received by a parent up the
* hierarchy. The parent adds one item at a time and calls [block] after each item is added.
* The parent continues adding new items as long as [block] returns null. Once you have all
* the items you need, you can perform some operation and return a non-null value. Returning
@@ -54,70 +54,70 @@
* may be disposed. Therefore you have to perform any custom logic within the [block] and return
* the value you need.
*/
- fun <T> searchBeyondBounds(
- direction: BeyondBoundsLayoutDirection,
- block: BeyondBoundsLayoutScope.() -> T?
+ fun <T> layout(
+ direction: LayoutDirection,
+ block: BeyondBoundsScope.() -> T?
): T?
-}
-/**
- * The scope used in [BeyondBoundsLayout.searchBeyondBounds].
- */
-interface BeyondBoundsLayoutScope {
/**
- * Whether we have more content to lay out in the specified direction.
+ * The scope used in [BeyondBoundsLayout.layout].
*/
- val hasMoreContent: Boolean
-}
-
-/**
- * The direction (from the visible bounds) that a [BeyondBoundsLayout] is requesting more
- * items to be laid.
- */
-@JvmInline
-value class BeyondBoundsLayoutDirection internal constructor(
- @Suppress("unused") private val value: Int
-) {
- companion object {
+ interface BeyondBoundsScope {
/**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items before the current bounds.
+ * Whether we have more content to lay out in the specified direction.
*/
- val Before = BeyondBoundsLayoutDirection(1)
- /**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items after the current bounds.
- */
- val After = BeyondBoundsLayoutDirection(2)
- /**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items to the left of the current bounds.
- */
- val Left = BeyondBoundsLayoutDirection(3)
- /**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items to the right of the current bounds.
- */
- val Right = BeyondBoundsLayoutDirection(4)
- /**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items above the current bounds.
- */
- val Above = BeyondBoundsLayoutDirection(5)
- /**
- * Direction used in [BeyondBoundsLayout.searchBeyondBounds] to request the layout of
- * extra items below the current bounds.
- */
- val Below = BeyondBoundsLayoutDirection(6)
+ val hasMoreContent: Boolean
}
- override fun toString(): String = when (this) {
- Before -> "Before"
- After -> "After"
- Left -> "Left"
- Right -> "Right"
- Above -> "Above"
- Below -> "Below"
- else -> "invalid BeyondBoundsLayoutDirection"
+ /**
+ * The direction (from the visible bounds) that a [BeyondBoundsLayout] is requesting more items
+ * to be laid.
+ */
+ @JvmInline
+ value class LayoutDirection internal constructor(
+ @Suppress("unused") private val value: Int
+ ) {
+ companion object {
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * before the current bounds.
+ */
+ val Before = LayoutDirection(1)
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * after the current bounds.
+ */
+ val After = LayoutDirection(2)
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * to the left of the current bounds.
+ */
+ val Left = LayoutDirection(3)
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * to the right of the current bounds.
+ */
+ val Right = LayoutDirection(4)
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * above the current bounds.
+ */
+ val Above = LayoutDirection(5)
+ /**
+ * Direction used in [BeyondBoundsLayout.layout] to request the layout of extra items
+ * below the current bounds.
+ */
+ val Below = LayoutDirection(6)
+ }
+
+ override fun toString(): String = when (this) {
+ Before -> "Before"
+ After -> "After"
+ Left -> "Left"
+ Right -> "Right"
+ Above -> "Above"
+ Below -> "Below"
+ else -> "invalid LayoutDirection"
+ }
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index e99e754..d9d8be5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -199,6 +199,7 @@
subcompositionsState ?: LayoutNodeSubcompositionsState(this, slotReusePolicy).also {
subcompositionsState = it
}
+ state.makeSureStateIsConsistent()
state.slotReusePolicy = slotReusePolicy
}
internal val setCompositionContext:
@@ -510,12 +511,20 @@
makeSureStateIsConsistent()
}
- private fun makeSureStateIsConsistent() {
+ fun makeSureStateIsConsistent() {
require(nodeToNodeState.size == root.foldedChildren.size) {
"Inconsistency between the count of nodes tracked by the state (${nodeToNodeState
.size}) and the children count on the SubcomposeLayout (${root.foldedChildren
.size}). Are you trying to use the state of the disposed SubcomposeLayout?"
}
+ require(root.foldedChildren.size - reusableCount - precomposedCount >= 0) {
+ "Incorrect state. Total children ${root.foldedChildren.size}. Reusable children " +
+ "$reusableCount. Precomposed children $precomposedCount"
+ }
+ require(precomposeMap.size == precomposedCount) {
+ "Incorrect state. Precomposed children $precomposedCount. Map size " +
+ "${precomposeMap.size}"
+ }
}
private fun takeNodeFromReusables(slotId: Any?): LayoutNode? {
@@ -628,6 +637,7 @@
}
return object : PrecomposedSlotHandle {
override fun dispose() {
+ makeSureStateIsConsistent()
val node = precomposeMap.remove(slotId)
if (node != null) {
check(precomposedCount > 0)
@@ -689,11 +699,18 @@
root.ignoreRemeasureRequests(block)
fun disposeCurrentNodes() {
- nodeToNodeState.values.forEach {
- it.composition?.dispose()
+ root.ignoreRemeasureRequests {
+ nodeToNodeState.values.forEach {
+ it.composition?.dispose()
+ }
+ root.removeAll()
}
nodeToNodeState.clear()
slotIdToNode.clear()
+ precomposedCount = 0
+ reusableCount = 0
+ precomposeMap.clear()
+ makeSureStateIsConsistent()
}
private class NodeState(
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index 3d00f81..ba48c48 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -22,7 +22,6 @@
import android.view.View
import android.view.ViewTreeObserver
import androidx.core.splashscreen.SplashScreenViewProvider
-import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -36,6 +35,7 @@
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -168,7 +168,7 @@
* before and after the removal of the SplashScreenView.
*/
@Test
- @FlakyTest(bugId = 213634077)
+ @Ignore // b/213634077
fun endStateStableWithAndWithoutListener() {
// Take a screenshot of the activity when no OnExitAnimationListener is set.
// This is our reference.
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index e07b981..efb810a 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -4492,7 +4492,14 @@
/**
* Structure to encapsulate a named action that can be shown as part of this notification.
* It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is
- * selected by the user. Action buttons won't appear on platforms prior to Android 4.1.
+ * selected by the user. Action buttons won't appear on platforms prior to Android
+ * {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
+ * <p>
+ * As of Android {@link android.os.Build.VERSION_CODES#N},
+ * action button icons will not be displayed on action buttons, but are still required and
+ * are available to
+ * {@link android.service.notification.NotificationListenerService notification listeners},
+ * which may display them in other contexts, for example on a wearable device.
* <p>
* Apps should use {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)}
* or {@link NotificationCompat.Builder#addAction(NotificationCompat.Action)}
diff --git a/core/settings.gradle b/core/settings.gradle
index 01b0750..fe37a85 100644
--- a/core/settings.gradle
+++ b/core/settings.gradle
@@ -11,6 +11,7 @@
playground {
setupPlayground("..")
selectProjectsFromAndroidX({ name ->
+ if (name == ":core:uwb:uwb") return false
if (name.startsWith(":core")) return true
if (name == ":internal-testutils-mockito") return true
if (name == ":internal-testutils-runtime") return true
diff --git a/core/uwb/uwb/api/current.txt b/core/uwb/uwb/api/current.txt
index e6f50d0..0b2add9 100644
--- a/core/uwb/uwb/api/current.txt
+++ b/core/uwb/uwb/api/current.txt
@@ -1 +1,165 @@
// Signature format: 4.0
+package androidx.core.uwb {
+
+ public final class RangingCapabilities {
+ ctor public RangingCapabilities(boolean supportsDistance, boolean supportsAzimuthalAngle, boolean supportsElevationAngle);
+ method public boolean getSupportsAzimuthalAngle();
+ method public boolean getSupportsDistance();
+ method public boolean getSupportsElevationAngle();
+ property public final boolean supportsAzimuthalAngle;
+ property public final boolean supportsDistance;
+ property public final boolean supportsElevationAngle;
+ }
+
+ public final class RangingMeasurement {
+ ctor public RangingMeasurement(float value);
+ method public float getValue();
+ property public final float value;
+ }
+
+ public final class RangingParameters {
+ ctor public RangingParameters(int uwbConfigId, int sessionId, byte[]? sessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List<androidx.core.uwb.UwbDevice> peerDevices, int updateRate);
+ method public androidx.core.uwb.UwbComplexChannel? getComplexChannel();
+ method public java.util.List<androidx.core.uwb.UwbDevice> getPeerDevices();
+ method public int getSessionId();
+ method public byte[]? getSessionKeyInfo();
+ method public int getUpdateRate();
+ method public int getUwbConfigId();
+ property public final androidx.core.uwb.UwbComplexChannel? complexChannel;
+ property public final java.util.List<androidx.core.uwb.UwbDevice> peerDevices;
+ property public final int sessionId;
+ property public final byte[]? sessionKeyInfo;
+ property public final int updateRate;
+ property public final int uwbConfigId;
+ field public static final androidx.core.uwb.RangingParameters.Companion Companion;
+ field public static final int RANGING_UPDATE_RATE_AUTOMATIC;
+ field public static final int RANGING_UPDATE_RATE_FREQUENT;
+ field public static final int RANGING_UPDATE_RATE_INFREQUENT;
+ }
+
+ public static final class RangingParameters.Companion {
+ }
+
+ public final class RangingPosition {
+ ctor public RangingPosition(androidx.core.uwb.RangingMeasurement distance, androidx.core.uwb.RangingMeasurement? azimuth, androidx.core.uwb.RangingMeasurement? elevation, long elapsedRealtimeNanos);
+ method public androidx.core.uwb.RangingMeasurement? getAzimuth();
+ method public androidx.core.uwb.RangingMeasurement getDistance();
+ method public long getElapsedRealtimeNanos();
+ method public androidx.core.uwb.RangingMeasurement? getElevation();
+ property public final androidx.core.uwb.RangingMeasurement? azimuth;
+ property public final androidx.core.uwb.RangingMeasurement distance;
+ property public final long elapsedRealtimeNanos;
+ property public final androidx.core.uwb.RangingMeasurement? elevation;
+ }
+
+ public abstract class RangingResult {
+ ctor public RangingResult();
+ method public abstract androidx.core.uwb.UwbDevice getDevice();
+ property public abstract androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPeerDisconnected extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPeerDisconnected(androidx.core.uwb.UwbDevice device);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ property public androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPosition extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPosition(androidx.core.uwb.UwbDevice device, androidx.core.uwb.RangingPosition position);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ method public androidx.core.uwb.RangingPosition getPosition();
+ property public androidx.core.uwb.UwbDevice device;
+ property public final androidx.core.uwb.RangingPosition position;
+ }
+
+ public final class UwbAddress {
+ ctor public UwbAddress(byte[] address);
+ ctor public UwbAddress(String address);
+ method public byte[] getAddress();
+ property public final byte[] address;
+ field public static final androidx.core.uwb.UwbAddress.Companion Companion;
+ }
+
+ public static final class UwbAddress.Companion {
+ }
+
+ public interface UwbClientSessionScope {
+ method public androidx.core.uwb.UwbAddress getLocalAddress();
+ method public androidx.core.uwb.RangingCapabilities getRangingCapabilities();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.uwb.RangingResult> initSession(androidx.core.uwb.RangingParameters parameters);
+ property public abstract androidx.core.uwb.UwbAddress localAddress;
+ property public abstract androidx.core.uwb.RangingCapabilities rangingCapabilities;
+ }
+
+ public final class UwbComplexChannel {
+ ctor public UwbComplexChannel(int channel, int preambleIndex);
+ method public int getChannel();
+ method public int getPreambleIndex();
+ property public final int channel;
+ property public final int preambleIndex;
+ }
+
+ public interface UwbControleeSessionScope extends androidx.core.uwb.UwbClientSessionScope {
+ }
+
+ public final class UwbDevice {
+ ctor public UwbDevice(androidx.core.uwb.UwbAddress address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ method public androidx.core.uwb.UwbAddress getAddress();
+ property public final androidx.core.uwb.UwbAddress address;
+ field public static final androidx.core.uwb.UwbDevice.Companion Companion;
+ }
+
+ public static final class UwbDevice.Companion {
+ method public androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ }
+
+ public interface UwbManager {
+ method public suspend <R> Object? clientSessionScope(kotlin.jvm.functions.Function2<? super androidx.core.uwb.UwbClientSessionScope,? super kotlin.coroutines.Continuation<? super R>,?> sessionHandler, kotlin.coroutines.Continuation<? super R>);
+ method public default static androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ field public static final androidx.core.uwb.UwbManager.Companion Companion;
+ }
+
+ public static final class UwbManager.Companion {
+ method public androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ }
+
+}
+
+package androidx.core.uwb.exceptions {
+
+ public class UwbApiException extends java.lang.Exception {
+ ctor public UwbApiException(String message);
+ }
+
+ public final class UwbBackgroundPolicyException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbBackgroundPolicyException(String message);
+ }
+
+ public final class UwbHardwareNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbHardwareNotAvailableException(String message);
+ }
+
+ public final class UwbRangingAlreadyStartedException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbRangingAlreadyStartedException(String message);
+ }
+
+ public final class UwbServiceNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbServiceNotAvailableException(String message);
+ }
+
+ public final class UwbSystemCallbackException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbSystemCallbackException(String message);
+ }
+
+}
+
+package androidx.core.uwb.helper {
+
+ public final class UwbHelperKt {
+ }
+
+}
+
diff --git a/core/uwb/uwb/api/public_plus_experimental_current.txt b/core/uwb/uwb/api/public_plus_experimental_current.txt
index e6f50d0..0b2add9 100644
--- a/core/uwb/uwb/api/public_plus_experimental_current.txt
+++ b/core/uwb/uwb/api/public_plus_experimental_current.txt
@@ -1 +1,165 @@
// Signature format: 4.0
+package androidx.core.uwb {
+
+ public final class RangingCapabilities {
+ ctor public RangingCapabilities(boolean supportsDistance, boolean supportsAzimuthalAngle, boolean supportsElevationAngle);
+ method public boolean getSupportsAzimuthalAngle();
+ method public boolean getSupportsDistance();
+ method public boolean getSupportsElevationAngle();
+ property public final boolean supportsAzimuthalAngle;
+ property public final boolean supportsDistance;
+ property public final boolean supportsElevationAngle;
+ }
+
+ public final class RangingMeasurement {
+ ctor public RangingMeasurement(float value);
+ method public float getValue();
+ property public final float value;
+ }
+
+ public final class RangingParameters {
+ ctor public RangingParameters(int uwbConfigId, int sessionId, byte[]? sessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List<androidx.core.uwb.UwbDevice> peerDevices, int updateRate);
+ method public androidx.core.uwb.UwbComplexChannel? getComplexChannel();
+ method public java.util.List<androidx.core.uwb.UwbDevice> getPeerDevices();
+ method public int getSessionId();
+ method public byte[]? getSessionKeyInfo();
+ method public int getUpdateRate();
+ method public int getUwbConfigId();
+ property public final androidx.core.uwb.UwbComplexChannel? complexChannel;
+ property public final java.util.List<androidx.core.uwb.UwbDevice> peerDevices;
+ property public final int sessionId;
+ property public final byte[]? sessionKeyInfo;
+ property public final int updateRate;
+ property public final int uwbConfigId;
+ field public static final androidx.core.uwb.RangingParameters.Companion Companion;
+ field public static final int RANGING_UPDATE_RATE_AUTOMATIC;
+ field public static final int RANGING_UPDATE_RATE_FREQUENT;
+ field public static final int RANGING_UPDATE_RATE_INFREQUENT;
+ }
+
+ public static final class RangingParameters.Companion {
+ }
+
+ public final class RangingPosition {
+ ctor public RangingPosition(androidx.core.uwb.RangingMeasurement distance, androidx.core.uwb.RangingMeasurement? azimuth, androidx.core.uwb.RangingMeasurement? elevation, long elapsedRealtimeNanos);
+ method public androidx.core.uwb.RangingMeasurement? getAzimuth();
+ method public androidx.core.uwb.RangingMeasurement getDistance();
+ method public long getElapsedRealtimeNanos();
+ method public androidx.core.uwb.RangingMeasurement? getElevation();
+ property public final androidx.core.uwb.RangingMeasurement? azimuth;
+ property public final androidx.core.uwb.RangingMeasurement distance;
+ property public final long elapsedRealtimeNanos;
+ property public final androidx.core.uwb.RangingMeasurement? elevation;
+ }
+
+ public abstract class RangingResult {
+ ctor public RangingResult();
+ method public abstract androidx.core.uwb.UwbDevice getDevice();
+ property public abstract androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPeerDisconnected extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPeerDisconnected(androidx.core.uwb.UwbDevice device);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ property public androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPosition extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPosition(androidx.core.uwb.UwbDevice device, androidx.core.uwb.RangingPosition position);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ method public androidx.core.uwb.RangingPosition getPosition();
+ property public androidx.core.uwb.UwbDevice device;
+ property public final androidx.core.uwb.RangingPosition position;
+ }
+
+ public final class UwbAddress {
+ ctor public UwbAddress(byte[] address);
+ ctor public UwbAddress(String address);
+ method public byte[] getAddress();
+ property public final byte[] address;
+ field public static final androidx.core.uwb.UwbAddress.Companion Companion;
+ }
+
+ public static final class UwbAddress.Companion {
+ }
+
+ public interface UwbClientSessionScope {
+ method public androidx.core.uwb.UwbAddress getLocalAddress();
+ method public androidx.core.uwb.RangingCapabilities getRangingCapabilities();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.uwb.RangingResult> initSession(androidx.core.uwb.RangingParameters parameters);
+ property public abstract androidx.core.uwb.UwbAddress localAddress;
+ property public abstract androidx.core.uwb.RangingCapabilities rangingCapabilities;
+ }
+
+ public final class UwbComplexChannel {
+ ctor public UwbComplexChannel(int channel, int preambleIndex);
+ method public int getChannel();
+ method public int getPreambleIndex();
+ property public final int channel;
+ property public final int preambleIndex;
+ }
+
+ public interface UwbControleeSessionScope extends androidx.core.uwb.UwbClientSessionScope {
+ }
+
+ public final class UwbDevice {
+ ctor public UwbDevice(androidx.core.uwb.UwbAddress address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ method public androidx.core.uwb.UwbAddress getAddress();
+ property public final androidx.core.uwb.UwbAddress address;
+ field public static final androidx.core.uwb.UwbDevice.Companion Companion;
+ }
+
+ public static final class UwbDevice.Companion {
+ method public androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ }
+
+ public interface UwbManager {
+ method public suspend <R> Object? clientSessionScope(kotlin.jvm.functions.Function2<? super androidx.core.uwb.UwbClientSessionScope,? super kotlin.coroutines.Continuation<? super R>,?> sessionHandler, kotlin.coroutines.Continuation<? super R>);
+ method public default static androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ field public static final androidx.core.uwb.UwbManager.Companion Companion;
+ }
+
+ public static final class UwbManager.Companion {
+ method public androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ }
+
+}
+
+package androidx.core.uwb.exceptions {
+
+ public class UwbApiException extends java.lang.Exception {
+ ctor public UwbApiException(String message);
+ }
+
+ public final class UwbBackgroundPolicyException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbBackgroundPolicyException(String message);
+ }
+
+ public final class UwbHardwareNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbHardwareNotAvailableException(String message);
+ }
+
+ public final class UwbRangingAlreadyStartedException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbRangingAlreadyStartedException(String message);
+ }
+
+ public final class UwbServiceNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbServiceNotAvailableException(String message);
+ }
+
+ public final class UwbSystemCallbackException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbSystemCallbackException(String message);
+ }
+
+}
+
+package androidx.core.uwb.helper {
+
+ public final class UwbHelperKt {
+ }
+
+}
+
diff --git a/core/uwb/uwb/api/restricted_current.txt b/core/uwb/uwb/api/restricted_current.txt
index e6f50d0..0b2add9 100644
--- a/core/uwb/uwb/api/restricted_current.txt
+++ b/core/uwb/uwb/api/restricted_current.txt
@@ -1 +1,165 @@
// Signature format: 4.0
+package androidx.core.uwb {
+
+ public final class RangingCapabilities {
+ ctor public RangingCapabilities(boolean supportsDistance, boolean supportsAzimuthalAngle, boolean supportsElevationAngle);
+ method public boolean getSupportsAzimuthalAngle();
+ method public boolean getSupportsDistance();
+ method public boolean getSupportsElevationAngle();
+ property public final boolean supportsAzimuthalAngle;
+ property public final boolean supportsDistance;
+ property public final boolean supportsElevationAngle;
+ }
+
+ public final class RangingMeasurement {
+ ctor public RangingMeasurement(float value);
+ method public float getValue();
+ property public final float value;
+ }
+
+ public final class RangingParameters {
+ ctor public RangingParameters(int uwbConfigId, int sessionId, byte[]? sessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List<androidx.core.uwb.UwbDevice> peerDevices, int updateRate);
+ method public androidx.core.uwb.UwbComplexChannel? getComplexChannel();
+ method public java.util.List<androidx.core.uwb.UwbDevice> getPeerDevices();
+ method public int getSessionId();
+ method public byte[]? getSessionKeyInfo();
+ method public int getUpdateRate();
+ method public int getUwbConfigId();
+ property public final androidx.core.uwb.UwbComplexChannel? complexChannel;
+ property public final java.util.List<androidx.core.uwb.UwbDevice> peerDevices;
+ property public final int sessionId;
+ property public final byte[]? sessionKeyInfo;
+ property public final int updateRate;
+ property public final int uwbConfigId;
+ field public static final androidx.core.uwb.RangingParameters.Companion Companion;
+ field public static final int RANGING_UPDATE_RATE_AUTOMATIC;
+ field public static final int RANGING_UPDATE_RATE_FREQUENT;
+ field public static final int RANGING_UPDATE_RATE_INFREQUENT;
+ }
+
+ public static final class RangingParameters.Companion {
+ }
+
+ public final class RangingPosition {
+ ctor public RangingPosition(androidx.core.uwb.RangingMeasurement distance, androidx.core.uwb.RangingMeasurement? azimuth, androidx.core.uwb.RangingMeasurement? elevation, long elapsedRealtimeNanos);
+ method public androidx.core.uwb.RangingMeasurement? getAzimuth();
+ method public androidx.core.uwb.RangingMeasurement getDistance();
+ method public long getElapsedRealtimeNanos();
+ method public androidx.core.uwb.RangingMeasurement? getElevation();
+ property public final androidx.core.uwb.RangingMeasurement? azimuth;
+ property public final androidx.core.uwb.RangingMeasurement distance;
+ property public final long elapsedRealtimeNanos;
+ property public final androidx.core.uwb.RangingMeasurement? elevation;
+ }
+
+ public abstract class RangingResult {
+ ctor public RangingResult();
+ method public abstract androidx.core.uwb.UwbDevice getDevice();
+ property public abstract androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPeerDisconnected extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPeerDisconnected(androidx.core.uwb.UwbDevice device);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ property public androidx.core.uwb.UwbDevice device;
+ }
+
+ public final class RangingResultPosition extends androidx.core.uwb.RangingResult {
+ ctor public RangingResultPosition(androidx.core.uwb.UwbDevice device, androidx.core.uwb.RangingPosition position);
+ method public androidx.core.uwb.UwbDevice getDevice();
+ method public androidx.core.uwb.RangingPosition getPosition();
+ property public androidx.core.uwb.UwbDevice device;
+ property public final androidx.core.uwb.RangingPosition position;
+ }
+
+ public final class UwbAddress {
+ ctor public UwbAddress(byte[] address);
+ ctor public UwbAddress(String address);
+ method public byte[] getAddress();
+ property public final byte[] address;
+ field public static final androidx.core.uwb.UwbAddress.Companion Companion;
+ }
+
+ public static final class UwbAddress.Companion {
+ }
+
+ public interface UwbClientSessionScope {
+ method public androidx.core.uwb.UwbAddress getLocalAddress();
+ method public androidx.core.uwb.RangingCapabilities getRangingCapabilities();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.uwb.RangingResult> initSession(androidx.core.uwb.RangingParameters parameters);
+ property public abstract androidx.core.uwb.UwbAddress localAddress;
+ property public abstract androidx.core.uwb.RangingCapabilities rangingCapabilities;
+ }
+
+ public final class UwbComplexChannel {
+ ctor public UwbComplexChannel(int channel, int preambleIndex);
+ method public int getChannel();
+ method public int getPreambleIndex();
+ property public final int channel;
+ property public final int preambleIndex;
+ }
+
+ public interface UwbControleeSessionScope extends androidx.core.uwb.UwbClientSessionScope {
+ }
+
+ public final class UwbDevice {
+ ctor public UwbDevice(androidx.core.uwb.UwbAddress address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public static androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ method public androidx.core.uwb.UwbAddress getAddress();
+ property public final androidx.core.uwb.UwbAddress address;
+ field public static final androidx.core.uwb.UwbDevice.Companion Companion;
+ }
+
+ public static final class UwbDevice.Companion {
+ method public androidx.core.uwb.UwbDevice createForAddress(String address);
+ method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
+ }
+
+ public interface UwbManager {
+ method public suspend <R> Object? clientSessionScope(kotlin.jvm.functions.Function2<? super androidx.core.uwb.UwbClientSessionScope,? super kotlin.coroutines.Continuation<? super R>,?> sessionHandler, kotlin.coroutines.Continuation<? super R>);
+ method public default static androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ field public static final androidx.core.uwb.UwbManager.Companion Companion;
+ }
+
+ public static final class UwbManager.Companion {
+ method public androidx.core.uwb.UwbManager getInstance(android.content.Context context);
+ }
+
+}
+
+package androidx.core.uwb.exceptions {
+
+ public class UwbApiException extends java.lang.Exception {
+ ctor public UwbApiException(String message);
+ }
+
+ public final class UwbBackgroundPolicyException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbBackgroundPolicyException(String message);
+ }
+
+ public final class UwbHardwareNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbHardwareNotAvailableException(String message);
+ }
+
+ public final class UwbRangingAlreadyStartedException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbRangingAlreadyStartedException(String message);
+ }
+
+ public final class UwbServiceNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbServiceNotAvailableException(String message);
+ }
+
+ public final class UwbSystemCallbackException extends androidx.core.uwb.exceptions.UwbApiException {
+ ctor public UwbSystemCallbackException(String message);
+ }
+
+}
+
+package androidx.core.uwb.helper {
+
+ public final class UwbHelperKt {
+ }
+
+}
+
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 3793f28..508a3e5 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -24,7 +24,23 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ api("androidx.annotation:annotation:1.1.0")
+ api("androidx.core:core-ktx:1.2.0")
+ api(libs.kotlinCoroutinesAndroid)
+ implementation(libs.guavaAndroid)
+ implementation('com.google.android.gms:play-services-base:18.0.1')
+ implementation(libs.kotlinCoroutinesPlayServices)
+ implementation('com.google.android.gms:play-services-nearby:18.3.0', {
+ exclude group: "androidx.core"
+ })
+
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.espressoCore)
}
androidx {
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/impl/UwbClientSessionScopeImplTest.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/impl/UwbClientSessionScopeImplTest.kt
new file mode 100644
index 0000000..c4fad37
--- /dev/null
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/impl/UwbClientSessionScopeImplTest.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb.impl
+
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.RangingResultPeerDisconnected
+import androidx.core.uwb.RangingResultPosition
+import androidx.core.uwb.UwbDevice
+import androidx.core.uwb.exceptions.UwbRangingAlreadyStartedException
+import androidx.core.uwb.mock.TestUwbClient
+import com.google.android.gms.nearby.uwb.RangingCapabilities
+import com.google.android.gms.nearby.uwb.UwbAddress
+import com.google.android.gms.nearby.uwb.UwbComplexChannel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Test
+
+class UwbClientSessionScopeImplTest {
+ private val complexChannel = UwbComplexChannel.Builder()
+ .setPreambleIndex(0)
+ .setChannel(0)
+ .build()
+ private val localAddress = UwbAddress(ByteArray(0))
+ private val rangingCapabilities = RangingCapabilities(true, false, false)
+ private val uwbClient = TestUwbClient(complexChannel, localAddress, rangingCapabilities, true)
+ private val uwbClientSessionScopeImpl = UwbClientSessionScopeImpl(
+ uwbClient,
+ androidx.core.uwb.RangingCapabilities(
+ rangingCapabilities.supportsDistance(),
+ rangingCapabilities.supportsAzimuthalAngle(),
+ rangingCapabilities.supportsElevationAngle()),
+ androidx.core.uwb.UwbAddress(localAddress.address))
+ private val uwbDevice = UwbDevice.createForAddress(ByteArray(0))
+ private val rangingParameters = RangingParameters(
+ RangingParameters.UWB_CONFIG_ID_1,
+ 0,
+ null,
+ null,
+ listOf(uwbDevice),
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+ )
+
+ @Test
+ public fun testInitSession_singleConsumer() {
+ val sessionFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+ var rangingResult: RangingResult? = null
+ val job = sessionFlow
+ .cancellable()
+ .onEach { rangingResult = it }
+ .launchIn(CoroutineScope(Dispatchers.Main.immediate))
+
+ runBlocking {
+ // wait for the coroutines for UWB to get launched.
+ delay(500)
+ }
+ // a non-null RangingResult should return from the TestUwbClient.
+ if (rangingResult != null) {
+ assertThat(rangingResult is RangingResultPosition).isTrue()
+ } else {
+ job.cancel()
+ Assert.fail()
+ }
+
+ // cancel and wait for the job to terminate.
+ job.cancel()
+ runBlocking {
+ job.join()
+ }
+ // StopRanging should have been called after the coroutine scope completed.
+ assertThat(uwbClient.stopRangingCalled).isTrue()
+ }
+
+ @Test
+ public fun testInitSession_multipleSharedConsumers() {
+ var passed1 = false
+ var passed2 = false
+ val sharedFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+ .shareIn(CoroutineScope(Dispatchers.Main.immediate), SharingStarted.WhileSubscribed(),
+ replay = 1)
+ val job = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sharedFlow
+ .onEach {
+ if (it is RangingResultPosition) {
+ passed1 = true
+ }
+ }
+ .collect()
+ }
+ val job2 = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sharedFlow
+ .onEach {
+ if (it is RangingResultPosition) {
+ passed2 = true
+ }
+ }
+ .collect()
+ }
+
+ runBlocking {
+ // wait for coroutines for flow to start.
+ delay(500)
+ }
+
+ // a non-null RangingResult should return from the TestUwbClient.
+ assertThat(passed1).isTrue()
+ assertThat(passed2).isTrue()
+
+ // cancel and wait for the first job to terminate.
+ job.cancel()
+ runBlocking {
+ job.join()
+ }
+ // StopRanging should not have been called because not all consumers have finished.
+ assertThat(uwbClient.stopRangingCalled).isFalse()
+
+ // cancel and wait for the second job to terminate.
+ job2.cancel()
+ runBlocking {
+ job2.join()
+ }
+ // StopRanging should have been called because all consumers have finished.
+ assertThat(uwbClient.stopRangingCalled).isTrue()
+ }
+
+ @Test
+ public fun testInitSession_singleConsumer_disconnectPeerDevice() {
+ val sessionFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+ var peerDisconnected = false
+ val job = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sessionFlow
+ .cancellable()
+ .onEach {
+ if (it is RangingResultPeerDisconnected) {
+ peerDisconnected = true
+ }
+ }
+ .collect()
+ }
+
+ runBlocking {
+ // wait for coroutines for flow to start.
+ delay(500)
+ uwbClient.disconnectPeer(com.google.android.gms.nearby.uwb.UwbDevice.createForAddress(
+ uwbDevice.address.address))
+
+ // wait for rangingResults to get filled.
+ delay(500)
+ }
+
+ // a peer disconnected event should have occurred.
+ assertThat(peerDisconnected).isTrue()
+
+ // cancel and wait for the job to terminate.
+ job.cancel()
+ runBlocking {
+ job.join()
+ }
+ // StopRanging should have been called after the coroutine scope completed.
+ assertThat(uwbClient.stopRangingCalled).isTrue()
+ }
+
+ @Test
+ public fun testInitSession_multipleSharedConsumers_disconnectPeerDevice() {
+ val sharedFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+ .shareIn(CoroutineScope(Dispatchers.Main.immediate), SharingStarted.WhileSubscribed())
+
+ var peerDisconnected = false
+ var peerDisconnected2 = false
+ val job = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sharedFlow
+ .onEach {
+ if (it is RangingResultPeerDisconnected) {
+ peerDisconnected = true
+ }
+ }
+ .collect()
+ }
+ val job2 = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sharedFlow
+ .onEach {
+ if (it is RangingResultPeerDisconnected) {
+ peerDisconnected2 = true
+ }
+ }
+ .collect()
+ }
+
+ runBlocking {
+ // wait for coroutines for flow to start.
+ delay(500)
+ uwbClient.disconnectPeer(com.google.android.gms.nearby.uwb.UwbDevice.createForAddress(
+ uwbDevice.address.address))
+
+ // wait for rangingResults to get filled.
+ delay(500)
+ }
+
+ // a peer disconnected event should have occurred.
+ assertThat(peerDisconnected).isTrue()
+ assertThat(peerDisconnected2).isTrue()
+
+ // cancel and wait for the job to terminate.
+ job.cancel()
+ runBlocking {
+ job.join()
+ }
+ // StopRanging should not have been called because not all consumers have finished.
+ assertThat(uwbClient.stopRangingCalled).isFalse()
+
+ // cancel and wait for the job to terminate.
+ job2.cancel()
+ runBlocking {
+ job2.join()
+ }
+ // StopRanging should have been called because all consumers have finished.
+ assertThat(uwbClient.stopRangingCalled).isTrue()
+ }
+
+ @Test
+ public fun testInitSession_multipleSessions_throwsUwbApiException() {
+ val sessionFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+ val sessionFlow2 = uwbClientSessionScopeImpl.initSession(rangingParameters)
+
+ val job = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sessionFlow.collect()
+ }
+ runBlocking {
+ // wait for coroutines for flow to start.
+ delay(500)
+ }
+ val job2 = CoroutineScope(Dispatchers.Main.immediate).launch {
+ try {
+ sessionFlow2.collect()
+ Assert.fail()
+ } catch (e: UwbRangingAlreadyStartedException) {
+ // verified the exception was thrown.
+ }
+ }
+ job.cancel()
+ job2.cancel()
+ }
+
+ @Test
+ public fun testInitSession_reusingSession_throwsUwbApiException() {
+ val sessionFlow = uwbClientSessionScopeImpl.initSession(rangingParameters)
+
+ val job = CoroutineScope(Dispatchers.Main.immediate).launch {
+ sessionFlow.collect()
+ }
+ runBlocking {
+ // wait for coroutines for flow to start.
+ delay(500)
+ }
+ // cancel and wait for the job to terminate.
+ job.cancel()
+ runBlocking {
+ job.join()
+ }
+ // StopRanging should not have been called because not all consumers have finished.
+ assertThat(uwbClient.stopRangingCalled).isTrue()
+ val job2 = CoroutineScope(Dispatchers.Main.immediate).launch {
+ try {
+ // Reuse the same session after it was closed.
+ sessionFlow.collect()
+ Assert.fail()
+ } catch (e: UwbRangingAlreadyStartedException) {
+ // verified the exception was thrown.
+ }
+ }
+ job2.cancel()
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
new file mode 100644
index 0000000..afe7418
--- /dev/null
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb.mock
+
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.Status
+import com.google.android.gms.common.api.internal.ApiKey
+import com.google.android.gms.nearby.uwb.RangingCapabilities
+import com.google.android.gms.nearby.uwb.RangingMeasurement
+import com.google.android.gms.nearby.uwb.RangingParameters
+import com.google.android.gms.nearby.uwb.RangingPosition
+import com.google.android.gms.nearby.uwb.RangingSessionCallback
+import com.google.android.gms.nearby.uwb.UwbAddress
+import com.google.android.gms.nearby.uwb.UwbClient
+import com.google.android.gms.nearby.uwb.UwbComplexChannel
+import com.google.android.gms.nearby.uwb.UwbDevice
+import com.google.android.gms.nearby.uwb.UwbStatusCodes
+import com.google.android.gms.nearby.uwb.zze
+import com.google.android.gms.tasks.Task
+import com.google.android.gms.tasks.Tasks
+
+/** A default implementation of [UwbClient] used in testing. */
+class TestUwbClient(
+ val complexChannel: UwbComplexChannel,
+ val localAddress: UwbAddress,
+ val rangingCapabilities: RangingCapabilities,
+ val isAvailable: Boolean
+) : UwbClient {
+ var stopRangingCalled = false
+ private set
+ private lateinit var callback: RangingSessionCallback
+ private var startedRanging = false
+ companion object {
+ val rangingPosition = RangingPosition(
+ RangingMeasurement(1, 1.0F), null, null, 20)
+ }
+ override fun getApiKey(): ApiKey<zze> {
+ TODO("Not yet implemented")
+ }
+
+ override fun getComplexChannel(): Task<UwbComplexChannel> {
+ return Tasks.forResult(complexChannel)
+ }
+
+ override fun getLocalAddress(): Task<UwbAddress> {
+ return Tasks.forResult(localAddress)
+ }
+
+ override fun getRangingCapabilities(): Task<RangingCapabilities> {
+ return Tasks.forResult(rangingCapabilities)
+ }
+
+ override fun isAvailable(): Task<Boolean> {
+ return Tasks.forResult(isAvailable)
+ }
+
+ override fun startRanging(
+ parameters: RangingParameters,
+ sessionCallback: RangingSessionCallback
+ ): Task<Void> {
+ if (startedRanging) {
+ throw ApiException(Status(UwbStatusCodes.RANGING_ALREADY_STARTED))
+ }
+ callback = sessionCallback
+ val peer = parameters.peerDevices.first()
+ callback.onRangingResult(peer, rangingPosition)
+ startedRanging = true
+ return Tasks.forResult(null)
+ }
+
+ override fun stopRanging(callback: RangingSessionCallback): Task<Void> {
+ if (stopRangingCalled) {
+ throw RuntimeException("Stop Ranging has already been called.")
+ }
+ stopRangingCalled = true
+ return Tasks.forResult(null)
+ }
+
+ fun disconnectPeer(device: UwbDevice) {
+ callback.onRangingSuspended(device, 0)
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingCapabilities.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingCapabilities.kt
new file mode 100644
index 0000000..1366a64
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingCapabilities.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/**
+ * Describes UWB ranging capabilities for the current device.
+ * @property supportsDistance - Whether distance ranging is supported
+ * @property supportsAzimuthalAngle - Whether azimuthal angle of arrival is supported
+ * @property supportsElevationAngle - Whether elevation angle of arrival is supported
+ **/
+class RangingCapabilities(
+ val supportsDistance: Boolean,
+ val supportsAzimuthalAngle: Boolean,
+ val supportsElevationAngle: Boolean
+)
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingMeasurement.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingMeasurement.kt
new file mode 100644
index 0000000..424669c
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingMeasurement.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/** Measurement providing the value and confidence of the ranging.
+ *
+ * @property value the value of this measurement.
+ */
+class RangingMeasurement(val value: Float)
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt
new file mode 100644
index 0000000..a6aa775
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingParameters.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/**
+ * Set of parameters which should be passed to the UWB chip to start ranging.
+ *
+ * @property uwbConfigId
+ * The UWB configuration ID. One ID specifies one fixed set of pre-defined parameters.
+ *
+ * @property sessionId
+ * The ID of the ranging session. If the value is SESSION_ID_UNSET (0), it will
+ * be created from the hash of controller address and complex channel values.
+ *
+ * The same session IDs should be used at both ends (Controller and controlee).
+ *
+ * @property sessionKeyInfo
+ * The session key info to use for the ranging.
+ * If the profile uses STATIC
+ * STS, this byte array is 8-byte long with first two bytes as
+ * Vendor_ID and next six bytes as STATIC_STS_IV.
+ *
+ * The same session keys should be used at both ends (Controller and controlee).
+ *
+ * @property complexChannel
+ * Optional. If device type is ROLE_CONTROLEE then complex channel should be set.
+ *
+ * @property peerDevices
+ * The peers to perform ranging with. If using unicast, length should be 1.
+ *
+ * @property updateRate
+ * The update rate of the ranging data
+ */
+class RangingParameters(
+ val uwbConfigId: Int,
+ val sessionId: Int,
+ val sessionKeyInfo: ByteArray?,
+ val complexChannel: UwbComplexChannel?,
+ val peerDevices: List<UwbDevice>,
+ val updateRate: Int
+) {
+
+ companion object {
+
+ /**
+ * Pre-defined unicast STATIC STS DS-TWR ranging, deferred mode, ranging
+ * interval 240 ms.
+ *
+ * <p> Typical use case: device tracking tags
+ */
+ @JvmField
+ internal val UWB_CONFIG_ID_1 = 1
+
+ /**
+ * Pre-defined one-to-many STATIC STS DS-TWR ranging, deferred mode, ranging
+ * interval 200 ms.
+ *
+ * <p> Typical use case: smart phone interacts with many smart devices
+ */
+ @JvmField
+ internal val UWB_CONFIG_ID_2 = 2
+
+ /** Same as CONFIG_ID_1, except AoA data is not reported. */
+ @JvmField
+ internal val UWB_CONFIG_ID_3 = 3
+
+ /**
+ * When the screen is on, the reporting interval is hundreds of milliseconds.
+ * When the screen is off, the reporting interval is a few seconds.
+ */
+ @JvmField
+ val RANGING_UPDATE_RATE_AUTOMATIC = 1
+
+ /**
+ * The reporting interval is the same as in the AUTOMATIC screen-off case. The
+ * The power consumption is optimized by turning off the radio between ranging
+ * reports. (The implementation is hardware and software dependent and it may
+ * change between different versions.)
+ */
+ @JvmField
+ val RANGING_UPDATE_RATE_INFREQUENT = 2
+
+ /**
+ * The reporting interval is the same as in the AUTOMATIC screen-on case.
+ *
+ * The actual reporting interval is UwbConfigId related. Different
+ * configuration may use different values. (The default reporting interval at INFREQUENT mode is 4 seconds)
+ */
+ @JvmField
+ val RANGING_UPDATE_RATE_FREQUENT = 3
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingPosition.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingPosition.kt
new file mode 100644
index 0000000..5ae737b
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingPosition.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/**
+ * Position of a device during ranging.
+ *
+ * @property distance
+ * The line-of-sight distance in meters of the ranging device, or null if not
+ * available.
+ *
+ * @property azimuth
+ * The azimuth angle in degrees of the ranging device, or null if not available.
+ * The range is [-90, 90].
+ *
+ * @property elevation
+ * The elevation angle in degrees of the ranging device, or null if not
+ * available. The range is [-90, 90].
+ *
+ * @property elapsedRealtimeNanos
+ * The elapsed realtime in nanos from when the system booted up to this position
+ * measurement.
+ */
+class RangingPosition(
+ val distance: RangingMeasurement,
+ val azimuth: RangingMeasurement?,
+ val elevation: RangingMeasurement?,
+ val elapsedRealtimeNanos: Long
+)
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingResult.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingResult.kt
new file mode 100644
index 0000000..e43c336
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/RangingResult.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/** A data class for ranging result update. */
+abstract class RangingResult {
+ abstract val device: UwbDevice
+}
+
+/** A ranging result with the device position update. */
+class RangingResultPosition(
+ override val device: UwbDevice,
+ val position: RangingPosition
+) : RangingResult()
+
+/** A ranging result with peer disconnected status update. */
+class RangingResultPeerDisconnected(override val device: UwbDevice) : RangingResult()
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbAddress.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbAddress.kt
new file mode 100644
index 0000000..f1db3ca
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbAddress.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+import com.google.common.io.BaseEncoding
+
+/**
+ * Represents a UWB address.
+ *
+ * @property address the device address (eg, MAC address).
+ */
+class UwbAddress(val address: ByteArray) {
+
+ /** @throws an [IllegalArgumentException] if address is invalid. */
+ constructor(address: String) : this(BASE_16_SEPARATOR.decode(address))
+
+ companion object {
+ private val BASE_16_SEPARATOR: BaseEncoding = BaseEncoding.base16().withSeparator(":", 2)
+ }
+
+ /** Checks that two UwbAddresses are equal. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as UwbAddress
+
+ if (!address.contentEquals(other.address)) return false
+
+ return true
+ }
+
+ /** Returns the hashcode. */
+ override fun hashCode(): Int {
+ return address.contentHashCode()
+ }
+
+ /** Returns the string format of [UwbAddress]. */
+ override fun toString(): String {
+ return BASE_16_SEPARATOR.encode(address)
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbClientSessionScope.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbClientSessionScope.kt
new file mode 100644
index 0000000..0151426
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbClientSessionScope.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+import kotlinx.coroutines.flow.Flow
+
+/** Interface for client session that is established between nearby UWB devices. */
+interface UwbClientSessionScope {
+ /**
+ * Returns a flow of [RangingResult]. Consuming the flow will initiate the UWB ranging and only
+ * one flow can be initiated. To consume the flow from multiple consumers,
+ * convert the flow to a SharedFlow.
+ *
+ * @throws [UwbRangingAlreadyStartedException] if a new flow was consumed again after the UWB
+ * ranging is already initiated.
+ *
+ * @throws [UwbSystemCallbackException] if the backend UWB system has resulted in an error.
+ *
+ * @throws [SecurityException] if ranging does not have the
+ * android.permission.UWB_RANGING permission. Apps must
+ * have requested and been granted this permission before calling this method.
+ *
+ * @throws [IllegalArgumentException] if the client starts a controlee session
+ * without setting complex channel and peer address.
+ */
+ fun initSession(parameters: RangingParameters): Flow<RangingResult>
+
+ /** Returns the [RangingCapabilities] which the device supports. */
+ val rangingCapabilities: RangingCapabilities
+
+ /**
+ * A local address can only be used for a single ranging session. After
+ * a ranging session is ended, a new address will be allocated.
+ *
+ * Ranging session duration may also be limited to prevent addresses
+ * from being used for too long. In this case, your ranging session would be
+ * suspended and clients would need to exchange the new address with their peer
+ * before starting again.
+ */
+ val localAddress: UwbAddress
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
new file mode 100644
index 0000000..644dd5b
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/**
+ * Represents the channel which a UWB device is currently active on.
+ *
+ * @property channel the current channel for the device.
+ * @property preambleIndex the current preamble index for the device.
+ */
+class UwbComplexChannel(val channel: Int, val preambleIndex: Int)
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbControleeSessionScope.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbControleeSessionScope.kt
new file mode 100644
index 0000000..5e02a8e
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbControleeSessionScope.kt
@@ -0,0 +1,20 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb
+
+/** Interface for controlee client session that is established between nearby UWB devices. */
+interface UwbControleeSessionScope : UwbClientSessionScope
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbDevice.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbDevice.kt
new file mode 100644
index 0000000..da8a982
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbDevice.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+/**
+ * Represents a UWB device.
+ *
+ * @property address the device address (e.g., MAC address).
+ */
+class UwbDevice(val address: UwbAddress) {
+
+ companion object {
+ /**
+ * Creates a new UwbDevice for a given address.
+ *
+ * @throws an [IllegalArgumentException] if address is invalid.
+ */
+ @JvmStatic
+ fun createForAddress(address: String): UwbDevice {
+ return UwbDevice(UwbAddress(address))
+ }
+
+ /** Creates a new UwbDevice for a given address. */
+ @JvmStatic
+ fun createForAddress(address: ByteArray): UwbDevice {
+ return UwbDevice(UwbAddress(address))
+ }
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
new file mode 100644
index 0000000..cf21af3
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb
+
+import android.content.Context
+import androidx.core.uwb.impl.UwbManagerImpl
+
+/**
+ * Interface for getting UWB capabilities and interacting with nearby UWB devices to perform
+ * ranging.
+ */
+interface UwbManager {
+ companion object {
+
+ /** Creates a new UwbManager that is used for creating controlee client sessions. */
+ @JvmStatic
+ fun getInstance(context: Context): UwbManager {
+ return UwbManagerImpl(context)
+ }
+ }
+
+ /**
+ * Establishes a new [UwbClientSessionScope] that tracks the lifecycle of a UWB connection.
+ *
+ * @throws [UwbServiceNotAvailableException] if the UWB is turned off.
+ * @throws [UwbHardwareNotAvailableException] if the hardware is not available on the device.
+ */
+ suspend fun <R> clientSessionScope(sessionHandler: suspend UwbClientSessionScope.() -> R): R
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbApiException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbApiException.kt
new file mode 100644
index 0000000..0dc12f7
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbApiException.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/** Exception class for Uwb service errors. */
+open class UwbApiException(message: String) : Exception(message)
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbBackgroundPolicyException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbBackgroundPolicyException.kt
new file mode 100644
index 0000000..1d8d7e2
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbBackgroundPolicyException.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/**
+ * Calls to the UWB service fail for background systems. Only foreground
+ * systems can call the UWB service.
+ */
+class UwbBackgroundPolicyException(message: String) : UwbApiException(message)
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbHardwareNotAvailableException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbHardwareNotAvailableException.kt
new file mode 100644
index 0000000..72ad22d
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbHardwareNotAvailableException.kt
@@ -0,0 +1,20 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/** The uwb hardware is not available on the device. */
+class UwbHardwareNotAvailableException(message: String) : UwbApiException(message)
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbRangingAlreadyStartedException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbRangingAlreadyStartedException.kt
new file mode 100644
index 0000000..8a300b0
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbRangingAlreadyStartedException.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/**
+ * The ranging has already started for the [UwbClientSessionScope], but still received another
+ * request to start ranging. You cannot reuse a [UwbClientSessionScope] for multiple ranging
+ * sessions. To have multiple consumers for a single ranging session, use a [SharedFlow].
+ */
+class UwbRangingAlreadyStartedException(message: String) : UwbApiException(message)
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbServiceNotAvailableException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbServiceNotAvailableException.kt
new file mode 100644
index 0000000..ad1db5b
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbServiceNotAvailableException.kt
@@ -0,0 +1,20 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/** The service was not available on the device. */
+class UwbServiceNotAvailableException(message: String) : UwbApiException(message)
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbSystemCallbackException.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbSystemCallbackException.kt
new file mode 100644
index 0000000..32546ff
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/exceptions/UwbSystemCallbackException.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.exceptions
+
+/**
+ * Unusual failures happened in UWB system callback, such as stopping
+ * ranging or removing a known controlee failed.
+ */
+class UwbSystemCallbackException(message: String) : UwbApiException(message)
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/helper/UwbHelper.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/helper/UwbHelper.kt
new file mode 100644
index 0000000..965c741
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/helper/UwbHelper.kt
@@ -0,0 +1,56 @@
+/*
+ * 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
+ *
+ * http://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 androidx.core.uwb.helper
+
+import android.content.Context
+import androidx.core.uwb.exceptions.UwbHardwareNotAvailableException
+import androidx.core.uwb.exceptions.UwbRangingAlreadyStartedException
+import androidx.core.uwb.exceptions.UwbServiceNotAvailableException
+import androidx.core.uwb.exceptions.UwbSystemCallbackException
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.nearby.uwb.UwbStatusCodes
+
+internal const val UWB_FEATURE = "android.hardware.uwb"
+
+/** Returns whether the Uwb System Feature is available on the device. */
+internal fun isSystemFeatureAvailable(context: Context): Boolean {
+ return context.packageManager.hasSystemFeature(UWB_FEATURE)
+}
+
+/** Checks if the uwb system feature is supported and throws an UwbApiException otherwise. */
+internal fun checkSystemFeature(context: Context) {
+ if (!isSystemFeatureAvailable(context)) {
+ throw UwbHardwareNotAvailableException("UWB Hardware is not available on this device.")
+ }
+}
+
+internal fun handleApiException(e: ApiException) {
+ when (e.statusCode) {
+ UwbStatusCodes.INVALID_API_CALL ->
+ throw IllegalArgumentException("Illegal api call was received.")
+ UwbStatusCodes.RANGING_ALREADY_STARTED ->
+ throw UwbRangingAlreadyStartedException("Ranging has already started for the" +
+ " clientSessionScope.")
+ UwbStatusCodes.SERVICE_NOT_AVAILABLE ->
+ throw UwbServiceNotAvailableException("UWB Service is not available.")
+ UwbStatusCodes.UWB_SYSTEM_CALLBACK_FAILURE ->
+ throw UwbSystemCallbackException("UWB system has failed to deliver ")
+ else ->
+ throw RuntimeException("Unexpected error. This indicates that the library is not " +
+ "up-to-date with the service backend.")
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
new file mode 100644
index 0000000..1dada71
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb.impl
+
+import android.util.Log
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResultPeerDisconnected
+import androidx.core.uwb.RangingResultPosition
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.exceptions.UwbRangingAlreadyStartedException
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.nearby.uwb.RangingPosition
+import com.google.android.gms.nearby.uwb.RangingSessionCallback
+import com.google.android.gms.nearby.uwb.UwbClient
+import com.google.android.gms.nearby.uwb.UwbComplexChannel
+import com.google.android.gms.nearby.uwb.UwbDevice
+import kotlinx.coroutines.flow.callbackFlow
+import androidx.core.uwb.helper.handleApiException
+import kotlinx.coroutines.channels.awaitClose
+
+internal class UwbClientSessionScopeImpl(
+ private val uwbClient: UwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress
+) : UwbControleeSessionScope {
+ companion object {
+ private const val TAG = "UwbClientSessionScope"
+ }
+ private var sessionStarted = false
+
+ override fun initSession(parameters: RangingParameters) = callbackFlow {
+ if (sessionStarted) {
+ throw UwbRangingAlreadyStartedException("Ranging has already started. To initiate " +
+ "a new ranging session, create a new client session scope.")
+ }
+
+ val configId = when (parameters.uwbConfigId) {
+ RangingParameters.UWB_CONFIG_ID_1 ->
+ com.google.android.gms.nearby.uwb.RangingParameters.UwbConfigId.CONFIG_ID_1
+ RangingParameters.UWB_CONFIG_ID_3 ->
+ com.google.android.gms.nearby.uwb.RangingParameters.UwbConfigId.CONFIG_ID_3
+ else ->
+ com.google.android.gms.nearby.uwb.RangingParameters.UwbConfigId.UNKNOWN
+ }
+ val updateRate = when (parameters.updateRate) {
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC ->
+ com.google.android.gms.nearby.uwb.RangingParameters.RangingUpdateRate.AUTOMATIC
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT ->
+ com.google.android.gms.nearby.uwb.RangingParameters.RangingUpdateRate.FREQUENT
+ RangingParameters.RANGING_UPDATE_RATE_INFREQUENT ->
+ com.google.android.gms.nearby.uwb.RangingParameters.RangingUpdateRate.INFREQUENT
+ else ->
+ com.google.android.gms.nearby.uwb.RangingParameters.RangingUpdateRate.UNKNOWN
+ }
+ val parametersBuilder = com.google.android.gms.nearby.uwb.RangingParameters.Builder()
+ .setSessionId(parameters.sessionId)
+ .setUwbConfigId(configId)
+ .setRangingUpdateRate(updateRate)
+ .setSessionKeyInfo(parameters.sessionKeyInfo)
+ .setUwbConfigId(parameters.uwbConfigId)
+ .setComplexChannel(
+ parameters.complexChannel?.let {
+ UwbComplexChannel.Builder()
+ .setChannel(it.channel)
+ .setPreambleIndex(it.preambleIndex)
+ .build()
+ })
+ for (peer in parameters.peerDevices) {
+ parametersBuilder.addPeerDevice(UwbDevice.createForAddress(peer.address.address))
+ }
+ val callback =
+ object : RangingSessionCallback {
+ override fun onRangingInitialized(device: UwbDevice) {
+ Log.i(TAG, "Started UWB ranging.")
+ }
+
+ override fun onRangingResult(device: UwbDevice, position: RangingPosition) {
+ trySend(
+ RangingResultPosition(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address.address)),
+ androidx.core.uwb.RangingPosition(
+ RangingMeasurement(position.distance.value),
+ position.azimuth?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elevation?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elapsedRealtimeNanos
+ )
+ )
+ )
+ }
+
+ override fun onRangingSuspended(device: UwbDevice, reason: Int) {
+ trySend(
+ RangingResultPeerDisconnected(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address.address))
+ )
+ )
+ }
+ }
+
+ try {
+ uwbClient.startRanging(parametersBuilder.build(), callback)
+ sessionStarted = true
+ } catch (e: ApiException) {
+ handleApiException(e)
+ }
+
+ awaitClose {
+ try {
+ uwbClient.stopRanging(callback)
+ } catch (e: ApiException) {
+ handleApiException(e)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
new file mode 100644
index 0000000..09de8dc
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 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
+ *
+ * http://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 androidx.core.uwb.impl
+
+import android.content.Context
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbManager
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.nearby.Nearby
+import kotlinx.coroutines.tasks.await
+import androidx.core.uwb.helper.checkSystemFeature
+import androidx.core.uwb.helper.handleApiException
+
+internal class UwbManagerImpl(val context: Context) : UwbManager {
+ override suspend fun <R> clientSessionScope(
+ sessionHandler: suspend UwbClientSessionScope.() -> R
+ ): R {
+ // Check whether UWB hardware is available on the device.
+ checkSystemFeature(context)
+ val uwbClient = Nearby.getUwbControleeClient(context)
+ val localAddress: com.google.android.gms.nearby.uwb.UwbAddress
+ val rangingCapabilities: com.google.android.gms.nearby.uwb.RangingCapabilities
+ try {
+ localAddress = uwbClient.localAddress.await()
+ rangingCapabilities = uwbClient.rangingCapabilities.await()
+ } catch (e: ApiException) {
+ handleApiException(e)
+ throw RuntimeException("Unexpected error. This indicates that the library is not " +
+ "up-to-date with the service backend.")
+ }
+ return UwbClientSessionScopeImpl(
+ uwbClient,
+ RangingCapabilities(
+ rangingCapabilities.supportsDistance(),
+ rangingCapabilities.supportsAzimuthalAngle(),
+ rangingCapabilities.supportsElevationAngle()),
+ UwbAddress(localAddress.address)
+ ).sessionHandler()
+ }
+}
\ No newline at end of file
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 2dcb1ac..4228fc7 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -127,12 +127,6 @@
GRADLE_USER_HOME=\$GRADLE_USER_HOME
Downloading file\:\$SUPPORT\/gradle\/wrapper\/.*
[.]+10%[.]+20%[.]+30%[.]+40%[.]+50%[.]+60%[.]+70%[.]+80%[.]+90%[.]+100%
-Welcome to Gradle .*
-Here are the highlights of this release:
-\- Aggregated test and JaCoCo reports
-\- Marking additional test source directories as tests in IntelliJ
-\- Support for Adoptium JDKs in Java toolchains
-For more details see .*
Daemon will be stopped at the end of the build
# > Configure project :appsearch:appsearch\-local\-backend
Configuration on demand is an incubating feature\.
@@ -290,6 +284,12 @@
w\: \$CHECKOUT\/prebuilts\/androidx\/external\/org\/jetbrains\/kotlin\/kotlin\-stdlib\-common\/[0-9]+\.[0-9]+\.[0-9]+\/kotlin\-stdlib\-common\-[0-9]+\.[0-9]+\.[0-9]+\.jar\: Runtime JAR file has version [0-9]+\.[0-9]+ which is older than required for API version [0-9]+\.[0-9]+
# > Task :compose:material:material:icons:generator:zipHtmlResultsOfTest
Html results of .* zipped into.*\.zip
+# https://github.com/gradle/common-custom-user-data-gradle-plugin/issues/44
+See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:external_processes
+\- Class `com\.gradle\.Utils`: external process started .*
+# b/230127926
+\- Plugin 'com\.android\.internal\.application': external process started .*
+\- Plugin 'com\.android\.internal\.library': external process started .*
[0-9]+ problems were found storing the configuration cache, [0-9]+ of which seem unique\.
\- Task `:[:A-Za-z0-9#\-]+` of type `org\.jetbrains\.kotlin\.gradle\.plugin\.mpp\.[A-Za-z0-9]+`: invocation of 'Task\.project' at execution time is unsupported\.
See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:disallowed_types
diff --git a/docs/api_guidelines.md b/docs/api_guidelines.md
index 257c7da..a5d1192 100644
--- a/docs/api_guidelines.md
+++ b/docs/api_guidelines.md
@@ -42,6 +42,11 @@
New modules in androidx can be created using the
[project creator script](#module-creator).
+NOTE Modules for OEM-implemented shared libraries (also known as extensions or
+sidecars) that ship on-device and are referenced via the `<uses-library>` tag
+should follow the naming convention `com.android.extensions.<feature-name>` to
+avoid placing `androidx`-packaged code in the platform's boot classpath.
+
#### Project directory structure {#module-structure}
Libraries developed in AndroidX follow a consistent project naming and directory
diff --git a/docs/onboarding.md b/docs/onboarding.md
index f922cd6..4449682 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -123,6 +123,12 @@
included version of pip. You can execute `Install Certificates.command` under
`/Applications/Python 3.6/` to do so.
+NOTE On MacOS, if you receive a Repo or GPG error like `repo: error: "gpg"
+failed with exit status -6` with cause `md_enable: algorithm 10 not available`
+you may need to install a build of `gpg` that supports SHA512, such as the
+latest version available from [Homebrew](https://brew.sh/) using `brew install
+gpg`.
+
### Increase Git rename limit {#source-config}
To ensure `git` can detect diffs and renames across significant changes (namely,
@@ -976,3 +982,13 @@
resources. It does not count library dependencies. It does not account for a
minification step (e.g. with R8), as that is dynamic, and done at app build time
(and depend on which entrypoints the app uses).
+
+### How do I add content to a library's Overview reference doc page?
+
+Put content in a markdown file that ends with `-documentation.md` in the
+directory that corresponds to the Overview page that you'd like to document.
+
+For example, the `androidx.compose.runtime`
+[Overview page](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary)
+includes content from
+[compose-runtime-documentation.md](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/compose-runtime-documentation.md).
diff --git a/gradle.properties b/gradle.properties
index be96a8f..22d731a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,6 +3,7 @@
org.gradle.configureondemand=true
org.gradle.parallel=true
org.gradle.caching=true
+org.gradle.welcome=never
# Disabled due to https://github.com/gradle/gradle/issues/18626
# org.gradle.vfs.watch=true
org.gradle.dependency.verification.console=verbose
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 39b93e1..b37c949 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -36,7 +36,7 @@
kotlinCoroutines = "1.6.1"
ksp = "1.6.21-1.0.5"
ktlint = "0.43.0"
-leakcanary = "2.7"
+leakcanary = "2.8.1"
metalava = "1.0.0-alpha06"
mockito = "2.25.0"
protobuf = "3.19.4"
@@ -67,6 +67,7 @@
apacheCommonsCodec = { module = "commons-codec:commons-codec", version = "1.15" }
assertj = { module = "org.assertj:assertj-core", version = "3.11.1" }
checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
+checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.2" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
dackka = { module = "com.google.devsite:dackka", version = "0.0.17" }
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
@@ -113,6 +114,7 @@
kotlinCoroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinCoroutines" }
kotlinCoroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" }
kotlinCoroutinesGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinCoroutines" }
+kotlinCoroutinesPlayServices = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinCoroutines" }
kotlinCoroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutines" }
kotlinCoroutinesRx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinCoroutines" }
kotlinCoroutinesRx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinCoroutines" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 6a4d022..fd5130b 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -10471,3 +10471,61 @@
Pt2uco8an9pO9/oqU6vlZUr38w==
=alQS
-----END PGP PUBLIC KEY BLOCK-----
+pub rsa4096 2022-04-25 [SC]
+ 1CB7A3DBC99B562D69BFDFEDAE7AF7AE095EB290
+uid [ unknown] David Saff <david@saff.net>
+sig 3 AE7AF7AE095EB290 2022-04-25 David Saff <david@saff.net>
+sub rsa4096 2022-04-25 [E]
+sig AE7AF7AE095EB290 2022-04-25 David Saff <david@saff.net>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBGJm9OEBEAClTz80QmRmi9bpX4m77aas5Q+x+gRtlEg6IWU6QfrGdazVO/3S
+brF3KmsEnxW8fjqv5drswed8FmUVdEsTcco31jxeD+fiBFCAU8BnrpL/+iIALMRY
+EXQDTkvYt+tAVDEcMOuR5HPhVtjVBl6Ez2d81B87AL4+iy0W3Qw8QMBaq+Cy52iU
+H/CknORLOT0i6a/u3aa7lvb1lcQ+NcINXJSr/NC4x8kvo4H/9XhSY8qxmp9B3/oN
+VwpkE7pi/Hxev4P+5B+Bls+F/x48+Vf9bF1XwtjFLe+hmQehFRqAy4H3fWBEVhQr
+MNlzseP7keyxAE70hr620u+TB8U9fi3z1rZFFlDuLIcGmCNgnyVWUmE0Pg0qnga9
+AmA8DLD9fBrYR5ZRCVor2BEkgKydgTKe6nrGi+AOw/QYbVYbX04X4IOPGYFf0Jm3
+vnEHxW3njTrUhHSejtA1sbwb5ISdL6JJhj+q8h199McgZwt7zzS9zU5bjQcZbZfU
+hPBrTZcrVd2y1A6Nw4g60em7SI8e/n7OAJCwZajQN6j7WGoFZ+JMeEcbXN7wNDVF
+pTKZAefTTxzuF5quovhOKq/lwiqaaCTcMQdlICytklFPydRZqnmon6U47Dce5ksH
+Kw8DE4vNA5RZd8z+7jcm1DI+EOOHY7Lcyy794onIxHedgdn5CxFTgXZUdQARAQAB
+tBtEYXZpZCBTYWZmIDxkYXZpZEBzYWZmLm5ldD6JAk4EEwEKADgWIQQct6PbyZtW
+LWm/3+2ueveuCV6ykAUCYmb04QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK
+CRCueveuCV6ykClgD/9VfINfRn5jd5+AoTIHIVcAnbnw9jLz2B645nhs8E8YVuyJ
+g+wmEvSqN1K/jj+PpdW69ymC/ezmOcYp967pRogMe5SlybZnVTDpXjhlCNEPAJES
+yCBTRsk5HaKEj4cq58sWm4nS/willNshyhIBVq0pPbLKU/faG8l+6yUMqAJLJ/aC
+9q4JjsBwTz60BiTpAJcIDTSfN0FeTsi8h28ty/p3flB1W7hZEGwtr5UB1W9lWbQd
+4oZRShMpuzlgk8E0rtoc8g0tmghBy4RuZpeNIfjiycLSAYvAXbxaQGy5IWTNyEf5
+q0GMgjzbXfwBSOGzOLnNVDA6ymwOZT9IH32VIefpJZxcrZXaIoAZmKjzw9B8nMvw
++BnbdpiyhgD8xTprNBPfzjv29/NWIrl94IYZ6FGvE+VxvBnQR/c74yzmsZZOKEW3
+4PQKVreUfsjuSq2ESIWjE1UNRgfXppAjT2ibfvjla5ebi68TNlBVU4wO/GuvXGr7
+gNu12b0SYQaPr7XXIOtEaW811WetTUiQp/t00F/bqYpdhwn7r/DGTmxTJOSr3gnx
+W7pr0wvCgIhKy7Yl4LkKVx3dQvwzpZAyQCvNs2OXbjBcNueW/Jg7PDtGr7SGzD0j
+gK5VBmcpMLZNCf8BmAzP2l/gVnhfShZb9/31aitqI4KvVFAvEhU6/ulh0pMzwLkC
+DQRiZvThARAA13JCyiwA0GNaqo2wb4uq5DArysO72Gtp/uOLpDyWTLIpGa6e/lAC
+6yOB0q9jYt9SDf5Zwp7DQxdiy9kcaPf2I1LXNfAdhb4QesIEeoGRdHGRh/1I3ZtY
+FXnp50Tk3vSEJEgqLM3OsnVtqRQRUUdOT313A1X2O1HKREDEMmRa/OWD2XiNMiZ0
+TLUvFHsEJKxzUk5PYE0RbpPoAC+zrGpC9EC6fruWpt/fOGDYDzYGY9rX01e/fIew
+FyDKQ+TwzCFNDOneMCK/MrtKo1f+q6HmlIH2+NpZ4+mVFOMKkutMqzhPub1pS31N
+vExnLhOcKYO13b/xl69AonnLavkP/eJUfGEPeZh9vWrZq5H9+K64rTYxCp15HXGH
+RcsKi9h1iQojXGpxcORDJVev9inF2WdM6dQbX/f1jRJUzpUiGB+tnPLV0tT6M3MY
+1qxtCBVJ2jjQ5141v8Lz4vF7Gs2jIkVZZObttiBC8JLxbdFFURVRurYTZYvWl1oC
+sNXtWrDi3q1jCX6zwIxzJIw8UjwL9jjpp39oB58PnlotAFtNJhwkoN/j2P5accbs
+tMSUGAn4tKOabcpvLTgZh1RZrKP3q9lvkCAfbWv0hlaKsGqHMv3kLpGeI4/MyICY
+apqqGHSLTVvPssoCa4cIY0+ybexc6R2tdNofCFeeKkx+bZ9jZpX35isAEQEAAYkC
+NgQYAQoAIBYhBBy3o9vJm1Ytab/f7a56964JXrKQBQJiZvThAhsMAAoJEK56964J
+XrKQOHgP/jo51A2C8qTZD3peRU8AkFs6jfHybw0t4Rk2X4+MzbmSX8UWFtRzcryJ
+2UgKGlS7FWeAjHzieWqkp/ZtfTjl9GsQzjALbmBzQudFClhDCYfzxuHYi2G3rr2G
+7TSPk5SQC9a19euDeNpKxCPIpgEZ+wr95T3XZVIK44pWhZYYyKAKFu4gYwCNUGSt
+EDp/pILl5c0OZ2L2QdEjvtSV9hNL+5FuS/FGamEpQbqFjMcta2e26giS1CA9LdbY
+gThn2QCE5XQpMFH+RHwnAzJ0EbwSMO8476OfbHdUy+GfTM1BKwr5oSOAoLGAdcYT
+PyUCubfH+OezMBb8JCMjs+V5atX/9tMKPEcm2E5aC/U/2sr8Mf77v2JIwB5T7vkL
+diuk7Bti1RBYVQ+07wb30REzam7OrbBd/nv7xK9pisp1oxY2qs80ozRJcivcKj6q
+pUqsbO4+yjio/SNvDUehio26SOnGk+JQriYxRjSVA4p3F6mHDrq4rQZvvWwyAu8M
+/ZKiRSj4XTqS+j0q1DYfO3XZltHYnl41XLOzOS5YIcRi8be9fGr4SBFBn13ctVix
+kBL0hpa5s1yKwSHSXp0DWEI07LsT5OxZdp/yXCTiM75zSOQ5Ed1UZixj28JOxR7C
+C3w5t4+mmawdzZQGaBZyeHL6bx4uYnzzpaeuEKtwJWSOkOEApPQe
+=Exzw
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 7d7b1a1..9020a9f 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -60,6 +60,7 @@
<trusted-key id="19beab2d799c020f17c69126b16698a4adf4d638" group="org.checkerframework"/>
<trusted-key id="1b2718089ce964b8" group="com.thoughtworks.qdox"/>
<trusted-key id="1bc86444bbd2a24c3a40904a438e9634a2319637" group="co.nstant.in" name="cbor"/>
+ <trusted-key id="1cb7a3dbc99b562d69bfdfedae7af7ae095eb290" group="net.saff.checkmark" name="checkmark"/>
<trusted-key id="1d0a8b5e77c678a7c724445abf984b4145ea13f7" group="com.squareup"/>
<trusted-key id="1d9aa7f9e1e2824728b8cd1794b291aef984a085">
<trusting group="io.reactivex"/>
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ab0bf2e..ec71961 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-7.4-bin.zip
+distributionUrl=../../../../tools/external/gradle/gradle-7.5-20220421031748+0000-bin.zip
diff --git a/gradlew b/gradlew
index c982c93..68ab9d3 100755
--- a/gradlew
+++ b/gradlew
@@ -266,8 +266,10 @@
-Pandroidx.validateNoUnrecognizedMessages\
-Pandroidx.verifyUpToDate\
--no-watch-fs\
- --no-daemon\
- --offline"
+ --no-daemon"
+ if [ "$USE_ANDROIDX_REMOTE_BUILD_CACHE" == "" ]; then
+ expanded="$expanded --offline"
+ fi
fi
# if compact is something else then we parsed the argument above but
# still have to remove it (expanded == "") to avoid confusing Gradle
diff --git a/health/health-services-client/build.gradle b/health/health-services-client/build.gradle
index 467e4aa..b135b8b 100644
--- a/health/health-services-client/build.gradle
+++ b/health/health-services-client/build.gradle
@@ -29,7 +29,7 @@
api("androidx.annotation:annotation:1.1.0")
implementation(libs.guavaListenableFuture)
implementation(libs.guavaAndroid)
- implementation("androidx.core:core-ktx:1.5.0-alpha04")
+ implementation("androidx.core:core-ktx:1.7.0")
implementation(libs.protobufLite)
}
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
index 366b2a1..34039e9 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -26,22 +26,21 @@
import androidx.paging.PageEvent.StaticList
import androidx.paging.PagePresenter.ProcessPageEventCallback
import androidx.paging.internal.BUGANIZER_URL
-import kotlinx.coroutines.CoroutineDispatcher
+import java.util.concurrent.CopyOnWriteArrayList
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
-import java.util.concurrent.CopyOnWriteArrayList
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class PagingDataDiffer<T : Any>(
private val differCallback: DifferCallback,
- private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
+ private val mainContext: CoroutineContext = Dispatchers.Main
) {
private var presenter: PagePresenter<T> = PagePresenter.initial()
private var receiver: UiReceiver? = null
@@ -143,7 +142,7 @@
receiver = pagingData.receiver
pagingData.flow.collect { event ->
- withContext(mainDispatcher) {
+ withContext(mainContext) {
if (event is Insert && event.loadType == REFRESH) {
presentNewList(
pages = event.pages,
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
index 1cd04d9..cca39c2 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
@@ -95,7 +95,7 @@
private val pagingDataDiffer = object : PagingDataDiffer<T>(
differCallback = differCallback,
- mainDispatcher = mainDispatcher
+ mainContext = mainDispatcher
) {
override suspend fun presentNewList(
previousList: NullPaddedList<T>,
diff --git a/paging/paging-runtime/api/current.ignore b/paging/paging-runtime/api/current.ignore
index 41c88d3..2bd7f73 100644
--- a/paging/paging-runtime/api/current.ignore
+++ b/paging/paging-runtime/api/current.ignore
@@ -5,3 +5,13 @@
Attempted to remove parameter name from parameter arg1 in androidx.paging.LoadStateAdapter.setLoadState
ParameterNameChange: androidx.paging.PagingDataAdapter#submitData(androidx.paging.PagingData<T>, kotlin.coroutines.Continuation<? super kotlin.Unit>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.PagingDataAdapter.submitData
+
+
+RemovedMethod: androidx.paging.AsyncPagingDataDiffer#AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, androidx.recyclerview.widget.ListUpdateCallback, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,androidx.recyclerview.widget.ListUpdateCallback,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.AsyncPagingDataDiffer#AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, androidx.recyclerview.widget.ListUpdateCallback, kotlinx.coroutines.CoroutineDispatcher, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,androidx.recyclerview.widget.ListUpdateCallback,kotlinx.coroutines.CoroutineDispatcher,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.PagingDataAdapter#PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.PagingDataAdapter#PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, kotlinx.coroutines.CoroutineDispatcher, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,kotlinx.coroutines.CoroutineDispatcher,kotlinx.coroutines.CoroutineDispatcher)
diff --git a/paging/paging-runtime/api/current.txt b/paging/paging-runtime/api/current.txt
index 9d1b058..c6e5705 100644
--- a/paging/paging-runtime/api/current.txt
+++ b/paging/paging-runtime/api/current.txt
@@ -24,9 +24,11 @@
}
public final class AsyncPagingDataDiffer<T> {
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method public T? getItem(@IntRange(from=0L) int index);
@@ -102,9 +104,11 @@
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public final void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method protected final T? getItem(@IntRange(from=0L) int position);
diff --git a/paging/paging-runtime/api/public_plus_experimental_current.txt b/paging/paging-runtime/api/public_plus_experimental_current.txt
index 9d1b058..c6e5705 100644
--- a/paging/paging-runtime/api/public_plus_experimental_current.txt
+++ b/paging/paging-runtime/api/public_plus_experimental_current.txt
@@ -24,9 +24,11 @@
}
public final class AsyncPagingDataDiffer<T> {
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method public T? getItem(@IntRange(from=0L) int index);
@@ -102,9 +104,11 @@
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public final void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method protected final T? getItem(@IntRange(from=0L) int position);
diff --git a/paging/paging-runtime/api/restricted_current.ignore b/paging/paging-runtime/api/restricted_current.ignore
index 41c88d3..2bd7f73 100644
--- a/paging/paging-runtime/api/restricted_current.ignore
+++ b/paging/paging-runtime/api/restricted_current.ignore
@@ -5,3 +5,13 @@
Attempted to remove parameter name from parameter arg1 in androidx.paging.LoadStateAdapter.setLoadState
ParameterNameChange: androidx.paging.PagingDataAdapter#submitData(androidx.paging.PagingData<T>, kotlin.coroutines.Continuation<? super kotlin.Unit>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.PagingDataAdapter.submitData
+
+
+RemovedMethod: androidx.paging.AsyncPagingDataDiffer#AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, androidx.recyclerview.widget.ListUpdateCallback, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,androidx.recyclerview.widget.ListUpdateCallback,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.AsyncPagingDataDiffer#AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, androidx.recyclerview.widget.ListUpdateCallback, kotlinx.coroutines.CoroutineDispatcher, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,androidx.recyclerview.widget.ListUpdateCallback,kotlinx.coroutines.CoroutineDispatcher,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.PagingDataAdapter#PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,kotlinx.coroutines.CoroutineDispatcher)
+RemovedMethod: androidx.paging.PagingDataAdapter#PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>, kotlinx.coroutines.CoroutineDispatcher, kotlinx.coroutines.CoroutineDispatcher):
+ Removed constructor androidx.paging.PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>,kotlinx.coroutines.CoroutineDispatcher,kotlinx.coroutines.CoroutineDispatcher)
diff --git a/paging/paging-runtime/api/restricted_current.txt b/paging/paging-runtime/api/restricted_current.txt
index 9d1b058..c6e5705 100644
--- a/paging/paging-runtime/api/restricted_current.txt
+++ b/paging/paging-runtime/api/restricted_current.txt
@@ -24,9 +24,11 @@
}
public final class AsyncPagingDataDiffer<T> {
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.AsyncPagingDataDiffer<T>! AsyncPagingDataDiffer(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, androidx.recyclerview.widget.ListUpdateCallback updateCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method public T? getItem(@IntRange(from=0L) int index);
@@ -102,9 +104,11 @@
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
- ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher, optional kotlin.coroutines.CoroutineContext workerDispatcher);
+ ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlin.coroutines.CoroutineContext mainDispatcher);
ctor public PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher);
+ method @Deprecated public androidx.paging.PagingDataAdapter<T,VH>! PagingDataAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T> diffCallback, optional kotlinx.coroutines.CoroutineDispatcher mainDispatcher, optional kotlinx.coroutines.CoroutineDispatcher workerDispatcher);
method public final void addLoadStateListener(kotlin.jvm.functions.Function1<? super androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
method protected final T? getItem(@IntRange(from=0L) int position);
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/PagingDataAdapterTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/PagingDataAdapterTest.kt
index c352faf..9c2c2ee 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/PagingDataAdapterTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/PagingDataAdapterTest.kt
@@ -17,15 +17,24 @@
package androidx.paging
import android.view.ViewGroup
+import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlin.test.assertContentEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlin.test.fail
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertFailsWith
-import kotlin.test.fail
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -34,7 +43,7 @@
@Test
fun hasStableIds() {
- val pagingDataAdapter = object : PagingDataAdapter<Int, RecyclerView.ViewHolder>(
+ val pagingDataAdapter = object : PagingDataAdapter<Int, ViewHolder>(
diffCallback = object : DiffUtil.ItemCallback<Int>() {
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
@@ -48,15 +57,61 @@
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
- ): RecyclerView.ViewHolder {
+ ): ViewHolder {
fail("Should never get here")
}
- override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
fail("Should never get here")
}
}
assertFailsWith<UnsupportedOperationException> { pagingDataAdapter.setHasStableIds(true) }
}
-}
\ No newline at end of file
+
+ @Test
+ fun workerContext() = runTest {
+ val workerExecutor = TestExecutor()
+ val workerContext: CoroutineContext = workerExecutor.asCoroutineDispatcher()
+ val adapter = object : PagingDataAdapter<Int, ViewHolder>(
+ object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ coroutineContext,
+ workerContext,
+ ) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ViewHolder {
+ return object : ViewHolder(TextView(parent.context)) {}
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+ }
+
+ val job = launch {
+ adapter.submitData(PagingData.from(listOf(1)))
+ adapter.submitData(PagingData.from(listOf(2)))
+ }
+
+ // Fast-forward to diff gets scheduled on workerExecutor
+ advanceUntilIdle()
+
+ // Check that some work was scheduled on workerExecutor and let everything else run to
+ // completion after.
+ workerExecutor.autoRun = true
+ assertTrue { workerExecutor.executeAll() }
+ advanceUntilIdle()
+
+ // Make sure we actually did submit some data and fully present it.
+ job.join()
+ assertContentEquals(listOf(2), adapter.snapshot().items)
+ }
+}
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index 4a56dbc..2217df4 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -22,12 +22,13 @@
import androidx.paging.LoadType.REFRESH
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.util.concurrent.atomic.AtomicInteger
/**
* Helper class for mapping a [PagingData] into a
@@ -37,15 +38,98 @@
* [AsyncPagingDataDiffer] is exposed for complex cases, and where overriding [PagingDataAdapter] to
* support paging isn't convenient.
*/
-class AsyncPagingDataDiffer<T : Any> @JvmOverloads constructor(
+class AsyncPagingDataDiffer<T : Any>
+/**
+ * Construct an [AsyncPagingDataDiffer].
+ *
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster
+ * path for generating the UI events required to display the new list.
+ * @param updateCallback [ListUpdateCallback] which receives UI events dispatched by this
+ * [AsyncPagingDataDiffer] as items are loaded.
+ * @param mainDispatcher [CoroutineContext] where UI events are dispatched. Typically, this should
+ * be [Dispatchers.Main].
+ * @param workerDispatcher [CoroutineContext] where the work to generate UI events is dispatched,
+ * for example when diffing lists on [REFRESH]. Typically, this should dispatch on a background
+ * thread; [Dispatchers.Default] by default.
+ */
+@JvmOverloads
+constructor(
private val diffCallback: DiffUtil.ItemCallback<T>,
- @Suppress("ListenerLast") // have to suppress for each, due to defaults / JvmOverloads
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
private val updateCallback: ListUpdateCallback,
- @Suppress("ListenerLast") // have to suppress for each, due to defaults / JvmOverloads
- private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
- @Suppress("ListenerLast") // have to suppress for each, due to defaults / JvmOverloads
- private val workerDispatcher: CoroutineDispatcher = Dispatchers.Default
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ private val mainDispatcher: CoroutineContext = Dispatchers.Main,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ private val workerDispatcher: CoroutineContext = Dispatchers.Default,
) {
+ /**
+ * Construct an [AsyncPagingDataDiffer].
+ *
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster
+ * path for generating the UI events required to display the new list.
+ * @param updateCallback [ListUpdateCallback] which receives UI events dispatched by this
+ * [AsyncPagingDataDiffer] as items are loaded.
+ * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically,
+ * this should be [Dispatchers.Main].
+ */
+ @Deprecated(
+ message = "Superseded by constructors which accept CoroutineContext",
+ level = DeprecationLevel.HIDDEN
+ )
+ // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would
+ // conflict with the primary constructor.
+ @Suppress("MissingJvmstatic")
+ constructor(
+ diffCallback: DiffUtil.ItemCallback<T>,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ updateCallback: ListUpdateCallback,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
+ ) : this(
+ diffCallback = diffCallback,
+ updateCallback = updateCallback,
+ mainDispatcher = mainDispatcher,
+ workerDispatcher = Dispatchers.Default
+ )
+
+ /**
+ * Construct an [AsyncPagingDataDiffer].
+ *
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster
+ * path for generating the UI events required to display the new list.
+ * @param updateCallback [ListUpdateCallback] which receives UI events dispatched by this
+ * [AsyncPagingDataDiffer] as items are loaded.
+ * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically,
+ * this should be [Dispatchers.Main].
+ * @param workerDispatcher [CoroutineDispatcher] where the work to generate UI events is
+ * dispatched, for example when diffing lists on [REFRESH]. Typically, this should dispatch on a
+ * background thread; [Dispatchers.Default] by default.
+ */
+ @Deprecated(
+ message = "Superseded by constructors which accept CoroutineContext",
+ level = DeprecationLevel.HIDDEN
+ )
+ // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would
+ // conflict with the primary constructor.
+ @Suppress("MissingJvmstatic")
+ constructor(
+ diffCallback: DiffUtil.ItemCallback<T>,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ updateCallback: ListUpdateCallback,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
+ @Suppress("ListenerLast") // have to suppress for each, due to optional args
+ workerDispatcher: CoroutineDispatcher = Dispatchers.Default,
+ ) : this(
+ diffCallback = diffCallback,
+ updateCallback = updateCallback,
+ mainDispatcher = mainDispatcher,
+ workerDispatcher = workerDispatcher
+ )
+
@Suppress("MemberVisibilityCanBePrivate") // synthetic access
internal val differCallback = object : DifferCallback {
override fun onInserted(position: Int, count: Int) {
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/PagingDataAdapter.kt b/paging/paging-runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
index 50e53e1..28b20bb 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
@@ -26,6 +26,7 @@
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW
import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -58,13 +59,81 @@
*
* @sample androidx.paging.samples.pagingDataAdapterSample
*/
-abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(
+abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder>
+/**
+ * Construct a [PagingDataAdapter].
+ *
+ * @param mainDispatcher [CoroutineContext] where UI events are dispatched. Typically, this should be
+ * [Dispatchers.Main].
+ * @param workerDispatcher [CoroutineContext] where the work to generate UI events is dispatched, for
+ * example when diffing lists on [REFRESH]. Typically, this should have a background
+ * [CoroutineDispatcher] set; [Dispatchers.Default] by default.
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster path
+ * for generating the UI events required to display the new list.
+ */
+@JvmOverloads
+constructor(
diffCallback: DiffUtil.ItemCallback<T>,
- mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
- workerDispatcher: CoroutineDispatcher = Dispatchers.Default
+ mainDispatcher: CoroutineContext = Dispatchers.Main,
+ workerDispatcher: CoroutineContext = Dispatchers.Default,
) : RecyclerView.Adapter<VH>() {
/**
+ * Construct a [PagingDataAdapter].
+ *
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster
+ * path for generating the UI events required to display the new list.
+ * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically,
+ * this should be [Dispatchers.Main].
+ */
+ @Deprecated(
+ message = "Superseded by constructors which accept CoroutineContext",
+ level = DeprecationLevel.HIDDEN
+ )
+ // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would
+ // conflict with the primary constructor.
+ @Suppress("MissingJvmstatic")
+ constructor(
+ diffCallback: DiffUtil.ItemCallback<T>,
+ mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
+ ) : this(
+ diffCallback = diffCallback,
+ mainDispatcher = mainDispatcher,
+ workerDispatcher = Dispatchers.Default,
+ )
+
+ /**
+ * Construct a [PagingDataAdapter].
+ *
+ * @param diffCallback Callback for calculating the diff between two non-disjoint lists on
+ * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster
+ * path for generating the UI events required to display the new list.
+ * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically,
+ * this should be [Dispatchers.Main].
+ * @param workerDispatcher [CoroutineDispatcher] where the work to generate UI events is
+ * dispatched, for example when diffing lists on [REFRESH]. Typically, this should dispatch on a
+ * background thread; [Dispatchers.Default] by default.
+ */
+ @Deprecated(
+ message = "Superseded by constructors which accept CoroutineContext",
+ level = DeprecationLevel.HIDDEN
+ )
+ // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would
+ // conflict with the primary constructor.
+ @Suppress("MissingJvmstatic")
+ constructor(
+ diffCallback: DiffUtil.ItemCallback<T>,
+ mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
+ workerDispatcher: CoroutineDispatcher = Dispatchers.Default,
+ ) : this(
+ diffCallback = diffCallback,
+ mainDispatcher = mainDispatcher,
+ workerDispatcher = workerDispatcher,
+ )
+
+ /**
* Track whether developer called [setStateRestorationPolicy] or not to decide whether the
* automated state restoration should apply or not.
*/
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index 41dfb87..626da35 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-7.5-20220421031748+0000-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
index ea0f050..b3fbeac 100644
--- a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
+++ b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
@@ -88,9 +88,15 @@
*/
fun setupPlayground(relativePathToRoot: String) {
// gradlePluginPortal has a variety of unsigned binaries that have proper signatures
- // in mavenCentral, so don't use gradlePluginPortal()
+ // in mavenCentral, so don't use gradlePluginPortal() if you can avoid it
settings.pluginManagement.repositories {
it.mavenCentral()
+ it.gradlePluginPortal().content {
+ it.includeModule(
+ "org.jetbrains.kotlin.plugin.serialization",
+ "org.jetbrains.kotlin.plugin.serialization.gradle.plugin"
+ )
+ }
}
val projectDir = settings.rootProject.projectDir
val supportRoot = File(projectDir, relativePathToRoot).canonicalFile
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index e37d11c..e8056f2 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=8485169
+androidx.playground.snapshotBuildId=8498920
androidx.playground.metalavaBuildId=8444773
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
index 13c9967..32add64 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
@@ -25,8 +25,8 @@
import androidx.room.processor.ProcessorErrors
import androidx.room.solver.QueryResultBinderProvider
import androidx.room.solver.TypeAdapterExtras
+import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
import androidx.room.solver.query.result.ListQueryResultAdapter
-import androidx.room.solver.query.result.PagingSourceQueryResultBinder
import androidx.room.solver.query.result.QueryResultBinder
import com.squareup.javapoet.TypeName
@@ -63,10 +63,10 @@
(listAdapter?.accessedTableNames() ?: emptyList()) +
query.tables.map { it.name }
).toSet()
-
- return PagingSourceQueryResultBinder(
+ return MultiTypedPagingSourceQueryResultBinder(
listAdapter = listAdapter,
tableNames = tableNames,
+ className = RoomPagingTypeNames.LIMIT_OFFSET_PAGING_SOURCE
)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PagingSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
similarity index 79%
rename from room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PagingSourceQueryResultBinder.kt
rename to room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
index 0220184..e88f895 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PagingSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 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.
@@ -20,8 +20,8 @@
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.L
import androidx.room.ext.N
-import androidx.room.ext.RoomPagingTypeNames
import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
@@ -31,17 +31,21 @@
import javax.lang.model.element.Modifier
/**
- * This Binder uses room/room-paging artifact and binds queries directly to native Paging3
- * PagingSource through `LimitOffsetPagingSource`. Used solely by Paging3.
+ * This Binder binds queries directly to native Paging3
+ * PagingSource (i.e. [LimitOffsetPagingSource]) or its subclasses such as
+ * [LimitOffsetListenableFuturePagingSource]. Used solely by Paging3.
*/
-class PagingSourceQueryResultBinder(
+class MultiTypedPagingSourceQueryResultBinder(
private val listAdapter: ListQueryResultAdapter?,
private val tableNames: Set<String>,
+ className: ClassName
) : QueryResultBinder(listAdapter) {
+
private val itemTypeName: TypeName =
listAdapter?.rowAdapters?.firstOrNull()?.out?.typeName ?: TypeName.OBJECT
- private val limitOffsetPagingSourceTypeNam: ParameterizedTypeName = ParameterizedTypeName.get(
- RoomPagingTypeNames.LIMIT_OFFSET_PAGING_SOURCE, itemTypeName
+
+ private val pagingSourceTypeName: ParameterizedTypeName = ParameterizedTypeName.get(
+ className, itemTypeName
)
override fun convertAndReturn(
@@ -53,16 +57,16 @@
) {
scope.builder().apply {
val tableNamesList = tableNames.joinToString(", ") { "\"$it\"" }
- val limitOffsetPagingSourceSpec = TypeSpec.anonymousClassBuilder(
+ val pagingSourceSpec = TypeSpec.anonymousClassBuilder(
"$L, $N, $L",
roomSQLiteQueryVar,
dbField,
tableNamesList
).apply {
- addSuperinterface(limitOffsetPagingSourceTypeNam)
+ addSuperinterface(pagingSourceTypeName)
addMethod(createConvertRowsMethod(scope))
}.build()
- addStatement("return $L", limitOffsetPagingSourceSpec)
+ addStatement("return $L", pagingSourceSpec)
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 5a958c7..3208dfa 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -21,6 +21,7 @@
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.compiler.processing.XProcessingEnv
+import androidx.room.compiler.processing.XRawType
import androidx.room.compiler.processing.isTypeElement
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
@@ -49,7 +50,7 @@
import androidx.room.solver.binderprovider.PagingSourceQueryResultBinderProvider
import androidx.room.solver.binderprovider.RxQueryResultBinderProvider
import androidx.room.solver.query.parameter.CollectionQueryParameterAdapter
-import androidx.room.solver.query.result.PagingSourceQueryResultBinder
+import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
@@ -861,7 +862,7 @@
}
@Test
- fun testNewPagingSourceBinder() {
+ fun testPagingSourceBinder() {
val inputSource =
Source.java(
qName = "foo.bar.MyDao",
@@ -897,7 +898,14 @@
val parsedDao = parser.process()
val binder = parsedDao.queryMethods.filterIsInstance<ReadQueryMethod>()
.first().queryResultBinder
- assertThat(binder is PagingSourceQueryResultBinder).isTrue()
+ assertThat(binder is MultiTypedPagingSourceQueryResultBinder).isTrue()
+
+ val pagingSourceXRawType: XRawType? = invocation.context.processingEnv
+ .findType(PagingTypeNames.PAGING_SOURCE)?.rawType
+ val returnedXRawType = parsedDao.queryMethods
+ .filterIsInstance<ReadQueryMethod>().first().returnType.rawType
+ // make sure returned type is the original PagingSource
+ assertThat(returnedXRawType).isEqualTo(pagingSourceXRawType)
}
}
diff --git a/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
index a9fd319..88e0bdc 100644
--- a/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
+++ b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
@@ -30,6 +30,7 @@
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
+import androidx.room.paging.util.ThreadSafeInvalidationObserver
import androidx.room.util.getColumnIndexOrThrow
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.test.core.app.ApplicationProvider
@@ -45,6 +46,7 @@
import java.util.concurrent.CancellationException
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@@ -67,8 +69,32 @@
val countingTaskExecutorRule = CountingTaskExecutorRule()
@Test
+ fun initialLoad_registersInvalidationObserver() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(
+ db = db,
+ registerObserver = true
+ )
+
+ val listenableFuture = pagingSource.refresh()
+ assertFalse(pagingSource.privateObserver().privateRegisteredState().get())
+
+ // observer registration is queued up on queryExecutor by refresh() call
+ queryExecutor.executeAll()
+
+ assertTrue(pagingSource.privateObserver().privateRegisteredState().get())
+ // note that listenableFuture is not done yet
+ // The future has been transformed into a ListenableFuture<LoadResult> whose result
+ // is still pending
+ assertFalse(listenableFuture.isDone)
+ }
+
+ @Test
fun initialEmptyLoad_futureIsDone() = setupAndRun { db ->
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(
+ db = db,
+ registerObserver = true
+ )
runTest {
val listenableFuture = pagingSource.refresh()
@@ -81,16 +107,19 @@
@Test
fun initialLoad_returnsFutureImmediately() =
- setupAndRunWithTestExecutor { db, _, transactionExecutor ->
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(
+ db = db,
+ registerObserver = true
+ )
val listenableFuture = pagingSource.refresh()
// ensure future is returned even as its result is still pending
assertFalse(listenableFuture.isDone)
assertThat(pagingSource.itemCount.get()).isEqualTo(-1)
- // now execute db queries
- transactionExecutor.executeAll() // initial transactional refresh load
+ queryExecutor.executeAll() // run loadFuture
+ transactionExecutor.executeAll() // start initialLoad callable + load data
val page = listenableFuture.await() as LoadResult.Page
assertThat(page.data).containsExactlyElementsIn(
@@ -103,14 +132,15 @@
fun append_returnsFutureImmediately() =
setupAndRunWithTestExecutor { db, queryExecutor, _ ->
val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
- pagingSource.itemCount.set(100) // bypass check for initial load
+
+ pagingSource.itemCount.set(100)
val listenableFuture = pagingSource.append(key = 20)
// ensure future is returned even as its result is still pending
assertFalse(listenableFuture.isDone)
- // load append
- queryExecutor.executeNext()
+ // run transformAsync and async function
+ queryExecutor.executeAll()
val page = listenableFuture.await() as LoadResult.Page
assertThat(page.data).containsExactlyElementsIn(
@@ -129,8 +159,8 @@
// ensure future is returned even as its result is still pending
assertFalse(listenableFuture.isDone)
- // load prepend
- queryExecutor.executeNext()
+ // run transformAsync and async function
+ queryExecutor.executeAll()
val page = listenableFuture.await() as LoadResult.Page
assertThat(page.data).containsExactlyElementsIn(
@@ -150,8 +180,7 @@
pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
assertTrue(pagingSource.invalid)
- // executing the load Callable
- queryExecutor.executeNext()
+ queryExecutor.executeAll() // run transformAsync and async function
val result = listenableFuture.await()
assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
@@ -169,8 +198,7 @@
pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
assertTrue(pagingSource.invalid)
- // executing the load Callable
- queryExecutor.executeNext()
+ queryExecutor.executeAll() // run transformAsync and async function
val result = listenableFuture.await()
assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
@@ -180,8 +208,8 @@
@Test
fun refresh_consecutively() = setupAndRun { db ->
db.dao.addAllItems(ITEMS_LIST)
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
- val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
+ val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db, true)
val listenableFuture1 = pagingSource.refresh(key = 10)
val listenableFuture2 = pagingSource2.refresh(key = 15)
@@ -210,22 +238,33 @@
val listenableFuture1 = pagingSource.append(key = 10)
val listenableFuture2 = pagingSource.append(key = 15)
- // both appends should be queued
+ // both load futures are queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+ queryExecutor.executeNext() // first transformAsync
+ queryExecutor.executeNext() // second transformAsync
+
+ // both async functions are queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+ queryExecutor.executeNext() // first async function
+ queryExecutor.executeNext() // second async function
+
+ // both nonInitial loads are queued
assertThat(queryExecutor.queuedSize()).isEqualTo(2)
- // run next append in queue and make sure it is the first append
- queryExecutor.executeNext()
+ queryExecutor.executeNext() // first db load
val page1 = listenableFuture1.await() as LoadResult.Page
assertThat(page1.data).containsExactlyElementsIn(
ITEMS_LIST.subList(10, 15)
)
- // now run the second append
- queryExecutor.executeNext()
+ queryExecutor.executeNext() // second db load
val page2 = listenableFuture2.await() as LoadResult.Page
assertThat(page2.data).containsExactlyElementsIn(
ITEMS_LIST.subList(15, 20)
)
+
+ assertTrue(listenableFuture1.isDone)
+ assertTrue(listenableFuture2.isDone)
}
@Test
@@ -236,31 +275,41 @@
assertThat(queryExecutor.queuedSize()).isEqualTo(0)
- val listenableFuture1 = pagingSource.prepend(key = 30)
- val listenableFuture2 = pagingSource.prepend(key = 25)
+ val listenableFuture1 = pagingSource.prepend(key = 25)
+ val listenableFuture2 = pagingSource.prepend(key = 20)
- // both prepends should be queued
+ // both load futures are queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+ queryExecutor.executeNext() // first transformAsync
+ queryExecutor.executeNext() // second transformAsync
+
+ // both async functions are queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+ queryExecutor.executeNext() // first async function
+ queryExecutor.executeNext() // second async function
+
+ // both nonInitial loads are queued
assertThat(queryExecutor.queuedSize()).isEqualTo(2)
- // run next prepend in queue and make sure it is the first prepend
- queryExecutor.executeNext()
+ queryExecutor.executeNext() // first db load
val page1 = listenableFuture1.await() as LoadResult.Page
assertThat(page1.data).containsExactlyElementsIn(
- ITEMS_LIST.subList(25, 30)
- )
-
- // now run the second prepend
- queryExecutor.executeNext()
- val page2 = listenableFuture2.await() as LoadResult.Page
- assertThat(page2.data).containsExactlyElementsIn(
ITEMS_LIST.subList(20, 25)
)
- }
+ queryExecutor.executeNext() // second db load
+ val page2 = listenableFuture2.await() as LoadResult.Page
+ assertThat(page2.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(15, 20)
+ )
+
+ assertTrue(listenableFuture1.isDone)
+ assertTrue(listenableFuture2.isDone)
+ }
@Test
fun refresh_onSuccess() = setupAndRun { db ->
db.dao.addAllItems(ITEMS_LIST)
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
val listenableFuture = pagingSource.refresh(key = 30)
@@ -342,21 +391,91 @@
}
@Test
- fun refresh_awaitThrowsCancellationException() =
+ fun refresh_cancelBeforeObserverRegistered_CancellationException() =
setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
val listenableFuture = pagingSource.refresh(key = 50)
- // the initial runInTransaction load
- assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1) // transformAsync
+
+ // cancel before observer has been registered. This queues up another task which is
+ // the cancelled async function
+ listenableFuture.cancel(true)
+
+ // even though future is cancelled, transformAsync was already queued up which means
+ // observer will still get registered
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+ // start async function but doesn't proceed further
+ queryExecutor.executeAll()
+
+ // ensure initial load is not queued up
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+
+ // await() should throw after cancellation
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // executors should be idle
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+ assertTrue(listenableFuture.isDone)
+ // even though initial refresh load is cancelled, the paging source itself
+ // is NOT invalidated
+ assertFalse(pagingSource.invalid)
+ }
+
+ @Test
+ fun refresh_cancelAfterObserverRegistered_CancellationException() =
+ setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
+
+ val listenableFuture = pagingSource.refresh(key = 50)
+
+ // start transformAsync and register observer
+ queryExecutor.executeNext()
+
+ // cancel after observer registration
+ listenableFuture.cancel(true)
+
+ // start the async function but it has been cancelled so it doesn't queue up
+ // initial load
+ queryExecutor.executeNext()
+
+ // initialLoad not queued
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+
+ // await() should throw after cancellation
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // executors should be idle
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+ assertTrue(listenableFuture.isDone)
+ // even though initial refresh load is cancelled, the paging source itself
+ // is NOT invalidated
+ assertFalse(pagingSource.invalid)
+ }
+
+ @Test
+ fun refresh_cancelAfterLoadIsQueued_CancellationException() =
+ setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
+
+ val listenableFuture = pagingSource.refresh(key = 50)
+
+ queryExecutor.executeAll() // run loadFuture and queue up initial load
listenableFuture.cancel(true)
- assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ // initialLoad has been queued
assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
- transactionExecutor.executeNext() // initial load
- queryExecutor.executeNext() // refreshVersionsAsync from the end runInTransaction
+ transactionExecutor.executeAll() // room starts transaction but doesn't complete load
+ queryExecutor.executeAll() // InvalidationTracker from end of transaction
// await() should throw after cancellation
assertFailsWith<CancellationException> {
@@ -383,7 +502,7 @@
assertThat(queryExecutor.queuedSize()).isEqualTo(1)
listenableFuture.cancel(true)
- queryExecutor.executeNext()
+ queryExecutor.executeAll()
// await() should throw after cancellation
assertFailsWith<CancellationException> {
@@ -407,7 +526,7 @@
assertThat(queryExecutor.queuedSize()).isEqualTo(1)
listenableFuture.cancel(true)
- queryExecutor.executeNext()
+ queryExecutor.executeAll()
// await() should throw after cancellation
assertFailsWith<CancellationException> {
@@ -422,10 +541,12 @@
@Test
fun refresh_canceledFutureRunsOnFailureCallback() =
- setupAndRunWithTestExecutor { db, _, transactionExecutor ->
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
val listenableFuture = pagingSource.refresh(key = 30)
+
+ queryExecutor.executeAll() // start transformAsync & async function
assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
val callbackExecutor = TestExecutor()
@@ -437,7 +558,7 @@
// now cancel future and execute the refresh load. The refresh should not complete.
listenableFuture.cancel(true)
- transactionExecutor.executeNext()
+ transactionExecutor.executeAll()
assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
callbackExecutor.executeAll()
@@ -448,12 +569,11 @@
}
@Test
- fun append_canceledFutureRunsOnFailureCallback() =
+ fun append_canceledFutureRunsOnFailureCallback2() =
setupAndRunWithTestExecutor { db, queryExecutor, _ ->
val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
pagingSource.itemCount.set(100) // bypass check for initial load
- // queue up the append first
val listenableFuture = pagingSource.append(key = 20)
assertThat(queryExecutor.queuedSize()).isEqualTo(1)
@@ -466,7 +586,9 @@
// now cancel future and execute the append load. The append should not complete.
listenableFuture.cancel(true)
- queryExecutor.executeNext()
+
+ queryExecutor.executeNext() // transformAsync
+ queryExecutor.executeNext() // nonInitialLoad
// if load was erroneously completed, InvalidationTracker would be queued
assertThat(queryExecutor.queuedSize()).isEqualTo(0)
@@ -475,7 +597,7 @@
// make sure onFailure callback was executed
assertTrue(onFailureReceived)
assertTrue(listenableFuture.isDone)
- }
+ }
@Test
fun prepend_canceledFutureRunsOnFailureCallback() =
@@ -496,7 +618,8 @@
// now cancel future and execute the prepend which should not complete.
listenableFuture.cancel(true)
- queryExecutor.executeNext()
+ queryExecutor.executeNext() // transformAsync
+ queryExecutor.executeNext() // nonInitialLoad
// if load was erroneously completed, InvalidationTracker would be queued
assertThat(queryExecutor.queuedSize()).isEqualTo(0)
@@ -510,7 +633,7 @@
@Test
fun refresh_AfterCancellation() = setupAndRun { db ->
db.dao.addAllItems(ITEMS_LIST)
- val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db, true)
pagingSource.itemCount.set(100) // bypass check for initial load
val listenableFuture = pagingSource.prepend(key = 50)
@@ -521,7 +644,7 @@
}
// new gen after query from previous gen was cancelled
- val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db, true)
val listenableFuture2 = pagingSource2.refresh()
val result = listenableFuture2.await() as LoadResult.Page
@@ -580,6 +703,74 @@
}
@Test
+ fun append_insertInvalidatesPagingSource() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(
+ db = db,
+ registerObserver = true
+ )
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the append first
+ val listenableFuture = pagingSource.append(key = 20)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ queryExecutor.executeNext() // start transformAsync
+ queryExecutor.executeNext() // start async function
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1) // nonInitialLoad is queued up
+
+ // run this async separately from queryExecutor
+ run {
+ db.dao.addItem(TestItem(101))
+ }
+
+ // tasks in queue [nonInitialLoad, InvalidationTracker(from additem)]
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+ // run nonInitialLoad first. The InvalidationTracker
+ // is still queued up. This imitates delayed notification from Room.
+ queryExecutor.executeNext()
+
+ val result = listenableFuture.await()
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+ assertThat(pagingSource.invalid)
+ }
+
+ @Test
+ fun prepend_insertInvalidatesPagingSource() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(
+ db = db,
+ registerObserver = true
+ )
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the append first
+ val listenableFuture = pagingSource.prepend(key = 20)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ queryExecutor.executeNext() // start transformAsync
+ queryExecutor.executeNext() // start async function
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1) // nonInitialLoad is queued up
+
+ // run this async separately from queryExecutor
+ run {
+ db.dao.addItem(TestItem(101))
+ }
+
+ // tasks in queue [nonInitialLoad, InvalidationTracker(from additem)]
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+ // run nonInitialLoad first. The InvalidationTracker
+ // is still queued up. This imitates delayed notification from Room.
+ queryExecutor.executeNext()
+
+ val result = listenableFuture.await()
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+ assertThat(pagingSource.invalid)
+ }
+
+ @Test
fun test_jumpSupport() = setupAndRun { db ->
val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
assertTrue(pagingSource.jumpingSupported)
@@ -685,7 +876,7 @@
runTest {
db.dao.addAllItems(ITEMS_LIST)
- queryExecutor.executeNext() // InvalidationTracker from the addAllItems
+ queryExecutor.executeAll() // InvalidationTracker from the addAllItems
test(db, queryExecutor, transactionExecutor)
}
tearDown(db)
@@ -700,6 +891,7 @@
private class LimitOffsetListenableFuturePagingSourceImpl(
db: RoomDatabase,
+ registerObserver: Boolean = false,
queryString: String = "SELECT * FROM $tableName ORDER BY id ASC",
) : LimitOffsetListenableFuturePagingSource<TestItem>(
sourceQuery = RoomSQLiteQuery.acquire(
@@ -709,6 +901,14 @@
db = db,
tables = arrayOf(tableName)
) {
+
+ init {
+ // bypass register check and avoid registering observer
+ if (!registerObserver) {
+ privateObserver().privateRegisteredState().set(true)
+ }
+ }
+
override fun convertRows(cursor: Cursor): List<TestItem> {
return convertRowsHelper(cursor)
}
@@ -747,6 +947,27 @@
return tasks.size
}
+@Suppress("UNCHECKED_CAST")
+private fun ThreadSafeInvalidationObserver.privateRegisteredState(): AtomicBoolean {
+ return ThreadSafeInvalidationObserver::class.java
+ .getDeclaredField("registered")
+ .let {
+ it.isAccessible = true
+ it.get(this)
+ } as AtomicBoolean
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.privateObserver():
+ ThreadSafeInvalidationObserver {
+ return LimitOffsetListenableFuturePagingSource::class.java
+ .getDeclaredField("observer")
+ .let {
+ it.isAccessible = true
+ it.get(this)
+ } as ThreadSafeInvalidationObserver
+}
+
private fun LimitOffsetListenableFuturePagingSource<TestItem>.refresh(
key: Int? = null,
): ListenableFuture<LoadResult<Int, TestItem>> {
@@ -883,4 +1104,7 @@
interface TestItemDao {
@Insert
fun addAllItems(testItems: List<TestItem>)
+
+ @Insert
+ fun addItem(testItem: TestItem)
}
diff --git a/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt b/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
index fb2d209..6e2839d 100644
--- a/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
+++ b/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
@@ -19,6 +19,7 @@
import android.database.Cursor
import androidx.annotation.NonNull
import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
import androidx.paging.ListenableFuturePagingSource
import androidx.paging.PagingState
import androidx.room.RoomDatabase
@@ -32,6 +33,7 @@
import androidx.room.paging.util.queryItemCount
import androidx.room.util.createCancellationSignal
import androidx.sqlite.db.SupportSQLiteQuery
+import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicInteger
@@ -53,7 +55,7 @@
tables = tables,
)
- // internal for testing visibility
+ @VisibleForTesting
internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
private val observer = ThreadSafeInvalidationObserver(tables = tables, ::invalidate)
@@ -65,13 +67,18 @@
* cancellation of await() will transitively cancel this future as well.
*/
override fun loadFuture(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Value>> {
- observer.registerIfNecessary(db)
- val tempCount = itemCount.get()
- return if (tempCount == INITIAL_ITEM_COUNT) {
- initialLoad(params)
- } else {
- nonInitialLoad(params, tempCount)
- }
+ return Futures.transformAsync(
+ createListenableFuture(db, false) { observer.registerIfNecessary(db) },
+ {
+ val tempCount = itemCount.get()
+ if (tempCount == INITIAL_ITEM_COUNT) {
+ initialLoad(params)
+ } else {
+ nonInitialLoad(params, tempCount)
+ }
+ },
+ db.queryExecutor
+ )
}
/**
@@ -124,7 +131,7 @@
val result = queryDatabase(
params, sourceQuery, db, tempCount, cancellationSignal, ::convertRows
)
- db.invalidationTracker.refreshVersionsAsync()
+ db.invalidationTracker.refreshVersionsSync()
@Suppress("UNCHECKED_CAST")
if (invalid) INVALID as LoadResult.Invalid<Int, Value> else result
}
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
index d7e7e0c..9baaf87 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
@@ -24,7 +24,7 @@
import android.text.StaticLayout
import android.text.TextPaint
import androidx.compose.ui.text.android.style.BaselineShiftSpan
-import androidx.compose.ui.text.android.style.LineHeightBehaviorSpan
+import androidx.compose.ui.text.android.style.LineHeightStyleSpan
import androidx.compose.ui.text.font.test.R
import androidx.core.content.res.ResourcesCompat
import androidx.test.filters.SmallTest
@@ -423,7 +423,7 @@
val textPaint = createTextPaint(fontSize)
val spannable = SpannableString(text)
spannable.setSpan(
- LineHeightBehaviorSpan(
+ LineHeightStyleSpan(
lineHeight = lineHeight,
startIndex = 0,
endIndex = text.length,
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpanTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
similarity index 98%
rename from text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpanTest.kt
rename to text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
index b74124d..502b37a7 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpanTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
@@ -34,7 +34,7 @@
@OptIn(InternalPlatformTextApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
-class LineHeightBehaviorSpanTest {
+class LineHeightStyleSpanTest {
@Test
fun negative_line_height_does_not_chage_the_values() {
@@ -827,7 +827,7 @@
trimFirstLineTop: Boolean,
trimLastLineBottom: Boolean,
newLineHeight: Int
- ): LineHeightBehaviorSpan = LineHeightBehaviorSpan(
+ ): LineHeightStyleSpan = LineHeightStyleSpan(
lineHeight = newLineHeight.toFloat(),
startIndex = SingleLineStartIndex,
endIndex = SingleLineEndIndex,
@@ -844,7 +844,7 @@
trimFirstLineTop: Boolean,
trimLastLineBottom: Boolean,
fontMetrics: FontMetricsInt
- ): LineHeightBehaviorSpan = createMultiLineSpan(
+ ): LineHeightStyleSpan = createMultiLineSpan(
topPercentage = topPercentage,
trimFirstLineTop = trimFirstLineTop,
trimLastLineBottom = trimLastLineBottom,
@@ -859,7 +859,7 @@
trimFirstLineTop: Boolean,
trimLastLineBottom: Boolean,
newLineHeight: Int
- ): LineHeightBehaviorSpan = LineHeightBehaviorSpan(
+ ): LineHeightStyleSpan = LineHeightStyleSpan(
lineHeight = newLineHeight.toFloat(),
startIndex = MultiLineStartIndex,
endIndex = MultiLineEndIndex,
@@ -915,7 +915,7 @@
* updated values.
*/
@OptIn(InternalPlatformTextApi::class)
-private fun LineHeightBehaviorSpan.runFirstLine(fontMetrics: FontMetricsInt): FontMetricsInt {
+private fun LineHeightStyleSpan.runFirstLine(fontMetrics: FontMetricsInt): FontMetricsInt {
return this.runMultiLine(0, fontMetrics)
}
@@ -924,7 +924,7 @@
* updated values.
*/
@OptIn(InternalPlatformTextApi::class)
-private fun LineHeightBehaviorSpan.runSecondLine(fontMetrics: FontMetricsInt): FontMetricsInt {
+private fun LineHeightStyleSpan.runSecondLine(fontMetrics: FontMetricsInt): FontMetricsInt {
return this.runMultiLine(1, fontMetrics)
}
@@ -933,7 +933,7 @@
* updated values.
*/
@OptIn(InternalPlatformTextApi::class)
-private fun LineHeightBehaviorSpan.runLastLine(fontMetrics: FontMetricsInt): FontMetricsInt {
+private fun LineHeightStyleSpan.runLastLine(fontMetrics: FontMetricsInt): FontMetricsInt {
return this.runMultiLine(2, fontMetrics)
}
@@ -942,7 +942,7 @@
* updated values.
*/
@OptIn(InternalPlatformTextApi::class)
-private fun LineHeightBehaviorSpan.runMultiLine(
+private fun LineHeightStyleSpan.runMultiLine(
line: Int,
fontMetrics: FontMetricsInt
): FontMetricsInt {
@@ -962,7 +962,7 @@
* used.
*/
@OptIn(InternalPlatformTextApi::class)
-private fun LineHeightBehaviorSpan.chooseHeight(
+private fun LineHeightStyleSpan.chooseHeight(
start: Int,
end: Int,
fontMetricsInt: FontMetricsInt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 4b6e963..7a645c1 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -51,7 +51,7 @@
import androidx.compose.ui.text.android.LayoutCompat.TextDirection
import androidx.compose.ui.text.android.LayoutCompat.TextLayoutAlignment
import androidx.compose.ui.text.android.style.BaselineShiftSpan
-import androidx.compose.ui.text.android.style.LineHeightBehaviorSpan
+import androidx.compose.ui.text.android.style.LineHeightStyleSpan
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max
@@ -674,11 +674,11 @@
}
@OptIn(InternalPlatformTextApi::class)
-private fun TextLayout.getLineHeightSpans(): Array<LineHeightBehaviorSpan> {
+private fun TextLayout.getLineHeightSpans(): Array<LineHeightStyleSpan> {
if (text !is Spanned) return emptyArray()
- val lineHeightBehaviorSpans = (text as Spanned).getSpans(
- 0, text.length, LineHeightBehaviorSpan::class.java
+ val lineHeightStyleSpans = (text as Spanned).getSpans(
+ 0, text.length, LineHeightStyleSpan::class.java
)
- if (lineHeightBehaviorSpans.isEmpty()) return emptyArray()
- return lineHeightBehaviorSpans
+ if (lineHeightStyleSpans.isEmpty()) return emptyArray()
+ return lineHeightStyleSpans
}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
similarity index 99%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
index e641e64..24e2065 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightBehaviorSpan.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
@@ -44,7 +44,7 @@
* @suppress
*/
@InternalPlatformTextApi
-class LineHeightBehaviorSpan(
+class LineHeightStyleSpan(
val lineHeight: Float,
private val startIndex: Int,
private val endIndex: Int,
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorScreenshotTest.kt
new file mode 100644
index 0000000..c2fbee9
--- /dev/null
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorScreenshotTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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
+ *
+ * http://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 androidx.wear.compose.material
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class PositionIndicatorScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule
+ val testName = TestName()
+
+ @Test
+ fun left_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.Left,
+ value = 0.2f,
+ ltr = true,
+ goldenIdentifier = testName.methodName
+ )
+
+ @Test
+ fun left_in_rtl_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.Left,
+ value = 0.4f,
+ ltr = false,
+ goldenIdentifier = testName.methodName
+ )
+
+ @Test
+ fun right_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.Right,
+ value = 0.3f,
+ ltr = true,
+ goldenIdentifier = testName.methodName
+ )
+
+ @Test
+ fun right_in_rtl_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.Right,
+ value = 0.5f,
+ ltr = false,
+ goldenIdentifier = testName.methodName
+ )
+
+ @Test
+ fun end_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.End,
+ value = 0.1f,
+ ltr = true,
+ goldenIdentifier = testName.methodName
+ )
+
+ @Test
+ fun end_in_rtl_position_indicator() =
+ position_indicator_position_test(
+ position = PositionIndicatorAlignment.End,
+ value = 0.8f,
+ ltr = false,
+ goldenIdentifier = testName.methodName
+ )
+
+ private fun position_indicator_position_test(
+ position: PositionIndicatorAlignment,
+ value: Float,
+ goldenIdentifier: String,
+ ltr: Boolean = true,
+ ) {
+ rule.setContentWithTheme {
+ val actualLayoutDirection =
+ if (ltr) LayoutDirection.Ltr
+ else LayoutDirection.Rtl
+ CompositionLocalProvider(LocalLayoutDirection provides actualLayoutDirection) {
+ PositionIndicator(
+ value = { value },
+ position = position,
+ modifier = Modifier.testTag(TEST_TAG).background(Color.Black)
+ )
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenIdentifier)
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
index 62199e3..bb25f0c 100644
--- a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
@@ -78,62 +79,28 @@
properties: DialogProperties = DialogProperties(),
content: @Composable () -> Unit,
) {
- var transitionState by remember {
- mutableStateOf(MutableTransitionState(DialogStage.Intro))
+ // Transitions for background and 'dialog content' alpha.
+ var alphaTransitionState by remember {
+ mutableStateOf(MutableTransitionState(AlphaStage.IntroFadeOut))
}
- val transition = updateTransition(transitionState)
- if (showDialog || transitionState.targetState != DialogStage.Intro) {
+ val alphaTransition = updateTransition(alphaTransitionState)
+
+ // Transitions for dialog content scaling.
+ var scaleTransitionState by remember {
+ mutableStateOf(MutableTransitionState(ScaleStage.Intro))
+ }
+ val scaleTransition = updateTransition(scaleTransitionState)
+
+ if (showDialog ||
+ alphaTransitionState.targetState != AlphaStage.IntroFadeOut ||
+ scaleTransitionState.targetState != ScaleStage.Intro) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties,
) {
- val backgroundAlpha by transition.animateFloat(
- transitionSpec = {
- if (transitionState.targetState != DialogStage.Outro)
- tween(durationMillis = RAPID, easing = STANDARD_OUT)
- else
- tween(durationMillis = QUICK, delayMillis = RAPID, easing = STANDARD_IN)
- },
- label = "background-alpha"
- ) { stage ->
- when (stage) {
- DialogStage.Intro -> 1.0f
- DialogStage.Display -> 0.1f
- DialogStage.Outro -> 1.0f
- }
- }
-
- val alpha by transition.animateFloat(
- transitionSpec = {
- if (transitionState.targetState != DialogStage.Outro)
- tween(durationMillis = QUICK, delayMillis = RAPID, easing = STANDARD_IN)
- else
- tween(durationMillis = RAPID, easing = STANDARD_OUT)
- },
- label = "alpha"
- ) { stage ->
- when (stage) {
- DialogStage.Intro -> 0.1f
- DialogStage.Display -> 1.0f
- DialogStage.Outro -> 0.1f
- }
- }
-
- val scale by transition.animateFloat(
- transitionSpec = {
- if (transitionState.targetState != DialogStage.Outro)
- tween(durationMillis = CASUAL, easing = STANDARD_IN)
- else
- tween(durationMillis = CASUAL, easing = STANDARD_OUT)
- },
- label = "scale"
- ) { stage ->
- when (stage) {
- DialogStage.Intro -> 1.25f
- DialogStage.Display -> 1.0f
- DialogStage.Outro -> 1.25f
- }
- }
+ val backgroundAlpha by animateBackgroundAlpha(alphaTransition, alphaTransitionState)
+ val alpha by animateDialogAlpha(alphaTransition, alphaTransitionState)
+ val scale by animateDialogScale(scaleTransition, scaleTransitionState)
Scaffold(
vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
@@ -150,7 +117,8 @@
onDismissed = {
onDismissRequest()
// Reset state for the next time this dialog is shown.
- transitionState = MutableTransitionState(DialogStage.Intro)
+ alphaTransitionState = MutableTransitionState(AlphaStage.IntroFadeOut)
+ scaleTransitionState = MutableTransitionState(ScaleStage.Intro)
}
) { isBackground ->
Box(
@@ -161,32 +129,117 @@
}
}
- // Trigger initial intro animation when dialog is displayed.
SideEffect {
- if (transitionState.currentState == DialogStage.Intro) {
- transitionState.targetState = DialogStage.Display
+ // Trigger initial Intro animation
+ if (alphaTransitionState.currentState == AlphaStage.IntroFadeOut) {
+ // a) Fade out previous screen contents b) Scale down dialog contents.
+ alphaTransitionState.targetState = AlphaStage.IntroFadeIn
+ scaleTransitionState.targetState = ScaleStage.Display
+ } else if (alphaTransitionState.currentState == AlphaStage.IntroFadeIn) {
+ // Now conclude the Intro animation by fading in the dialog contents.
+ alphaTransitionState.targetState = AlphaStage.Display
}
}
- // Trigger leaving the Dialog when the caller updates showDialog to false.
+ // Trigger Outro animation when the caller updates showDialog to false.
LaunchedEffect(showDialog) {
if (!showDialog) {
- transitionState.targetState = DialogStage.Outro
+ // a) Fade out dialog contents b) Scale up dialog contents.
+ alphaTransitionState.targetState = AlphaStage.OutroFadeOut
+ scaleTransitionState.targetState = ScaleStage.Outro
}
}
- // After the outro animation, request to leave the dialog and reset stage to Intro.
- LaunchedEffect(transitionState.currentState) {
- if (transitionState.currentState == DialogStage.Outro) {
+ LaunchedEffect(alphaTransitionState.currentState) {
+ if (alphaTransitionState.currentState == AlphaStage.OutroFadeOut) {
+ // Conclude the Outro animation by fading in the background contents.
+ alphaTransitionState.targetState = AlphaStage.OutroFadeIn
+ } else if (alphaTransitionState.currentState == AlphaStage.OutroFadeIn) {
+ // After the outro animation, leave the dialog & reset alpha/scale transitions.
onDismissRequest()
- transitionState = MutableTransitionState(DialogStage.Intro)
+ alphaTransitionState = MutableTransitionState(AlphaStage.IntroFadeOut)
+ scaleTransitionState = MutableTransitionState(ScaleStage.Intro)
}
}
}
}
}
-private enum class DialogStage {
+@Composable
+private fun animateBackgroundAlpha(
+ alphaTransition: Transition<AlphaStage>,
+ alphaTransitionState: MutableTransitionState<AlphaStage>
+) = alphaTransition.animateFloat(
+ transitionSpec = {
+ if (alphaTransitionState.currentState == AlphaStage.IntroFadeOut)
+ tween(durationMillis = RAPID, easing = STANDARD_OUT)
+ else if (alphaTransitionState.targetState == AlphaStage.OutroFadeIn)
+ tween(durationMillis = QUICK, easing = STANDARD_IN)
+ else
+ tween(durationMillis = 0)
+ },
+ label = "background-alpha"
+) { stage ->
+ when (stage) {
+ AlphaStage.IntroFadeOut -> 0.0f
+ AlphaStage.IntroFadeIn -> 0.9f
+ AlphaStage.Display -> 1.0f
+ AlphaStage.OutroFadeOut -> 0.9f
+ AlphaStage.OutroFadeIn -> 0.0f
+ }
+}
+
+@Composable
+private fun animateDialogAlpha(
+ alphaTransition: Transition<AlphaStage>,
+ alphaTransitionState: MutableTransitionState<AlphaStage>
+) = alphaTransition.animateFloat(
+ transitionSpec = {
+ if (alphaTransitionState.currentState == AlphaStage.IntroFadeIn)
+ tween(durationMillis = QUICK, easing = STANDARD_IN)
+ else if (alphaTransitionState.targetState == AlphaStage.OutroFadeOut)
+ tween(durationMillis = RAPID, easing = STANDARD_OUT)
+ else
+ tween(durationMillis = 0)
+ },
+ label = "alpha"
+) { stage ->
+ when (stage) {
+ AlphaStage.IntroFadeOut -> 0.0f
+ AlphaStage.IntroFadeIn -> 0.1f
+ AlphaStage.Display -> 1.0f
+ AlphaStage.OutroFadeOut -> 0.1f
+ AlphaStage.OutroFadeIn -> 0.0f
+ }
+}
+
+@Composable
+private fun animateDialogScale(
+ scaleTransition: Transition<ScaleStage>,
+ scaleTransitionState: MutableTransitionState<ScaleStage>
+) = scaleTransition.animateFloat(
+ transitionSpec = {
+ if (scaleTransitionState.currentState == ScaleStage.Intro)
+ tween(durationMillis = CASUAL, easing = STANDARD_IN)
+ else
+ tween(durationMillis = CASUAL, easing = STANDARD_OUT)
+ },
+ label = "scale"
+) { stage ->
+ when (stage) {
+ ScaleStage.Intro -> 1.25f
+ ScaleStage.Display -> 1.0f
+ ScaleStage.Outro -> 1.25f
+ }
+}
+
+// Alpha transition stages - Intro and Outro are split into FadeIn/FadeOut stages.
+private enum class AlphaStage {
+ IntroFadeOut, IntroFadeIn, Display, OutroFadeOut, OutroFadeIn;
+}
+
+// Scale transition stages - scaling is applied as single Intro/Outro animations.
+private enum class ScaleStage {
Intro, Display, Outro;
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ContentAlpha.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ContentAlpha.kt
index f8b383f..314e312 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ContentAlpha.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ContentAlpha.kt
@@ -106,7 +106,7 @@
private object HighContrastContentAlpha {
const val high: Float = 1.00f
const val medium: Float = 0.74f
- const val disabled: Float = 0.50f
+ const val disabled: Float = 0.38f
}
/**
@@ -121,5 +121,5 @@
private object LowContrastContentAlpha {
const val high: Float = 0.87f
const val medium: Float = 0.60f
- const val disabled: Float = 0.50f
+ const val disabled: Float = 0.38f
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index 88e9896..a1cf2ac 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -296,26 +296,29 @@
val itemHeightPx = (itemBottomPx - itemTopPx).toFloat()
val viewPortHeightPx = (viewPortEndPx - viewPortStartPx).toFloat()
- val itemEdgeDistanceFromViewPortEdge =
- min(viewPortEndPx - itemTopPx, itemBottomPx - viewPortStartPx)
- val itemEdgeAsFractionOfViewPort = itemEdgeDistanceFromViewPortEdge / viewPortHeightPx
- val heightAsFractionOfViewPort = itemHeightPx / viewPortHeightPx
+ if (viewPortHeightPx > 0) {
+ val itemEdgeDistanceFromViewPortEdge =
+ min(viewPortEndPx - itemTopPx, itemBottomPx - viewPortStartPx)
+ val itemEdgeAsFractionOfViewPort = itemEdgeDistanceFromViewPortEdge / viewPortHeightPx
+ val heightAsFractionOfViewPort = itemHeightPx / viewPortHeightPx
- // Work out the scaling line based on size, this is a value between 0.0..1.0
- val sizeRatio =
- inverseLerp(scalingParams.minElementHeight, scalingParams.maxElementHeight,
- heightAsFractionOfViewPort)
+ // Work out the scaling line based on size, this is a value between 0.0..1.0
+ val sizeRatio =
+ inverseLerp(scalingParams.minElementHeight, scalingParams.maxElementHeight,
+ heightAsFractionOfViewPort)
- val scalingLineAsFractionOfViewPort =
- lerp(scalingParams.minTransitionArea, scalingParams.maxTransitionArea, sizeRatio)
+ val scalingLineAsFractionOfViewPort =
+ lerp(scalingParams.minTransitionArea, scalingParams.maxTransitionArea, sizeRatio)
- if (itemEdgeAsFractionOfViewPort < scalingLineAsFractionOfViewPort) {
- // We are scaling
- val scalingProgressRaw = 1f - itemEdgeAsFractionOfViewPort / scalingLineAsFractionOfViewPort
- val scalingInterpolated = scalingParams.scaleInterpolator.transform(scalingProgressRaw)
+ if (itemEdgeAsFractionOfViewPort < scalingLineAsFractionOfViewPort) {
+ // We are scaling
+ val scalingProgressRaw = 1f - itemEdgeAsFractionOfViewPort /
+ scalingLineAsFractionOfViewPort
+ val scalingInterpolated = scalingParams.scaleInterpolator.transform(scalingProgressRaw)
- scaleToApply = lerp(1.0f, scalingParams.edgeScale, scalingInterpolated)
- alphaToApply = lerp(1.0f, scalingParams.edgeAlpha, scalingInterpolated)
+ scaleToApply = lerp(1.0f, scalingParams.edgeScale, scalingInterpolated)
+ alphaToApply = lerp(1.0f, scalingParams.edgeAlpha, scalingInterpolated)
+ }
}
return ScaleAndAlpha(scaleToApply, alphaToApply)
}
diff --git a/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt b/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
index 2c071f6..98ce3be 100644
--- a/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
+++ b/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
@@ -22,9 +22,7 @@
import android.content.Context
import android.content.Intent
import android.content.res.XmlResourceParser
-import android.os.Handler
import android.os.IBinder
-import android.os.Looper
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -36,6 +34,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.MainScope
private const val TIMEOUT_MS = 500L
@@ -47,7 +46,7 @@
private val realService = object : WatchFaceControlService() {
@SuppressLint("NewApi")
override fun createServiceStub(): IWatchFaceInstanceServiceStub =
- IWatchFaceInstanceServiceStub(this, Handler(Looper.getMainLooper()))
+ IWatchFaceInstanceServiceStub(this, MainScope())
init {
setContext(ApplicationProvider.getApplicationContext<Context>())
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 1220dfe..0dd334c 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -751,14 +751,9 @@
]!!
}
- var isFirstCall = true
handlerCoroutineScope.launch {
leftComplicationSlot.complicationData.collect {
- if (!isFirstCall) {
- updateCountDownLatch.countDown()
- } else {
- isFirstCall = false
- }
+ updateCountDownLatch.countDown()
}
}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
index 98b337e..621ae29 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
@@ -20,13 +20,12 @@
import android.content.Context
import android.content.Intent
import android.os.Build
-import android.os.Handler
import android.os.IBinder
-import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.test.core.app.ApplicationProvider
import androidx.wear.watchface.control.IWatchFaceInstanceServiceStub
import androidx.wear.watchface.control.WatchFaceControlService
+import kotlinx.coroutines.MainScope
/**
* Test shim to allow us to connect to WatchFaceControlService from
@@ -42,9 +41,7 @@
private val realService = object : WatchFaceControlService() {
override fun createServiceStub(): IWatchFaceInstanceServiceStub =
- object : IWatchFaceInstanceServiceStub(
- this@WatchFaceControlTestService, Handler(Looper.getMainLooper())
- ) {
+ object : IWatchFaceInstanceServiceStub(this@WatchFaceControlTestService, MainScope()) {
@RequiresApi(Build.VERSION_CODES.O_MR1)
override fun getApiVersion(): Int = apiVersionOverride ?: super.getApiVersion()
}
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
index bd18d42..691bf55 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
@@ -22,6 +22,7 @@
import androidx.wear.watchface.complications.data.ComplicationText;
import androidx.wear.watchface.complications.data.ComplicationType;
import androidx.wear.watchface.complications.data.DataKt;
+import androidx.wear.watchface.complications.data.LongTextComplicationData;
import androidx.wear.watchface.complications.data.NoDataComplicationData;
import androidx.wear.watchface.complications.data.PlainComplicationText;
import androidx.wear.watchface.complications.data.ShortTextComplicationData;
@@ -32,6 +33,11 @@
import org.junit.runner.RunWith;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.time.Instant;
/** Tests for {@link ComplicationDataTimeline}. */
@@ -158,4 +164,63 @@
assertThat(complicationData.asWireComplicationData().getTimelineEntries().get(0).getType())
.isEqualTo(ComplicationType.NO_DATA.toWireComplicationType());
}
+
+ @Test
+ public void cachedLongTextPlaceholder() throws IOException, ClassNotFoundException {
+ ComplicationDataTimeline timeline =
+ new ComplicationDataTimeline(
+ new LongTextComplicationData.Builder(new PlainComplicationText.Builder(
+ "Hello").build(), ComplicationText.EMPTY).build(),
+ ImmutableList.of(
+ new TimelineEntry(
+ new TimeInterval(Instant.ofEpochMilli(100000000),
+ Instant.ofEpochMilli(200000000)),
+ new NoDataComplicationData(
+ new LongTextComplicationData.Builder(
+ ComplicationText.PLACEHOLDER,
+ ComplicationText.EMPTY).build()
+ )
+ )
+ ));
+
+ @SuppressWarnings("KotlinInternal")
+ ComplicationData complicationData = DataKt.toApiComplicationData(
+ timeline.asWireComplicationData$watchface_complications_data_source_debug()
+ );
+
+ // Simulate caching by a round trip conversion to byteArray.
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(stream);
+ objectOutputStream.writeObject(complicationData.asWireComplicationData());
+ objectOutputStream.close();
+ byte[] byteArray = stream.toByteArray();
+
+ ObjectInputStream objectInputStream =
+ new ObjectInputStream(new ByteArrayInputStream(byteArray));
+ android.support.wearable.complications.ComplicationData wireData =
+ (android.support.wearable.complications.ComplicationData)
+ objectInputStream.readObject();
+ objectInputStream.close();
+
+ // Check the deserialized complication matches the input.
+ ComplicationData deserializedComplicationData = DataKt.toApiComplicationData(wireData);
+ assertThat(deserializedComplicationData.getType()).isEqualTo(ComplicationType.LONG_TEXT);
+
+ LongTextComplicationData longText = (LongTextComplicationData) deserializedComplicationData;
+ assertThat(longText.getText().isPlaceholder()).isFalse();
+
+ ComplicationData timeLineEntry =
+ DataKt.toApiComplicationData(
+ longText.asWireComplicationData().getTimelineEntries().stream().findFirst()
+ .get());
+
+ assertThat(timeLineEntry.getType()).isEqualTo(ComplicationType.NO_DATA);
+ NoDataComplicationData noDataComplicationData = (NoDataComplicationData) timeLineEntry;
+
+ ComplicationData placeholder = noDataComplicationData.getPlaceholder();
+ assertThat(placeholder).isNotNull();
+
+ LongTextComplicationData longTextPlaceholder = (LongTextComplicationData) placeholder;
+ assertThat(longTextPlaceholder.getText().isPlaceholder()).isTrue();
+ }
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index 9f71ee1..3235c2d 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -434,7 +434,7 @@
@RequiresApi(api = Build.VERSION_CODES.P)
private static class SerializedForm implements Serializable {
- private static final int VERSION_NUMBER = 7;
+ private static final int VERSION_NUMBER = 8;
@NonNull
ComplicationData mComplicationData;
@@ -549,6 +549,7 @@
oos.writeLong(start);
long end = mComplicationData.mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
oos.writeLong(end);
+ oos.writeInt(mComplicationData.mFields.getInt(FIELD_TIMELINE_ENTRY_TYPE));
List<ComplicationData> listEntries = mComplicationData.getListEntries();
int listEntriesLength = (listEntries != null) ? listEntries.size() : 0;
@@ -698,6 +699,10 @@
if (end != -1) {
fields.putLong(FIELD_TIMELINE_END_TIME, end);
}
+ int timelineEntryType = ois.readInt();
+ if (timelineEntryType != 0) {
+ fields.putInt(FIELD_TIMELINE_ENTRY_TYPE, timelineEntryType);
+ }
int listEntriesLength = ois.readInt();
if (listEntriesLength != 0) {
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index c34845c..18b19ef 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -16,8 +16,11 @@
package androidx.wear.watchface.complications.data
+import android.content.ComponentName
import android.content.Context
import android.graphics.drawable.Icon
+import android.support.wearable.complications.ComplicationData
+import android.support.wearable.complications.ComplicationData.IMAGE_STYLE_ICON
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import java.time.Instant
@@ -276,6 +279,55 @@
assertThat(placeholderPhotoImage.photoImage).isEqualTo(icon)
assertThat(placeholderPhotoImage.hasPlaceholderFields()).isFalse()
}
+
+ @Test
+ fun wireLongTextWithPlaceholder_toApi() {
+ val timelineEntry =
+ ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
+ .setPlaceholder(
+ ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
+ .setLongText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+ .build()
+ )
+ .build()
+ timelineEntry.timelineStartEpochSecond = 100
+ timelineEntry.timelineEndEpochSecond = 1000
+
+ val wireLongTextComplication = WireComplicationDataBuilder(
+ ComplicationType.LONG_TEXT.toWireComplicationType()
+ )
+ .setEndDateTimeMillis(1650988800000)
+ .setDataSource(ComponentName("a", "b"))
+ .setLongText(
+ android.support.wearable.complications.ComplicationText.plainText("longText")
+ )
+ .setIcon(icon)
+ .setSmallImageStyle(IMAGE_STYLE_ICON)
+ .setContentDescription(
+ android.support.wearable.complications.ComplicationText.plainText("test")
+ )
+ .build()
+ wireLongTextComplication.setTimelineEntryCollection(listOf(timelineEntry))
+
+ val apiLongTextComplicationData = wireLongTextComplication.toApiComplicationData()
+
+ assertThat(apiLongTextComplicationData.type).isEqualTo(ComplicationType.LONG_TEXT)
+ apiLongTextComplicationData as LongTextComplicationData
+ assertThat(apiLongTextComplicationData.text.isPlaceholder()).isFalse()
+
+ val noDataComplicationData =
+ apiLongTextComplicationData.asWireComplicationData().timelineEntries!!.first()
+ .toApiComplicationData()
+
+ assertThat(noDataComplicationData.type).isEqualTo(ComplicationType.NO_DATA)
+ noDataComplicationData as NoDataComplicationData
+
+ val placeholder = noDataComplicationData.placeholder!!
+ assertThat(placeholder.type).isEqualTo(ComplicationType.LONG_TEXT)
+
+ placeholder as LongTextComplicationData
+ assertThat(placeholder.text.isPlaceholder()).isTrue()
+ }
}
fun NoDataComplicationData.toWireFormatRoundTrip() =
diff --git a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
index 65312b5..9cdf97c 100644
--- a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
+++ b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
@@ -44,6 +44,7 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.MainScope
internal const val TIMEOUT_MILLIS = 1000L
@@ -64,7 +65,7 @@
override fun createServiceStub(): IWatchFaceInstanceServiceStub =
object : IWatchFaceInstanceServiceStub(
ApplicationProvider.getApplicationContext<Context>(),
- Handler(Looper.getMainLooper())
+ MainScope()
) {
@RequiresApi(Build.VERSION_CODES.O_MR1)
override fun getApiVersion(): Int = apiVersionOverride ?: super.getApiVersion()
diff --git a/wear/watchface/watchface-style/api/restricted_1.1.0-beta02.txt b/wear/watchface/watchface-style/api/restricted_1.1.0-beta02.txt
index 2174ce1..77eb0fc 100644
--- a/wear/watchface/watchface-style/api/restricted_1.1.0-beta02.txt
+++ b/wear/watchface/watchface-style/api/restricted_1.1.0-beta02.txt
@@ -264,6 +264,7 @@
}
public final class UserStyleSettingKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void moveToStart(org.xmlpull.v1.XmlPullParser, String expectedNode);
}
public enum WatchFaceLayer {
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index 2174ce1..77eb0fc 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -264,6 +264,7 @@
}
public final class UserStyleSettingKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void moveToStart(org.xmlpull.v1.XmlPullParser, String expectedNode);
}
public enum WatchFaceLayer {
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
index 35ca4e9..72857ef 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
@@ -236,4 +236,88 @@
parser.close()
}
+
+ @Test
+ public fun test_inflate_schema_with_parent() {
+ val parser1 = context.resources.getXml(R.xml.schema_with_parent_1)
+ val parser2 = context.resources.getXml(R.xml.schema_with_parent_2)
+
+ parser1.moveToStart("UserStyleSchema")
+ parser2.moveToStart("UserStyleSchema")
+
+ val schema1 = UserStyleSchema.inflate(context.resources, parser1)
+ val schema2 = UserStyleSchema.inflate(context.resources, parser2)
+
+ assertThat(schema1.userStyleSettings.size).isEqualTo(6)
+ assertThat(schema2.userStyleSettings.size).isEqualTo(2)
+
+ // List
+ val simpleListWithParent1 = schema1.userStyleSettings[0]
+ as UserStyleSetting.ListUserStyleSetting
+ val simpleListWithParent2 = schema2.userStyleSettings[0]
+ as UserStyleSetting.ListUserStyleSetting
+
+ assertThat(simpleListWithParent1).isEqualTo(simpleListWithParent2)
+
+ val listParser = context.resources.getXml(R.xml.list_setting_common)
+ listParser.moveToStart("ListUserStyleSetting")
+
+ val simpleListSetting = UserStyleSetting.ListUserStyleSetting.inflate(
+ context.resources, listParser, emptyMap()
+ )
+
+ assertThat(simpleListWithParent1).isEqualTo(simpleListSetting)
+
+ // Check override
+ val listSetting1 = schema1.userStyleSettings[1] as UserStyleSetting.ListUserStyleSetting
+ val listSetting2 = schema1.userStyleSettings[2] as UserStyleSetting.ListUserStyleSetting
+ val listSetting3 = schema1.userStyleSettings[3] as UserStyleSetting.ListUserStyleSetting
+
+ assertThat(listSetting1.id.value).isEqualTo("list_id0")
+ assertThat(listSetting1.displayName).isEqualTo("id0")
+ assertThat(listSetting2.id.value).isEqualTo("list_id1")
+ assertThat(listSetting2.description).isEqualTo("id1")
+ assertThat(listSetting3.id.value).isEqualTo("list_id2")
+ assertThat(listSetting3.affectedWatchFaceLayers).containsExactly(WatchFaceLayer.BASE)
+
+ assertThat(listSetting1.description).isEqualTo(simpleListSetting.description)
+ assertThat(listSetting1.icon!!.resId).isEqualTo(simpleListSetting.icon!!.resId)
+ assertThat(listSetting1.defaultOptionIndex).isEqualTo(simpleListSetting.defaultOptionIndex)
+
+ // Double
+ val simpleDoubleWithParent1 = schema1.userStyleSettings[4]
+ as UserStyleSetting.DoubleRangeUserStyleSetting
+ val simpleDoubleWithParent2 = schema2.userStyleSettings[1]
+ as UserStyleSetting.DoubleRangeUserStyleSetting
+
+ assertThat(simpleDoubleWithParent1).isEqualTo(simpleDoubleWithParent2)
+
+ val doubleParser = context.resources.getXml(R.xml.double_setting_common)
+ doubleParser.moveToStart("DoubleRangeUserStyleSetting")
+
+ val simpleDoubleSetting = UserStyleSetting.DoubleRangeUserStyleSetting.inflate(
+ context.resources, doubleParser
+ )
+
+ assertThat(simpleDoubleWithParent1).isEqualTo(simpleDoubleSetting)
+
+ // Check override
+ val doubleSetting1 = schema1.userStyleSettings[5]
+ as UserStyleSetting.DoubleRangeUserStyleSetting
+
+ assertThat(doubleSetting1.id.value).isEqualTo("double_id0")
+ assertThat(doubleSetting1.defaultValue).isEqualTo(0.0)
+ assertThat(doubleSetting1.maximumValue).isEqualTo(0.0)
+ assertThat(doubleSetting1.minimumValue).isEqualTo(-1.0)
+
+ assertThat(doubleSetting1.displayName).isEqualTo(simpleDoubleSetting.displayName)
+ assertThat(doubleSetting1.affectedWatchFaceLayers).isEqualTo(
+ simpleDoubleSetting.affectedWatchFaceLayers
+ )
+
+ doubleParser.close()
+ listParser.close()
+ parser1.close()
+ parser2.close()
+ }
}
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/double_setting_common.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/double_setting_common.xml
new file mode 100644
index 0000000..0c1475f
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/double_setting_common.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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
+
+ http://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.
+ -->
+
+<DoubleRangeUserStyleSetting
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:affectedWatchFaceLayers="BASE|COMPLICATIONS"
+ app:description="Double range description"
+ app:displayName="Double range"
+ app:id="DoubleRange"
+ app:defaultDouble="2.5"
+ app:maxDouble="10.5"
+ app:minDouble="-1.5"/>
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
new file mode 100644
index 0000000..13eb891
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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
+
+ http://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.
+ -->
+
+<ListUserStyleSetting
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:icon="@drawable/color_style_icon"
+ app:defaultOptionIndex="1"
+ app:description="@string/colors_style_setting_description"
+ app:displayName="@string/colors_style_setting"
+ app:affectedWatchFaceLayers="BASE|COMPLICATIONS|COMPLICATIONS_OVERLAY"
+ app:id="ColorStyle">
+ <OnWatchEditorData android:icon="@drawable/color_style_icon_wf"/>
+ <ListOption
+ android:icon="@drawable/red_icon"
+ app:displayName="@string/red_style_name"
+ app:id="red">
+ <OnWatchEditorData android:icon="@drawable/red_icon_wf"/>
+ </ListOption>
+ <ListOption
+ android:icon="@drawable/green_icon"
+ app:displayName="@string/green_style_name"
+ app:id="green">
+ <OnWatchEditorData android:icon="@drawable/green_icon_wf"/>
+ </ListOption>
+</ListUserStyleSetting>
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_1.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_1.xml
new file mode 100644
index 0000000..7227544
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_1.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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
+
+ http://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.
+ -->
+
+<UserStyleSchema xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <ListUserStyleSetting app:parent="@xml/list_setting_common"/>
+
+ <ListUserStyleSetting
+ app:parent="@xml/list_setting_common"
+ app:id="list_id0"
+ app:displayName="id0"/>
+ <ListUserStyleSetting
+ app:parent="@xml/list_setting_common"
+ app:id="list_id1"
+ app:description="id1"/>
+ <ListUserStyleSetting
+ app:parent="@xml/list_setting_common"
+ app:id="list_id2"
+ app:affectedWatchFaceLayers="BASE"/>
+
+ <DoubleRangeUserStyleSetting app:parent="@xml/double_setting_common"/>
+
+ <DoubleRangeUserStyleSetting
+ app:parent="@xml/double_setting_common"
+ app:id="double_id0"
+ app:defaultDouble="0"
+ app:maxDouble="0"
+ app:minDouble="-1.0"/>
+</UserStyleSchema>
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_2.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_2.xml
new file mode 100644
index 0000000..529c145
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/schema_with_parent_2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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
+
+ http://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.
+ -->
+
+<UserStyleSchema xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <ListUserStyleSetting app:parent="@xml/list_setting_common"/>
+ <DoubleRangeUserStyleSetting app:parent="@xml/double_setting_common"/>
+</UserStyleSchema>
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 2112f62..842cf24 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -157,29 +157,6 @@
)
return WatchFaceEditorData(icon)
}
-
- fun inflateSingleOnWatchEditorData(
- resources: Resources,
- parser: XmlResourceParser
- ): WatchFaceEditorData? {
- var watchFaceEditorData: WatchFaceEditorData? = null
- var type = 0
- val outerDepth = parser.depth
- do {
- if (type == XmlPullParser.START_TAG) {
- if (watchFaceEditorData == null && parser.name == "OnWatchEditorData") {
- watchFaceEditorData = inflate(resources, parser)
- } else {
- throw IllegalArgumentException(
- "Unexpected node ${parser.name} at line ${parser.lineNumber}"
- )
- }
- }
- type = parser.next()
- } while (type != XmlPullParser.END_DOCUMENT && parser.depth > outerDepth)
-
- return watchFaceEditorData
- }
}
}
@@ -309,14 +286,17 @@
internal fun createDisplayText(
resources: Resources,
parser: XmlResourceParser,
- attributeId: String
+ attributeId: String,
+ defaultValue: DisplayText? = null
): DisplayText {
val displayNameId = parser.getAttributeResourceValue(NAMESPACE_APP, attributeId, -1)
return if (displayNameId != -1) {
DisplayText.ResourceDisplayText(resources, displayNameId)
- } else {
+ } else if (parser.hasValue(attributeId) || defaultValue == null) {
DisplayText.CharSequenceDisplayText(
parser.getAttributeValue(NAMESPACE_APP, attributeId) ?: "")
+ } else {
+ defaultValue
}
}
@@ -331,6 +311,127 @@
null
}
}
+
+ /**
+ * Creates appropriate UserStyleSetting base on parent="@xml/..." resource reference.
+ */
+ internal fun <T> createParent(
+ resources: Resources,
+ parser: XmlResourceParser,
+ parentNodeName: String,
+ inflateSetting: (resources: Resources, parser: XmlResourceParser) -> T
+ ): T? {
+ val parentRef = parser.getAttributeResourceValue(NAMESPACE_APP, "parent", 0)
+ return if (0 != parentRef) {
+ val parentParser = resources.getXml(parentRef)
+ parentParser.moveToStart(parentNodeName)
+ inflateSetting(resources, parentParser)
+ } else {
+ null
+ }
+ }
+
+ internal class Params(
+ val id: Id,
+ val displayName: DisplayText,
+ val description: DisplayText,
+ val icon: Icon?,
+ val watchFaceEditorData: WatchFaceEditorData?,
+ val options: List<Option>,
+ val defaultOptionIndex: Int?,
+ val affectedWatchFaceLayers: Collection<WatchFaceLayer>
+ )
+
+ /**
+ * Parses base UserStyleSettings params. If a parent is specified, inherits its attributes
+ * unless they are explicitly specified.
+ */
+ internal fun createBaseWithParent(
+ resources: Resources,
+ parser: XmlResourceParser,
+ parent: UserStyleSetting?,
+ inflateDefault: Boolean,
+ optionInflater:
+ Pair<String, ((resources: Resources, parser: XmlResourceParser) -> Option)>? = null
+ ): Params {
+ val settingType = "UserStyleSetting"
+ val id = getAttributeChecked(
+ parser, "id", String::toString, parent?.id?.value, settingType
+ )
+ val displayName = createDisplayText(
+ resources,
+ parser,
+ "displayName",
+ parent?.displayNameInternal
+ )
+ val description = createDisplayText(
+ resources,
+ parser,
+ "description",
+ parent?.descriptionInternal
+ )
+ val icon = createIcon(
+ resources,
+ parser
+ ) ?: parent?.icon
+
+ val defaultOptionIndex = if (inflateDefault) {
+ getAttributeChecked(
+ parser,
+ "defaultOptionIndex",
+ String::toInt,
+ parent?.defaultOptionIndex ?: 0,
+ settingType
+ )
+ } else null
+
+ val affectsWatchFaceLayers =
+ getAttributeChecked(
+ parser,
+ "affectedWatchFaceLayers",
+ { value -> affectsWatchFaceLayersFlagsToSet(Integer.decode(value)) },
+ parent?.affectedWatchFaceLayers ?: WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ settingType
+ )
+
+ var watchFaceEditorData: WatchFaceEditorData? = null
+ val options = ArrayList<Option>()
+ var type = 0
+ val outerDepth = parser.depth
+ do {
+ if (type == XmlPullParser.START_TAG) {
+ if (parser.name == "OnWatchEditorData") {
+ if (watchFaceEditorData == null) {
+ watchFaceEditorData =
+ WatchFaceEditorData.inflate(resources, parser)
+ } else {
+ throw IllegalArgumentException(
+ "Unexpected node OnWatchEditorData at line " +
+ parser.lineNumber
+ )
+ }
+ } else if (optionInflater != null && optionInflater.first == parser.name) {
+ options.add(optionInflater.second(resources, parser))
+ } else {
+ throw IllegalArgumentException(
+ "Unexpected node ${parser.name} at line ${parser.lineNumber}"
+ )
+ }
+ }
+ type = parser.next()
+ } while (type != XmlPullParser.END_DOCUMENT && parser.depth > outerDepth)
+
+ return Params(
+ Id(id),
+ displayName,
+ description,
+ icon,
+ watchFaceEditorData ?: parent?.watchFaceEditorData,
+ if (parent == null || options.isNotEmpty()) options else parent.options,
+ defaultOptionIndex,
+ affectsWatchFaceLayers
+ )
+ }
}
init {
@@ -699,48 +800,29 @@
internal companion object {
@SuppressLint("ResourceType")
fun inflate(resources: Resources, parser: XmlResourceParser): BooleanUserStyleSetting {
- val id = parser.getAttributeValue(NAMESPACE_APP, "id")
- require(id != null) { "BooleanUserStyleSetting must have an id" }
- val displayName = createDisplayText(
- resources,
+ val settingType = "BooleanUserStyleSetting"
+ val parent =
+ createParent(resources, parser, settingType, ::inflate)
+ val defaultValue = getAttributeChecked(
parser,
- "displayName"
- )
- val description = createDisplayText(
- resources,
- parser,
- "description"
- )
- val icon = createIcon(
- resources,
- parser
- )
- require(parser.hasValue("defaultBoolean")) {
- "defaultBoolean is required for BooleanUserStyleSetting"
- }
- val defaultValue = parser.getAttributeBooleanValue(
- NAMESPACE_APP,
"defaultBoolean",
- true
+ String::toBoolean,
+ parent?.getDefaultValue(),
+ settingType
)
- val affectsWatchFaceLayers = affectsWatchFaceLayersFlagsToSet(
- parser.getAttributeIntValue(
- NAMESPACE_APP,
- "affectedWatchFaceLayers",
- 0b111 // first 3 bits set
- )
+ val params = createBaseWithParent(
+ resources,
+ parser,
+ parent,
+ inflateDefault = false
)
-
- val watchFaceEditorData =
- WatchFaceEditorData.inflateSingleOnWatchEditorData(resources, parser)
-
return BooleanUserStyleSetting(
- Id(id),
- displayName,
- description,
- icon,
- watchFaceEditorData,
- affectsWatchFaceLayers,
+ params.id,
+ params.displayName,
+ params.description,
+ params.icon,
+ params.watchFaceEditorData,
+ params.affectedWatchFaceLayers,
defaultValue
)
}
@@ -1135,79 +1217,29 @@
internal companion object {
@SuppressLint("ResourceType")
+ @Suppress("UNCHECKED_CAST")
fun inflate(
resources: Resources,
parser: XmlResourceParser
): ComplicationSlotsUserStyleSetting {
- val id = parser.getAttributeValue(NAMESPACE_APP, "id")
- require(id != null) { "ComplicationSlotsUserStyleSetting must have an id" }
- val displayName = createDisplayText(
+ val params = createBaseWithParent(
resources,
parser,
- "displayName"
+ createParent(
+ resources, parser, "ComplicationSlotsUserStyleSetting", ::inflate
+ ),
+ inflateDefault = true,
+ optionInflater = "ComplicationSlotsOption" to ComplicationSlotsOption::inflate
)
- val description = createDisplayText(
- resources,
- parser,
- "description"
- )
- val icon = createIcon(
- resources,
- parser
- )
- val defaultOptionIndex = parser.getAttributeIntValue(
- NAMESPACE_APP,
- "defaultOptionIndex",
- 0
- )
- val affectsWatchFaceLayers = affectsWatchFaceLayersFlagsToSet(
- parser.getAttributeIntValue(
- NAMESPACE_APP,
- "affectedWatchFaceLayers",
- 0b111 // first 3 bits set
- )
- )
-
- var watchFaceEditorData: WatchFaceEditorData? = null
- val options = ArrayList<ComplicationSlotsOption>()
- var type = 0
- val outerDepth = parser.depth
- do {
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- "ComplicationSlotsOption" -> options.add(
- ComplicationSlotsOption.inflate(resources, parser)
- )
-
- "OnWatchEditorData" -> {
- if (watchFaceEditorData == null) {
- watchFaceEditorData =
- WatchFaceEditorData.inflate(resources, parser)
- } else {
- throw IllegalArgumentException(
- "Unexpected node OnWatchEditorData at line " +
- parser.lineNumber
- )
- }
- }
-
- else -> throw IllegalArgumentException(
- "Unexpected node ${parser.name} at line ${parser.lineNumber}"
- )
- }
- }
- type = parser.next()
- } while (type != XmlPullParser.END_DOCUMENT && parser.depth > outerDepth)
-
return ComplicationSlotsUserStyleSetting(
- Id(id),
- displayName,
- description,
- icon,
- watchFaceEditorData,
- options,
- affectsWatchFaceLayers,
- defaultOptionIndex
+ params.id,
+ params.displayName,
+ params.description,
+ params.icon,
+ params.watchFaceEditorData,
+ params.options as List<ComplicationSlotsOption>,
+ params.affectedWatchFaceLayers,
+ params.defaultOptionIndex!!
)
}
}
@@ -1470,56 +1502,34 @@
resources: Resources,
parser: XmlResourceParser
): DoubleRangeUserStyleSetting {
- val id = parser.getAttributeValue(NAMESPACE_APP, "id")
- require(id != null) { "DoubleRangeUserStyleSetting must have an id" }
- val displayName = createDisplayText(
+ val settingType = "DoubleRangeUserStyleSetting"
+ val parent =
+ createParent(resources, parser, settingType, ::inflate)
+ val maxDouble = getAttributeChecked(
+ parser, "maxDouble", String::toDouble, parent?.maximumValue, settingType
+ )
+ val minDouble = getAttributeChecked(
+ parser, "minDouble", String::toDouble, parent?.minimumValue, settingType
+ )
+ val defaultDouble = getAttributeChecked(
+ parser, "defaultDouble", String::toDouble, parent?.defaultValue, settingType
+ )
+ val params = createBaseWithParent(
resources,
parser,
- "displayName"
+ parent,
+ inflateDefault = false
)
- val description = createDisplayText(
- resources,
- parser,
- "description"
- )
- val icon = createIcon(
- resources,
- parser
- )
- require(parser.hasValue("maxDouble")) {
- "maxInteger is required for DoubleRangeUserStyleSetting"
- }
- require(parser.hasValue("minDouble")) {
- "minInteger is required for DoubleRangeUserStyleSetting"
- }
- require(parser.hasValue("defaultDouble")) {
- "defaultInteger is required for DoubleRangeUserStyleSetting"
- }
- val maxDouble = parser.getAttributeValue(NAMESPACE_APP, "maxDouble")!!.toDouble()
- val minDouble = parser.getAttributeValue(NAMESPACE_APP, "minDouble")!!.toDouble()
- val defaultDouble = parser.getAttributeValue(
- NAMESPACE_APP,
- "defaultDouble")!!.toDouble()
- val affectsWatchFaceLayers = affectsWatchFaceLayersFlagsToSet(
- parser.getAttributeIntValue(
- NAMESPACE_APP,
- "affectedWatchFaceLayers",
- 0b111 // first 3 bits set
- )
- )
- val watchFaceEditorData =
- WatchFaceEditorData.inflateSingleOnWatchEditorData(resources, parser)
-
return DoubleRangeUserStyleSetting(
- Id(id),
- displayName,
- description,
- icon,
- watchFaceEditorData,
- minDouble.toDouble(),
- maxDouble.toDouble(),
- affectsWatchFaceLayers,
- defaultDouble.toDouble()
+ params.id,
+ params.displayName,
+ params.description,
+ params.icon,
+ params.watchFaceEditorData,
+ minDouble,
+ maxDouble,
+ params.affectedWatchFaceLayers,
+ defaultDouble
)
}
}
@@ -1875,77 +1885,47 @@
)
internal companion object {
+ private fun <T> bindIdToSetting(
+ function: (
+ resources: Resources,
+ parser: XmlResourceParser,
+ idToSetting: Map<String, UserStyleSetting>
+ ) -> T,
+ idToSetting: Map<String, UserStyleSetting>
+ ): (resources: Resources, parser: XmlResourceParser) -> T {
+ return { resources: Resources, parser: XmlResourceParser ->
+ function(resources, parser, idToSetting)
+ }
+ }
+
@SuppressLint("ResourceType")
+ @Suppress("UNCHECKED_CAST")
fun inflate(
resources: Resources,
parser: XmlResourceParser,
idToSetting: Map<String, UserStyleSetting>
): ListUserStyleSetting {
- val id = parser.getAttributeValue(NAMESPACE_APP, "id")
- require(id != null) { "ListUserStyleSetting must have an id" }
- val displayName = createDisplayText(
+ val params = createBaseWithParent(
resources,
parser,
- "displayName"
+ createParent(
+ resources,
+ parser,
+ "ListUserStyleSetting",
+ bindIdToSetting(::inflate, idToSetting)
+ ),
+ inflateDefault = true,
+ "ListOption" to bindIdToSetting(ListOption::inflate, idToSetting)
)
- val description = createDisplayText(
- resources,
- parser,
- "description"
- )
- val icon = createIcon(
- resources,
- parser
- )
- val defaultOptionIndex =
- parser.getAttributeIntValue(NAMESPACE_APP, "defaultOptionIndex", 0)
- val affectsWatchFaceLayers = affectsWatchFaceLayersFlagsToSet(
- parser.getAttributeIntValue(
- NAMESPACE_APP,
- "affectedWatchFaceLayers",
- 0b111 // first 3 bits set
- )
- )
-
- var watchFaceEditorData: WatchFaceEditorData? = null
- val options = ArrayList<ListOption>()
- var type = 0
- val outerDepth = parser.depth
- do {
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- "ListOption" ->
- options.add(ListOption.inflate(resources, parser, idToSetting))
-
- "OnWatchEditorData" -> {
- if (watchFaceEditorData == null) {
- watchFaceEditorData =
- WatchFaceEditorData.inflate(resources, parser)
- } else {
- throw IllegalArgumentException(
- "Unexpected node OnWatchEditorData at line " +
- parser.lineNumber
- )
- }
- }
-
- else -> throw IllegalArgumentException(
- "Unexpected node ${parser.name} at line ${parser.lineNumber}"
- )
- }
- }
- type = parser.next()
- } while (type != XmlPullParser.END_DOCUMENT && parser.depth > outerDepth)
-
return ListUserStyleSetting(
- Id(id),
- displayName,
- description,
- icon,
- watchFaceEditorData,
- options,
- affectsWatchFaceLayers,
- defaultOptionIndex
+ params.id,
+ params.displayName,
+ params.description,
+ params.icon,
+ params.watchFaceEditorData,
+ params.options as List<ListOption>,
+ params.affectedWatchFaceLayers,
+ params.defaultOptionIndex!!
)
}
}
@@ -2229,56 +2209,33 @@
resources: Resources,
parser: XmlResourceParser
): LongRangeUserStyleSetting {
- val id = parser.getAttributeValue(NAMESPACE_APP, "id")
- require(id != null) { "LongRangeUserStyleSetting must have an id" }
- val displayName = createDisplayText(
+ val settingType = "LongRangeUserStyleSetting"
+ val parent =
+ createParent(resources, parser, settingType, ::inflate)
+ val maxInteger = getAttributeChecked(
+ parser, "maxLong", String::toLong, parent?.maximumValue, settingType
+ )
+ val minInteger = getAttributeChecked(
+ parser, "minLong", String::toLong, parent?.minimumValue, settingType
+ )
+ val defaultInteger = getAttributeChecked(
+ parser, "defaultLong", String::toLong, parent?.defaultValue, settingType
+ )
+ val params = createBaseWithParent(
resources,
parser,
- "displayName"
+ parent,
+ inflateDefault = false
)
- val description = createDisplayText(
- resources,
- parser,
- "description"
- )
- val icon = createIcon(
- resources,
- parser
- )
- require(parser.hasValue("maxLong")) {
- "maxLong is required for LongRangeUserStyleSetting"
- }
- require(parser.hasValue("minLong")) {
- "minLong is required for LongRangeUserStyleSetting"
- }
- require(parser.hasValue("defaultLong")) {
- "defaultLong is required for LongRangeUserStyleSetting"
- }
- val maxInteger =
- parser.getAttributeValue(NAMESPACE_APP, "maxLong")!!.toLong()
- val minInteger =
- parser.getAttributeValue(NAMESPACE_APP, "minLong")!!.toLong()
- val defaultInteger =
- parser.getAttributeValue(NAMESPACE_APP, "defaultLong")!!.toLong()
- val affectsWatchFaceLayers = affectsWatchFaceLayersFlagsToSet(
- parser.getAttributeIntValue(
- NAMESPACE_APP,
- "affectedWatchFaceLayers",
- 0b111 // first 3 bits set
- )
- )
- val watchFaceEditorData =
- WatchFaceEditorData.inflateSingleOnWatchEditorData(resources, parser)
-
return LongRangeUserStyleSetting(
- Id(id),
- displayName,
- description,
- icon,
- watchFaceEditorData,
+ params.id,
+ params.displayName,
+ params.description,
+ params.icon,
+ params.watchFaceEditorData,
minInteger,
maxInteger,
- affectsWatchFaceLayers,
+ params.affectedWatchFaceLayers,
defaultInteger
)
}
@@ -2694,3 +2651,35 @@
stream.close()
}
}
+
+/**
+ * Gets the attribute specified by name. If there is no such attribute, applies defaultValue.
+ * Throws exception if calculated result is null.
+ */
+private fun <T> getAttributeChecked(
+ parser: XmlResourceParser,
+ name: String,
+ converter: (String) -> T,
+ defaultValue: T?,
+ settingType: String
+): T {
+ return if (parser.hasValue(name)) {
+ converter(parser.getAttributeValue(NAMESPACE_APP, name)!!)
+ } else {
+ defaultValue ?: throw IllegalArgumentException(
+ "$name is required for $settingType")
+ }
+}
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun XmlPullParser.moveToStart(expectedNode: String) {
+ var type: Int
+ do {
+ type = next()
+ } while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG)
+
+ require(name == expectedNode) {
+ "Expected a $expectedNode node but is $name"
+ }
+}
diff --git a/wear/watchface/watchface-style/src/main/res/values/attrs.xml b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
index 258767f..83ed1f9 100644
--- a/wear/watchface/watchface-style/src/main/res/values/attrs.xml
+++ b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
@@ -25,6 +25,7 @@
<flag name="COMPLICATIONS" value="0x2"/>
<flag name="COMPLICATIONS_OVERLAY" value="0x4"/>
</attr>
+ <attr name="parent" format="reference"/>
<!-- A UserStyleSchema has 0 or more StyleSetting child nodes. -->
<declare-styleable name="UserStyleSchema" />
@@ -52,6 +53,9 @@
<attr name="affectedWatchFaceLayers" />
<!-- The default value, if not specified this defaults to 'true'. -->
<attr name="defaultBoolean" format="boolean" />
+ <!-- Reference (XML) to a setting to base this setting on.
+ All attributes will be inherited from the parent unless specified. -->
+ <attr name="parent"/>
</declare-styleable>
<!-- A ListUserStyleSetting node should contain 1 or more child ComplicationSlotsOption nodes.
@@ -71,6 +75,9 @@
<!-- The index of the default child ComplicationSlotsOption. If not specified, then 0 will
be used. -->
<attr name="defaultOptionIndex" />
+ <!-- Reference (XML) to a setting to base this setting on.
+ All attributes will be inherited from the parent unless specified. -->
+ <attr name="parent"/>
</declare-styleable>
<!-- A DoubleRangeUserStyleSetting node may only contain an optional OnWatchEditorData child
@@ -93,6 +100,9 @@
<attr name="defaultDouble" format="string" />
<!-- maxDouble is required. -->
<attr name="maxDouble" format="string" />
+ <!-- Reference (XML) to a setting to base this setting on.
+ All attributes will be inherited from the parent unless specified. -->
+ <attr name="parent"/>
</declare-styleable>
<!-- A ListUserStyleSetting node should contain 1 or more child ComplicationSlotsOption nodes
@@ -111,6 +121,9 @@
<attr name="affectedWatchFaceLayers" />
<!-- The index of the default child ListOption. If not specified, then 0 will be used. -->
<attr name="defaultOptionIndex" />
+ <!-- Reference (XML) to a setting to base this setting on.
+ All attributes will be inherited from the parent unless specified. -->
+ <attr name="parent"/>
</declare-styleable>
<!-- A LongRangeUserStyleSetting node may only contain an optional OnWatchEditorData child
@@ -133,6 +146,9 @@
<attr name="defaultLong" format="string" />
<!-- maxInteger is required. -->
<attr name="maxLong" format="string" />
+ <!-- Reference (XML) to a setting to base this setting on.
+ All attributes will be inherited from the parent unless specified. -->
+ <attr name="parent"/>
</declare-styleable>
<!-- A ComplicationSlotOverlay node may have a single ComplicationSlotBounds child node without
diff --git a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface.xml b/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface.xml
index dab6de7..6c24d84 100644
--- a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface.xml
+++ b/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface.xml
@@ -16,18 +16,8 @@
<XmlWatchFace xmlns:app="http://schemas.android.com/apk/res-auto">
<UserStyleSchema>
<ListUserStyleSetting
- app:affectedWatchFaceLayers="BASE|COMPLICATIONS|COMPLICATIONS_OVERLAY"
- app:defaultOptionIndex="1"
- app:description="description"
- app:displayName="displayName"
- app:id="TimeStyle">
- <ListOption
- app:displayName="Minimal"
- app:id="minimal" />
- <ListOption
- app:displayName="Seconds"
- app:id="seconds" />
- </ListUserStyleSetting>
+ app:parent="@xml/xml_time_style"
+ app:defaultOptionIndex="1"/>
<BooleanUserStyleSetting
app:affectedWatchFaceLayers="BASE"
app:defaultBoolean="false"
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index a5f6699..e2a2b8e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -684,7 +684,7 @@
* [complicationData] is selected.
*/
private var timelineComplicationData: ComplicationData = NoDataComplicationData()
- private var timelineEntries: List<ComplicationData>? = null
+ private var timelineEntries: List<WireComplicationData>? = null
/**
* Sets the current [ComplicationData] and if it's a timeline, the correct override for
@@ -697,7 +697,7 @@
) {
timelineComplicationData = complicationData
timelineEntries = complicationData.asWireComplicationData().timelineEntries?.map {
- it.toApiComplicationData()
+ it
}
selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
}
@@ -717,15 +717,14 @@
// Select the shortest valid timeline entry.
timelineEntries?.let {
- for (entry in it) {
- val wireEntry = entry.asWireComplicationData()
+ for (wireEntry in it) {
val start = wireEntry.timelineStartEpochSecond
val end = wireEntry.timelineEndEpochSecond
if (start != null && end != null && time >= start && time < end) {
val duration = end - start
if (duration < previousShortest) {
previousShortest = duration
- best = entry
+ best = wireEntry.toApiComplicationData()
}
}
}
@@ -888,6 +887,7 @@
)
writer.println("defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
defaultDataSourcePolicy.systemDataSourceFallbackDefaultType)
+ writer.println("timelineComplicationData=$timelineComplicationData")
writer.println("data=${renderer.getData()}")
val bounds = complicationSlotBounds.perComplicationTypeBounds.map {
"${it.key} -> ${it.value}"
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 8a4926a..cfc5e8b 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -84,7 +84,6 @@
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
@@ -96,7 +95,6 @@
import java.io.ObjectOutputStream
import java.io.PrintWriter
import java.time.Instant
-import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
@@ -2399,50 +2397,6 @@
}
/**
- * Runs the supplied task on the handler thread. If we're not on the handler thread a task is posted
- * and we block until it's been processed.
- *
- * AIDL calls are dispatched from a thread pool, but for simplicity WatchFaceImpl code is largely
- * single threaded so we need to post tasks to the UI thread and wait for them to execute.
- *
- * @param traceEventName The name of the trace event to emit.
- * @param task The task to post on the handler.
- * @return [R] the return value of [task].
- */
-internal fun <R> Handler.runBlockingOnHandlerWithTracing(
- traceEventName: String,
- task: () -> R
-): R = TraceEvent(traceEventName).use {
- if (looper == Looper.myLooper()) {
- task.invoke()
- } else {
- val latch = CountDownLatch(1)
- var returnVal: R? = null
- var exception: Exception? = null
- if (post {
- try {
- returnVal = TraceEvent("$traceEventName invokeTask").use {
- task.invoke()
- }
- } catch (e: Exception) {
- // Will rethrow on the calling thread.
- exception = e
- }
- latch.countDown()
- }
- ) {
- latch.await()
- if (exception != null) {
- throw exception as Exception
- }
- }
- // R might be nullable so we can't assert nullability here.
- @Suppress("UNCHECKED_CAST")
- returnVal as R
- }
-}
-
-/**
* Runs the supplied task on the handler thread. If we're not on the handler thread a task is
* posted.
*
@@ -2472,19 +2426,16 @@
traceEventName: String,
task: suspend () -> R
): R = TraceEvent(traceEventName).use {
- val latch = CountDownLatch(1)
- var r: R? = null
- launch {
- try {
- r = task()
- } catch (e: Exception) {
- Log.e("CoroutineScope", "Exception in traceEventName", e)
- throw e
+ try {
+ return runBlocking {
+ withContext(coroutineContext) {
+ task()
+ }
}
- latch.countDown()
+ } catch (e: Exception) {
+ Log.e("CoroutineScope", "Exception in traceEventName", e)
+ throw e
}
- latch.await()
- return r!!
}
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/XmlSchemaAndComplicationSlotsDefinition.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/XmlSchemaAndComplicationSlotsDefinition.kt
index 925a9f7..b0abada 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/XmlSchemaAndComplicationSlotsDefinition.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/XmlSchemaAndComplicationSlotsDefinition.kt
@@ -29,6 +29,7 @@
import androidx.wear.watchface.complications.hasValue
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.moveToStart
import org.xmlpull.v1.XmlPullParser
import kotlin.jvm.Throws
@@ -46,21 +47,13 @@
resources: Resources,
parser: XmlResourceParser
): XmlSchemaAndComplicationSlotsDefinition {
- // Parse next until start tag is found
- var type: Int
- do {
- type = parser.next()
- } while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG)
-
- require(parser.name == "XmlWatchFace") {
- "Expected a XmlWatchFace node"
- }
+ parser.moveToStart("XmlWatchFace")
var schema: UserStyleSchema? = null
var flavors: UserStyleFlavors? = null
val outerDepth = parser.depth
- type = parser.next()
+ var type = parser.next()
// Parse the XmlWatchFace declaration.
val complicationSlots = ArrayList<ComplicationSlotStaticData>()
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index ed79006..544256e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -20,9 +20,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.os.Handler
import android.os.IBinder
-import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -42,11 +40,13 @@
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.ComplicationSlotMetadataWireFormat
import androidx.wear.watchface.editor.EditorService
-import androidx.wear.watchface.runBlockingOnHandlerWithTracing
+import androidx.wear.watchface.runBlockingWithTracing
import androidx.wear.watchface.style.data.UserStyleFlavorsWireFormat
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
import java.io.FileDescriptor
import java.io.PrintWriter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
/**
* A service for creating and controlling watch face instances.
@@ -77,7 +77,7 @@
@VisibleForTesting
public open fun createServiceStub(): IWatchFaceInstanceServiceStub =
TraceEvent("WatchFaceControlService.createServiceStub").use {
- IWatchFaceInstanceServiceStub(this, Handler(Looper.getMainLooper()))
+ IWatchFaceInstanceServiceStub(this, MainScope())
}
@VisibleForTesting
@@ -95,29 +95,12 @@
}
}
-/**
- * Factory for use by on watch face editors to create [IWatchFaceControlService].
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(27)
-public class WatchFaceControlServiceFactory {
- public companion object {
- @JvmStatic
- public fun createWatchFaceControlService(
- context: Context,
- uiThreadHandler: Handler
- ): IWatchFaceControlService = IWatchFaceInstanceServiceStub(context, uiThreadHandler)
- }
-}
-
/** @hide */
@RequiresApi(27)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class IWatchFaceInstanceServiceStub(
private val context: Context,
- private val uiThreadHandler: Handler
+ private val uiThreadCoroutineScope: CoroutineScope
) : IWatchFaceControlService.Stub() {
override fun getApiVersion(): Int = IWatchFaceControlService.API_VERSION
@@ -139,7 +122,7 @@
val engine = createHeadlessEngine(params.watchFaceName, context)
engine?.let {
// This is serviced on a background thread so it should be fine to block.
- uiThreadHandler.runBlockingOnHandlerWithTracing("createHeadlessInstance") {
+ uiThreadCoroutineScope.runBlockingWithTracing("createHeadlessInstance") {
// However the WatchFaceService.createWatchFace method needs to be run on the UI
// thread.
it.createHeadlessInstance(params)
diff --git a/wear/watchface/watchface/src/main/res/values/config.xml b/wear/watchface/watchface/src/main/res/values/config.xml
index a0e0eec..756b882 100644
--- a/wear/watchface/watchface/src/main/res/values/config.xml
+++ b/wear/watchface/watchface/src/main/res/values/config.xml
@@ -17,5 +17,5 @@
<resources>
<bool name="watch_face_instance_service_enabled">false</bool>
- <integer name="watch_face_xml_version">0</integer>
+ <integer name="watch_face_xml_version">1</integer>
</resources>
diff --git a/wear/watchface/watchface/src/main/res/xml/xml_time_style.xml b/wear/watchface/watchface/src/main/res/xml/xml_time_style.xml
new file mode 100644
index 0000000..47c1ebe
--- /dev/null
+++ b/wear/watchface/watchface/src/main/res/xml/xml_time_style.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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
+
+ http://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.
+ -->
+<ListUserStyleSetting
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:affectedWatchFaceLayers="BASE|COMPLICATIONS|COMPLICATIONS_OVERLAY"
+ app:description="description"
+ app:displayName="displayName"
+ app:id="TimeStyle">
+ <ListOption
+ app:displayName="Minimal"
+ app:id="minimal" />
+ <ListOption
+ app:displayName="Seconds"
+ app:id="seconds" />
+</ListUserStyleSetting>
\ No newline at end of file
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index b478cb7..47f767d 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -54,6 +54,7 @@
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.CountUpTimeReference
import androidx.wear.watchface.complications.data.EmptyComplicationData
+import androidx.wear.watchface.complications.data.LongTextComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
@@ -4665,6 +4666,55 @@
}
@Test
+ public fun selectComplicationDataForInstant_timeLineWithPlaceholder() {
+ val placeholderText =
+ androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER
+ val timelineEntry =
+ ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
+ .setPlaceholder(
+ ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
+ .setLongText(placeholderText.toWireComplicationText())
+ .build()
+ )
+ .build()
+ timelineEntry.timelineStartEpochSecond = 100
+ timelineEntry.timelineEndEpochSecond = 1000
+
+ val wireLongTextComplication = ComplicationData.Builder(
+ ComplicationType.LONG_TEXT.toWireComplicationType()
+ )
+ .setEndDateTimeMillis(1650988800000)
+ .setDataSource(ComponentName("a", "b"))
+ .setLongText(ComplicationText.plainText("longText"))
+ .setSmallImageStyle(ComplicationData.IMAGE_STYLE_ICON)
+ .setContentDescription(ComplicationText.plainText("test"))
+ .build()
+ wireLongTextComplication.setTimelineEntryCollection(listOf(timelineEntry))
+
+ initEngine(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication),
+ UserStyleSchema(emptyList())
+ )
+
+ watchFaceImpl.onComplicationSlotDataUpdate(
+ LEFT_COMPLICATION_ID,
+ wireLongTextComplication.toApiComplicationData()
+ )
+
+ complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(0))
+ assertThat(getLeftLongTextComplicationDataText()).isEqualTo("longText")
+
+ complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(100))
+ val leftComplication = complicationSlotsManager[
+ LEFT_COMPLICATION_ID
+ ]!!.complicationData.value as NoDataComplicationData
+
+ val placeholder = leftComplication.placeholder as LongTextComplicationData
+ assertThat(placeholder.text.isPlaceholder()).isTrue()
+ }
+
+ @Test
public fun renderParameters_isScreenshot() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
@@ -4815,6 +4865,17 @@
)
}
+ private fun getLeftLongTextComplicationDataText(): CharSequence {
+ val complication = complicationSlotsManager[
+ LEFT_COMPLICATION_ID
+ ]!!.complicationData.value as LongTextComplicationData
+
+ return complication.text.getTextAt(
+ ApplicationProvider.getApplicationContext<Context>().resources,
+ Instant.EPOCH
+ )
+ }
+
@SuppressLint("NewApi")
@Suppress("DEPRECATION")
private fun getChinWindowInsetsApi25(@Px chinHeight: Int): WindowInsets =
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
index b3d8ad5..81c7a37 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
@@ -26,6 +26,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,6 +45,7 @@
* Test the algorithmic darkening is disabled by default.
*/
@Test
+ @Ignore("Disabled due to b/230365968")
public void testSimplifiedDarkMode_default() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
@@ -55,6 +57,7 @@
* Test the algorithmic darkening on web content that doesn't support dark style.
*/
@Test
+ @Ignore("Disabled due to b/230365968")
public void testSimplifiedDarkMode_rendersDark() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
@@ -77,6 +80,7 @@
* Test the algorithmic darkening on web content that supports dark style.
*/
@Test
+ @Ignore("Disabled due to b/230365968")
public void testSimplifiedDarkMode_pageSupportDarkTheme() {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);