- 投稿日:2019-11-02T19:00:28+09:00
Android Dev Summit '19で紹介されたサンプルから学ぶ、AndroidのTheme、Style、Colorの設定方法
Android Dev Summit '19のDeveloping Themes with Styleで説明されているリソース定義の方法メモという感じで説明しようと思ったのですが、
https://www.youtube.com/watch?v=Owkf8DhAOSo全体的に、このリポジトリに入っている要素についての話が多かった、かつコードから参考になる部分が多かったので紹介していきます。
https://github.com/material-components/material-components-android-examples/Themeの定義方法
Owlではピンクのテーマの中にブルーのテーマが出てきたりなど複数のベースのテーマを切り替えることができる仕組みがあります。
Owlではそのピンクのテーマとブルーのテーマで共通のテーマ
Base.Owlを作って利用する方針にしているようです。
https://github.com/material-components/material-components-android-examples/blob/ff18481c00e878e9760ce57718e2b4195113bac6/Owl/app/src/main/res/values/theme.xmlShapeやTypographyなどなど、参考になります
<!-- ** ここにベーステーマがある ** --> <style name="Base.Owl" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar"> <!--Material shape attributes--> <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.Owl.SmallComponent</item> ... <!--Material type attributes--> <item name="textAppearanceHeadline1">@style/TextAppearance.Owl.Headline1</item> ... <!--Platform attributes--> <item name="android:navigationBarColor">@color/nav_bar</item> <item name="android:statusBarColor">@color/immersive_sys_ui</item> <!--Default styles--> <item name="bottomNavigationStyle">@style/Widget.Owl.BottomNavigationView</item> </style> <!-- ** ベーステーマを継承してテーマが作られている ** --> <style name="Owl" parent="@style/Base.Owl"/> <style name="Owl.Yellow"> <item name="colorPrimary">@color/owl_yellow_500</item> <item name="colorPrimaryVariant">@color/owl_yellow_400</item> <item name="colorSecondary">@color/owl_blue_700</item> <item name="colorSecondaryVariant">@color/owl_blue_800</item> </style> <style name="Owl.Blue"> <item name="colorPrimary">@color/owl_blue_700</item> <item name="colorPrimaryVariant">@color/owl_blue_800</item> <item name="colorSecondary">@color/owl_yellow_500</item> <item name="colorSecondaryVariant">@color/owl_yellow_400</item> </style>逆に1テーマだけでやっていっているサンプルであるReplyでは普通にそのまま記述されていました。
https://github.com/material-components/material-components-android-examples/blob/ff18481c00e878e9760ce57718e2b4195113bac6/Reply/app/src/main/res/values/themes.xml<!--Final, top-level theme--> <style name="Theme.Reply.DayNight" parent="Theme.Reply"/> <style name="Theme.Reply" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!--Color--> <item name="colorPrimary">@color/reply_blue_700</item> ...Themeの命名規則
Theme.アプリ名.バリエーション名
Theme.AppName.Blue
(Owlは守っていませんが。。)
https://youtu.be/Owkf8DhAOSo?t=1193Themeの使い方
ActivityでBlueが指定されています。ここは普通ですね
<activity android:name=".ui.MainActivity" android:theme="@style/Owl.Blue">OwlではFragmentごとにThemeが異なるので、 Fragmentのレイアウト全てで以下のようにthemeを指定しています。
<FrameLayout android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" ** android:theme="@style/Owl.Pink" ** tools:context=".ui.learn.LearnFragment">またアプリ内で一部テーマを変えたい場所は以下のようにThemeOverlayを作って利用しています。これによって
ThemeOverlayを使うことで、外側のテーマのshapeなどをそのまま使いながら、指定した部分だけ変更できます。<com.google.android.material.appbar.AppBarLayout ... android:theme="@style/ThemeOverlay.Owl.Blue.Dark"><style name="ThemeOverlay.Owl.Blue.Dark" parent="@style/ThemeOverlay.MaterialComponents.Dark"> <item name="colorPrimary">@color/owl_blue_700</item> <item name="colorPrimaryVariant">@color/owl_blue_800</item> <item name="colorSecondary">@color/owl_yellow_500</item> <item name="colorSecondaryVariant">@color/owl_yellow_400</item> <item name="colorOnPrimary">#fff</item> </style>Style
Themeによって色などを変更しているため、ほとんどStyleは使われていません。以下の2つだけでした。
<style name="Widget.Owl.BottomNavigationView" parent="@style/Widget.MaterialComponents.BottomNavigationView.PrimarySurface"> <item name="labelVisibilityMode">selected</item> <item name="itemIconTint">@color/bottom_nav_item</item> <item name="itemTextColor">@color/bottom_nav_item</item> <item name="itemTextAppearanceActive">@style/TextAppearance.Owl.BottomNavigation</item> </style> <style name="Widget.Owl.SeekBar" parent="@style/Widget.AppCompat.SeekBar"> <item name="android:progressTint">@color/owl_yellow_500</item> <item name="android:secondaryProgressTint">#99ffffff</item> <item name="android:thumbTint">@color/owl_yellow_500</item> <item name="android:progressDrawable">@drawable/seekbar_track</item> <item name="android:paddingStart">0dp</item> <item name="android:paddingEnd">0dp</item> </style>Styleの命名規則
Widget.アプリ名.ウィジェット名.バリエーション名
Widget.AppName.Toolbar.Blue
これによって、ThemeとしてStyleを利用してしまっていたり、StyleをThemeとして利用している場合にきづけるようになります。StyleとThemeの保存場所
themeもstyleもtypographyも、すべてstyleタグなのでstyles.xmlに保存するとカオスになります。
そのため、目的によってファイルを分けます。
以下の場所に保存するようです。themes.xml : themeとthemeoverlay
type.xml : TextAppearance、テキストサイズのdimention
styles.xml : ウィジェットのStyleのみhttps://youtu.be/Owkf8DhAOSo?t=1383
Color
こちらはAndroid Dev Summit '19のセッション内で説明があって、意味のある名前ではなく、色の名前は色をそのまま表すような名前にするのが良いという説明がありました。
https://youtu.be/Owkf8DhAOSo?t=1093
bad good
- 投稿日:2019-11-02T18:09:50+09:00
Medium on Android で GoldenDict を使う
Medium で記事を読んでいると時折知らない単語が出てくる。そんなとき iOS であれば簡単に辞書を引けるのだろうが、あいにく Android ではこのへんのサービスレベルが低い。ブラウザ上ではアドオンを使って似たようなこともできるようだが、Medium の専用アプリを使っているとこの手も使えない。結局、範囲選択→コピー→アプリ切替→ペーストという冴えない動作が必要になる。Look Up を使うと英英辞典を引けるらしいが、日本語や非西欧言語は蚊帳の外のようだ。
Medium アプリでは共有メニューから選択範囲を辞書へ渡すことも一応できる。一応、というのは、実際に渡される文字列は
“選択文字列” by AUTHORNAME https://link.medium.com/ARTICLEIDとなるからだ。これでは辞書を引くことはできない。英語以外の言語も扱う必要から、私は GoldenDict を使っている。Google 翻訳アプリを導入すると、「タップして翻訳」という機能が使える。これは常駐してクリップボードを監視する。クリップボードへテキストが入り、オーバーレイ表示されているアイコンがクリックされると、Google 翻訳の結果を表示してくれる。使い勝手はなかなかよい。だが、Google 翻訳は辞書ではないので単語を調べるには不向きだ。Google がこちらの操作を監視しているのもなんとなく気持ち悪い。 Android を使っておいてなんだという気もするけれど。
仕方がないので、共有メニューから渡された文字列を適当に加工して再度辞書アプリへ投げる処理を用意する。今回は Automagic を使う。トリガーは
Send/Share Intent Receivedとし、文字列中にlink.medium.comがあればよけいな部分を除去して単語だけを抽出し、それを GoldenDict へインテントで送る。見切れている部分は次のとおり。
mobi.goldendict.android.PopupActivity
putString("android.intent.extra.TEXT", "{text}")辞書を引くには、範囲選択→SHARE→Send/Share Intent Received とする。手元の端末では GoldenDict に英和辞典以外の辞書も導入してあるから、いつでも多言語の迷宮に入れる。
MacroDroid でも同じようなものを作ろうとしたが、インテント受信の選択肢はあるのに、なぜか共有メニューで MacroDroid が出てこなかった。調べ方が悪いのか、明示的インテントしか受けられない仕様なのかはわからないが、惜しい。
- 投稿日:2019-11-02T17:15:54+09:00
Glide4系のDiskCacheについて
ギガを食うのを改善したいとの要望がありGlideのDiskCacheを調査したときのログ
TL;DL
- /data/data/app_dir/image_manager_disk_cacheに保存される
- キャッシュアルゴリズムはLRU(Least Recently Used)
- キャッシュ容量はデフォルトで250MB(250*1024*1024)
キャッシュの保存場所や容量について
デフォルトはinternalなimage_manager_disc_cacheディレクトリに容量250MBまで保存可
これらはGlideModuleを定義すれば変更可
example.java@GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setDiskCache(new ExternalCacheDiskCacheFactory(context)); } } @GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { int diskCacheSizeBytes = 1024 * 1024 * 100; // 100 MB builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes)); } } @GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { int diskCacheSizeBytes = 1024 * 1024 * 100; // 100 MB builder.setDiskCache( new InternalCacheDiskCacheFactory(context, "cacheFolderName", diskCacheSizeBytes)); } }refs
キャッシュ戦略とアルゴリズムについて
デフォルトではAUTOMATIC
リモートデータは特に処理を加えずそのままキャッシュする。ローカルデータは必要があればリサイズ等の処理を加えた上でキャッシュする。
実装
image_manager_disc_cacheには
.0拡張子ファイルとjornalが存在する
これらはDevice File Explorerでも確認可jornalの中身は以下のような構成
libcore.io.DiskLruCache 1 100 2 CLEAN 832 21054 DIRTY 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
3400330d1dfc7f3f7f4b8d4d803dfcf6→ 画像urlから生成されるハッシュ値
3934 2342→ メタデータと実レスポンスサイズ
DIRTY→ キャッシュ中
CLEAN→ キャッシュ済み
READ→ アプリによりアクセス中
REMOVE→ 削除対象これらステータスを逐一読み取りキャッシュ管理を行う
アルゴリズム
LRU(Least Recently Used)
上記実装で更新が走ったものが後列に積まれるジャーナルのリビルド
キャッシュサイズが上限に達する or
redundantOpCount(REMOVE) が2000以上かつサイズがhalveになっている場合にjournalのリビルド及びcacheの削除を行う(とコメントにあったが実装と食い違っているような気がするので詳しい人教えてください。)DiskLruCahe.java/** * We only rebuild the journal when it will halve the size of the journal * and eliminate at least 2000 ops. */ private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); }DiskLruCahe.javaprivate void trimToSize() throws IOException { while (size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); } } /** This cache uses a single background thread to evict entries. */ final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DiskLruCacheThreadFactory()); private final Callable<Void> cleanupCallable = new Callable<Void>() { public Void call() throws Exception { synchronized (DiskLruCache.this) { if (journalWriter == null) { return null; // Closed. } trimToSize(); if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0; } } return null; } };refs
https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/engine/DiskCacheStrategy.java
https://futurestud.io/tutorials/retrofit-2-analyze-cache-filesapendix
主要なクラスの関係は以下の通り
DiskLruCache.java
|
DiskLruCacheWrapper.java
|
DiskLruCacheFactory.java
|
InternalCacheDiskCacheFactory.java/InternalCacheDiskCacheFactory.java
|
GlideBuilder.java
- 投稿日:2019-11-02T17:15:54+09:00
Glide4系のDiskCacheStrategyについて
ギガを食うのを改善したいとの要望がありGlideのDiskCacheを調査したときのログ
TL;DL
- /data/data/app_dir/image_manager_disk_cacheに保存される
- キャッシュアルゴリズムはLRU(Least Recently Used)
- キャッシュ容量はデフォルトで250MB(250*1024*1024)
キャッシュの保存場所や容量について
デフォルトはinternalなimage_manager_disc_cacheディレクトリに容量250MBまで保存可
これらはGlideModuleを定義すれば変更可
example.java@GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setDiskCache(new ExternalCacheDiskCacheFactory(context)); } } @GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { int diskCacheSizeBytes = 1024 * 1024 * 100; // 100 MB builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes)); } } @GlideModule public class YourAppGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { int diskCacheSizeBytes = 1024 * 1024 * 100; // 100 MB builder.setDiskCache( new InternalCacheDiskCacheFactory(context, "cacheFolderName", diskCacheSizeBytes)); } }refs
キャッシュ戦略とアルゴリズムについて
デフォルトではAUTOMATIC
リモートデータは特に処理を加えずそのままキャッシュする。ローカルデータは必要があればリサイズ等の処理を加えた上でキャッシュする。
実装
image_manager_disc_cacheには
.0拡張子ファイルとjornalが存在する
これらはDevice File Explorerでも確認可jornalの中身は以下のような構成
libcore.io.DiskLruCache 1 100 2 CLEAN 832 21054 DIRTY 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
3400330d1dfc7f3f7f4b8d4d803dfcf6→ 画像urlから生成されるハッシュ値
3934 2342→ メタデータと実レスポンスサイズ
DIRTY→ キャッシュ中
CLEAN→ キャッシュ済み
READ→ アプリによりアクセス中
REMOVE→ 削除対象これらステータスを逐一読み取りキャッシュ管理を行う
アルゴリズム
LRU(Least Recently Used)
上記実装で更新が走ったものが後列に積まれるジャーナルのリビルド
キャッシュサイズが上限に達する or
redundantOpCount(REMOVE) が2000以上かつサイズがhalveになっている場合にjournalのリビルド及びcacheの削除を行う(とコメントにあったが実装と食い違っているような気がするので詳しい人教えてください。)DiskLruCahe.java/** * We only rebuild the journal when it will halve the size of the journal * and eliminate at least 2000 ops. */ private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); }DiskLruCahe.javaprivate void trimToSize() throws IOException { while (size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); } } /** This cache uses a single background thread to evict entries. */ final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DiskLruCacheThreadFactory()); private final Callable<Void> cleanupCallable = new Callable<Void>() { public Void call() throws Exception { synchronized (DiskLruCache.this) { if (journalWriter == null) { return null; // Closed. } trimToSize(); if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0; } } return null; } };refs
https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/engine/DiskCacheStrategy.java
https://futurestud.io/tutorials/retrofit-2-analyze-cache-filesapendix
主要なクラスの関係は以下の通り
DiskLruCache.java
|
DiskLruCacheWrapper.java
|
DiskLruCacheFactory.java
|
InternalCacheDiskCacheFactory.java/InternalCacheDiskCacheFactory.java
|
GlideBuilder.java
- 投稿日:2019-11-02T17:04:10+09:00
Firebase Dynamic Linksによる起動時の細かい挙動メモ
本当に細かい挙動の話です。
Firebase Dynamic Links(FDL)が起動されるパターンは以下があります
- アプリ未インストール時、FDLによってGoogle Playからインストールされてから起動される場合
- アプリがインストールされている時、FDLからアプリを直接起動する場合
1. アプリ未インストール時、FDLによってGoogle Playからインストールされてから起動される場合
activity.intent.dataString は空になっており FDLの
FirebaseDynamicLinks.getInstance().getDynamicLink(activity.intent)からでないとリンクが取得できない。
Google PlayやLauncherなどから起動されてくるので当たり前といえば当たり前なのですが、そうなります。
ちなみにFDLはどのようにリンクを取得してくるかというと、プロセス間通信によって、Google Play Servicesから取得しているようです。2. アプリがインストールされている時、FDLからアプリを起動する場合
activity.intent.dataStringにもリンクのURLが入っていて、FDLからも取得可能です
そのためリンクを取得するだけであれば、わざわざFDLを使う必要はないです。
しかし、FDLではリンクにパラメーターamvをつけて、リンクを開くことができるアプリの最小バージョンの versionCodeなどを設定でき、そのための情報などが渡ってきたりするので、その情報が欲しい場合はFDLから情報を取得する必要があります。
https://developers.google.com/android/reference/com/google/firebase/dynamiclinks/PendingDynamicLinkData
- 投稿日:2019-11-02T14:53:50+09:00
Android FragmentContainerViewとは
はじめに
AndroidでFragmentを表示する際のコンテナに、何を利用していますか?
一般的なコンテナとして<FrameLayout>を、Navigation ComponentのFragmentのコンテナとして<fragment>を利用できます。これからはandroidx.fragment 1.2.0から導入された
FragmentContainerViewを利用するべきだと考えています。
この記事ではFragmentContainerViewを調査した結果を記載しています。FragmentContainerViewの概要
ActivityでFragmentを表示する際にActivity側でFragmentのコンテナとなるViewを作成する必要があります。
コンテナとなり得るViewの1つがFragmentContainerViewです。
xmlで記載する場合の例が下記です。example.xml<androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />FragmentContainerViewはFragmentのコンテナに特化したViewとなるので、FrameLayoutのようにそれ以外の用途で利用することはできません。
FragmentContainerViewとFrameLayoutの比較
Android Dev Summit 2019でFragmentContainerViewとFrameLayoutを比較した発表がありました。Fragments: Past, Present, and Future (Android Dev Summit '19)
この発表で取り上げている遷移アニメーション時のZ orderingのissueについて記載します。Z orderingのissue
準備
Z orderingのissueを再現するために、ホストとなるActivity、3つのFragment(FirstFragment、SecondFragment、ThirdFragment)、アニメーションのレイアウトを作成します。
ActivityはFragmentを表示する処理のみです。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) val fragmentTransaction = supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.nav_enter, R.anim.nav_exit) .replace(R.id.container, FirstFragment()) .addToBackStack(null) fragmentTransaction.commit() } }activity_main.xmlはFragmentのコンテナとなるViewのみとなります。
比較のため、まずはFrameLayoutを置きます。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".activity.MainActivity"> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- <androidx.fragment.app.FragmentContainerView--> <!-- android:id="@+id/container"--> <!-- android:layout_width="match_parent"--> <!-- android:layout_height="match_parent" />--> </androidx.constraintlayout.widget.ConstraintLayout> </layout>次にFragmentです。遷移アニメーションを確認するために、ボタンを押下すると次のFragmentに遷移させています。処理が同じであるためFirstFragmentのみ記載します。
FirstFragmentclass FirstFragment : Fragment() { @SuppressLint("CheckResult") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { super.onCreateView(inflater, container, savedInstanceState) val binding = DataBindingUtil.inflate<FragmentFirstBinding>( inflater, R.layout.fragment_first, container, false ) binding.apply { lifecycleOwner = this@FirstFragment destinationButton.clicks().subscribe { val fragmentTransaction = parentFragmentManager .beginTransaction() .setCustomAnimations(R.anim.nav_enter, R.anim.nav_exit) .replace(R.id.container, SecondFragment()) .addToBackStack(null) fragmentTransaction.commit() } } return binding.root } }次にアニメーションです。上記の
setCustomAnimationsの引数にしていしているEnterアニメーションとExitアニメーションです。nav_enter.xml<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" android:shareInterpolator="true"> <translate android:duration="400" android:fromXDelta="100%" android:fromYDelta="0%" android:toXDelta="0%" android:toYDelta="0%" /> </set>nav_exit<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" android:shareInterpolator="true" android:zAdjustment="bottom"> <translate android:duration="400" android:fromXDelta="0%" android:fromYDelta="0%" android:toXDelta="-50%" android:toYDelta="0%" /> </set>FragmentのコンテナがFrameLayoutの場合の結果
FragmentのコンテナがFragmentContainerViewの場合の結果
activity_main.xmlのFrameLayoutをコメントアウトし、FragmentContainerViewのコメントアウトを外し実行します。
考察
FrameLayoutの場合、まず次のFragmentのEnterアニメーションが始まり、次に現在のFragmentのExitアニメーションが始まるので、このような結果となっています。
これを防ぐために、nav_exit.xmlにandroid:zAdjustment="bottom"を指定(Z ordering)することにより解決を試みていますが、効いていません。これがZ orderingのissueです。FragmentContainerViewの場合、まず現在のFragmentのExitアニメーションが始まり、次に次のFragmentのEnterアニメーションが始まるので、このような結果となっています。
おまけ
Navigation ComponentのFragmentのコンテナとして
<fragment>がよく利用されます。
このコンテナをFragmentContainerViewに置き換えることができます。
実際にFragmentContainerViewを利用している例をこちらの記事に記載しています。
Android NavigationとSharedViewModel
- 投稿日:2019-11-02T14:53:09+09:00
Android NavigationとSharedViewModel
はじめに
Navigation Architecture ComponentはAndroid Jetpackに含まれているコンポーネントです。
AndroidアプリにおけるActivityやFragment間の画面遷移をシンプルに実装することができます。
1つのActivityをホストとし、複数のFragmentを管理するように設計されています。複数のFragmentで共通する処理をActivityに任せたくなるシーンに出会いませんか?
色々な実現方法があると思いますが、今回はSharedViewModelを用いた方法を紹介します。言語はKotlinでバージョンは1.3.50です。
概要
Navigationを利用し、FragmentからFragmentへ値渡しをする場合、通常Safe Argsを用いた型安全の値渡しをします。
しかし、ホストしているActivityと各Fragmentで何かを共有する場合工夫が必要です。
また、ToolbarのTitleの変更や、Snackbarの表示は各Fragmentで行うと、同じような処理を複数記述することになるので、まとめたいです。このような課題をSharedViewModelを用いて解決します。
図で表すとこのようになります。ActivityでSharedViewModelのLiveDataをobserveし、各FragmentでpostValueするだけでActivityに処理を任せられるようになります。
必要となるライブラリの導入
app/build.gradle// ... dependencies { // ... // Fragment implementation "androidx.fragment:fragment:1.2.0-rc01" // Lifecycle def arch_lifecycle_version = '2.1.0' implementation "androidx.lifecycle:lifecycle-runtime:$arch_lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version" implementation "androidx.lifecycle:lifecycle-reactivestreams:$arch_lifecycle_version" // Navigation def arch_navigation_version = '2.2.0-rc01' implementation "androidx.navigation:navigation-fragment:$arch_navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$arch_navigation_version" implementation "androidx.navigation:navigation-ui:$arch_navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$arch_navigation_version" }SharedViewModelの作成
今回作成するSharedViewModelは非常にシンプルです。
どのFragmentを表示しているかを共有するためのFragmentType型のMutableLiveDataを保有しています。SharedViewModel.ktclass SharedViewModel : ViewModel(){ val fragmentType = MutableLiveData<FragmentType>().apply { this.value = FragmentType.FIRST } }FragmentTypeをenumを用いて下記のように定義しました。
FragmentTypeenum class FragmentType(val type: String) { FIRST("first"), SECOND("second"), THIRD("third"), FOURTH("fourth") }ホストとなるActivityの作成
Navigationを利用する際の各FragmentをホストするActivityを作成します。
今回はSharedViewModelのfragmentTypeが変更された時にSnackbarが表示される処理を入れています。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivitySubBinding>(this, R.layout.activity_main) val sharedViewModel = ViewModelProviders.of(this).get(SharedViewModel::class.java) sharedViewModel.fragmentType.observe(this, Observer { value -> value?.let { Snackbar.make( findViewById(android.R.id.content), it.type, Snackbar.LENGTH_SHORT ).show() } }) } }SharedViewModelのfragmentTypeをobserveしています。
これにより、Fragment側でSharedViewModelのfragmentTypeをpostValueするだけで、Activity側でSnackbarを表示することができます。また、レイアウトは下記になります。
activity_main.xml<?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" xmlns:tools="http://schemas.android.com/tools"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".activity.SubActivity"> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container_view" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/activity_navigation" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>FragmentContainerViewをコンテナとして利用しています。
このnavGraph属性に次に説明するnavigationレイアウトを指定します。FragmentContainerViewについて気になる方はこちらの記事をご覧ください。
Android FragmentContainerViewとはnavigationレイアウトの作成
Fragmentを4つ(FirstFragment、SecondFragment、ThirdFragment、FourthFragment)用意しました。
navigation要素のstartDestination属性に最初に表示するfragment要素のidを指定しています。
今回はそれぞれのFragmentが「First→Second」「Second→Third」「Third→Fourth」「Fourth→First」という画面遷移させたかったので、action属性を下記のように記載しています。activity_navigation<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_navigation" app:startDestination="@id/firstFragment"> <fragment android:id="@+id/firstFragment" android:name="e.yoppie.sample.fragment.FirstFragment" android:label="FirstFragment"> <action android:id="@+id/action_first_to_second" app:destination="@id/secondFragment" /> </fragment> <fragment android:id="@+id/secondFragment" android:name="e.yoppie.sample.fragment.SecondFragment" android:label="SecondFragment"> <action android:id="@+id/action_second_to_third" app:destination="@id/thirdFragment" /> </fragment> <fragment android:id="@+id/thirdFragment" android:name="e.yoppie.sample.fragment.ThirdFragment" android:label="ThirdFragment"> <action android:id="@+id/action_third_to_fourth" app:destination="@id/fourthFragment" /> </fragment> <fragment android:id="@+id/fourthFragment" android:name="e.yoppie.sample.fragment.FourthFragment" android:label="FourthFragment"> <action android:id="@+id/action_fourth_to_first" app:destination="@id/firstFragment" /> </fragment> </navigation>4つのFragmentの作成
前述のFragmentを作成します。
同じ処理を持つFragmentなのでFirstFragmentのみ説明します。
Fragmentには、次へボタンを押下すると、次のFragmentへ遷移し、
Snackbarボタンを押すと、SharedViewModelのfragmentTypeをpostValueし値を変更する処理を作成しました。FirstFragmentclass FirstFragment : Fragment() { private lateinit var sharedViewModel: SharedViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedViewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class.java) } @SuppressLint("CheckResult") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { super.onCreateView(inflater, container, savedInstanceState) val binding = DataBindingUtil.inflate<FragmentFirstBinding>( inflater, R.layout.fragment_first, container, false ) binding.apply { lifecycleOwner = this@FirstFragment destinationButton.clicks().subscribe { findNavController().navigate(R.id.action_first_to_second) } snackbarButton.clicks().subscribe { sharedViewModel.fragmentType.postValue(FragmentType.FIRST) } } return binding.root } }Activity側でSharedViewModelのfragmentTypeをobserveしているので、Fragment側ではpostValueするだけでSnackbarが表示されます。
今回作成したアプリの一連の流れ
- 投稿日:2019-11-02T14:52:23+09:00
ActionBarに画像を表示する
はじめに
Androidアプリ作成において、ActionBarに画像を表示する方法のメモになります。
アイコンを表示する方法が出てきたり、ToolBarに差し替えましょう、とあったのですが、ロゴなどの画像をそのまま表示する方法は見受けられませんでした。
筆者はAndroidに詳しくなく、知っていれば簡単なのに検索しても簡易的な記事が見つからなかったので記事として残します。
環境は、
- AndroidStudio: 3.4.1
- Kotlin: 1.3.40-release-Studio3.4-1
です
結果
サンプルロゴは【Free Logo Maker — Create a Logo — Squarespace】で作成しました。
解法
手順としては、
- 画像の取り込み
- ImageViewのxmlを作成
- ActionBarにCustomViewを設定
です。
1. 画像の取り込み
New -> Image Assets で画像の取り込み。
2. ImageViewのxmlを作成
今回は
app/res/layout/logo_image_view.xmlという名前で作成しました。WidgetsのImageViewを追加し、取り込んだ画像を設定、レイアウトを整え点完了
3. ActionBarにCustomViewを設定
MainActivity.ktを開き、(2)で作成したImageViewをセットしますoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ActionBarにcustomViewを設定する supportActionBar?.setCustomView(R.layout.logo_image_view) // CustomViewを表示する supportActionBar?.setDisplayShowCustomEnabled(true) // ... }これだけで画像を表示するだけなら問題ないと思います、
おまけ: 画像を左寄せにするTips
logo_iamge_view.xmlをtextモードで開いて、ImageViewにandroid:scaleType="fitStart"を追加すればok参考: Android layout - alignment issue with ImageView - Stack Overflow
おわり
サンプルリポジトリ(SampleAppImageViewOnActionBar)を置いておきます
画像の取り込み方の最適解も知らず、レイアウトも雰囲気なのでご指摘いただけると幸いです!
- 投稿日:2019-11-02T14:09:16+09:00
【Android】DataBindingでチェックボックスの色(buttonTint)を動的に変更する
背景
下記の仕様を見たす画面を作るために、チェックボックスの色を動的に変更する必要がありました。
- チェックボックスが全て選択済みの場合のみ、ボタンタップで処理を行う
- 未選択のチェックボックスがある状態でボタンをタップした場合は、未選択のチェックボックスを警告色にする
- 他のチェックボックスの状態が変わったら再度デフォルト色に戻す
実装方針
MVVM + DataBindingで実装します。
ViewModelでチェックボックスの色と選択状態を保持します。チェックボックスの色はColorStateListで実装します。
デフォルト色と警告色の2種類のColorStateListをxmlで用意します。チェックボックスの色を変えるにはCompoundButtonの属性buttonTintを変更する必要があります。
デフォルトではbuttonTintにColorStateListのリソースIDをバインドできないため、カスタムBindingAdapterを実装します。ソースコード
完全なソースコードはGithubで公開しています。
下記はポイントとなるコードの抜粋です。main_fragment.xml<?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" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="com.example.checkboxcolorchange.ui.main.MainViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.main.MainFragment"> <CheckBox android:id="@+id/checkBox1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:checked="@{viewModel.isCheckedCheckBox1}" android:onClick="@{() -> viewModel.onCheckedChanged(@id/checkBox1)}" android:text="CheckBox1" app:buttonTint="@{viewModel.checkBox1Color}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <CheckBox android:id="@+id/checkBox2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:checked="@{viewModel.isCheckedCheckBox2}" android:onClick="@{() -> viewModel.onCheckedChanged(@id/checkBox2)}" android:text="CheckBox2" app:buttonTint="@{viewModel.checkBox2Color}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/checkBox1" /> <Button android:id ="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:onClick="@{() -> viewModel.onClickValidateButton()}" android:text="Validate" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/checkBox2" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>MainViewModelpackage com.example.checkboxcolorchange.ui.main import androidx.annotation.IdRes import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.example.checkboxcolorchange.R class MainViewModel : ViewModel() { val validated = MutableLiveData<Unit>() val isCheckedCheckBox1 = MutableLiveData<Boolean>(false) val isCheckedCheckBox2 = MutableLiveData<Boolean>(false) val checkBox1Color = MutableLiveData<Int>(R.color.check_box_color_valid) val checkBox2Color = MutableLiveData<Int>(R.color.check_box_color_valid) fun onCheckedChanged(@IdRes resId: Int) { checkBox1Color.value = R.color.check_box_color_valid checkBox2Color.value = R.color.check_box_color_valid when (resId) { R.id.checkBox1 -> isCheckedCheckBox1.value = isCheckedCheckBox1.value?.not() R.id.checkBox2 -> isCheckedCheckBox2.value = isCheckedCheckBox2.value?.not() } } fun onClickValidateButton() { if (!isValid()) { checkBox1Color.value = R.color.check_box_color_invalid checkBox2Color.value = R.color.check_box_color_invalid return } validated.value = Unit } private fun isValid() = isCheckedCheckBox1.value == true && isCheckedCheckBox2.value == true }CompoundButton.ktpackage com.example.checkboxcolorchange.ui.extention import android.widget.CompoundButton import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import androidx.databinding.BindingAdapter @BindingAdapter("buttonTint") fun CompoundButton.setButtonTint(@ColorRes resId: Int) { buttonTintList = ContextCompat.getColorStateList(context, resId) }check_box_color_valid.xml<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/valid" android:state_checked="false" /> <item android:color="@color/colorPrimary" android:state_checked="true" /> </selector>check_box_color_invalid.xml<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/invalid" android:state_checked="false" /> <item android:color="@color/colorPrimary" android:state_checked="true" /> </selector>参考リンク
◆CheckBoxのボタン色(buttonTint)属性について
https://developer.android.com/reference/android/widget/CompoundButton◆ColorStateListとは?
https://developer.android.com/reference/android/content/res/ColorStateList◆リソースIDからColorStateListを取得する方法
https://stackoverflow.com/questions/12476189/how-to-use-colorstatelist-in-android/18594364
- 投稿日:2019-11-02T11:28:44+09:00
Ionic + Firebaseで記録するカレンダーアプリを作る その2 実機でビルド
完成イメージ
こんな感じ。 pic.twitter.com/CvOFjFq2th
— げん げんと (@gento34165638) November 2, 2019はじめに
環境は以下の通り
・ionic4
・Nativeとの橋渡しにはcapacitorを使用
・カレンダーについてはionic2-calendarというライブラリーを使用前回はとりあえずカレンダーを表示させた。
前回の記事はこちら
XcodeとAndroid Studioがインストールされている前提で進める。
実機でカレンダーを表示させる
前回カレンダーを表示させる段階まで行った。
今回はそれを実機でやってみよう。基本公式ページをそのまま真似していく。
もし
ionic start myApp blankでプロジェクトを始めているなら、config.xmlが存在しcapacitor.config.jsonがないかと思われる。そんな場合はプロジェクトフォルダで
ionic integrations enable capacitorのコマンドを実行しよう。
これでcapacitorを使ったビルドに必要な、capacitor.config.jsonが作成されるかと。アプリの初期設定
npx cap init [appName] [appId]・[appName]は日本語でもいける。当然後から変更できるが、ちょっと面倒やし何より変な所を触ってバグられたら困る。。変えないつもりで入力した方が安全。
・[appId]はいわゆる逆ドメインの事。
例:com.example.app少しネイティブアプリを触ったことがある人は「あーあれか」となるだろう。
ここでは
com.名前.アプリ名で登録する。よって
npx cap init 僕のカレンダー com.gento.mycalendarとする。ビルドする
iOSとAndroidのプラットフォームをionicプロジェクトに追加する前に、一度ビルドする必要がある。
ionic build20秒ぐらいはかかるかな?知らんけど。
この
ionic buildが具体的に何をしているのか、僕はあんまりわかっていない。ビルドが終わると
wwwフォルダが作成させる。プラットフォームの追加
npx cap add ios
npx cap add androidionicプロジェクトフォルダ直下にiosフォルダとandroidフォルダが作成された。
XcodeとAndroid Studioでアプリを起動
npx cap open ios
npx cap open androidさぁドキドキの瞬間。
iosならxcodeが、androidならAndroid Studioが起動してくれる。
シュミレーターでもいいけど、遅いので僕は実機で起動した。
Android成功!!
iOS成功!!
実機ビルドできなかった時に確かめるといい事
xcode
signingのTearmの所がNoneになっていて、僕は何度も失敗を喰らった・・・。
Android Studio
これについてはよくわからん。
ビルドに失敗しても勝手に治ったりするし、逆に何もしていないのに失敗したり・・・。一晩寝かせてみるといいかも。。(こんなクソ情報ですいません。。。)
※追記
頻繁に発生していたGradle関連のエラーは、このゾウさんのボタンで治るかもしれません!!
最後に
各ストアで配信されてます。
興味があれば使ってみてください。
- 投稿日:2019-11-02T00:57:08+09:00
RobolectricでBackボタンのテスト
Androidのローカルテストで、フレームワーク依存のコードのテストを書く際にはみなさん、Robolectricを使っているかと思います。
今回はActivityやViewに対するBackボタン=戻るボタン処理の挙動をテストする方法をメモります。大した内容じゃないですが、再度調べなくて良いように。
なぜ戻るボタンのテストをしたいか?
普通にライフサイクルのテストとかするなら、activity.pause()やactivity.finish()などを呼ぶのがいいと思います。
今回テストしたかったのは、以下の動作確認がしたかったからです。
- Activity内のViewが特定条件で戻るボタンをconsumeするので、条件を変えた際の戻るボタンのイベント伝播を確認したい。
- Activityが戻るボタンを受けとった際の処理をテストしたい。
- onBackPressedがコールされること。
- 特定viewに対するkeyEventを送信することでテストを記述したい。
- 手動で呼べば再現できるけど、なるべくフレームワークの実装通りに伝播させたい。
戻るボタンイベントの渡し方
結論ですが、以下のようにKeyEventをdispatchすることで、Viewを通してActivityにイベントを伝播させることができました。
@Test fun バックボタンを押すとActivityが閉じる() { // onBackPressedは戻るボタンのUp時に発火される。 val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) // ただし、KeyEvent自体が該当Viewに対してDownされてTracking状態になっていないと発火しない。 // そのため、trackingを監視するクラスにDownイベントを渡して開始させて、Upイベントにフラグを立たせる。 val state = KeyEvent.DispatcherState() state.startTracking(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK), null) state.handleUpEvent(keyEvent) // KeyEventがisTrackingかつisCanceledじゃない時、onBackPressedが呼ばれる。 println("event: ${keyEvent} ${keyEvent.isTracking()}, ${keyEvent.isCanceled()}") // イベントは伝播して誰かにconsumeされる val consumed = view?.dispatchKeyEvent(keyEvent) assertThat(consumed).isTrue() // onBackPressedが呼ばれてActivityがfinishしている。 assertThat(activity.get().isFinishing).isTrue() }KeyEventの伝播の仕方は次のような順番でした
- Focusの当たったView
- Parent ViewGroupをたどる(行き着くまで)
- DecorView
- PhoneWindow
- Window.Callback(Activityが継承してWindowに登録している)
ActivityとWindowクラスの関係がイマイチ理解できていないですが、Activityで受け取るタイミングは後ろの方でした。
参考
AOSPのActivityやKeyEventのコードを見ればわかることでした。
Activity.javapublic boolean onKeyUp(int keyCode, KeyEvent event) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.ECLAIR) { if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) { onBackPressed(); return true; } } return false; }KeyEvent.javapublic void startTracking(KeyEvent event, Object target) { if (event.getAction() != ACTION_DOWN) { throw new IllegalArgumentException( "Can only start tracking on a down event"); } if (DEBUG) Log.v(TAG, "Start trackingt in " + target + ": " + this); mDownKeyCode = event.getKeyCode(); mDownTarget = target; }ちょっと行き着くのに時間かかってしまったのでメモしておきます。






















