Infinito Nirone 7

白羽の矢を刺すスタイル

今年楽しんだビール振り返り

Beer Advent Calendar 2018 3日目です。

今年楽しんだビールを振り返ります。

adventar.org

僕ビール君ビール流星レイディオ

10月末に発売になった僕ビール君ビールの新種。強めの苦味が逆に好み。

f:id:KeithYokoma:20181117213716j:plain
中村航氏の小説のタイトルみたいなネーミング

OLD ENGINE OIL

濃いめ。学芸大学のAIMSというお店で飲みました。

f:id:KeithYokoma:20181027203812j:plain
スタウトが好きならきっと気に入る

白濁り

缶が逆さまになっているのが特徴的。甘さがあってスッキリ飲みやすいのでついグビグビいってしまうので注意。

f:id:KeithYokoma:20181023204922j:plain
スッキリ飲みやすい

KAWABA BEER WEIZEN AND AMBER ALE

苦かったなぁ…

f:id:KeithYokoma:20180922175516j:plain
キャンプ場で飲む最高のビール

INDEPENDENCE STOUT

ベトナムにもたくさん美味しいビールがあり、ドラフトビールからスタウトまで色とりどりです。ベトナムでスタウトが飲めると思ってなかったので最高でした。

f:id:KeithYokoma:20180428181012j:plain
ベトナムで出会った最高のスタウト

ヴァイツェン

控えめに言って飲みすぎましたよね。

f:id:KeithYokoma:20181124190329j:plain
しこたま飲んだ

以上です。

RxJava において Observable#hide() とは何者か

Observable#hide() の Javadoc にある説明は次の通り。

Hides the identity of this Observable and its Disposable.

Allows hiding extra features such as Subject's Observer methods or preventing certain identity-based optimizations (fusion). ... 中略 ... Returns: the new Observable instance

ぱっと見た限りは「いつ使うのか分からんな…」という感じなのですが、この文で例示されているSubject がもつ Observer インタフェースのメソッドを隠蔽するという部分と、hide() が新しい Observable を返すという部分がミソで、次の StackOverflow にある回答がまさにこの Javadoc の言いたいことになります。

stackoverflow.com

Subject は Observable のサブタイプなので、Subject を Observable として外部に公開するときはそのまま Observable にキャストして公開できます。

class Something {

  private val subject: Subject<Something> = PublishSubject.create()

  fun observeSomething(): Observable<Something> = subject

}

しかしその実体は Subject のままなので、公開 API で受け取った Observable を Subject に明示的にキャストして使えます。

class Client(val something: Something) {

  fun doSomething() {
    val observable = something.observeSomething() // this observable is actually a subject
    val subject = observable as Subject // no failure
  }
}

およそ Subject を Observable として公開する場合、それを使う側であえて Subject にキャストし直して使われることは意図しない使い方になると思います。特に Subject の場合、このままでは onNext や onError など Observer インタフェースのメソッドが外部から呼べてしまいます。

これを防ぐため、あえて新しい Observable を作って元の Subject の正体(identity)を隠すのがObservable#hide()です。次のようにすると、

class Something {

  private val subject: Subject<Something> = PublishSubject.create()

  fun observeSomething(): Observable<Something> = subject.hide() // hide() で新しい Observable に変換する

}

API を使う側で、受け取った Observable を Subject にキャストできなくなります。

class Client(val something: Something) {

  fun doSomething() {
    val observable = something.observeSomething() // this observable is a new observable created from a subject
    val subject = observable as Subject // ClassCastException!
  }
}

Observable#hide()はもともと、Observable#asObservable()というメソッドでした。これが 2.x へのバージョンアップにおいて名前を変えた、という経緯もあります。

ホッ転トリ

TL;DR

実を言うと私はDrivemode, Inc.を退職します。 突然こんなこと言ってごめんね。 でも本当です。

本日が Drivemode, Inc. に籍がある最後の日になります。お疲れさまでした。 次も変わらず Android アプリのエンジニアで、明日からメルペイで働き始めます。六本木界隈の皆様よろしくおねがいします。

