Infinito Nirone 7

白羽の矢を刺すスタイル

C93 Android モダンプログラミングに RTL 対応の章を書きました

明日から C93 が始まりますね。TechBooster から Android モダンプログラミングという新刊が出ますが、そのなかにある RTL 対応の章を担当しました。

techbooster.github.io

この章を見れば RTL 対応に必要なことが大体網羅できるはずです。よくあるレイアウトの話以外にも RTL 対応が必要なことはたくさんあるので、その辺をシュッとイイカンジにやるための内容をコンパクトにまとめました。

で、一つこの章には載せきれなかった話があります。レビューをしてもらっているときに「ViewPager ってどうなの」という話が出てきて、そういえばヤツは RTL だからといって向きが逆になったりしないな?と思ってちょっと調べてみました。

結論というか、普通にしてても ViewPager は RTL だろうとなんだろうと左から右にページを追加していくので、何かしらのトリックが必要です。いくつか方法があります。

一つ目は、ViewPager を rotationY で 180度まわしたあと、それぞれのページをさらに rotationY で 180度まわしたら向きを逆にできるのでこれで RTL 対応できる!というものすごくアクロバティックな Workaround。いろいろ犠牲にしているものがありそうな気がしますが、PagerTabStrip を一緒に使っているとそれの様子がおかしくなるようです。

mobikul.com

二つ目は、RTL のときと LTR のときで Adapter でもつコレクションの中身を逆順にする方法。最初に表示する位置を LTR のときは 0 で RTL のときは size - 1 にすれば擬似的に RTL 対応できます。単純でわかりやすい解決方法です。

stackoverflow.com

この他、サードパーティ製の RTL に対応した ViewPager ライブラリがいくつか存在します。ViewPager のロジックに RTL へ対応するためのコードを書き足す一番素直な解決方法です。

github.com

github.com

github.com

何にしても頑張りが必要ですね。一番ラクなのは二つ目の"Adapter でもつコレクションの中身を逆順にする"方法だと思います。

諸君、私はAndroidが好きだ

諸君、私は戦争が好きだ: wids.net

作ってみました。

諸君、私はAndroidが好きだ
諸君、私はAndroidが好きだ
諸君、私はAndroidが大好きだ

クラッシュが好きだ
ANRが好きだ
機種依存問題が好きだ
激安端末が好きだ
売れないタブレットが好きだ

アメリカで
日本で
中国で
インドで
ヨーロッパで

この地上に存在するありとあらゆるAndroidが大好きだ

機種依存問題でクラッシュするときが好きだ
OSバージョンが上がると別の問題が発生するときなど心がおどる

フレームワークをハックすることが好きだ
リフレクションとIPCを駆使して便利機能を実現したときなど胸がすくような気持ちだった

OSのカスタマイズが好きだ
iOS と寸分違わぬ見た目になったときなど感動すらおぼえる

とても小さな筐体の端末で Android が動いているときなどもうたまらない
Google I/O で新しいバージョンが発表されるのは最高だ

iPhone 端末を落として画面を破壊したのを
見た時など絶頂すら覚える

ハードウェアのカスタマイズが好きだ
SDカードの書き込み先が内部ストレージのときはとてもとても悲しいものだ

多様性のある端末の筐体が好きだ
Android なんてダサいよねと言われるのは屈辱の極みだ

諸君 私はAndroidを 重戦車様なAndroidを望んでいる
諸君 私に付き従うAndroid好きの諸君 君たちは一体何を望んでいる?
更なるAndroidを望むか 
糞の様なAndroidを望むか?
核シェルターのようなAndroidを望むか?


Android!! Android!! Android!!


よろしい ならばAndroidだ

だが、Galaxy端末で無限に設定が保存されないバグに耐え続けて来た我々には
ただのAndroidではもはや足りない!!
大Androidを!! 一心不乱の大Androidを!!

我々はわずかに小数
iPhoneユーザーに比べれば物の数ではない
だが諸君は一騎当千のAndroidユーザーだと私は信じている
ならば我らは諸君と私で総兵力100万と1人の幾多のクラッシュを超えて無敗の集団となる
我らを忘却の彼方へと追いやり、iPhoneユーザーを叩きのめそう
髪の毛をつかんで引きずり下ろし 眼(まなこ)をあけて思い出させよう

