Androidのウィジェットでできること、できないこと

iOS 14でホーム画面ウィジェットが使えるようになるそうです。Androidでは昔からウィジェットがありましたが、このタイミングでAndroidのウィジェットにももう一度注目が集まりそうな予感がします。しかし、ウィジェットはかなり制約が大きく、何ができて何ができないのかをエンジニア以外に理解してもらうのは難しかったりします。
ここでは、どうやって作るかという話ではなく、Android開発に詳しくない人や非エンジニア向けに、ウィジェットってどういうものなのか、何ができて何ができないのかをざっくり理解していただけるような内容で書いてみようと思います。

## ウィジェットの制約の考え方

あるアプリのウィジェットを表示しているのは、そのアプリ自身ではなく、ホームアプリ等のウィジェットが設置されているアプリです。ウィジェットを提供するアプリは、こういう内容を表示してくださいとお願いすることしかできません。
まずはここを押さえておいてもらえば以降の制約についても理解しやすいかもしれません。

実際に表示しているのは別のアプリなので、ウィジェットが表示されている間、アプリのプロセスは生きているとは限りません。更新のタイミングでは起動しますが、更新のタイミング以外では、アプリは何もしていないのでプロセスも終了している可能性があります。当然バックグラウンドで動作することになるので、バックグラウンド動作の制約もかかります（位置情報へのアクセスに別のパーミッションが必要など）

全部を制御できてほぼ何でもありのアプリの画面とは全然ちがって、できないことが多く、仮にできたとしても、かなり工夫しないといけません。

## 使えるView（UIのパーツ）が限られる

アプリの画面上では何でもありですが、ウィジェットは使うことができるView（UIのパーツ）が限定されています。

レイアウトは以下の4種類