近況について

実のところ、10月中旬からずっと有給消化をしていて、引っ越しをやったり、ジャパンカップクリテリウムツール・ド・フランスさいたまクリテリウムの観戦をしたり、ジャパンカップのあと LottoNL Jumbo チームのアフターパーティーで選手たちとセルフィーを撮ったり、あとは吉祥寺ニワカを脱するためにハモニカ横丁やらなんやらでランチを楽しんだり、自転車でロングライドをちょくちょくやったりしました。Cafe Kiki という飯能にあるサイクリスト御用達のカフェへド平日に行ってしまい、開いてない悲しみを味わったりもしましたが、私は元気です。その近くにある古民家レストランぽれぽれというオシャレなところでランチを食べたところ、将来古民家で暮らしたい欲が湧きました。

有給消化している間、ほとんどの時間を一人でモクモクとでかけるなり作業するなりしていましたが、その中で自分にはリモートワークはあまり向かないなという気持ちを得ました。もしかするとやり方が違えばまた感想も変わるのかもしれませんが、自分にとっては物理的なオフィスに足を運んで仕事をするスタイルのほうが性に合っている気がします(もちろん、必要に応じてフレキシブルにできるのもも大事なんですが)。

Drivemode での日々について

Drivemode にはアメリカで会社が立ち上がってすぐのタイミングでジョインしたので、日本で仕事をするときは渋谷のコワーキングスペースを借りてやっていました。そのあと表参道にうつり単独でオフィスを構え、そこからさらに東新宿のマンションにうつって現在に至ります。

4年と4ヶ月くらい在籍しましたが、その間アメリカだけではなくルクセンブルクに行ったり、そのついでにイギリスを旅行したり、帰りにパリの空港(CDG)の乗り継ぎで迷子になったり、その当時のアプリがA3というアワードを受賞したときに有名なグラビアアイドルと写真を撮ってもらったり、東新宿にうつって日本のメンバーが増えてからは、オフィスで様々な国の料理が振る舞われたり、急にネットミームが飛び交い始めたり、あと最近はあまりできていなかったけど、DriveKaigi という名前からしパクリオマージュなイベントもやったりしました。

全く英語をしゃべれないところから、同僚たちにサポートしてもらって徐々に喋れるようになってきて、気がつけば一人でロンドンやらルクセンブルクにいくまでになりました。駅前留学やら語学サポートやら、そういったものも特に使いませんでしたが、とにかく仕事をする上で英語が使えなければどうにもならない環境で少しずつでもしゃべることを覚えていった感じです。

エンジニアとしては、プロダクト初期のカオスのなかで、実現したいことのためにその方法を調べては考え、PoCを組んでうまくいくか試すプロセスはとても興奮する体験です。失敗もたくさんありますが、最終的にうまく動いてくれたときの達成感はこのタイミングだからこそ感じられると思います。

このあたりの話の体系だったまとめが最近ツイッターの連続ツイートでまとめられたので貼っておきます。

そういえば、DroidKaigi の運営スタッフ業も Drivemode に入社して以後関わっています。気がつけば最古参のひとりになっていて、ここまであっという間だったなと振り返ってみると思います。

次のチャレンジについて

会社規模としてはこれまでと比べて圧倒的に大きくなるのですが、プロダクトのフェーズとしては初期段階のカオスの中を駆け抜けるチャレンジングな時期だと思います。すごい人たちがめちゃくちゃたくさん集まった環境で、それでも自分の存在感や価値を見失わず磨きをかけていける場所での仕事でもあると思っています。

そういえば、Drivemode にいるころから「どこで仕事しているのか」とよく聞かれてきましたが、これまでと変わらずこれからも日本で仕事をします。海外に行く機会は減ってしまうでしょうが……場所はご存知六本木になります。今までは渋谷や表参道、新宿など山手線に近いエリアで仕事をしてきましたが、ついに一歩踏み込んで六本木になります。入社を決意するまで自分が六本木で働くというイメージは全くもっていませんでしたが、引っ越しやら何やらでこれまでよりもアクセスのいい場所を選んでみたらなんとなく実感が湧いてきました。

