Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[DroidKaigi 2018] Android WearのWatch Faceを作ろう 〜時計の盤面に小さな情報を添えて〜

Da5a59469ce3ebb55619ce34f85f8c4f?s=47 syarihu
February 09, 2018

[DroidKaigi 2018] Android WearのWatch Faceを作ろう 〜時計の盤面に小さな情報を添えて〜

「Android WearのWatch Faceを作ろう 〜時計の盤面に小さな情報を添えて〜」の発表資料です。

Da5a59469ce3ebb55619ce34f85f8c4f?s=128

syarihu

February 09, 2018
Tweet

Transcript

  1. Android WearのWatch Faceを作ろう 〜時計の盤面に小さな情報を添えて〜 DroidKaigi 2018 2018/02/09 (Fri.) Taichi Sato

    (@syarihu)
  2. 今回話すこと • デジタル時計を例にしたWatch Faceの作り方 • Watch FaceへのComplications 対応 • Complication

    Data Providerの 作り方
  3. 最終的に作れるもの

  4. 最終的に作れるもの Complications

  5. 自己紹介

  6. Taichi Sato (@syarihu) • MoneyForward, Inc. ◦ Android Engineer •

    TechBooster
  7. TechBooster C92 技術書典3 C93

  8. Watch Faceとは

  9. Watch Faceとは • Android Wearでは 時計の盤面を変更できる • その時計の盤面のことをWatch Faceと呼ぶ

  10. Watch Faceとは

  11. 以前のWatch Face

  12. 以前のWatch Face • WatchViewStubというクラスを 使って作る非公式の Watch Faceが多くあった WatchViewStub | Android

    Developers https://developer.android.com/reference/android/support/wearable/view/WatchViewStub.html
  13. 以前のWatch Face

  14. 以前のWatch Face

  15. 以前のWatch Face WatchViewStub | Android Developers https://developer.android.com/reference/android/support/wearable/view/WatchViewStub.html

  16. 以前のWatch Face • 非公式のWatch Faceが増加 する中、ようやく公式のWatch Face APIが公開される

  17. Watch Face APIが公開される Android Developers Blog: Watch Face API Now

    Available for Android Wear https://android-developers.googleblog.com/2014/12/watch-face-api-now-available-for.html
  18. Android Developers Blog: Watch Face API Now Available for Android

    Wear https://android-developers.googleblog.com/2014/12/watch-face-api-now-available-for.html Watch Face APIが公開される
  19. Watch Face APIが公開される • 2015年1月31日までに公式の Watch Face APIに移行しないと 削除するよというアナウンスがさ れる

  20. Watch Face APIが公開される • 2015年1月31日までに公式の Watch Face APIに移行しないと 削除するよというアナウンスがさ れる

    • 非公式のWatch Faceが終わり を迎えた
  21. Watch Face API

  22. Watch Face API • Gles2WatchFaceService • CanvasWatchFaceService

  23. Watch Face API • Gles2WatchFaceService • CanvasWatchFaceService

  24. Watch Faceを作る

  25. 今回作るWatch Face • 普通のデジタル時計 • アンビエントモードに 変わった時に背景色を変える

  26. アンビエントモード • Android Wearの画面をタップし てからしばらくすると暗くなる • その暗くなった状態を アンビエントモードという

  27. プロジェクトを作成

  28. プロジェクトを作成

  29. プロジェクトを作成

  30. class DigitalWatchFaceService : CanvasWatchFaceService() { override fun onCreateEngine(): Engine {

    return Engine() } WatchFaceServiceの作成 (DigitalWatchFaceService.kt)
  31. inner class Engine : CanvasWatchFaceService.Engine() { override fun onCreate(holder: SurfaceHolder?)

    { super.onCreate(holder) } override fun onAmbientModeChanged(inAmbientMode: Boolean) { super.onAmbientModeChanged(inAmbientMode) } override fun onTimeTick() { super.onTimeTick() } override fun onDraw(canvas: Canvas, bounds: Rect) { super.onDraw(canvas, bounds) } } WatchFaceServiceの作成 (DigitalWatchFaceService.kt)
  32. inner class Engine : CanvasWatchFaceService.Engine() { // 時間を取得するためのCalendar private val

    calendar: Calendar = Calendar.getInstance() // 時計の背景色 private var backgroundColor = Color.BLACK 初期化処理 (DigitalWatchFaceService.kt)
  33. … // 時間を描画するためのPaint private val timePaint: Paint = Paint().apply {

    color = Color.WHITE textSize = 75f isAntiAlias = true } // 日付を描画するためのPaint private val datePaint: Paint = Paint().apply { color = Color.WHITE textSize = 20f isAntiAlias = true } 初期化処理 (DigitalWatchFaceService.kt)
  34. … // 時間の位置を保持するためのPoint private val timePosition: Point = Point() //

    日付の位置を保持するためのPoint private val datePosition: Point = Point() // 時間の文字の大きさを保持するためのRect private val timeBounds: Rect = Rect() // 日付の文字の大きさを保持するためのRect private val dateBounds: Rect = Rect() 初期化処理 (DigitalWatchFaceService.kt)
  35. inner class Engine : CanvasWatchFaceService.Engine() { ... override fun onDraw(canvas:

    Canvas, bounds: Rect) { super.onDraw(canvas, bounds) val now = System.currentTimeMillis() // 時間を更新 calendar.timeInMillis = now drawWatchFace(canvas) } 時計の描画 (DigitalWatchFaceService.kt)
  36. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { // 時間 val strTime = DateUtils.formatDateTime( this@DigitalWatchFaceService, calendar.timeInMillis, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_24HOUR ) 時計の描画 (DigitalWatchFaceService.kt)
  37. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... // 時間のテキストの大きさを再取得 timePaint.getTextBounds( strTime, 0, strTime.length, timeBounds) // 中央に配置するための座標を計算する timePosition.set( canvas.width / 2 - timeBounds.width() / 2, canvas.height / 2 ) 時計の描画 (DigitalWatchFaceService.kt)
  38. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... // 日付 val strDate = DateUtils.formatDateTime( this@DigitalWatchFaceService, calendar.timeInMillis, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY ) 時計の描画 (DigitalWatchFaceService.kt)
  39. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... // 日付のテキストの大きさを取得 datePaint.getTextBounds( strDate, 0, strDate.length, dateBounds) datePosition.set( canvas.width / 2 - dateBounds.width() / 2, canvas.height / 2 + timeBounds.height() / 2 + (10 / resources.displayMetrics.density).toInt() ) 時計の描画 (DigitalWatchFaceService.kt)
  40. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... // 背景色、日時を描画 canvas.drawColor(backgroundColor) canvas.drawText( strTime, timePosition.x.toFloat(), timePosition.y.toFloat(), timePaint ) 時計の描画 (DigitalWatchFaceService.kt)
  41. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... canvas.drawText( strDate, datePosition.x.toFloat(), datePosition.y.toFloat(), datePaint ) } 時計の描画 (DigitalWatchFaceService.kt)
  42. inner class Engine : CanvasWatchFaceService.Engine() { ... override fun onAmbientModeChanged(

    inAmbientMode: Boolean) { super.onAmbientModeChanged(inAmbientMode) backgroundColor = if (inAmbientMode) Color.BLACK else applicationContext.getColor( android.R.color.holo_blue_light ) invalidate() } アンビエントモード (DigitalWatchFaceService.kt)
  43. 時間が変わったときの処理 • 時間が変わったときには、 onTimeTickというメソッドが呼 ばれる

  44. onTimeTickが呼ばれる条件 • アンビエントモードとインタラク ティブモードで少なくとも1分に1 回

  45. onTimeTickが呼ばれる条件 • 日付または時刻が変わった とき • タイムゾーンが変わったとき

  46. inner class Engine : CanvasWatchFaceService.Engine() { ... override fun onTimeTick()

    { super.onTimeTick() // 時間が変わったら再描画 invalidate() } 時間が変わったときの処理 (DigitalWatchFaceService.kt)
  47. AndroidManifest.xml <uses-feature android:name="android.hardware.type.watch" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

  48. AndroidManifest.xml <service android:name=".DigitalWatchFaceService" android:allowEmbedded="true" android:label="DroidKaigi WatchFaceSample" android:permission="android.permission.BIND_WALLPAPER" android:taskAffinity=""> </service>

  49. AndroidManifest.xml <service ...> <meta-data android:name="android.service.wallpaper" android:resource="@xml/watch_face" /> ... </service>

  50. watch_face.xml <?xml version="1.0" encoding="UTF-8"?> <wallpaper />

  51. AndroidManifest.xml <service ...> ... <meta-data android:name="com.google.android.wearable.watchface.preview" android:resource="@drawable/preview" /> <meta-data android:name="com.google.android.wearable.watchface.preview_circular"

    android:resource="@drawable/preview_circular" /> ... </service>
  52. AndroidManifest.xml <service ...> ... <intent-filter> <action android:name="android.service.wallpaper.WallpaperService" /> <category android:name=

    "com.google.android.wearable.watchface.category.WATCH_FACE" /> </intent-filter> </service>
  53. 実行する

  54. インタラクティブモード

  55. アンビエントモード

  56. Complications API

  57. Complicationsとは • 時間に加えて表示されるWatch Faceの機能のこと • Android Wear 2.0から使える

  58. Complicationsとは

  59. Complication Data Providers • Watch Faceにデータを提供 するアプリ

  60. Complication Data Providers Watch Face Complications | Android Developers https://developer.android.com/training/wearables/watch-faces/complications.html

  61. System Providers • 日付 • 時間 • 歩数 • アプリ

    • 未読通知数 • 世界時計 • 次の予定 • バッテリー残量
  62. Complications Types • Complicationに表示する データの種類

  63. Complications Types • ICON • SHORT_TEXT • LONG_TEXT • SMALL_IMAGE

    • LARGE_IMAGE • RANGED_VALUE
  64. Complications Types Adding Complications to a Watch Face | Android

    Developers https://developer.android.com/training/wearables/watch-faces/adding-complications.html
  65. Complications Types Type 必須フィールド オプション SHORT_TEXT Short text Icon Burn-in

    protection icon Short title ICON Icon Burn-in protection icon RANGED_VALUE Value Min value Max value Icon Burn-in protection icon Short text Short title
  66. Complications Types Type 必須フィールド オプション LONG_TEXT Long text Long title

    Icon Burn-in protection icon Small image SMALL_IMAGE Small image LARGE_IMAGE Large image
  67. Watch Faceに Complicationを 追加する

  68. Complicationを追加する • Complicationを表示するために は、Watch Faceがそれに 対応している必要がある

  69. Complicationを追加する • Watch FaceにComplication を配置 • Complicationsを設定するため の設定画面

  70. 今回作るもの

  71. 今回作るもの

  72. enum class ComplicationLocation( val complicationId: Int, val complicationSupportedTypes: IntArray )

    { LEFT(101, intArrayOf( ComplicationData.TYPE_RANGED_VALUE, ComplicationData.TYPE_ICON, ComplicationData.TYPE_SHORT_TEXT, ComplicationData.TYPE_SMALL_IMAGE )), Complicationsの設定を決める (ComplicationLocation.kt)
  73. ... RIGHT(102, intArrayOf( ComplicationData.TYPE_RANGED_VALUE, ComplicationData.TYPE_ICON, ComplicationData.TYPE_SHORT_TEXT, ComplicationData.TYPE_SMALL_IMAGE )); Complicationsの設定を決める (ComplicationLocation.kt)

  74. ... companion object { fun getComplicationIds(): IntArray = values().map {

    it.complicationId }.toIntArray() fun valueOf( complicationId: Int ): ComplicationLocation? = values().firstOrNull { it.complicationId == complicationId } } } Complicationsの設定を決める (ComplicationLocation.kt)
  75. 設定画面のレイアウトを作る (activity_complications_config.xml) <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"

    android:background="@color/black" android:paddingEnd="24dp" android:paddingStart="24dp"> <android.support.wear.widget.WearableRecyclerView android:id="@+id/wearable_recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="true" android:padding="20dp" /> </FrameLayout> </layout>
  76. <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <net.syarihu.android.watchfacesample.ComplicationsConfigPreviewView

    android:id="@+id/complication_settings_preview" android:layout_width="@dimen/complication_settings_preview_size" android:layout_height="@dimen/complication_settings_preview_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> 設定画面のレイアウトを作る (viewholder_preview_and_complications.xml)
  77. class ComplicationsConfigPreviewView : View { ... constructor(context: Context?) : super(context)

    constructor( context: Context?, attrs: AttributeSet? ) : super(context, attrs) constructor( context: Context?, attrs: AttributeSet?, defStyleAttr: Int ) : super(context, attrs, defStyleAttr) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) 設定画面のレイアウトを作る (ComplicationsConfigPreviewView.kt)
  78. <android.support.constraint.ConstraintLayout ...> ... <android.support.constraint.Guideline android:id="@+id/guideline_center_vertical" android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_percent="0.5" />

    設定画面のレイアウトを作る (viewholder_preview_and_complications.xml)
  79. <ImageView android:id="@+id/left_complication_background" style="?android:borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" android:importantForAccessibility="no" android:src="@drawable/added_complication" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toLeftOf="@+id/guideline_center_vertical"

    /> <ImageButton android:id="@+id/left_complication" style="?android:borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toLeftOf="@+id/guideline_center_vertical" /> 設定画面のレイアウトを作る (viewholder_preview_and_complications.xml)
  80. 設定画面のレイアウトを作る (viewholder_preview_and_complications.xml) <ImageView android:id="@+id/right_complication_background" style="?android:borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" android:importantForAccessibility="no" android:src="@drawable/added_complication"

    app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@+id/guideline_center_vertical" /> <ImageButton android:id="@+id/right_complication" style="?android:borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@+id/guideline_center_vertical" />
  81. プレビュー画面の初期化 (PreviewAndComplicationsViewHolder.kt) class PreviewAndComplicationsViewHolder( context: Context, parent: ViewGroup? ) :

    RecyclerView.ViewHolder( ViewholderPreviewAndComplicationsBinding.inflate( LayoutInflater.from(context), parent, false ).root ) { private val binding: ViewholderPreviewAndComplicationsBinding = DataBindingUtil.bind(itemView)
  82. プレビュー画面の初期化 (PreviewAndComplicationsViewHolder.kt) fun bind( context: Context, complicationClickListener: (complicationLocation: ComplicationLocation) ->Unit

    ) { val defaultDrawable = context.getDrawable(R.drawable.add_complication) binding.run { leftComplication.run { setImageDrawable(defaultDrawable) setOnClickListener { view -> onClickComplication(view, complicationClickListener) } } leftComplicationBackground.visibility = View.INVISIBLE
  83. プレビュー画面の初期化 (PreviewAndComplicationsViewHolder.kt) rightComplication.run { setImageDrawable(defaultDrawable) setOnClickListener { view -> onClickComplication(view,

    complicationClickListener) } } rightComplicationBackground.visibility = View.INVISIBLE } }
  84. プレビュー画面の初期化 (PreviewAndComplicationsViewHolder.kt) private fun onClickComplication( view: View, complicationClickListener: (complicationLocation: ComplicationLocation)

    -> Unit ) { val complicationLocation: ComplicationLocation? = when (view.id) { R.id.left_complication -> ComplicationLocation.LEFT R.id.right_complication -> ComplicationLocation.RIGHT else -> null } complicationLocation?.let { complicationClickListener(it) launchComplicationHelperActivity(view.context, it) } }
  85. プレビュー画面の初期化 (ComplicationsConfigRecyclerViewAdapter.kt) override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int )

    { if (holder is PreviewAndComplicationsViewHolder) { holder.bind(context, { selectedComplicationLocation = it })
  86. Complication選択画面 (PreviewAndComplicationsViewHolder.kt) private fun launchComplicationHelperActivity( context: Context, complicationLocation: ComplicationLocation )

    { val activity = context as? Activity ?: return // Complicationを表示するWatchFace val watchFace = ComponentName( activity, DigitalWatchFaceService::class.java)
  87. Complication選択画面 (PreviewAndComplicationsViewHolder.kt) // Complication選択画面へのIntent val intent = ComplicationHelperActivity.createProviderChooserHelperIntent( activity, watchFace,

    complicationLocation.complicationId, *complicationLocation.complicationSupportedTypes ) activity.startActivityForResult( intent, ComplicationsConfigActivity.COMPLICATION_CONFIG_REQUEST_CODE ) }
  88. Complication選択画面 (ComplicationsConfigActivity.kt) override fun onActivityResult( requestCode: Int, resultCode: Int, data:

    Intent? ) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == COMPLICATION_CONFIG_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { complicationsConfigRecyclerViewAdapter?.updateSelectedComplication( data.getParcelableExtra( ProviderChooserIntent.EXTRA_PROVIDER_INFO )) }
  89. ComplicationをUIに反映 (ComplicationsConfigRecyclerViewAdapter.kt) fun updateSelectedComplication( complicationProviderInfo: ComplicationProviderInfo? ) { val complicationLocation

    = selectedComplicationLocation ?: return updateComplicationView( complicationProviderInfo, complicationLocation ) }
  90. ComplicationをUIに反映 (ComplicationsConfigRecyclerViewAdapter.kt) fun updateComplicationView( complicationProviderInfo: ComplicationProviderInfo?, complicationLocation: ComplicationLocation ) {

    previewAndComplicationsViewHolder?.updateComplicationViews( complicationProviderInfo, complicationLocation ) }
  91. ComplicationをUIに反映 (PreviewAndComplicationsViewHolder.kt) fun updateComplicationViews( complicationProviderInfo: ComplicationProviderInfo?, complicationLocation: ComplicationLocation ) {

    val button: ImageButton val background: ImageView when (complicationLocation) { ComplicationLocation.LEFT -> { button = binding.leftComplication background = binding.leftComplicationBackground } ComplicationLocation.RIGHT -> { button = binding.rightComplication background = binding.rightComplicationBackground } }
  92. ComplicationをUIに反映 (PreviewAndComplicationsViewHolder.kt) // データが入っていなければ「+」アイコンをセットする if (complicationProviderInfo == null) { button.setImageDrawable(

    button.context.getDrawable(R.drawable.add_complication)) background.visibility = View.INVISIBLE return } // データが入っていればProviderIconをセットする button.setImageIcon(complicationProviderInfo.providerIcon) background.visibility = View.VISIBLE }
  93. 起動時にComplicationを取得 (ComplicationsConfigRecyclerViewAdapter.kt) class ComplicationsConfigRecyclerViewAdapter( private val context: Context ) :

    RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val providerInfoRetriever: ProviderInfoRetriever = ProviderInfoRetriever( context.applicationContext, Executors.newCachedThreadPool() ).apply { init() }
  94. 起動時にComplicationを取得 (ComplicationsConfigRecyclerViewAdapter.kt) override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int )

    { if (holder is PreviewAndComplicationsViewHolder) { ... providerInfoRetriever.retrieveProviderInfo( callback, ComponentName(context, DigitalWatchFaceService::class.java), *ComplicationLocation.getComplicationIds() ) } }
  95. 起動時にComplicationを取得 (ComplicationsConfigRecyclerViewAdapter.kt) val callback = object : ProviderInfoRetriever.OnProviderInfoReceivedCallback() { override

    fun onProviderInfoReceived( watchFaceComplicationId: Int, complicationProviderInfo: ComplicationProviderInfo? ) { complicationProviderInfo?.let { providerInfo -> ComplicationLocation.valueOf(watchFaceComplicationId) ?.let { complicationLocation -> updateComplicationView(providerInfo, complicationLocation) } } } }
  96. 起動時にComplicationを取得 (ComplicationsConfigRecyclerViewAdapter.kt) override fun onDetachedFromRecyclerView(recyclerView: RecyclerView?) { super.onDetachedFromRecyclerView(recyclerView) providerInfoRetriever.release() }

  97. Complicationを表示する (DigitalWatchFaceService.Engine) private var complicationDrawableSparseArray = SparseArray<ComplicationDrawable>( ComplicationLocation.getComplicationIds().size ).apply {

    put( ComplicationLocation.LEFT.complicationId, ComplicationDrawable(applicationContext) ) put( ComplicationLocation.RIGHT.complicationId, ComplicationDrawable(applicationContext) ) }
  98. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onCreate(holder: SurfaceHolder?) { super.onCreate(holder) setWatchFaceStyle( WatchFaceStyle.Builder(this@DigitalWatchFaceService)

    .setAcceptsTapEvents(true) .build()) // 有効なComplicationIdをセットする setActiveComplications(*ComplicationLocation.getComplicationIds()) }
  99. Complicationを表示する (DigitalWatchFaceService.Engine) private fun drawComplications(canvas: Canvas, currentTimeMillis: Long) { val

    complicationSize = canvas.width / 4 complicationDrawableSparseArray[ComplicationLocation.LEFT.complicationId].run { setBounds( canvas.width / 2 - COMPLICATION_MARGIN - complicationSize, datePosition.y + dateBounds.height(), canvas.width / 2 - COMPLICATION_MARGIN, datePosition.y + dateBounds.height() + complicationSize ) }
  100. Complicationを表示する (DigitalWatchFaceService.Engine) complicationDrawableSparseArray[ComplicationLocation.RIGHT.complicationId].run { setBounds( canvas.width / 2 + COMPLICATION_MARGIN,

    datePosition.y + dateBounds.height(), canvas.width / 2 + COMPLICATION_MARGIN + complicationSize, datePosition.y + dateBounds.height() + complicationSize ) }
  101. Complicationを表示する (DigitalWatchFaceService.Engine) ComplicationLocation.getComplicationIds().forEach { complicationId -> // Complicationをcanvasに描画する complicationDrawableSparseArray[complicationId].draw(canvas, currentTimeMillis)

    } }
  102. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onDraw(canvas: Canvas, bounds: Rect) { super.onDraw(canvas,

    bounds) val now = System.currentTimeMillis() // 時間を更新 calendar.timeInMillis = now ... drawComplications(canvas, now) }
  103. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onTapCommand( tapType: Int, x: Int, y:

    Int, eventTime: Long) { super.onTapCommand(tapType, x, y, eventTime) if (tapType == WatchFaceService.TAP_TYPE_TAP) { ComplicationLocation.getComplicationIds().forEach { // ComplicationDrawableの範囲内ならtrue val successfulTap = complicationDrawableSparseArray[it].onTap(x, y) if (successfulTap) return } } }
  104. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onComplicationDataUpdate( watchFaceComplicationId: Int, data: ComplicationData) {

    super.onComplicationDataUpdate(watchFaceComplicationId, data) complicationDrawableSparseArray[watchFaceComplicationId].run { setComplicationData(data) } invalidate() }
  105. AndroidManifest.xml <uses-permission android:name= "com.google.android.wearable.permission.RECEIVE_COMPLICATION_ DATA" />

  106. AndroidManifest.xml <activity android:name="android.support.wearable.complications.ComplicationHelperActivity" /> <activity android:name=".ComplicationsConfigActivity"> <intent-filter> <action android:name="net.syarihu.android.watchfacesample.CONFIG_COMPLICATION" />

    <category android:name= "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <receiver android:name=".ComplicationReceiver" />
  107. AndroidManifest.xml <service android:name=".DigitalWatchFaceService" … > <meta-data android:name="com.google.android.wearable.watchface.wearableConfigurationAction" android:value="net.syarihu.android.watchfacesample.CONFIG_COMPLICATION" />

  108. AndroidManifest.xml <service android:name=".DigitalWatchFaceService" … > <meta-data android:name="com.google.android.wearable.watchface.wearableConfigurationAction" android:value="net.syarihu.android.watchfacesample.CONFIG_COMPLICATION" />

  109. Complications Data Providerを作る

  110. Complication Data Providers • Watch Faceにデータを提供 するアプリ

  111. 今回作るもの • タップしたらインクリメントする RANGED_VALUE のData Provider

  112. class ComplicationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent:

    Intent) { val extras = intent.extras ?: return val pref = PreferenceManager .getDefaultSharedPreferences(context) val provider = extras.getParcelable<ComponentName>( EXTRA_PROVIDER_COMPONENT) val complicationId = extras.getInt(EXTRA_COMPLICATION_ID) タップ後のイベントを受け取る (ComplicationReceiver.kt)
  113. override fun onReceive(context: Context, intent: Intent) { ... val preferenceKey

    = getPreferenceKey( provider, complicationId) val value = pref.getFloat(preferenceKey, 0f) pref.edit().apply { putFloat(preferenceKey, if (value + 1f < 11f) { value + 1f } else { 0f }) }.apply() タップ後のイベントを受け取る (ComplicationReceiver.kt)
  114. override fun onReceive(context: Context, intent: Intent) { … // 対象のComplicationIdの変更をリクエストする

    val requester = ProviderUpdateRequester(context, provider) requester.requestUpdate(complicationId) } タップ後のイベントを受け取る (ComplicationReceiver.kt)
  115. companion object { private const val EXTRA_PROVIDER_COMPONENT = "providerComponent" private

    const val EXTRA_COMPLICATION_ID = "complicationId" internal fun getPreferenceKey( provider: ComponentName, complicationId: Int): String { return provider.className + complicationId } タップ後のイベントを受け取る (ComplicationReceiver.kt)
  116. internal fun getIntent( context: Context, provider: ComponentName, complicationId: Int ):

    PendingIntent { val intent = Intent(context, ComplicationReceiver::class.java) intent.putExtra(EXTRA_PROVIDER_COMPONENT, provider) intent.putExtra(EXTRA_COMPLICATION_ID, complicationId) return PendingIntent.getBroadcast( context, complicationId, intent, PendingIntent.FLAG_UPDATE_CURRENT) } } タップ後のイベントを受け取る (ComplicationReceiver.kt)
  117. class RangedValueProviderService : ComplicationProviderService() { override fun onComplicationUpdate( complicationId: Int,

    type: Int, manager: ComplicationManager ) { // 対象のComplicationDataでなければ何もしない if (type != ComplicationData.TYPE_RANGED_VALUE) { manager.noUpdateRequired(complicationId) return } ProviderServiceを作る (RangedValueProviderService.kt)
  118. val thisProvider = ComponentName(this, javaClass) val complicationPendingIntent = ComplicationReceiver.getIntent( this,

    thisProvider, complicationId ) val preferences = PreferenceManager.getDefaultSharedPreferences(this) val state = preferences.getFloat( ComplicationReceiver.getPreferenceKey( thisProvider, complicationId ), 0f) ProviderServiceを作る (RangedValueProviderService.kt)
  119. val complicationData = ComplicationData.Builder(type) .setMinValue(0f) .setMaxValue(10f) .setValue(state) .setShortText( ComplicationText.plainText(state.toInt().toString())) .setIcon(Icon.createWithResource(

    this, R.drawable.ic_cc_settings_button_bottom)) .setTapAction(complicationPendingIntent) .build() // ComplicationDataの変更を通知する manager.updateComplicationData( complicationId, complicationData) ProviderServiceを作る (RangedValueProviderService.kt)
  120. AndroidManifest.xml <service android:name=".RangedValueProviderService" android:icon="@drawable/ic_donut_large_24dp" android:label="@string/ranged_value" android:permission= "com.google.android.wearable.permission.BIND_COMPLICATION_PR OVIDER"> <intent-filter> <action

    android:name=" android.support.wearable.complications.ACTION_COMPLICATION_U PDATE_REQUEST" /> </intent-filter>
  121. AndroidManifest.xml <service ...> <meta-data android:name= "android.support.wearable.complications.SUPPORTED_TYPES" android:value="RANGED_VALUE" /> <meta-data android:name=

    "android.support.wearable.complications.UPDATE_PERIOD_SECOND S" android:value="0" /> </service>
  122. 実行する

  123. 実行する

  124. Complication Provider Serviceが複数ある場合

  125. まとめ

  126. まとめ • CanvasWatchFaceServiceを 使えばデジタル時計であれば簡 単に作成できる

  127. まとめ • Complications対応するには ◦ Complicationsの設定画面 ◦ ComplicationDrawableを 利用したWatchFaceへの Complicationsの描画

  128. まとめ • Complication Data Providerを 作れば、Complicationsに対応 しているどのWatchFaceにもそ のデータを描画できる

  129. まとめ • 今回作ったサンプルコード https://github.com/syarihu/WatchFaceSample

  130. ぜひオリジナルのWatch Faceや Complication Providersを 作ってみてください!

  131. ご清聴ありがとうございました