RecyclerView源码详解(第二篇ItemDecoration源码详解)

看源码之前,先看一下ItemDecoration能给我们实现的效果图:




看静态图标记的这条分割线,每个子itemView如果想空开距离或者想产生明显的边界的话,就应该写ItemDecoration类的实现类,也就是说RecyclerView没有像ListView一样为我们实现了分割线功能,先看一下实现的代码

 mRecyclerView.addItemDecoration(new DividerItemDecoration(this,LinearLayoutManager.VERTICAL));

RecyclerView实现分割线就只需要实现addItemDecoration这个方法就好了,前提是你必须重写ItemDecoration类的几个方法,DividerItemDecoration是ItemDecoration的实现子类

@Override
	public void onDrawOver(Canvas c, RecyclerView parent, State state) {
		// TODO Auto-generated method stub
		super.onDrawOver(c, parent, state);
	}

	/**
	 * 分垂直方向画和水平方向画
	 */
	@Override
	public void onDraw(Canvas c, RecyclerView parent) {
		if (mOrientation == VERTICAL_LIST) {
			drawVertical(c, parent);
		} else {
			drawHorizontal(c, parent);
		}
	}

	/**
	 * 垂直方向画线
	 * 
	 * @param c
	 * @param parent
	 */
	public void drawVertical(Canvas c, RecyclerView parent) {
		final int left = parent.getPaddingLeft();
		final int right = parent.getWidth() - parent.getPaddingRight();

		final int childCount = parent.getChildCount();
		for (int i = 0; i < childCount; i++) {
			if (i == childCount - 1)
				break;
			final View child = parent.getChildAt(i);
			final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
					.getLayoutParams();
			final int top = child.getBottom() + params.bottomMargin;
			final int bottom = top + mDivider.getIntrinsicHeight();
			mDivider.setBounds(left, top, right, bottom);
			mDivider.draw(c);
		}
	}

	/*
	 * 水平方向画线
	 */
	public void drawHorizontal(Canvas c, RecyclerView parent) {
		final int top = parent.getPaddingTop();
		final int bottom = parent.getHeight() - parent.getPaddingBottom();

		final int childCount = parent.getChildCount();
		for (int i = 0; i < childCount; i++) {
			if (i == childCount - 1)
				break;
			final View child = parent.getChildAt(i);
			final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
					.getLayoutParams();
			final int left = child.getRight() + params.rightMargin;
			final int right = left + mDivider.getIntrinsicHeight();
			mDivider.setBounds(left, top, right, bottom);
			mDivider.draw(c);
		}
	}

	/**
	 * 获得分割线的大小
	 */
	@Override
	public void getItemOffsets(Rect outRect, int itemPosition,
			RecyclerView parent) {
		if (mOrientation == VERTICAL_LIST) {
			outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
		} else {
			outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
		}
	}

这几个方法是最主要的几个方法,如果不看文档的话不知道实现ItemDecoration的实现类需要覆写哪几个方法,看了文档也不一定知道它的一些子类方法是干什么的,那么这个时候就该看看源码怎么实现的了,好,现在就当不知道这个实现类实现的这几个方法是干什么的,从入口mRecyclerView.addItemDecoration进入

public void addItemDecoration(ItemDecoration decor, int index) {
		if (mLayout != null) {
			mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
					+ " layout");
		}
		// 将decor加入集合中
		if (mItemDecorations.isEmpty()) {
			setWillNotDraw(false);
		}
		if (index < 0) {
			mItemDecorations.add(decor);
		} else {
			mItemDecorations.add(index, decor);
		}
		//标记当前的显示的子view的params中的mInsetsDirty为true
		markItemDecorInsetsDirty();
		//重新测量,布局重画
		requestLayout();
	}
这个方法意思很明了了,就是向mItemDecorations加入ItemDecoration 对象,然后将在屏幕中的itemView的params中的mInsetsDirty设为true,将缓存中的view的params中的mInsetsDirty设为true,最后执行View树的重新测量、重写布局位置、重写画。根据最后的View树的重画后,那么大体可以推测出ItemDecoration应该在重绘的重要的方法中都有用到。好,首先看一下测量

protected void onMeasure(int widthSpec, int heightSpec) {
		// 正在测量的时候数据通知改变
		if (mAdapterUpdateDuringMeasure) {
			eatRequestLayout();
			processAdapterUpdatesAndSetAnimationFlags();
			/**
			 * 如果设置了动画的话mState.mInPreLayout为true
			 */
			if (mState.mRunPredictiveAnimations) {
				// TODO: try to provide a better approach.
				// When RV decides to run predictive animations, we need to
				// measure in pre-layout
				// state so that pre-layout pass results in correct layout.
				// On the other hand, this will prevent the layout manager from
				// resizing properly.
				mState.mInPreLayout = true;
			} else {
				// consume remaining updates to provide a consistent state with
				// the layout pass.
				mAdapterHelper.consumeUpdatesInOnePass();
				mState.mInPreLayout = false;
			}
			mAdapterUpdateDuringMeasure = false;
			resumeRequestLayout(false);
		}

		if (mAdapter != null) {
			// 为状态添加子View的数量
			mState.mItemCount = mAdapter.getItemCount();
		} else {
			mState.mItemCount = 0;
		}
		// 假如布局文件为空,那么采用默认测量
		if (mLayout == null) {
			defaultOnMeasure(widthSpec, heightSpec);
		} else {
			// 否则用布局文件测量
			mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
		}

		mState.mInPreLayout = false; // clear
	}