干芋

http://amzn.asia/8gORgFr

さよなら吉祥寺

大学を卒業してからずっと住み続けた吉祥寺(正確には吉祥寺から北に離れた練馬区のエリア)からついに離れることにした。今の家は広くて設備も整っていて家賃もお手頃なので、およそ考えうる限り通勤時間の短縮以外に引っ越す理由は見当たらないのだけれど、もう7年以上も住み続けてきて、不便に思ったこともないわけではないし、ちょうど世間的には引っ越しが落ち着く時期でもあるので、重い腰を上げて引っ越すことにした。

もともとは渋谷にアクセスの良いところでお手頃に住める場所ということと、住みたい街ランキング上位の常連であること、漫画の影響でなんとなくのんびりできてよさそうな街であることという基準で吉祥寺に住むことにした。その思惑通り、吉祥寺にはだいたいなんでもあって電車で渋谷や新宿に出なくても大抵のことは済んだし、井の頭公園にいけばのんびりもできた。これは後付けではあるけど、ロードバイクを趣味にしているので奥多摩方面へのアクセスがよいのも助かる。ただ一方で、家が吉祥寺の中心からは離れていて、むしろ西武新宿線のエリアといったほうがよいくらいだったので、そこは家選びをしくったなとは思っている。実際電車も西武新宿線はそれほど便利とは言えないし……その分で家賃がお手頃だったのは間違いない。

それでもやはり吉祥寺は気に入っていて、行きつけのつけ麺屋や油そばのお店があったり、松亭があったり、あの雰囲気の中で過ごす機会が減ってしまうのは寂しい。そう思う程度には長居をしすぎたとも言えるけど、とにかく今は新居での生活を楽しみにしようと思う。なんせ前回の引っ越しが7年以上も前だったのもあって、引越の手続きなどの勝手を完全に忘れ去っていていろいろてんやわんやしてしまったためにちょっとストレスを溜めてしまったけれども、この引っ越しのタイミングの一時的なものだし、引っ越しが終わったあとの生活にも楽しみなことがたくさんあるので、やっていこうな。

MotionLayout 内にある RecyclerView のスクロールを制御する

Note

この記事はcom.android.support.constraint:constraint-layout:2.0.0-alpha2時点のMotionLayoutについての記述です。 まだアルファ版なので将来的に挙動が大きく変わる可能性があります。

MotionLayout 内にある RecyclerView をスクロール可能にする

MotionScene内の<OnSwipe>moveWhenScrollAtTopというプロパティがあり、これがtrueのときにRecyclerViewがスクロール可能になります。これがfalseになると、アニメーションが動くべき方向にドラッグしたときにアニメーションが始まってしまいます。

たとえば、MotionScene で定義した動きとして縦にスクロールするRecyclerView を下にドラッグするとアニメーションするような場合、moveWhenScrollAtToptrueの場合は RecylerView のもつ 0 番目のアイテムが一番上にきたときにアニメーションを開始し、falseのときはスクロールせずにアニメーションを開始します。

RecylerView のスクロールと MotionLayout のアニメーション開始を別に扱いたい

moveWhenScrollAtToptrueのとき、RecyclerView を勢いよくフリングすると、0 番目のアイテムが一番上に来た直後に MotionLayout へMotionEventが流れてアニメーションが動いてしまいます。この動きは RecyclerView の定義にあるもので、自分がこれ以上スクロールできない位置まで来ると、NestedScroll できる親にMotionEventが流れるようになっています。

