言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

便利なTextInputLayoutとその内部実装

はじめに

Material Designにいつの間にか入っていたTextInputLayoutが便利なのでメモを残す。

TextInputLayout とは

公式ドキュメント によると、TextInputLayout とは「テキストを入力する時にhintを隠す代わりにfloating labelを表示するEditTextをラップしたview」らしい。 これだけ読んでも謎だと思うので、Google Code LabのMaterial Design Componentsのコースをgit cloneしてきて動かしてみるとわかりやすいです。 あとは最近WebのGoogle Login画面でこのcomponentが使われている気がする。

MDC-104 Android: Material Advanced Components (Kotlin)

なんかgifがうまくアップロードできなくてanimation部分が見えない…

f:id:muumuumuumuu:20190104182847g:plain:w200

ちなみに上記はstyleにWidget.MaterialComponents.TextInputLayout.OutlineBoxが設定されている。

どうやってhintを動かしているのか?

せっかくなのでこのhintのanimationがどのように実装されているのかコードを読んでみた。 ちなみに上記のCode Labからgit cloneしてきたプロジェクトだとTextInputLayout はAndroidXではなく com.android.support:support-v4:28.0.0-alpha3 だったので、これを読んでいく。

animationしているのは下記のメソッド。このメソッドの引数のfloatには、collapseの場合は1.0F, expandの時は0.0Fが渡される。

    @VisibleForTesting
    void animateToExpansionFraction(float target) {
        if (this.collapsingTextHelper.getExpansionFraction() != target) {
            if (this.animator == null) {
                this.animator = new ValueAnimator();
                this.animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                this.animator.setDuration(167L);
                this.animator.addUpdateListener(new AnimatorUpdateListener() {
                    public void onAnimationUpdate(ValueAnimator animator) {
                        TextInputLayout.this.collapsingTextHelper.setExpansionFraction((Float)animator.getAnimatedValue());
                    }
                });
            }

            this.animator.setFloatValues(new float[]{this.collapsingTextHelper.getExpansionFraction(), target});
            this.animator.start();
        }
    }

ValueAnimator を使ってanimationしている。Interpolatorを設定して167Lのdurationをセット。(なんで167に決まったんだろう?)
で現在のexpansion fractionをHelperクラスからとってきて、そこから引数のtargetまでをvalueの変更幅に指定する。

start()を読んでやるとlistenerに設定したcallback methodが呼ばれる。

実際にviewをanimationするのはまたもやhelperクラスの方に移譲している。

    public void setExpansionFraction(float fraction) {
        fraction = MathUtils.clamp(fraction, 0.0F, 1.0F);
        if (fraction != this.expandedFraction) {
            this.expandedFraction = fraction;
            this.calculateCurrentOffsets();
        }

    }
    private void calculateOffsets(float fraction) {
        this.interpolateBounds(fraction);
        this.currentDrawX = lerp(this.expandedDrawX, this.collapsedDrawX, fraction, this.positionInterpolator);
        this.currentDrawY = lerp(this.expandedDrawY, this.collapsedDrawY, fraction, this.positionInterpolator);
        this.setInterpolatedTextSize(lerp(this.expandedTextSize, this.collapsedTextSize, fraction, this.textSizeInterpolator));
        if (this.collapsedTextColor != this.expandedTextColor) {
            this.textPaint.setColor(blendColors(this.getCurrentExpandedTextColor(), this.getCurrentCollapsedTextColor(), fraction));
        } else {
            this.textPaint.setColor(this.getCurrentCollapsedTextColor());
        }

        this.textPaint.setShadowLayer(lerp(this.expandedShadowRadius, this.collapsedShadowRadius, fraction, (TimeInterpolator)null), lerp(this.expandedShadowDx, this.collapsedShadowDx, fraction, (TimeInterpolator)null), lerp(this.expandedShadowDy, this.collapsedShadowDy, fraction, (TimeInterpolator)null), blendColors(this.expandedShadowColor, this.collapsedShadowColor, fraction));
        ViewCompat.postInvalidateOnAnimation(this.view);
    }

currentDrawXcurrentDrawYそのほか諸々の計算を済ませておいてpostInvalidateOnAnimation()をコールして描画し直す時にその値を使う感じっぽい。 ViewCompat.postInvalidateOnAnimation() を辿っていくとView#invalidate() が呼ばれていることがわかる。

