言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

MediaStoreのThumbnailsの罠

先日参加したCA.apk #2 で前川さんが発表されていた「やさしい画像ギャラリー改善tips」がいい感じだったので、試してみた結果と気になった部分のメモ書きです。

発表の概要

画像ギャラリーを作る時に、MediaStore.Images.Media.EXTERNAL_CONTENT_URI だと画像の元サイズで読み込んでメモリが逼迫されるので、MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URIを使って小さいサイズで読み込むと動作がサクサクになるよ!という話だと理解しました。

気になったこと

基本的にいい感じなのですが、thumbnailのテーブルだとコンテンツが登録されない契機があるように見えます。具体的にいうと、ギャラリーを表示した直後にカメラアプリを起動し、ギャラリーに戻って来た際にthumbnailのテーブルには先ほど撮った写真はまだ登録されていません。(MediaStore.Images.Media.EXTERNAL_CONTENT_URIには登録されていました。)手元のNexus 6Pでしか試せていないのですが、/storage/emulated/0/DCIM/.thumbnails/配下のキャッシュされたファイルしか探しに行けてないような?thunbmail生成のタイミングに気をつけたほうがよさそうです。

で、どうする?

確実な方法としては、MediaStore.Images.Media.EXTERNAL_CONTENT_URIでちゃんと最新のデータが登録されているので、このテーブルからID一覧を取得するのが楽そうです。こっちのテーブルにはthumbnailのテーブルのようにKIND列が用意されていないので、サイズを指定してデータを参照することはできません。というわけで、Thumbnails.getThumbnail() を使って元データのIDからthumbnail画像のbitmapを取得するのが正攻法なのかなぁという気がしています。パフォーマンスはちょっと下がる気がしているので、他にいい方法があれば教えてください。

おまけ:thumbnail テーブルが更新されるタイミングを知りたくてframeworkを読んで見た

MediaProviderupdate()が呼ばれたタイミングでthumbnailが生成されているけど、insert()のタイミングではthumbnailは生成されていないような気がする…1
ということはMediaScannerでscanすればいいのでは?と思ってMediaScanner#scanDirectories() とか使いたかったけどそもそもクラスが @hide だし、publicなMediaScannerConnection#scanFile()ディレクト単位でscanできるのかわからないので諦めました :p

BottomNavigationViewのコード読んでみた

このエントリは先日参加した「まったりAndroid Framework Code Reading #5」の成果です٩( ‘ω’ )و
Support LibraryのBottomNavigationViewのコード読んできたのでまとめるよ!

mandroidfcr.connpass.com

BottomNavigationViewとは

Support Library 25.0.0から追加された画面下部に配置するviewです。
API Referenceはこちら

なぜBottomNavigationVIewを読もうと思ったか?

iOSではおなじみの下タブデザインをAndroidでも取り入れたいという話はよく出てくるかと思いますが、このBottomNavigationViewはボタンの数が多くなってくるほどめちゃめちゃアニメーションが入ってきます。

アニメーションいらないし、inactiveなボタンだとtext表示されないしで、割とプロダクトに取り入れるのはハードル高かったりします。 そういった場合、自分でcustom viewを作ることになると思うのですが、次に問題になるのはmenuをxmlで設定できるようにするか?という部分になります。 menuで設定できた方が綺麗だけど、そこまで汎用性高める必要もないしもはやコード量によるよなぁと思って、では本家ではどれくらいのコード量なのか?を知るために読んでみました。

で、どうだった?

アニメーションを含めるとBottomNavigationViewを構成する関連クラスは7つほどでした。というわけでmenuで設定できる必要はないかなーという結論。Library作っていろんなプロダクトで使い回すなら別ですけどね。

わかったこと

関連コードとそれぞれ読んだ時のメモを残しておきます。