- [FrameLayout](https://developer.android.com/reference/android/widget/FrameLayout)
  - レイヤーを重ねるようにViewを重ねて表示するLayoutです。それぞれの要素の上下左右のマージンを指定できるので親を絶対座標とする配置が可能です
- [LinearLayout](https://developer.android.com/reference/android/widget/LinearLayout)
  - Viewを縦、もしくは横方向に並べるLayoutです
- [RelativeLayout](https://developer.android.com/reference/android/widget/RelativeLayout)
  - 他のViewを基準に相対的な位置関係を指定して配置していくViewです。自由度が高いですがLegacy扱いになっています
- [GridLayout](https://developer.android.com/reference/android/widget/GridLayout)
  - Viewを格子状に配置するLayoutです

Viewは以下のみ

- [AnalogClock](https://developer.android.com/reference/android/widget/AnalogClock)
  - アナログ時計を表示するViewです。ただし、API 23（Android 6）で非推奨になっています<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4b702750-5033-aa5a-d9d4-27911556d18b.png)
- [Button](https://developer.android.com/reference/android/widget/Button)
  - ボタン、テキストだけでなく画像を表示させることもできます<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/f8fece64-3a8c-e99f-f67c-f1e9c1124ac9.png)
- [Chronometer](https://developer.android.com/reference/android/widget/Chronometer)
  - カウントアップ・ダウンのデジタルタイマーです<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/10add771-9e40-a896-23ee-16a880f8e754.png)
- [ImageButton](https://developer.android.com/reference/android/widget/ImageButton)
  - ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a141a8c0-52d7-69bf-cf8c-32d927106374.png)
- [ImageView](https://developer.android.com/reference/android/widget/ImageView)
  - ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/14247b7b-0092-cd3d-f9b1-f59b7f2155b6.png)
- [ProgressBar](https://developer.android.com/reference/android/widget/ProgressBar)
  - 進捗を表示させるバーです。いわゆるクルクルもProgressBarのバリエーションです。<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/c2ef45d5-64ac-b9d3-d2b1-6a9c79a38557.png)<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/501062b2-4789-04ca-5956-1a0baa43a1a8.png)
- [TextView](https://developer.android.com/reference/android/widget/TextView)
  - テキストと画像を表示させることができます<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/eeb4a281-5741-813d-14cf-8d6f0e86c9b1.png)
- [ViewFlipper](https://developer.android.com/reference/android/widget/ViewFlipper)
  - スライドショー形式で複数の表示の切り替えを自動再生できます。（スクショでは表現できませんが一定時間ごとに表示を切り替えることができます）<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/5c5c44b6-b174-a79a-9fb2-8852a0276d33.png)
- [ListView](https://developer.android.com/reference/android/widget/ListView)
  - 複数の要素を縦に並べるViewです。画面に入りきらない場合はスクロール動作になります<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/66c6eaf3-a1af-bff7-bd99-ca4a3cf07a31.png)
- [GridView](https://developer.android.com/reference/android/widget/GridView)
  - ListViewに列数を指定できるようにしたものです、画面に入りきらない場合はスクロール動作になります<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a60f8b89-a3a0-ca79-9947-49dc8e600217.png)
- [StackView](https://developer.android.com/reference/android/widget/StackView)
  - 複数の要素を重ねて表示させるViewです<br>![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/c8fffcee-ff6f-e6d8-3764-46a4844e4624.png)
- [AdapterViewFlipper](https://developer.android.com/reference/android/widget/AdapterViewFlipper)
  - できることはViewFlipperと同じですが、実装方法が異なります

ポイントは、この制限は本当にこのViewしか使えないということです。
Viewは拡張元、拡張先の親子関係を持っているのですが、ここに上げたViewの親や子は使うことができません。

すべてのViewは「View」というクラスを拡張して作られていますが、Viewは使える要素に含まれていないため、View自体を使うことができません。
同様に独自に拡張したものやライブラリ提供のViewも一切使うことができません。

できるのは、ここにあるViewを組み合わせて使うことだけです。

### OSバージョンによる動作の違いが大きく出る

通常アプリ開発では、AppCompatという新OSの機能を古いOS上でも使えるようにして、OSバージョン間の差異を吸収するライブラリを使用します。
使えるViewの中にあるView、例えば、TextViewも実際はAppCompatTextViewというTextViewを拡張したクラスが使われますが、ウィジェットではAppCompatのクラスは使えません。

OSバージョン間の差異を吸収するライブラリが使えませんので、当然OSバージョン間での動作の違いなどが現れやすく、かつ、それが問題となった場合に解決が困難です。

### Viewの制限によりできなくなることの代表例

Viewに制限があって使えるのはこれだけです、といわれても、何ができて何ができないのかピンとこないでしょう。
具体的にこれできる？と言われそうでできないことの例を上げてみようと思います。

- Webコンテンツの表示
    - WebViewは使えません
- 動画コンテンツの表示
    - VideoViewやSurfacaViewは使えません
- 3Dレンダリング
    - GLSurfaceViewは使えません

## Viewに対する命令の制限

使えるViewに制限がある話をしましたが、使えるViewに対しても可能な命令が制限されます。
この制限をエンジニア以外に分かってもらうのは難しいですが、わかりやすいところで言うと、表示させられる画像リソースは、ビルド時に固定で組み込んだリソース、もしくはBitmapのみという制約があります。
例えば、角丸や縁取りの色や太さを動的に変更できるようにして欲しいというリクエストをされると、すごくハードルが上がります。

## 操作

ウィジェットで受け取ることができる操作はタップのみです。それ以外の操作を受け取ることはできません。
スクロール可能なViewを配置している場合は、スクロール操作がウィジェットに適用されますが、その操作をアプリで受け取ることはできません。

そのため、例えば、お絵かきソフトのようにドラッグした軌跡を書き出すといったことはできません。

## 表示場所の制御、取得

- ウィジェットが追加された
- ウィジェットが削除された
- 表示サイズが変わった

といった情報は受け取ることができるため、どのような大きさで何個設置されているか、をアプリから知ることは可能です。
しかし、それがどのランチャーアプリに設置されているのか、ランチャーアプリ内のどこページのどの位置なのか、といった情報を取ることはできません。
ウィジェットがユーザーから見える位置にあるか否かも知ることはできません。

## 表示の更新

ウィジェットの属性で更新周期を指定することができますが、30分以下を指定すると無視され、更新されなくなります。
端末のWakeupが伴うため、1時間に1回以下にすることが推奨されています。

これとは別に、アプリ側から更新を要求することができます。
このときは当然アプリのプロセスが生きている必要がありますが、能動的な更新が可能なので、アプリ側の定期実行の仕組みを使えば、ある程度柔軟な制御ができます。また、ウィジェット上のボタンのタップなどでアプリにメッセージを飛ばすことができますので、このタイミングで更新をかけることもできます。

いずれにせよ、アプリはバックグラウンドで動作することになりますので、高頻度な更新はできないと思った方がよいでしょう。

当然アニメーションもあらかじめ決められた動きの一部は可能ですが、自由度はかなり限られます。アプリがバックグラウンドで常駐して、表示する内容を高速に更新、という方法は使えなくもないでしょうが、垂直同期を取ることはできませんし、常駐状態でこのような処理をするのは現実的ではないです。無理にぶん回せばこのぐらいの更新はできるみたいですが
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/9890fabd-c87f-a1b3-8339-b4ee8d6d3f0e.gif)

## 表示サイズの制御

表示サイズとして指定できるのは以下です

- デフォルトの高さ、幅
- 最小の高さ、幅
- 高さ、幅それぞれが可変か否か

### セル数でサイズの指定はできない

まず、高さ、幅の指定で使う単位は dp です。一般的なランチャーアプリでは、「3 x 1」のように表示されるセル数で表示されているため、セル数を指定したくなるかもしれませんが、これは不可能です。セルのサイズおよびマージンは端末やランチャーアプリによって異なります。場合によっては縦向きか横向きかでも変わります。ウィジェットを提供するアプリはデフォルトのレイアウトが収まる具体的なサイズを指定し、ホームアプリは自分のセルサイズから計算して、それが納まる最小のセル数を表示します。
セル数サイズが変わっては困る仕様は避ける必要があります（例えば名前に「1 x 1ウィジェット」とサイズの入った名前をつけるなど）

### 上限は指定できない

高さ、幅、それぞれについて、デフォルト、最小、可変か否か、しか指定できません。上限がないため、可変にすると上限なしとするしかありません。
デフォルトの大きさは「1 x 1」だけど、「1 x 2」までは拡大できる、それ以上はできない、みたいな仕様は実現できません。
広い領域が割り当てられても、その中心にだけ表示するという制御は可能です。

## まとめ

思いつく範囲で、Androidのウィジェットの制約をまとめました。
あれが書かれてない、これ間違っている、などありましたらご指摘をお願いします。