もし RecyclerView のスクロールに続いて MotionLayout のアニメーションを動かしたくない(RecyclerView のスクロールジェスチャと MotionLayout のスワイプジェスチャを別にする)場合は、RecyclerView のnestedScrollingEnabledfalseにします。 ただし、RecyclerView の ViewPort が一番端にないときに MotionLayout のアニメーションを動かすと、つぎに RecyclerView でスクロールジェスチャをしようとすると RecyclerView はスクロールせず MotionLayout のアニメーションが始まってしまうバグが起きる可能性があります。

MotionLayout の MotionScene で touchAnchorId に指定した View のクリックをハンドルしたい

MotionLayout は ConstraintLayout 2.0.0 から導入された、スワイプジェスチャやクリックジェスチャで View をキーフレームアニメーションさせるときに使うレイアウトです。キーフレームアニメーションは<MotionScene>をルートとする XML リソースで記述でき、ジェスチャ判定を割り当てる View の指定と、アニメーションの開始・終了時点での View のプロパティを宣言すると、MotionLayout がよしなにアニメーションを作ってくれます。

<MotionScene
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- constraintSetStart が開始時点の状態、constraintSetEnd が終了時点の状態 -->
  <Transition
    motion:constraintSetStart="@id/start"
    motion:constraintSetEnd="@id/end"
    motion:duration="500">
    <!-- スワイプジェスチャで動かすときの設定 -->
    <!-- touchAnchorId を使ってどの View にジェスチャ判定をさせるか指定する -->
    <OnSwipe
      motion:dragDirection="dragUp"
      motion:touchAnchorId="@id/swipe_gesture_anchor"
      motion:touchAnchorSide="bottom"
      motion:maxVelocity="1"
      motion:maxAcceleration="1"
      motion:moveWhenScrollAtTop="true"/>
  </Transition>
  <ConstraintSet android:id="@+id/start">
    <!-- ... -->
  </ConstraintSet>
  <ConstraintSet android:id="@+id/end">
    <!-- ... -->
  </ConstraintSet>
</MotionScene>

このとき、<OnSwipe>touchAnchorIdに指定した View でクリック判定をして何かしら処理をしようと思うといくつかの壁にぶち当たります。 touchAnchorId に指定した View で onTouchEvent を拾ってみると、MotionEvent.ACTION_DOWN は onTouchEvent に流れてくるものの、MotionEvent.ACTION_UP は onTouchEvent に流れてきません。 一方で、touchAnchorIdに指定した View に OnClickListener を設定すると、今度は MotionLayout 側に onTouchEvent が流れていきません(ジェスチャによるアニメーションが動かなくなる)。 MotionScene にはもう一つ<OnClick>もありますが、こちらはクリック時にキーフレームアニメーションを開始するためのもので、汎用的に何かしらの処理をトリガーするためのものではありません。

苦肉の策として次のようなコードを書いてみましたが、もうちょっといいやり方無いかな…

/* クリック判定を少し遅らせ、MotionLayout によるキーフレームアニメーションが始まらなければそのまま実行する */

val motionLayout: MotionLayout = // ...
val anchorView: View = // ...
val handler: Handler = // ...
val delayedClickTask: () -> Unit = {
  // ...
}

anchorView.setOnTouchListener { v, event ->
  handler.postDelayed(delayedClickTask, 300L)
  return@setOnTouchListener false
}
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
  override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
    handler.removeCallbacks(delayedClickTask)
    // ...
  }
  override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
    // ...
  }
})

Android の Picture in Picture と Window Focus

TL;DR

他のアプリが Picture in Picture モードに入り、そのオーバーレイ表示が自分のアプリに重なる場合、自分のアプリのActivityViewonWindowFocusChanged(boolean)は次の順で計3回呼び出されます。

  1. onWindowFocusChanged(true): タスクが他のアプリから自分のアプリに切り替わったとき
  2. onWindowFocusChanged(false): Picture in Picture の表示(アニメーション)が完了してフォーカスがその表示に奪われたとき
  3. onWindowFocusChanged(true): 2からおよそ3〜4秒後に自分のアプリにフォーカスが戻ったとき

なお Android P Beta3 on Essential Phone と Android P on Essential Phone で確認しています。

