2018年6月9日土曜日

ViewOutlineProvider を使う

API Level 21 に追加された ViewOutlineProvider では shadow casting と outline clipping に利用する Outline を指定できます。

ViewOutlineProvider の getOutline() の引数で渡される Outline に Rect, RoundRect, Oval または ConvexPath を指定します。

View が持つ Drawable が invalidate されたり、View のサイズが変わったり、View の invalidateOutline() が呼ばれると getOutline() が呼ばれます。 private val clipOutlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val margin = min(view.width, view.height) / 10 outline.setOval( 0, 0, view.width, view.height ) } } View の setOutlineProvider() で ViewOutlineProvider を指定します。outline clipping を有効にするには setClipToOutline() で true を指定します。

outline clipping は現状 RoundRect か Circle(Oval で縦横のサイズが同じ)のときだけ効きます。
clip しても View のサイズには影響しないためクリック領域などはそのままです。 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) clippedView.outlineProvider = clipOutlineProvider clippedView.clipToOutline = true } 左 : clipToOutline = false, 右 : clipToOutline = true





2018年6月7日木曜日

ConstraintLayout で float で指定する属性

ConstraintLayout 1.1.0 の属性のうち、TypedArray から getFloat() で取得されている属性は
  • app:layout_constraintCircleAngle
  • app:layout_constraintGuide_percent
  • app:layout_constraintHorizontal_bias
  • app:layout_constraintVertical_bias
  • app:layout_constraintHeight_percent
  • app:layout_constraintWidth_percent
  • app:layout_constraintHorizontal_weight
  • app:layout_constraintVertical_weight
です。

これらの属性は app:layout_constraintGuide_percent="0.3" のように値を直接指定するときは float 形式で書きます。

float 値を resource で定義するには、 <?xml version="1.0" encoding="utf-8"?> <resources> <item name="guideline_percent" type="dimen" format="float">0.3</item> </resources> のように item タグ を使って float format、dimen type で定義します。 app:layout_constraintGuide_percent="@dimen/guideline_percent"


2018年5月31日木曜日

I/O Recap : Slices

https://developer.android.com/guide/slices/

Slices は自分のアプリの情報を他のアプリに表示するための仕組みです。
コンセプトとしては App Widget で使う RemoteViews と似ていますが、使えるレイアウトは用意されているテンプレートだけなど制限があります。 Google Search の検索結果で利用することを例示されていることからも、統一された Look and Feel を持たせたいのかなと思います。

Google Search アプリで2018年夏に対応することが Google I/O 2018 のセッションでアナウンスされていました。 将来的に Google Assistant での応答結果に利用できるようになることもアナウンスされていました。

Slices のサポートが Jetpack に組み込まれており、これを利用すると Android 4.4 以降に対応できます。

前提条件

  • AndroidX にリファクタリングされていること
  • 必須ではないが Android Studio 3.2 以降には Slice を開発するのに便利なツールや機能が追加されている

Slice Provider Widzard

Android Studio 3.2 には Slice Provider を追加する Widzard が用意されています。 この Widzard を使うと
  • build.gradle に slice の dependencies を追加
  • SliceProvider を継承したクラスの生成
  • AndroidManifest に Provider の宣言を追加
を自動で行ってくれます。

* Android Studio 3.2 Canary 15 まで Wizard で追加される dependencies が typo しているバグがありました。Canary 16 では修正されています。 https://issuetracker.google.com/issues/79996770


dependencies の設定

現時点(2018年5月31日)での最新版は 1.0.0-alpha2 です。 implementation "androidx.slice:slice-core:1.0.0-alpha2" implementation "androidx.slice:slice-builders:1.0.0-alpha2"

SliceProvider を継承したクラスを用意

自分のアプリで Slice を提供するには SliceProvider を継承したクラスを用意します。 androidx.slice.SliceProviderandroid.app.slice.SliceProvider があるので注意してください。 import androidx.slice.Slice import androidx.slice.SliceProvider ... class MySliceProvider : SliceProvider() { override fun onCreateSliceProvider(): Boolean { return true } override fun onBindSlice(sliceUri: Uri): Slice? { ... } }

AndroidManifest に Provider の宣言を追加

android:exported="true" が必要です。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.example.slicecodelab"> <application ... > ... <provider android:name=".MySliceProvider" android:authorities="com.android.example.slicecodelab" android:exported="true" /> </application> </manifest>

Uri と Slice の bind

