1、W5表盘新变化
-
官网Wear WatchFace页面,Google添加了新的提示,在WearOS
5.0及以后的版本中表盘必须使用表盘格式才能预装在新手表上。 -
另外,Google与Samsung共同提供了不编写代码设计表盘的工具:https://developer.samsung.com/watch-face-studio/user-guide/index.html
里面详细描述了无代码设计表盘的基本流程,相比于编写代码,大大提高了制作表盘的效率。
2、表盘基本架构
2.1、代码编写模式下
- 在代码编写环境下制作表盘,主要涉及到两个类:WatchFaceService、WatchFaceRender。其中WatchFaceService是表盘的入口,官方代码demo中,在AndroidManifest.xml中配置WatchFaceService:
ps:每一个新建的表盘都需要在AndroidManifest.xml中配置。
<service
android:name=".AnalogWatchFaceService"
android:directBootAware="true"
android:exported="true"
android:label="@string/analog_watch_face_name"
android:permission="android.permission.BIND_WALLPAPER">
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/watch_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/watch_preview" />
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
<meta-data
android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
android:value="true" />
</service>
- 其中在标签中添加当前表盘的预览图和wallpaper,预览图用于在表盘选择界面、表盘编辑界面以及变盘添加界面的展示;wallpaper中设置watch_face.xml,这个配置文件官方demo中没有具体的代码体现,但是在此文件中可以配置宽、高、屏幕形状等等属性。
<WatchFace width="positive-integer" height="positive-integer"
clipShape="NONE | CIRCLE | RECTANGLE" cornerRadiusX="float"
cornerRadiusY="float">
<!-- Only the required inner element is shown here. -->
<Scene>
</Scene>
</WatchFace>
- 另外,可以在WatchFace中自定义位图的字体: name表示字词或字符本身。 resource表示字符或字词的资源ID。
<BitmapFonts>
<BitmapFont name="string">
<!-- Only the most common inner element is shown here. -->
<Character name="string" resource="string"
width="positive-integer" height="positive-integer" />
...
</BitmapFont>
</BitmapFonts>
设置元数据,主要以键值对的形式:
其中key的定义有三类:
- PREVIEW_TIME:设置表盘预览中显示的时间。此值必须采用 HH:MM:SS 格式;例如,22:10:00 表示晚上 10:10。如果此值无效或未指定,则表盘会使用系统的默认时间。
- CLOCK_TYPE:指定主要表盘类型(DIGITAL(数字模式) 或 ANALOG(指针模式))。即使表盘支持这两种类型,您也必须指定要在表盘预览中显示的主要类型。如果此值无效或未指定,则系统会使用默认值ANALOG。
- STEP_GOAL:设置表盘预览中显示的每日“步数”目标。此值必须是正整数。如果此值无效或未指定,则表盘会使用系统的默认每日步数目标。
<Metadata key="string" value="string" />
- 每一个自定的WatchFaceService类都需要继承自WatchFaceService这个抽象的父类,需要实现createWatchFace方法来实现表盘的绘制,这个方法需要返回WatchFace对象,需要携带watchFaceType和renderer两个参数。
override suspend fun createWatchFace(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository
): WatchFace {
Log.d(TAG, "createWatchFace()")
// Creates class that renders the watch face.
val renderer = AnalogWatchCanvasRenderer(
context = applicationContext,
surfaceHolder = surfaceHolder,
watchState = watchState,
complicationSlotsManager = complicationSlotsManager,
currentUserStyleRepository = currentUserStyleRepository,
canvasType = CanvasType.HARDWARE
)
// Creates the watch face.
return WatchFace(
watchFaceType = WatchFaceType.ANALOG,
renderer = renderer
)
}
- Renderer类主要包括绘制表盘以及处理表盘数据的逻辑,在init机构体中添加对于StateFlow类型数据userStyle的监听,当数据发生变化时,则执行对应的操作,比如:数字表盘时间变化,指针表盘重新绘制指针、运动健康数据更新展示等操作。
init {
scope.launch {
currentUserStyleRepository.userStyle.collect { userStyle ->
updateWatchFaceData(userStyle)
}
}
}
private fun updateWatchFaceData(userStyle: UserStyle) {
Log.d(TAG, "updateWatchFace(): $userStyle")
var newWatchFaceData: WatchFaceData = watchFaceData
// Loops through user style and applies new values to watchFaceData.
for (options in userStyle) {
when (options.key.id.toString()) {
COLOR_STYLE_SETTING -> {
val listOption = options.value as
UserStyleSetting.ListUserStyleSetting.ListOption
newWatchFaceData = newWatchFaceData.copy(
activeColorStyle = ColorStyleIdAndResourceIds.getColorStyleConfig(
listOption.id.toString()
)
)
}
DRAW_HOUR_PIPS_STYLE_SETTING -> {
val booleanValue = options.value as
UserStyleSetting.BooleanUserStyleSetting.BooleanOption
newWatchFaceData = newWatchFaceData.copy(
drawHourPips = booleanValue.value
)
}
WATCH_HAND_LENGTH_STYLE_SETTING -> {
val doubleValue = options.value as
UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
// The arm lengths are usually only calculated the first time the watch face is
// loaded to reduce the ops in the onDraw(). Because we updated the minute hand
// watch length, we need to trigger a recalculation.
armLengthChangedRecalculateClockHands = true
// Updates length of minute hand based on edits from user.
val newMinuteHandDimensions = newWatchFaceData.minuteHandDimensions.copy(
lengthFraction = doubleValue.value.toFloat()
)
newWatchFaceData = newWatchFaceData.copy(
minuteHandDimensions = newMinuteHandDimensions
)
}
}
}
// Only updates if something changed.
if (watchFaceData != newWatchFaceData) {
watchFaceData = newWatchFaceData
// Recreates Color and ComplicationDrawable from resource ids.
watchFaceColors = convertToWatchFaceColorPalette(
context,
watchFaceData.activeColorStyle,
watchFaceData.ambientColorStyle
)
// Applies the user chosen complication color scheme changes. ComplicationDrawables for
// each of the styles are defined in XML so we need to replace the complication's
// drawables.
for ((_, complication) in complicationSlotsManager.complicationSlots) {
ComplicationDrawable.getDrawable(
context,
watchFaceColors.complicationStyleDrawableId
)?.let {
(complication.renderer as CanvasComplicationDrawable).drawable = it
}
}
}
}
- 对于表盘数据项的样式以及对应图标的绘制,我们可以在drawComplications这个方法里编写对应的逻辑,通过complication.complicationData.value来判断更新的数据类型以及对此数据类型要做的操作(更新数据展示、进度条变化等等)。当然,一个表盘上所有的数据项不可能都是同一个数据类型,所以要通过在ComplicationUtils中配置的数据项id来进行对应位置、对应数据类型的操作。
private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) {
for ((_, complication) in complicationSlotsManager.complicationSlots) {
if (complication.enabled) {
complication.render(canvas, zonedDateTime, renderParameters)
when (complication.complicationData.value) {
is MonochromaticImageComplicationData -> {
when (complication.id) {
/**
* complication.id在ComplicationUtils中定义
* 每个id对应一个数据项,分布于表盘的任意坐标上(需要自己设置数据项的范围和坐标)
*/
}
}
is NoDataComplicationData -> {}
is ShortTextComplicationData -> {}
is LongTextComplicationData -> {}
is WeightedElementsComplicationData -> {}
is SmallImageComplicationData -> {}
is PhotoImageComplicationData -> {}
is NoPermissionComplicationData -> {}
is GoalProgressComplicationData -> {}
is RangedValueComplicationData -> {}
else -> {}
}
}
}
}
- 上面说到了ComplicationUtils,在WatchFaceService中调用了createComplicationSlotManager方法,来获取ComplicationSlotsManager对象,它就是用于drawComplications方法中表盘数据项的遍历,并进行表盘数据项相关操作的重要对象。
override fun createComplicationSlotsManager(
currentUserStyleRepository: CurrentUserStyleRepository
): ComplicationSlotsManager = createComplicationSlotManager(
context = applicationContext,
currentUserStyleRepository = currentUserStyleRepository
)
fun createComplicationSlotManager(
context: Context,
currentUserStyleRepository: CurrentUserStyleRepository,
drawableId: Int = DEFAULT_COMPLICATION_STYLE_DRAWABLE_ID
): ComplicationSlotsManager {
val defaultCanvasComplicationFactory =
CanvasComplicationFactory { watchState, listener ->
CanvasComplicationDrawable(
ComplicationDrawable.getDrawable(context, drawableId)!!,
watchState,
listener
)
}
val leftComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
id = ComplicationConfig.Left.id,
canvasComplicationFactory = defaultCanvasComplicationFactory,
supportedTypes = ComplicationConfig.Left.supportedTypes,
defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy(
SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
ComplicationType.SHORT_TEXT
),
bounds = ComplicationSlotBounds(
RectF(
LEFT_COMPLICATION_LEFT_BOUND,
LEFT_AND_RIGHT_COMPLICATIONS_TOP_BOUND,
LEFT_COMPLICATION_RIGHT_BOUND,
LEFT_AND_RIGHT_COMPLICATIONS_BOTTOM_BOUND
)
)
)
.build()
val rightComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
id = ComplicationConfig.Right.id,
canvasComplicationFactory = defaultCanvasComplicationFactory,
supportedTypes = ComplicationConfig.Right.supportedTypes,
defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy(
SystemDataSources.DATA_SOURCE_WATCH_BATTERY,
ComplicationType.SHORT_TEXT
),
bounds = ComplicationSlotBounds(
RectF(
RIGHT_COMPLICATION_LEFT_BOUND,
LEFT_AND_RIGHT_COMPLICATIONS_TOP_BOUND,
RIGHT_COMPLICATION_RIGHT_BOUND,
LEFT_AND_RIGHT_COMPLICATIONS_BOTTOM_BOUND
)
)
).build()
return ComplicationSlotsManager(
listOf(leftComplication, rightComplication),
currentUserStyleRepository
)
}
- 到这里WearOS WatchFace的主要类和方法就讲完了,其他次要的类包括WatchFaceConfigActivity、WatchFaceColorPalette。
- 其中WatchFaceConfigActivity中可以自己定义表盘编辑界面的样式风格、添加更改样式的操作等等。
- WatchFaceColorPalette类中的convertToWatchFaceColorPalette方法可以设置在进入AOD息屏后的背景图片、表盘数据项的颜色等等。
fun convertToWatchFaceColorPalette(
context: Context,
activeColorStyle: ColorStyleIdAndResourceIds,
ambientColorStyle: ColorStyleIdAndResourceIds
): WatchFaceColorPalette {
return WatchFaceColorPalette(
// Active colors
activePrimaryColor = context.getColor(activeColorStyle.primaryColorId),
activeSecondaryColor = context.getColor(activeColorStyle.secondaryColorId),
activeBackgroundColor = context.getColor(activeColorStyle.backgroundColorId),
activeOuterElementColor = context.getColor(activeColorStyle.outerElementColorId),
// Complication color style
complicationStyleDrawableId = activeColorStyle.complicationStyleDrawableId,
// Ambient colors
ambientPrimaryColor = context.getColor(ambientColorStyle.primaryColorId),
ambientSecondaryColor = context.getColor(ambientColorStyle.secondaryColorId),
ambientBackgroundColor = context.getColor(ambientColorStyle.backgroundColorId),
ambientOuterElementColor = context.getColor(ambientColorStyle.outerElementColorId)
)
}
2.2、使用Samsung工具
- 详细操作参考官方文档:user-guide
- 下载链接:download
3、总结
- WearOS 5.0上表盘模块的更新不大,最主要是增加了Samsung的绘制工具,大大减少了工作量,但是还是要从代码层面去理解表盘绘制以及数据展示的逻辑。