WearOS Offload模式下的錶盤開發

  WearOS手錶offload模式下的錶盤渲染,是通過BG繪製的。

一.錶盤進入offload的條件是手錶處於微光模式且錶盤是Decomposable的。

爲了滿足上述條件,需要錶盤開發中做如下配置:

  a.manifest中爲DecompositionWatchFaceService增加mera-data

        <meta-data
                android:name="com.google.android.wearable.watchface.decomposable"
                android:value="true" />

b.updateDecomposition。

     必須調用updateDecomposition來生成一個有效的WatchFaceDecomposition,否則系統被認爲普通錶盤。常用方法是

    1.直接繼承DecompositionWatchFaceService實現buildDecomposition方法。

   2.也可自定義CanvasWatchFaceService,但必須實現一個可顯示的WatchFaceDecomposition且通過updateDecomposition通知系統。

c.資源要求。

    offload模式下表盤顯示的字體、字符串都必須用"圖片摳圖"的形式呈現,例如想顯示數字1,必須從字體資源(FontComponent)中扣出數字1的圖片,字符串顯示也是先轉成drawable再通過BG繪製,因此涉及到錶盤翻譯的字符串,建議都用複雜數據顯示,因爲複雜數據本身包含drawable,home可將該drawable轉成ImageComponent方便錶盤展示。資源中圖片格式必須是點陣圖,且必須是RGB332,整個錶盤最多能顯示16種顏色,不支持扛鋸齒,防燒屏機制在該模式下繼續生效。

二.如何確認手錶是否成功進入offload模式。

除了看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,表示錶盤無效。

三.Decomposable指針錶盤的顯示問題 。

   進入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.驗證過我們自定義的複雜數據是能按預期刷新的。

五、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%。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章