postInvalidateOnAnimation() の引数にはTextInputLayoutが渡されているので、このクラスのdraw()をみてみるとhelperクラスのdraw()が呼ばれている。

    public void draw(Canvas canvas) {
        if (this.boxBackground != null) {
            this.boxBackground.draw(canvas);
        }

        super.draw(canvas);
        if (this.hintEnabled) {
            this.collapsingTextHelper.draw(canvas);
        }

    }

この中でcalculateOffsets() で計算したx,yの値やsetInterpolatedTextSize()の内部で計算・設定されているscaleを使ってviewを動かしている。 動かすのはcanvasに対してscale()とかdrawBitmap()/drawText()とかをコールして実装している。
(あまり関係ないけどvar10000とかvar7の変数はなんのために作られたのかわからない…)

    public void draw(Canvas canvas) {
        int saveCount = canvas.save();
        if (this.textToDraw != null && this.drawTitle) {
            float x = this.currentDrawX;
            float y = this.currentDrawY;
            boolean drawTexture = this.useTexture && this.expandedTitleTexture != null;
            float ascent;
            if (drawTexture) {
                ascent = this.textureAscent * this.scale;
                float var10000 = this.textureDescent * this.scale;
            } else {
                ascent = this.textPaint.ascent() * this.scale;
                float var7 = this.textPaint.descent() * this.scale;
            }

            if (drawTexture) {
                y += ascent;
            }

            if (this.scale != 1.0F) {
                canvas.scale(this.scale, this.scale, x, y);
            }

            if (drawTexture) {
                canvas.drawBitmap(this.expandedTitleTexture, x, y, this.texturePaint);
            } else {
                canvas.drawText(this.textToDraw, 0, this.textToDraw.length(), x, y, this.textPaint);
            }
        }

        canvas.restoreToCount(saveCount);
    }

アニメーションが早すぎて見えないけどTextColorもアニメーションに合わせて徐々に変わるように設定されていて細かい。すごい。でも見えない。

    private static int blendColors(int color1, int color2, float ratio) {
        float inverseRatio = 1.0F - ratio;
        float a = (float)Color.alpha(color1) * inverseRatio + (float)Color.alpha(color2) * ratio;
        float r = (float)Color.red(color1) * inverseRatio + (float)Color.red(color2) * ratio;
        float g = (float)Color.green(color1) * inverseRatio + (float)Color.green(color2) * ratio;
        float b = (float)Color.blue(color1) * inverseRatio + (float)Color.blue(color2) * ratio;
        return Color.argb((int)a, (int)r, (int)g, (int)b);
    }

2018年後半を振り返る

はじめに

2018年後半に自分が何をしていたか後から振り返られるようにメモを残しておく。前回のまとめはこちら。

muumuutech.hatenablog.com

7月

今年はスパイス系の料理にハマっていた。特に六本木ヒルズ内のインド料理屋さんのビリヤニ美味しいのでたくさん食べた気がする。

Go Bold Dayというチームのハッカソンに参加して3位入賞した。ご褒美に高級すき焼きランチ美味しかった!

この頃はクライアント側で軽量に動くOCRに興味があったんだけど、なかなかいいやつに出会えない…クレカの文字認識むずかしすぎでは????

これ今年なんども言っている気がする。twitterとかblogとかでめっちゃ勉強になること教えてくれる人に投げ銭送りたいのでみんなtwitterのbioになんかそれ系サービスのリンク貼っててほしいお気持ち。

エウレカさんのもくもく会に参加した。

もくもく会、最近主催も参加もできてないので来年はもうちょっとやりたい。

Clova開発の中の人に「SDKはKotlin対応しないの?」って聞いてしばらくレスがなかった後に突然Kotlin対応がリリースされたのめっちゃいい話だった。

8月

大学時代の友人と会っていた。みんなママになってた!

バリウムデビューした。みんなこんな怖い思いするのか…?検査終わった後に渡される下剤の量を自分で調節しろとか正気か…?など色々学びがあった。世界は思ったよりも運用でカバーされている。

会社でKotlinの可愛さを布教しようとして失敗。(Kotlinの良さは理解してもらったのでよしとする)

社内LT大会を開催した。楽しかった!

9月

イヤリングを片方だけなくすのをたくさん繰り返した一年だった。最近は落とす前提で、落とした時に気がつけるように大きなイヤリングを買う作戦に切り替えている。

DroidKaigiのCfP募集が開始。今年はスタッフとして参加するので応募は見送りました。

30歳になりました。

10月

