RecyclerView in RecyclerViewだとAppBarLayoutがスクロールしないことがある
Nested Scroll(RecyclerView in RecyclerView)とAppBarの組み合わせでうまくいかないことがあったのでメモします。
前提
RecyclerViewが入れ子になってNested Scrollする画面で、
- Parent
RecyclerView
はvertical scroll - Child
RecyclerView
はhorizontal scroll
といった挙動をします。
ParentのRecyclerView
は CoordinatorLayout
を親に持っていて、AppBarLayout
がParentのRecyclerView
のスクロールに応じてアニメーションするといった挙動を想定しています。
問題
ParentのRecyclerView
をスクロールさせてもAppBarLayoutは変化しません。
(水色のViewがAppBarLayoutに包まれています。)
(同じParentにスクロールしないchildを入れてやり、その上からスクロールを開始するとAppBarはちゃんと正常にアニメーションします。)
直し方
Child RecyclerView
の NestedScrollingEnabled()
にfalseを設定してやればうまくいきます。
コードからだとこんな感じ
childRecyclerView.isNestedScrollingEnabled = false
xmlからも指定できます
<android.support.v7.widget.RecyclerView ...(省略)... android:nestedScrollingEnabled="false"
上記のようにChild `RecyclerViewがnested scrollしないことを明示的に指定するだけでAppBarがアニメーションするようになります。
何が起こったか?
setNestedScrollingEnabled()
を設定することによってどこが変わるかというと、NestedScrollingChildHelper#dispatchNestedPreScroll()
がfalseを返すようになります。
RecyclerViewのdispatchNestedPreScroll()
を呼んだ先でNestedScrollingChildHelper#dispatchNestedPreScroll()
-> ViewParentCompat.onNestedPreScroll()
-> CoordinatorLayout#onNestedPreScroll()
-> AppBarLayout.BaseBehavior#onNestedPreScroll()
と伝播していきます。
Scrollしない時はAppBarLayoutの onNestedPreScroll()
のdyの値が正常に渡って来ません。
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) { if (this.isNestedScrollingEnabled()) { ViewParentCompat.onNestedPreScroll(parent, this.mView, dx, dy, consumed, type); } return false; }
ここでisNestedScrollingEnabled()
がfalseを返さないと本来子のRecyclerViewはVerticalなscrollをしないはずなのにonNestedPreScroll()
のdyの値が変なまま伝播していってしまってうまくAppBarLayoutがアニメーションできないのかな?と予想しています。(RecyclerViewが巨大すぎて細かい部分まで追えていないのでもし違っていたらコメントで指摘してもらえると助かります。)
AppBarLayout.BaseBehavior#onNestedPreScroll()
でscroll()
が呼ばれます。
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dx, int dy, int[] consumed, int type) { if (dy != 0) { int min; int max; if (dy < 0) { min = -child.getTotalScrollRange(); max = min + child.getDownNestedPreScrollRange(); } else { min = -child.getUpNestedPreScrollRange(); max = 0; } if (min != max) { consumed[1] = this.scroll(coordinatorLayout, child, dy, min, max); this.stopNestedScrollIfNeeded(dy, child, target, type); } } }
この先HeaderBehavior#scroll()
がsetHeaderTopBottomOffset()
などを呼んで最終的にView#offsetTopAndBottom()
が呼ばれます。
ここで不正に渡って来たdyの値によって、スクロールしていなかったことになっているんじゃないかなぁ。
ちなみにsetNestedScrollingEnabled()
はデフォルトでtrueを返すの?と思った人がいるかもしれませんが、このフラグはRecyclerViewのコンストラクタでtrueが設定されます。
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { boolean nestedScrollingEnabled = true; // defaultでtrue if (attrs != null) { if (VERSION.SDK_INT >= 21) { a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, defStyle, defStyleRes); nestedScrollingEnabled = a.getBoolean(0, true); // xmlで指定されていれば反映する a.recycle(); } this.setNestedScrollingEnabled(nestedScrollingEnabled); }
selectableItemBackgroundはBackgroundにセットしないと意図した挙動にならない話
タイトルが意味わからないことになっている😇
はじめに
AndroidはAPI Level 21 (Android 5.0 / Lollipop) からRipple Effectがサポートされ、これによりより良いタッチフィードバックをユーザに提供できることができる。ボタンなど、「もともとタップが想定されているView」については、タップした際にデフォルトでRipple Effectが表示されている。ただのViewだったりTextViewなど「デフォルトでタップが想定されていないView」についてもシステムが用意したdrawable resourceを設定することで簡単に実現できる。
システムが用意したdrawable resourceは(自分の知る限り)2種類ある。
これをViewのbackground/foregroundにセットするかでまた挙動が変わるという面白い現象を見つけたので調べてみた。
組み合わせでどう変わる?
まずは2種類のresourceの説明をそれぞれしていこう。Ripple Effectのcolorやdurationは変わらないのだが、selectableItemBackgroundBorderless
の方はその名の通りBorderless、つまりViewのboundaryを超えてRipple Effectを表示することができる。幅・高さが小さいViewに対してタッチフィードバックをつけるのに大変便利だ。ただし、ViewのBackgroundにセットした場合のみで、Foregroundにセットした時にはselectableItemBackground
と同じ挙動になる。
(上記のサンプルコードはリンクを参照)
さて、それぞれどうして差が出るのかコードを追っていこう。
それぞれ指定されたResourceは何をやっているのか?
Borderless
selectableItemBackgroundBorderless
を指定した場合、最終的にこのxmlが読み込まれる。
17 <ripple xmlns:android="http://schemas.android.com/apk/res/android" 18 android:color="?attr/colorControlHighlight" />
Borderlessじゃない方
selectableItemBackground
を指定した場合、最終的にこのxmlが読み込まれる。
17 <ripple xmlns:android="http://schemas.android.com/apk/res/android" 18 android:color="?attr/colorControlHighlight"> 19 <item android:id="@id/mask"> 20 <color android:color="@color/white" /> 21 </item> 22 </ripple>
これらのripple
タグは最終的にRippleDrawableに変換される。
BackgroundとForegroundで挙動が異なる
RippleDrawableクラスの公式ドキュメントを見ると下記のような記載がある。
If no child layers or mask is specified and the ripple is set as a View background, the ripple will be drawn atop the first available parent background within the View's hierarchy. In this case, the drawing region may extend outside of the Drawable bounds.
わざわざ "as a View background" と書いてあるように、backgroundに指定した場合のみViewのhierarchyを辿ってparentのbackground内まで描画することができるようだ。
せっかくなのでコードを読んでみよう
RippleDrawableはLayerDrawableをextendsしているのでlayerを重ねることができる。これでmaskをかけている。
189 public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content, 190 @Nullable Drawable mask) { 191 this(new RippleState(null, null, null), null); 201 if (mask != null) { 202 addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0); 203 }
このmastが影響してくるのはdraw()の時
688 @Override 689 public void draw(@NonNull Canvas canvas) { 690 pruneRipples(); 691 692 // Clip to the dirty bounds, which will be the drawable bounds if we 693 // have a mask or content and the ripple bounds if we're projecting. 694 final Rect bounds = getDirtyBounds(); 695 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 696 canvas.clipRect(bounds); 697 698 drawContent(canvas); 699 drawBackgroundAndRipples(canvas); 700 701 canvas.restoreToCount(saveCount); 702 }
getDirtyBounds() はmaskがある時とないときで挙動が変わる
922 @Override 923 public Rect getDirtyBounds() { 924 if (!isBounded()) { 925 final Rect drawingBounds = mDrawingBounds; 926 final Rect dirtyBounds = mDirtyBounds; 927 dirtyBounds.set(drawingBounds); 928 drawingBounds.setEmpty(); 929 930 final int cX = (int) mHotspotBounds.exactCenterX(); 931 final int cY = (int) mHotspotBounds.exactCenterY(); 932 final Rect rippleBounds = mTempRect; 933 934 final RippleForeground[] activeRipples = mExitingRipples; 935 final int N = mExitingRipplesCount; 936 for (int i = 0; i < N; i++) { 937 activeRipples[i].getBounds(rippleBounds); 938 rippleBounds.offset(cX, cY); 939 drawingBounds.union(rippleBounds); 940 } 941 942 final RippleBackground background = mBackground; 943 if (background != null) { 944 background.getBounds(rippleBounds); 945 rippleBounds.offset(cX, cY); 946 drawingBounds.union(rippleBounds); 947 } 948 949 dirtyBounds.union(drawingBounds); 950 dirtyBounds.union(super.getDirtyBounds()); 951 return dirtyBounds; 952 } else { 953 return getBounds(); 954 } 955 }
さて、ここまででselectableItemBackgroundBorderless
とselectableItemBackground
の違いがわかった。
次はBackgroundとForegroundで挙動が変わるところをみてみよう。
RippleDrawable#isProjected()というメソッドの中で、タップされた場所から円を描いて自分のサイズに収まるかどうかをみている。maskされたlayerが存在する場合もfalseを返している。
344 @Override 345 public boolean isProjected() { 346 // If the layer is bounded, then we don't need to project. 347 if (isBounded()) { 348 return false; 349 } 350 351 // Otherwise, if the maximum radius is contained entirely within the 352 // bounds then we don't need to project. This is sort of a hack to 353 // prevent check box ripples from being projected across the edges of 354 // scroll views. It does not impact rendering performance, and it can 355 // be removed once we have better handling of projection in scrollable 356 // views. 357 final int radius = mState.mMaxRadius; 358 final Rect drawableBounds = getBounds(); 359 final Rect hotspotBounds = mHotspotBounds; 360 if (radius != RADIUS_AUTO 361 && radius <= hotspotBounds.width() / 2 362 && radius <= hotspotBounds.height() / 2 363 && (drawableBounds.equals(hotspotBounds) 364 || drawableBounds.contains(hotspotBounds))) { 365 return false; 366 } 367 368 return true; 369 }
このメソッドが呼ばれるのはViewクラスのgetDrawableRenderNode().
19433 private RenderNode getDrawableRenderNode(Drawable drawable, RenderNode renderNode) { 19434 if (renderNode == null) { 19435 renderNode = RenderNode.create(drawable.getClass().getName(), this); 19436 } 19457 renderNode.setProjectBackwards(drawable.isProjected());
ここでsetしたProjectBackwardsが呼ばれるのはこの辺?(Nativeコード詳しくないマンなので間違ってたらごめんなさい)
101 void RenderNodeDrawable::forceDraw(SkCanvas* canvas) { 120 //pass this outline to the children that may clip backward projected nodes 121 displayList->mProjectedOutline = displayList->containsProjectionReceiver() 122 ? &properties.getOutline() : nullptr; 123 if (!properties.getProjectBackwards()) { 124 drawContent(canvas); 125 if (mProjectedDisplayList) { 126 acr.restore(); //draw projected children using parent matrix 127 LOG_ALWAYS_FATAL_IF(!mProjectedDisplayList->mProjectedOutline); 128 const bool shouldClip = mProjectedDisplayList->mProjectedOutline->getPath(); 129 SkAutoCanvasRestore acr2(canvas, shouldClip); 130 canvas->setMatrix(mProjectedDisplayList->mProjectedReceiverParentMatrix); 131 if (shouldClip) { 132 clipOutline(*mProjectedDisplayList->mProjectedOutline, canvas, nullptr); 133 } 134 drawBackwardsProjectedNodes(canvas, *mProjectedDisplayList); 135 } 136 } 137 displayList->mProjectedOutline = nullptr; 138 }
確かにProjectedされたoutlineをclipしているように見える。
で、話を戻してViewクラスのgetDrawableRenderNode()がいつ呼ばれるかというと、View#drawBackground()
からのみコールされている。
19375 private void drawBackground(Canvas canvas) { 19376 final Drawable background = mBackground; 19377 if (background == null) { 19378 return; 19379 } 19380 19381 setBackgroundBounds(); 19382 19383 // Attempt to use a display list if requested. 19384 if (canvas.isHardwareAccelerated() && mAttachInfo != null 19385 && mAttachInfo.mThreadedRenderer != null) { 19386 mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode); 19387 19388 final RenderNode renderNode = mBackgroundRenderNode; 19389 if (renderNode != null && renderNode.isValid()) { 19390 setBackgroundRenderNodeProperties(renderNode); 19391 ((DisplayListCanvas) canvas).drawRenderNode(renderNode); 19392 return; 19393 } 19394 }
というわけでbackgroundとforegruondで挙動に差分が出るのはここでした。解決!
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しているので、めんどくさいところは全てやってくれていて最高。
言いたいことは以上です。