20191102のAndroidに関する記事は11件です。

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ではピンクのテーマの中にブルーのテーマが出てきたりなど複数のベースのテーマを切り替えることができる仕組みがあります。
image.png

Owlではそのピンクのテーマとブルーのテーマで共通のテーマ Base.Owl を作って利用する方針にしているようです。
https://github.com/material-components/material-components-android-examples/blob/ff18481c00e878e9760ce57718e2b4195113bac6/Owl/app/src/main/res/values/theme.xml

ShapeやTypographyなどなど、参考になります :eyes:

  <!-- ** ここにベーステーマがある ** -->
  <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=1193

Themeの使い方

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のみ

image.png

https://youtu.be/Owkf8DhAOSo?t=1383

Color

こちらはAndroid Dev Summit '19のセッション内で説明があって、意味のある名前ではなく、色の名前は色をそのまま表すような名前にするのが良いという説明がありました。
https://youtu.be/Owkf8DhAOSo?t=1093

bad good
image.png image.png

(ちょっとサンプルにprimaryとか入っちゃっているような。。?)
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 へインテントで送る。

0.png 1.png 2.png
3.png 4.png

見切れている部分は次のとおり。

mobi.goldendict.android.PopupActivity
putString("android.intent.extra.TEXT", "{text}")

辞書を引くには、範囲選択→SHARE→Send/Share Intent Received とする。手元の端末では GoldenDict に英和辞典以外の辞書も導入してあるから、いつでも多言語の迷宮に入れる。

MacroDroid でも同じようなものを作ろうとしたが、インテント受信の選択肢はあるのに、なぜか共有メニューで MacroDroid が出てこなかった。調べ方が悪いのか、明示的インテントしか受けられない仕様なのかはわからないが、惜しい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/engine/cache/DiskCache.java

キャッシュ戦略とアルゴリズムについて

デフォルトでは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.java
 private 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-files

apendix

主要なクラスの関係は以下の通り
DiskLruCache.java
|
DiskLruCacheWrapper.java
|
DiskLruCacheFactory.java
|
InternalCacheDiskCacheFactory.java / InternalCacheDiskCacheFactory.java
|
GlideBuilder.java

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/engine/cache/DiskCache.java

キャッシュ戦略とアルゴリズムについて

デフォルトでは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.java
 private 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-files

apendix

主要なクラスの関係は以下の通り
DiskLruCache.java
|
DiskLruCacheWrapper.java
|
DiskLruCacheFactory.java
|
InternalCacheDiskCacheFactory.java / InternalCacheDiskCacheFactory.java
|
GlideBuilder.java

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase Dynamic Linksによる起動時の細かい挙動メモ

本当に細かい挙動の話です。

Firebase Dynamic Links(FDL)が起動されるパターンは以下があります

  1. アプリ未インストール時、FDLによってGoogle Playからインストールされてから起動される場合
  2. アプリがインストールされている時、FDLからアプリを直接起動する場合

1. アプリ未インストール時、FDLによってGoogle Playからインストールされてから起動される場合

activity.intent.dataString は空になっており FDLのFirebaseDynamicLinks.getInstance().getDynamicLink(activity.intent)からでないとリンクが取得できない。
Google PlayやLauncherなどから起動されてくるので当たり前といえば当たり前なのですが、そうなります。
ちなみにFDLはどのようにリンクを取得してくるかというと、プロセス間通信によって、Google Play Servicesから取得しているようです。

https://github.com/firebase/firebase-android-sdk/blob/master/firebase-dynamic-links/src/main/aidl/com/google/firebase/dynamiclinks/internal/IDynamicLinksCallbacks.aidl#L8

2. アプリがインストールされている時、FDLからアプリを起動する場合

activity.intent.dataStringにもリンクのURLが入っていて、FDLからも取得可能です
そのためリンクを取得するだけであれば、わざわざFDLを使う必要はないです。
しかし、FDLではリンクにパラメーターamvをつけて、リンクを開くことができるアプリの最小バージョンの versionCodeなどを設定でき、そのための情報などが渡ってきたりするので、その情報が欲しい場合はFDLから情報を取得する必要があります。
https://developers.google.com/android/reference/com/google/firebase/dynamiclinks/PendingDynamicLinkData

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.kt
class 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のみ記載します。

FirstFragment
class 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.xmlandroid:zAdjustment="bottom"を指定(Z ordering)することにより解決を試みていますが、効いていません。これがZ orderingのissueです。

FragmentContainerViewの場合、まず現在のFragmentのExitアニメーションが始まり、次に次のFragmentのEnterアニメーションが始まるので、このような結果となっています。

おまけ

Navigation ComponentのFragmentのコンテナとして<fragment>がよく利用されます。
このコンテナをFragmentContainerViewに置き換えることができます。
実際にFragmentContainerViewを利用している例をこちらの記事に記載しています。
Android NavigationとSharedViewModel

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.kt
class SharedViewModel : ViewModel(){
    val fragmentType = MutableLiveData<FragmentType>().apply {
        this.value = FragmentType.FIRST
    }
}

FragmentTypeをenumを用いて下記のように定義しました。

FragmentType
enum class FragmentType(val type: String) {
    FIRST("first"),
    SECOND("second"),
    THIRD("third"),
    FOURTH("fourth")
}

