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

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

Intentを使って複数枚画像を取得するときのメモ

Intentを使ってPhotoとかいい感じの(=ユーザが選択した)アプリから画像を選択したいときに、1枚なのか複数枚なのかで色々違うのでメモ。

複数枚画像を選択する場合

選択させるアプリを起動するとき

ポイントは Intent.EXTRA_ALLOW_MULTIPLE のextraをtrueに設定すること。
色々ググったときにactionが Intent.ACTION_GET_CONTENT でいけると書いていたけど、これだと複数画像選択できず一枚選択した時点で元のアプリに戻ってしまう挙動になった。(OS 7.1.2/Nexus 6P実機環境)
下記コードのように Intent.ACTION_PICK だと動く。

val intent = Intent()
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
intent.action = Intent.ACTION_PICK
this.startActivityForResult(Intent.createChooser(intent, "Choose Photo"),
                                        CHOOSE_PHOTO_REQUEST_CODE)

選択した画像の情報を取り出すとき

Activity#onActivityResult() で受け取るintentの clipDatauriの情報が入っているのでこれを使う。 下記のサンプルコードは選択した画像のuriの情報を取り出してlistViewに表示するコードの一部。 getItemAt() で各要素にアクセスできる。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == CHOOSE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            val itemCount = data?.clipData?.itemCount ?: 0
            val uriList = mutableListOf<Uri>()
            for (i in 0..itemCount - 1) {
                val uri = data?.clipData?.getItemAt(i)?.uri
                uri?.let { uriList.add(it) }
            }
            this.adapter.data = uriList
            this.adapter.notifyDataSetChanged()
        }
    }

余談だけど、 clipData が保持しているuriの情報を持っているArrayListがpublicではない、かつ直接アクセスするためのAPIが公開されていないのでこんな汚い感じでmutableListだったりfor文だったりを使わないといけない感じになった(´;ω;`)

↓ClipDataのコード
Cross Reference: /frameworks/base/core/java/android/content/ClipData.java

153 public class ClipData implements Parcelable {

167     final ArrayList<Item> mItems; // publicじゃない

819     /**
820      * Return a single item inside of the clip data.  The index can range
821      * from 0 to {@link #getItemCount()}-1.
822      */
823     public Item getItemAt(int index) {
824         return mItems.get(index); // 指定したindexの要素しか取れない
825     }

もしimmutableなmItemsがgetできたり別のiterableな何かが取得できるAPIが生えてたら、こんな感じでもっと綺麗にかけるのに…

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == CHOOSE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            // もしmItemsに外部からアクセスできたらこんな感じで1行で済ませられそう
            this.adapter.data = data?.clipData?.items.map { it.uri } ?: emptyList()
            this.adapter.notifyDataSetChanged()
        }
    }

拡張関数とか自分で書いてもいいけど、そんなに利用頻度高くなさそう、ということで諦めてしまいそう。

1枚だけ画像を選択する場合

選択させるアプリを起動するとき

この場合のactionは Intent.ACTION_GET_CONTENT でいけた。

            val intent = Intent()
            intent.type = "image/*"
            intent.action = Intent.ACTION_GET_CONTENT
            this.startActivityForResult(Intent.createChooser(intent, "Choose Photo"),
                                        CHOOSE_PHOTO_REQUEST_CODE)

選択した画像の情報を取り出すとき

取り出すときは複数枚の場合と同じく、Activity#onActivityResult() で受け取るintentの clipDatauriの情報が入っているので同様に扱うことができる。

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を読むとコミットメントの話題が出てきますね。