各 Slice は URI と紐づいており、Slice の表示がリクエストされると SliceProvider の onBindSlice(uri: Uri) が呼ばれます。
onBindSlice(uri: Uri) では送られてきた Uri を処理し、対応する Slice インスタンスを返します。 override fun onBindSlice(sliceUri: Uri): Slice? { return when (sliceUri.path) { "/temperature" -> { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("Temperature : $temperature") } .build() } else -> null } } 引数の sliceUri には content://<authorities>/<path> が入ってきます。


Slice の更新

Slice で表示している内容を更新したいときは Slice の Uri に対して notifyChange() を呼びます。 そうすると Provider の onBindSlice(uri: Uri) が呼ばれるので新しい内容で Slice を作ります。 class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // update slice related data ... context.contentResolver.notifyChange(sliceUri, null) } }

Templates

https://developer.android.com/guide/slices/templates

Slice は TemplateSliceBuilder を 継承したクラスを使って作ります。 このうち、一番外側の Slice は ListBuilder で作ります。


ListBuilder

List 形式の Slice を作成できます。
子要素(一行の要素)として ListBuilder.RowBuilder、ListBuilder.HeaderBuilder、ListBuilder.RangeBuilder、ListBuilder.InputRangeBuilder、GridRowBuilder を追加できます。HeaderBuilder は一つしか指定できませんが、その他は複数追加できます。

MODE_SHORTCUT では、Header に PrimaryAction が指定されていればそれが使われ、なければ最初の Row の PrimaryAction が使われます。
MODE_SMALL では 1 row だけ表示されます。Header が指定されていればそれだけが表示され、なければ最初の Row だけが表示されます。
MODE_LARGE では表示可能な数だけ row が表示されます。Slice を表示する側が scrolling に対応していれば全ての row がスクロールできるビュー内に表示されます。

ListBuilder のコンストラクタでは TTL をミリ秒で指定します。time sensitive なコンテンツを含まない場合は ListBuilder.INFINITY を指定します。

setAccentColor(color: Int) で accentColor を指定できます。accentColor はアイコンの tint や toggle の tint、horizontal progress の tint などに適用されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setAccentColor(Color.BLUE) ... .build() }


setKeywords(keywords: List) で指定したキーワードは Slice の Hints(getHints() で取得できる)に追加されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setKeywords(listOf("Cupcake", "Donuts", "Eclair")) ... .build() }

WiFi 設定など表示したい row が多いときは setSeeMoreAction(intent: PendingIntent) を指定しておくと、全ての row が表示できないときに右下に See more ボタンが表示されるようになります。 private fun createSlice(sliceUri: Uri): Slice? { val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setSeeMoreAction(seeMoreIntent) ... .build() }


addAction() で Header の右側に SliceAction を表示させることができます(Header がない場合最初の Row の右側に表示されますが、Header が指定されている前提の機能だと思います)。
複数追加できますが 3つまでしか表示されませんでした。 private fun createSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addAction(homeAction) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") } .build() }


ListBuilder.RowBuilder

ListBuilder の子要素(一行の要素)として Row を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストに追加されず ListBuilder.addRow() で明示的にリストに追加する必要があります。

setPrimaryAction() で指定した SliceAction は Row がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher), "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setPrimaryAction(action) } .build() } 左 : MODE_SHORTCUT、右 : MODE_LARGE




addEndItem(timeStamp: Long) で右側に timestamp を表示することができます。
指定した timeStamp の long 値から自動で差分時間表示(10 min ago とか)になります。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(System.currentTimeMillis() - 10 * 60 * 1000) } .build() }


addEndItem(icon: IconCompat, imageMode: Int) で右端にアイコンを表示することができます。

imageMode には のどれかを指定します。 ICON_IMAGE を指定すると小さいサイズで表示され tint されます。SMALL_IMAGE、LARGE_IMAGE では tint されません。

アイコンは複数追加できますが 3つまでしか表示されませんでした。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE) } .build() } 左 : ICON_IMAGE、中 : SMALL_IMAGE、右 : LARGE_IMAGE



左 : ICON_IMAGE での MODE_SHORTCUT、右 : LARGE_IMAGE での MODE_SHORTCUT




addEndItem(action: SliceAction) で右側に SliceAction を表示できます。

EndItem にアイコン(上記)と SliceAction を併用することはできません。
private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(homeAction) } .build() }


toggle 表示用の SliceAction を指定するとスイッチを表示できます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val toggleAction = SliceAction( pendingIntent, "Toggle", true) return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(toggleAction) } .build() }


setTitleItem() で左側に Timestamp、アイコン、SliceAction が表示できるはずなのですが、指定しても表示されませんでした。


ListBuilder.HeaderBuilder