連中にAutoLayoutの苦行を思い出させてやる
連中にXCodeの苦行を思い出させてやる
Androidには奴らの哲学では思いもよらないGoogleがある事を思い出させてやる
1000人のAndroidユーザーの集団で 世界をAndroidで埋め尽くしてやる

目標 Apple

Pixel作戦 状況を開始せよ

征くぞ 諸君

Canon EOS 6D MarkII を持って散歩した

前まで使っていた EOS Kiss X3 が天寿を全うしまして、新しいのほしいなと思っていたときに神楽坂つむりさんのブログ記事を読んでフルサイズよさそうだな?と思っていたところ、フルサイズはいいぞという声をTwitterでも頂いたので、自転車もあることだしあちこち行った先でパシャパシャ写真でも!ということでいろいろ撮ってみました。写真のウデマエは……構図の基本が知識としてあるくらいなのでウマいわけではないんですが、色んな人の写真を見ているとなにで撮ってても「この構図いい!」と思ったら即パク真似してみたりしています。来年はもっとカメラと一緒に出かけたいなぁ。ちなみに新しいカメラはEOS 6D MarkIIで今年一番高い買い物でした。たぶん。結果フルサイズはいいぞってことで、買ってから既に2ヶ月くらいたってますがあちこちで撮りまくってます。

f:id:KeithYokoma:20171105123249j:plain

f:id:KeithYokoma:20171105124956j:plain

f:id:KeithYokoma:20171105125352j:plain

f:id:KeithYokoma:20171105125403j:plain

f:id:KeithYokoma:20171105153920j:plain

f:id:KeithYokoma:20171105154317j:plain

f:id:KeithYokoma:20171105154434j:plain

f:id:KeithYokoma:20171105154451j:plain

f:id:KeithYokoma:20171106230925j:plain

f:id:KeithYokoma:20171111130529j:plain

f:id:KeithYokoma:20171111180813j:plain

f:id:KeithYokoma:20171112152543j:plain

f:id:KeithYokoma:20171112152643j:plain

f:id:KeithYokoma:20171218134844j:plain

f:id:KeithYokoma:20171218132403j:plain

f:id:KeithYokoma:20171209142001j:plain

f:id:KeithYokoma:20171209140651j:plain

f:id:KeithYokoma:20171210130839j:plain

フルサイズはいいぞ。

レンズは標準のやつを使っています。EOS Kiss のときのものは使いまわせないのでどうにかしよう。フルサイズともなるとレンズ沼にはまったらすごい勢いでお金が溶けていくのが目に見えているので、レンタルとかで楽しむのもアリかなと思っています。一応パンケーキレンズもあるし、困るのはすごい遠い被写体に寄りたいときだけ。でも知ってるんだ、望遠レンズは暗くなりがちでその中でも明るいレンズを選ぼうとするとカメラがもう一台買えるくらいすることを……やはりレンタルだ。

SparseArray から要素を取り出したときに ClassCastException が発生するパターン

SparseArrayAndroidフレームワークにあるコレクションの一種で、Integer を key にした HashMap よりもメモリ効率がよいとされるコレクションです。

SparseArray には 2 通りの値を取り出すメソッドがあります。一つはSparseArray#get(int)もう一つはSparseArray#valueAt(int)です。 どちらのメソッドも同じint型の引数をとりますが、getメソッドの引数はkeyで渡された値をもとにバイナリサーチをかけて内部の配列のindexを決めており、valuesAtメソッドの引数はindexで値がそのまま内部の配列のindexとして扱われます。

SparseArray はまた要素を追加した後に削除することもできます。こちらも 2 通りのメソッドがあり、それぞれSparseArray#delete(int) / SparseArray#remove(int)SparseArray#removeAt(int)で、delete(int) / remove(int)の引数はkeyでこれをもとにバイナリサーチをしてアクセスすべきindexを決め、removeAt(int)の引数はindexでそのまま内部の配列のインデックスとなります。

他にも同じパターンで引数のintkeyなのかindexなのかで挙動の異なるメソッドがあります。

さてここで、一度 SparseArray に保存した値を削除し、再度取り出すことを試してみます。