ホストとなるActivityの作成

Navigationを利用する際の各FragmentをホストするActivityを作成します。
今回はSharedViewModelのfragmentTypeが変更された時にSnackbarが表示される処理を入れています。

MainActivity.kt
class 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>

デザインタブで見ると下記になります。
スクリーンショット 2019-11-01 0.39.01.png

4つのFragmentの作成

前述のFragmentを作成します。
同じ処理を持つFragmentなのでFirstFragmentのみ説明します。
Fragmentには、次へボタンを押下すると、次のFragmentへ遷移し、
Snackbarボタンを押すと、SharedViewModelのfragmentTypeをpostValueし値を変更する処理を作成しました。

FirstFragment
class 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が表示されます。

今回作成したアプリの一連の流れ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActionBarに画像を表示する

はじめに

Androidアプリ作成において、ActionBarに画像を表示する方法のメモになります。

アイコンを表示する方法が出てきたり、ToolBarに差し替えましょう、とあったのですが、ロゴなどの画像をそのまま表示する方法は見受けられませんでした。

筆者はAndroidに詳しくなく、知っていれば簡単なのに検索しても簡易的な記事が見つからなかったので記事として残します。

環境は、

  • AndroidStudio: 3.4.1
  • Kotlin: 1.3.40-release-Studio3.4-1

です

結果

サンプルロゴは【Free Logo Maker — Create a Logo — Squarespace】で作成しました。

解法

手順としては、

  1. 画像の取り込み
  2. ImageViewのxmlを作成
  3. ActionBarにCustomViewを設定

です。

1. 画像の取り込み

New -> Image Assets で画像の取り込み。

Screen Shot 2019-11-02 at 14.48.26.png

2. ImageViewのxmlを作成

今回はapp/res/layout/logo_image_view.xmlという名前で作成しました。

WidgetsのImageViewを追加し、取り込んだ画像を設定、レイアウトを整え点完了

Screen Shot 2019-11-02 at 14.39.21.png

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)を置いておきます

画像の取り込み方の最適解も知らず、レイアウトも雰囲気なのでご指摘いただけると幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】DataBindingでチェックボックスの色(buttonTint)を動的に変更する

背景

下記の仕様を見たす画面を作るために、チェックボックスの色を動的に変更する必要がありました。

  • チェックボックスが全て選択済みの場合のみ、ボタンタップで処理を行う
  • 未選択のチェックボックスがある状態でボタンをタップした場合は、未選択のチェックボックスを警告色にする
  • 他のチェックボックスの状態が変わったら再度デフォルト色に戻す

完成イメージはこちらです。
screenshot.gif

実装方針

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>
MainViewModel
package 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.kt
package 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ionic + Firebaseで記録するカレンダーアプリを作る その2 実機でビルド

完成イメージ

はじめに

環境は以下の通り
・ionic4
・Nativeとの橋渡しにはcapacitorを使用
・カレンダーについてはionic2-calendarというライブラリーを使用

前回はとりあえずカレンダーを表示させた。
:point_right:前回の記事はこちら

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 build

20秒ぐらいはかかるかな?知らんけど。

このionic buildが具体的に何をしているのか、僕はあんまりわかっていない。

ビルドが終わるとwwwフォルダが作成させる。

プラットフォームの追加

npx cap add ios
npx cap add android

ionicプロジェクトフォルダ直下にiosフォルダとandroidフォルダが作成された。

XcodeとAndroid Studioでアプリを起動

npx cap open ios
npx cap open android

さぁドキドキの瞬間。

iosならxcodeが、androidならAndroid Studioが起動してくれる。

シュミレーターでもいいけど、遅いので僕は実機で起動した。

Android成功!!

IMG_6139.JPG

iOS成功!!

IMG_20191007_220416.jpg

実機ビルドできなかった時に確かめるといい事

xcode

スクリーンショット 2019-10-07 22.10.21.jpg

signingのTearmの所がNoneになっていて、僕は何度も失敗を喰らった・・・。

Android Studio

これについてはよくわからん。
ビルドに失敗しても勝手に治ったりするし、逆に何もしていないのに失敗したり・・・。

一晩寝かせてみるといいかも。。(こんなクソ情報ですいません。。。)

※追記

頻繁に発生していたGradle関連のエラーは、このゾウさんのボタンで治るかもしれません!!

スクリーンショット 2019-10-17 20.48.25.jpg

最後に

各ストアで配信されてます。
興味があれば使ってみてください。:relieved:

Apple Store

Google Play

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の伝播の仕方は次のような順番でした

  1. Focusの当たったView
  2. Parent ViewGroupをたどる(行き着くまで)
  3. DecorView
  4. PhoneWindow
  5. Window.Callback(Activityが継承してWindowに登録している)

ActivityとWindowクラスの関係がイマイチ理解できていないですが、Activityで受け取るタイミングは後ろの方でした。

参考

AOSPのActivityやKeyEventのコードを見ればわかることでした。

https://github.com/aosp-mirror/platform_frameworks_base/blob/android10-release/core/java/android/app/Activity.java#L3636-L3646

Activity.java
    public 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;
    }

https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/KeyEvent.java#L2890-L2898

KeyEvent.java
        public 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;
        }

ちょっと行き着くのに時間かかってしまったのでメモしておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む