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

JavaとかAndroidとか調べたことをメモします。٩( 'ω' )و

Kotlin初心者から抜け出したい?それなら、

Land of Lispを読んだらどうだろう。 (すみません。9割ネタです。)

Land of Lisp

Land of Lisp

Kotlinを初めて数ヶ月だったあなたへ

今年のGoogle I/OでKotlinがAndroidで正式サポートされるとアナウンスされた頃からでしょうか、Kotlin初心者向けの勉強会が一気に増え、実際に自分の周りもAndroidエンジニアを中心にKotlinを触る人が増えてきました。
最初はみなさんこちらの赤べこ本から始める人が多いかと思います。

Kotlinスタートブック -新しいAndroidプログラミング

Kotlinスタートブック -新しいAndroidプログラミング

(英語が得意な人であればKotlin in Actionもいいですね)

Kotlin in Action

Kotlin in Action

赤ベコ本、初心者でもとっつきやすくて本当に素敵な入門書です。ただ、入門書も読み終わって初心者からそろそろもう一歩進みたいそこのあなた。

Java(特にAndroid Java1 )をずっと続けてきてからKotlinに移行した人、そんな人におすすめなのが Land of Lisp!

なぜLand of Lispがおすすめなのか

一体どこからLispが出てきたのか?この本は半分Lispについての本ですが、 半分は関数型プログラミングについての入門書です。
Kotlinは関数型プログラミングパラダイムを取り入れています。

例えば、高階関数。公式ドキュメントを読む時も高階関数(Higher-Order Functions)とかさらっと出てきます。それが一体なんなのか、なんのためにあるのかちゃんと理解していますか?

例えば、遅延評価。Java 8が使えないAndroidでは初めましての人も多いかと思います。Kotlinではvalで再代入不可な変数を宣言することができますが、これをpropertyで持つとなると当然初期化をする必要があります。クラスのインスタンス化時点では取れない情報を使って初期化したい時、遅延評価が本当に強力な機能となります。
Androidの例で恐縮ですが、Activityのpropertyとしてintentのextraを持ちたい場合、下記のようなコードはかけません。なぜならクラスのインスタンス化時点でthis.intentは正しいintentが入っていないからです。

class HogeActivity : AppCompatActivity() {

    private val fuga = this.intent.getStringExtra(FUGA_KEY) // 動かないよ!

}

そこでvalで宣言するのを諦めてvarにして書けないことはないのですが、イマイチなコードです。

class HogeActivity : AppCompatActivity() {

    private var fuga = "" // 動くけどイマイチだよ!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        this.fuga = this.intent.getStringExtra(FUGA_KEY)

    }

}

そこで、下記のように遅延初期化すれば解決します。このコードの場合、初めてfugaにアクセスしようとした時にfugaが初期化されます。なので、例えばonCreate()fugaにアクセスするとこの時にはthis.intentに正しい情報が入っているのでfugaには想定通りの値で初期化されます。これでfugaをvalで宣言することができます。

class HogeActivity : AppCompatActivity() {

    private val fuga by lazy {
        this.intent.getStringExtra(FUGA_KEY) // 動くよ!
    }

}

おまけ

そもそもなんでLand of Lispなんか読み始めたかというと昨年の誕生日に同僚氏が自宅に送りつけてきたからです。(当然wishlistには入れていません。)今まで5人くらいに送りつけて実際に読み始めたのは私が初めてだそうです😇
読了して思ったのは「この本やっぱり頭おかしいな」でした。

最後に。この本を読んで副作用満載のコードをみると発狂しそうになるかもしれませんが責任取れませんので悪しからず。


  1. Android Javaは長いことJava本流から取り残されてJava8の便利なあれこれが使えず悲しい気持ちになりますね

Dexopenerを使ってKotlinで書かれたfinalなclassをmockする話

[2017.7.27 追記あり]

4ヶ月以上前の話の続きです。下書きにずっと眠っていたのですが、諸事情により公開が遅くなってしまった…

こちらの記事でAndroidでKotlinのmockつらい〜〜〜 😇😇😇😇って話をしたら、

muumuutech.hatenablog.com

