- 投稿日:2020-02-24T22:17:32+09:00
[Android][Kotlin]Androidの設定値について
指定したターゲット(パッケージ名や利用するサービスの設定)でAndroidアプリケーションをビルドする方法は複数ある。初期のものであったり、それを改善したものであったり。最新の方法だと利点が多いので選択できればいいのだが、歴史が長いソースコードだと、様々な理由から方法を変更できなかったり。
何がベストなのかは状況によって異なるが、まずは知らないことには話にならないということで調べてた方法を列挙してみる。
androidコマンド
最新のAndroid SDKのツールではなくなったコマンド。オプションで指定したAndroidアプリケーションの雛形となるソース一式を生成されるので、例えば、異なるパッケージ名のソースを生成する場合に使っていた。
$ android create project \ --name プロジェクト名 \ --target ターゲットID \ --path プロジェクトを生成するファイル・パス \ --package パッケージ名 \ --activity デフォルト・アクティビティ名代替リソース
条件毎に用意したディレクトリにリソースを置いて切り替える方法。
以下は高密度画面(hdpi)とそれ以外の画面の端末でアイコン画像を切り替える例だ。
res/drawable/icon.png res/drawable-hdpi/icon.pnghdpiは設定修飾子と呼ばれる識別子だが、どんな設定修飾子があるのか表にしてみる。
設定 修飾子の値 説明 MCC と MNC mcc310 mcc310-mnc004 モバイル カントリー コード(MCC)とモバイル ネットワーク コード(MNC) 言語と地域 en en-rUS b+en+US ISO 639-1言語コード BCP 47言語タグ レイアウトの方向 ldrtl ldltr ldrtlは右から左。ldltrは左から右。 smallestWidth swdp 使用可能な画面領域の最小寸法で指定。 使用可能な幅 wdp 使用可能な最小の画面幅をdp単位で指定。 使用可能な高さ hdp 使用可能な最小の画面の高さをdp単位で指定。 画面サイズ small normal large xlarge 画面アスペクト long notlong 円形の画面 round notround 広色域 widecg nowidecg HDR highdr lowdr ハイ ダイナミック レンジ 画面の向き port land UI モード car desk ... 端末をホルダーに装着したり、取り外す場合 ナイトモード night notnight 画面ピクセル密度 ldpi xxxhdpi ... 画面ピクセル密度(dpi) タッチスクリーン タイプ notouch finger テキスト入力方法 nokeys qwerty 12key ナビゲーション キー navexposed navhidden ナビゲーション方法 nonav dpad trackball wheel プラットフォーム Ver. v3 v4 v7 ... API レベル ローカライズ
設定修飾子を使った代替リソースを利用する方法。例を挙げる。
res/values/strings.xml res/drawable/res/values-en/strings.xmlres/values-ja/strings.xml res/drawable-ja/文字列リソースを英語と日本語で切り替えるのと、画像を日本語とそれ以外で切り替える例となっている。
ビルド バリアント
ビルド・バリアントは、ビルドタイプとプロダクト フレーバーを組み合わせたも。build.gradleの例だ。
android { defaultConfig { } signingConfigs { release {} } buildTypes { release { signingConfig signingConfigs.release } debug {} staging {} } flavorDimensions "api", "mode" productFlavors { demo { dimension "mode" } full { dimension "mode" } minApi24 { dimension "api" } minApi23 { dimension "api" } minApi21s { dimension "api" } } variantFilter { variant -> def names = variant.flavors*.name if (names.contains("minApi21") && names.contains("demo")) { setIgnore(true) } } }ビルド バリアントの形式は、<product-flavor><Build-Type>となる。
例えば、minApi24DemoDebugは、minApi24とdemoとdebugの組み合わせという意味。この場合、apkはapp-minApi24-demo-debug.apkとなる。
ビルド・バリアントに対応したJavaソースのパスは、例えば src/demoMinApi24/java/ となる。
ソースセットの優先順位の例だ。
- src/demoDebug/
- src/debug/
- src/demo/
- src/main/
gradle.properties
build.gradleに渡すパラメータを設定できる。
gradle.propertiesの例。
DEMO_STRING="demo string" DEMO_NUM_DEMO=1234 DEMO_NUM_FULL=5678 DEMO_BOOLEAN=truebuild.gradleの例。
android { defaultConfig { // resの値を追加(AndroidManifest.xml で参照) resValue 'string', 'DEMO_STRING', "${DEMO_STRING}" // BuildConfigクラスのメンバーを追加(Kotlinコードで参照) buildConfigField 'boolean', 'DEMO_BOOLEAN', "${DEMO_BOOLEAN}" } productFlavors { demo { // BuildConfigクラスのメンバーを追加(Kotlinコードで参照) buildConfigField 'long', 'DEMO_NUM', "${DEMO_NUM_DEMO}" } full { // BuildConfigクラスのメンバーを追加(Kotlinコードで参照) buildConfigField 'long', 'DEMO_NUM', "${DEMO_NUM_FULL}" } }リソースの値を設定することでAndroidManifest.xmlで使い値を切り替えたり、BuildConfigクラスのメンバーを設定することで、ソースコードで参照する値を切り替えられる。
【関連情報】
集まれKotlin好き!Kotlin愛好会 vol.20
Cocoa.swift
Cocoa勉強会 関東
Cocoa練習帳
- 投稿日:2020-02-24T16:19:50+09:00
Flutter WidgetのKey指定が必要な理由と仕組みについて解説 - 前編
はじめに
Flutterの Widget でたまに指定する
Key
について、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。今回は前編として、主に
Key
とは何かと、Key
が必要になる理由について解説します。
後編では、Key
の種類一覧やKey
指定位置について解説予定です。
Key
とは?簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。
Key
は意図的に指定しないとデフォルトではnull
です。必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。
- ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
- Listのスクロール位置の保存
本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。
何故
Key
が必要になるのか?サンプルアプリを用いて、
Key
の必要性を理解していきます。タイルを入れ替える簡単なアプリ
FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ソースコードは以下です。
StatelessWidget
を用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。ソースコードimport 'package:flutter/material.dart'; import 'dart:math'; class StatelessColorfulTile extends StatelessWidget { final Color _color = UniqueColorGenerator.getColor(); @override Widget build(BuildContext context) { return Container( color: _color, child: Padding(padding: EdgeInsets.all(100.0))); } } class UniqueColorGenerator { static List _colorOptions = [ Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.orange, Colors.indigo, Colors.amber, Colors.black, ]; static Random _random = new Random(); static Color getColor() { if (_colorOptions.length > 0) return _colorOptions.removeAt(_random.nextInt(_colorOptions.length)); else return Color.fromARGB(_random.nextInt(256), _random.nextInt(256), _random.nextInt(256), _random.nextInt(256)); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ StatelessColorfulTile(), StatelessColorfulTile(), ]; } void _swapTiles() { setState(() { _tiles.insert(1, _tiles.removeAt(0)); }); } @override Widget build(BuildContext context) { return Scaffold( body: Row(children: _tiles), floatingActionButton: FloatingActionButton( child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles), ); } }StatelessWidgetの動作
StatelessWidget
の場合、作成されるStatelessElement
はState
を保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetState
をトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。また、Flutterは各Widgetをツリー構成で管理しており、
Widget
/Element
/RenderObject
の3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。
仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChild
→StatelessElement.update
→Element.rebuild
→ComponentElement.performRebuild
→StatelessElement.build
→StatelessColorfulTile.build
という順番でコールされます。StatelessElement.updateclass StatelessElement extends ComponentElement { /// Creates an element that uses the given widget as its configuration. StatelessElement(StatelessWidget widget) : super(widget); @override StatelessWidget get widget => super.widget; @override Widget build() => widget.build(this); @override void update(StatelessWidget newWidget) { super.update(newWidget); assert(widget == newWidget); _dirty = true; rebuild(); // ← ここでrebuildがコールされる } }上記サンプルをStatefulWidgetで作ってみると?
今度は
StatelessWidget
からStatefulWidget
に置き換えて動かしてみます。StatefulWidgetに置き換えclass StatefulColorfulTile extends StatefulWidget { StatefulColorfulTile({Key key}) : super(key: key); @override ColorfulTileState createState() => ColorfulTileState(); } class ColorfulTileState extends State<StatefulColorfulTile> { Color _color; @override void initState() { super.initState(); _color = UniqueColorGenerator.getColor(); } @override Widget build(BuildContext context) { return Container( color: _color, child: Padding( padding: EdgeInsets.all(100.0), )); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), StatefulColorfulTile(), // 変更!! StatefulColorfulTile(), // 変更!! ]; }何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)StatefulWidget (Key指定なし) の動作
StatefulWidget
とStatelessWidget
の最大の違いはState
の有無です。
StatefulWidget
はsetState
をトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKey
とRuntimeType
(クラス名のこと) を参照しています。ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。
Element.updateChild@protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { assert(() { if (newWidget != null && newWidget.key is GlobalKey) { final GlobalKey key = newWidget.key; key._debugReserveFor(this); } return true; }()); if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了 if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); assert(() { child.owner._debugElementWasRebuilt(child); return true; }()); return child; } deactivateChild(child); assert(child._parent == null); } return inflateWidget(newWidget, newSlot); }Widget.canUpdate@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected Element createElement(); @override String toStringShort() { return key == null ? '$runtimeType' : '$runtimeType-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } // 新旧でRuntimeTypeとKeyが同じかどうかチェック static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。
この時点でおかしな状態になっていることが分かります。
Key追加による解決
先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyに
UniqueKey()
を追加します。Key追加class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), //StatefulColorfulTile(), //StatefulColorfulTile(), StatefulColorfulTile(key: UniqueKey()), // 変更!! StatefulColorfulTile(key: UniqueKey()), // 変更!! ]; }StatefulWidget (Key指定あり) の動作
Keyを指定した場合は、
Element.updateChild
内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidget
とは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。図示すると以下のような感じになります。
参考文献
- 投稿日:2020-02-24T16:19:50+09:00
Flutter WidgetにKeyが必要な理由, 仕組み, 使い方について解説 - 前編
はじめに
Flutterの Widget でたまに指定する
Key
について、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。今回は前編として、主に
Key
とは何かと、Key
が必要になる理由について解説します。
後編では、Key
の種類一覧やKey
指定位置について解説予定です。
Key
とは?簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。
Key
は意図的に指定しないとデフォルトではnull
です。必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。
- ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
- Listのスクロール位置の保存
本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。
Key
が必要な理由についてサンプルアプリを用いて、
Key
の必要性を理解していきます。タイルを入れ替える簡単なアプリ
FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ソースコードは以下です。
StatelessWidget
を用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。ソースコードimport 'package:flutter/material.dart'; import 'dart:math'; class StatelessColorfulTile extends StatelessWidget { final Color _color = UniqueColorGenerator.getColor(); @override Widget build(BuildContext context) { return Container( color: _color, child: Padding(padding: EdgeInsets.all(100.0))); } } class UniqueColorGenerator { static List _colorOptions = [ Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.orange, Colors.indigo, Colors.amber, Colors.black, ]; static Random _random = new Random(); static Color getColor() { if (_colorOptions.length > 0) return _colorOptions.removeAt(_random.nextInt(_colorOptions.length)); else return Color.fromARGB(_random.nextInt(256), _random.nextInt(256), _random.nextInt(256), _random.nextInt(256)); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ StatelessColorfulTile(), StatelessColorfulTile(), ]; } void _swapTiles() { setState(() { _tiles.insert(1, _tiles.removeAt(0)); }); } @override Widget build(BuildContext context) { return Scaffold( body: Row(children: _tiles), floatingActionButton: FloatingActionButton( child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles), ); } }StatelessWidgetの動作
StatelessWidget
の場合、作成されるStatelessElement
はState
を保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetState
をトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。また、Flutterは各Widgetをツリー構成で管理しており、
Widget
/Element
/RenderObject
の3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。
仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChild
→StatelessElement.update
→Element.rebuild
→ComponentElement.performRebuild
→StatelessElement.build
→StatelessColorfulTile.build
という順番でコールされます。StatelessElement.updateclass StatelessElement extends ComponentElement { /// Creates an element that uses the given widget as its configuration. StatelessElement(StatelessWidget widget) : super(widget); @override StatelessWidget get widget => super.widget; @override Widget build() => widget.build(this); @override void update(StatelessWidget newWidget) { super.update(newWidget); assert(widget == newWidget); _dirty = true; rebuild(); // ← ここでrebuildがコールされる } }StatefulWidgetだとどうなるか?
今度は
StatelessWidget
からStatefulWidget
に置き換えて動かしてみます。StatefulWidgetに置き換えclass StatefulColorfulTile extends StatefulWidget { StatefulColorfulTile({Key key}) : super(key: key); @override ColorfulTileState createState() => ColorfulTileState(); } class ColorfulTileState extends State<StatefulColorfulTile> { Color _color; @override void initState() { super.initState(); _color = UniqueColorGenerator.getColor(); } @override Widget build(BuildContext context) { return Container( color: _color, child: Padding( padding: EdgeInsets.all(100.0), )); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), StatefulColorfulTile(), // 変更!! StatefulColorfulTile(), // 変更!! ]; }何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)StatefulWidget (Key指定なし) の動作
StatefulWidget
とStatelessWidget
の最大の違いはState
の有無です。
StatefulWidget
はsetState
をトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKey
とRuntimeType
(クラス名のこと) を参照しています。ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。
Element.updateChild@protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { assert(() { if (newWidget != null && newWidget.key is GlobalKey) { final GlobalKey key = newWidget.key; key._debugReserveFor(this); } return true; }()); if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了 if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); assert(() { child.owner._debugElementWasRebuilt(child); return true; }()); return child; } deactivateChild(child); assert(child._parent == null); } return inflateWidget(newWidget, newSlot); }Widget.canUpdate@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected Element createElement(); @override String toStringShort() { return key == null ? '$runtimeType' : '$runtimeType-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } // 新旧でRuntimeTypeとKeyが同じかどうかチェック static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。
この時点でおかしな状態になっていることが分かります。
Keyを指定して解決
先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyに
UniqueKey()
を追加します。Key追加class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), //StatefulColorfulTile(), //StatefulColorfulTile(), StatefulColorfulTile(key: UniqueKey()), // 変更!! StatefulColorfulTile(key: UniqueKey()), // 変更!! ]; }StatefulWidget (Key指定あり) の動作
Keyを指定した場合は、
Element.updateChild
内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidget
とは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。図示すると以下のような感じになります。
参考文献
- 投稿日:2020-02-24T15:26:21+09:00
AndroidのTextViewでフォントサイズの変更について考えてみた
はじめに
Androidアプリ開発をしていて、
- デザイナーさんがデザインを起こしてくれる
- デザインの数値通りにレイアウトを組む
- 解像度、ディスプレイサイズの違いに対応する
- フォントサイズ変更で崩れるところに対応する
までが一連の流れになってきたので、特に4点目に対応することを考えました..
そもそも、デザインが文字サイズの変更や解像度の違いを考慮してくれたら最高に有り難いですが、
基本iPhoneベースのデザインでAndroidを作ることが多いので、そんなことはなく...4点目は「一旦置いて」、後で「対応しよう」になることが多いので先に対応方法をまとめておきたいと思いました。
試してみたこと
- LinearLayout と ConstraintLayout の違いの比較
- Layoutの高さを固定しないようにする
- AutoSizingTextView
sampleアプリ
キャプチャ(フォント標準)
単純なレイアウトですが、この時点で崩れはありません。
キャプチャ(フォント最小)
小さくするだけなので、崩れはありません。
キャプチャ(フォント最大)
このあたりで差が生まれてきます。
- LinearLayout が ConstraintLayoutよりも崩れが大きいように見える(合計のところ)のは、 合計のwidthが固定だから
- 高さをwrap_contentにして、文字サイズ(20sp + margin)で高さを合わせるのが一番応用が効きそう (ただし、見た目のレイアウトの上下は変わる...スクロールないとダメかも)
- TextViewAutoSizingは一番いい感じにしてくれそうだけど、全部同じspにはできないから、 どの道ダメなパターンは出てきそう(同じspにするとクラッシュする)。
Ng <TextView android:id="@+id/amount3_cl_3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical|end" android:layout_marginEnd="10dp" android:textSize="30sp" android:autoSizeTextType="uniform" android:autoSizeMaxTextSize="30sp" android:autoSizeMinTextSize="30sp" android:textStyle="bold" android:text="¥3,000" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> Safe <TextView android:id="@+id/amount3_cl_3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical|end" android:layout_marginEnd="10dp" android:textSize="30sp" android:autoSizeTextType="uniform" android:autoSizeMaxTextSize="32sp" android:autoSizeMinTextSize="20sp" android:textStyle="bold" android:text="¥3,000" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>まとめ
レイアウトを崩さない対応するなら
dp
にするのが一番楽ですが、
ユーザビリティの低下・ソースが警告で読みにくくなるのであまり採用したくない手法です。TextViewAutoSizingをきちんと扱えれば一番良さそうですが、
情報が少なくて表示をコントロールするには一苦労しそうです。
そもそも、高さ・幅を固定にする前提のレイアウトや文字が詰まったレイアウトにしないのが一番いいかもしれません。
(キャプチャのうち、3つ目のパターン)一旦、整理してみましたが、あまりいい方法を整理できませんでした..
ベストプラクティスがあれば共有して欲しいです...追伸
後、アプリ起動中にフォントを変更すると、
アプリが一度キルされて、再度作成されるのでそれも予期せぬ不具合を生みます...
- 投稿日:2020-02-24T15:07:29+09:00
Material Design motion system (Beta)とAndroidの実装 (Android Material Component 1.2.0-alpha05時点)
きっかけ
Android Material Component 1.2.0-alpha05がリリースされ、これが入ったので見てみました。
https://github.com/material-components/material-components-android/releases
概要
このドキュメントによると、Material Design’s motion systemのUIのコンポーネントや画面の遷移のパターンは4つから構成されます。
https://material.io/design/motion/the-motion-system.html#transition-patterns
- Container transform
- Shared axis
- Fade through
- Fade
それぞれの説明と実装方法
基本的にはここにあるとおりですが、ちょっとかいつまんで説明します。
https://material.io/develop/android/theming/motion/#material-container-transformContainer transform
共通の要素がある場合の遷移、リストの一行と詳細画面が関連あるなどの場合。
例:
- DroidKaigiのアプリでいうとセッション一覧とセッション詳細。
- セッション詳細にあるスピーカーの情報とスピーカー詳細。どんなアニメーション?
AndroidでいうとViewGroupなどコンテナになっている部分をSharedElementTransionするイメージ
実装方法
MaterialContainerTransformを利用します。MaterialContainerTransformはAndroidXではなくAndroidのプラットフォームにあるandroid.transition.Transitionを継承しています。
これまでは画像など単体のViewをShared Element Transitionとして共有していましたが、この方法ではViewGroupなどを共有します。MaterialContainerTransformは具体的にどうやってレイアウトを描画するか?
MaterialContainerTransform.TransitionDrawableというのがMaterialContainerTransformの中にあり、そのcanvasに始まりと終わりのViewをdrawすることによって実現しているようです。結構コード読みやすいので、ハマったらコード読むと解決できます。
※ MaterialContainerTransform.TransitionDrawable内
Fragment AからFragment Bに遷移するときの基本的な実装方法
- 遷移元フラグメントFragment A transitionNameを遷移元レイアウトにつけ、FragmentNavigatorExtrasで渡して(Navigationを使わないでやる場合は
.addSharedElement(view, “shared_element_container”)
といった感じでやるみたいです。)、Navigationのextraで渡してあげればOKです。// 今まではImageViewなどにつけていたが、レイアウトにtransitionNameをつける binding.root.transitionName = "${speaker.id}-${SessionDetailFragment.TRANSITION_NAME_SUFFIX}" binding.root.setOnClickListener { // ViewとTransitionNameの関連付けをextraに入れる。 val extras = FragmentNavigatorExtras( binding.root to binding.root.transitionName ) binding.root.findNavController() .navigate( SessionDetailFragmentDirections.actionSessionToSpeaker( speaker.id, SessionDetailFragment.TRANSITION_NAME_SUFFIX, null ), // extraをNavigationで渡す extras ) }
- Fragment BのenterTransitionにMaterialContainerTransform()を入れる。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedElementEnterTransition = MaterialContainerTransform(requireContext())
- 行き先のルートViewなどに同じTransitionNameをつけてあげる。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = FragmentSpeakerBinding.bind(view) binding.speakerRoot.transitionName = "${navArgs.speakerId}-${navArgs.transitionNameSuffix}"基本的にはMaterialContainerTransformを使うこと以外はレイアウトを指定したShared Element Transitionを実装すればOKです
プルリクで実装を見たければこちらで見られます。
https://github.com/DroidKaigi/conference-app-2020/pull/810/filesハマりどころ(今回は詳しく書かないです)
- exit transitionがデフォルトでAndroidXのtransitionになっていて、enter transitionがプラットフォームのtransitionになるのでクラッシュする。両方プラットフォームかAndroidXのtransitionでなくてはならない。
- アニメーションする先が画面全体ではない場合(DroidKaigiアプリではToolbarがActivityにあった)、drawingViewIdを使って画面の裏がフェードする範囲を変える必要がある。
- Transition対象のViewにはbackgroundをつけておかないとうまく動かなかったりする。
MaterialContainerTransform.setDrawDebugEnabledを使うことでアニメーションのデバッグができます。
Shared axis
空間やナビゲーションなどで位置関係があるUI要素での切り替え。X Y Z軸でスライドさせるような形で利用でき、要素間の関係を強化できる。
例:
- チュートリアルで下にカルーセルがあって、押したときに横にスクロールされて切り替えるなど。並列の要素で位置関係があるもの。MaterialSharedAxisというTransitionを利用します。
詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101Fade through
お互いに関係がないUI要素間での切り替えに利用します。BottomNavigationでの切り替えなどに利用します。
MaterialFadeThroughというTransitionを利用します。
詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101Fade
例えばFabが表示されたり、Dialogが表示されたり消えたりと言った。UI要素が表示されるときに利用するパターンのようです。
MaterialFadeというTransitionを利用します。
https://material.io/develop/android/theming/motion/#fade
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionFadeDemoFragment.java#L62まとめ
新しいMaterial Design motion systemではアニメーションの種類がかなり絞られて、また実装的にも簡単にできるように工夫されており、Transitionでハマって時間をめちゃくちゃ吸われるようなことが少なくなりました。
Jetpack Composeにも将来的には同じような仕組みが入るのかなと予想できますが待ってみましょう。
- 投稿日:2020-02-24T02:28:13+09:00
AndroidのAudioTrackで繰り返し再生する
はじめに
初投稿です。
Androidでメトロノームアプリを作りたいと思い、
AudioTrackで実装を進めていましたが、かなりハマったので備忘のために残します。環境
Windows10
AndroidStudio
Kotlin
minSdkVersion:23
targetSdkVersion:28AudioTrack
MediaPlayer、SoundPool、AudioTrackがありますが、
繰り返し同じリズムを刻むにはAudioTrackが良いようでした。初期化
AudioTrack.Builder()を使います。
・setUsage:USAGE_MEDIA=音楽など
・setContentType:CONTENT_TYPE_MUSIC=音楽など
他にも音声やビープ音などもありますが、変更しても何も変わりませんでした。・setEncoding:ENCODING_PCM_8BIT
・setSampleRate:44100
・setChannelMask:CHANNEL_OUT_MONO
再生するデータに合わせます。・setBufferSizeInBytes:バッファサイズ
再生するデータに合わせます。
MODE_STREAMの場合は余裕がないと途切れます。2~4倍くらい?・setTransferMode:MODE_STREAMまたはMODE_STATIC
MODE_STREAM→バッファにデータを追加して、消費していくイメージ。
MODE_STATIC→最初にセットしたデータを使いまわす。var audioTrack: AudioTrack? = null val SMPL = 44100 val bufSize = SMPL * 2 val mode = AudioTrack.MODE_STATIC fun init() { audioTrack = AudioTrack.Builder() .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) .setAudioFormat( AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_8BIT) .setSampleRate(SMPL) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build() ) .setBufferSizeInBytes(bufSize) .setTransferMode(mode) .build() }主要メソッド
各メソッドのイメージがつかみにくかったので、
自分なりに解釈した内容です。
これが分かるまで、呼び出す順番が良くわかりませんでした。音が出るタイミングは、直観的にはplayだと思い込んでいたのですが、
writeのタイミングでした。ここでかなりハマりました。
メソッド 目的 play playStateを再生状態(PLAYSTATE_PLAYING)にする。 stop playStateを停止状態(PLAYSTATE_STOPPED)にする。 write データを再生バッファに書き込む。 flush 再生バッファのデータをクリアする。 release AudioTrackオブジェクトを破棄する。 音を再生する
呼び出し元ではinit→readyの後、playを繰り返します。
MODE_STATICの場合
前処理でデータをセットして、
あとはreloadStaticData()で繰り返し再利用する感じです。同じパターンを繰り返し再生するメトロノームは、
こちらが適しているようでした。毎回stop()していますが、stop()なしでも連続再生できます。
今回の要件的に必要なので入れています。ループ再生もできますが、
再生タイミングとUI操作を同期する要件があるので使用していません。//前処理 fun ready(arr: ByteArray) { //バッファにあらかじめデータをセットしておく audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING) } //再生 fun play(arr: ByteArray) { if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) { //再生中の場合は止める audioTrack?.stop() //再生バッファをクリアする audioTrack?.flush() } //読み込み済データを再度読み出す audioTrack?.reloadStaticData() //再生バッファにデータを書き込む audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING) //再生中にする audioTrack?.play() }MODE_STREAMの場合
再生完了時にいったんstop()しないと音が出なくなります。
また、バッファの中身が枯渇すると以下のエラーが出て、1回分の再生がスキップされます。
「releaseBuffer() track %p name=%s disabled due to previous underrun, restarting」前処理でバッファを埋めておくことで一応解決しましたが、
この方法で良いのかどうかわかりません。。//前処理 fun ready(arr: ByteArray) { if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) { //再生中の場合は止める audioTrack?.stop() audioTrack?.flush() } if (audioTrack != null) { //バッファを埋めておく val loopCount = bufSize / arr.count() for (i in 0 until loopCount) { audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING) } } } //再生 fun play(arr: ByteArray) { //再生バッファにデータを書き込む audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING) //1回分の再生終了を検知して停止する audioTrack?.setPlaybackPositionUpdateListener( object : AudioTrack.OnPlaybackPositionUpdateListener { override fun onPeriodicNotification(track: AudioTrack){ } override fun onMarkerReached(track: AudioTrack) { //再生完了 if(track.playState == AudioTrack.PLAYSTATE_PLAYING){ //停止 track.stop() } } } ) //再生終了検知するためのNotificationをセット audioTrack?.setNotificationMarkerPosition(arr.count()) //再生状態にする if (audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) { audioTrack?.play() } } ```kotlin:停止・終了(共通)
//停止 fun stop() { if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) { //再生中の場合は止める audioTrack?.stop() //再生バッファをクリアする audioTrack?.flush() } } //終了時にオブジェクトを破棄する fun release() { try { this.stop() audioTrack?.release() audioTrack = null Logger.set(LogKind.INFO, "sound released") } catch (e: Exception) { Logger.set(LogKind.ERROR, e.toString()) } }WRITE_BLOCKINGとWRITE_NON_BLOCKING
writeメソッドでWRITE_BLOCKINGとWRITE_NON_BLOCKINGが指定できます。
WRITE_BLOCKINGは前の再生が終わるまで次の再生を待ち、
WRITE_NON_BLOCKINGは前の再生が終わってなくても次の再生をする感じでした。WRITE_NON_BLOCKINGにすると、繰り返す際に
最後の音と最初の音がかぶって再生されることがありました。今回の要件ではBPMを保ち、一定の間隔で繰り返し再生する必要があるため、
WRITE_BLOCKINGで再生のたびに停止→再生することで開始のタイミングがずれないようにしています。おわりに
情報が少ない&古いものが多く、手探りでの実装となりましたので、
より良い方法をご存知の方は是非、教えていただけると幸いです。
- 投稿日:2020-02-24T01:46:17+09:00
JSON 以外のデータ形式を Retrofit で扱う(なら Converter を実装する)
概要
JSON 以外のデータ形式を Retrofit で扱う方法について述べます。時間がない方は Retrofit の公式 を見てください。
前提
Retrofit2 の使い方については知っているものとします。
なぜやるのか?
趣味で RSS リーダーを自作したいと思った際に、XML を Retrofit で扱うにはどうしたら良いのかとわからなくなり、だったら自分で実装しようと思ってやってみたので、それを残しておきます。
RSS は XML の派生形式です。JSON ではありません。かつては、Retrofit で XML を扱う際は Simple XML を使うのが一般的のようでした。
Deprecation
……が、いま SimpleXmlConverterFactory をコードに書くと Deprecated の警告が出ます。
@deprecated we recommend switching to the JAXB converter.
「JAXB の Converter を使いましょう」とのことです。
does not work on Android.
ではと思って JAXB Converter を見に行くと、README に以下の記述があります。
Note that JAXB does not work on Android.
ちょっと何て書いてあるか英語がよくわからないですね。Android で XML を処理したいという開発者のニーズは存在しなくなったのでしょうか?Issue
どうしたらよいのかと思って調べたら、別に悩んでいる人がいました。
……が、特に解決策があるわけでもないようです。Solution?
もうちょっと探したら、以下のQ&Aを発見しました。 Tickaroo TikXML というものがあるらしいです。
What kind of XmlConverter can I use for Retrofit in Android?GitHub Repository
Retrofit2 用の Converter も用意されています。
implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.15'普通の方はこれを用いると良いでしょう。
My solution?
上の Converter の導入が上手くいかないので諦め、ほかにどうしたらよいのかと思って調べたら、
faruktoptas/RetrofitRssConverterFactory という Converter を見つけました。 これを見ると、自分で Converter を作ればどんなレスポンスでもオブジェクトに Mapping できるようです。RSS のパースに関しては過去に自分で作った Parser があるので、せっかくだからそれを使ってみました。
Official solution
これは大発見でも何でもなく、 Retrofit の公式 にちゃんと書いてありました。
If you need to communicate with an API that uses a content-format that Retrofit does not support out of the box (e.g. YAML, txt, custom format) or you wish to use a different library to implement an existing format, you can easily create your own converter. Create a class that extends the Converter.Factory class and pass in an instance when building your adapter.
というわけで、HTTP 通信で JSON 以外のデータをやり取りするアプリでは、 Retrofit の Converter をがんばって作りましょう。もしくは JSON に置き換えましょう。
実装
Converter の実装
今回重要なのはここです。retrofit2.Converter インターフェイスを実装します。
convert
メソッドで、受け取った OkHttp3 の ResponseBody を特定の型に変換します。
この処理を変えることで、あらゆるデータ形式を扱うことができるようになります。class RssResponseConverter : Converter<ResponseBody, Rss> { private val parser = Parser() override fun convert(responseBody: ResponseBody): Rss? { val bodyString = responseBody.string() return parser.parse(bodyString.split(findSplitter(bodyString))) } private fun findSplitter(body: String) = if (body.contains(LINE_SEPARATOR)) LINE_SEPARATOR else "\n" companion object { private val LINE_SEPARATOR = System.getProperty("line.separator") } }Parser
今回は勉強だと思ってゴリゴリのパーサーを自作しました。実際の開発は既存のライブラリーを用いましょう。
Converter.Fatctory の実装
先ほど実装した Converter のインスタンスを生成するファクトリーを返すクラスです。
class RssConverterFactory private constructor(): Converter.Factory() { override fun responseBodyConverter( type: Type?, annotations: Array<Annotation>?, retrofit: Retrofit? ): Converter<ResponseBody, *> = RssResponseConverter() companion object { fun create() = RssConverterFactory() } }Converter の利用
あとは addConverterFactory で自作の ConverterFactory を指定して、Retrofit を普通に使いましょう。Retrofit の普通の使い方については多数の記事があるので、ここでは特に触れません。
class RssReaderApi { private val converter = RssConverterFactory.create() @WorkerThread operator fun invoke(rssUrl: String): Rss? { val uri = rssUrl.toUri() val retrofit = Retrofit.Builder() .baseUrl("${uri.scheme}://${uri.host}") .addConverterFactory(converter) .build() val service = retrofit.create(RssService::class.java) val call = service.call(rssUrl) return call.execute().body() } }一応書いておくと RssService インターフェイスはこんな感じです。
interface RssService { @GET fun call(@Url url: String): Call<Rss?> }おわりに
JSON 以外のデータ形式、今回は XML を Retrofit で扱う方法について述べました。
で、これを知って何の役に立つの?
自分で Converter を作れば、どんなデータ形式でも(CSVやYAML等々)扱うことができるということだけ知っておけば良いかと思います。
新しいデータ形式や社内独自のデータ形式も Retrofit で扱えて幸せになるかもしれません。そして、メジャーなデータ形式であれば、誰かが Converter を作っているかもしれないので、それを GitHub から探し出した方が早いかもしれない、ということを知っておくのも大事なことです。
- 投稿日:2020-02-24T00:54:20+09:00
BaseAdapter で ViewHolder パターンを実装する
概要
BaseAdapter で ViewHolder を実装する方法について述べます。主に Spinner を扱う際に役立ちます。
なぜやるのか?
Spinner と BaseAdapter を組み合わせた実装だと、LayoutInfrater.inflate する行で Android Studio の警告が出ます。
Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling.
別にこれを放置してもビルドはできますし、Spinner でスクロールのパフォーマンスが致命的になるほど要素を詰め込むことはそうそうないので、直さなくても構わないのではないかと思ってしまいがちです。実際私もずっと放置していました。
(ちなみに、リンク先を開くと RecyclerView のドキュメント が出てきます)
……が、昨今では技術者の評価に GitHub のリポジトリーを見ることも増えてきているようです。こうした雑なコードを残しておくのは自身の今後に悪い影響を与えかねないため、直しておくべきでしょう。
というのは冗談です。ViewHolder パターン自体は普遍的なデザインパターンですので、覚えておいて損はしません。私も RecyclerView を使い始めるまでは何度か書いていました。もう数年前になるので完全に忘れていましたが、せっかくなので思い出します。
BaseAdapterで画像とテキストをListView表示
実装
1. ViewHolder クラスを定義
各 View を持つクラスを定義します。これは Kotlin だと簡単です。Item を受け取って属性を各 View に当てる関数もこのクラスに実装させると良いでしょう。
class ViewHolder(private val icon: ImageView, private val text: TextView) { fun bindItem(item: Item) { icon.setImageDrawable(AppCompatResources.getDrawable(context, item.iconId)) text.setText(item.id) } }2. getView で convertView の状態による分岐を書く
2-1 convertView が null の時
以下の 1-6 の処理をします。
- Item を入れる Layout を inflate
- 要素 View を find
- ViewHolder を初期化
- view の tag に ViewHolder のインスタンスをセット
- viewHolder に item の属性を設定
- inflate した Layout を return
if (convertView == null) { val view = inflater.inflate(LAYOUT_ID, parent, false) val icon = view.findViewById<ImageView>(R.id.image) val text = view.findViewById<TextView>(R.id.text) val viewHolder = ViewHolder(icon, text) view.tag = viewHolder viewHolder.bindItem(item) return view }2-2 convertView が null でない時
Layout の inflate は不要なので、 tag にセットした ViewHolder を取り出して item の属性を View に当てればよいです。
val viewHolder = (convertView.tag as ViewHolder) viewHolder.bindItem(item) return convertView全体のコード
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val item = items[position] if (convertView == null) { val view = inflater.inflate(LAYOUT_ID, parent, false) val icon = view.findViewById<ImageView>(R.id.image) val text = view.findViewById<TextView>(R.id.text) val viewHolder = ViewHolder(icon, text) view.tag = viewHolder viewHolder.bindItem(item) return view } val viewHolder = (convertView.tag as ViewHolder) viewHolder.bindItem(item) return convertView }おわりに
Spinner を扱う際に知っておくと有益な ViewHolder パターンの実装について紹介しました。Kotlin だと大分書きやすくなったという印象を受けました。そもそも Spinner 自体そんなに使わないかもしれないですが、忘れた時にこのドキュメントが役立つとちょっとうれしいです。
参考