Infinito Nirone 7

白羽の矢を刺すスタイル

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 の表示をタップするとソフトキーボードが閉じるというわけです。

Android O からの Service を foreground で動かすときのベストプラクティス

Android Oreo から、Oreo 以上のバージョンをtargetSdkVersionにしているアプリケーションがForeground Serviceを起動するには、Context#startForegroundService()によるサービスの起動とService#startForeground()による通知の表示の両方を実行しなければならなくなりました。またContext#startForegroundService()から5秒以内にService#startForeground()を呼び出さないと、ANRとしてクラッシュレポートが作成されます。

この"Context#startForegroundService()から5秒以内にService#startForeground()を呼ぶ"というのが厄介で、ServiceがContext#startForegroundService()で動き始めたら問答無用で5秒のカウントが始まるため、その5秒以内にService#stopSelf()などでサービスを止めてもANRになります。 さらに、ANRのときに吐き出されるスタックトレース(実際には例外が飛んでアプリがクラッシュするのでクラッシュレポートも作られるはず)には、Service#startForeground()を呼ばなかったこと以外の情報が皆無なので、ログ出力などツールの力を借りる必要があります。

ここでServiceの起動にいくつかのパラメータが必要なパターンを考えてみます。ServiceActivityと同様に自分で直接インスタンスをつくることはしないので、Intentにパラメータを突っ込んで起動させます。このとき、パラメータの検証をServiceの責務として、検証が通らないとき(≒Serviceが期待するパラメータでないとき)にService#stopSelf()で止めてしまうと、先程説明したとおりService#startForeground()を呼ばなかったとしてクラッシュしてしまいます。

よって、Android O からはContext#startForegroundService()Serviceを起動させる側がServiceにわたすパラメータの検証をしなければなりません。

岐阜について feat. 田舎fm

田舎fm の出演回で自分の出身地である岐阜について話しました。

komatatsu.github.io

そして現在の最新回でまたまた岐阜の話題がでました。

komatatsu.github.io

自分は岐阜の西の方で、最新話では岐阜の東の方の話がでてきて、岐阜の東西と北の認識に分かりを得たのでもう少しこの記事で深く掘り下げてみようかと思います。 ちなみに、岐阜がどこにあるか不明な方のために地図のリンクをはっておきます。琵琶湖は隣の滋賀県にあります。

岐阜県内の地域

岐阜県は大きく分けると美濃地方(名古屋に近い南の方)と飛騨地方(飛騨・高山のある北の方)の2つの地域に分かれます。さらに美濃地方は東西に広く、おおきく東濃(多治見市や恵那市中津川市など)と西濃(大垣市や関が原町、海津市など)に分けられますが、実際には真ん中のあたりも区分けして中濃(関市や美濃加茂市可児市など)と言うこともあります。カンガルーの西濃運輸の西濃はこの西濃のことを指しています。

地図にある線路や道路のつながりを見ると、岐阜を通る路線はだいたいどれも名古屋にたどり着くようになっています。

電車の場合、西は京都方面から東海道線がつながっていて、東は長野方面から中央線がつながっています。北は富山まで続く高山本線があり、飛騨・高山の観光地に行く場合は大阪や名古屋から出ている特急が便利です。 高速道路もだいたい似たようなつながり方をしていて、西は名神高速、東は中央道、北は東海北陸道があり、すべて名古屋にたどり着きます。

なので、西の人も東の人もちょっと街に出て遊ぶといえば名古屋で遊ぶことになります。北の飛騨・高山と名古屋は流石に離れているので、富山に遊びに出る人たちがいると聞いたことがあります。

逆に、東西の行き来はあまり便利ではありません。国道21号線(ほぼ中山道)が東西を結ぶ主要な道路ですが、それ以外には大きな国道が通っていません。 美濃地方の東西の移動に鉄道路線を使う場合は名古屋まで出たほうが楽です。

新幹線も通っていますが辺鄙なところにある謎の駅なので、たいてい名古屋まで電車で行って乗り換えるほうが便利だったりします。

岐阜というと「豪雪地帯」のイメージがある人もいるようですが、西濃やその近辺の平野部にはほとんど積もりません。滋賀県福井県の県境にある山々が雪雲を止めてしまうので、平野部にはその山を超えてくる強風(伊吹おろし)が吹き付けます。