这句话的核心意思就是如果mLayout 不为空测量就会交给mLayout,为空则采用的默认的defaultOnMeasure测量,mLayout是啥?布局管理器了,此例子中设置的mLayout是LinearLayoutManager,也就是说RecycleView会被测量交给LinearLayoutManager,那么onLayout方法会不会也交给它。

void dispatchLayout() {
		// 没有adapter和mLayout直接返回
		if (mAdapter == null) {
			Log.e(TAG, "No adapter attached; skipping layout");
			return;
		}
		if (mLayout == null) {
			Log.e(TAG, "No layout manager attached; skipping layout");
			return;
		}
		// 储存动画信息的类

		mViewInfoStore.clear();
		eatRequestLayout();
		// 布局和滚动的累加器
		onEnterLayoutOrScroll();
		// 设置一些必要的参数
		processAdapterUpdatesAndSetAnimationFlags();
		mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations
				&& mItemsChanged;
		mItemsAddedOrRemoved = mItemsChanged = false;
		// 如果有动画的话mInPreLayout为true
		mState.mInPreLayout = mState.mRunPredictiveAnimations;
		mState.mItemCount = mAdapter.getItemCount();
		findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

		此处省去若干行……
		mState.mItemCount = mAdapter.getItemCount();
		mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

		// Step 2: Run layout
		mState.mInPreLayout = false;
//最终调用mLayout的onLayoutChildren进行布局
		mLayout.onLayoutChildren(mRecycler, mState);

		此处省去若干行…….
					// Step 3: Find out where things are now, and process change
		}
	}

可以看到最终怎么布局,RecyclerView将布局交给了mLayout,好既然测量和布局位置都交给mLayout,那么直接看LinearLayoutManager的这两个方法好了。


	public void onMeasure(Recycler recycler, State state, int widthSpec,
				int heightSpec) {
			mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
		}

纵观LinearLayoutManager的方法,它没有实现测量子View,只是又回调了RecyclerView的defaultOnMeasure

private void defaultOnMeasure(int widthSpec, int heightSpec) {
		final int widthMode = MeasureSpec.getMode(widthSpec);
		final int heightMode = MeasureSpec.getMode(heightSpec);
		final int widthSize = MeasureSpec.getSize(widthSpec);
		final int heightSize = MeasureSpec.getSize(heightSpec);

		int width = 0;
		int height = 0;

		switch (widthMode) {
		case MeasureSpec.EXACTLY:
		case MeasureSpec.AT_MOST:
			width = widthSize;
			break;
		case MeasureSpec.UNSPECIFIED:
		default:
			width = ViewCompat.getMinimumWidth(this);
			break;
		}

		switch (heightMode) {
		case MeasureSpec.EXACTLY:
		case MeasureSpec.AT_MOST:
			height = heightSize;
			break;
		case MeasureSpec.UNSPECIFIED:
		default:
			height = ViewCompat.getMinimumHeight(this);
			break;
		}

		setMeasuredDimension(width, height);
	}

这个方法只是实现了自身大小的设置,并没有测量子View,那么是不是在布局代码里实现了测量呢?最后确定子View的方法是下面这个方法

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);
			}
		} else {
			if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
				addDisappearingView(view);
			} else {
				addDisappearingView(view, 0);
			}
		}
		//测量子View、
		measureChildWithMargins(view, 0, 0);
		//result.mConsumed=子View总够需要的空间
		result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
		int left, top, right, bottom;
		if (mOrientation == VERTICAL) {
			if (isLayoutRTL()) {
				right = getWidth() - getPaddingRight();
				left = right
						- mOrientationHelper
								.getDecoratedMeasurementInOther(view);
			} else {
				left = getPaddingLeft();
				right = left
						+ mOrientationHelper
								.getDecoratedMeasurementInOther(view);
			}
			if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
				bottom = layoutState.mOffset;
				top = layoutState.mOffset - result.mConsumed;
			} else {
				top = layoutState.mOffset;
				bottom = layoutState.mOffset + result.mConsumed;
			}
		} else {
			top = getPaddingTop();
			//需加上分割线的距离定义top的位置
			bottom = top
					+ mOrientationHelper.getDecoratedMeasurementInOther(view);

			if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
				right = layoutState.mOffset;
				left = layoutState.mOffset - result.mConsumed;
			} else {
				left = layoutState.mOffset;
				right = layoutState.mOffset + result.mConsumed;
			}
		}
		// We calculate everything with View's bounding box (which includes
		// decor and margins)
		// To calculate correct layout position, we subtract margins.
		layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
				right - params.rightMargin, bottom - params.bottomMargin);
		if (DEBUG) {
			Log.d(TAG, "laid out child at position " + getPosition(view)
					+ ", with l:" + (left + params.leftMargin) + ", t:"
					+ (top + params.topMargin) + ", r:"
					+ (right - params.rightMargin) + ", b:"
					+ (bottom - params.bottomMargin));
		}
		// Consume the available space if the view is not removed OR changed
		if (params.isItemRemoved() || params.isItemChanged()) {
			result.mIgnoreConsumed = true;
		}
		result.mFocusable = view.isFocusable();
	}

