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

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

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をデコンパイルしてみています。

Android/Kotlin関連の情報収集とか。

最近Android周りの情報をどうやって追っているか?について質問されることが何度かあったのでまとめました。自分向けに2016年末時点でのスナップショットも兼ねて。

Twitter

知り合いのエンジニアさんや勉強会であった方とTwitterで繋がる機会が多いせいか、Android関連の情報が流れてくるのが多いのは自分にとって圧倒的にTwitterです。みなさんいつも有用な情報をつぶやいてくれたりニュースをretweetしてくれたり本当に助かっています。ありがとうございます。
また、Google公式アカウントも幾つかフォローしています。(@AndroidDevとか。)
Googleの有名なエンジニアの方とかSquareのJakeさんとかフォローしてる方も多いと思いますが、私は海外のエンジニアさんはフォローしていません。そこまで追い始めるとキリがなさそうなので。あと話題になるtweetは誰かがretweetしてくれるからどのみち目にとまるからいいかなと思ったりします。
blogの中の人や更新情報用アカウントをフォローしておりRSS代わりのような用途にも使っていたりします。

メルマガ

Android WeeklyKotlin Weeklyをsubscribeしています。毎週月曜に一週間のトピックをまとめてくれるので便利です。英語のblogやVideoの情報はだいたいここから入ってきています。

Podcast

Podcastdex.fmを聞いています。Androidの技術的な話だけでなくチーム開発とかサービスグロースの観点の話題も出てきて非常に参考になります。
AndroidPodcastと言えばFragmentedなども有名かと思いますが、こちらは全然追えていません。

Advent Calendar

今の時期だとAdvent Calendarも楽しく読んでいます。今年はAndroidのAdvent Calendarが3つもできてていいですね!
Android Advent Calendar 2016, Androidその2 Advent Calendar 2016, Android その3 Advent Calendar 2016

社内Slack

社内はTwitterではなくFacebook文化なので社内のメンバーとTwitterで全く繋がっていません。そのためTwitterで情報を共有することはないのですが、代わりに社内Slackに専用channelを作ってそこで情報を流しています。
うちは分報文化*1が根付いているのですが、そのノリで#times_androidや#times_kotlinを作って興味がある人たちがそこを覗いて情報を流したりそこにコメントして議論に発展したりします。AndroidとKotlinを分けている理由は、サーバサイドの一部でKotlinを採用していおり、Androidを書かないエンジニアもKotlinの情報を必要としているからです。

そういえばpublicなAndroidのslack channelに怖くてjoinできていません。あそこって誰でも勝手に入っていいのかわからない…

というわけで2016年はこんな感じでした。来年の年末に振り返ってみると何か変わってるかなぁ。

*1:こちらの記事を見て導入しました。分報はいいぞ。ただし気をつけないと時間泥棒にもなります。 c16e.com