自社のTech Confで登壇した。USチームとしてブースも出していました。

技術書店で売り子をしました。

本当にルー大柴化が止まらない。英語勉強中の私と日本語勉強中の同僚と日々会話すると英語と日本語が混ざってくるのでマジでルー大柴みたいになる。

秋田の友人の結婚式に出席するため初めて東北新幹線に乗った。東海道新幹線のつもりで言ったら死ぬやつだった。

社の健康診断のオプションが充実いていたおかげで病気の可能性を早めに検出することができた。みなさんちゃんと検診受けましょう。

11月

今年の個人的ベストレストランは多分ここ。

あまりにダメダメだったのでオンライン朝もくもく会をはじめることにした。(この記事ももくもく会で書いています。) 1ヶ月以上ちゃんと続いているので習慣化したと言っても良いでしょう。 参加したい!って人はTwitterでDMください。

12月

前職の人たちとお疲れ様でした会を兼ねた忘年会。ずっと行きたかったお店に行けて最高だった。

来年に向けて

たくさん溜まっている下書きちゃんと仕上げてたくさん出していくぞ!

はい。

いい加減Pixel 3買うぞ!

温泉でゴロゴロするぞ

たまたまやってみたけど、これは積読消化のいいモチベーションになった。積読消化していくぞ!!

夫シリーズ

RecyclerView in RecyclerViewだとAppBarLayoutがスクロールしないことがある

Nested Scroll(RecyclerView in RecyclerView)とAppBarの組み合わせでうまくいかないことがあったのでメモします。

前提

RecyclerViewが入れ子になってNested Scrollする画面で、

  • Parent RecyclerViewはvertical scroll
  • Child RecyclerViewはhorizontal scroll

といった挙動をします。

ParentのRecyclerViewCoordinatorLayoutを親に持っていて、AppBarLayoutがParentのRecyclerViewのスクロールに応じてアニメーションするといった挙動を想定しています。

問題

ParentのRecyclerViewをスクロールさせてもAppBarLayoutは変化しません。 (水色のViewがAppBarLayoutに包まれています。)

f:id:muumuumuumuu:20181204090257g:plain:w200

(同じParentにスクロールしないchildを入れてやり、その上からスクロールを開始するとAppBarはちゃんと正常にアニメーションします。)

直し方

Child RecyclerViewNestedScrollingEnabled() にfalseを設定してやればうまくいきます。

コードからだとこんな感じ

childRecyclerView.isNestedScrollingEnabled = false

xmlからも指定できます

<android.support.v7.widget.RecyclerView 
    ...(省略)...
    android:nestedScrollingEnabled="false"