岐阜県には岐阜弁という方言がありますが、地域によってアクセントや訛りに違いがあります。岐阜県を舞台にした某のうりんアニメはヒロインが岐阜弁を喋っていますが、西の方に住んでいた自分としては「三河弁っぽいものが混じっている!」という感じで聞き慣れなさがありました。西の方では関西色が強くなるので、東の方の人たちからすると「関西弁が混じっている!」というように聞こえるかもしれません。 岐阜弁独特の語彙に、「机をつる(運ぶ)」や「鍵をかう(しめる)」、「ご飯をつける(よそう)」などがあります。

岐阜の観光地

定番はやはり白川郷や飛騨・高山のスキーリゾート、下呂温泉でしょう。岐阜はわからなくても飛騨・高山はわかる人が多いような気がするのは気のせいでしょうか。 これは完全に個人の感想ですが、下呂温泉にあるホテルくさかべアルメリアは、長野県にある白樺リゾート池の平ホテルの次くらいに東海地区でCMをバンバン打っている気がします。

登山が好きなら伊吹山地伊吹山鈴鹿山脈・養老山地、飛騨山脈北アルプス)など目白押しです。伊吹山乗鞍岳では自転車のヒルクライムレースも開催されています。 城好きなら岐阜城大垣城・一夜城・郡上八幡城などがあります。

この他、自然を楽しむという意味では、西の方では根尾谷断層やその近くにある淡墨の桜が有名ですし、養老の滝やその近くにある天命反転地も有名です。東の方では恵那峡景勝地として有名で、近くに恵那峡ランドという遊園地もあります。

県の中央付近に目を向けると、美濃市ではツアー・オブ・ジャパンという自転車レースのステージが開催されていたり、各務原市(かかみがはら)には航空自衛隊の基地があり、その近所に航空宇宙博物館があります。

最近は岐阜を見に来る人たちがいるようで、例えば君の名は聖地巡礼だったり、聲の形聖地巡礼だったりと、何かしらの聖地巡礼で密かに人気のようです。聲の形は完全に地元なのでいつも見てたような景色がそこかしこに出てくるのが良かったです。ちなみにのうりんは実在する高校がモデルで岐阜の真ん中のあたりにあり、僕らはみんな河合荘岐阜市内が舞台です。

岐阜を見に来るときに使うであろう岐阜駅に来たときには、駅前にぽつんと立っている黄金の信長像を一度見ておくと、謎の満足感を得られると思います。

岐阜の食

自分はほぼ西濃でよくある食文化しか知らないのですが、おおよそ名古屋に似て赤味噌が中心です。味噌汁は塩辛さがありますが、おでんやとんかつ、豆腐、焼きなす等につける味噌には甘みがあります。「つけてみそかけてみそ」という商品が定番です。

茶店では朝からモーニングメニューをかこんでおばちゃんたちが無限に井戸端会議をしています。分厚いトースト、ゆで卵、コールスローサラダ、ヨーグルト、コーヒーはおそらく定番のモーニングです。 地元の喫茶店のほうがよりいろんなバリエーションが楽しめるかもしれませんが、コメダ珈琲でも一通りのモーニングが楽しめます。

岐阜県特有の食べ物というと地域によって様々ありますが、実際のところ他地域の食べ物はあまり知らないことも多いです。以前会社の人に、岐阜の郷土料理であるところの朴葉焼きが出るところに連れて行ってもらったことがありますが、その時初めてそういう料理があるのだということを知ったくらいです。

岐阜の気候

西濃の特に大垣市海津市あたりは内陸にもかかわらず海抜5m地帯でさながらオランダのような感じです。大きな河川がいくつも(揖斐川長良川木曽川が特に大きい)あり、雨季に河川の氾濫が起きやすく、その対策としてあちこちに堤防が作られています。地下水が豊富で湧いて出てくる水を汲みにくる人たちが集まるほどですが、その分夏はものすごくジメジメした暑さになります。 東濃では特に多治見市あたりは内陸の盆地で夏はひたすら暑いです。毎年のようにニュースで取り上げられています。

おわりに

気がついたら長々と書いてしまいましたが、そんな感じです。

Firebase Storage にある複数の Bucket の Rules を Git で管理するための手順

Firebase Storage には Firestore や Realtime Database のようにルールの設定があり、JSON でパスやファイルごとに読み書きの権限設定を変更できます。 Firebase Storage は複数のバケットを作ることができます。おそらくは保管するファイルの用途ごとに使用するバケットを変える(公開用と非公開用とか)ような使い方を想定しているように思いますが、Firebase Storage のルールはこのバケットごとに設定可能な点が他のプロダクトと異なります。