BottomNavigationView

  • コードはここ
  • FrameLayoutをextendsしたクラス
  • prensenter, menu, menuViewをそれぞれお互いにバインド
  • custom attributeを取得してmenuViewにセット
  • custom attributeからmenuを取ってきてinflateするメソッドがpublicなので、コードから任意のタイミングでinflateできるっぽい
        181     /**
        182      * Inflate a menu resource into this navigation view.
        183      *
        184      * <p>Existing items in the menu will not be modified or removed.</p>
        185      *
        186      * @param resId ID of a menu resource to inflate
        187      */
        188     public void inflateMenu(int resId) {
        189         mPresenter.setUpdateSuspended(true);
        190         getMenuInflater().inflate(resId, mMenu);
        191         mPresenter.initForMenu(getContext(), mMenu);
        192         mPresenter.setUpdateSuspended(false);
        193         mPresenter.updateMenuView(true);
        194     }
  • このView自体はただのFrameLayoutでコンテナ状態
    • menu viewをaddView()することによりViewの描画を行う
      • menu viewがさらにitem viewを再帰的にaddViewしている
  • itemIconTintがattributeで指定されていなかったら、disable/ emptyにはandroid.R.attr.textColorSecondary がが設定される。checkedはprimary
  • BottomNavigationMenuにcall backを設定
    • 提供するinterfaceはonMenuItemSelected()とonMenuModeChange()

BottomNavigationMenu

  • コードはここ
  • 50行程度の小さなクラス
  • MenuBuilderクラスの拡張
  • @hideかつpublic
  • このクラスのMAX_ITEM_COUNTは5
  • sub menuは非サポート

BottomNavigationMenuView

  • コードはここ
  • 300行程度のクラス
  • ViewGroupの拡張、MenuViewをimplements
  • @hideかつpublic
  • BottomNavigationViewから強制的にGravity.centerをセットされる
        111         mMenuView = new BottomNavigationMenuView(context);
        112         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
        113                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        114         params.gravity = Gravity.CENTER;
        115         mMenuView.setLayoutParams(params);
  • initialize()でメンバで持ってるMenuが更新されてもcheckedの位置はリセットされず引き継ぐ
         97     @Override
         98     public void initialize(MenuBuilder menu) {
         99         mMenu = menu;
        100         if (mMenu == null) return;
        101         if (mMenu.size() > mActiveButton) {
        102             mMenu.getItem(mActiveButton).setChecked(true);
        103         }
        104     }
  • BottomNavigationItemViewの配列を持っている
  • Pools.SynchronizedPoolというhideなクラスを使ってButtonのオンジェクトをプールしている
  • Buttonが3つ以上ある場合はアニメーション(shifting mode)
    • アニメーション機能ははBottomNavigationAnimationHelper* クラスに移譲
    • ここでいうアニメーションとは、ボタン自体の移動をさす
      • ボタンのコンテンツのサイズが変わるtransitionはBottomNavigationItemViewの責務

BottomNavigationItemView

  • コードはここ
  • 実際のボタンを担当するクラス
  • FrameLayoutをextends, MenuView.ItemViewをimplements
  • active/inactiveが切り替わったタイミングでボタン内のコンテンツ(lable, icon)の拡大・縮小、表示・非表示の制御を行う

BottomNavigationPresenter

  • コードはここ
  • 100行程度の小さなクラス
  • MenuPresenterをimplements
    • MenuPresenterはhideなinterface
  • @hideかつpublic
  • updateViewのロックは意外とフラグ制御

BottomNavigationAnimationHelperIcs

  • コードはここ
  • ボタンが3つ以上だった時のanimationのhelper class
  • AutoTransitionとTransitionManagerを使ってanimation
    • material motionの土台となってるのはこのクラスっぽい
    • 意外とめっちゃシンプルなコードでかけるっぽい
         30     BottomNavigationAnimationHelperIcs() {
         31         mSet = new AutoTransition();
         32         mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
         33         mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
         34         mSet.setInterpolator(new FastOutSlowInInterpolator());
         35         TextScale textScale = new TextScale();
         36         mSet.addTransition(textScale);
         37     }
         38 
         39     void beginDelayedTransition(ViewGroup view) {
         40         TransitionManager.beginDelayedTransition(view, mSet);
         41     }