Picture in Picture とは

Picture in Picture (PiP) は Android O で登場した機能で、アプリの画面 (Activity) を他のアプリに重ねてスクリーンの隅に小さく表示する機能です。たとえば Google Maps でナビゲーションをしているときに他のアプリに切り替えると、ナビゲーションの地図が自動で縮小されてスクリーンの隅に表示されるような機能を指して Picture in Picture と呼んでいます。

この機能はActivityを縮小すると同時にその縮小表示を他のアプリに重ねるため、Runtime Permissions とは別の権限を必要とします。インストールと同時に許可がおりるので普段はあまり気にすることはありませんが、Picture in Picture の表示やシステムの設定画面から、Picture in Picture の権限を取り消すことができます。

Window Focus とは

ざっくりいうと、KeyEvent や TouchEvent を受け付けるWindowを管理する仕組みです。

WindowそのものについてはAndroid を支える技術〈Ⅰ〉に解説がありますが、基本的には各ActivityはひとつのWindowに属し、WindowManagerの管理下に置かれるようになっています。WindowManagerに直接自分でViewを登録する方法もあり、この場合登録したViewは新たなWindowに属し、WindowManagerの管理下に入ります。つまり一つのアプリケーションで複数のWindowが存在し得ます。

Picture in Picture に入る処理

AndroidManifestでPicture in Pictureをサポートしている宣言をし、アプリケーションがActivity#enterPictureInPictureMode(PictureInPictureParams)を呼び出すとPicture in Pictureモードに入ります。このときの処理の実体は(ActivityManagerService#enterPictureInPictureMode(IBinder, PictureInPictureParams))https://android.googlesource.com/platform/frameworks/base.git/+/android-cts-8.1_r7/services/core/java/com/android/server/am/ActivityManagerService.java#8089にあります。おおよそ次のような順で Picture in Picture モードに入る処理を呼び出します。

  1. すでに Picture in Picture モードの場合は何もしない
  2. Activity が Picture in Picture モードをサポートしない場合も何もしない
  3. KeyGuard が表示されている場合は解除を試み、成功したら Picture in Picture モードに入る
  4. KeyGuard が表示されていないければ即座に Picture in Picture モードに入る

この処理の中の"Picture in Picture モードに入る"部分にあたるのはActivityStackSupervisor#moveActivityToPinnedStackLocked(ActivityRecord, Rect, float, boolean, String)です。 ここではフォアグラウンドにいた Activity を Picture in Picture モードにするため、画面のリサイズに関する計算やアニメーションの再生をしています。 このメソッドの最後で Picture in Picture の状態を監視しているリスナーに、Picture in Picture モードに入ったことを知らせています。

Picture in Picture モードに入ったかどうかを監視するリスナーのひとつに、PipManagerがあります。 このPipManagerはハンドセット端末用のものと、TV 端末用のものとがそれぞれ別に存在します。

ハンドセット端末用のPipManagerは、Picture in Picture の表示を管理する PipMenuActivityControllerや、タッチのインタラクションを制御するPipMotionHelperPipTouchHandlerなどへ処理を委譲します。

Picture in Picture に入ってからの処理

Picture in Picture の表示は SystemUI の PipMenuActivity が担当しています。このActivityは WindowFocus を取得してから 3500 ミリ秒後に、Picture in Picture のメニュー(閉じるボタンや設定ボタンなど)を非表示にします。このタイミングでPipMenuActivityから Window Focus が失われ、その時フォアグラウンドにいるアプリの Activity に Window Focus が移動します。冒頭のTL;DRに書いた"およそ3~4秒後"というのはまさに、この 3500 ミリ秒後にメニューを非表示にする処理によるものです。

Picture in Picture に入ったとき以外にも、Picture in Picture モードの表示をタップしてすこし拡大したときにもPipMenuActivityが立ち上がって WindowFocus を奪います。よって、ソフトキーボードが出ている状態で Picture in Picture の表示をタップするとソフトキーボードが閉じるというわけです。