LoginSignup
68
52

More than 5 years have passed since last update.

RecyclerViewの動きを追いたい (初回レイアウト編)

Last updated at Posted at 2015-10-17

割とただのメモ書きなので、読まなくてよいです!
ただ分かっているとRecyclerViewと仲良くなれる気がします。

RecyclerViewのソースコードを見たことがありますか?
いくつかのパーツで出来ていて結構でかいですよね、、
普通に読むとつらそうです(自分には辛いです)
また、以下の様な画像がAndroid Developerにありますがちょっとこれだけだとちょっとうーんという感じです。。
https://developer.android.com/training/material/lists-cards.html
公式の画像

ですがRecyclerViewもただのViewGropを継承したViewのはずです。
なので、普通にカスタムViewGroupと同じようにonLayout()があります。

RecyclerViewはライブラリとして提供されているので、デバッガーでブレークポイントをつければ、出ているコードと同じ場所を実行してくれるので、読むときに必要な場所だけ読めて良いです。

今回はバージョンが23.1.0でLinearLayoutManagerで縦に並べて普通に表示する場合について説明します。

子Viewが作られるまで(Adapter.onCreateViewHolderが呼ばれるまで)

RecyclerView#onLayout()でRecyclerView#dispatchLayout()を呼び出しています。(関係ないですがTraceCompat便利そうですね、、)

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        eatRequestLayout();
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        resumeRequestLayout(false);
        mFirstLayoutComplete = true;
    }