BottomNavigationAnimationHelperBase

  • コードはここ
  • ICSより前のOSだとこっちが使われる
  • 中身はこれだけ(つまりanimationしない)
         21 class BottomNavigationAnimationHelperBase {
         22     void beginDelayedTransition(ViewGroup view) {
         23         // Do nothing.
         24     }
         25 }

SearchViewの詰まったところ備忘録

久しぶりにSearchViewをさわると忘れているポイントが結構あったので、今更感がありますがメモ書きを残します。サンプルコードはKotlinです。

SearchViewをactionbarいっぱいに表示したい

SearchViewをAction Barに配置するとデフォルトで左側に謎の余白ができてしまう問題によく出会います。 SearchViewの android:layout_widthmatch_parent にしても効きません。 ポイントはmaxWidthを変えること。

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        this.menuInflater.inflate(R.menu.search_menu, menu)
        val menuItem = menu?.findItem(R.id.search_view)
        val searchView = menuItem?.actionView as SearchView
        searchView.maxWidth = Int.MAX_VALUE
        return super.onCreateOptionsMenu(menu)
    }

hint textの色を変えたい

SearchViewの中で定義されているidを使ってSeachView内のEditText(実際はEditTextをextendsした@hideなクラスであるSearchAutoComplete)にアクセスし、hint text colorを書き換えます。

        (searchView.findViewById(R.id.search_src_text) as EditText)
                .setHintTextColor(ContextCompat.getColor(this, android.R.color.white))

他にもなんか思い出したら適宜追記したい。

DroidKaigi 2017で登壇しました

3月9日、10日と二日間に渡って開催されたDroid Kaigi 2017にスピーカーとして参加してきました。

 

私のトークテーマはパフォーマンス改善について。オーディエンスの対象を初心者から中級者としていたため、わかりやすく丁寧に伝えるように心掛けたのですが、その結果たぶん全セッションの中で一番ゆるふわでほっこりセッションになったんじゃないかなぁと思います。(実際に聞きに来て頂いた人達に手を上げてもらったところ、「Android開発を初めて1年以内」、「2, 3年の経験がある」「それ以上」が同じくらいいらっしゃいました。あれ?笑)

 

 

発表練習に何度か付き合ってくれて沢山フィードバックくれたAndroidチームのみんなとか、社内のSlackに投げてたスライドにツッコミをいれてくれたエンジニア勢とか、セッション見に来てくれた皆様やオフィスアワーや懇親会で話しかけて下さった方々、本当にありがとうございますという圧倒的感謝でいっぱいです。

 

何が言いたかったかというと😺は可愛くて正義ってことです。

Android + Kotlin + Mockito のメモ書き

Kotlin大好きで、Korlin最高だよって言いまくってるんですけど、Kotlinのつらみみたいな部分も残しておかないとフェアじゃないかなと思ったので残しておきます。

Instrument Testがつらい

実機だったりエミュレータ上で実行するAndroid Instrument Testがつらいです。なぜかというとKotlinは基本的にクラスがデフォルトでfinalになるからです。
Auto testはみなさん大抵mockitoを使うと思います。mockito 2.0 からfinal classもmockできるようになりましたが、こちらの機能はopt in機能で素の(Androidではない) Java では src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker のパスに以下の1行を書いたファイルをおけば動くようになります。

mock-maker-inline

ところがAndroidJVMだとこちらのopt inがうまく機能しません。

mockito-androidに期待したでしょ?

mockito-androidがmockito 2.6.1 から出て来ました。こちらでdexmakerとの依存から解放されると喜んでいる皆さん、残念ながらinline mock makerは対象外です\(^o^)/

Mockito (Mockito 2.7.13 API)

Be aware that you cannot use the inline mock maker on Android due to limitations in the Android VM.

最終手段All Open

Kotlin 1.0.6からcompile pluginでall openが追加されました。
こちらを利用するとclassに open 修飾子をつけなくても特定のアノテーションをつけたクラスがopenになります。
blog.jetbrains.com

