添加 TimeText

This commit is contained in:
2025-02-11 21:01:38 +08:00
parent 9e9054d98a
commit abe20e58e0
2 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.compose.material
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.text.format.DateFormat
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.wear.compose.material.TimeTextDefaults.TextSeparator
import androidx.wear.compose.material.TimeTextDefaults.timeFormat
import androidx.wear.compose.materialcore.currentTimeMillis
import androidx.wear.compose.materialcore.is24HourFormat
import java.util.Calendar
import java.util.Locale
/**
* Layout to show the current time and a label at the top of the screen. If device has a round
* screen, then the time will be curved along the top edge of the screen, if rectangular - then the
* text and the time will be straight
*
* This composable supports additional composable views to the left and to the right of the clock:
* Start Content and End Content. [startCurvedContent], [endCurvedContent] and [textCurvedSeparator]
* are used for Round screens. [startLinearContent], [endLinearContent] and [textLinearSeparator]
* are used for Square screens. For proper support of Square and Round screens both Linear and
* Curved methods should be implemented.
*
* Note that Wear Material UX guidance recommends that time text should not be larger than 90
* degrees of the screen edge on round devices and prefers short status messages be shown in start
* content only using the MaterialTheme.colors.primary color for the status message.
*
* For more information, see the
* [Curved Text](https://developer.android.com/training/wearables/components/curved-text) guide.
*
* A [TimeText] with a short app status message shown in the start content:
*
* @sample androidx.wear.compose.material.samples.TimeTextWithStatus
*
* An example of a [TimeText] with a different date and time format:
*
* @sample androidx.wear.compose.material.samples.TimeTextWithFullDateAndTimeFormat
*
* An example of a [TimeText] animating a message that is added or removed
*
* @sample androidx.wear.compose.material.samples.TimeTextAnimation
* @param modifier Current modifier.
* @param timeSource [TimeSource] which retrieves the current time and formats it.
* @param timeTextStyle Optional textStyle for the time text itself
* @param contentPadding The spacing values between the container and the content
* @param startLinearContent a slot before the time which is used only on Square screens
* @param endLinearContent a slot after the time which is used only on Square screens
* @param textLinearSeparator a separator slot which is used only on Square screens
*/
@Composable
public fun TimeText(
modifier: Modifier = Modifier,
timeSource: TimeSource = TimeTextDefaults.timeSource(timeFormat()),
timeTextStyle: TextStyle = TimeTextDefaults.timeTextStyle(),
contentPadding: PaddingValues = TimeTextDefaults.ContentPadding,
startLinearContent: (@Composable () -> Unit)? = null,
endLinearContent: (@Composable () -> Unit)? = null,
textLinearSeparator: @Composable () -> Unit = { TextSeparator(textStyle = timeTextStyle) },
) {
val timeText = timeSource.currentTime
Row(
modifier = modifier.padding(contentPadding),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.Center
) {
startLinearContent?.let {
it.invoke()
textLinearSeparator()
}
Text(
text = timeText,
maxLines = 1,
style = timeTextStyle,
)
endLinearContent?.let {
textLinearSeparator()
it.invoke()
}
}
}
/** Contains the default values used by [TimeText] */
public object TimeTextDefaults {
private val Padding = 2.dp
/** Default format for 24h clock. */
public const val TimeFormat24Hours: String = "HH:mm"
/** Default format for 12h clock. */
public const val TimeFormat12Hours: String = "h:mm"
/** The default content padding used by [TimeText] */
public val ContentPadding: PaddingValues = PaddingValues(Padding)
/**
* Retrieves default timeFormat for the device. Depending on settings, it can be either 12h or
* 24h format
*/
@Composable
public fun timeFormat(): String {
val format = if (is24HourFormat()) TimeFormat24Hours else TimeFormat12Hours
return DateFormat.getBestDateTimePattern(Locale.getDefault(), format)
.replace("a", "")
.trim()
}
/**
* Creates a [TextStyle] with default parameters used for showing time on square screens. By
* default a copy of MaterialTheme.typography.caption1 style is created
*
* @param background The background color
* @param color The main color
* @param fontSize The font size
*/
@Composable
public fun timeTextStyle(
background: Color = Color.Unspecified,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
): TextStyle =
MaterialTheme.typography.labelLarge +
TextStyle(color = color, background = background, fontSize = fontSize)
/**
* A default implementation of Separator shown between start/end content and the time on square
* screens
*
* @param modifier A default modifier for the separator
* @param textStyle A [TextStyle] for the separator
* @param contentPadding The spacing values between the container and the separator
*/
@Composable
public fun TextSeparator(
modifier: Modifier = Modifier,
textStyle: TextStyle = timeTextStyle(),
contentPadding: PaddingValues = PaddingValues(horizontal = 4.dp)
) {
Text(text = "·", style = textStyle, modifier = modifier.padding(contentPadding))
}
/**
* A default implementation of [TimeSource]. Once the system time changes, it triggers an update
* of the [TimeSource.currentTime] which is formatted before that using [timeFormat] param.
*
* Android implementation: [DefaultTimeSource] for Android uses [android.text.format.DateFormat]
* [timeFormat] should follow the standard
* [Date and Time patterns](https://developer.android.com/reference/java/text/SimpleDateFormat#date-and-time-patterns)
* Examples: "h:mm a" - 12:08 PM "yyyy.MM.dd HH:mm:ss" - 2021.11.01 14:08:56 More examples can
* be found [here](https://developer.android.com/reference/java/text/SimpleDateFormat#examples)
*
* Desktop implementation: TBD
*
* @param timeFormat Date and time string pattern
*/
public fun timeSource(timeFormat: String): TimeSource = DefaultTimeSource(timeFormat)
}
/** An interface which is responsible for retrieving a formatted time. */
public interface TimeSource {
/**
* A method responsible for returning updated time string.
*
* @return Formatted time string.
*/
public val currentTime: String
@Composable get
}
internal class DefaultTimeSource constructor(timeFormat: String) : TimeSource {
private val _timeFormat = timeFormat
override val currentTime: String
@Composable get() = currentTime({ currentTimeMillis() }, _timeFormat).value
}
@Composable
@VisibleForTesting
internal fun currentTime(time: () -> Long, timeFormat: String): State<String> {
var calendar by remember { mutableStateOf(Calendar.getInstance()) }
var currentTime by remember { mutableLongStateOf(time()) }
val timeText = remember { derivedStateOf { formatTime(calendar, currentTime, timeFormat) } }
val context = LocalContext.current
val updatedTimeLambda by rememberUpdatedState(time)
DisposableEffect(context, updatedTimeLambda) {
val receiver =
TimeBroadcastReceiver(
onTimeChanged = { currentTime = updatedTimeLambda() },
onTimeZoneChanged = { calendar = Calendar.getInstance() }
)
receiver.register(context)
onDispose { receiver.unregister(context) }
}
return timeText
}
/** A [BroadcastReceiver] to receive time tick, time change, and time zone change events. */
private class TimeBroadcastReceiver(
val onTimeChanged: () -> Unit,
val onTimeZoneChanged: () -> Unit
) : BroadcastReceiver() {
private var registered = false
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
onTimeZoneChanged()
} else {
onTimeChanged()
}
}
fun register(context: Context) {
if (!registered) {
val filter = IntentFilter()
filter.addAction(Intent.ACTION_TIME_TICK)
filter.addAction(Intent.ACTION_TIME_CHANGED)
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
context.registerReceiver(this, filter)
registered = true
}
}
fun unregister(context: Context) {
if (registered) {
context.unregisterReceiver(this)
registered = false
}
}
}
private fun formatTime(calendar: Calendar, currentTime: Long, timeFormat: String): String {
calendar.timeInMillis = currentTime
return DateFormat.format(timeFormat, calendar).toString()
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.materialcore
import android.provider.Settings
import android.text.format.DateFormat
import androidx.annotation.RestrictTo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun isLayoutDirectionRtl(): Boolean {
val layoutDirection: LayoutDirection = LocalLayoutDirection.current
return layoutDirection == LayoutDirection.Rtl
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun isRoundDevice(): Boolean {
val configuration = LocalConfiguration.current
return configuration.isScreenRound
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun isLeftyModeEnabled(): Boolean {
val context = LocalContext.current
return remember(context) {
Settings.System.getInt(
context.contentResolver,
Settings.System.USER_ROTATION,
android.view.Surface.ROTATION_0
) == android.view.Surface.ROTATION_180
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun is24HourFormat(): Boolean = DateFormat.is24HourFormat(LocalContext.current)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun currentTimeMillis(): Long = System.currentTimeMillis()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun screenHeightDp(): Int = LocalConfiguration.current.screenHeightDp
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun screenHeightPx(): Int =
with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.roundToPx() }
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun screenWidthDp(): Int = LocalConfiguration.current.screenWidthDp
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun isSmallScreen(): Boolean =
LocalConfiguration.current.screenWidthDp < LARGE_SCREEN_WIDTH_DP
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun isLargeScreen(): Boolean =
LocalConfiguration.current.screenWidthDp >= LARGE_SCREEN_WIDTH_DP
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public const val LARGE_SCREEN_WIDTH_DP: Int = 225