RecyclerView#dispatchLayout();

    void dispatchLayout() {
...
        mState.mItemCount = mAdapter.getItemCount();

さっそくmAdapterからアイテムの数を取得しました!これはRecyclerView.Adapterでいつも実装するやつです!
まだRecyclerView#dispatchLayout()です。

...
        mLayout.onLayoutChildren(mRecycler, mState);
...

このmLayoutはLayoutManagerです。

LinearLayoutManager#onLayoutChildren()をみていきましょう

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

...
        if (mAnchorInfo.mLayoutFromEnd) {
...
        } else {
//レイアウトの指定の仕方は普通LayoutFromEndにしないとおもうのでこちらです。
            fill(recycler, mLayoutState, state, false);

RecyclerView.LayoutStateクラスでfillするときの状態を管理するみたいです

    /**
     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
     * space.
     */
    static class LayoutState {

LinearLayoutManager#fillを呼び出します。

    /**
     * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
     * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
     * and with little change, can be made publicly available as a helper class.
     *
     * @param recycler        Current recycler that is attached to RecyclerView
     * @param layoutState     Configuration on how we should fill out the available space.
     * @param state           Context passed by the RecyclerView to control scroll steps.
     * @param stopOnFocusable If true, filling stops in the first focusable new child
     * @return Number of pixels that it added. Useful for scoll functions.
     */
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
...
        while (remainingSpace > 0 && layoutState.hasMore(state)) {
...
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
...

ここのwhileで子View一個一個を実際にレイアウトしていきます!!
remainingSpace > 0で残りの表示スペースが有るかどうかと、次のデータがあるかをlayoutState.hasMoreで聞いて、なければwhileが終わります。

LayoutManager#layoutChunk()を呼び出します。
このメソッド内でViewのアイテムのほとんどの処理を行います。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);

LayoutState#next()

        View next(RecyclerView.Recycler recycler) {
...
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

Recycler#getViewForPosition()
分かりやすい名前ですが、これはとてもでかいメソッドです。
scrapという再利用するViewHolderを保持しておくキャッシュを駆使してViewを返そうとします。

        View getViewForPosition(int position, boolean dryRun) {
...
            ViewHolder holder = null;
...
            if (holder == null) {
                holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...

getScrapViewForPositionを実行しますがScrap(RecyclerViewのViewHolderのキャッシュ)がないのでholderはnullです。

Recycler#getViewForPosition()続き

...
            if (holder == null) {
...
                final int type = mAdapter.getItemViewType(offsetPosition);

ここでAdapterからgetItemViewTypeしてViewTypeを取得します。
RecyclerViewにはViewの種類を管理する仕組みがあり、その仕組のためのメソッドです。

Recycler#getViewForPosition()続き

...
    holder = getRecycledViewPool().getRecycledView(type);
...

こちらもscrapから取り出そうとする処理のようですが、これを行った後も、まだnullです。

Recycler#getViewForPosition()続き

                    holder = mAdapter.createViewHolder(RecyclerView.this, type);

やっとAdapterのonCreateViewHolderを呼び出すためのメソッドが来ました!

RecyclerView.Adapter#createViewHolder

        public final VH createViewHolder(ViewGroup parent, int viewType) {
            TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
            final VH holder = onCreateViewHolder(parent, viewType);
            holder.mItemViewType = viewType;
            TraceCompat.endSection();
            return holder;
        }

これでRecyclerView.Adapterを継承したクラスでonCreateViewHolderが呼びだされました!
holder.mItemViewTypeを代入しています。

子Viewにデータをバインドするまで(Adapter.onBindViewHolderが呼ばれるまで)

Recycler#getViewForPosition()続き

                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                holder.mOwnerRecyclerView = RecyclerView.this;
                mAdapter.bindViewHolder(holder, offsetPosition);

mAdapterHelper.findPositionOffsetこちらのメソッドでpostponed?になっている数を除いてpositionが取得できて、RecyclerView.Adapter#bindViewHolderを呼び出してデータをバインドします!

Viewの大きさを決定する

            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
...
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
...
            rvLayoutParams.mViewHolder = holder;
...
            return holder.itemView;

大体は普通にLayoutParamsを作成してセットするのみです。特別なことは多分この時点ではしていないと思います。
またここでLayoutParamsにViewHolderをセットしています。

ここでLinearLayoutManager#layoutChunk()まで戻ってきました。
layoutState.next(recycler)でAdapterを使ってViewを作って、データがバインドされたViewが取得されました。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
...
        View view = layoutState.next(recycler); // ←ここまででクリア!
...
        LayoutParams params = (LayoutParams) view.getLayoutParams();
...
                addView(view);

これでやっとレイアウトに追加されました
LinearLayoutManager#layoutChunk内

        measureChildWithMargins(view, 0, 0);
        public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;

            final int widthSpec = getChildMeasureSpec(getWidth(),
                    getPaddingLeft() + getPaddingRight() +
                            lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(),
                    getPaddingTop() + getPaddingBottom() +
                            lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            child.measure(widthSpec, heightSpec);
        }

子Viewのレイアウトの大きさがこのコードで決まります。
RecyclerView#getItemDecorInsetsForChildを呼び出します

    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

これで、ItemDecoratorのレイアウトを反映した子Viewの大きさの設定を行います。
これで子Viewの大きさが決定されました

Viewの位置(x, y)を決定する

LinearLayoutManager#layoutChunk内

        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
...
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
...

result.mConsumedはViewの高さになるメソッドで先ほど決まった大きさを利用しているようです。

OrientationHelper.getDecoratedMeasurementInOtherで先ほど大きさが決定したのを考慮して、横幅を取得して、右と左の場所を決定しました。

                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;

layoutStateが持っていたmOffsetでyが決まります。ちなみにこのmOffsetはfillメソッド内でlayoutChunkが終わった後更新されています。

LinearLayoutManager#layoutChunk続き

        layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
                right - params.rightMargin, bottom - params.bottomMargin);
        public void layoutDecorated(View child, int left, int top, int right, int bottom) {
            final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
            child.layout(left + insets.left, top + insets.top, right - insets.right,
                    bottom - insets.bottom);
        }

やっと、、子Viewがレイアウトされました、、

これをLinearLayoutManager#fillメソッド内で必要なViewの数、ぐるぐる回してlayoutChunkを呼ぶことで、Layoutしていっているようです。

多分ここまで読んでくれた人はいないと思いますが、お疲れ様でした

まとめ

ViewGroup#onLayoutから始まって、
RecyclerView.Adapterからアイテム数を取得して、
RecyclerView.AdapterからViewを取得して、
LayoutStateでレイアウト中の状態を管理して、
データをBindして、addViewして、
ItemDecolatorのレイアウトを経てレイアウトされました。

LayoutManagerはレイアウト関連のところを行う。
RecyclerViewがリサイクル関連の部分を管理している。
などが役割が分かれているのは名前で分かっていましたが少し具体的に知ることができました。

ここは間違っているとか、こういう意味だとか、RecyclerView内でここのコードを読むのをおすすめするよとか、そういったことがございましたらお教えください!!

68
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
52