Merge "Expose Capture Latency in CameraPipe API" into androidx-main
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index eadf4b8..0aa561b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -19,7 +19,10 @@
import android.content.ComponentCallbacks2
import android.content.pm.ActivityInfo
import android.content.res.Configuration
+import android.os.Build
import android.os.Build.VERSION.SDK_INT
+import android.os.Bundle
+import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.ScrollState
@@ -35,6 +38,8 @@
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.internal.Strings
+import androidx.compose.material3.internal.getString
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@@ -53,6 +58,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
@@ -65,6 +71,7 @@
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performClick
@@ -82,6 +89,7 @@
import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.common.truth.Truth.assertThat
@@ -1117,4 +1125,31 @@
// Size entirely filled by padding provided by WindowInsetPadding
rule.onNodeWithTag(sheetTag).onParent().assertHeightIsEqualTo(sheetHeight)
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun modalBottomSheet_assertSheetContentIsReadBeforeScrim() {
+ lateinit var composeView: View
+ var closeSheet = ""
+ rule.setContent {
+ closeSheet = getString(Strings.CloseSheet)
+ ModalBottomSheet(onDismissRequest = {}, modifier = Modifier.testTag(sheetTag)) {
+ composeView = LocalView.current
+ Box(Modifier.fillMaxWidth().height(sheetHeight))
+ }
+ }
+
+ val scrimViewId = rule.onNodeWithContentDescription(closeSheet).fetchSemanticsNode().id
+ val sheetViewId = rule.onNodeWithTag(sheetTag).fetchSemanticsNode().id
+
+ rule.runOnUiThread {
+ val accessibilityNodeProvider = composeView.accessibilityNodeProvider
+ val sheetViewANI = accessibilityNodeProvider.createAccessibilityNodeInfo(sheetViewId)
+ // Ensure that sheet A11y info is read before scrim view.
+ assertThat(sheetViewANI?.extras?.traversalBefore).isAtMost(scrimViewId)
+ }
+ }
+
+ private val Bundle.traversalBefore: Int
+ get() = getInt("android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL")
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index e144c31..d1ac45a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -64,9 +64,11 @@
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
@@ -176,11 +178,11 @@
},
predictiveBackProgress = predictiveBackProgress,
) {
- Box(modifier = Modifier.fillMaxSize().imePadding()) {
+ Box(modifier = Modifier.fillMaxSize().imePadding().semantics { isTraversalGroup = true }) {
Scrim(
color = scrimColor,
onDismissRequest = animateToDismiss,
- visible = sheetState.targetValue != Hidden
+ visible = sheetState.targetValue != Hidden,
)
ModalBottomSheetContent(
predictiveBackProgress,
@@ -277,7 +279,10 @@
startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
onDragStopped = { settleToDismiss(it) }
)
- .semantics { paneTitle = bottomSheetPaneTitle }
+ .semantics {
+ paneTitle = bottomSheetPaneTitle
+ traversalIndex = 0f
+ }
.graphicsLayer {
val sheetOffset = sheetState.anchoredDraggableState.offset
val sheetHeight = size.height
@@ -442,6 +447,7 @@
if (visible) {
Modifier.pointerInput(onDismissRequest) { detectTapGestures { onDismissRequest() } }
.semantics(mergeDescendants = true) {
+ traversalIndex = 1f
contentDescription = closeSheet
onClick {
onDismissRequest()
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 5b99cea..21ab6f74 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -586,7 +586,7 @@
package androidx.compose.ui.test.internal {
@SuppressCompatibility @androidx.compose.ui.test.InternalTestApi public abstract class DelayPropagatingContinuationInterceptorWrapper extends kotlin.coroutines.AbstractCoroutineContextElement implements kotlin.coroutines.ContinuationInterceptor kotlinx.coroutines.Delay {
- ctor public DelayPropagatingContinuationInterceptorWrapper(kotlin.coroutines.ContinuationInterceptor? wrappedInterceptor);
+ ctor public DelayPropagatingContinuationInterceptorWrapper(kotlin.coroutines.ContinuationInterceptor wrappedInterceptor);
}
}
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index b1bbd4a..2f5bc6c 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -587,7 +587,7 @@
package androidx.compose.ui.test.internal {
@SuppressCompatibility @androidx.compose.ui.test.InternalTestApi public abstract class DelayPropagatingContinuationInterceptorWrapper extends kotlin.coroutines.AbstractCoroutineContextElement implements kotlin.coroutines.ContinuationInterceptor kotlinx.coroutines.Delay {
- ctor public DelayPropagatingContinuationInterceptorWrapper(kotlin.coroutines.ContinuationInterceptor? wrappedInterceptor);
+ ctor public DelayPropagatingContinuationInterceptorWrapper(kotlin.coroutines.ContinuationInterceptor wrappedInterceptor);
}
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/internal/DelayPropagatingContinuationInterceptorWrapper.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/internal/DelayPropagatingContinuationInterceptorWrapper.kt
index 50c0cb7..37c58c3 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/internal/DelayPropagatingContinuationInterceptorWrapper.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/internal/DelayPropagatingContinuationInterceptorWrapper.kt
@@ -20,16 +20,14 @@
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.ContinuationInterceptor
import kotlinx.coroutines.Delay
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
/**
- * A [ContinuationInterceptor] that wraps another interceptor and implements [Delay]. If the wrapped
- * interceptor also implements [Delay], the delay implementation is delegated to it, otherwise it's
- * delegated to the default delay implementation (i.e. [Dispatchers.Default]). It is necessary that
- * interceptors used in tests, with one of the [TestDispatcher]s, propagate delay like this in order
- * to work with the delay skipping that those dispatchers perform.
+ * A [ContinuationInterceptor] that wraps another interceptor and implements [Delay] by delegating
+ * to the wrapped interceptor. It is necessary that interceptors used in tests, with one of the
+ * [TestDispatcher]s, propagate delay like this in order to work with the delay skipping that those
+ * dispatchers perform.
*/
// TODO(b/263369561): avoid InternalCoroutinesApi - it is not expected that Delay gain a method but
// if it ever did this would have potential runtime crashes for tests. Medium term we will leave
@@ -38,10 +36,13 @@
@OptIn(InternalCoroutinesApi::class)
@InternalTestApi
abstract class DelayPropagatingContinuationInterceptorWrapper(
- wrappedInterceptor: ContinuationInterceptor?
+ wrappedInterceptor: ContinuationInterceptor
) :
AbstractCoroutineContextElement(ContinuationInterceptor),
ContinuationInterceptor,
// Coroutines will internally use the Default dispatcher as the delay if the
// ContinuationInterceptor does not implement Delay.
- Delay by ((wrappedInterceptor as? Delay) ?: (Dispatchers.Default as Delay))
+ Delay by ((wrappedInterceptor as? Delay)
+ ?: error(
+ "wrappedInterceptor of DelayPropagatingContinuationInterceptorWrapper must implement Delay"
+ ))
diff --git a/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/FrameDeferringContinuationInterceptor.jvm.kt b/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/FrameDeferringContinuationInterceptor.jvm.kt
index d77b90b..3d97309 100644
--- a/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/FrameDeferringContinuationInterceptor.jvm.kt
+++ b/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/FrameDeferringContinuationInterceptor.jvm.kt
@@ -31,7 +31,7 @@
* continuation needs to be dispatched.
*/
@OptIn(InternalTestApi::class)
-internal class FrameDeferringContinuationInterceptor(parentInterceptor: ContinuationInterceptor?) :
+internal class FrameDeferringContinuationInterceptor(parentInterceptor: ContinuationInterceptor) :
DelayPropagatingContinuationInterceptorWrapper(parentInterceptor) {
private val parentDispatcher = parentInterceptor as? CoroutineDispatcher
private val toRunTrampolined = ArrayDeque<TrampolinedTask<*>>()
diff --git a/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/TestMonotonicFrameClock.jvm.kt b/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/TestMonotonicFrameClock.jvm.kt
index 6e84196..bd1ca2b 100644
--- a/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/TestMonotonicFrameClock.jvm.kt
+++ b/compose/ui/ui-test/src/jvmMain/kotlin/androidx/compose/ui/test/TestMonotonicFrameClock.jvm.kt
@@ -39,7 +39,7 @@
* [coroutineScope] contain the test dispatcher controlled by [delayController].
*
* @param coroutineScope The [CoroutineScope] used to simulate the main thread and schedule frames
- * on. It must contain a [TestCoroutineScheduler].
+ * on. It must contain a [TestCoroutineScheduler] and a [ContinuationInterceptor].
* @param frameDelayNanos The number of nanoseconds to [delay] between executing frames.
* @param onPerformTraversals Called with the frame time of the frame that was just executed, after
* running all `withFrameNanos` callbacks, but before resuming their callers' continuations. Any
@@ -60,9 +60,13 @@
) : MonotonicFrameClock {
private val delayController =
requireNotNull(coroutineScope.coroutineContext[TestCoroutineScheduler]) {
- "coroutineScope should have TestCoroutineScheduler"
+ "TestMonotonicFrameClock's coroutineScope must have a TestCoroutineScheduler"
}
- private val parentInterceptor = coroutineScope.coroutineContext[ContinuationInterceptor]
+ // The parentInterceptor resolves to the TestDispatcher
+ private val parentInterceptor =
+ requireNotNull(coroutineScope.coroutineContext[ContinuationInterceptor]) {
+ "TestMonotonicFrameClock's coroutineScope must have a ContinuationInterceptor"
+ }
private val lock = Any()
private var awaiters = mutableListOf<(Long) -> Unit>()
private var spareAwaiters = mutableListOf<(Long) -> Unit>()
diff --git a/libraryversions.toml b/libraryversions.toml
index bed356b..32eda8f 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -96,10 +96,10 @@
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
METRICS = "1.0.0-beta02"
-NAVIGATION = "2.8.0-beta07"
+NAVIGATION = "2.8.0-rc01"
PAGING = "3.4.0-alpha01"
PALETTE = "1.1.0-alpha01"
-PDF = "1.0.0-alpha01"
+PDF = "1.0.0-alpha02"
PERCENTLAYOUT = "1.1.0-alpha01"
PREFERENCE = "1.3.0-alpha01"
PRINT = "1.1.0-beta01"
@@ -115,7 +115,7 @@
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
REMOTECALLBACK = "1.0.0-alpha02"
RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.7.0-alpha06"
+ROOM = "2.7.0-alpha07"
SAFEPARCEL = "1.0.0-alpha01"
SAVEDSTATE = "1.3.0-alpha01"
SECURITY = "1.1.0-alpha07"
@@ -131,7 +131,7 @@
SLICE_BUILDERS_KTX = "1.0.0-alpha09"
SLICE_REMOTECALLBACK = "1.0.0-alpha01"
SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.5.0-alpha06"
+SQLITE = "2.5.0-alpha07"
SQLITE_INSPECTOR = "2.1.0-alpha01"
STABLE_AIDL = "1.0.0-alpha01"
STARTUP = "1.2.0-alpha03"
diff --git a/lifecycle/lifecycle-runtime/proguard-rules.pro b/lifecycle/lifecycle-runtime/proguard-rules.pro
index 95192c1..4335578 100644
--- a/lifecycle/lifecycle-runtime/proguard-rules.pro
+++ b/lifecycle/lifecycle-runtime/proguard-rules.pro
@@ -15,6 +15,12 @@
@androidx.lifecycle.OnLifecycleEvent *;
}
+# The deprecated `android.app.Fragment` creates `Fragment` instances using reflection.
+# See: b/338958225, b/341537875
+-keepclasseswithmembers,allowobfuscation public class androidx.lifecycle.ReportFragment {
+ public <init>();
+}
+
# this rule is need to work properly when app is compiled with api 28, see b/142778206
# Also this rule prevents registerIn from being inlined.
--keepclassmembers class androidx.lifecycle.ReportFragment$LifecycleCallbacks { *; }
\ No newline at end of file
+-keepclassmembers class androidx.lifecycle.ReportFragment$LifecycleCallbacks { *; }
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 5969262..9fa7d67 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,11 +28,11 @@
implementation(libs.kotlinStdlib)
api("androidx.activity:activity-compose:1.8.0")
- api("androidx.compose.animation:animation:1.7.0-beta06")
- implementation("androidx.compose.foundation:foundation-layout:1.7.0-beta06")
- api("androidx.compose.runtime:runtime:1.7.0-beta06")
- api("androidx.compose.runtime:runtime-saveable:1.7.0-beta06")
- api("androidx.compose.ui:ui:1.7.0-beta06")
+ api("androidx.compose.animation:animation:1.7.0-rc01")
+ implementation("androidx.compose.foundation:foundation-layout:1.7.0-rc01")
+ api("androidx.compose.runtime:runtime:1.7.0-rc01")
+ api("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
+ api("androidx.compose.ui:ui:1.7.0-rc01")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
implementation(libs.kotlinSerializationCore)
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 66ef0c3..82b13ec 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -147,7 +147,6 @@
private var shouldRedrawOnDocumentLoaded = false
private var isAnnotationIntentResolvable = false
private var documentLoaded = false
- private var isDocumentLoadedFirstTime = false
/**
* The URI of the PDF document to display defaulting to `null`.
@@ -294,9 +293,8 @@
shouldRedrawOnDocumentLoaded = false
}
annotationButton?.let { button ->
- if (isDocumentLoadedFirstTime && isAnnotationIntentResolvable) {
+ if ((savedInstanceState == null) && isAnnotationIntentResolvable) {
button.visibility = View.VISIBLE
- isDocumentLoadedFirstTime = false
}
}
},
@@ -304,6 +302,9 @@
)
setUpEditFab()
+ if (savedInstanceState != null) {
+ paginatedView?.isConfigurationChanged = true
+ }
// Need to adjust the view only after the layout phase is completed for the views to
// accurately calculate the height of the view
@@ -467,6 +468,12 @@
val showAnnotationButton = state.getBoolean(KEY_SHOW_ANNOTATION)
isAnnotationIntentResolvable =
showAnnotationButton && findInFileView!!.visibility != View.VISIBLE
+ if (
+ isAnnotationIntentResolvable &&
+ state.getBoolean(KEY_ANNOTATION_BUTTON_VISIBILITY)
+ ) {
+ annotationButton?.visibility = View.VISIBLE
+ }
}
}
@@ -621,7 +628,8 @@
return
}
if (
- annotationButton?.visibility != View.VISIBLE &&
+ isAnnotationIntentResolvable &&
+ annotationButton?.visibility != View.VISIBLE &&
findInFileView?.visibility != View.VISIBLE
) {
annotationButton?.post {
@@ -713,6 +721,10 @@
pdfLoaderCallbacks?.selectionModel?.let {
outState.putParcelable(KEY_PAGE_SELECTION, it.selection().get())
}
+ outState.putBoolean(
+ KEY_ANNOTATION_BUTTON_VISIBILITY,
+ (annotationButton?.visibility == View.VISIBLE)
+ )
}
private fun loadFile(fileUri: Uri) {
@@ -743,7 +755,6 @@
annotationButton?.visibility = View.GONE
}
localUri = fileUri
- isDocumentLoadedFirstTime = true
}
private fun validateFileUri(fileUri: Uri) {
@@ -834,5 +845,6 @@
private const val KEY_SHOW_ANNOTATION: String = "showEditFab"
private const val KEY_PAGE_SELECTION: String = "currentPageSelection"
private const val KEY_DOCUMENT_URI: String = "documentUri"
+ private const val KEY_ANNOTATION_BUTTON_VISIBILITY = "isAnnotationVisible"
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 5af4b14..0ab2bfe 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -66,6 +66,8 @@
private PageViewFactory mPageViewFactory;
+ private boolean mIsConfigurationChanged = false;
+
public PaginatedView(@NonNull Context context) {
this(context, null);
}
@@ -444,4 +446,12 @@
}
});
}
+
+ public void setConfigurationChanged(boolean configurationChanged) {
+ this.mIsConfigurationChanged = configurationChanged;
+ }
+
+ public boolean isConfigurationChanged() {
+ return mIsConfigurationChanged;
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
index 037f875..2fb40ab 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
@@ -80,7 +80,7 @@
mIsPageScrollingUp = false;
}
- if (mIsAnnotationIntentResolvable) {
+ if (mIsAnnotationIntentResolvable && !mPaginatedView.isConfigurationChanged()) {
if (!isAnnotationButtonVisible() && position.scrollY == 0
&& mFindInFileView.getVisibility() == View.GONE) {
@@ -108,6 +108,9 @@
}
});
}
+ } else if (mPaginatedView.isConfigurationChanged()
+ && position.scrollY != oldPosition.scrollY) {
+ mPaginatedView.setConfigurationChanged(false);
}
if (position.scrollY > 0) {
diff --git a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
index a7a90a4..873c2a5 100644
--- a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
@@ -63,7 +63,8 @@
android:cropToPadding="true"
android:padding="3dp"
android:scaleType="centerInside"
- android:layout_margin="5dp"/>
+ android:layout_margin="5dp"
+ android:contentDescription = "@string/previous_button_description"/>
<ImageButton
android:id="@+id/find_next_btn"
@@ -76,7 +77,7 @@
android:padding="3dp"
android:scaleType="centerInside"
android:layout_margin="5dp"
- />
+ android:contentDescription = "@string/next_button_description"/>
<ImageButton
android:id="@+id/close_btn"
android:layout_width="34dp"
@@ -89,6 +90,7 @@
android:scaleType="centerInside"
android:layout_marginVertical="5dp"
android:layout_marginLeft="5dp"
- android:layout_marginRight="10dp"/>
+ android:layout_marginRight="10dp"
+ android:contentDescription = "@string/close_button_description"/>
</merge>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values/strings.xml b/pdf/pdf-viewer/src/main/res/values/strings.xml
index 80fb57f..79cf9a2 100644
--- a/pdf/pdf-viewer/src/main/res/values/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values/strings.xml
@@ -117,6 +117,15 @@
<!-- Hint text: Placeholder text shown in search box until the user enters query text. [CHAR LIMIT=20] -->
<string name="hint_find">Find in file</string>
+ <!-- Content description for previous button in find in file menu -->
+ <string name="previous_button_description">Previous</string>
+
+ <!-- Content description for next button in find in file menu -->
+ <string name="next_button_description">Next</string>
+
+ <!-- Content description for close button in find in file menu -->
+ <string name="close_button_description">Close</string>
+
<!-- Message for no matches found when searching for query text inside the file. [CHAR LIMIT=40] -->
<string name="message_no_matches_found">No matches found.</string>
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 265b837..3642f7a 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -485,7 +485,7 @@
}
public final class PickerGroupItem {
- ctor public PickerGroupItem(androidx.wear.compose.material3.PickerState pickerState, optional androidx.compose.ui.Modifier modifier, optional String? contentDescription, optional androidx.compose.ui.focus.FocusRequester? focusRequester, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> option);
+ ctor public PickerGroupItem(androidx.wear.compose.material3.PickerState pickerState, optional androidx.compose.ui.Modifier modifier, optional String? contentDescription, optional androidx.compose.ui.focus.FocusRequester? focusRequester, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional float spacing, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> option);
method public String? getContentDescription();
method public androidx.compose.ui.focus.FocusRequester? getFocusRequester();
method public androidx.compose.ui.Modifier getModifier();
@@ -493,6 +493,7 @@
method public kotlin.jvm.functions.Function3<androidx.wear.compose.material3.PickerScope,java.lang.Integer,java.lang.Boolean,kotlin.Unit> getOption();
method public androidx.wear.compose.material3.PickerState getPickerState();
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? getReadOnlyLabel();
+ method public float getSpacing();
property public final String? contentDescription;
property public final androidx.compose.ui.focus.FocusRequester? focusRequester;
property public final androidx.compose.ui.Modifier modifier;
@@ -500,6 +501,7 @@
property public final kotlin.jvm.functions.Function3<androidx.wear.compose.material3.PickerScope,java.lang.Integer,java.lang.Boolean,kotlin.Unit> option;
property public final androidx.wear.compose.material3.PickerState pickerState;
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel;
+ property public final float spacing;
}
public final class PickerGroupKt {
@@ -522,7 +524,7 @@
}
public final class PickerKt {
- method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material3.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float separation, optional @FloatRange(from=0.0, to=0.5) float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
+ method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material3.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float spacing, optional @FloatRange(from=0.0, to=0.5) float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
method @androidx.compose.runtime.Composable public static androidx.wear.compose.material3.PickerState rememberPickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
}
@@ -1070,15 +1072,58 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
+ @androidx.compose.runtime.Immutable public final class TimePickerColors {
+ ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
+ method public long getConfirmButtonContainerColor();
+ method public long getConfirmButtonContentColor();
+ method public long getPickerLabelColor();
+ method public long getSelectedPickerContentColor();
+ method public long getSeparatorColor();
+ method public long getUnselectedPickerContentColor();
+ property public final long confirmButtonContainerColor;
+ property public final long confirmButtonContentColor;
+ property public final long pickerLabelColor;
+ property public final long selectedPickerContentColor;
+ property public final long separatorColor;
+ property public final long unselectedPickerContentColor;
+ }
+
+ public final class TimePickerDefaults {
+ method @androidx.compose.runtime.Composable public int getTimePickerType();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimePickerColors timePickerColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimePickerColors timePickerColors(optional long selectedPickerContentColor, optional long unselectedPickerContentColor, optional long separatorColor, optional long pickerLabelColor, optional long confirmButtonContentColor, optional long confirmButtonContainerColor);
+ property @androidx.compose.runtime.Composable public final int timePickerType;
+ field public static final androidx.wear.compose.material3.TimePickerDefaults INSTANCE;
+ }
+
+ public final class TimePickerKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.compose.runtime.Composable public static void TimePicker(java.time.LocalTime initialTime, kotlin.jvm.functions.Function1<? super java.time.LocalTime,kotlin.Unit> onTimePicked, optional androidx.compose.ui.Modifier modifier, optional int timePickerType, optional androidx.wear.compose.material3.TimePickerColors colors);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TimePickerType {
+ field public static final androidx.wear.compose.material3.TimePickerType.Companion Companion;
+ }
+
+ public static final class TimePickerType.Companion {
+ method public int getHoursMinutes24H();
+ method public int getHoursMinutesAmPm12H();
+ method public int getHoursMinutesSeconds24H();
+ property public final int HoursMinutes24H;
+ property public final int HoursMinutesAmPm12H;
+ property public final int HoursMinutesSeconds24H;
+ }
+
public interface TimeSource {
method @androidx.compose.runtime.Composable public String currentTime();
}
public final class TimeTextDefaults {
+ method public float getAutoTextWeight();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
method @androidx.compose.runtime.Composable public String timeFormat();
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
+ property public final float AutoTextWeight;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material3.TimeTextDefaults INSTANCE;
field public static final float MaxSweepAngle = 70.0f;
@@ -1093,7 +1138,7 @@
public abstract sealed class TimeTextScope {
method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
- method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style);
+ method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
method public abstract void time();
}
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 265b837..3642f7a 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -485,7 +485,7 @@
}
public final class PickerGroupItem {
- ctor public PickerGroupItem(androidx.wear.compose.material3.PickerState pickerState, optional androidx.compose.ui.Modifier modifier, optional String? contentDescription, optional androidx.compose.ui.focus.FocusRequester? focusRequester, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> option);
+ ctor public PickerGroupItem(androidx.wear.compose.material3.PickerState pickerState, optional androidx.compose.ui.Modifier modifier, optional String? contentDescription, optional androidx.compose.ui.focus.FocusRequester? focusRequester, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional float spacing, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> option);
method public String? getContentDescription();
method public androidx.compose.ui.focus.FocusRequester? getFocusRequester();
method public androidx.compose.ui.Modifier getModifier();
@@ -493,6 +493,7 @@
method public kotlin.jvm.functions.Function3<androidx.wear.compose.material3.PickerScope,java.lang.Integer,java.lang.Boolean,kotlin.Unit> getOption();
method public androidx.wear.compose.material3.PickerState getPickerState();
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? getReadOnlyLabel();
+ method public float getSpacing();
property public final String? contentDescription;
property public final androidx.compose.ui.focus.FocusRequester? focusRequester;
property public final androidx.compose.ui.Modifier modifier;
@@ -500,6 +501,7 @@
property public final kotlin.jvm.functions.Function3<androidx.wear.compose.material3.PickerScope,java.lang.Integer,java.lang.Boolean,kotlin.Unit> option;
property public final androidx.wear.compose.material3.PickerState pickerState;
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel;
+ property public final float spacing;
}
public final class PickerGroupKt {
@@ -522,7 +524,7 @@
}
public final class PickerKt {
- method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material3.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float separation, optional @FloatRange(from=0.0, to=0.5) float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
+ method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material3.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float spacing, optional @FloatRange(from=0.0, to=0.5) float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material3.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
method @androidx.compose.runtime.Composable public static androidx.wear.compose.material3.PickerState rememberPickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
}
@@ -1070,15 +1072,58 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
+ @androidx.compose.runtime.Immutable public final class TimePickerColors {
+ ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
+ method public long getConfirmButtonContainerColor();
+ method public long getConfirmButtonContentColor();
+ method public long getPickerLabelColor();
+ method public long getSelectedPickerContentColor();
+ method public long getSeparatorColor();
+ method public long getUnselectedPickerContentColor();
+ property public final long confirmButtonContainerColor;
+ property public final long confirmButtonContentColor;
+ property public final long pickerLabelColor;
+ property public final long selectedPickerContentColor;
+ property public final long separatorColor;
+ property public final long unselectedPickerContentColor;
+ }
+
+ public final class TimePickerDefaults {
+ method @androidx.compose.runtime.Composable public int getTimePickerType();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimePickerColors timePickerColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimePickerColors timePickerColors(optional long selectedPickerContentColor, optional long unselectedPickerContentColor, optional long separatorColor, optional long pickerLabelColor, optional long confirmButtonContentColor, optional long confirmButtonContainerColor);
+ property @androidx.compose.runtime.Composable public final int timePickerType;
+ field public static final androidx.wear.compose.material3.TimePickerDefaults INSTANCE;
+ }
+
+ public final class TimePickerKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.compose.runtime.Composable public static void TimePicker(java.time.LocalTime initialTime, kotlin.jvm.functions.Function1<? super java.time.LocalTime,kotlin.Unit> onTimePicked, optional androidx.compose.ui.Modifier modifier, optional int timePickerType, optional androidx.wear.compose.material3.TimePickerColors colors);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TimePickerType {
+ field public static final androidx.wear.compose.material3.TimePickerType.Companion Companion;
+ }
+
+ public static final class TimePickerType.Companion {
+ method public int getHoursMinutes24H();
+ method public int getHoursMinutesAmPm12H();
+ method public int getHoursMinutesSeconds24H();
+ property public final int HoursMinutes24H;
+ property public final int HoursMinutesAmPm12H;
+ property public final int HoursMinutesSeconds24H;
+ }
+
public interface TimeSource {
method @androidx.compose.runtime.Composable public String currentTime();
}
public final class TimeTextDefaults {
+ method public float getAutoTextWeight();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TimeSource rememberTimeSource(String timeFormat);
method @androidx.compose.runtime.Composable public String timeFormat();
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
+ property public final float AutoTextWeight;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material3.TimeTextDefaults INSTANCE;
field public static final float MaxSweepAngle = 70.0f;
@@ -1093,7 +1138,7 @@
public abstract sealed class TimeTextScope {
method public abstract void composable(kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public abstract void separator(optional androidx.compose.ui.text.TextStyle? style);
- method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style);
+ method public abstract void text(String text, optional androidx.compose.ui.text.TextStyle? style, optional float weight);
method public abstract void time();
}
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
index 4068a7f..bc201f1 100644
--- a/wear/compose/compose-material3/build.gradle
+++ b/wear/compose/compose-material3/build.gradle
@@ -52,6 +52,7 @@
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(libs.testParameterInjector)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":wear:compose:compose-material3-samples"))
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
index 83f1564..7149870 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
@@ -16,27 +16,88 @@
package androidx.wear.compose.material3.demos
+import android.os.Build
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.unit.dp
import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.Picker
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
import androidx.wear.compose.material3.rememberPickerState
import androidx.wear.compose.material3.samples.AutoCenteringPickerGroup
import androidx.wear.compose.material3.samples.PickerAnimateScrollToOption
import androidx.wear.compose.material3.samples.PickerGroupSample
import androidx.wear.compose.material3.samples.SimplePicker
+import androidx.wear.compose.material3.samples.TimePickerSample
+import androidx.wear.compose.material3.samples.TimePickerWith12HourClockSample
+import androidx.wear.compose.material3.samples.TimePickerWithSecondsSample
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
val PickerDemos =
listOf(
+ // Requires API level 26 or higher due to java.time dependency.
+ *(if (Build.VERSION.SDK_INT >= 26)
+ arrayOf(
+ ComposableDemo("Time HH:MM:SS") { TimePickerWithSecondsSample() },
+ ComposableDemo("Time HH:MM") {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ val formatter = DateTimeFormatter.ofPattern("HH:mm")
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutes24H,
+ // Initialize with last picked time on reopen
+ initialTime = timePickerTime
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Selected Time")
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text(timePickerTime.format(formatter)) },
+ icon = {
+ Icon(
+ imageVector = Icons.Filled.Edit,
+ contentDescription = "Edit"
+ )
+ },
+ )
+ }
+ }
+ },
+ ComposableDemo("Time 12 Hour") { TimePickerWith12HourClockSample() },
+ ComposableDemo("Time System time format") { TimePickerSample() },
+ )
+ else emptyArray<ComposableDemo>()),
ComposableDemo("Simple Picker") { SimplePicker() },
ComposableDemo("No gradient") { PickerWithoutGradient() },
ComposableDemo("Animate picker change") { PickerAnimateScrollToOption() },
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PickerSample.kt
index 771e384..adc77cc 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PickerSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PickerSample.kt
@@ -64,7 +64,7 @@
val contentDescription by remember { derivedStateOf { "${state.selectedOption + 1}" } }
Picker(
state = state,
- separation = 4.dp,
+ spacing = 4.dp,
contentDescription = contentDescription,
) {
Button(
@@ -84,7 +84,7 @@
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Picker(
state = state,
- separation = 4.dp,
+ spacing = 4.dp,
contentDescription = contentDescription,
) {
Button(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt
new file mode 100644
index 0000000..78cc200
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 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.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+@Sampled
+@Composable
+fun TimePickerSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ val formatter =
+ DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
+ .withLocale(LocalConfiguration.current.locales[0])
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ initialTime = timePickerTime // Initialize with last picked time on reopen
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Selected Time")
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TimePickerWithSecondsSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ val formatter = DateTimeFormatter.ofPattern("HH:mm:ss")
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutesSeconds24H,
+ initialTime = timePickerTime // Initialize with last picked time on reopen
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Selected Time")
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TimePickerWith12HourClockSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ val formatter = DateTimeFormatter.ofPattern("hh:mm a")
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutesAmPm12H,
+ initialTime = timePickerTime // Initialize with last picked time on reopen
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Selected Time")
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PickerTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PickerTest.kt
index d579f24..6a66a954 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PickerTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PickerTest.kt
@@ -212,7 +212,7 @@
modifier =
Modifier.testTag(TEST_TAG)
.requiredSize(itemSizeDp * 11 + separationDp * 10 * separationSign),
- separation = separationDp * separationSign
+ spacing = separationDp * separationSign
) {
Box(Modifier.requiredSize(itemSizeDp))
}
@@ -759,7 +759,7 @@
Modifier.testTag(TEST_TAG)
.requiredSize(pickerHeightDp)
.onGloballyPositioned { pickerLayoutCoordinates = it },
- separation = separationDp * separationSign,
+ spacing = separationDp * separationSign,
readOnly = readOnly.value,
contentDescription = CONTENT_DESCRIPTION,
) { optionIndex ->
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt
new file mode 100644
index 0000000..ed1aaf9
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 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.material3
+
+import android.content.res.Configuration
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import java.time.LocalTime
+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 TimePickerScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun timePicker24h_withoutSeconds() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutes24H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ )
+ }
+ )
+
+ @Test
+ fun timePicker24h_withSeconds() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutesSeconds24H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 37)
+ )
+ }
+ )
+
+ @Test
+ fun timePicker12h() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutesAmPm12H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ )
+ }
+ )
+
+ @Test
+ fun timePicker24h_withoutSeconds_largeScreen() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutes24H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ )
+ }
+ )
+
+ @Test
+ fun timePicker24h_withSeconds_largeScreen() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutesSeconds24H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 37)
+ )
+ }
+ )
+
+ @Test
+ fun timePicker12h_largeScreen() =
+ rule.verifyTimePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutesAmPm12H,
+ initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ )
+ }
+ )
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun ComposeContentTestRule.verifyTimePickerScreenshot(
+ methodName: String,
+ screenshotRule: AndroidXScreenshotTestRule,
+ testTag: String = TEST_TAG,
+ isLargeScreen: Boolean = false,
+ content: @Composable () -> Unit
+ ) {
+ val screenSizeDp = if (isLargeScreen) SCREENSHOT_SIZE_LARGE else SCREENSHOT_SIZE
+ setContentWithTheme {
+ val originalConfiguration = LocalConfiguration.current
+ val fixedScreenSizeConfiguration =
+ remember(originalConfiguration) {
+ Configuration(originalConfiguration).apply {
+ screenWidthDp = screenSizeDp
+ screenHeightDp = screenSizeDp
+ }
+ }
+ CompositionLocalProvider(LocalConfiguration provides fixedScreenSizeConfiguration) {
+ Box(
+ modifier =
+ Modifier.size(screenSizeDp.dp)
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ content()
+ }
+ }
+ }
+
+ onNodeWithTag(testTag).captureToImage().assertAgainstGolden(screenshotRule, methodName)
+ }
+}
+
+private const val SCREENSHOT_SIZE = 192
+private const val SCREENSHOT_SIZE_LARGE = 228
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerTest.kt
new file mode 100644
index 0000000..9f347f0
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerTest.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2024 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.material3
+
+import android.content.res.Resources
+import android.os.Build
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollToIndex
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.wear.compose.material3.internal.Plurals
+import androidx.wear.compose.material3.internal.Strings
+import androidx.wear.compose.material3.samples.TimePickerSample
+import androidx.wear.compose.material3.samples.TimePickerWith12HourClockSample
+import androidx.wear.compose.material3.samples.TimePickerWithSecondsSample
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import java.time.temporal.ChronoField
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TimePickerTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun timepicker_supports_testtag() {
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ initialTime = LocalTime.now()
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun timepicker12h_supports_testtag() {
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ timePickerType = TimePickerType.HoursMinutesAmPm12H,
+ initialTime = LocalTime.now()
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun timepicker_samples_build() {
+ rule.setContentWithTheme {
+ TimePickerSample()
+ TimePickerWithSecondsSample()
+ TimePickerWith12HourClockSample()
+ }
+ }
+
+ @Test
+ fun timepicker_hhmmss_initial_state() {
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 31)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutesSeconds24H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.hour,
+ selectionMode = SelectionMode.Hour
+ )
+ .assertIsDisplayed()
+ .assertIsFocused()
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .assertIsDisplayed()
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.second,
+ selectionMode = SelectionMode.Second
+ )
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun timepicker_hhmm_initial_state() {
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutes24H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.hour,
+ selectionMode = SelectionMode.Hour
+ )
+ .assertIsDisplayed()
+ .assertIsFocused()
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun timepicker_hhmm12h_initial_state() {
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutesAmPm12H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.get(ChronoField.CLOCK_HOUR_OF_AMPM),
+ selectionMode = SelectionMode.Hour
+ )
+ .assertIsDisplayed()
+ .assertIsFocused()
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .assertIsDisplayed()
+ rule.onNodeWithText("AM", useUnmergedTree = true).assertIsDisplayed()
+ rule.onNodeWithText("PM", useUnmergedTree = true).assertIsDisplayed()
+ }
+
+ @Test
+ fun timePicker_switch_to_minutes() {
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 31)
+ rule.setContentWithTheme { TimePicker(onTimePicked = {}, initialTime = initialTime) }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .performClick()
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .assertIsFocused()
+ }
+
+ @Test
+ fun timePicker_select_hour() {
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 31)
+ val expectedHour = 9
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = {},
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutesSeconds24H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.hour,
+ selectionMode = SelectionMode.Hour
+ )
+ .performScrollToIndex(expectedHour)
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTimeValue(selectedValue = expectedHour, selectionMode = SelectionMode.Hour)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun timePicker_hhmmss_confirmed() {
+ lateinit var confirmedTime: LocalTime
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23, /* second= */ 31)
+ val expectedTime = LocalTime.of(/* hour= */ 9, /* minute= */ 11, /* second= */ 59)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = { confirmedTime = it },
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutesSeconds24H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.hour,
+ selectionMode = SelectionMode.Hour
+ )
+ .performScrollToIndex(expectedTime.hour)
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .performScrollToIndex(expectedTime.minute)
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.second,
+ selectionMode = SelectionMode.Second
+ )
+ .performScrollToIndex(expectedTime.second)
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(confirmedTime).isEqualTo(expectedTime)
+ }
+
+ @Test
+ fun timePicker_hhmm_confirmed() {
+ lateinit var confirmedTime: LocalTime
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ val expectedTime = LocalTime.of(/* hour= */ 9, /* minute= */ 11)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = { confirmedTime = it },
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutes24H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.hour,
+ selectionMode = SelectionMode.Hour
+ )
+ .performScrollToIndex(expectedTime.hour)
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .performScrollToIndex(expectedTime.minute)
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(confirmedTime).isEqualTo(expectedTime)
+ }
+
+ @Test
+ fun timePicker_12h_confirmed() {
+ lateinit var confirmedTime: LocalTime
+ val initialTime = LocalTime.of(/* hour= */ 14, /* minute= */ 23)
+ val expectedTime = LocalTime.of(/* hour= */ 9, /* minute= */ 11)
+ rule.setContentWithTheme {
+ TimePicker(
+ onTimePicked = { confirmedTime = it },
+ initialTime = initialTime,
+ timePickerType = TimePickerType.HoursMinutesAmPm12H
+ )
+ }
+
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.get(ChronoField.CLOCK_HOUR_OF_AMPM),
+ selectionMode = SelectionMode.Hour
+ )
+ .performScrollToIndex(expectedTime.get(ChronoField.CLOCK_HOUR_OF_AMPM) - 1)
+ rule
+ .onNodeWithTimeValue(
+ selectedValue = initialTime.minute,
+ selectionMode = SelectionMode.Minute
+ )
+ .performScrollToIndex(expectedTime.minute)
+ rule.onNodeWithContentDescription("PM").performScrollToIndex(0)
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(confirmedTime).isEqualTo(expectedTime)
+ }
+
+ private fun SemanticsNodeInteractionsProvider.onNodeWithContentDescription(
+ label: String
+ ): SemanticsNodeInteraction = onAllNodesWithContentDescription(label).onFirst()
+
+ private fun SemanticsNodeInteractionsProvider.confirmButton(): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .resources
+ .getString(Strings.PickerConfirmButtonContentDescription.value)
+ )
+ .onFirst()
+
+ private fun SemanticsNodeInteractionsProvider.onNodeWithTimeValue(
+ selectedValue: Int,
+ selectionMode: SelectionMode,
+ ): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ contentDescriptionForValue(
+ InstrumentationRegistry.getInstrumentation().context.resources,
+ selectedValue,
+ selectionMode.contentDescriptionResource
+ )
+ )
+ .onFirst()
+
+ private fun contentDescriptionForValue(
+ resources: Resources,
+ selectedValue: Int,
+ contentDescriptionResource: Plurals,
+ ): String =
+ resources.getQuantityString(contentDescriptionResource.value, selectedValue, selectedValue)
+
+ private enum class SelectionMode(val contentDescriptionResource: Plurals) {
+ Hour(Plurals.TimePickerHoursContentDescription),
+ Minute(Plurals.TimePickerMinutesContentDescription),
+ Second(Plurals.TimePickerSecondsContentDescription),
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
index 9c9d5cb..352df46 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
@@ -33,17 +33,18 @@
import androidx.compose.ui.test.then
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
@MediumTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(TestParameterInjector::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class TimeTextScreenshotTest {
@get:Rule val rule = createComposeRule()
@@ -217,6 +218,28 @@
}
@Test
+ fun time_text_with_very_long_text_non_round_device() =
+ verifyScreenshot(false) {
+ val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
+ val timeTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Cyan)
+ val separatorStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ TimeText(
+ contentColor = Color.Green,
+ timeTextStyle = timeTextStyle,
+ modifier = Modifier.testTag(TEST_TAG),
+ timeSource = MockTimeSource,
+ ) {
+ text(
+ "Very long text to ensure we are not taking more than one line and " +
+ "leaving room for the time",
+ customStyle
+ )
+ separator(separatorStyle)
+ time()
+ }
+ }
+
+ @Test
fun time_text_with_very_long_text_smaller_angle_on_round_device() =
verifyScreenshot(true) {
val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Red)
@@ -238,9 +261,48 @@
}
}
+ @Test
+ fun time_text_long_text_before_time(@TestParameter shape: ScreenShape) =
+ TimeTextWithDefaults(shape.isRound) {
+ text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
+ separator()
+ time()
+ separator()
+ text("More")
+ }
+
+ @Test
+ fun time_text_long_text_after_time(@TestParameter shape: ScreenShape) =
+ TimeTextWithDefaults(shape.isRound) {
+ text("More")
+ separator()
+ time()
+ separator()
+ text("Very long text to ensure we are respecting the weight parameter", weight = 1f)
+ }
+
+ // This is to get better names, so it says 'round_device' instead of 'true'
+ enum class ScreenShape(val isRound: Boolean) {
+ ROUND_DEVICE(true),
+ SQUARE_DEVICE(false)
+ }
+
+ private fun TimeTextWithDefaults(isDeviceRound: Boolean, content: TimeTextScope.() -> Unit) =
+ verifyScreenshot(isDeviceRound) {
+ TimeText(
+ contentColor = Color.Green,
+ maxSweepAngle = 180f,
+ modifier = Modifier.testTag(TEST_TAG),
+ timeSource = MockTimeSource,
+ content = content
+ )
+ }
+
private fun verifyScreenshot(isDeviceRound: Boolean = true, content: @Composable () -> Unit) {
rule.verifyScreenshot(
- methodName = testName.methodName,
+ // Valid characters for golden identifiers are [A-Za-z0-9_-]
+ // TestParameterInjector adds '[' + parameter_values + ']' to the test name.
+ methodName = testName.methodName.replace("[", "_").replace("]", ""),
screenshotRule = screenshotRule,
content = {
val screenSize = LocalContext.current.resources.configuration.smallestScreenWidthDp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
index 70f79b2..84634d8 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
@@ -251,6 +251,9 @@
// Progress Indicator
internal var defaultProgressIndicatorColorsCached: ProgressIndicatorColors? = null
+
+ // Picker
+ internal var defaultTimePickerColorsCached: TimePickerColors? = null
}
/**
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
index 05a9703..e5c965c 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
@@ -109,8 +109,8 @@
* semantics, which facilitates implementation of multi-picker screens.
* @param scalingParams The parameters to configure the scaling and transparency effects for the
* component. See [ScalingParams].
- * @param separation The amount of separation in [Dp] between items. Can be negative, which can be
- * useful for Text if it has plenty of whitespace.
+ * @param spacing The amount of spacing in [Dp] between items. Can be negative, which can be useful
+ * for Text if it has plenty of whitespace.
* @param gradientRatio The size relative to the Picker height that the top and bottom gradients
* take. These gradients blur the picker content on the top and bottom. The default is 0.33, so
* the top 1/3 and the bottom 1/3 of the picker are taken by gradients. Should be between 0.0 and
@@ -142,7 +142,7 @@
readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
onSelected: () -> Unit = {},
scalingParams: ScalingParams = PickerDefaults.scalingParams(),
- separation: Dp = 0.dp,
+ spacing: Dp = 0.dp,
@FloatRange(from = 0.0, to = 0.5) gradientRatio: Float = PickerDefaults.GradientRatio,
gradientColor: Color = MaterialTheme.colorScheme.background,
flingBehavior: FlingBehavior = PickerDefaults.flingBehavior(state),
@@ -199,7 +199,7 @@
val shimHeight =
(size.height -
centerItem.unadjustedSize.toFloat() -
- separation.toPx()) / 2.0f
+ spacing.toPx()) / 2.0f
drawShim(gradientColor, shimHeight)
}
}
@@ -229,7 +229,7 @@
contentPadding = PaddingValues(0.dp),
scalingParams = scalingParams,
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(space = separation),
+ verticalArrangement = Arrangement.spacedBy(space = spacing),
flingBehavior = flingBehavior,
autoCentering = AutoCenteringParams(itemIndex = 0),
userScrollEnabled = userScrollEnabled
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
index bed5dae..855847c 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
@@ -39,6 +39,8 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxOfOrNull
@@ -141,6 +143,7 @@
readOnlyLabel = pickerData.readOnlyLabel,
flingBehavior = flingBehavior,
onSelected = pickerData.onSelected,
+ spacing = pickerData.spacing,
userScrollEnabled = !touchExplorationServicesEnabled || pickerSelected,
option = { optionIndex ->
with(pickerData) {
@@ -220,6 +223,8 @@
* @param focusRequester Optional [FocusRequester] for the [Picker]. If not provided, a local
* instance of [FocusRequester] will be created to handle the focus between different pickers.
* @param onSelected Action triggered when the [Picker] is selected by clicking.
+ * @param spacing The amount of spacing in [Dp] between items. Can be negative, which can be useful
+ * for Text if it has plenty of whitespace.
* @param readOnlyLabel A slot for providing a label, displayed above the selected option when the
* [Picker] is read-only. The label is overlaid with the currently selected option within a Box,
* so it is recommended that the label is given [Alignment.TopCenter].
@@ -233,6 +238,7 @@
val focusRequester: FocusRequester? = null,
val onSelected: () -> Unit = {},
val readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
+ val spacing: Dp = 0.dp,
val option: @Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
new file mode 100644
index 0000000..df4bb34
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
@@ -0,0 +1,762 @@
+/*
+ * Copyright 2024 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.material3
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.collection.IntObjectMap
+import androidx.collection.MutableIntObjectMap
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.focused
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
+import androidx.wear.compose.material3.ButtonDefaults.buttonColors
+import androidx.wear.compose.material3.internal.Plurals
+import androidx.wear.compose.material3.internal.Strings
+import androidx.wear.compose.material3.internal.getPlurals
+import androidx.wear.compose.material3.internal.getString
+import androidx.wear.compose.material3.tokens.TimePickerTokens
+import androidx.wear.compose.materialcore.is24HourFormat
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
+
+/**
+ * A full screen TimePicker with configurable columns that allows users to select a time.
+ *
+ * This component is designed to take most/all of the screen and utilizes large fonts.
+ *
+ * Example of a [TimePicker]:
+ *
+ * @sample androidx.wear.compose.material3.samples.TimePickerSample
+ *
+ * Example of a [TimePicker] with seconds:
+ *
+ * @sample androidx.wear.compose.material3.samples.TimePickerWithSecondsSample
+ *
+ * Example of a 12 hour clock [TimePicker]:
+ *
+ * @sample androidx.wear.compose.material3.samples.TimePickerWith12HourClockSample
+ * @param initialTime The initial time to be displayed in the TimePicker. The default value is the
+ * current time.
+ * @param onTimePicked The callback that is called when the user confirms the time selection. It
+ * provides the selected time as [LocalTime].
+ * @param modifier Modifier to be applied to the `Box` containing the UI elements.
+ * @param timePickerType The different [TimePickerType] supported by this time picker. It indicates
+ * whether to show seconds or AM/PM selector as well as hours and minutes.
+ * @param colors [TimePickerColors] be applied to the TimePicker.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun TimePicker(
+ initialTime: LocalTime,
+ onTimePicked: (LocalTime) -> Unit,
+ modifier: Modifier = Modifier,
+ timePickerType: TimePickerType = TimePickerDefaults.timePickerType,
+ colors: TimePickerColors = TimePickerDefaults.timePickerColors(),
+) {
+ val inspectionMode = LocalInspectionMode.current
+ val fullyDrawn = remember { Animatable(if (inspectionMode) 1f else 0f) }
+
+ val touchExplorationStateProvider = remember { DefaultTouchExplorationStateProvider() }
+ val touchExplorationServicesEnabled by touchExplorationStateProvider.touchExplorationState()
+ // When the time picker loads, none of the individual pickers are selected in talkback mode,
+ // otherwise hours picker should be focused.
+ val pickerGroupState =
+ if (touchExplorationServicesEnabled) {
+ rememberPickerGroupState(FocusableElementsTimePicker.NONE.index)
+ } else {
+ rememberPickerGroupState(FocusableElementsTimePicker.HOURS.index)
+ }
+ val focusRequesterConfirmButton = remember { FocusRequester() }
+
+ val hourString = getString(Strings.TimePickerHour)
+ val minuteString = getString(Strings.TimePickerMinute)
+
+ val is12hour = timePickerType == TimePickerType.HoursMinutesAmPm12H
+ val hourState =
+ if (is12hour) {
+ rememberPickerState(
+ initialNumberOfOptions = 12,
+ initiallySelectedOption = initialTime[ChronoField.CLOCK_HOUR_OF_AMPM] - 1,
+ )
+ } else {
+ rememberPickerState(
+ initialNumberOfOptions = 24,
+ initiallySelectedOption = initialTime.hour,
+ )
+ }
+ val minuteState =
+ rememberPickerState(
+ initialNumberOfOptions = 60,
+ initiallySelectedOption = initialTime.minute,
+ )
+
+ val hoursContentDescription =
+ createDescription(
+ pickerGroupState,
+ if (is12hour) hourState.selectedOption + 1 else hourState.selectedOption,
+ hourString,
+ Plurals.TimePickerHoursContentDescription,
+ )
+ val minutesContentDescription =
+ createDescription(
+ pickerGroupState,
+ minuteState.selectedOption,
+ minuteString,
+ Plurals.TimePickerMinutesContentDescription,
+ )
+
+ val thirdPicker = getOptionalThirdPicker(timePickerType, pickerGroupState, initialTime)
+
+ val onPickerSelected =
+ { current: FocusableElementsTimePicker, next: FocusableElementsTimePicker ->
+ if (pickerGroupState.selectedIndex != current.index) {
+ pickerGroupState.selectedIndex = current.index
+ } else {
+ pickerGroupState.selectedIndex = next.index
+ if (next == FocusableElementsTimePicker.CONFIRM_BUTTON) {
+ focusRequesterConfirmButton.requestFocus()
+ }
+ }
+ }
+
+ Box(modifier = modifier.fillMaxSize().alpha(fullyDrawn.value)) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(Modifier.height(14.dp))
+ val focusedPicker = FocusableElementsTimePicker[pickerGroupState.selectedIndex]
+ FontScaleIndependent {
+ val styles = getTimePickerStyles(timePickerType, thirdPicker)
+ Text(
+ text =
+ when {
+ focusedPicker == FocusableElementsTimePicker.HOURS -> hourString
+ focusedPicker == FocusableElementsTimePicker.MINUTES -> minuteString
+ focusedPicker == FocusableElementsTimePicker.SECONDS_OR_PERIOD &&
+ thirdPicker != null -> thirdPicker.label
+ else -> ""
+ },
+ color = colors.pickerLabelColor,
+ style = styles.labelTextStyle,
+ maxLines = 1,
+ modifier =
+ Modifier.height(24.dp)
+ .fillMaxWidth(0.76f)
+ .align(Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center
+ )
+ Spacer(Modifier.height(styles.sectionVerticalPadding))
+ Row(
+ modifier = Modifier.fillMaxWidth().weight(1f),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ val pickerGroupItems =
+ mutableListOf(
+ PickerGroupItem(
+ pickerState = hourState,
+ modifier = Modifier.width(styles.optionWidth).fillMaxHeight(),
+ onSelected = {
+ onPickerSelected(
+ FocusableElementsTimePicker.HOURS,
+ FocusableElementsTimePicker.MINUTES,
+ )
+ },
+ contentDescription = hoursContentDescription,
+ option =
+ pickerTextOption(
+ textStyle = styles.optionTextStyle,
+ selectedPickerColor = colors.selectedPickerContentColor,
+ unselectedPickerColor = colors.unselectedPickerContentColor,
+ indexToText = {
+ "%02d".format(if (is12hour) it + 1 else it)
+ },
+ optionHeight = styles.optionHeight,
+ ),
+ spacing = styles.optionSpacing
+ ),
+ PickerGroupItem(
+ pickerState = minuteState,
+ modifier = Modifier.width(styles.optionWidth).fillMaxHeight(),
+ onSelected = {
+ onPickerSelected(
+ FocusableElementsTimePicker.MINUTES,
+ if (timePickerType == TimePickerType.HoursMinutes24H) {
+ FocusableElementsTimePicker.CONFIRM_BUTTON
+ } else {
+ FocusableElementsTimePicker.SECONDS_OR_PERIOD
+ }
+ )
+ },
+ contentDescription = minutesContentDescription,
+ option =
+ pickerTextOption(
+ textStyle = styles.optionTextStyle,
+ indexToText = { "%02d".format(it) },
+ selectedPickerColor = colors.selectedPickerContentColor,
+ unselectedPickerColor = colors.unselectedPickerContentColor,
+ optionHeight = styles.optionHeight,
+ ),
+ spacing = styles.optionSpacing
+ ),
+ )
+
+ if (thirdPicker != null) {
+ pickerGroupItems.add(
+ PickerGroupItem(
+ pickerState = thirdPicker.state,
+ modifier = Modifier.width(styles.optionWidth).fillMaxHeight(),
+ onSelected = {
+ onPickerSelected(
+ FocusableElementsTimePicker.SECONDS_OR_PERIOD,
+ FocusableElementsTimePicker.CONFIRM_BUTTON,
+ )
+ },
+ contentDescription = thirdPicker.contentDescription,
+ option =
+ pickerTextOption(
+ textStyle = styles.optionTextStyle,
+ indexToText = thirdPicker.indexToText,
+ selectedPickerColor = colors.selectedPickerContentColor,
+ unselectedPickerColor = colors.unselectedPickerContentColor,
+ optionHeight = styles.optionHeight,
+ ),
+ spacing = styles.optionSpacing
+ ),
+ )
+ }
+ PickerGroup(
+ *pickerGroupItems.toTypedArray(),
+ modifier = Modifier.fillMaxWidth(),
+ pickerGroupState = pickerGroupState,
+ separator = {
+ Separator(
+ textStyle = styles.optionTextStyle,
+ color = colors.separatorColor,
+ separatorPadding = styles.separatorPadding,
+ text = if (it == 0 || !is12hour) ":" else ""
+ )
+ },
+ autoCenter = false,
+ touchExplorationStateProvider = touchExplorationStateProvider,
+ )
+ }
+ Spacer(Modifier.height(styles.sectionVerticalPadding))
+ }
+ EdgeButton(
+ onClick = {
+ val secondOrPeriodSelectedOption = thirdPicker?.state?.selectedOption ?: 0
+ val confirmedTime =
+ if (is12hour) {
+ LocalTime.of(
+ hourState.selectedOption + 1,
+ minuteState.selectedOption,
+ 0,
+ )
+ .with(
+ ChronoField.AMPM_OF_DAY,
+ secondOrPeriodSelectedOption.toLong()
+ )
+ } else {
+ LocalTime.of(
+ hourState.selectedOption,
+ minuteState.selectedOption,
+ secondOrPeriodSelectedOption,
+ )
+ }
+ onTimePicked(confirmedTime)
+ },
+ modifier =
+ Modifier.semantics {
+ focused =
+ pickerGroupState.selectedIndex ==
+ FocusableElementsTimePicker.CONFIRM_BUTTON.index
+ }
+ .focusRequester(focusRequesterConfirmButton)
+ .focusable(),
+ buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
+ colors =
+ buttonColors(
+ contentColor = colors.confirmButtonContentColor,
+ containerColor = colors.confirmButtonContainerColor
+ ),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = getString(Strings.PickerConfirmButtonContentDescription),
+ modifier = Modifier.size(24.dp).wrapContentSize(align = Alignment.Center),
+ )
+ }
+ }
+ }
+
+ if (!inspectionMode) {
+ LaunchedEffect(Unit) { fullyDrawn.animateTo(1f) }
+ }
+}
+
+/** Specifies the types of columns to display in the TimePicker. */
+@Immutable
+@JvmInline
+value class TimePickerType internal constructor(internal val value: Int) {
+ companion object {
+ /** Displays two columns for hours (24-hour format) and minutes. */
+ val HoursMinutes24H = TimePickerType(0)
+ /** Displays three columns for hours (24-hour format), minutes and seconds. */
+ val HoursMinutesSeconds24H = TimePickerType(1)
+ /** Displays three columns for hours (12-hour format), minutes and AM/PM label. */
+ val HoursMinutesAmPm12H = TimePickerType(2)
+ }
+
+ override fun toString() =
+ when (this) {
+ HoursMinutes24H -> "HoursMinutes24H"
+ HoursMinutesSeconds24H -> "HoursMinutesSeconds24H"
+ HoursMinutesAmPm12H -> "HoursMinutesAmPm12H"
+ else -> "Unknown"
+ }
+}
+
+/** Contains the default values used by [TimePicker] */
+object TimePickerDefaults {
+
+ /** The default [TimePickerType] for [TimePicker] aligns with the current system time format. */
+ val timePickerType: TimePickerType
+ @Composable
+ get() =
+ if (is24HourFormat()) {
+ TimePickerType.HoursMinutes24H
+ } else {
+ TimePickerType.HoursMinutesAmPm12H
+ }
+
+ /** Creates a [TimePickerColors] for a [TimePicker]. */
+ @Composable fun timePickerColors() = MaterialTheme.colorScheme.defaultTimePickerColors
+
+ /**
+ * Creates a [TimePickerColors] for a [TimePicker].
+ *
+ * @param selectedPickerContentColor The content color of selected picker.
+ * @param unselectedPickerContentColor The content color of unselected pickers.
+ * @param separatorColor The color of separator between the pickers.
+ * @param pickerLabelColor The color of the picker label.
+ * @param confirmButtonContentColor The content color of the confirm button.
+ * @param confirmButtonContainerColor The container color of the confirm button.
+ */
+ @Composable
+ fun timePickerColors(
+ selectedPickerContentColor: Color = Color.Unspecified,
+ unselectedPickerContentColor: Color = Color.Unspecified,
+ separatorColor: Color = Color.Unspecified,
+ pickerLabelColor: Color = Color.Unspecified,
+ confirmButtonContentColor: Color = Color.Unspecified,
+ confirmButtonContainerColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultTimePickerColors.copy(
+ selectedPickerContentColor = selectedPickerContentColor,
+ unselectedPickerContentColor = unselectedPickerContentColor,
+ separatorColor = separatorColor,
+ pickerLabelColor = pickerLabelColor,
+ confirmButtonContentColor = confirmButtonContentColor,
+ confirmButtonContainerColor = confirmButtonContainerColor,
+ )
+
+ private val ColorScheme.defaultTimePickerColors: TimePickerColors
+ get() {
+ return defaultTimePickerColorsCached
+ ?: TimePickerColors(
+ selectedPickerContentColor =
+ fromToken(TimePickerTokens.SelectedPickerContentColor),
+ unselectedPickerContentColor =
+ fromToken(TimePickerTokens.UnselectedPickerContentColor),
+ separatorColor = fromToken(TimePickerTokens.SeparatorColor),
+ pickerLabelColor = fromToken(TimePickerTokens.PickerLabelColor),
+ confirmButtonContentColor =
+ fromToken(TimePickerTokens.ConfirmButtonContentColor),
+ confirmButtonContainerColor =
+ fromToken(TimePickerTokens.ConfirmButtonContainerColor),
+ )
+ .also { defaultTimePickerColorsCached = it }
+ }
+}
+
+/**
+ * Represents the colors used by a [TimePicker].
+ *
+ * @param selectedPickerContentColor The content color of selected picker.
+ * @param unselectedPickerContentColor The content color of unselected pickers.
+ * @param separatorColor The color of separator between the pickers.
+ * @param pickerLabelColor The color of the picker label.
+ * @param confirmButtonContentColor The content color of the confirm button.
+ * @param confirmButtonContainerColor The container color of the confirm button.
+ */
+@Immutable
+class TimePickerColors
+constructor(
+ val selectedPickerContentColor: Color,
+ val unselectedPickerContentColor: Color,
+ val separatorColor: Color,
+ val pickerLabelColor: Color,
+ val confirmButtonContentColor: Color,
+ val confirmButtonContainerColor: Color,
+) {
+ internal fun copy(
+ selectedPickerContentColor: Color,
+ unselectedPickerContentColor: Color,
+ separatorColor: Color,
+ pickerLabelColor: Color,
+ confirmButtonContentColor: Color,
+ confirmButtonContainerColor: Color,
+ ) =
+ TimePickerColors(
+ selectedPickerContentColor =
+ selectedPickerContentColor.takeOrElse { this.selectedPickerContentColor },
+ unselectedPickerContentColor =
+ unselectedPickerContentColor.takeOrElse { this.unselectedPickerContentColor },
+ separatorColor = separatorColor.takeOrElse { this.separatorColor },
+ pickerLabelColor = pickerLabelColor.takeOrElse { this.pickerLabelColor },
+ confirmButtonContentColor =
+ confirmButtonContentColor.takeOrElse { this.confirmButtonContentColor },
+ confirmButtonContainerColor =
+ confirmButtonContainerColor.takeOrElse { this.confirmButtonContainerColor },
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is TimePickerColors) return false
+
+ if (selectedPickerContentColor != other.selectedPickerContentColor) return false
+ if (unselectedPickerContentColor != other.unselectedPickerContentColor) return false
+ if (separatorColor != other.separatorColor) return false
+ if (pickerLabelColor != other.pickerLabelColor) return false
+ if (confirmButtonContentColor != other.confirmButtonContentColor) return false
+ if (confirmButtonContainerColor != other.confirmButtonContainerColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = selectedPickerContentColor.hashCode()
+ result = 31 * result + unselectedPickerContentColor.hashCode()
+ result = 31 * result + separatorColor.hashCode()
+ result = 31 * result + pickerLabelColor.hashCode()
+ result = 31 * result + confirmButtonContentColor.hashCode()
+ result = 31 * result + confirmButtonContainerColor.hashCode()
+
+ return result
+ }
+}
+
+@Composable
+private fun getTimePickerStyles(
+ timePickerType: TimePickerType,
+ optionalThirdPicker: PickerData?
+): TimePickerStyles {
+ val isLargeScreen = LocalConfiguration.current.screenWidthDp > 225
+ val labelTextStyle =
+ if (isLargeScreen) {
+ TimePickerTokens.PickerLabelLargeTypography
+ } else {
+ TimePickerTokens.PickerLabelTypography
+ }
+ .value
+
+ val optionTextStyle =
+ if (isLargeScreen || timePickerType == TimePickerType.HoursMinutes24H) {
+ TimePickerTokens.PickerContentLargeTypography
+ } else {
+ TimePickerTokens.PickerContentTypography
+ }
+ .value
+ .copy(textAlign = TextAlign.Center)
+
+ val optionHeight =
+ if (isLargeScreen || timePickerType == TimePickerType.HoursMinutes24H) {
+ 40.dp
+ } else {
+ 30.dp
+ }
+ val optionSpacing = if (isLargeScreen) 6.dp else 4.dp
+ val separatorPadding =
+ when {
+ timePickerType == TimePickerType.HoursMinutes24H && isLargeScreen -> 12.dp
+ timePickerType == TimePickerType.HoursMinutes24H && !isLargeScreen -> 8.dp
+ timePickerType == TimePickerType.HoursMinutesAmPm12H && isLargeScreen -> 0.dp
+ isLargeScreen -> 6.dp
+ else -> 2.dp
+ }
+
+ val measurer = rememberTextMeasurer()
+ val density = LocalDensity.current
+ val indexToText = optionalThirdPicker?.indexToText ?: { "" }
+
+ val (twoDigitsWidth, textLabelWidth) =
+ remember(
+ density.density,
+ LocalConfiguration.current.screenWidthDp,
+ ) {
+ val mm =
+ measurer.measure(
+ "0123456789\n${indexToText(0)}\n${indexToText(1)}",
+ style = optionTextStyle,
+ density = density,
+ )
+
+ (0..9).maxOf { mm.getBoundingBox(it).width } * 2 to
+ (1..2).maxOf { mm.getLineRight(it) - mm.getLineLeft(it) }
+ }
+ val measuredOptionWidth =
+ with(LocalDensity.current) {
+ if (timePickerType == TimePickerType.HoursMinutesAmPm12H) {
+ max(twoDigitsWidth.toDp(), textLabelWidth.toDp())
+ } else {
+ twoDigitsWidth.toDp()
+ } + 1.dp // Add 1dp buffer to compensate for potential conversion loss
+ }
+
+ return TimePickerStyles(
+ labelTextStyle = labelTextStyle,
+ optionTextStyle = optionTextStyle,
+ optionWidth = max(measuredOptionWidth, minimumInteractiveComponentSize),
+ optionHeight = optionHeight,
+ optionSpacing = optionSpacing,
+ separatorPadding = separatorPadding,
+ sectionVerticalPadding = if (isLargeScreen) 6.dp else 4.dp
+ )
+}
+
+/* Returns the picker data for the third column (AM/PM or seconds) based on the time picker type. */
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+private fun getOptionalThirdPicker(
+ timePickerType: TimePickerType,
+ pickerGroupState: PickerGroupState,
+ time: LocalTime
+): PickerData? =
+ when (timePickerType) {
+ TimePickerType.HoursMinutesSeconds24H -> {
+ val secondString = getString(Strings.TimePickerSecond)
+ val secondState =
+ rememberPickerState(
+ initialNumberOfOptions = 60,
+ initiallySelectedOption = time.second,
+ )
+ val secondsContentDescription =
+ createDescription(
+ pickerGroupState,
+ secondState.selectedOption,
+ secondString,
+ Plurals.TimePickerSecondsContentDescription,
+ )
+ PickerData(
+ state = secondState,
+ contentDescription = secondsContentDescription,
+ label = secondString,
+ indexToText = { "%02d".format(it) }
+ )
+ }
+ TimePickerType.HoursMinutesAmPm12H -> {
+ val periodString = getString(Strings.TimePickerPeriod)
+ val periodState =
+ rememberPickerState(
+ initialNumberOfOptions = 2,
+ initiallySelectedOption = time[ChronoField.AMPM_OF_DAY],
+ repeatItems = false,
+ )
+ val primaryLocale = LocalConfiguration.current.locales[0]
+ val (amString, pmString) =
+ remember(primaryLocale) {
+ DateTimeFormatter.ofPattern("a", primaryLocale).let { formatter ->
+ LocalTime.of(0, 0).format(formatter) to
+ LocalTime.of(12, 0).format(formatter)
+ }
+ }
+ val periodContentDescription by
+ remember(
+ pickerGroupState.selectedIndex,
+ periodState.selectedOption,
+ ) {
+ derivedStateOf {
+ if (
+ pickerGroupState.selectedIndex == FocusableElementsTimePicker.NONE.index
+ ) {
+ periodString
+ } else if (periodState.selectedOption == 0) {
+ amString
+ } else {
+ pmString
+ }
+ }
+ }
+ PickerData(
+ state = periodState,
+ contentDescription = periodContentDescription,
+ label = "",
+ indexToText = { if (it == 0) amString else pmString }
+ )
+ }
+ else -> null
+ }
+
+private class PickerData(
+ val state: PickerState,
+ val contentDescription: String,
+ val label: String,
+ val indexToText: (Int) -> String,
+)
+
+private class TimePickerStyles(
+ val labelTextStyle: TextStyle,
+ val optionTextStyle: TextStyle,
+ val optionWidth: Dp,
+ val optionHeight: Dp,
+ val optionSpacing: Dp,
+ val separatorPadding: Dp,
+ val sectionVerticalPadding: Dp,
+)
+
+@Composable
+private fun Separator(
+ textStyle: TextStyle,
+ color: Color,
+ modifier: Modifier = Modifier,
+ separatorPadding: Dp,
+ text: String = ":",
+) {
+ Box(modifier = Modifier.padding(horizontal = separatorPadding)) {
+ Text(
+ text = text,
+ style = textStyle,
+ color = color,
+ modifier = modifier.width(12.dp).clearAndSetSemantics {},
+ )
+ }
+}
+
+private fun pickerTextOption(
+ textStyle: TextStyle,
+ selectedPickerColor: Color,
+ unselectedPickerColor: Color,
+ indexToText: (Int) -> String,
+ optionHeight: Dp,
+): (@Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit) =
+ { value: Int, pickerSelected: Boolean ->
+ Box(
+ modifier = Modifier.fillMaxSize().height(optionHeight),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = indexToText(value),
+ maxLines = 1,
+ style = textStyle,
+ color =
+ if (pickerSelected) {
+ selectedPickerColor
+ } else {
+ unselectedPickerColor
+ },
+ modifier = Modifier.align(Alignment.Center).wrapContentSize(),
+ )
+ }
+ }
+
+@Composable
+private fun createDescription(
+ pickerGroupState: PickerGroupState,
+ selectedValue: Int,
+ label: String,
+ plurals: Plurals,
+) =
+ when (pickerGroupState.selectedIndex) {
+ FocusableElementsTimePicker.NONE.index -> label
+ else -> getPlurals(plurals, selectedValue, selectedValue)
+ }
+
+@Composable
+private fun FontScaleIndependent(content: @Composable () -> Unit) {
+ CompositionLocalProvider(
+ value =
+ LocalDensity provides
+ Density(
+ density = LocalDensity.current.density,
+ fontScale = 1f,
+ ),
+ content = content
+ )
+}
+
+private enum class FocusableElementsTimePicker(val index: Int) {
+ HOURS(0),
+ MINUTES(1),
+ SECONDS_OR_PERIOD(2),
+ CONFIRM_BUTTON(3),
+ NONE(-1),
+ ;
+
+ companion object {
+ private val map: IntObjectMap<FocusableElementsTimePicker> =
+ MutableIntObjectMap<FocusableElementsTimePicker>().apply {
+ values().forEach { put(it.index, it) }
+ }
+
+ operator fun get(value: Int) = map[value]
+ }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
index 2151332..39436f5 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
@@ -125,8 +126,10 @@
.sizeIn(maxSweepDegrees = maxSweepAngle)
.padding(contentPadding.toArcPadding())
) {
- CurvedTimeTextScope(this, timeText, timeTextStyle, maxSweepAngle, contentColor)
- .content()
+ CurvedTimeTextScope(timeText, timeTextStyle, maxSweepAngle, contentColor).apply {
+ content()
+ Show()
+ }
}
}
} else {
@@ -151,8 +154,16 @@
*
* @param text The text to display.
* @param style configuration for the [text] such as color, font etc.
+ * @param weight Size the text's width proportional to its weight relative to other weighted
+ * sibling elements in the TimeText. Specify NaN to make this text not have a weight
+ * specified. The default value, [TimeTextDefaults.AutoTextWeight], makes this text have
+ * weight 1f if it's the only one, and not have weight if there are two or more.
*/
- abstract fun text(text: String, style: TextStyle? = null)
+ abstract fun text(
+ text: String,
+ style: TextStyle? = null,
+ weight: Float = TimeTextDefaults.AutoTextWeight
+ )
/** Adds a text displaying current time. */
abstract fun time()
@@ -277,6 +288,13 @@
modifier = CurvedModifier.padding(contentArcPadding)
)
}
+
+ /**
+ * Weight value used to specify that the value is automatic. It will be 1f when there is one
+ * text, and no weight will be used if there are 2 or more texts. For the 2+ texts case, usually
+ * one of them should have weight manually specified to ensure its properly cut and ellipsized.
+ */
+ val AutoTextWeight = -1f
}
interface TimeSource {
@@ -291,46 +309,63 @@
/** Implementation of [TimeTextScope] for round devices. */
internal class CurvedTimeTextScope(
- private val scope: CurvedScope,
private val timeText: String,
private val timeTextStyle: TextStyle,
private val maxSweepAngle: Float,
contentColor: Color,
) : TimeTextScope() {
-
+ private var textCount = 0
+ private val pending = mutableListOf<CurvedScope.() -> Unit>()
private val contentTextStyle = timeTextStyle.merge(contentColor)
- override fun text(text: String, style: TextStyle?) {
- scope.curvedText(
- text = text,
- overflow = TextOverflow.Ellipsis,
- maxSweepAngle = maxSweepAngle,
- style = CurvedTextStyle(style = contentTextStyle.merge(style)),
- modifier = CurvedModifier.weight(1f)
- )
+ override fun text(text: String, style: TextStyle?, weight: Float) {
+ textCount++
+ pending.add {
+ curvedText(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ maxSweepAngle = maxSweepAngle,
+ style = CurvedTextStyle(style = contentTextStyle.merge(style)),
+ modifier =
+ if (weight.isValidWeight()) CurvedModifier.weight(weight)
+ // Note that we are creating a lambda here, but textCount is actually read
+ // later, during the call to Show, when the pending list is fully constructed.
+ else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
+ CurvedModifier.weight(1f)
+ else CurvedModifier
+ )
+ }
}
override fun time() {
- scope.curvedText(
- timeText,
- maxSweepAngle = maxSweepAngle,
- style = CurvedTextStyle(timeTextStyle)
- )
+ pending.add {
+ curvedText(
+ timeText,
+ maxSweepAngle = maxSweepAngle,
+ style = CurvedTextStyle(timeTextStyle)
+ )
+ }
}
override fun separator(style: TextStyle?) {
- scope.CurvedTextSeparator(CurvedTextStyle(style = timeTextStyle.merge(style)))
+ pending.add { CurvedTextSeparator(CurvedTextStyle(style = timeTextStyle.merge(style))) }
}
override fun composable(content: @Composable () -> Unit) {
- scope.curvedComposable {
- CompositionLocalProvider(
- LocalContentColor provides contentTextStyle.color,
- LocalTextStyle provides contentTextStyle,
- content = content
- )
+ pending.add {
+ curvedComposable {
+ CompositionLocalProvider(
+ LocalContentColor provides contentTextStyle.color,
+ LocalTextStyle provides contentTextStyle,
+ content = content
+ )
+ }
}
}
+
+ fun CurvedScope.Show() {
+ pending.fastForEach { it() }
+ }
}
/** Implementation of [TimeTextScope] for non-round devices. */
@@ -339,11 +374,27 @@
private val timeTextStyle: TextStyle,
contentColor: Color,
) : TimeTextScope() {
- private val pending = mutableListOf<@Composable () -> Unit>()
+ private var textCount = 0
+ private val pending = mutableListOf<@Composable RowScope.() -> Unit>()
private val contentTextStyle = timeTextStyle.merge(contentColor)
- override fun text(text: String, style: TextStyle?) {
- pending.add { Text(text = text, style = contentTextStyle.merge(style)) }
+ override fun text(text: String, style: TextStyle?, weight: Float) {
+ textCount++
+ pending.add {
+ Text(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = contentTextStyle.merge(style),
+ modifier =
+ if (weight.isValidWeight()) Modifier.weight(weight)
+ // Note that we are creating a lambda here, but textCount is actually read
+ // later, during the call to Show, when the pending list is fully constructed.
+ else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
+ Modifier.weight(1f)
+ else Modifier
+ )
+ }
}
override fun time() {
@@ -365,11 +416,13 @@
}
@Composable
- fun Show() {
+ fun RowScope.Show() {
pending.fastForEach { it() }
}
}
+private fun Float.isValidWeight() = !isNaN() && this > 0f
+
internal class DefaultTimeSource(timeFormat: String) : TimeSource {
private val _timeFormat = timeFormat
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt
new file mode 100644
index 0000000..4edf6fd
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 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.material3.internal
+
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.wear.compose.material3.R
+
+@Composable
+@ReadOnlyComposable
+internal fun getString(string: Strings): String {
+ return stringResource(string.value)
+}
+
+@Composable
+@ReadOnlyComposable
+internal fun getString(string: Strings, vararg formatArgs: Any): String {
+ return stringResource(string.value, *formatArgs)
+}
+
+@Composable
+@ReadOnlyComposable
+internal fun getPlurals(plurals: Plurals, quantity: Int): String {
+ return pluralStringResource(plurals.value, quantity)
+}
+
+@Composable
+@ReadOnlyComposable
+internal fun getPlurals(plurals: Plurals, quantity: Int, vararg formatArgs: Any): String {
+ return pluralStringResource(plurals.value, quantity, *formatArgs)
+}
+
+@JvmInline
+@Immutable
+internal value class Strings(@StringRes val value: Int) {
+ companion object {
+ inline val TimePickerHour
+ get() = Strings(R.string.wear_m3c_time_picker_hour)
+
+ inline val TimePickerMinute
+ get() = Strings(R.string.wear_m3c_time_picker_minute)
+
+ inline val TimePickerSecond
+ get() = Strings(R.string.wear_m3c_time_picker_second)
+
+ inline val TimePickerPeriod
+ get() = Strings(R.string.wear_m3c_time_picker_period)
+
+ inline val PickerConfirmButtonContentDescription
+ get() = Strings(R.string.wear_m3c_picker_confirm_button_content_description)
+ }
+}
+
+@JvmInline
+@Immutable
+internal value class Plurals(@PluralsRes val value: Int) {
+ companion object {
+ inline val TimePickerHoursContentDescription
+ get() = Plurals(R.plurals.wear_m3c_time_picker_hours_content_description)
+
+ inline val TimePickerMinutesContentDescription
+ get() = Plurals(R.plurals.wear_m3c_time_picker_minutes_content_description)
+
+ inline val TimePickerSecondsContentDescription
+ get() = Plurals(R.plurals.wear_m3c_time_picker_seconds_content_description)
+ }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TimePickerTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TimePickerTokens.kt
new file mode 100644
index 0000000..8700fc6
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TimePickerTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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.material3.tokens
+
+internal object TimePickerTokens {
+ val SelectedPickerContentColor = ColorSchemeKeyTokens.OnBackground
+ val UnselectedPickerContentColor = ColorSchemeKeyTokens.SecondaryDim
+ val SeparatorColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val PickerLabelColor = ColorSchemeKeyTokens.Primary
+ val ConfirmButtonContentColor = ColorSchemeKeyTokens.OnPrimary
+ val ConfirmButtonContainerColor = ColorSchemeKeyTokens.PrimaryDim
+
+ val PickerLabelLargeTypography = TypographyKeyTokens.TitleLarge
+ val PickerLabelTypography = TypographyKeyTokens.TitleMedium
+ val PickerContentLargeTypography = TypographyKeyTokens.NumeralMedium
+ val PickerContentTypography = TypographyKeyTokens.NumeralSmall
+}
diff --git a/wear/compose/compose-material3/src/main/res/values/strings.xml b/wear/compose/compose-material3/src/main/res/values/strings.xml
new file mode 100644
index 0000000..35e6bd7
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 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.
+ -->
+
+<resources>
+ <string description="Lets the user know that the time unit being changed is hour. Appears on the top of the TimePicker component. [CHAR_LIMIT=8]" name="wear_m3c_time_picker_hour">Hour</string>
+ <string description="Lets the user know that the time unit being changed is minute. Appears on the top of the TimePicker component. [CHAR_LIMIT=8]" name="wear_m3c_time_picker_minute">Minute</string>
+ <string description="Lets the user know that the time unit being changed is second. Appears on the top of the TimePicker component. [CHAR_LIMIT=8]" name="wear_m3c_time_picker_second">Second</string>
+ <plurals description="Content description of the current number of hours being selected in TimePicker component. [CHAR_LIMIT=NONE]" name="wear_m3c_time_picker_hours_content_description">
+ <item quantity="one">%d Hour</item>
+ <item quantity="other">%d Hours</item>
+ </plurals>
+ <plurals description="Content description of the current number of hours being selected in TimePicker component. [CHAR_LIMIT=NONE]" name="wear_m3c_time_picker_minutes_content_description">
+ <item quantity="one">%d Minute</item>
+ <item quantity="other">%d Minutes</item>
+ </plurals>
+ <plurals description="Content description of the current number of hours being selected in TimePicker component. [CHAR_LIMIT=NONE]" name="wear_m3c_time_picker_seconds_content_description">
+ <item quantity="one">%d Second</item>
+ <item quantity="other">%d Seconds</item>
+ </plurals>
+ <string description="Content description of the period picker in TimePickerWith12HourClock. It lets the user select the period for 12H time format. [CHAR_LIMIT=NONE]" name="wear_m3c_time_picker_period">Period</string>
+ <string description="Content description of the confirm button of DatePicker and TimePicker components. It lets the user confirm the date or time selected. [CHAR_LIMIT=NONE]" name="wear_m3c_picker_confirm_button_content_description">Confirm</string>
+</resources>
\ No newline at end of file