为了满足上述条件,需要表盘开发中做如下配置:
必须调用updateDecomposition来生成一个有效的WatchFaceDecomposition,否则系统被认为普通表盘。常用方法是
1.直接继承DecompositionWatchFaceService实现buildDecomposition方法。
2.也可自定义CanvasWatchFaceService,但必须实现一个可显示的WatchFaceDecomposition且通过updateDecomposition通知系统。
offload模式下表盘显示的字体、字符串都必须用"图片抠图"的形式呈现,例如想显示数字1,必须从字体资源(FontComponent)中扣出数字1的图片,字符串显示也是先转成drawable再通过BG绘制,因此涉及到表盘翻译的字符串,建议都用复杂数据显示,因为复杂数据本身包含drawable,home可将该drawable转成ImageComponent方便表盘展示。资源中图片格式必须是点阵图,且必须是RGB332,整个表盘最多能显示16种颜色,不支持扛锯齿,防烧屏机制在该模式下继续生效。
除了看AmbientService、SidekickService的log外,还可以直接看系统power wake状态,如:
# dumpsys power | grep -i wake
no_cached_wake_locks=true
mWakefulness=Dozing
mWakefulnessChanging=false
mWakeLockSummary=0x40
mLastWakeTime=45074118 (11793 ms ago)
mHoldingWakeLockSuspendBlocker=false
mWakeUpWhenPluggedOrUnpluggedConfig=true
mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig=false
mDrawWakeLockOverrideFromSidekick=true
mDoubleTapWakeEnabled=false
Wake Locks: size=1
DOZE_WAKE_LOCK 'DreamManagerService' ACQ=-1s396ms (uid=1000 pid=1752)
PowerManagerService.WakeLocks: ref count=0
mGravitySensor={Sensor name="gravity Non-wakeup", vendor="qualcomm", version=1, type=9, maxRange=1.0, resolution=0.1, power=0.515, minDelay=20000}
当SidekickService.mShouldControlDisplay为true时,该值也为true,保持屏幕处于状态STATE_DOZE_SUSPEND:The display is dozing in a suspended low power state; it is still on but the CPU is not updating it。
SidekickService会计算出buildDecomposition()返回的WatchFaceDecomposition中所用的Components,并解析每个Components
的显示区域,最后调用replaceWatchFaceComponents交给home更新。
03-09 17:18:45.329 1752 1911 I SidekickService: endDisplayLocked()
03-09 17:18:45.359 1752 1911 D SidekickService: ... endDisplay returns void
03-09 17:18:51.765 2390 4681 I SidekickManager: clearWatchFace called:
03-09 17:18:51.765 1752 1775 D SidekickService: resetLocked()
03-09 17:18:51.989 2390 4681 I SidekickManager: sendWatchFaceImpl(): watchFace = com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition@6673f0c forTWM = false
03-09 17:18:52.099 1752 1775 D SidekickService: sendWatchFaceComponents called: watchFace = com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition@468521shouldReplace = true
03-09 17:18:52.100 1752 1775 D SidekickService: sendWatchFaceLocked(): shouldReplace = true, mSidekickIsControlling = false, 8 images, 0 fonts, 0 numbers, 0 proportionalFonts, 0 strings
03-09 17:18:52.101 1752 1775 D SidekickService: ISidekickGraphics#beginResources()...
03-09 17:18:52.122 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.208 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.0, 0.0, 1.0, 1.0), drawableInfo = {.id = 2, .width = 338, .height = 412, .display = true, .offsetX = 0.0, .offsetY = 0.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 2, .displayInTwm = true}, image.size() = 1462
03-09 17:18:52.270 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 840
03-09 17:18:52.274 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.275 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.0, 0.0, 0.088757396, 0.07281554), drawableInfo = {.id = 10, .width = 30, .height = 30, .display = true, .offsetX = 0.0, .offsetY = 0.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 3, .displayInTwm = true}, image.size() = 131
03-09 17:18:52.279 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 75
03-09 17:18:52.279 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.281 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.9112426, 0.0, 1.0, 0.07281554), drawableInfo = {.id = 11, .width = 30, .height = 30, .display = true, .offsetX = 308.0, .offsetY = 0.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 3, .displayInTwm = true}, image.size() = 131
03-09 17:18:52.282 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 75
03-09 17:18:52.283 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.286 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.0, 0.92718446, 0.088757396, 1.0), drawableInfo = {.id = 12, .width = 30, .height = 30, .display = true, .offsetX = 0.0, .offsetY = 382.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 3, .displayInTwm = true}, image.size() = 131
03-09 17:18:52.289 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 75
03-09 17:18:52.290 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.291 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.9112426, 0.92718446, 1.0, 1.0), drawableInfo = {.id = 13, .width = 30, .height = 30, .display = true, .offsetX = 308.0, .offsetY = 382.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 3, .displayInTwm = true}, image.size() = 131
03-09 17:18:52.293 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 75
03-09 17:18:52.295 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.296 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.4704142, 0.47572815, 0.5295858, 0.52427185), drawableInfo = {.id = 14, .width = 20, .height = 20, .display = true, .offsetX = 159.0, .offsetY = 196.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 3, .displayInTwm = true}, image.size() = 143
03-09 17:18:52.298 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 60
03-09 17:18:52.299 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.303 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.46745563, 0.2669903, 0.5325444, 0.526699), drawableInfo = {.id = 15, .width = 22, .height = 107, .display = true, .offsetX = 158.0, .offsetY = 110.0, .rotationInfo = {.hasRotation = true, .pivotX = 11.0, .pivotY = 96.0, .degreesPerDay = 518400.0, .degreesPerStep = 6.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 60000}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = ROTATING, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 4, .displayInTwm = true}, image.size() = 241
03-09 17:18:52.306 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 2897
03-09 17:18:52.307 1752 1775 D SidekickServiceUtils: bitmapFromDrawable: Returning bitmap directly
03-09 17:18:52.309 1752 1775 D SidekickService: sendBitmapsLocked(): bounds = RectF(0.0, 0.0, 0.0027173914, 0.002232143), drawableInfo = {.id = 100000, .width = 1, .height = 1, .display = true, .offsetX = 0.0, .offsetY = 0.0, .rotationInfo = {.hasRotation = false, .pivotX = 0.0, .pivotY = 0.0, .degreesPerDay = 0.0, .degreesPerStep = 0.0, .zeroDegreesTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .transformInfo = {.transformed = false, .flipX = false, .flipY = false, .flip45 = false, .scaleX = 0.0, .scaleY = 0.0}, .type = GENERIC, .blink = {.blinking = false, .periodOnMs = 0.0, .periodOffMs = 0.0, .startTime = {.daysSinceLocalEpoch = 0, .msSinceMidnight = 0}}, .zOrder = 5, .displayInTwm = true}, image.size() = 84
03-09 17:18:52.310 1752 1775 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 18
03-09 17:18:52.310 1752 1775 D SidekickService: ISidekickGraphics#endResources()...
03-09 17:21:32.580 1752 2548 D SidekickService: sendBitmapsLocked(): status = 0, bytesUsed = 155
03-09 17:21:32.580 1752 2548 D SidekickService: ISidekickGraphics#endResources()...
03-09 17:21:32.812 2390 2390 D SidekickManagerAsync: replaceWatchFaceComponents(): Callback#onResult(): 0
03-09 17:21:32.819 1752 2548 D SidekickService: setShouldControlDisplay called: true
03-09 17:21:32.875 1752 1911 I SidekickService: beginDisplayLocked(): DOZE_SUSPEND
03-09 17:21:32.965 1752 1911 D SidekickService: ... beginDisplay result: 0 for halPower: 1
(standard input):17378:03-09 17:21:26.048 1752 19744 I AmbientService: [6e93f94] Showing watch face in offload mode, canceling alarm.
replaceWatchFaceComponents会对资源进行检查,如果资源格式不对,会有相关错误信息提示,如
03-09 17:32:01.810 2390 2390 D SidekickManagerAsync: sendWatchFace(): Callback#onResult(): 2
03-09 17:32:09.808 1752 1752 D SidekickService: setShouldControlDisplay called: false
03-09 17:32:09.861 2390 7074 I SidekickManager: replaceWatchFaceComponentsImpl(): watchFace = com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition@f4d00e1
03-09 17:32:09.862 1752 26357 D SidekickService: sendWatchFaceComponents called: watchFace = com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition@dfbff84shouldReplace = false
03-09 17:32:09.864 1752 26357 W SidekickService: Ignoring partial update to an invalid watchface
03-09 17:32:09.923 2390 2390 D SidekickManagerAsync: replaceWatchFaceComponents(): Callback#onResult(): 2
replaceWatchFaceComponents(): Callback#onResult(): 返回2,表示表盘无效。
进入offload模式下,具有以下显示特点。
1.表盘上只能显示原图(如果设置指针ImageComponent的矩形区域与原图不一致,显示也不会被放大/缩小),基于这个规律,当给imageComponent设置的RECF区域与原图不一致时,只有左上角的坐标有效。这在谷歌Wear Partner Doc_ Decomposable Watch Face API文档有介绍。
2.指针旋转轴永远是BG绘制区域的中心位置(BG绘制参考本文 五)。基于这个规律,Decomposable Watchface表盘开发时,必须将指针的"旋转点儿"固定到BG中心位置。
3.即便满足以上2点时,实测发现,指针在旋转过程中,"旋转点儿"的位置还是会发生肉眼可见的偏移。为降低这个问题,修改指针形状,在表盘中心位置固定一个圆形的ImageComponent circle,把circle放到时针和分针的上面,秒针放到circle上方。
4.因为offload模式不支持扛锯齿,不能显示矢量图,只能显示点阵图,不可避免会出现粗糙边缘,所以UI出图时,含有圆形、椭圆形的,尽可能把圆弧弧度渐变做的细腻一些。且尽量不要使用彩色,实测发现彩色可能会因为屏幕较暗时,颜色越鲜艳屏幕越容易闪。
5.offload模式下显示指针表盘时,无法扛锯齿,显示效果确实不好,谷歌说在未来版本或许能优化,但目前只能通过调整指针图片来改善显示效果。
四、手表Decomposable表盘产品需求。
因为Decomposable表盘能节约功耗,所以项目中希望能使用Decomposable表盘,但指针表盘显示效果不好,经与产品、设计沟通,更倾向于数字版的Decomposable表盘。数字版Decomposable表盘要考虑的问题。
1.需要显示的基础信息,基本确定要显示时 分、日期、星期天。
2.由于offload模式表盘只能显示ImageComponent、ComplicationComponent、FontComponent、NumberComponent。这些component均以drawable形式存在,考虑到手表国际化字符串翻译问题,只能通过ComplicationComponent形式来展示上述基础信息。
3.系统自带的时钟复杂数据很难满足我们UI要求。如果要满足产品要求,需要我们自定义复杂数据,分别显示时分,日期,星期天。手表要开发世界时钟,可以在这个app里增加复杂数据。
4.验证过我们自定义的复杂数据是能按预期刷新的(显示时间/日期/星期等时间TEXT的复杂数据,要
使用ComplicationText.TimeFormatBuilder(),生成一个TimeDependentText)。
五、BG绘制区域。
手表进入ambient时,为了防止屏幕中某区域常亮导致屏幕出现坏块,高通提供了"防烧屏"机制,该机制的原理是在原显示屏中显示一块较小的区域,在1~2分钟的时间内移动该区域。显示区域大小可调,但具体调整规则目前不清楚,只知道
2*(10+2-DISPLAY_BURNIN_PIXEL_WIDTH*2+2*DISPLAY_BURNIN_PIXEL_HEIGHT不能大于255,其中DISPLAY_BURNIN_PIXEL_WIDTH和DISPLAY_BURNIN_PIXEL_HEIGHT分别表示防烧屏时减去的宽度、高度。
例如原屏幕分辨率368*448,DISPLAY_BURNIN_PIXEL_WIDTH=30,DISPLAY_BURNIN_PIXEL_HEIGHT=36.那么在
ambient模式下(使能防烧屏)实际显示的分辨率是338*412。
logcat.01:1044:03-09 19:10:10.950033 1691 1732 D SidekickService: SidekickService#getCapabilities()
logcat.01:1067:03-09 19:10:11.002619 413 1733 D SKGHAL : getCapabilities: Capabilities returned
logcat.01:1068:03-09 19:10:11.002643 413 1733 D SKGHAL : getCapabilities: RGB bits = [3,3,2]
logcat.01:1069:03-09 19:10:11.002663 413 1733 D SKGHAL : getCapabilities: paletteSize = 16
logcat.01:1070:03-09 19:10:11.002680 413 1733 D SKGHAL : getCapabilities: operations = 1102000b
logcat.01:1071:03-09 19:10:11.002698 413 1733 D SKGHAL : getCapabilities: availMemory = 49800
logcat.01:1072:03-09 19:10:11.002718 413 1733 D SKGHAL : getCapabilities: (x, y) = (338, 412)
logcat.01:1074:03-09 19:10:11.004113 1691 1732 D SidekickService: SidekickService#getCapabilities(): {.capabilities = 285343755, .displaySizeX = 338, .displaySizeY = 412}
在只保留基本运动传感器功能,未开启运动模式/GPS/WIFI/心率灯等模块时,bg绘制表盘相对ap绘制,功耗大约能节约10%。