这个方法先用measureChildWithMargins(view, 0, 0)测量了子View的大小后又计算了子View的left,top,right,bottom位置,最后调用layoutDecorated方法对子View进行布局,先看下measureChildWithMargins方法

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的大小如果设置为FILL_PARENT或者WRAP_CONTENT或者match_parent,那么resultSize=RecyclerView的大小-pading-margin-分割线大小,如果子View设置了固定大小,那么宽高就是固定值。这里面最重要的方法就是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;
	}


循环调用我们集合里面存入的ItemDecoration对象,将位置数据left,top,right ,bottom 叠加,也就是说可以加入多个分割线,当然这里不单单指分割线,你也可以画其他的东西。这里面就回调了getItemOffsets方法,

/**
	 * 获得分割线的大小
	 */
	@Override
	public void getItemOffsets(Rect outRect, int itemPosition,
			RecyclerView parent) {
		if (mOrientation == VERTICAL_LIST) {
			outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
		} else {
			outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
		}
	}

也就是说这个方法用于确定分割线的left,top,right ,bottom。测量完毕后,接下来是布局,在确定itemView的位置的时候,也在不断的计算itemView的left,top,right ,bottom,right = left+ mOrientationHelper.getDecoratedMeasurementInOther(view),bottom = top+ mOrientationHelper.getDecoratedMeasurementInOther(view),这里又看到了Decorate相关的东西,这两句话的意思是一次排版子View的时候,假如是垂直方向的话那么第二个子View的top是第一个子View的高度+间距+分割线的大小,这样就空出来的分割线的距离。

/**
			 * 返回水平方向上上此控件加上间距和分割线总的距离(所需要占用的空间)
			 */
			@Override
			public int getDecoratedMeasurementInOther(View view) {
				final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view
						.getLayoutParams();
				return mLayoutManager.getDecoratedMeasuredWidth(view)
						+ params.leftMargin + params.rightMargin;
			}

这个方法很明了的证明了上面的说法,既然测量、布局都完成了的话,那么现在就剩画那条灰色的线了,OK看看RecycleView的draw方法

@Override
	public void draw(Canvas c) {
		super.draw(c);

		final int count = mItemDecorations.size();
		for (int i = 0; i < count; i++) {
			mItemDecorations.get(i).onDrawOver(c, this, mState);
		}

进行绘制的时候调用draw了这个方法,父类的View写方式实现了下面这个方法
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);


从代码可看到View树进行画的时候先画背景 drawBackground,再画自己onDraw方法,最后画子View调用   dispatchDraw(canvas)方法,下面看一下onDraw方法

@Override
	public void onDraw(Canvas c) {
		super.onDraw(c);

		final int count = mItemDecorations.size();
		for (int i = 0; i < count; i++) {
			mItemDecorations.get(i).onDraw(c, this, mState);
		}
	}
draw和onDraw这两个方法可 以看出ItemDecoration的onDraw方法会在绘制完自身之后再回调,而onDrawOver方法会在绘制完子view之后再回调,所以ItemDecoration进行绘制的时候先回调onDraw再回调onDrawOver,也就是说我们画分割线的时候覆写onDraw或onDrawOver都可以,那么看一下实现


public void onDraw(Canvas c, RecyclerView parent) {
		if (mOrientation == VERTICAL_LIST) {
			drawVertical(c, parent);
		} else {
			drawHorizontal(c, parent);
		}
	}


	/**
	 * 垂直方向画线
	 * 
	 * @param c
	 * @param parent
	 */
	public void drawVertical(Canvas c, RecyclerView parent) {
		final int left = parent.getPaddingLeft();
		final int right = parent.getWidth() - parent.getPaddingRight();

		final int childCount = parent.getChildCount();
		for (int i = 0; i < childCount; i++) {
			if (i == childCount - 1)
				break;
			final View child = parent.getChildAt(i);
			final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
					.getLayoutParams();
			final int top = child.getBottom() + params.bottomMargin;
			final int bottom = top + mDivider.getIntrinsicHeight();
			mDivider.setBounds(left, top, right, bottom);
			mDivider.draw(c);
		}
	}

是不是很明了了,根据子View的高度不断累加Drawable的高度,最后将Drawable画到屏幕上。


总结:1、RecycleView通过ItemDecoration的getItemOffsets辅助测量子View的大小

            2、RecycleView通过ItemDecoration的getItemOffsets方法将间距扩大

            3、RecycleView通过ItemDecoration的onDraw方法或onDrawOver方法将分割线画到屏幕上





  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值