id:tmurakami さんがコメントでDexOpenerを教えてくださったので試してみました。 (id:tmurakamiさんがDexOpenerの作者さんです。)

github.com

使い方

上記RepositoryのREADMEにある通り、testコードのcompile時に対象libraryを指定して、 testInstrumentationRunner を指定するだけです。

dependencies {
    androidTestCompile 'com.github.tmurakami:dexopener:0.9.1'
}
android {

    defaultConfig {
        testInstrumentationRunner "com.github.tmurakami.dexopener.DexOpenerAndroidJUnitRunner"
    }

これだけでfinal classをmockできるようになります!楽!!

中で何が起こっているのか

せっかくなのでlibraryの中身を読んでみました。Open source最高!!
こちらのlibraryは同じ作者さんの classinjector というlibraryに依存を持っているのでコードを読むときは手元にcloneして読むことをおすすめします。 読み間違いなどあったらすみません!ご指摘いただけると嬉しいです!

github.com

ざっくり概要

[2017.7.27 追記]
最新version( 0.10.3 )で方針が変わってかなり早くなりました!圧倒的…!
ただし、minSdkVersionが16にあがっていましたのでご注意を。

version time
0.9.1 Total time: 3 mins 37.83 secs
0.10.3 Total time: 9.162 secs

dexファイルを取って来て、下記のクラス・メソッドのアクセス修飾子からfinalをマスクしてbit反転していました。

  • class
  • inner class
  • method

というわけでクラスやメソッドが増えれば増えるほど処理に時間がかかります。

コード詳細

まずはTest Runner ( DexOpenerAndroidJUnitRunner.java ) のnewApplicationで DexOpener#install() をコール。 このメソッドの中で ApplicationInfo を持ったbuilderを生成し、 DexOpenerImpl に処理を移譲します。 この時、Builderに BuiltinClassNameFilter を設定して、buildinのclassはmockできないようにしています。

BuiltinClassNameFilter.java

    private final String[] disallowedPackages = {
            "android.",
            "com.android.",
            "com.github.tmurakami.classinjector.",
            "com.github.tmurakami.dexmockito.",
            "com.github.tmurakami.dexopener.",
            "com.github.tmurakami.mockito4k.",
            "java.",
            "javax.",
            "junit.",
            "kotlin.",
            "kotlinx.",
            "net.bytebuddy.",
            "org.hamcrest.",
            "org.jacoco.",
            "org.junit.",
            "org.mockito.",
            "org.objenesis.",
    };

ApplicationInfo.dataDircode_cache/dexopener でcacheを生成した後、 ClassInjector を使ってApplicationInfo からclasses*.dexファイルを取って来て、

AndroidClassSource.java

    @SuppressWarnings("TryFinallyCanBeTryWithResources")
    private ClassSource newDelegate() throws IOException {
        List<ClassSource> sources = new ArrayList<>();
        ClassNameReader r = new ClassNameReader(classNameFilter);
        ZipInputStream in = new ZipInputStream(new FileInputStream(sourceDir)); // sourceDirはApplicationInfoから取得したやつ
        try {
            for (ZipEntry e; (e = in.getNextEntry()) != null; ) {
                String name = e.getName();
                if (name.startsWith("classes") && name.endsWith(".dex")) {
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    byte[] buffer = new byte[16384];
                    for (int l; (l = in.read(buffer)) != -1; ) {
                        out.write(buffer, 0, l);
                    }
                    ApplicationReader ar = new ApplicationReader(ASM4, out.toByteArray());
                    sources.add(dexClassSourceFactory.newClassSource(ar, r.readClassNames(ar)));
                }
            }
        } finally {
            in.close();
        }
        return new ClassSources(sources);
    }

[2017.7.27 追記]
コメントいただきました通り、最新版ではASMDEXではなくdexlib2を使う方針に変わっていました。失礼しました。

下記のfinalをbit反転しています。

  • class
  • inner class
  • method

visitorパターンで実装されていました。ASMDEXのClassVisitor を使っているためかな?

ApplicationOpener

    @Override
    public ClassVisitor visitClass(int access,
                                   String name,
                                   String[] signature,
                                   String superName,
                                   String[] interfaces) {
        int acc = access & ~ACC_FINAL;
        return new ClassOpener(api, super.visitClass(acc, name, signature, superName, interfaces));
    }

感想

本体側をKotlinで書かれているアプリのinstrumented auto testしたい〜〜って時に選択肢の一つとして良さそうです🎉🎉

[2017.7.27 追記]
パフォーマンスもかなり良いので、minSdkVersionが16以上という方はぜひ使ってみたらいいと思います!🎉🎉

Kotlin 1.1.2-5くらいからSAM変換がちょっと変わってcrashが起きた件

Kotlinのバージョンあげたら既存コードがcrashしたのでちょっと調べてみたメモ。

何が起こったのか

kotlin のversionを1.0.5-3から1.1.2-5にあげたら既存コードがcrashするようになった。具体的には、こんな感じのIllegalStateExceptionが吐かれる。

java.lang.IllegalStateException: invoke(...) must not be null

何が悪かったか

Kotlin側に渡すJavaで定義したFunction0のオブジェクトでinvoke()をoverrideした際にreturn nullしていたのが悪かった。Javaでいうvoidに当たるUnitのインスタンスをreturnすることで落ちなくなる。

        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });

Android Studio(v2.3)だとinvoke()メソッドを補完するときにreturn nullで補完してくれるからいちいち手で書き換えないといけない_:(´ཀ`」 ∠):
Javaでいうvoidはkotlinのnull許容・非許容と離れた概念だからどっち返しても大丈夫でしょ、とか思ってたら思わぬところで痛い目をみる。

で、問題はなんだったの?

実はこのcrashが発生するのにいくつか条件があって、

  1. JavaでFunctionN系のinvoke()メソッドをreturn nullしてoverrideする
  2. 1で定義したインスタンスをkotlinのコードに渡す
  3. 2のkotlinのコードからさらにjavaのコードに渡して、invoke()ではなくSAM変換後のメソッドとして実行する

3がちょっとなんて言ったらわからないのでコードを交えて説明。

適当なAndroidのコードで試したものです。まずはFunction0型のインスタンスをkotlinのコードに渡します。

public class SamConversionExperimental {

    public void invokeFunction() {
        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });
    }

    private void doSomething() {

    }
}

その後、受け取ったkotlinコード側でSAM変換がかかるような感じで実行してやればcrash. 手元だとRunnable#run() が走るようにしてcrashさせた。
(Runnableは単一メソッドrun()だけを持つインターフェイス

    fun invokeFunctionFromKotlin(function: () -> Unit) =
        Handler(Looper.getMainLooper()).post(function)

SAM変換変わったね?

エラー文から察するに、この辺りのチェックコードがversion上がって入ってきたっぽい。 github.com

    public static void checkExpressionValueIsNotNull(Object value, String expression) {
        if (value == null) {
            throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null"));
        }
    }

で、このメソッドがどこから呼ばれているかというと、この辺り。

kotlin/RedundantNullCheckMethodTransformer.kt at a5620454fa2fef926b4ca35b95fdb46a44506211 · JetBrains/kotlin · GitHub

        private fun analyzeTypesAndRemoveDeadCode(): Map<AbstractInsnNode, Type> {
            val insns = methodNode.instructions.toArray()
            val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter())

            val checkedReferenceTypes = HashMap<AbstractInsnNode, Type>()
            for (i in insns.indices) {
                val insn = insns[i]
                val frame = frames[i]
                if (insn.isInstanceOfOrNullCheck()) {
                    checkedReferenceTypes[insn] = frame?.top()?.type ?: continue
                }
                else if (insn.isCheckParameterIsNotNull() || insn.isCheckExpressionValueIsNotNull()) { // ここにチェックが入っている
                    checkedReferenceTypes[insn] = frame?.peek(1)?.type ?: continue
                }
            }

            val dceResult = DeadCodeEliminationMethodTransformer().removeDeadCodeByFrames(methodNode, frames)
            if (dceResult.hasRemovedAnything()) {
                changes = true
            }

            return checkedReferenceTypes
        }

なんかこの辺りのcommitでcheck増えてそうな気配を感じるのでこれかなぁ

github.com

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))

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