美文网首页
带weight的LinearLayout嵌套RecyclerVi

带weight的LinearLayout嵌套RecyclerVi

作者: 8060d0fd59ad | 来源:发表于2018-05-17 20:41 被阅读0次

        在偶然的一次调试中,发现了RecyclerView的onCreateViewHolder和onBindViewHolder发生了多次调用:

    而我的布局很简单:

        xmlns:android="http://schemas.android.com/apk/res/android"

        android:layout_width="match_parent"

        android:layout_height="match_parent">

            android:id="@+id/first_column"

            android:layout_width="100dp"

            android:layout_height="match_parent"

            android:text="first column"/>

            android:id="@+id/second_column"

            android:layout_width="0dp"

            android:layout_height="match_parent"

            android:layout_weight="1"

            android:orientation="vertical">

                android:id="@+id/recyclerview"

                android:layout_width="match_parent"

                android:layout_height="match_parent"/>

    ```

    ```

    public class MyActivityextends Activity {

    private RecyclerViewmRecyclerView;

        @Override

        protected void onCreate(@Nullable Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

            requestWindowFeature(Window.FEATURE_NO_TITLE);

            setContentView(R.layout.my_layout);

            initView();

            setRecyclerView();

        }

    private void initView() {

    mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);

        }

    private void setRecyclerView() {

    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

            mRecyclerView.setAdapter(new RecyclerView.Adapter() {

    @Override

                public RecyclerView.ViewHolderonCreateViewHolder(ViewGroup parent, int viewType) {

    Log.d("NQG", "onCreateViewHolder: ");

                    TextView textView =new TextView(MyActivity.this);

                    textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200));

                    return new RecyclerView.ViewHolder(textView) {};

                }

    @Override

                public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    Log.d("NQG", "onBindViewHolder: " + position);

                    ((TextView) holder.itemView).setText("" + position);

                }

    @Override

                public int getItemCount() {

    return 20;

                }

    });

        }

    }

    ```

        这就很奇怪了,于是便从RecycleView开始下手,从onCreateViewHolder回溯,发现调用的地方在RecycleView.Recycler#tryGetViewHolderForPositionByDeadline():

    ```

    ViewHoldertryGetViewHolderForPositionByDeadline(int position,

            boolean dryRun, long deadlineNs) {

    ViewHolder holder =null;

    // 省略从缓存中取ViewHolder的相关代码

    if (holder ==null) {

    long start = getNanoTime();

        if (deadlineNs !=FOREVER_NS

                && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {

    // abort - we have a deadline we can't meet

            return null;

        }

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

        if (ALLOW_THREAD_GAP_WORK) {

    // only bother finding nested RV if prefetching

            RecyclerView innerView =findNestedRecyclerView(holder.itemView);

            if (innerView !=null) {

    holder.mNestedRecyclerView =new WeakReference<>(innerView);

            }

    }

    long end = getNanoTime();

        mRecyclerPool.factorInCreateTime(type, end - start);

        if (DEBUG) {

    Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");

        }

    }

    // 省略部分代码

    if (mState.isPreLayout() && holder.isBound()) {

    // do not update unless we absolutely have to.

        holder.mPreLayoutPosition = position;

    }else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {

    if (DEBUG && holder.isRemoved()) {

    throw new IllegalStateException("Removed holder should be bound and it should"

                    +" come here only in pre-layout. Holder: " + holder

    + exceptionLabel());

        }

    final int offsetPosition =mAdapterHelper.findPositionOffset(position);

        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

    }

    ```

    注意到在第一次布局的时候,ViewHolder没有成功从RecycleView的缓存中取到过一次,每次都是new出来的,RecycleView的缓存失效?不应该的,

    注意到在此时,onBindViewHolder的pos参数,显示每个item都执行了bind操作,猜想可能在RecycleView layout的时候,将所有item都进行了layout操作,虽然某些

    view是无法显示下的,再回溯注意到tryGetViewHolderForPositionByDeadline是由LinearLayoutManager#next方法调用的,而next是在LinearLayoutManager#layoutChunk中调用的:

    ```

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,

            LayoutState layoutState, LayoutChunkResult result) {

    View view = layoutState.next(recycler);

        if (view ==null) {

    if (DEBUG && layoutState.mScrapList ==null) {

    throw new RuntimeException("received null view when unexpected");

            }

    // if we are laying out views in scrap, this may return null which means there is

    // no more items to layout.

            result.mFinished =true;

    return;

        }

    LayoutParams params = (LayoutParams) view.getLayoutParams();

        if (layoutState.mScrapList ==null) {

    if (mShouldReverseLayout == (layoutState.mLayoutDirection

                    == LayoutState.LAYOUT_START)) {

    addView(view);

            }else {

    addView(view, 0);

            }

    }

    // 省略之后代码

    ```

    很明显layoutChunk方法会根据一些参数,将item add到RecycleView中,而layoutChunk是由LinearLayoutManager#fill方法调用的:

    ```

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,

            RecyclerView.State state, boolean stopOnFocusable) {

    // max offset we should set is mFastScroll + available

        final int start = layoutState.mAvailable;

        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {

    // TODO ugly bug fix. should not happen

            if (layoutState.mAvailable <0) {

    layoutState.mScrollingOffset += layoutState.mAvailable;

            }

    recycleByLayoutState(recycler, layoutState);

        }

    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;

        LayoutChunkResult layoutChunkResult =mLayoutChunkResult;

        while ((layoutState.mInfinite || remainingSpace >0) && layoutState.hasMore(state)) {

    layoutChunkResult.resetInternal();

            if (VERBOSE_TRACING) {

    TraceCompat.beginSection("LLM LayoutChunk");

            }

    layoutChunk(recycler, state, layoutState, layoutChunkResult);

            if (VERBOSE_TRACING) {

    TraceCompat.endSection();

            }

    if (layoutChunkResult.mFinished) {

    break;

            }

    layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;

            /**

    * Consume the available space if:

    * * layoutChunk did not request to be ignored

    * * OR we are laying out scrap children

    * * OR we are not doing pre-layout

    */

            if (!layoutChunkResult.mIgnoreConsumed ||mLayoutState.mScrapList !=null

                    || !state.isPreLayout()) {

    layoutState.mAvailable -= layoutChunkResult.mConsumed;

                // we keep a separate remaining space because mAvailable is important for recycling

                remainingSpace -= layoutChunkResult.mConsumed;

            }

    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {

    layoutState.mScrollingOffset += layoutChunkResult.mConsumed;

                if (layoutState.mAvailable <0) {

    layoutState.mScrollingOffset += layoutState.mAvailable;

                }

    recycleByLayoutState(recycler, layoutState);

            }

    if (stopOnFocusable && layoutChunkResult.mFocusable) {

    break;

            }

    }

    if (DEBUG) {

    validateChildOrder();

        }

    return start - layoutState.mAvailable;

    }

    ```

    其中while执行条件为:

    1.layoutState.mInfinite为true或者RecycleView剩余的空间大于0

    2.layoutState.hasMore(state)为true

    先看第二个条件,layoutState.hasMore(state)代码如下:

    ```

    /**

    * @return true if there are more items in the data adapter

    */

    boolean hasMore(RecyclerView.State state) {

    return mCurrentPosition >=0 &&mCurrentPosition < state.getItemCount();

    }

    ```

    很明显,判断是否到了最后一个item.

    DeBug到此处:

    注意while循环中的layoutState.mInfinite变量为true,这会造成将所有item view都add到RecycleView中,这就解释了为何调用

    onCreateViewHolder和onBindViewHolder的次数与Adapter#getItemCount()值一致了.

    那么接下来,问题来了,为何layoutState.mInfinite值为true呢?注意到LinearLayoutManager#fill方法由LinearLayoutManager#onLayoutChildren调用,其中有如下代码:

    mLayoutState.mInfinite = resolveIsInfinite();

    boolean resolveIsInfinite() {

    return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED

                &&mOrientationHelper.getEnd() ==0;

    }

    最终是根据RecycleView.LayoutManager#mHeightMode == View.MeasureSpec.UNSPECIFIED判断的,而mHeightMode赋值的地方一处在设置LayoutManager的时候

    ,另一处在调用RecycleView.LayoutManager#setMeasureSpecs的地方,在RecycleView#onMeasure中有如下代码:

    很明显,传入onMeasure的heightSpec为0.

    可能会奇怪了,为啥最终显示的效果是正确的呢?这是因为进行了多次测量后,skipMeasure变量值为true,便不会走RecycleView.LayoutManager#setMeasureSpecs流程,

    也不会再走onLayoutChildren等接下来的流程了.

    再分析为何传入onMeasure的heightSpec为0,在布局中RecycleView的父布局为LinearLayout,recyclerview的heightSpec由父布局measure时传入,

    在LinearLayout#measureHorizontal中有如下代码:

    此时child就是RecycleView的父布局,注意到heightMeasureSpec,明显是有值的,但为何得到的值为0呢?那就要看View#makeSafeMeasureSpec方法了:

    /**

    * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED

    * will automatically get a size of 0. Older apps expect this.

    *

    * @hide internal use only for compatibility with system widgets and older apps

    */

    public static int makeSafeMeasureSpec(int size, int mode) {

    if (sUseZeroUnspecifiedMeasureSpec && mode ==UNSPECIFIED) {

    return 0;

        }

    return makeMeasureSpec(size, mode);

    }

    很明显View#sUseZeroUnspecifiedMeasureSpec变量为true了,返回0了,注意到sUseZeroUnspecifiedMeasureSpec赋值的地方:

    // In M and newer, our widgets can pass a "hint" value in the size

    // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers

    // know what the expected parent size is going to be, so e.g. list items can size

    // themselves at 1/3 the size of their container. It breaks older apps though,

    // specifically apps that use some popular open source libraries.

    sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;

    噢,原来只要targetSdkVersion小于AndroidM就会为true,参考工程中AndroidManifest.xml:

    uses-sdk android:minSdkVersion="17" android:targetSdkVersion="19"

    的确为true,这下问题的来龙去脉就理清了.

    解决这个问题,我目前想到了两个办法:

    1).更改App的targetSdkVersion为M及以上(不适合我对应的场景)

    2).将其LinearLayout父布局改为match_parent/match_parent(PS:RecycleView的直接或者间接LinearLayout父布局均不能带weight)

    相关文章

      网友评论

          本文标题:带weight的LinearLayout嵌套RecyclerVi

          本文链接:https://www.haomeiwen.com/subject/cspidftx.html