SparseArray#put(int, V)で指定したkeyに保存したのち、SparseArray#remove(int)で指定したkeyに対応する値を削除、SparseArray#valueAt(int)で先頭の要素を取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")

同じようなことをSparseArray#get(int)で実行する場合は次の通りで、get メソッドに渡すkeykeyAt(int)で取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")

それぞれどのような結果になるかというと、SparseArray#valueAt(int)の場合は最後の行でClassCastExceptionが発生してクラッシュし、SparseArray#get(int)の場合は最後の行でログに"Value at [0] == null"と出力されます。

SparseArray では要素の削除を実行すると、内部で保持している配列の該当箇所に削除したことを示す DELETED という Object 型の定数を代入します。SparseArray は型パラメータでどの型のオブジェクトが保存されるか指定できますが、実際には内部で要素を保持している配列は Object[] です。そして SparseArray#get(int) はその場所にある要素が DELETED なら null ないしは指定した値を返すようになっていますが、SparseArray#valueAt(int)は特にそのようなチェックなしに指定した場所にある要素を返しています。これがSparseArray#valueAt(int)を使ったときにClassCastExceptionが投げられる理由です。

要素が全部なくなったのに要素にアクセスしようとするというのはよくない状況です。get(int)keyAt(int)を組み合わせて null チェックをすることでClassCastExceptionは回避できますが、根本的に並行処理に問題がある(SparseArray はスレッドセーフではない)ということなので、クラッシュレポート等で身に覚えのない ClassCastException がある時にはこのパターンを疑ってみると良いと思います。

例外をネストしたクラスとして定義するときは必ず static なネストしたクラスにする

いい具合にトラブってしまったのでメモです。

クラスの定義にはいくつかの種類がありますが、あるクラスの内部にネストした(入れ子になった)クラスを定義することができます。このとき、修飾子として static をつけたネストしたクラスと、それがないネストしたクラスのどちらも正しい記述です。

public class OuterClass {

  // static でないネストしたクラス
  public class InnerClass {

  }

  // static なネストしたクラス
  public static class StaticInnerClass {

  }
}

static の有無で何が違うかといえば、static でない方は暗黙的に外側のクラス(上記の例ではOuterClass)への参照を持つため、外側のクラスのメンバにアクセスできるという点があります。 ここで、例外を何かのクラスにネストしたクラスとして定義してみたとすると、直列化(シリアライズ)で問題が起こります。

public class OuterClass {

  // static でないネストしたクラスとして例外を定義
  public class BadException extends RuntimeException {}
}

暗黙のうちにもつ外側のクラスへの参照は transient ではないので、シリアライズの対象になります。例外はSerializableを実装しているので直列化ができますが、このBadExceptionを直列化すると外側のクラスへの参照もシリアライズの対象となるものの、肝心のOuterClassSerializableを実装していないので、直列化できずに例外が投げられます。

暗黙の内に持つ外側のクラスへの参照が引き起こす問題は、メモリリークの文脈で語られることが多いように思いますが、このように直列化でも問題となります。

public class OuterClass {

  // static なネストしたクラスとして例外を定義
  public static class MyException extends RuntimeException {}
}

例外にかぎらず、ネストしたクラスがSerializableなクラスを継承する場合も同様に、static なネストしたクラスとして定義しましょう。

RxJava1 から RxJava2 へ移行する時に nullable な値とうまくつきあう

次のような RxJava1 のコードを RxJava2 に移行することを考えます。

import rx.Observable; // RxJava1

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.fromEmitter(emitter -> emitter.onNext(nullableValue))
      .filter(nullableValue -> nullableValue != null);
}

Observable のソースとなる値は Nullable で、それを filter で null チェックをかませることで値を受け取る側は NonNull を前提にできるような感じですね。 RxJava2 では onNext() に値を渡す時点で NonNull であることを求められるため、これをそのまま RxJava2 に書き換えると実行時に例外がスローされます。

import io.reactivex.Observable; // RxJava2

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.create(emitter -> emitter.onNext(nullableValue)) // nullable な値を onNext には渡せない
      .filter(nullableValue -> nullableValue != null); // 無意味な filter になる
}