上記のようにChild `RecyclerViewがnested scrollしないことを明示的に指定するだけでAppBarがアニメーションするようになります。

f:id:muumuumuumuu:20181204091114g:plain:w200

何が起こったか?

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にセットしないと意図した挙動にならない話

タイトルが意味わからないことになっている😇

はじめに

AndroidAPI Level 21 (Android 5.0 / Lollipop) からRipple Effectがサポートされ、これによりより良いタッチフィードバックをユーザに提供できることができる。ボタンなど、「もともとタップが想定されているView」については、タップした際にデフォルトでRipple Effectが表示されている。ただのViewだったりTextViewなど「デフォルトでタップが想定されていないView」についてもシステムが用意したdrawable resourceを設定することで簡単に実現できる。

システムが用意したdrawable resourceは(自分の知る限り)2種類ある。

  • ?android:attr/selectableItemBackgroundBorderless
  • ?android:attr/selectableItemBackground

これをViewのbackground/foregroundにセットするかでまた挙動が変わるという面白い現象を見つけたので調べてみた。

組み合わせでどう変わる?

まずは2種類のresourceの説明をそれぞれしていこう。Ripple Effectのcolorやdurationは変わらないのだが、selectableItemBackgroundBorderlessの方はその名の通りBorderless、つまりViewのboundaryを超えてRipple Effectを表示することができる。幅・高さが小さいViewに対してタッチフィードバックをつけるのに大変便利だ。ただし、ViewのBackgroundにセットした場合のみで、Foregroundにセットした時にはselectableItemBackgroundと同じ挙動になる。

f:id:muumuumuumuu:20181014155223g:plain:w300

(上記のサンプルコードはリンクを参照)
さて、それぞれどうして差が出るのかコードを追っていこう。

それぞれ指定された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     }

さて、ここまででselectableItemBackgroundBorderlessselectableItemBackgroundの違いがわかった。 次は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でやってみる。

できた

いきなりだけど成果物

f:id:muumuumuumuu:20180909122945g:plain

コードはこの辺

github.com

やったこと

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年は半分に区切っていく。

前回のまとめはこちら

muumuutech.hatenablog.com

1月

この時点で割と仕事に余裕があるときは隙あらば有休消化に励んでいた気がする。

普段アニメをあまりみないけどずっと気になっていた廻るピングドラムを見始めた。最初はサイコホラーなのかサスペンスなのかシュールコメディなのかわからなかったけど後半一気に止まらない感じがすごかった。
社のslackに「生存戦略」のスタンプがあってなんだろうと思っていたけど元ネタがわかってスッキリ。

長らく続けていたAndroidもくもく会、自分の退職に伴い最終回を迎えた。運営している自分もとっても楽しかったし本当にありがとうございました!

今年は(まだ半分しか終わってないけど)これと同じ内容5回くらいつぶやいている気がする。今でも絶対にフォローしているつもりだけど実はフォローしていない人いっぱいいそうだ。

DroidKaigi appにコントリビュートする実績解除した。

DroidKaigi preludeでgfxさんと二人でセッション解説をやった。会場にいた誰よりも楽しんでいた自信があるw

最終出社日、最後のPRで自分で自分を消すやつ、一度やってみたかったので記念スクショ撮ってた。

1月は送別会とかでたくさん美味しいもの食べたり、寄せ書きが嬉しすぎて号泣したり忙しかった。幸せなことだな〜〜〜

2月

2月から丸一ヶ月有休消化で寒すぎて引きこもりまくってた。

DroidKaigi、今年はオーディエンス参加だった。濃い二日間でした!

今年は初めて確定申告した。一度フローを理解するとそんなでもないはずなんだけど、初回はなかなかつらみがあった。

2月にこんなことをつぶやいているけど、2018年後半始まっても未だに毎月スターエンジニアが誰かしら転職している気がする。

Paymoの残高まだまだ余ってます。Paymo支払いの飲み会とかやりたい。ただしPaymoで支払う側として。

有休消化でのんびり南の島にバカンスに行った。一人で。

主にビーチサイドで本を読んでいたんだけど、そのとき読んだ本の記録はこちら。 muumuutech.hatenablog.com

日本に戻って来たら花粉が飛び始めていた。

3月

新しい職場。外国人の同僚も多いのでここから英語も併記したりし始めている。

花粉がマジで辛い

新しい職場でももくもく会をやっていくぞ!

次回の開催はこちら

mercari-android-mokumoku.connpass.com

Flutter盛り上がって来た

shibuya.apkがライブ配信されるようになった。DeployGateさんのオフィスで配信見ながら女子エンジニア会。美味しいお料理とお酒で最高だった

友人の家でバーフバリ鑑賞会をやった。ボリウッド映画多分初めてだったけど楽しかった!

4月

Android Dagashiデビューしてテンションが上がっていた

IWDのトークセッションに参加した。 kinukoさんkeynoteが素晴らしすぎて、この資料は定期的に見返したい



Flutter勉強会やった

勉強会の前に自分でも触ってみるか〜と思って試したらこのザマ

築地市場が移転する前に早朝のツアーに行ってみた。

ツアーは基本的に英語だし参加者もほとんど外国人でここどこだっけ?ってなった

この日は築地だけじゃなく色々日本っぽいところを巡るデートをしていた

5月

出張で福岡に来た

移動中の飛行機のお供で読んだOKR本よかった!

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

Kotlin愛好会で登壇した

6月

出張でアメリカのオフィスに一週間ほど行った。

海外オフィスでの雑な感想が今までで一番伸びた

UberiPhone置き忘れて大変だった

muumuutech.hatenablog.com

久しぶりに新卒で入った会社の人たちと飲んでいた。普段エンジニアの中でもweb系の本当に限られた人としかあってなかったんだなーと実感した

おまけ

夫シリーズ

Android Frameworkのコードにbreakpointを止めるメモ

Android開発をしていると、Frameworkがどういう挙動をしているか調べたくなる時がある。そういう時はFrameworkのコードにbreakpointを置くんだけど、止まってくれたり止まらなかったりすることがあるので困っていた。

このTweetに対して神リプライがついたので流れないようにメモしておく。

実際試したところ、Compile SDK versionと合わせたEmulatorを作ってそこで動かすというのが良さそう。