【WearOS】【WatchFace】基于WearOS的表盘绘制相关逻辑

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的定义有三类:

  1. PREVIEW_TIME:设置表盘预览中显示的时间。此值必须采用 HH:MM:SS 格式;例如,22:10:00 表示晚上 10:10。如果此值无效或未指定,则表盘会使用系统的默认时间。
  2. CLOCK_TYPE:指定主要表盘类型(DIGITAL(数字模式) 或 ANALOG(指针模式))。即使表盘支持这两种类型,您也必须指定要在表盘预览中显示的主要类型。如果此值无效或未指定,则系统会使用默认值ANALOG。
  3. 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工具

3、总结

  • WearOS 5.0上表盘模块的更新不大,最主要是增加了Samsung的绘制工具,大大减少了工作量,但是还是要从代码层面去理解表盘绘制以及数据展示的逻辑。
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Wear OS是由Google开发的智能手表操作系统。它提供了许多功能和应用程序,让用户能够更方便地使用他们的智能手表。 首先,要使用Wear OS,您需要将其安装在您的智能手表上。许多智能手表默认都预装了此操作系统,但如果没有,您可以从Google Play商店中下载并安装它。 安装完成后,您需要将手表与您的手机进行配对。打开手表上的蓝牙设置,并在您的手机上搜索新设备。找到您的手表后,将其与手机进行配对。一旦配对成功,您的手表就可以与手机同步,并开始接收通知、查看日历、控制音乐等功能。 接下来,您可以开始个性化设置您的手表。通过访问手表的设置菜单,您可以更改手表的表盘、背景、字体大小等。您还可以调整手表的通知设置,选择您想要在手表上显示的通知类型。 Wear OS还支持许多应用程序,包括Google Fit健身追踪、Google Maps导航、Google Pay支付等。您可以在手表上打开Google Play商店,浏览并安装您感兴趣的应用程序。 最后,Wear OS还有一些手势和快捷方式,可以帮助您更方便地操作手表。例如,您可以通过在手表面板上向左滑动来访问快速设置,如亮度调整、飞行模式等。通过向上滑动,您可以查看手表的通知历史记录。 总体而言,Wear OS是一个功能强大的智能手表操作系统,提供了许多定制选项和方便功能。通过理解操作系统的不同功能和设置,您可以最大限度地发挥手表的潜力,并让它更好地满足您的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JD_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值