Firebase にはfirebase-toolsというCLIコマンドがあり、このコマンドのdeployオプションを使うとCLIからルールを変更できます。ルールはテキストファイルに書き出したものをデプロイするので、ルールを Git の管理下に置いてバージョン管理できるようになります。

ここで何も考えずにルールのJSONをファイルに書き出して、次のようにfirebase.jsonを設定すると、デフォルトのバケットのみにルールが適用されます。

# storage.rules.json
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}
# firebase.json
{
  "storage": {
    "rules": "storage.rules.json"
  }
}

複数バケットがある場合はバケットごとのルールを書き出した個別のファイルを用意し、デプロイ時にはそれらを対応するバケットに適用されるよう設定するには、firebase-toolsのデプロイターゲットを使います。

$ firebase target:apply storage <name> <resource_name>

たとえば、バケットURI がそれぞれ、デフォルト=gs://myproject.appspot.com, 追加したもの=gs://myproject-a89ee.appspot.comだとすると

$ firebase target:apply storage main myproject.appspot.com
$ firebase target:apply storage sub myproject-a89ee.appspot.com

とします。<name>は好きなものを選びます。 これらのコマンドで、.firebasercに次のような差分が生成されます。

...
+  },
+  "targets": {
+    "myproject": {
+      "storage": {
+        "main": [
+          "myproject.appspot.com"
+        ],
+        "sub": [
+          "myproject-a89ee"
+        ]
+      }
+    }
... 

これをもとに、firebase.jsonを次のように書き換えます。

# firebase.json
{
  "storage": [
    {
      "target": "main"
      "rules": "storage.rules.json"
    },
    {
      "target": "sub"
      "rules": "storage_sub.rules.json"
    }
  ]
}

これで、storage.rules.jsonはデフォルトのバケット用に、storage_sub.rules.jsonは追加したバケット用にそれぞれルールを定義できるようになります。

初キャンプ@ふもとっぱら

つい最近はやった某キャンプアニメにあやかって、ゆるくキャンプしようぜ!と知人と話しをしていて、ふもとっぱらキャンプ場でキャンプをしてきました。

ふもとっぱら - fumotoppara.net -

ふもとっぱらは、本栖湖の南、富士山麓のなかでも静岡県側にあります。車でのアクセスでは中央道でも東名でも行けると思います。 とにかくかなり広い高原で車に横付けしてテントを設営できるオートキャンプ場で、朝と夕方はそれぞれ見ごたえのある富士山を拝めます。

自分は車もキャンプ用具も持ち合わせてはいないので、ほとんどをレンタルサービスに頼りました。車はもともとレンタカーをよくつかうので慣れているのですが、テントやシュラフもレンタルが可能なことをつい最近知りました。今回は旧知の友人と行きましたが、自分たちで買ったものと言えば、食材・飲み物と簡易コンロ・木炭など食事に関するもののみでした。

レンタルテントやシュラフは郵送で受取・返却ができます。この週末は受取のタイミングがギリギリだったので郵送での受取はできませんでしたが、返却では着払いの郵送をしました(レンタル時の金額によっては元払いになるそう)。

テントの設営からして完全に素人でしたが、なんやかんやサッとテントが設営でき、楽しくキャンプができました。 これからの季節日の出がかなり早いので、朝起きるのがはやくなります。いいカメラをもっていると、朝焼けの富士山をおさめることもできます。

次にキャンプする時は、もう少し良い炭を使って焼肉をしたり、お湯を沸かす装備を持っていってゆったりとスープやコーヒーを楽しむなどもしてみたいなと思いました。

Mirage Solo で開発者オプションをいじるときは「ポインタの位置」を有効にしない

そろそろ自分も VRバイスを一個くらい手元において遊べるようにしておこうとおもって、先週末 Mirage Solo を注文しました。Amazon 先生がサッと発送してくれたので週末にシュッと受け取れました。

開発者なので電源投入してすぐ開発者オプションを ON にしたのですが、この開発者オプションの中にある「ポインタの位置」を ON にすると、ポインタの位置のオーバレイはきちんと出るのですが設定画面が表示されなくなるようです。設定画面自体は Chrome など VR 非対応のアプリとおなじように、スマホの画面のようなものが VR 空間に浮かび上がるように表示されますが、なぜか Chrome は大丈夫で設定画面はダメなようです… もし ON にしてしまって設定画面が触れなくなった場合は、adb shell settings put system pointer_location 0を叩いて戻してあげましょう。

VR コンテンツそのものは軽くサッと触っただけでまだほとんど楽しめてないので、もうしばらくはいろいろ試しながら遊んでみようと思います。