ここで、nullableValue を onNext() に渡す時点で Optional*1 を使ってラップしてみると、うまく onNext には NonNull な値を渡せるようになります。 また、その後に filter と map を駆使すると、メソッドの返り値の宣言を変更することなく RxJava2 に移行できます。

import io.reactivex.Observable; // RxJava2

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.create(emitter -> emitter.onNext(Optional.of(nullableValue))) // Optional<Value> は NonNull
      .filter(Optional::isPresent) // RxJava1 での null チェックと同じ効果が得られる
      .map(Optional::get); // filter で値が存在することをチェックしているので安全に get できる
}

Android の場合 Optional は GitHub - memoizr/retro-optional: A backport of Java8 optionals for Java7 を使うと古い OS バージョンでも利用できます。

*1:java.util.Optional では ofNullable でラップします。バックポート版ではofでラップします。

自転車でめぐる SHIROBAKO の聖地

この記事は SHIROBAKO Advent Calendar 2017 の 17 日目の記事です。

adventar.org

SHIROBAKO には木佐さんという自転車が趣味のアニメーターさんがいます。えくそだすっ!のときには、落合がカナンの人と話をしているところを目撃したり、8 カット上げたことあるしヨユーヨユーとか言って直後に調子でないと言ったりとしれっと重要な場面に登場していました。 そんな木佐さんが発した「仕事大事!だけどヒルクライム入賞も夢なんだ!」と言う言葉には宮森と同じく自分もはっとさせられ、気がついたら今年は自分もヒルクライムレースに何度か出場していました。分かるんですよ、あの「敵はいるけど、本当の敵はむしろ自分ッ」とか「自分を追い込んで追い込んで……こうねっ」って言いたくなる気持ち。

SHIROBAKO 放送当時はあまり気が付きませんでしたが、よく見ていると自分の家の近所だったり、よく自転車で走っている場所が描かれていることに気が付きました。 そこで今回のアドベントカレンダーのネタとして「自転車でめぐる SHIROBAKO の聖地」と題し、軽い運動がてら気軽に見て回れる場所を吉祥寺駅周辺から武蔵境駅周辺にかけて実際に走ってみたレポートをしてみようとおもいます。

コースは吉祥寺サンロード入口をスタート地点に、武蔵境駅周辺を巡ったあと松亭でフィニッシュです。

行程

今回の行程です。途中徒歩の区間があるのでその部分は自転車を押し歩きしています。

スタート:吉祥寺サンロード入口

4話で、神仏混淆七福神を作ったメンバーが吉祥寺に集まったときにいた場所です。メンチカツやコロッケで有名なサトウ(作中ではすどう)、帽子をいろいろと試着していたのは SHAZBOT、映画館(作中の NEW バウスシアター)は既に取り壊されて ROUND1 になっています。サンロードは自転車をおして歩きましょう。

f:id:KeithYokoma:20171210144056j:plain

吉祥寺駅

サンロード付近で遊ぶために集合した地点です。現在クリスマスシーズン真っ只中ですので、原画売の少女のときのような雰囲気を楽しめますね!

f:id:KeithYokoma:20171210144009j:plain

マルイ:Pancake House

作中では Pancake Home になっていたお店です。マルイの1Fに入っています。反対側には the 3rd Burger があります。

f:id:KeithYokoma:20171210144519j:plain

井の頭公園:弁財天

マルイからならば七井橋通り(一番人通りのあるスタバなどが面している通り)を通って七井橋をわたり、弁財天に向かいます。 この後吉祥寺通りに出たらまっすぐ北に向かいます。途中アニメイト吉祥寺の入っているパルコ(作中時点ではパルコではなくユニクロの向かい側にあった)が右手に見えます。

f:id:KeithYokoma:20171105125352j:plain
井の頭弁財天

青梅街道・都道7号線(五日市街道)分岐点

吉祥寺通りを青梅街道との交差点まで行き左折。西にしばらく行くとジョジョなどの制作を手掛けた会社が右手に見えます。

さらにそこから西へ行くと左車線が斜めに分岐する地点に到達します。

f:id:KeithYokoma:20171210150554j:plain

この地点は11話で宮森の車がドリフトしながら走り抜けていった場所です。

この都道7号線、SHIROBAKOではかなりの頻度で登場します。 落合がカナンスタジオの人と話しをしているところを木佐さんが目撃したのもこの道です。

