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

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

syarihu
February 09, 2018

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

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

syarihu

February 09, 2018
Tweet

More Decks by syarihu

Other Decks in Technology

Transcript

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

    Developers https://developer.android.com/reference/android/support/wearable/view/WatchViewStub.html
  2. 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
  3. 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が公開される
  4. class DigitalWatchFaceService : CanvasWatchFaceService() { override fun onCreateEngine(): Engine {

    return Engine() } WatchFaceServiceの作成 (DigitalWatchFaceService.kt)
  5. 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)
  6. inner class Engine : CanvasWatchFaceService.Engine() { // 時間を取得するためのCalendar private val

    calendar: Calendar = Calendar.getInstance() // 時計の背景色 private var backgroundColor = Color.BLACK 初期化処理 (DigitalWatchFaceService.kt)
  7. … // 時間を描画するための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)
  8. … // 時間の位置を保持するための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)
  9. 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)
  10. 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)
  11. 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)
  12. 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)
  13. 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)
  14. 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)
  15. inner class Engine : CanvasWatchFaceService.Engine() { ... private fun drawWatchFace(canvas:

    Canvas) { ... canvas.drawText( strDate, datePosition.x.toFloat(), datePosition.y.toFloat(), datePaint ) } 時計の描画 (DigitalWatchFaceService.kt)
  16. 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)
  17. inner class Engine : CanvasWatchFaceService.Engine() { ... override fun onTimeTick()

    { super.onTimeTick() // 時間が変わったら再描画 invalidate() } 時間が変わったときの処理 (DigitalWatchFaceService.kt)
  18. System Providers • 日付 • 時間 • 歩数 • アプリ

    • 未読通知数 • 世界時計 • 次の予定 • バッテリー残量
  19. Complications Types Adding Complications to a Watch Face | Android

    Developers https://developer.android.com/training/wearables/watch-faces/adding-complications.html
  20. 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
  21. Complications Types Type 必須フィールド オプション LONG_TEXT Long text Long title

    Icon Burn-in protection icon Small image SMALL_IMAGE Small image LARGE_IMAGE Large image
  22. 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)
  23. ... companion object { fun getComplicationIds(): IntArray = values().map {

    it.complicationId }.toIntArray() fun valueOf( complicationId: Int ): ComplicationLocation? = values().firstOrNull { it.complicationId == complicationId } } } Complicationsの設定を決める (ComplicationLocation.kt)
  24. 設定画面のレイアウトを作る (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>
  25. <?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)
  26. 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)
  27. <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)
  28. 設定画面のレイアウトを作る (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" />
  29. プレビュー画面の初期化 (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)
  30. プレビュー画面の初期化 (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
  31. プレビュー画面の初期化 (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) } }
  32. プレビュー画面の初期化 (ComplicationsConfigRecyclerViewAdapter.kt) override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int )

    { if (holder is PreviewAndComplicationsViewHolder) { holder.bind(context, { selectedComplicationLocation = it })
  33. 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)
  34. Complication選択画面 (PreviewAndComplicationsViewHolder.kt) // Complication選択画面へのIntent val intent = ComplicationHelperActivity.createProviderChooserHelperIntent( activity, watchFace,

    complicationLocation.complicationId, *complicationLocation.complicationSupportedTypes ) activity.startActivityForResult( intent, ComplicationsConfigActivity.COMPLICATION_CONFIG_REQUEST_CODE ) }
  35. 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 )) }
  36. 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 } }
  37. 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 }
  38. 起動時にComplicationを取得 (ComplicationsConfigRecyclerViewAdapter.kt) class ComplicationsConfigRecyclerViewAdapter( private val context: Context ) :

    RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val providerInfoRetriever: ProviderInfoRetriever = ProviderInfoRetriever( context.applicationContext, Executors.newCachedThreadPool() ).apply { init() }
  39. 起動時に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() ) } }
  40. 起動時に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) } } } }
  41. Complicationを表示する (DigitalWatchFaceService.Engine) private var complicationDrawableSparseArray = SparseArray<ComplicationDrawable>( ComplicationLocation.getComplicationIds().size ).apply {

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

    .setAcceptsTapEvents(true) .build()) // 有効なComplicationIdをセットする setActiveComplications(*ComplicationLocation.getComplicationIds()) }
  43. 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 ) }
  44. 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 ) }
  45. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onDraw(canvas: Canvas, bounds: Rect) { super.onDraw(canvas,

    bounds) val now = System.currentTimeMillis() // 時間を更新 calendar.timeInMillis = now ... drawComplications(canvas, now) }
  46. 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 } } }
  47. Complicationを表示する (DigitalWatchFaceService.Engine) override fun onComplicationDataUpdate( watchFaceComplicationId: Int, data: ComplicationData) {

    super.onComplicationDataUpdate(watchFaceComplicationId, data) complicationDrawableSparseArray[watchFaceComplicationId].run { setComplicationData(data) } invalidate() }
  48. 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" />
  49. 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)
  50. 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)
  51. override fun onReceive(context: Context, intent: Intent) { … // 対象のComplicationIdの変更をリクエストする

    val requester = ProviderUpdateRequester(context, provider) requester.requestUpdate(complicationId) } タップ後のイベントを受け取る (ComplicationReceiver.kt)
  52. 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)
  53. 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)
  54. 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)
  55. 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)
  56. 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)