diff options
| author | 2022-11-04 03:56:50 +0000 | |
|---|---|---|
| committer | 2022-11-04 03:56:50 +0000 | |
| commit | 1fa51d6102bbf85371acd5f41be916b2c1d43b8b (patch) | |
| tree | f6eceb9f5aa08fbac3371d70b589d1404b69a320 | |
| parent | 73f3d12db7ef0abdcef872c5b759ab6859b24577 (diff) | |
| parent | 4e2bf0631cb3bb1b40a52e369c97d60b1834a065 (diff) | |
Merge "Migrate from material2 to material3"
9 files changed, 1531 insertions, 241 deletions
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp index 25529bba382f..d8577c3ab3fc 100644 --- a/packages/CredentialManager/Android.bp +++ b/packages/CredentialManager/Android.bp @@ -17,7 +17,11 @@ android_app { static_libs: [ "androidx.activity_activity-compose", "androidx.appcompat_appcompat", - "androidx.compose.material_material", + "androidx.compose.animation_animation-core", + "androidx.compose.foundation_foundation", + "androidx.compose.material3_material3", + "androidx.compose.material_material-icons-core", + "androidx.compose.material_material-icons-extended", "androidx.compose.runtime_runtime", "androidx.compose.ui_ui", "androidx.compose.ui_ui-tooling", @@ -27,6 +31,7 @@ android_app { "androidx.lifecycle_lifecycle-runtime-ktx", "androidx.lifecycle_lifecycle-viewmodel-compose", "androidx.recyclerview_recyclerview", + "kotlinx-coroutines-core", ], platform_apis: true, diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 08ab1b4000f8..6178efcf86c0 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -27,4 +27,6 @@ <string name="passwords">passwords</string> <string name="sign_ins">sign-ins</string> <string name="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</string> + <!-- Spoken content description of an element which will close the sheet when clicked. --> + <string name="close_sheet">"Close sheet"</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt new file mode 100644 index 000000000000..f1f453da4f38 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.common.material + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.collapse +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.dismiss +import androidx.compose.ui.semantics.expand +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.android.credentialmanager.R +import com.android.credentialmanager.common.material.ModalBottomSheetValue.Expanded +import com.android.credentialmanager.common.material.ModalBottomSheetValue.HalfExpanded +import com.android.credentialmanager.common.material.ModalBottomSheetValue.Hidden +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Possible values of [ModalBottomSheetState]. + */ +enum class ModalBottomSheetValue { + /** + * The bottom sheet is not visible. + */ + Hidden, + + /** + * The bottom sheet is visible at full height. + */ + Expanded, + + /** + * The bottom sheet is partially visible at 50% of the screen height. This state is only + * enabled if the height of the bottom sheet is more than 50% of the screen height. + */ + HalfExpanded +} + +/** + * State of the [ModalBottomSheetLayout] composable. + * + * @param initialValue The initial value of the state. <b>Must not be set to + * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true.</b> + * @param animationSpec The default animation that will be used to animate to a new state. + * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should + * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the + * [Hidden] state when hiding the sheet, either programmatically or by user interaction. + * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b> + * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an + * [IllegalArgumentException] will be thrown. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +class ModalBottomSheetState( + initialValue: ModalBottomSheetValue, + animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec, + internal val isSkipHalfExpanded: Boolean, + confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } +) : SwipeableState<ModalBottomSheetValue>( + initialValue = initialValue, + animationSpec = animationSpec, + confirmStateChange = confirmStateChange +) { + /** + * Whether the bottom sheet is visible. + */ + val isVisible: Boolean + get() = currentValue != Hidden + + internal val hasHalfExpandedState: Boolean + get() = anchors.values.contains(HalfExpanded) + + constructor( + initialValue: ModalBottomSheetValue, + animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec, + confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } + ) : this(initialValue, animationSpec, isSkipHalfExpanded = false, confirmStateChange) + + init { + if (isSkipHalfExpanded) { + require(initialValue != HalfExpanded) { + "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" + + " true." + } + } + } + + /** + * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller + * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be + * fully expanded. + * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun show() { + val targetValue = when { + hasHalfExpandedState -> HalfExpanded + else -> Expanded + } + animateTo(targetValue = targetValue) + } + + /** + * Half expand the bottom sheet if half expand is enabled with animation and suspend until it + * animation is complete or cancelled + * + * @throws [CancellationException] if the animation is interrupted + */ + internal suspend fun halfExpand() { + if (!hasHalfExpandedState) { + return + } + animateTo(HalfExpanded) + } + + /** + * Fully expand the bottom sheet with animation and suspend until it if fully expanded or + * animation has been cancelled. + * * + * @throws [CancellationException] if the animation is interrupted + */ + internal suspend fun expand() = animateTo(Expanded) + + /** + * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has + * been cancelled. + * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun hide() = animateTo(Hidden) + + internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection + + companion object { + /** + * The default [Saver] implementation for [ModalBottomSheetState]. + */ + fun Saver( + animationSpec: AnimationSpec<Float>, + skipHalfExpanded: Boolean, + confirmStateChange: (ModalBottomSheetValue) -> Boolean + ): Saver<ModalBottomSheetState, *> = Saver( + save = { it.currentValue }, + restore = { + ModalBottomSheetState( + initialValue = it, + animationSpec = animationSpec, + isSkipHalfExpanded = skipHalfExpanded, + confirmStateChange = confirmStateChange + ) + } + ) + + /** + * The default [Saver] implementation for [ModalBottomSheetState]. + */ + @Deprecated( + message = "Please specify the skipHalfExpanded parameter", + replaceWith = ReplaceWith( + "ModalBottomSheetState.Saver(" + + "animationSpec = animationSpec," + + "skipHalfExpanded = ," + + "confirmStateChange = confirmStateChange" + + ")" + ) + ) + fun Saver( + animationSpec: AnimationSpec<Float>, + confirmStateChange: (ModalBottomSheetValue) -> Boolean + ): Saver<ModalBottomSheetState, *> = Saver( + animationSpec = animationSpec, + skipHalfExpanded = false, + confirmStateChange = confirmStateChange + ) + } +} + +/** + * Create a [ModalBottomSheetState] and [remember] it. + * + * @param initialValue The initial value of the state. + * @param animationSpec The default animation that will be used to animate to a new state. + * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should + * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the + * [Hidden] state when hiding the sheet, either programmatically or by user interaction. + * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b> + * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an + * [IllegalArgumentException] will be thrown. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +@Composable +fun rememberModalBottomSheetState( + initialValue: ModalBottomSheetValue, + animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec, + skipHalfExpanded: Boolean, + confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } +): ModalBottomSheetState { + return rememberSaveable( + initialValue, animationSpec, skipHalfExpanded, confirmStateChange, + saver = ModalBottomSheetState.Saver( + animationSpec = animationSpec, + skipHalfExpanded = skipHalfExpanded, + confirmStateChange = confirmStateChange + ) + ) { + ModalBottomSheetState( + initialValue = initialValue, + animationSpec = animationSpec, + isSkipHalfExpanded = skipHalfExpanded, + confirmStateChange = confirmStateChange + ) + } +} + +/** + * Create a [ModalBottomSheetState] and [remember] it. + * + * @param initialValue The initial value of the state. + * @param animationSpec The default animation that will be used to animate to a new state. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +@Composable +fun rememberModalBottomSheetState( + initialValue: ModalBottomSheetValue, + animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec, + confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true } +): ModalBottomSheetState = rememberModalBottomSheetState( + initialValue = initialValue, + animationSpec = animationSpec, + skipHalfExpanded = false, + confirmStateChange = confirmStateChange +) + +/** + * <a href="https://material.io/components/sheets-bottom#modal-bottom-sheet" class="external" target="_blank">Material Design modal bottom sheet</a>. + * + * Modal bottom sheets present a set of choices while blocking interaction with the rest of the + * screen. They are an alternative to inline menus and simple dialogs, providing + * additional room for content, iconography, and actions. + * + *  + * + * A simple example of a modal bottom sheet looks like this: + * + * @sample androidx.compose.material.samples.ModalBottomSheetSample + * + * @param sheetContent The content of the bottom sheet. + * @param modifier Optional [Modifier] for the entire component. + * @param sheetState The state of the bottom sheet. + * @param sheetShape The shape of the bottom sheet. + * @param sheetElevation The elevation of the bottom sheet. + * @param sheetBackgroundColor The background color of the bottom sheet. + * @param sheetContentColor The preferred content color provided by the bottom sheet to its + * children. Defaults to the matching content color for [sheetBackgroundColor], or if that is not + * a color from the theme, this will keep the same content color set above the bottom sheet. + * @param scrimColor The color of the scrim that is applied to the rest of the screen when the + * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no + * longer be applied and the bottom sheet will not block interaction with the rest of the screen + * when visible. + * @param content The content of rest of the screen. + */ +@Composable +fun ModalBottomSheetLayout( + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + sheetState: ModalBottomSheetState = + rememberModalBottomSheetState(Hidden), + sheetShape: Shape = MaterialTheme.shapes.large, + sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, + sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface, + sheetContentColor: Color = contentColorFor(sheetBackgroundColor), + scrimColor: Color = ModalBottomSheetDefaults.scrimColor, + content: @Composable () -> Unit +) { + val scope = rememberCoroutineScope() + BoxWithConstraints(modifier) { + val fullHeight = constraints.maxHeight.toFloat() + val sheetHeightState = remember { mutableStateOf<Float?>(null) } + Box(Modifier.fillMaxSize()) { + content() + Scrim( + color = scrimColor, + onDismiss = { + if (sheetState.confirmStateChange(Hidden)) { + scope.launch { sheetState.hide() } + } + }, + visible = sheetState.targetValue != Hidden + ) + } + Surface( + Modifier + .fillMaxWidth() + .nestedScroll(sheetState.nestedScrollConnection) + .offset { + val y = if (sheetState.anchors.isEmpty()) { + // if we don't know our anchors yet, render the sheet as hidden + fullHeight.roundToInt() + } else { + // if we do know our anchors, respect them + sheetState.offset.value.roundToInt() + } + IntOffset(0, y) + } + .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState) + .onGloballyPositioned { + sheetHeightState.value = it.size.height.toFloat() + } + .semantics { + if (sheetState.isVisible) { + dismiss { + if (sheetState.confirmStateChange(Hidden)) { + scope.launch { sheetState.hide() } + } + true + } + if (sheetState.currentValue == HalfExpanded) { + expand { + if (sheetState.confirmStateChange(Expanded)) { + scope.launch { sheetState.expand() } + } + true + } + } else if (sheetState.hasHalfExpandedState) { + collapse { + if (sheetState.confirmStateChange(HalfExpanded)) { + scope.launch { sheetState.halfExpand() } + } + true + } + } + } + }, + shape = sheetShape, + shadowElevation = sheetElevation, + color = sheetBackgroundColor, + contentColor = sheetContentColor + ) { + Column(content = sheetContent) + } + } +} + +@Suppress("ModifierInspectorInfo") +private fun Modifier.bottomSheetSwipeable( + sheetState: ModalBottomSheetState, + fullHeight: Float, + sheetHeightState: State<Float?> +): Modifier { + val sheetHeight = sheetHeightState.value + val modifier = if (sheetHeight != null) { + val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) { + mapOf( + fullHeight to Hidden, + fullHeight - sheetHeight to Expanded + ) + } else { + mapOf( + fullHeight to Hidden, + fullHeight / 2 to HalfExpanded, + max(0f, fullHeight - sheetHeight) to Expanded + ) + } + Modifier.swipeable( + state = sheetState, + anchors = anchors, + orientation = Orientation.Vertical, + enabled = sheetState.currentValue != Hidden, + resistance = null + ) + } else { + Modifier + } + + return this.then(modifier) +} + +@Composable +private fun Scrim( + color: Color, + onDismiss: () -> Unit, + visible: Boolean +) { + if (color.isSpecified) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = TweenSpec() + ) + LocalConfiguration.current + val resources = LocalContext.current.resources + val closeSheet = resources.getString(R.string.close_sheet) + val dismissModifier = if (visible) { + Modifier + .pointerInput(onDismiss) { detectTapGestures { onDismiss() } } + .semantics(mergeDescendants = true) { + contentDescription = closeSheet + onClick { onDismiss(); true } + } + } else { + Modifier + } + + Canvas( + Modifier + .fillMaxSize() + .then(dismissModifier) + ) { + drawRect(color = color, alpha = alpha) + } + } +} + +/** + * Contains useful Defaults for [ModalBottomSheetLayout]. + */ +object ModalBottomSheetDefaults { + + /** + * The default elevation used by [ModalBottomSheetLayout]. + */ + val Elevation = 16.dp + + /** + * The default scrim color used by [ModalBottomSheetLayout]. + */ + val scrimColor: Color + @Composable + get() = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f) +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt new file mode 100644 index 000000000000..3e2de8321006 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt @@ -0,0 +1,875 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.common.material + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import com.android.credentialmanager.common.material.SwipeableDefaults.AnimationSpec +import com.android.credentialmanager.common.material.SwipeableDefaults.StandardResistanceFactor +import com.android.credentialmanager.common.material.SwipeableDefaults.VelocityThreshold +import com.android.credentialmanager.common.material.SwipeableDefaults.resistanceConfig +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.sign +import kotlin.math.sin + +/** + * State of the [swipeable] modifier. + * + * This contains necessary information about any ongoing swipe or animation and provides methods + * to change the state either immediately or by starting an animation. To create and remember a + * [SwipeableState] with the default animation clock, use [rememberSwipeableState]. + * + * @param initialValue The initial value of the state. + * @param animationSpec The default animation that will be used to animate to a new state. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +@Stable +open class SwipeableState<T>( + initialValue: T, + internal val animationSpec: AnimationSpec<Float> = AnimationSpec, + internal val confirmStateChange: (newValue: T) -> Boolean = { true } +) { + /** + * The current value of the state. + * + * If no swipe or animation is in progress, this corresponds to the anchor at which the + * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds + * the last anchor at which the [swipeable] was settled before the swipe or animation started. + */ + var currentValue: T by mutableStateOf(initialValue) + private set + + /** + * Whether the state is currently animating. + */ + var isAnimationRunning: Boolean by mutableStateOf(false) + private set + + /** + * The current position (in pixels) of the [swipeable]. + * + * You should use this state to offset your content accordingly. The recommended way is to + * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled. + */ + val offset: State<Float> get() = offsetState + + /** + * The amount by which the [swipeable] has been swiped past its bounds. + */ + val overflow: State<Float> get() = overflowState + + // Use `Float.NaN` as a placeholder while the state is uninitialised. + private val offsetState = mutableStateOf(0f) + private val overflowState = mutableStateOf(0f) + + // the source of truth for the "real"(non ui) position + // basically position in bounds + overflow + private val absoluteOffset = mutableStateOf(0f) + + // current animation target, if animating, otherwise null + private val animationTarget = mutableStateOf<Float?>(null) + + internal var anchors by mutableStateOf(emptyMap<Float, T>()) + + private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> = + snapshotFlow { anchors } + .filter { it.isNotEmpty() } + .take(1) + + internal var minBound = Float.NEGATIVE_INFINITY + internal var maxBound = Float.POSITIVE_INFINITY + + internal fun ensureInit(newAnchors: Map<Float, T>) { + if (anchors.isEmpty()) { + // need to do initial synchronization synchronously :( + val initialOffset = newAnchors.getOffset(currentValue) + requireNotNull(initialOffset) { + "The initial value must have an associated anchor." + } + offsetState.value = initialOffset + absoluteOffset.value = initialOffset + } + } + + internal suspend fun processNewAnchors( + oldAnchors: Map<Float, T>, + newAnchors: Map<Float, T> + ) { + if (oldAnchors.isEmpty()) { + // If this is the first time that we receive anchors, then we need to initialise + // the state so we snap to the offset associated to the initial value. + minBound = newAnchors.keys.minOrNull()!! + maxBound = newAnchors.keys.maxOrNull()!! + val initialOffset = newAnchors.getOffset(currentValue) + requireNotNull(initialOffset) { + "The initial value must have an associated anchor." + } + snapInternalToOffset(initialOffset) + } else if (newAnchors != oldAnchors) { + // If we have received new anchors, then the offset of the current value might + // have changed, so we need to animate to the new offset. If the current value + // has been removed from the anchors then we animate to the closest anchor + // instead. Note that this stops any ongoing animation. + minBound = Float.NEGATIVE_INFINITY + maxBound = Float.POSITIVE_INFINITY + val animationTargetValue = animationTarget.value + // if we're in the animation already, let's find it a new home + val targetOffset = if (animationTargetValue != null) { + // first, try to map old state to the new state + val oldState = oldAnchors[animationTargetValue] + val newState = newAnchors.getOffset(oldState) + // return new state if exists, or find the closes one among new anchors + newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!! + } else { + // we're not animating, proceed by finding the new anchors for an old value + val actualOldValue = oldAnchors[offset.value] + val value = if (actualOldValue == currentValue) currentValue else actualOldValue + newAnchors.getOffset(value) ?: newAnchors + .keys.minByOrNull { abs(it - offset.value) }!! + } + try { + animateInternalToOffset(targetOffset, animationSpec) + } catch (c: CancellationException) { + // If the animation was interrupted for any reason, snap as a last resort. + snapInternalToOffset(targetOffset) + } finally { + currentValue = newAnchors.getValue(targetOffset) + minBound = newAnchors.keys.minOrNull()!! + maxBound = newAnchors.keys.maxOrNull()!! + } + } + } + + internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f }) + + internal var velocityThreshold by mutableStateOf(0f) + + internal var resistance: ResistanceConfig? by mutableStateOf(null) + + internal val draggableState = DraggableState { + val newAbsolute = absoluteOffset.value + it + val clamped = newAbsolute.coerceIn(minBound, maxBound) + val overflow = newAbsolute - clamped + val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f + offsetState.value = clamped + resistanceDelta + overflowState.value = overflow + absoluteOffset.value = newAbsolute + } + + private suspend fun snapInternalToOffset(target: Float) { + draggableState.drag { + dragBy(target - absoluteOffset.value) + } + } + + private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) { + draggableState.drag { + var prevValue = absoluteOffset.value + animationTarget.value = target + isAnimationRunning = true + try { + Animatable(prevValue).animateTo(target, spec) { + dragBy(this.value - prevValue) + prevValue = this.value + } + } finally { + animationTarget.value = null + isAnimationRunning = false + } + } + } + + /** + * The target value of the state. + * + * If a swipe is in progress, this is the value that the [swipeable] would animate to if the + * swipe finished. If an animation is running, this is the target value of that animation. + * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. + */ + val targetValue: T + get() { + // TODO(calintat): Track current velocity (b/149549482) and use that here. + val target = animationTarget.value ?: computeTarget( + offset = offset.value, + lastValue = anchors.getOffset(currentValue) ?: offset.value, + anchors = anchors.keys, + thresholds = thresholds, + velocity = 0f, + velocityThreshold = Float.POSITIVE_INFINITY + ) + return anchors[target] ?: currentValue + } + + /** + * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details. + * + * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`. + */ + val progress: SwipeProgress<T> + get() { + val bounds = findBounds(offset.value, anchors.keys) + val from: T + val to: T + val fraction: Float + when (bounds.size) { + 0 -> { + from = currentValue + to = currentValue + fraction = 1f + } + 1 -> { + from = anchors.getValue(bounds[0]) + to = anchors.getValue(bounds[0]) + fraction = 1f + } + else -> { + val (a, b) = + if (direction > 0f) { + bounds[0] to bounds[1] + } else { + bounds[1] to bounds[0] + } + from = anchors.getValue(a) + to = anchors.getValue(b) + fraction = (offset.value - a) / (b - a) + } + } + return SwipeProgress(from, to, fraction) + } + + /** + * The direction in which the [swipeable] is moving, relative to the current [currentValue]. + * + * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is + * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress. + */ + val direction: Float + get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f + + /** + * Set the state without any animation and suspend until it's set + * + * @param targetValue The new target value to set [currentValue] to. + */ + suspend fun snapTo(targetValue: T) { + latestNonEmptyAnchorsFlow.collect { anchors -> + val targetOffset = anchors.getOffset(targetValue) + requireNotNull(targetOffset) { + "The target value must have an associated anchor." + } + snapInternalToOffset(targetOffset) + currentValue = targetValue + } + } + + /** + * Set the state to the target value by starting an animation. + * + * @param targetValue The new value to animate to. + * @param anim The animation that will be used to animate to the new value. + */ + suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) { + latestNonEmptyAnchorsFlow.collect { anchors -> + try { + val targetOffset = anchors.getOffset(targetValue) + requireNotNull(targetOffset) { + "The target value must have an associated anchor." + } + animateInternalToOffset(targetOffset, anim) + } finally { + val endOffset = absoluteOffset.value + val endValue = anchors + // fighting rounding error once again, anchor should be as close as 0.5 pixels + .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f } + .values.firstOrNull() ?: currentValue + currentValue = endValue + } + } + } + + /** + * Perform fling with settling to one of the anchors which is determined by the given + * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided + * since it will settle at the anchor. + * + * In general cases, [swipeable] flings by itself when being swiped. This method is to be + * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may + * want to trigger settling fling when the child scroll container reaches the bound. + * + * @param velocity velocity to fling and settle with + * + * @return the reason fling ended + */ + suspend fun performFling(velocity: Float) { + latestNonEmptyAnchorsFlow.collect { anchors -> + val lastAnchor = anchors.getOffset(currentValue)!! + val targetValue = computeTarget( + offset = offset.value, + lastValue = lastAnchor, + anchors = anchors.keys, + thresholds = thresholds, + velocity = velocity, + velocityThreshold = velocityThreshold + ) + val targetState = anchors[targetValue] + if (targetState != null && confirmStateChange(targetState)) animateTo(targetState) + // If the user vetoed the state change, rollback to the previous state. + else animateInternalToOffset(lastAnchor, animationSpec) + } + } + + /** + * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable] + * gesture flow. + * + * Note: This method performs generic drag and it won't settle to any particular anchor, * + * leaving swipeable in between anchors. When done dragging, [performFling] must be + * called as well to ensure swipeable will settle at the anchor. + * + * In general cases, [swipeable] drags by itself when being swiped. This method is to be + * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may + * want to force drag when the child scroll container reaches the bound. + * + * @param delta delta in pixels to drag by + * + * @return the amount of [delta] consumed + */ + fun performDrag(delta: Float): Float { + val potentiallyConsumed = absoluteOffset.value + delta + val clamped = potentiallyConsumed.coerceIn(minBound, maxBound) + val deltaToConsume = clamped - absoluteOffset.value + if (abs(deltaToConsume) > 0) { + draggableState.dispatchRawDelta(deltaToConsume) + } + return deltaToConsume + } + + companion object { + /** + * The default [Saver] implementation for [SwipeableState]. + */ + fun <T : Any> Saver( + animationSpec: AnimationSpec<Float>, + confirmStateChange: (T) -> Boolean + ) = Saver<SwipeableState<T>, T>( + save = { it.currentValue }, + restore = { SwipeableState(it, animationSpec, confirmStateChange) } + ) + } +} + +/** + * Collects information about the ongoing swipe or animation in [swipeable]. + * + * To access this information, use [SwipeableState.progress]. + * + * @param from The state corresponding to the anchor we are moving away from. + * @param to The state corresponding to the anchor we are moving towards. + * @param fraction The fraction that the current position represents between [from] and [to]. + * Must be between `0` and `1`. + */ +@Immutable +class SwipeProgress<T>( + val from: T, + val to: T, + /*@FloatRange(from = 0.0, to = 1.0)*/ + val fraction: Float +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SwipeProgress<*>) return false + + if (from != other.from) return false + if (to != other.to) return false + if (fraction != other.fraction) return false + + return true + } + + override fun hashCode(): Int { + var result = from?.hashCode() ?: 0 + result = 31 * result + (to?.hashCode() ?: 0) + result = 31 * result + fraction.hashCode() + return result + } + + override fun toString(): String { + return "SwipeProgress(from=$from, to=$to, fraction=$fraction)" + } +} + +/** + * Create and [remember] a [SwipeableState] with the default animation clock. + * + * @param initialValue The initial value of the state. + * @param animationSpec The default animation that will be used to animate to a new state. + * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. + */ +@Composable +fun <T : Any> rememberSwipeableState( + initialValue: T, + animationSpec: AnimationSpec<Float> = AnimationSpec, + confirmStateChange: (newValue: T) -> Boolean = { true } +): SwipeableState<T> { + return rememberSaveable( + saver = SwipeableState.Saver( + animationSpec = animationSpec, + confirmStateChange = confirmStateChange + ) + ) { + SwipeableState( + initialValue = initialValue, + animationSpec = animationSpec, + confirmStateChange = confirmStateChange + ) + } +} + +/** + * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.: + * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value. + * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the + * [value] will be notified to update their state to the new value of the [SwipeableState] by + * invoking [onValueChange]. If the owner does not update their state to the provided value for + * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value. + */ +@Composable +internal fun <T : Any> rememberSwipeableStateFor( + value: T, + onValueChange: (T) -> Unit, + animationSpec: AnimationSpec<Float> = AnimationSpec +): SwipeableState<T> { + val swipeableState = remember { + SwipeableState( + initialValue = value, + animationSpec = animationSpec, + confirmStateChange = { true } + ) + } + val forceAnimationCheck = remember { mutableStateOf(false) } + LaunchedEffect(value, forceAnimationCheck.value) { + if (value != swipeableState.currentValue) { + swipeableState.animateTo(value) + } + } + DisposableEffect(swipeableState.currentValue) { + if (value != swipeableState.currentValue) { + onValueChange(swipeableState.currentValue) + forceAnimationCheck.value = !forceAnimationCheck.value + } + onDispose { } + } + return swipeableState +} + +/** + * Enable swipe gestures between a set of predefined states. + * + * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). + * Note that this map cannot be empty and cannot have two anchors mapped to the same state. + * + * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe + * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`). + * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is + * reached, the value of the [SwipeableState] will also be updated to the state corresponding to + * the new anchor. The target anchor is calculated based on the provided positional [thresholds]. + * + * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe + * past these bounds, a resistance effect will be applied by default. The amount of resistance at + * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`. + * + * For an example of a [swipeable] with three states, see: + * + * @sample androidx.compose.material.samples.SwipeableSample + * + * @param T The type of the state. + * @param state The state of the [swipeable]. + * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa. + * @param thresholds Specifies where the thresholds between the states are. The thresholds will be + * used to determine which state to animate to when swiping stops. This is represented as a lambda + * that takes two states and returns the threshold between them in the form of a [ThresholdConfig]. + * Note that the order of the states corresponds to the swipe direction. + * @param orientation The orientation in which the [swipeable] can be swiped. + * @param enabled Whether this [swipeable] is enabled and should react to the user's input. + * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom + * swipe will behave like bottom to top, and a left to right swipe will behave like right to left. + * @param interactionSource Optional [MutableInteractionSource] that will passed on to + * the internal [Modifier.draggable]. + * @param resistance Controls how much resistance will be applied when swiping past the bounds. + * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed + * in order to animate to the next state, even if the positional [thresholds] have not been reached. + */ +fun <T> Modifier.swipeable( + state: SwipeableState<T>, + anchors: Map<Float, T>, + orientation: Orientation, + enabled: Boolean = true, + reverseDirection: Boolean = false, + interactionSource: MutableInteractionSource? = null, + thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) }, + resistance: ResistanceConfig? = resistanceConfig(anchors.keys), + velocityThreshold: Dp = VelocityThreshold +) = composed( + inspectorInfo = debugInspectorInfo { + name = "swipeable" + properties["state"] = state + properties["anchors"] = anchors + properties["orientation"] = orientation + properties["enabled"] = enabled + properties["reverseDirection"] = reverseDirection + properties["interactionSource"] = interactionSource + properties["thresholds"] = thresholds + properties["resistance"] = resistance + properties["velocityThreshold"] = velocityThreshold + } +) { + require(anchors.isNotEmpty()) { + "You must have at least one anchor." + } + require(anchors.values.distinct().count() == anchors.size) { + "You cannot have two anchors mapped to the same state." + } + val density = LocalDensity.current + state.ensureInit(anchors) + LaunchedEffect(anchors, state) { + val oldAnchors = state.anchors + state.anchors = anchors + state.resistance = resistance + state.thresholds = { a, b -> + val from = anchors.getValue(a) + val to = anchors.getValue(b) + with(thresholds(from, to)) { density.computeThreshold(a, b) } + } + with(density) { + state.velocityThreshold = velocityThreshold.toPx() + } + state.processNewAnchors(oldAnchors, anchors) + } + + Modifier.draggable( + orientation = orientation, + enabled = enabled, + reverseDirection = reverseDirection, + interactionSource = interactionSource, + startDragImmediately = state.isAnimationRunning, + onDragStopped = { velocity -> launch { state.performFling(velocity) } }, + state = state.draggableState + ) +} + +/** + * Interface to compute a threshold between two anchors/states in a [swipeable]. + * + * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold]. + */ +@Stable +interface ThresholdConfig { + /** + * Compute the value of the threshold (in pixels), once the values of the anchors are known. + */ + fun Density.computeThreshold(fromValue: Float, toValue: Float): Float +} + +/** + * A fixed threshold will be at an [offset] away from the first anchor. + * + * @param offset The offset (in dp) that the threshold will be at. + */ +@Immutable +data class FixedThreshold(private val offset: Dp) : ThresholdConfig { + override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { + return fromValue + offset.toPx() * sign(toValue - fromValue) + } +} + +/** + * A fractional threshold will be at a [fraction] of the way between the two anchors. + * + * @param fraction The fraction (between 0 and 1) that the threshold will be at. + */ +@Immutable +data class FractionalThreshold( + /*@FloatRange(from = 0.0, to = 1.0)*/ + private val fraction: Float +) : ThresholdConfig { + override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { + return lerp(fromValue, toValue, fraction) + } +} + +/** + * Specifies how resistance is calculated in [swipeable]. + * + * There are two things needed to calculate resistance: the resistance basis determines how much + * overflow will be consumed to achieve maximum resistance, and the resistance factor determines + * the amount of resistance (the larger the resistance factor, the stronger the resistance). + * + * The resistance basis is usually either the size of the component which [swipeable] is applied + * to, or the distance between the minimum and maximum anchors. For a constructor in which the + * resistance basis defaults to the latter, consider using [resistanceConfig]. + * + * You may specify different resistance factors for each bound. Consider using one of the default + * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user + * has run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe + * this right now. Also, you can set either factor to 0 to disable resistance at that bound. + * + * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive. + * @param factorAtMin The factor by which to scale the resistance at the minimum bound. + * Must not be negative. + * @param factorAtMax The factor by which to scale the resistance at the maximum bound. + * Must not be negative. + */ +@Immutable +class ResistanceConfig( + /*@FloatRange(from = 0.0, fromInclusive = false)*/ + val basis: Float, + /*@FloatRange(from = 0.0)*/ + val factorAtMin: Float = StandardResistanceFactor, + /*@FloatRange(from = 0.0)*/ + val factorAtMax: Float = StandardResistanceFactor +) { + fun computeResistance(overflow: Float): Float { + val factor = if (overflow < 0) factorAtMin else factorAtMax + if (factor == 0f) return 0f + val progress = (overflow / basis).coerceIn(-1f, 1f) + return basis / factor * sin(progress * PI.toFloat() / 2) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ResistanceConfig) return false + + if (basis != other.basis) return false + if (factorAtMin != other.factorAtMin) return false + if (factorAtMax != other.factorAtMax) return false + + return true + } + + override fun hashCode(): Int { + var result = basis.hashCode() + result = 31 * result + factorAtMin.hashCode() + result = 31 * result + factorAtMax.hashCode() + return result + } + + override fun toString(): String { + return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)" + } +} + +/** + * Given an offset x and a set of anchors, return a list of anchors: + * 1. [ ] if the set of anchors is empty, + * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' + * is x rounded to the exact value of the matching anchor, + * 3. [ min ] if min is the minimum anchor and x < min, + * 4. [ max ] if max is the maximum anchor and x > max, or + * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal. + */ +private fun findBounds( + offset: Float, + anchors: Set<Float> +): List<Float> { + // Find the anchors the target lies between with a little bit of rounding error. + val a = anchors.filter { it <= offset + 0.001 }.maxOrNull() + val b = anchors.filter { it >= offset - 0.001 }.minOrNull() + + return when { + a == null -> + // case 1 or 3 + listOfNotNull(b) + b == null -> + // case 4 + listOf(a) + a == b -> + // case 2 + // Can't return offset itself here since it might not be exactly equal + // to the anchor, despite being considered an exact match. + listOf(a) + else -> + // case 5 + listOf(a, b) + } +} + +private fun computeTarget( + offset: Float, + lastValue: Float, + anchors: Set<Float>, + thresholds: (Float, Float) -> Float, + velocity: Float, + velocityThreshold: Float +): Float { + val bounds = findBounds(offset, anchors) + return when (bounds.size) { + 0 -> lastValue + 1 -> bounds[0] + else -> { + val lower = bounds[0] + val upper = bounds[1] + if (lastValue <= offset) { + // Swiping from lower to upper (positive). + if (velocity >= velocityThreshold) { + return upper + } else { + val threshold = thresholds(lower, upper) + if (offset < threshold) lower else upper + } + } else { + // Swiping from upper to lower (negative). + if (velocity <= -velocityThreshold) { + return lower + } else { + val threshold = thresholds(upper, lower) + if (offset > threshold) upper else lower + } + } + } + } +} + +private fun <T> Map<Float, T>.getOffset(state: T): Float? { + return entries.firstOrNull { it.value == state }?.key +} + +/** + * Contains useful defaults for [swipeable] and [SwipeableState]. + */ +object SwipeableDefaults { + /** + * The default animation used by [SwipeableState]. + */ + val AnimationSpec = SpringSpec<Float>() + + /** + * The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. + */ + val VelocityThreshold = 125.dp + + /** + * A stiff resistance factor which indicates that swiping isn't available right now. + */ + const val StiffResistanceFactor = 20f + + /** + * A standard resistance factor which indicates that the user has run out of things to see. + */ + const val StandardResistanceFactor = 10f + + /** + * The default resistance config used by [swipeable]. + * + * This returns `null` if there is one anchor. If there are at least two anchors, it returns + * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds. + */ + fun resistanceConfig( + anchors: Set<Float>, + factorAtMin: Float = StandardResistanceFactor, + factorAtMax: Float = StandardResistanceFactor + ): ResistanceConfig? { + return if (anchors.size <= 1) { + null + } else { + val basis = anchors.maxOrNull()!! - anchors.minOrNull()!! + ResistanceConfig(basis, factorAtMin, factorAtMax) + } + } +} + +// temp default nested scroll connection for swipeables which desire as an opt in +// revisit in b/174756744 as all types will have their own specific connection probably +internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection + get() = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + performDrag(delta).toOffset() + } else { + Offset.Zero + } + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return if (source == NestedScrollSource.Drag) { + performDrag(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = Offset(available.x, available.y).toFloat() + return if (toFling < 0 && offset.value > minBound) { + performFling(velocity = toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + performFling(velocity = Offset(available.x, available.y).toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset(0f, this) + + private fun Offset.toFloat(): Float = this.y + }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index 13a892fa3592..278b8352f8d1 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -1,6 +1,5 @@ package com.android.credentialmanager.createflow -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,24 +8,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.ButtonColors -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Card -import androidx.compose.material.Chip -import androidx.compose.material.ChipDefaults -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TopAppBar +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -39,16 +33,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.R +import com.android.credentialmanager.common.material.ModalBottomSheetLayout +import com.android.credentialmanager.common.material.ModalBottomSheetValue +import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PASSWORD_CREDENTIAL import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL -import com.android.credentialmanager.ui.theme.Grey100 -import com.android.credentialmanager.ui.theme.Shapes -import com.android.credentialmanager.ui.theme.Typography -import com.android.credentialmanager.ui.theme.lightBackgroundColor -import com.android.credentialmanager.ui.theme.lightColorAccentSecondary -import com.android.credentialmanager.ui.theme.lightSurface1 -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CreatePasskeyScreen( viewModel: CreatePasskeyViewModel, @@ -92,8 +83,8 @@ fun CreatePasskeyScreen( ) } }, - scrimColor = Color.Transparent, - sheetShape = Shapes.medium, + scrimColor = MaterialTheme.colorScheme.scrim, + sheetShape = MaterialTheme.shapes.medium, ) {} LaunchedEffect(state.currentValue) { if (state.currentValue == ModalBottomSheetValue.Hidden) { @@ -107,9 +98,7 @@ fun ConfirmationCard( onConfirm: () -> Unit, onCancel: () -> Unit, ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { Icon( painter = painterResource(R.drawable.ic_passkey), @@ -119,7 +108,7 @@ fun ConfirmationCard( ) Text( text = stringResource(R.string.passkey_creation_intro_title), - style = Typography.subtitle1, + style = MaterialTheme.typography.titleMedium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally) @@ -130,7 +119,7 @@ fun ConfirmationCard( ) Text( text = stringResource(R.string.passkey_creation_intro_body), - style = Typography.body1, + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(horizontal = 28.dp) ) Divider( @@ -143,11 +132,11 @@ fun ConfirmationCard( ) { CancelButton( stringResource(R.string.string_cancel), - onclick = onCancel + onClick = onCancel ) ConfirmButton( stringResource(R.string.string_continue), - onclick = onConfirm + onClick = onConfirm ) } Divider( @@ -159,25 +148,23 @@ fun ConfirmationCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ProviderSelectionCard( providerList: List<ProviderInfo>, onProviderSelected: (String) -> Unit, onCancel: () -> Unit ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { Text( text = stringResource(R.string.choose_provider_title), - style = Typography.subtitle1, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) Text( text = stringResource(R.string.choose_provider_body), - style = Typography.body1, + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(horizontal = 28.dp) ) Divider( @@ -185,10 +172,10 @@ fun ProviderSelectionCard( color = Color.Transparent ) Card( - shape = Shapes.medium, + shape = MaterialTheme.shapes.large, modifier = Modifier .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally) + .align(alignment = Alignment.CenterHorizontally), ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(2.dp) @@ -219,28 +206,27 @@ fun ProviderSelectionCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionsSelectionCard( providerList: List<ProviderInfo>, onBackButtonSelected: () -> Unit, onOptionSelected: (ActiveEntry) -> Unit ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { TopAppBar( title = { - Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1) + Text( + text = stringResource(R.string.string_more_options), + style = MaterialTheme.typography.titleMedium + ) }, - backgroundColor = lightBackgroundColor, - elevation = 0.dp, - navigationIcon = - { + navigationIcon = { IconButton(onClick = onBackButtonSelected) { - Icon(Icons.Filled.ArrowBack, "backIcon" - ) + Icon( + Icons.Filled.ArrowBack, + "backIcon") } } ) @@ -250,12 +236,12 @@ fun MoreOptionsSelectionCard( ) Text( text = stringResource(R.string.create_passkey_at), - style = Typography.body1, + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(horizontal = 28.dp), textAlign = TextAlign.Center ) Card( - shape = Shapes.medium, + shape = MaterialTheme.shapes.large, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally) @@ -287,19 +273,17 @@ fun MoreOptionsSelectionCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionsRowIntroCard( providerInfo: ProviderInfo, onDefaultOrNotSelected: () -> Unit, ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { Text( text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName), - style = Typography.subtitle1, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) Row( @@ -308,11 +292,11 @@ fun MoreOptionsRowIntroCard( ) { CancelButton( stringResource(R.string.use_once), - onclick = onDefaultOrNotSelected + onClick = onDefaultOrNotSelected ) ConfirmButton( stringResource(R.string.set_as_default), - onclick = onDefaultOrNotSelected + onClick = onDefaultOrNotSelected ) } Divider( @@ -324,74 +308,45 @@ fun MoreOptionsRowIntroCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) { - Chip( + SuggestionChip( modifier = Modifier.fillMaxWidth(), onClick = {onProviderSelected(providerInfo.name)}, - leadingIcon = { + icon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = providerInfo.icon.toBitmap().asImageBitmap(), // painter = painterResource(R.drawable.ic_passkey), // TODO: add description. contentDescription = "") }, - colors = ChipDefaults.chipColors( - backgroundColor = Grey100, - leadingIconContentColor = Grey100 - ), - shape = Shapes.large - ) { - Text( - text = providerInfo.displayName, - style = Typography.button, - modifier = Modifier.padding(vertical = 18.dp) - ) - } -} - -@Composable -fun CancelButton(text: String, onclick: () -> Unit) { - val colors = ButtonDefaults.buttonColors( - backgroundColor = lightBackgroundColor + shape = MaterialTheme.shapes.large, + label = { + Text( + text = providerInfo.displayName, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(vertical = 18.dp) + ) + } ) - NavigationButton( - border = BorderStroke(1.dp, lightSurface1), - colors = colors, - text = text, - onclick = onclick) } @Composable -fun ConfirmButton(text: String, onclick: () -> Unit) { - val colors = ButtonDefaults.buttonColors( - backgroundColor = lightColorAccentSecondary - ) - NavigationButton( - colors = colors, - text = text, - onclick = onclick) +fun CancelButton(text: String, onClick: () -> Unit) { + TextButton(onClick = onClick) { + Text(text = text) + } } @Composable -fun NavigationButton( - border: BorderStroke? = null, - colors: ButtonColors, - text: String, - onclick: () -> Unit -) { - Button( - onClick = onclick, - shape = Shapes.small, - colors = colors, - border = border - ) { - Text(text = text, style = Typography.button) +fun ConfirmButton(text: String, onClick: () -> Unit) { + FilledTonalButton(onClick = onClick) { + Text(text = text) } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CreationSelectionCard( requestDisplayInfo: RequestDisplayInfo, @@ -403,9 +358,7 @@ fun CreationSelectionCard( multiProvider: Boolean, onMoreOptionsSelected: () -> Unit, ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { Icon( bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), @@ -422,14 +375,14 @@ fun CreationSelectionCard( else -> stringResource(R.string.choose_create_option_sign_in_title, providerInfo.displayName) }, - style = Typography.subtitle1, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally), textAlign = TextAlign.Center, ) Text( text = requestDisplayInfo.appDomainName, - style = Typography.body2, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.align(alignment = Alignment.CenterHorizontally) ) Text( @@ -441,15 +394,16 @@ fun CreationSelectionCard( else -> stringResource(R.string.sign_ins) }, providerInfo.displayName, - createOptionInfo.userProviderDisplayName), - style = Typography.body1, + createOptionInfo.userProviderDisplayName + ), + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) Card( - shape = Shapes.medium, + shape = MaterialTheme.shapes.large, modifier = Modifier .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally) + .align(alignment = Alignment.CenterHorizontally), ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(2.dp) @@ -489,11 +443,11 @@ fun CreationSelectionCard( ) { CancelButton( stringResource(R.string.string_cancel), - onclick = onCancel + onClick = onCancel ) ConfirmButton( stringResource(R.string.string_continue), - onclick = onConfirm + onClick = onConfirm ) } Divider( @@ -505,66 +459,59 @@ fun CreationSelectionCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PrimaryCreateOptionRow( requestDisplayInfo: RequestDisplayInfo, createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { - Chip( + SuggestionChip( modifier = Modifier.fillMaxWidth(), onClick = onOptionSelected, - leadingIcon = { + icon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), contentDescription = stringResource(R.string.createOptionInfo_icon_description)) }, - colors = ChipDefaults.chipColors( - backgroundColor = Grey100, - leadingIconContentColor = Grey100 - ), - shape = Shapes.large - ) { - Column() { - Text( - text = requestDisplayInfo.userName, - style = Typography.h6, - modifier = Modifier.padding(top = 16.dp) - ) - Text( - text = requestDisplayInfo.displayName, - style = Typography.body2, - modifier = Modifier.padding(bottom = 16.dp) - ) + shape = MaterialTheme.shapes.large, + label = { + Column() { + Text( + text = requestDisplayInfo.userName, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = requestDisplayInfo.displayName, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + } } - } + ) } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionsInfoRow( providerInfo: ProviderInfo, createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { - Chip( + SuggestionChip( modifier = Modifier.fillMaxWidth(), onClick = onOptionSelected, - leadingIcon = { + icon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = createOptionInfo.profileIcon.toBitmap().asImageBitmap(), // painter = painterResource(R.drawable.ic_passkey), // TODO: add description. contentDescription = "") }, - colors = ChipDefaults.chipColors( - backgroundColor = Grey100, - leadingIconContentColor = Grey100 - ), - shape = Shapes.large - ) { - Column() { + shape = MaterialTheme.shapes.large, + label = { + Column() { Text( text = if (providerInfo.createOptions.size > 1) @@ -572,15 +519,16 @@ fun MoreOptionsInfoRow( providerInfo.displayName, createOptionInfo.userProviderDisplayName)} else { stringResource(R.string.more_options_title_one_option, providerInfo.displayName)}, - style = Typography.h6, + style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(top = 16.dp) ) Text( text = stringResource(R.string.more_options_usage_data, createOptionInfo.passwordCount, createOptionInfo.passkeyCount), - style = Typography.body2, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(bottom = 16.dp) ) + } } - } + ) }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 8b8108319b89..76350218ca0b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -25,16 +25,13 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Card -import androidx.compose.material.Chip -import androidx.compose.material.ChipDefaults -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Text -import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -45,13 +42,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.R +import com.android.credentialmanager.common.material.ModalBottomSheetLayout +import com.android.credentialmanager.common.material.ModalBottomSheetValue +import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.createflow.CancelButton -import com.android.credentialmanager.ui.theme.Grey100 -import com.android.credentialmanager.ui.theme.Shapes -import com.android.credentialmanager.ui.theme.Typography -import com.android.credentialmanager.ui.theme.lightBackgroundColor -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GetCredentialScreen( viewModel: GetCredentialViewModel, @@ -76,7 +72,7 @@ fun GetCredentialScreen( } }, scrimColor = Color.Transparent, - sheetShape = Shapes.medium, + sheetShape = MaterialTheme.shapes.medium, ) {} LaunchedEffect(state.currentValue) { if (state.currentValue == ModalBottomSheetValue.Hidden) { @@ -85,7 +81,7 @@ fun GetCredentialScreen( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CredentialSelectionCard( requestDisplayInfo: RequestDisplayInfo, @@ -95,9 +91,7 @@ fun CredentialSelectionCard( multiProvider: Boolean, onMoreOptionSelected: () -> Unit, ) { - Card( - backgroundColor = lightBackgroundColor, - ) { + Card() { Column() { Icon( bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(), @@ -107,14 +101,14 @@ fun CredentialSelectionCard( ) Text( text = stringResource(R.string.choose_sign_in_title), - style = Typography.subtitle1, + style = MaterialTheme.typography.titleMedium, modifier = Modifier .padding(all = 24.dp) .align(alignment = Alignment.CenterHorizontally) ) Text( text = requestDisplayInfo.appDomainName, - style = Typography.body2, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(horizontal = 28.dp) ) Divider( @@ -122,7 +116,7 @@ fun CredentialSelectionCard( color = Color.Transparent ) Card( - shape = Shapes.medium, + shape = MaterialTheme.shapes.medium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally) @@ -161,57 +155,52 @@ fun CredentialSelectionCard( } } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CredentialOptionRow( credentialOptionInfo: CredentialOptionInfo, onOptionSelected: (String, String) -> Unit, ) { - Chip( + SuggestionChip( modifier = Modifier.fillMaxWidth(), onClick = {onOptionSelected(credentialOptionInfo.entryKey, credentialOptionInfo.entrySubkey)}, - leadingIcon = { + icon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(), // TODO: add description. contentDescription = "") }, - colors = ChipDefaults.chipColors( - backgroundColor = Grey100, - leadingIconContentColor = Grey100 - ), - shape = Shapes.large - ) { - Column() { - Text( - text = credentialOptionInfo.entryKey, - style = Typography.h6, - modifier = Modifier.padding(top = 16.dp) - ) - Text( - text = credentialOptionInfo.entrySubkey, - style = Typography.body2, - modifier = Modifier.padding(bottom = 16.dp) - ) + shape = MaterialTheme.shapes.large, + label = { + Column() { + // TODO: fix the text values. + Text( + text = credentialOptionInfo.entryKey, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = credentialOptionInfo.entrySubkey, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + } } - } + ) } -@ExperimentalMaterialApi +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionRow(onSelect: () -> Unit) { - Chip( + SuggestionChip( modifier = Modifier.fillMaxWidth().height(52.dp), onClick = onSelect, - colors = ChipDefaults.chipColors( - backgroundColor = Grey100, - leadingIconContentColor = Grey100 - ), - shape = Shapes.large - ) { - Text( - text = stringResource(R.string.string_more_options), - style = Typography.h6, - ) - } + shape = MaterialTheme.shapes.large, + label = { + Text( + text = stringResource(R.string.string_more_options), + style = MaterialTheme.typography.titleLarge, + ) + } + ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt index cba86585ee59..5ea69930e334 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt @@ -1,7 +1,7 @@ package com.android.credentialmanager.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes +import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp val Shapes = Shapes( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt index a9d20ae9c42e..248df92bac59 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt @@ -1,30 +1,21 @@ package com.android.credentialmanager.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable -private val DarkColorPalette = darkColors( +private val AppDarkColorScheme = darkColorScheme( primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200 + secondary = Purple700, + tertiary = Teal200 ) -private val LightColorPalette = lightColors( +private val AppLightColorScheme = lightColorScheme( primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200 - - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ + secondary = Purple700, + tertiary = Teal200 ) @Composable @@ -32,14 +23,14 @@ fun CredentialSelectorTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val colors = if (darkTheme) { - DarkColorPalette + val AppColorScheme = if (darkTheme) { + AppDarkColorScheme } else { - LightColorPalette + AppLightColorScheme } MaterialTheme( - colors = colors, + colorScheme = AppColorScheme, typography = Typography, shapes = Shapes, content = content diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt index d8fb01c17f95..e09abbb3ffff 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt @@ -1,6 +1,6 @@ package com.android.credentialmanager.ui.theme -import androidx.compose.material.Typography +import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -8,32 +8,32 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( - subtitle1 = TextStyle( + titleMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 24.sp, lineHeight = 32.sp, ), - body1 = TextStyle( + bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, ), - body2 = TextStyle( + bodyMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, color = textColorSecondary ), - button = TextStyle( + labelLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, ), - h6 = TextStyle( + titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 16.sp, |