ちなみに青梅街道をそのまままっすぐ進んで新青梅街道との交差点まで進むと、絵麻ちゃんと井口さんが散歩に来た場所が近くにあったり、ちょくちょく背景にうつるタワー(田無ワター)があったり、 新青梅街道をずっと西へいくと1話で宮森と富ヶ谷がレースをしていた陸橋2つがあったり、ゴスロリさんのオアシスがあったり(現在閉店)します。

柳橋交差点

えくそだすっ!に出てくる交差点です。3人が武蔵野ドームを飛び出してから間もないところで出てきます。作中ではいなげやの方角から青梅街道に向かって逃げていきます。

f:id:KeithYokoma:20171210151333j:plain

今回はここを左折して武蔵境通りを武蔵境駅に向かいます。

いなげや

2期オープニングで宮森の車がグイッと曲がって井の頭通りに出るシーンにうつるいなげやは武蔵境通りと井の頭通りの交差点にあります。

f:id:KeithYokoma:20171210151601j:plain

桜橋

1期オープニングで宮森の車が朝日にむかってシュッと飛んだり宮森が自転車で走っているところなど多々使われています。

武蔵野アニメーション社屋

桜橋を超えてすぐ、現在は老人ホームが建っているあたりが武蔵野アニメーション社屋があった場所なはずです。

f:id:KeithYokoma:20171210152002j:plain

社屋すぐ南の交差点は6話で宮森の姉が絵麻ちゃんを見かけて声をかけようとした場面で使われています。

f:id:KeithYokoma:20171210152055j:plain

TAIRAYA

5話で絵麻ちゃんと宮森が買い物をしたスーパーです。作中ではAIRAYAになっています。

f:id:KeithYokoma:20171210152207j:plain

すきっぷ通り

6話で宮森の姉がきたとき、またタローと平岡が酔いつぶれたいた場所でもあります。今でも日高屋が営業を続けています。

f:id:KeithYokoma:20171210152255j:plain

武蔵境駅

ここも宮森の姉の上京シーンのほか、りーちゃんが舞茸さんの弟子になりたくて「頑張りマスタング!」と食い下がったところでもあります。 駅の西側にあるSWINGビルは木下監督が複葉機にのって飛び去るシーンにも描かれていますね。

f:id:KeithYokoma:20171210153119j:plain

武蔵野プレイス

みーちゃんが宮森と話をするシーン、13話でりーちゃんが宮森と話をするシーン等あります。

f:id:KeithYokoma:20171210152826j:plain

ウルソン

本田さんが転職した先の洋菓子屋さん(作中ではウルリン)です。

f:id:KeithYokoma:20171210152418j:plain

ちなみにこの道を少し言って亜細亜大学の裏手を曲がると油そばで有名な珍々亭があります。

境橋

1話で瀬川さんの家に緊急のカットを取りに行く場面で出てきます。 この場面で宮森が走っていった方角の都道7号線を行くと小金井公園があり、矢野さんと平岡がタイタニックに向かうときに出てくる風景でもあります。 今回はこの逆、吉祥寺方面へ向かいます。

ガスト

境橋の次の信号、関前五丁目の交差点にあります。 9話でずかちゃんのガヤアフレコ後に夕食をとっていたところです。みーちゃんがプリウスを見かけてホイールに見とれている場所でもあります。

f:id:KeithYokoma:20171210153914j:plain

ゴール:松亭

誰もが知る松亭ですね。いい運動をしたので定食を食べても大丈夫なはずです。気をつけて帰りましょう。

f:id:KeithYokoma:20171216104301j:plain

動画

youtu.be

さいごに

最終話、木佐さんの自転車のサドルがシートポストごと外されブロッコリーが刺さっていたのは、一時期すこし話題になった事件をネタにしているのでしょうか。いずれにしても最後の最後完パケてから返してもらうことが出来たようです。最終話といえば、宮森の新幹線が雪で徐行するシーンがありましたが、あれは間違いなくうちの地元のちかくで毎年恒例なので何はなくともすまんッという気持ちで見ていました。

都内にはまだまだいろんな聖地があるようです。行ったことのない場所も多いので、年が明けたらまた巡ってみようと思います。