ListBuilder の子要素(一行の要素)として Header を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストにセットされず ListBuilder.setHeader() で明示的にリストにセットする必要があります。
ListBuilder にセットできる HeaderBuilder は一つだけです。

title, subTitle に加えて summary をセットすることができ、summary はモードが SliceView.MODE_SMALL のときに使われます。

setPrimaryAction() で指定した SliceAction は Header がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle, summary の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createHeaderSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground), "Temperature controls") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setSummary("summary") .setPrimaryAction(action) } .build() } 左: MODE_SHORTCUT、中 : MODE_SMALL、右 : MODE_LARGE




ListBuilder.RangeBuilder

ListBuilder の子要素(一行の要素)として horizontal progress を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addRange() で明示的にリストに追加する必要があります。

Min は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。 private fun createRangeSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMax(10) .setValue(5) } .build() } 左: setMax(), setValue() のみ、右 : title, subTitle あり




ListBuilder.InputRangeBuilder

ListBuilder の子要素(一行の要素)として horizontal slider を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addInputRange() で明示的にリストに追加する必要があります。

Min のデフォルト値は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。
InputAction は必須です。 private fun createInputRangeSlice(sliceUri: Uri): Slice? { val pendingIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addInputRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMin(5) .setMax(10) .setValue(8) .setThumb(IconCompat.createWithResource(context, R.drawable.square)) .setInputAction(pendingIntent) } .build() } 左: setMin(), setMax(), setValue() のみ、中 : title, subTitle あり、右 : setThumb()




GridRowBuilder

ListBuilder の子要素(一行の要素)として horizontal grid を表示できます。
grid の各アイテムは GridRowBuilder.CellBuilder で作成します。

コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addGridRow() で明示的にリストに追加する必要があります。

ListBuilder と同じように setSeeMoreAction(intent: PendingIntent) で see more ボタンを表示する機能があります。


GridRowBuilder.CellBuilder

GridRowBuilder の子要素(1アイテム)として cell を表示できます。

コンストラクタで親の GridRowBuilder インスタンスを指定する必要がありますが、 それだけではグリッドに追加されず GridRowBuilder.addCell() で明示的にグリッドに追加する必要があります。

addTitleText() でタイトルスタイルで表示したいテキストを追加し、addText() でノーマルスタイルで表示したいテキストを追加します。 両方含め最初に追加したテキスト2つが利用され、それ以降に追加されたテキストは無視されます。

addImage() で画像を追加します。最初に追加した画像1つが利用され、それ以降に追加した画像は無視されます。

setContentIntent() で cell をタップしたときに発行される PendingIntent を指定することができます。 private fun createGridSlice(sliceUri: Uri): Slice? { ... val action = ... val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addGridRow { it.setContentDescription("description") .setPrimaryAction(action) .setSeeMoreAction(seeMoreIntent) .addCell { it.addImage( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.LARGE_IMAGE) .addTitleText("Title") .addText("normal") .setContentDescription("description") .setContentIntent(cellIntent) } ... } .build() }

ドキュメントには cell の数について言及されていませんが、SliceViewer(後述)では5つまで表示されました。
また、setSeeMoreAction() すると、4つ表示 + More になりました。



なぜか SliceViewer(1.0.0-alpha1.1 で確認)では画像が長方形に表示されてしまうのですが、Google Search が対応したときにはちゃんと正方形で表示されると思います。

画像のみ



左: テキストのみ MODE_SMALL、右: テキストのみ MODE_LARGE



左: 画像 + テキスト MODE_SMALL、右: 画像 + テキスト MODE_LARGE



MODE_SMALL では表示されるテキストが1つだけになります。



Slice Viewer

https://developer.android.com/guide/slices/getting-started#run-the-slice-viewer

自分のアプリの Slice の表示を確認するために SliceViewer サンプル が用意されています。
このサンプルの apk ダウンロードしデバイスにインストールします。 $ adb install -r -t slice-viewer.apk 自分の Slice を表示確認をするには、slice-<表示させたい Slice の URI> を Intent の Data として指定します。 $ adb shell am start -a android.intent.action.VIEW -d slice-content://com.android.example.slicecodelab/temperature ランチャーから SliceViewer アプリを起動し、アプリ内の検索バーで <表示させたい Slice の URI> を検索すると、トップの一覧画面に検索した Slice が表示されます。


Codelabs

Creating Android Slices
  • Java のみで Kotlin 版はない
  • androidx.slice のバージョンが 1.0.0-alpha1 になっているが 1.0.0-alpha2 が出ている
  • P と androidx で同じ名前のクラスでも API が異なるので import 先が androidx になっているか注意すべし

Google I/O 2018 session