ただし、Test compile時だけこの機能を有効にする方法が見つからず、結局productionに導入するのは諦めました。
テスト時だけ動くようなGradle Taskを使ってうまくやろうと思ったけれど断念。誰かうまく言ったという人がいればぜひ教えてください(´;ω;`)

結論

powermockとか導入するのもtoo muchな感じでつらみがあるので、
Android JVMを利用しなくても良いunit testに一旦絞ってauto testを描く方針になりそう。

夕飯のカレーとコミットメントの話

先に言っておきますが、ただのポエムです。

 

エンジニアとして働いていると、「hoge機能を作りたいんだけど、今月中でできますか?」という質問をよくされます。ここでいうhogeがどれくらいしっかり固まっているかで話は変わってきます。

例えば、「今晩カレー食べたい!」というリクエストが来たとします。ここでいうカレーが

 

1. レトルトカレーを温めてご飯にかけるだけ

2. 市販のカレールーを使ってカレーをつくる

3. スパイスから拘ってインドカレーをつくる

4. おうちカレーじゃなくてカレー屋さんに行きたい

 

などなど無限に選択肢があります。

カレー食べたいと言い出した人が上のうちどれを想定しているのかによって、当然工数が変わってきます。困ったことに、カレー食べたいと言い出した人がどんなカレーを食べたいかわかっていないことがよくあります。

もしもカレーコンサルタントとして働いていて、それでお金を頂いているのであれば、手厚くヒアリングしましょう。でもそうじゃない場合、どうするのがいいのかなぁとよく考えます。

たとえば、上記4つの選択肢にかかる大体の工数をそれぞれ伝えてあげるのも一つの手段ですが、実際には1のレトルトカレーの選択肢でも、コンビニで買えるのか、Amazon Nowでポチってすぐ手に入るのか、Primeで1日かかるのか、それとも地方まで出かけないと手に入らないご当地カレーなのか様々です。

上のパラグラフのような話を長々としてしまっても、相談した人は困惑を浮かべるでしょう。エンジニアって面倒くさいな…と心の声が聞こえてきます。相談してきた人はできるかできないかの2択の答えを求めているからです。

かと言って適当に「ハイハイできます!」というのも違うなぁと思います。コミットメントした以上は約束を守る責任があるからです。(ここでいうコミットメントは、意識の高い人がよく使う「フルコミット」的な使い方ではなくて、「完遂すると約束すること」として使っています*1

付き合いが長い相手だと、「カレー食べたい!」って言われて、普通にお家カレーなんだろうなと予想がつくので「いいですね。今日のお夕飯はカレーにしますね」と言えます(=今晩の夕飯までにカレーを作るとコミットメントすることができる)が、ソフトウェア開発は斜め上のカレーをリクエストされる可能性が割と高い気がしています。

 

最近は一番可能性が高そうなケースを狙って、そこを一つの基準としておくのがいいのかなぁという気がしています。「どんなカレーを食べたいかによりますが、一旦週末目標で進めましょう。どんなカレーか具体的に決まったらもう一度話し合いましょうね。」って感じで。

何も決まってない以上、具体的なスケジュールを立てるのはもはや困難なので、相手の人を納得というか安心させてあげるだけでいいのかなという感じです。相談してきた人との信頼貯金を貯めていくというか、すぐに相談に来てもらえる関係性を作る点で有効かなと思っています。

 

「カレー」という文字を打ちすぎてカレー食べたくなってきたのでやめます。

本日のポエムは以上です。

 

 

*1:私がアンクルボブ信者なので。Clean Coderを読むとコミットメントの話題が出てきますね。

SnackbarでCustom Content を表示する

Support library revision25.1.0からSnackBarにCustom Contentを表示できるようになったらしいので試してみました٩( 'ω' )و

今回の変更概要

さて、release noteをよく読んでみましょう。

Snackbar has been refactored to allow apps to display custom content. BaseTransientBottomBar is the new base class that exposes the general sliding and animations behavior.

custom contentを表示するためにリファクタしたよって言ってますね。BaseTransientBottomBarが新しい基底クラスになるよって言ってます。誰よそれって感じなのでリファレンスやコードを読んでみましょう。

BaseTransientBottomBarって何よ

まずはSnackbarのクラス定義。確かにBaseTransientBottomBarをextendsしています。

public final class Snackbar extends BaseTransientBottomBar<Snackbar> {

このBaseTransientBottomBarが何をしてくれるクラスなのか、リファレンスを読んでみましょう。 主にやってくれそうなのは、この二つ。

  • 画面下部から現れるViewの表示制御
  • 表示・非表示のタイミングを取得するためのCallbackの提供

もともとSnackbarがになっていた機能の一部ですね。

Snackbarがどう変わったか

これらを踏まえて今度はSnackbarのコードを見てみましょう*1。Viewの表示制御をBaseTransientBottomBarへ移したからか、300行ほどの随分とスッキリしたクラスになりました。(以前は確か850行ほどありました。)
中身を読むと分かりますが、このクラスでやっていることは二つ。

  • Snackbarのコンテンツ制御
  • 表示・非表示のタイミングを取得するためのCallbackの提供

この通り、Viewの表示制御やアニメーションなどはやっていません。(Snackbar#make()の第3引数でdurationをとるので、ある意味ここだけアニメーションと言えなくはない。)

このようにコンテンツ制御というかSnackbar独自のviewの制御に特化したクラスになっています。 さらに言えばSnackbarのカスタムレイアウトはSnackbarContentLayout としてhideな別クラスに切り出されています。 なのでSnackbar#setText()の中身も結構無理矢理な感じ。

    /**
     * Update the text in this {@link Snackbar}.
     *
     * @param message The new text for this {@link BaseTransientBottomBar}.
     */
    @NonNull
    public Snackbar setText(@NonNull CharSequence message) {
        final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
        final TextView tv = contentLayout.getMessageView();
        tv.setText(message);
        return this;
    }

このように、VIewの表示制御とコンテンツ制御を切り離して実装することができるようになりました。 ということは、独自デザインのViewをSnackbarの用に表示・非表示するのが簡単にできるようになったってことです。

試してみた

今回は独自デザインの簡単なサンプルとして背景色が黒じゃないCustomSnackbarを作っていきます٩( 'ω' )و

まずはsupport libraryのrevisionを25.0.1にあげます。対象のlibraryをinstallしてbuild.gradleをアップデートします。

    compile 'com.android.support:design:25.1.0'

それではCustomSnackbarクラスを作っていきます。 BaseTransientBottomBarをextendsしてやって、

public class CustomSnackBar extends BaseTransientBottomBar<CustomSnackBar>  {

Snackbarを参考に make() メソッドを実装します。findSuitableParent() の中身はSnackbarのそれのコピペです。 ちなみに、BaseTransientBottomBarのコンストラクタはprotectedなので、自前でコンストラクタを作ってやらないと"There is no default constructor available"って怒られます。

  @NonNull
  public static CustomSnackBar make(@NonNull View view, @NonNull CharSequence text,
                              int duration) {
    LayoutInflater inflater = LayoutInflater.from(view.getContext());
    View content = inflater.inflate(R.layout.custom_snackbar, findSuitableParent(view), false);
    ((TextView) content.findViewById(R.id.custom_snackbar_textview)).setText(text);
    CustomSnackBar customSnackBar = new CustomSnackBar(findSuitableParent(view), content, new ViewCallBack());
    return customSnackBar;
  }

  private CustomSnackBar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
    super(parent, content, contentViewCallback);
  }

ここでinflateするレイアウトですが、下記のような適当な背景色をつけたTextViewを持つLinearLayoutを読み込みます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_margin="10dp">
    <TextView android:layout_width="match_parent" android:layout_height="wrap_content"
              android:id="@+id/custom_snackbar_textview"
              android:background="@color/colorAccent"
              android:textColor="@android:color/white"/>

</LinearLayout>

show()メソッドはBaseTransientBottomBarクラスの方ですでに実装されているので、たったこれだけで使うときは普通のSnackbarの様に使えます。

CustomSnackBar.make(view, "This is custom snackbar sample", Snackbar.LENGTH_LONG)
        .show();

こんな感じで表示されます٩( 'ω' )و

f:id:muumuumuumuu:20161225174903p:plain

*1:Web上にソースが見つからずリンク貼れませんでした…。手元のlibraryをデコンパイルしてみています。