Shared element transitionでリストの要素を並び替える
やりたいこと
Fragmentを切り替えてReordering(要素を並び替え)する shared element transition
の記事を見て楽しそうだったからやってみた。
medium.com
全く同じことをやっても楽しくないので、Fragment <-> Activityでやってみる。
できた
いきなりだけど成果物
コードはこの辺
やったこと
Activityで shared element transition
するには startActivity()
するときに第2引数にtransition animationのbundleを渡してやる必要がある。
transition animationを作るのに必要なのは下記のPair.
- 移動元になるview
- 上記viewに対して一意になるID
今回はリストの並び替えなので上記のpairを並び替えたいViewの数だけ持つ必要がある。
RecyclerView
でリストを表示している場合、Adapterの onBindViewHolder
で各Viewにアクセスできるのでこれを保持しておく。
class ReorderingAdapter(private val activity: Activity) : RecyclerView.Adapter<ReorderingViewHolder>() { val items = mutableListOf<ImageView>() // adapterのpropertyとしてmutableListを持つ override fun onBindViewHolder(holder: ReorderingViewHolder, position: Int) { holder.bind(position) if (!items.contains(holder.image)) { items.add(holder.image) // 画面に表示されたタイミングでlistに追加 } } }
実際に並び替えを実行するタイミングで保持していたviewのリストからtransition animation用のbundleを生成する。
fab.setOnClickListener { context?.let { val itemList = mutableListOf<Pair<View, String>>() (recycler.adapter as? ReorderingAdapter)?.items?.forEachIndexed { index, view -> itemList.add(Pair(view, IMAGE_TRANSITION_NAME + index)) } val options = ActivityOptionsCompat.makeSceneTransitionAnimation( activity as Activity, *itemList.toTypedArray() ).toBundle() ReorderingActivity.start(it, options) } }
並び替え後のviewの方でも同じIDを設定する。
itemView.setTag(R.id.position, position) ViewCompat.setTransitionName(image, IMAGE_TRANSITION_NAME + position)
あとは普通の Shared element transition
のように postponeEnterTransition()
したり見た目の微調整して終わり。
注意点として、画面に表示されていないものはanimationの対象にならない。
そもそも onBindViewHolder()
を通らないとitemsのlistにaddできないが、仮にlistに存在していたとしても画面外だったらanimationされない。RecyclerViewを使っていたのですでにrecycleされてしまっている場合はviewがなくなっちゃってるからanimationできないのかな。
おまけ
transition animation用のbundleを生成する時に ActivityOptionsCompat#makeSceneTransitionAnimation() を使うが、このメソッドの第2引数が可変長引数になっている。
@NonNull @SuppressWarnings("unchecked") public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity, Pair<View, String>... sharedElements) { if (Build.VERSION.SDK_INT >= 21) { android.util.Pair<View, String>[] pairs = null; if (sharedElements != null) { pairs = new android.util.Pair[sharedElements.length]; for (int i = 0; i < sharedElements.length; i++) { pairs[i] = android.util.Pair.create( sharedElements[i].first, sharedElements[i].second); } } return createImpl(ActivityOptions.makeSceneTransitionAnimation(activity, pairs)); } return new ActivityOptionsCompat(); }
可変長引数にListを渡すときは一度Arrayに型変換してからspread operator(*
)を使う。
*itemList.toTypedArray()
2018年前半を振り返る
はじめに
2018年前半に自分が何をしていたか後から振り返られるようにメモを残しておく。2017年のまとめと同じく自分のtweetを振り返ってペタペタしていく。本当は年末にまとめて一年分やりたかったがtweet数が多すぎて一気にやると辛いという前回の反省を生かして2018年は半分に区切っていく。
前回のまとめはこちら
1月
この時点で割と仕事に余裕があるときは隙あらば有休消化に励んでいた気がする。同じく(・∀・) https://t.co/4aR39jEUUh
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月4日
普段アニメをあまりみないけどずっと気になっていた廻るピングドラムを見始めた。最初はサイコホラーなのかサスペンスなのかシュールコメディなのかわからなかったけど後半一気に止まらない感じがすごかった。Prime Videoに廻るピングドラムきてるー!!やったー!!一話だけ見てみたけど「生存戦略」の元ネタこれだったのね
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月4日
社のslackに「生存戦略」のスタンプがあってなんだろうと思っていたけど元ネタがわかってスッキリ。
第24回 Androidもくもく勉強会@ Rettyオフィス を公開しました!今回が最終回となります!皆様ぜひご参加くださいませ〜!!٩( 'ω' )و #AndroidMokuMokuRetty https://t.co/qKFuWYNN0o
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月11日
過去最高に人数多いので今日は楽しい! #AndroidMokuMokuRetty
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月30日
長らく続けていたAndroidもくもく会、自分の退職に伴い最終回を迎えた。運営している自分もとっても楽しかったし本当にありがとうございました!みなさま本日はありがとうございました!またどこかでお会いしましょう!延長戦は大好きなビストロチック、フォアグラのパイ包みアップルパイ風とリゾットが絶品でした♥️ https://t.co/1CdPWK7j7m #AndroidMokuMokuRetty pic.twitter.com/Ljp49IgZ1U
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月30日
今年は(まだ半分しか終わってないけど)これと同じ内容5回くらいつぶやいている気がする。今でも絶対にフォローしているつもりだけど実はフォローしていない人いっぱいいそうだ。Twitter昔と違ってフォローしてない人の投稿もガンガン流れてくるので、あれ?まだこの人フォローしてなかったんだっけ!?みたいな気持ちになる。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月15日
DroidKaigi appにコントリビュートする実績解除した。DroidKaigi conf appについにコントリビュートできた 🎉🎉 去年は自分の資料作るのに必死だったから一年越しの念願達成だ〜!メンテナーのみなさんめちゃめちゃリアクションとレビューが早いのでびっくり!すごい!
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月18日
DroidKaigi preludeでgfxさんと二人でセッション解説をやった。会場にいた誰よりも楽しんでいた自信があるw今日はこれ解説枠で出ます!残念ながらshirajiさんはご家族の都合でいらっしゃらないとのことなので、gfxさんと二人解説。どきどき… 好きなこといっぱい話せるといいな! https://t.co/aSXxKfGNZW #dk_prelude
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月26日
最終出社日、最後のPRで自分で自分を消すやつ、一度やってみたかったので記念スクショ撮ってた。ずっとやりたかったやつ☺️☺️☺️ pic.twitter.com/vMsn2D9x6U
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月31日
1月は送別会とかでたくさん美味しいもの食べたり、寄せ書きが嬉しすぎて号泣したり忙しかった。幸せなことだな〜〜〜
2月
今日から暫く週休7日生活ですが、記念すべき第一日めは家から一歩も出ずに過ごせました
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月1日
2月から丸一ヶ月有休消化で寒すぎて引きこもりまくってた。週休七日生活、始める前はもっと昼くらいまで寝てたりとか、明るいうちからガンガンお酒飲んだりとか、会社員だとできない自堕落な生活を送りまくるかと思っていたけど、実際は出社していた時よりも早い時間から作業を開始するし毎日家事もちゃんとするし自分のハイ意識さにびっくりしている
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月19日
撮り逃してたと思ったらTwitterにあげてくれている方がいたので嬉しい☺️☺️☺️ https://t.co/XK2IqJiCsO
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月8日
こにふぁーさんに「今無職なんですか?」って聞かれたのと、おがぱんさんと写真とれたのが今日のハイライト
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月8日
DroidKaigi今年も楽しかったー!!!脳がへろへろだー。なにも考えずに雲丹食べるぞ!!!
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月9日
DroidKaigi、今年はオーディエンス参加だった。濃い二日間でした!
確定申告めちゃめちゃめんどくさい…なんで余分に労働したり消費したりして社会に貢献しているのにこんな罰ゲームみたいな目にあうんだ
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月11日
今年は初めて確定申告した。一度フローを理解するとそんなでもないはずなんだけど、初回はなかなかつらみがあった。確定申告一瞬で終わって逆にめっちゃ不安なんだけど…
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月21日
2月にこんなことをつぶやいているけど、2018年後半始まっても未だに毎月スターエンジニアが誰かしら転職している気がする。私が言うのもなんだけど、ここ最近のAndroid界隈、今までで一番転とか離とか多い気がするな?(当社比)
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月13日
Paymoの残高まだまだ余ってます。Paymo支払いの飲み会とかやりたい。ただしPaymoで支払う側として。そういえば弊社は美味しいもの好きな人が異常に多くて、みんなで外食する機会も多いのでまとめて払う事が多かった私はpaymo残高が40000円近く残っているけど、次の職場で割り勘アプリ使う機会はあるのだろうか🤔
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月22日
南の島にきました pic.twitter.com/2ExrMxuBmM
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月23日
有休消化でのんびり南の島にバカンスに行った。一人で。今までいったどの都市よりもサムイ島の接客業の人はにこにこしてる。さすが微笑みの国タイ!今日は笑顔が素敵な推しのレストランで働いている女の子が、夕方に彼氏が迎えに来てスクーターの後ろにのって一緒に帰ってるの目撃しちゃって、何それ最高じゃない?ってなってる今
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月24日
主にビーチサイドで本を読んでいたんだけど、そのとき読んだ本の記録はこちら。 muumuutech.hatenablog.com
日本に戻って来たら花粉が飛び始めていた。東京思ったより寒くない。やったー!しかし今日から花粉との戦いが始まるのかー( ;∀;)
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月27日
3月
新しい職場。外国人の同僚も多いのでここから英語も併記したりし始めている。同期入社(join at the same day) with @rallat 😎 https://t.co/9EiB6M22qB
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月1日
花粉がマジで辛い目と鼻と喉に深刻なダメージ
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月4日
新しい職場でももくもく会をやっていくぞ!どうも、もくもく会歴長い新メンバーです✋六本木でもやってくぞ!! #mokumoku_android https://t.co/13ucmQvvLN
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月14日
次回の開催はこちら
mercari-android-mokumoku.connpass.com
Flutter盛り上がって来たFlutter楽しそうだな〜 #potatotips
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月13日
shibuya.apkがライブ配信されるようになった。DeployGateさんのオフィスで配信見ながら女子エンジニア会。美味しいお料理とお酒で最高だったshibuya apk見ながら美味しいお酒とごはんで圧倒的勝ち組…!配信ありがとうございます🙏🙏 https://t.co/CaHikcnMIw
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月20日
友人の家でバーフバリ鑑賞会をやった。ボリウッド映画多分初めてだったけど楽しかった!バーフバリ鑑賞会楽しかった
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月31日
4月
Android Dagashiデビューしてテンションが上がっていたAndroid Dagashi デビューしてるぞわーい!!!! thagikuraさんがツイートしてくれてるのdagashiのコメントみて初めて知った…!!🙏🙏🙏🙏 https://t.co/pd0fng1nT3
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月2日
本日はこちらのトークセッションに参加しますー٩( 'ω' )وhttps://t.co/4lWclIplmm
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月7日
Women Techmakers Tokyo 2018 楽しかったー!!一番印象的だったのはAndroid Thingsのチュータで参加した @futabooo が「普段女性が勉強会に参加するとこんな感じなんですね」と言っていたこと。 #wtm18 #WTMTokyo
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月7日
IWDのトークセッションに参加した。 kinukoさんkeynoteが素晴らしすぎて、この資料は定期的に見返したい
#wtm18 #WTMTokyo 先ほどのスライドです。あまり普段話さない内容で緊張しました😀 https://t.co/Cq8tIgSeH9 技術者としてキャリアを築くには / 大きなプロジェクトを回すには
— Kinuko Yasuda (@kinu) 2018年4月7日
Flutter勉強会やった来週Flutterの勉強会やります!15分LT枠が空いているのでこれを機にFlutterやってみるぜ!!!って方!!!ぜひ!!!!!https://t.co/it29jmvUzr
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月9日
勉強会の前に自分でも触ってみるか〜と思って試したらこのザマFlutterのproject作る時に作成場所ミスったら既存アプリのコードが全部吹っ飛んだ時の顔してる
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月10日
築地市場が移転する前に早朝のツアーに行ってみた。マグロのせりの見学、何言っているか全く聞き取れなかった
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月13日
I can't understand what they yelling at tuna auction even though I am Japanese! 😂 pic.twitter.com/4rtl6Ee7jI
ツアーは基本的に英語だし参加者もほとんど外国人でここどこだっけ?ってなった築地のせり見学ツアー待ち。ここでは英語がスタンダードっぽい
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月13日
Now I came Tsukiji fish market and waiting tour started. English seems like standard language here.
優勝🍣🍣🍣🍣 pic.twitter.com/kMZM4GHpQf
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月13日
最高かよ~~~ pic.twitter.com/uHtW7tLK1I
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月14日
神田明神の狛犬はハンサム pic.twitter.com/IjLpk2uEbL
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月14日
この日は築地だけじゃなく色々日本っぽいところを巡るデートをしていた明るいうちから蕎麦屋で呑むという実績を解除しました pic.twitter.com/QPehL957jH
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月14日
5月
福岡のうどん食べても食べても減らなくてびっくりした pic.twitter.com/NbBjhT7Wn3
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年5月18日
出張で福岡に来た今日はこれに参加するために福岡に来ています。福岡の皆様ハロー!https://t.co/07bm9JFZ2I
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年5月18日
移動中の飛行機のお供で読んだOKR本よかった!
OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法
- 作者: クリスティーナ・ウォドキー,及川卓也(解説),二木夢子
- 出版社/メーカー: 日経BP社
- 発売日: 2018/03/15
- メディア: 単行本
- この商品を含むブログ (1件) を見る
今日はこれで喋ります〜 #love_kotlin https://t.co/fWqdXL3QbX
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年5月30日
Kotlin愛好会で登壇した
今からこの話します!Tシャツも最高に可愛いKotlin Tシャツなので見てくれ!! #love_kotlin https://t.co/IyzQwzKWqO
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年5月30日
6月
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月3日
白山パパ @fushiroyama と今井さん @tomoaki_imai とスシロール pic.twitter.com/nsdVQgNY8D
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月6日
出張でアメリカのオフィスに一週間ほど行った。
生まれて初めて日本以外のオフィスで働いているんですけど、みんなすごく頻繁に"How’s it going?" とか'What's up?"とか一日に何度も声かけまくっていて、これが通常だと感じていると日本の平均的な労働環境はさみしいだろうなぁと思った。来週日本に戻ったらたくさん声かけよう。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月5日
なんか誤解を招いているかもなので補足すると、単純に異なるcontext(或いはprotocol)なだけでどちらがいいという話ではなくて、今回私が新たなcontextについて学ぶことができたので、そっちのcontextで過ごしてきた人に対してそのcontextで接してみようというただそれだけの話です。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月5日
海外オフィスでの雑な感想が今までで一番伸びた
一昨日Uberに置き忘れたiPhoneを昨晩無事回収した。あとでブログにまとめるけど、SIMは通話付きのやつ買っておいて本当に良かったと思った…
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月8日
久しぶりに新卒で入った会社の人たちと飲んでいた。普段エンジニアの中でもweb系の本当に限られた人としかあってなかったんだなーと実感した新卒で入った会社の上司と先輩達と数年ぶりに飲んでめっちゃ楽しかった☺️☺️☺️
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月29日
普段Web系の会社の人としか接する機会がないのだけれど、例えばエンジニアと言っても諸々違う文化で回っている社会が存在しているのだと改めて実感するいい機会だった。
おまけ
夫シリーズ
今日の夢は夫と結婚してない世界で、お別れしたけど、顔もぼんやりとしか思い出せない、目が覚めて隣に夫がいて本当よかった。そういう世界線も存在していた可能性はあるのだ。とても怖かった。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月20日
飲みに行けなかったのでスーパーで買い物して帰って来た。凍えて帰ってくる旦那さまのためにグラタン作って待つのだ。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年1月22日
夫がDroid君のことを「Droidマン」って呼ぶんだけど、急に可愛くなくなるので本当にやめて欲しい
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月7日
激務時代の名残でごはんサボるときがたまにあるせいか、食事をとるだけで誉められるくらい夫に甘やかされていますが、今日はとうとうお茶を飲んでいるだけで誉められました。
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年2月10日
夫と朝食を食べながら金曜日だね~という話をしていたら「ひさしぶりに1週間働いたね!」って言ってもらえて、平日普通に働いただけで労いの言葉をくれるなんてやっぱり天使なのかもしれない😇😇😇
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年3月8日
夫に「指太くなった?」って聞かれたのでもうダメです
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年4月15日
Android Frameworkのコードにbreakpointを止めるメモ
Android開発をしていると、Frameworkがどういう挙動をしているか調べたくなる時がある。そういう時はFrameworkのコードにbreakpointを置くんだけど、止まってくれたり止まらなかったりすることがあるので困っていた。
完全に理解した!と言いたいけど、ViewRootImplの中でbreak pointおいても止まらないし、ログ埋め込むわけにもいかないので多分あってるくらいなのが歯がゆい…Ubuntu環境があればログ追加したfreamwork.jar作って挙動確かめたい🙄組み込み系の人、誰か気軽にできるなら代わりにやってほしい笑
— むーむー/Atsuko FUKUI (@muumuumuumuu) July 15, 2018
このTweetに対して神リプライがついたので流れないようにメモしておく。
完全に横からであれですけど、昔フレームワーク側コードにブレークポイント置いて止めたりwatchしてた記憶があるので(変わってなければ)おそらくできるはず?実機だとフレームワークのコードに手が入ってて行数がズレてて止まらないとかはありますが、エミュレータはいけるかと
— Yuki Fujisaki / tnj (@tnj) July 15, 2018
いくつかやり方がありますね。👀動かしている端末にコンパイルSDKをあわせれば止まります。あとはメソッドの宣言のところに貼れば止まるのと、どこかのサイトで動かしているAndroid OSのバージョンのソースコードを確認して、その行数と同じところに貼れば止まると言う感じでやっていますね
— takahirom (@new_runnable) July 15, 2018
実際試したところ、Compile SDK versionと合わせたEmulatorを作ってそこで動かすというのが良さそう。
StateListAnimatorを使ってXMLだけでAnimationをつける
こちらの記事を読んで「<selector>
の中にobject animator埋め込めるの知らなかった!!すげー!!!!」となったので遊んでみたメモ。
StateListAnimator
AndroidにはStateListAnimator
というクラスがあって、Viewのdrawable stateによってAnimationを書き分けることができる。何が最高かってこのAnimationはXMLでお手軽にかけるってところだ。1
ドキュメントはこちら
遊んでみた
アイコンに触っている間だけ大きくなるアニメーションを書いてみた。 xmlはこんな感じ
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <set> <objectAnimator android:duration="200" android:propertyName="scaleX" android:valueTo="1.5" /> <objectAnimator android:duration="200" android:propertyName="scaleY" android:valueTo="1.5" /> <objectAnimator android:duration="200" android:propertyName="transitionZ" android:valueTo="10dp" /> </set> </item> <item> <set> <objectAnimator android:duration="200" android:propertyName="scaleX" android:valueTo="1" /> <objectAnimator android:duration="200" android:propertyName="scaleY" android:valueTo="1" /> <objectAnimator android:duration="200" android:propertyName="transitionZ" android:valueTo="0dp" /> </set> </item> </selector>
android:state_pressed="true"
の時とそうでない場合でObjectAnimatorの振る舞いを変えることができる。上記公式ドキュメントのリンク先を参照するとpress以外のstateもたくさんある。
xmlのobjectAnimator
タグで使えるattribute一覧はこちら
Animation resources | Android Developers
<objectAnimator android:propertyName="string" android:duration="int" android:valueFrom="float | int | color" android:valueTo="float | int | color" android:startOffset="int" android:repeatCount="int" android:repeatMode=["repeat" | "reverse"] android:valueType=["intType" | "floatType"]/>
このanimationとviewを紐づける時はこんな感じでandroid:stateListAnimato
を使う。
android:stateListAnimator="@animator/fav_animator"
こんな感じでハートがドキドキする。
サンプルコードはこちら。
KotlinのRegexとDestructuredで文字列からdata classに変換する
Androidでアプリを書いていて、正規表現を扱うときにjava.util.regex.Matcher
を使うこと多いが、あれは個人的には好きではない。もっとスッキリかけるんじゃないかなぁといつも思ってしまう。
例えば、URLからprotocolとdomainを正規表現を使って取り出すコードを書こうと思うとこんな感じになるかと思う。
companion object { private const val REGEX: String = "(.*)://(.*)" } fun hoge() { val urlString = "http://www.com" val url = generateUrlFromString(urlString) } private fun generateUrlFromString(url: String): Url? { val matcher = Pattern.compile(REGEX).matcher(url) return if (matcher.find()) { val protocol = matcher.group(1) val domain = matcher.group(2) return Url(protocol, domain) } else { null } } data class Url(val protocol: String, val domain: String)
再帰の場合に注意が必要で、上記の場合だとmatcher.group()
が0ではなく1と2だったり何かとやらかしてしまったりする。(matcher.group(0)
にはhttp://www.com
が入ってくる)
そんなときに、この記事見て最高では????と思ったので自分でも試してみることにした。一行でまとめると Destructured
クラスが最高では?という話です。やっぱりKotlinは可愛い。
上記のgenerateUrlFromString()
をJavaの正規表現からKotlinの正規表現に書き換えたコードがこちら。
private fun generateUrlFromString(url: String): Url? = REGEX.toRegex().matchEntire(url) ?.destructured ?.let { (protocol, domain) -> Url(protocol, domain) }
これだけで十分可愛さが伝わる気がするが、蛇足ながら可愛いポイントを書いていく。
- 文字列REGEXをtoRegex() でRegexクラスに変換
- matchEntire()で引数url stringを渡すことによりMatchResultを取得
- 2で正規表現にマッチしなかった場合はnullになるので、
?.
でdestructuredを取得。これは正規表現にマッチしたgroupをDestructured
クラスで返してくれる - 3ですでにnullの可能性があるので引き続き
?.
でletを呼ぶ。ポイントはここでラベルをつけることができるという点。サンプルコードだとprotocol
とdomain
をすぐに使っているが、let blockのなかで複雑なことをしている場合にわざわざ名前をつけるために変数に代入しなくてもよくて可読性がグッと上がる。
これらをワインラインでスッキリかけるところがまた可愛い!
当然ながらKotlinのRegexもJavaのMatcherとかをwrapしているので、めんどくさいところは全てやってくれていて最高。
言いたいことは以上です。
kotlin.concurrent.threadは新規threadが作成されるけどcurrent threadのgroupで動く
[2018.7.15 ご指摘いただき記事の最後に追記しました]
あらまし
先日おもしろいTweetを見つけたのでちょっと調べてみたメモ。
別ActivityでUI操作の必要なマルチスレッド処理をしたいとき、runOnUiThreadを使わないとMarshmallowならクラッシュするけど、Oreoだとthreadだけで動作するという謎知見を得た
— omega (@equal_001) 2018年7月13日
残念ながら手元で上記の現象は再現しなくて、MashmallowでもCrashが発生しなかったんだけど、
Kotlinのthreadってmain threadじゃない別threadで動くんじゃなかったっけ…?🤔
と思ったので調査。
ちなみにMashmallowの端末が手元になかったのでEmulator環境でやってます。
まずは調査用の簡単なコードを準備。Activityの onResume()
でUI操作を行う。(textという名前のTextViewに"hogehoge"というtextをセットしています。)
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_thread) Timber.plant(Timber.DebugTree()) // main threadのログ出力結果を確認するためにログを入れる Timber.d("main thread: ${Looper.getMainLooper().thread}") } override fun onResume() { super.onResume() thread { Timber.d("current thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ThreadのtoString()
で表示しているのは下記の三つ。 thread name
と priority
と group name
public String toString() { ThreadGroup group = getThreadGroup(); if (group != null) { return "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]"; } else { return "Thread[" + getName() + "," + getPriority() + "," + "" + "]"; } }
ログとるときの操作は、単純に画面を表示させるだけ。結果はこんな感じ。
07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main]
thread blockの中でログを出力した方は確かにthread nameを見ると別のthreadになっている。しかし3つ目のgroup nameはmainになっている。
kotlin.concurrent.threadを読んでみよう
Kotlinのthreadは気軽にサンプルコードのように書いている人が多いと思うが、じつはたくさんのdefault引数が用意されている
public fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit): Thread { val thread = object : Thread() { public override fun run() { block() } } if (isDaemon) thread.isDaemon = true if (priority > 0) thread.priority = priority if (name != null) thread.name = name if (contextClassLoader != null) thread.contextClassLoader = contextClassLoader if (start) thread.start() return thread }
で、そんなdefault引数を置いておいて、メソッドの頭でJavaのjava.lang.Thread
をnewしているんだけど、このときcurrent threadをparentにとるようになっているので、main threadからthread()
を呼び出すとmain threadのgroupで新規threadが作られるのだ。
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { Thread parent = currentThread(); if (g == null) { g = parent.getThreadGroup(); } g.addUnstarted(); this.group = g;
UI操作できるのは一つのthreadからだけ
ちなみに上記のサンプルコードで、一度backgroundに行って再びonResume()
に帰ってくるとアプリがcrashする。
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
backgroundから復帰した時にonResume()
でもう一度threadから新規threadを作成しているので最初にさわったthreadではない別のthreadからUIを触ろうとしてcrashする。
ログを見てもthread nameが変わっていることがわかる
// Activity起動 07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main] // Backgroundから復帰(2回目のonResume) 07-14 08:31:06.983 5497-5518/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[Thread-269,5,main]
ここでthreadを新規作成するのではなくmain threadに固定することでこのcrashは避けられる。
override fun onResume() { super.onResume() // この部分を // thread { // Timber.d("current thread: ${Thread.currentThread()}") // text.text = "hogehoge" // } // こう変える runOnUiThread { Timber.d("current thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ログを見たら全てmain threadで固定されていることがわかる。
// Activity起動 07-14 08:07:18.328 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:07:18.329 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main] // Backgroundから復帰(2回目のonResume) 07-14 08:07:27.811 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main]
ちなみにもう一つおもしろい挙動を見つけたのでメモ。 こんな感じでthreadとrunOnUiThread両方書いてやるとCrashは発生しない。
override fun onResume() { super.onResume() thread { Timber.d("current thread from thread: ${Thread.currentThread()}") text.text = "hogehoge" } runOnUiThread { Timber.d("current thread from ui thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ログを見ると別threadよりも先に runOnUiThread
の中が実行されているのがわかる。先にmain threadでさわっているので以降main threadのgroupで動いている別threadでさわってもcrashしないということなのかな。 // この部分間違ってそうなので追記で捕捉しました(2018.7.15)
// 1回目のonResume() 07-14 08:40:11.749 5693-5693/? D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main] 07-14 08:40:11.749 5693-5709/? D/ThreadActivity$onResume: current threa from thread: Thread[Thread-278,5,main] // 2回目のonResume() 07-14 08:40:49.488 5693-5693/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main] 07-14 08:40:49.489 5693-5717/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current threa from thread: Thread[Thread-280,5,main]
[2018.7.15ここから追記]
ブログ公開ごにhydrakecatさんにご指摘いただいたのでさらに追加調査。
自分もちょっと調べただけなのですが、たぶんこのサンプルだとたまたまうまくいっているだけな気がします。thread { } の中で Thread.sleep(1000) とか呼んでみるとクラッシュすると思います。見当違いなことを言っていたらすみません m(_ _)m
— Hiroshi Kurokawa (@hydrakecat) July 14, 2018
確かに thread { } の中で Thread.sleep(1000)を入れるとcrashするようになった。 (emulator環境Mashmallow / 実機環境Oreo共にcrashすることを確認 )
override fun onResume() { super.onResume() thread { text.text = "hogehoge" // sleep以前にtextをさわってもcrashしないが、 Thread.sleep(1000) // 1000msecの遅延後 text.text = "hogehoge" // textをさわるとcrashする }
例外を投げているのはViewRootImpl.java
のこの部分。
Cross Reference: /frameworks/base/core/java/android/view/ViewRootImpl.java
7311 void checkThread() { 7312 if (mThread != Thread.currentThread()) { 7313 throw new CalledFromWrongThreadException( 7314 "Only the original thread that created a view hierarchy can touch its views."); 7315 } 7316 }
mThread
に何が入るのかとういうと、constructorでThread.currentThread()
を代入している。mThread
はfinalなのでsleep前後でも変わらない。
235 final Thread mThread; 478 public ViewRootImpl(Context context, Display display) { 479 mContext = context; 480 mWindowSession = WindowManagerGlobal.getWindowSession(); 481 mDisplay = display; 482 mBasePackageName = context.getBasePackageName(); 483 mThread = Thread.currentThread();
mThread
も Thread.currentThread()
もsleep前後で変化しないはずなのでおかしいなぁと思っていたら
たぶんですが、親Viewがレイアウト中だとうまく無視されるんじゃないかと思っています。そしてTextViewにテキストを反映するときにうまく変更後の文字列を拾ってくれるんじゃないかと推測しています。mainスレッドで触った直後に別スレッドで触ったらクラッシュしなかったのもたぶん同じ理由かとー。
— Hiroshi Kurokawa (@hydrakecat) July 15, 2018
というわけでcheckThread()
が呼ばれない可能性を考える。
ViewRootImpl
はmHandlingLayoutInLayoutRequest
というフラグを持っていて、Layout実行時にこのフラグがtrueになる。
1158 @Override 1159 public void requestLayout() { 1160 if (!mHandlingLayoutInLayoutRequest) { 1161 checkThread(); 1162 mLayoutRequested = true; 1163 scheduleTraversals(); 1164 } 1165 }
で、requestLayout()
にこのフラグが立っていたら checkThread()
が実行されない(というか何もしない)ので、threadのcheckも行われず例外も投げられないということのようだ。sleep後はlayoutが完了してフラグがfalseになっているのでthreadのcheckが行われて例外が発生している。
runOnUiThread()
と thread
両方書いたケースでも、runOnUiThread()
の方でlayout要求が出ているのでmHandlingLayoutInLayoutRequest
がtrueになり、そのタイミングでthread
でUIをさわったのでcheckThread()
が呼ばれず例外が発生しなかっただけだと思われる。
ViewRootImpl
はAndroid FrameworkのクラスなのでDebuggerが効かず本当にそうか?というのが確認できないので歯がゆいが、多分あってるんじゃ無いかなぁ。多分。
完全に理解した!と言いたいけど、ViewRootImplの中でbreak pointおいても止まらないし、ログ埋め込むわけにもいかないので多分あってるくらいなのが歯がゆい…Ubuntu環境があればログ追加したfreamwork.jar作って挙動確かめたい🙄組み込み系の人、誰か気軽にできるなら代わりにやってほしい笑
— むーむー/Atsuko FUKUI (@muumuumuumuu) July 15, 2018
[2018.7.15 追記ここまで]
RxTextView#textChanges()でdistinctUntilChangedする時にeventが流れない時がある
みんな大好きRxBinding1 ですが、ちょっとハマることがあったのでメモ。
RxTextView#textChanges()
で EditText
の入力イベントを監視する。その時に重複した入力をdistinctUntilChanged()
で削ろうとしたら以降何もeventが流れない現象に遭遇した。
RxTextView.textChanges(editText) .distinctUntilChanged() // ここでevnetが止められるから .observeOn(AndroidSchedulers.mainThread()) .subscribe { Log.d("SampleApp", it.toString()) // ここは実行されない }
textChanges()
は内部ではTextWatcher
を使っていて、これがTextの変更を検出するとCharSequence
を返してくれるのだが、こいつはmutableで毎度同じinstanceを返すのだ。同じinstanceなのでchangeしたと見なされずいつまでたっても次のeventが流れてこない。
なので、CharSequence
からStringを取り出してやれば想定していた挙動になる。
RxTextView.textChanges(editText) .map { it.toString() } // ここでStringに変換すると .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .subscribe { Log.d("SampleApp", it.toString()) // ここが実行される }
そもそもなんで RxTextView#textChanges()
で入力の重複を防ぎたかったかというと、機種依存でSoftware Keyboardのsearch button
をtapした時にtextChanges
のeventがemitされちゃう場合があって、これを防ぎたかったから。これはRxTextView
というよりは、内部で使われているTextWatcher
でeventが発火しているからなんだけど、keyboardの機種依存周りつらすぎてなんか仕様とか作って統一して欲しい…