从源码解析RecyclerView绘制流程
来源:互联网 发布:淘宝商家货到付款 编辑:程序博客网 时间:2024/05/16 10:59
Recycler View的出现,无疑是令android 开发工程师兴奋了许久。不久前看ListView 源码的时候,看得晕乎乎的,滑动功能以及复用机制堆积在了同一个类里面,RecyclerView 很好的解决这种尴尬的情况。
下面先从功能上看一下各个类的功能:
RecyclerView 这个类相当于中心的管理者
LayoutManager 主要负责item 添加 ,删除,以及循环利用机制等功能
Recycler.SmoothScroller 主要是负责跟踪目标view的index 和 平滑滑动
Recycler.UpdateOp 主要负责标志view的行为,增加 删除 更新
Recycler.ViewHolder 这个类大家都很熟悉,没错他就是相当于ListView 的adapter 中的ViewHolder
Recycler.State 主要是保留RecyclerView的一些信息。
Recycler.Adapter 主要使将dataset数据与itemView 绑定在一起
Recycler.Recycler 主要是保留正在显示的View和已经废弃或者是可回收利用的view
上面是主要的类,这篇文章中,我们不涉及RecyclerView 相关的动画
入口函数是setAdapter 或者是setLayoutManager ,我们 用其中一个即可,下面是
public void setAdapter(Adapter adapter) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver); } // end all running animations if (mItemAnimator != null) { mItemAnimator.endAnimations(); } // Since animations are ended, mLayout.children should be equal to recyclerView.children. // This may not be true if item animator's end does not work as expected. (e.g. not release // children instantly). It is safer to use mLayout's child count. if (mLayout != null) { mLayout.removeAndRecycleAllViews(mRecycler); mLayout.removeAndRecycleScrapInt(mRecycler, true); } final Adapter oldAdapter = mAdapter; mAdapter = adapter; if (adapter != null) { adapter.registerAdapterDataObserver(mObserver); } if (mLayout != null) { mLayout.onAdapterChanged(oldAdapter, mAdapter); } mRecycler.onAdapterChanged(oldAdapter, mAdapter); mState.mStructureChanged = true; markKnownViewsInvalid(); requestLayout(); }最后requestLayout 会调用RecyclerView的onMeasure 和 onLayout() 方法
@Override protected void onMeasure(int widthSpec , int heightSpec) { if (mAdapterUpdateDuringMeasure) { eatRequestLayout(); updateChildViews(); mAdapterUpdateDuringMeasure = false; resumeRequestLayout(false); } if (mAdapter != null) { mState.mItemCount = mAdapter.getItemCount(); } mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final int widthSize = getMeasuredWidth(); final int heightSize = getMeasuredHeight(); if (mLeftGlow != null) { mLeftGlow.setSize(heightSize, widthSize); } if (mTopGlow != null) { mTopGlow.setSize(widthSize, heightSize); } if (mRightGlow != null) { mRightGlow.setSize(heightSize, widthSize); } if (mBottomGlow != null) { mBottomGlow.setSize(widthSize, heightSize); } }
mAdapterUpdateDuringMeasure 这个变量在下一篇文章中会追踪一下,我们这里就看主要的函数,下面我们来看一下,LinearLayoutManager 的 onMeasure 方法
public void onMeasure(Recycler recycler , State state , 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 = getMinimumWidth(); break; } switch (heightMode) { case MeasureSpec.EXACTLY: case MeasureSpec.AT_MOST: height = heightSize; break; case MeasureSpec.UNSPECIFIED: default: height = getMinimumHeight(); break; } setMeasuredDimension(width, height); }
可以看出,主要就是来设置RecyclerView的Size,那么下面来看一下RecyclerView的 Onlayout方法
@Override protected void onLayout(boolean changed , int l , int t , int r , int b) { eatRequestLayout(); dispatchLayout(); resumeRequestLayout(false); mFirstLayoutComplete = true; }看一下dispatchLayout()方法
void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); return; } eatRequestLayout(); // simple animations are a subset of advanced animations (which will cause a // prelayout step) boolean animateChangesSimple = mItemAnimator != null && mItemsAddedOrRemoved && !mItemsChanged; final boolean animateChangesAdvanced = ENABLE_PREDICTIVE_ANIMATIONS && animateChangesSimple && predictiveItemAnimationsEnabled(); mItemsAddedOrRemoved = mItemsChanged = false; ArrayMap<View, Rect> appearingViewInitialBounds = null; mState.mInPreLayout = animateChangesAdvanced; mState.mItemCount = mAdapter.getItemCount(); if (animateChangesSimple) { // Step 0: Find out where all non-removed items are, pre-layout mState.mPreLayoutHolderMap.clear(); mState.mPostLayoutHolderMap.clear(); final int count = getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(getChildAt(i)); final View view = holder.itemView; mState.mPreLayoutHolderMap.put(holder, new ItemHolderInfo(holder, view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), holder.mPosition)); } } if (animateChangesAdvanced) { // Step 1: run prelayout: This will use the old positions of items. The layout manager // is expected to layout everything, even removed items (though not to add removed // items back to the container). This gives the pre-layout position of APPEARING views // which come into existence as part of the real layout. mInPreLayout = true; final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged = false; // temporarily disable flag because we are asking for previous layout mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = didStructureChange; mInPreLayout = false; appearingViewInitialBounds = new ArrayMap<View, Rect>(); for (int i = 0; i < getChildCount(); ++i) { boolean found = false; final View child = getChildAt(i); for (int j = 0; j < mState.mPreLayoutHolderMap.size(); ++j) { final ViewHolder holder = mState.mPreLayoutHolderMap.keyAt(j); if (holder.itemView == child) { found = true; continue; } } if (!found) { appearingViewInitialBounds.put(child, new Rect(child.getLeft(), child.getTop(), child.getRight(), child.getBottom())); } } } clearOldPositions(); dispatchLayoutUpdates(); mState.mItemCount = mAdapter.getItemCount(); // Step 2: Run layout mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; // onLayoutChildren may have caused client code to disable item animations; re-check animateChangesSimple = animateChangesSimple && mItemAnimator != null; if (animateChangesSimple) { // Step 3: Find out where things are now, post-layout int count = getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(getChildAt(i)); final View view = holder.itemView; mState.mPostLayoutHolderMap.put(holder, new ItemHolderInfo(holder, view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), holder.mPosition)); } // Step 4: Animate DISAPPEARING and REMOVED items final int preLayoutCount = mState.mPreLayoutHolderMap.size(); for (int i = preLayoutCount - 1; i >= 0; i--) { final ViewHolder itemHolder = mState.mPreLayoutHolderMap.keyAt(i); if (!mState.mPostLayoutHolderMap.containsKey(itemHolder)) { final ItemHolderInfo disappearingItem = mState.mPreLayoutHolderMap.valueAt(i); mState.mPreLayoutHolderMap.removeAt(i); final View disappearingItemView = disappearingItem.holder.itemView; removeDetachedView(disappearingItemView, false); mRecycler.unscrapView(disappearingItem.holder); animateDisappearance(disappearingItem); } } // Step 5: Animate APPEARING and ADDED items final int postLayoutCount = mState.mPostLayoutHolderMap.size(); if (postLayoutCount > 0) { for (int i = postLayoutCount - 1; i >= 0; i--) { final ViewHolder itemHolder = mState.mPostLayoutHolderMap.keyAt(i); final ItemHolderInfo info = mState.mPostLayoutHolderMap.valueAt(i); if ((mState.mPreLayoutHolderMap.isEmpty() || !mState.mPreLayoutHolderMap.containsKey(itemHolder))) { mState.mPostLayoutHolderMap.removeAt(i); final Rect initialBounds = (appearingViewInitialBounds != null) ? appearingViewInitialBounds.get(itemHolder.itemView) : null; animateAppearance(itemHolder, initialBounds, info.left, info.top); } } } // Step 6: Animate PERSISTENT items count = mState.mPostLayoutHolderMap.size(); for (int i = 0; i < count; ++i) { final ViewHolder postHolder = mState.mPostLayoutHolderMap.keyAt(i); final ItemHolderInfo postInfo = mState.mPostLayoutHolderMap.valueAt(i); final ItemHolderInfo preInfo = mState.mPreLayoutHolderMap.get(postHolder); if (preInfo != null && postInfo != null) { if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) { postHolder.setIsRecyclable(false); if (DEBUG) { Log.d(TAG, "PERSISTENT: " + postHolder + " with view " + postHolder.itemView); } if (mItemAnimator.animateMove(postHolder, preInfo.left, preInfo.top, postInfo.left, postInfo.top)) { postAnimationRunner(); } } } } } resumeRequestLayout(false); mLayout.removeAndRecycleScrapInt(mRecycler, !animateChangesAdvanced); mState.mPreviousLayoutItemCount = mState.mItemCount; mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; }这段代码看起来很多,很多都是有关动画,今天我们的重点不是动画,所以找出其中的关键代码,
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
主要的layout 部分,我们来看一下LayoutManager的onLayoutChildren 方法,
public void onLayoutChildren(RecyclerView.Recycler recycler , RecyclerView.State state) { // layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom. // create render state if (DEBUG) { Log.d(TAG, "is pre layout:" + state.isPreLayout()); } if (mPendingSavedState != null) { setOrientation(mPendingSavedState.mOrientation); setReverseLayout(mPendingSavedState.mReverseLayout); setStackFromEnd(mPendingSavedState.mStackFromEnd); mPendingScrollPosition = mPendingSavedState.mAnchorPosition; } ensureRenderState(); // resolve layout direction resolveShouldLayoutReverse(); // validate scroll position if exists if (mPendingScrollPosition != RecyclerView.NO_POSITION) { // validate it if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; if (DEBUG) { Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); } } } // this value might be updated if there is a target scroll position without an offset boolean layoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; final boolean stackFromEndChanged = mLastStackFromEnd != mStackFromEnd; int anchorCoordinate, anchorItemPosition; if (mPendingScrollPosition != RecyclerView.NO_POSITION) { // if child is visible, try to make it a reference child and ensure it is fully visible. // if child is not visible, align it depending on its virtual position. anchorItemPosition = mPendingScrollPosition; if (mPendingSavedState != null) { // Anchor offset depends on how that child was laid out. Here, we update it // according to our current view bounds layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; if (layoutFromEnd) { anchorCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingSavedState.mAnchorOffset; } else { anchorCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingSavedState.mAnchorOffset; } } else if (mPendingScrollPositionOffset == INVALID_OFFSET) { final View child = findViewByPosition(mPendingScrollPosition); if (child != null) { final int startGap = mOrientationHelper.getDecoratedStart(child) - mOrientationHelper.getStartAfterPadding(); final int endGap = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(child); final int childSize = mOrientationHelper.getDecoratedMeasurement(child); if (childSize > mOrientationHelper.getTotalSpace()) { // item does not fit. fix depending on layout direction anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); } else if (startGap < 0) { anchorCoordinate = mOrientationHelper.getStartAfterPadding(); layoutFromEnd = false; } else if (endGap < 0) { anchorCoordinate = mOrientationHelper.getEndAfterPadding(); layoutFromEnd = true; } else { anchorCoordinate = layoutFromEnd ? mOrientationHelper.getDecoratedEnd(child) : mOrientationHelper.getDecoratedStart(child); } } else { // item is not visible. if (getChildCount() > 0) { // get position of any child, does not matter final int pos = getPosition(getChildAt(0)); if (mPendingScrollPosition < pos == mShouldReverseLayout) { anchorCoordinate = mOrientationHelper.getEndAfterPadding(); layoutFromEnd = true; } else { anchorCoordinate = mOrientationHelper.getStartAfterPadding(); layoutFromEnd = false; } } else { anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); } } } else { // override layout from end values for consistency if (mShouldReverseLayout) { anchorCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingScrollPositionOffset; layoutFromEnd = true; } else { anchorCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingScrollPositionOffset; layoutFromEnd = false; } } } else if (getChildCount() > 0 && !stackFromEndChanged) { if (layoutFromEnd) { final View referenceChild = getChildClosestToEnd(); anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild); anchorItemPosition = getPosition(referenceChild); } else { final View referenceChild = getChildClosestToStart(); anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild); anchorItemPosition = getPosition(referenceChild); } } else { anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); anchorItemPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; } detachAndScrapAttachedViews(recycler); final int extraForStart; final int extraForEnd; final int extra = getExtraLayoutSpace(state); final boolean before = state.getTargetScrollPosition() < anchorItemPosition; if (before == mShouldReverseLayout) { extraForEnd = extra; extraForStart = 0; } else { extraForStart = extra; extraForEnd = 0; } // first fill towards start updateRenderStateToFillStart(anchorItemPosition, anchorCoordinate); mRenderState.mExtra = extraForStart; if (!layoutFromEnd) { mRenderState.mCurrentPosition += mRenderState.mItemDirection; } fill(recycler, mRenderState, state, false); int startOffset = mRenderState.mOffset; // fill towards end updateRenderStateToFillEnd(anchorItemPosition, anchorCoordinate); mRenderState.mExtra = extraForEnd; if (layoutFromEnd) { mRenderState.mCurrentPosition += mRenderState.mItemDirection; } fill(recycler, mRenderState, state, false); int endOffset = mRenderState.mOffset; // changes may cause gaps on the UI, try to fix them. if (getChildCount() > 0) { // because layout from end may be changed by scroll to position // we re-calculate it. // find which side we should check for gaps. if (mShouldReverseLayout ^ mStackFromEnd) { int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } else { int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } } // If there are scrap children that we did not layout, we need to find where they did go // and layout them accordingly so that animations can work as expected. // This case may happen if new views are added or an existing view expands and pushes // another view out of bounds. if (getChildCount() > 0 && !state.isPreLayout() && supportsPredictiveItemAnimations()) { // to make the logic simpler, we calculate the size of children and call fill. int scrapExtraStart = 0, scrapExtraEnd = 0; final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); final int scrapSize = scrapList.size(); final int firstChildPos = getPosition(getChildAt(0)); for (int i = 0; i < scrapSize; i++) { final RecyclerView.ViewHolder scrap = scrapList.get(i); final int position = scrap.getPosition(); final int direction = position < firstChildPos != mShouldReverseLayout ? RenderState.LAYOUT_START : RenderState.LAYOUT_END; if (direction == RenderState.LAYOUT_START) { scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } else { scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } } if (DEBUG) { Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart + " towards start and " + scrapExtraEnd + " towards end"); } mRenderState.mScrapList = scrapList; if (scrapExtraStart > 0) { final View anchor = getChildClosestToStart(); updateRenderStateToFillStart(getPosition(anchor), startOffset); mRenderState.mExtra = scrapExtraStart; mRenderState.mAvailable = 0; mRenderState.mCurrentPosition += mShouldReverseLayout ? 1 : -1; fill(recycler, mRenderState, state, false); } if (scrapExtraEnd > 0) { final View anchor = getChildClosestToEnd(); updateRenderStateToFillEnd(getPosition(anchor), endOffset); mRenderState.mExtra = scrapExtraEnd; mRenderState.mAvailable = 0; mRenderState.mCurrentPosition += mShouldReverseLayout ? -1 : 1; fill(recycler, mRenderState, state, false); } mRenderState.mScrapList = null; } mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; mLastStackFromEnd = mStackFromEnd; mPendingSavedState = null; // we don't need this anymore if (DEBUG) { validateChildOrder(); } }这段代码已经在开始的时候标注了逻辑
// layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom.
而2 ,3 就是关键的地方 ,这里包含循环利用的逻辑,那我们接下来就看这段代码。
/** * The magic functions :). Fills the given layout, defined by the renderState. 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 renderState 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. */ private int fill(RecyclerView.Recycler recycler , RenderState renderState , RecyclerView.State state , boolean stopOnFocusable) { // max offset we should set is mFastScroll + available final int start = renderState.mAvailable; if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (renderState.mAvailable < 0) { renderState.mScrollingOffset += renderState.mAvailable; } recycleByRenderState(recycler, renderState); } int remainingSpace = renderState.mAvailable + renderState.mExtra; while (remainingSpace > 0 && renderState.hasMore(state)) { final View view = renderState.next(recycler); if (view == null) { if (DEBUG) { if (renderState.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. break; } final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); if (!params.isItemRemoved() && mRenderState.mScrapList == null) { if (mShouldReverseLayout == (renderState.mLayoutDirection == RenderState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } measureChildWithMargins(view, 0, 0); final int consumed = 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 (renderState.mLayoutDirection == RenderState.LAYOUT_START) { bottom = renderState.mOffset; top = renderState.mOffset - consumed; } else { top = renderState.mOffset; bottom = renderState.mOffset + consumed; } } else { top = getPaddingTop(); bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); if (renderState.mLayoutDirection == RenderState.LAYOUT_START) { right = renderState.mOffset; left = renderState.mOffset - consumed; } else { left = renderState.mOffset; right = renderState.mOffset + consumed; } } // 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)); } renderState.mOffset += consumed * renderState.mLayoutDirection; if (!params.isItemRemoved()) { renderState.mAvailable -= consumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= consumed; } if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) { renderState.mScrollingOffset += consumed; if (renderState.mAvailable < 0) { renderState.mScrollingOffset += renderState.mAvailable; } recycleByRenderState(recycler, renderState); } if (stopOnFocusable && view.isFocusable()) { break; } if (state != null && state.getTargetScrollPosition() == getPosition(view)) { break; } } if (DEBUG) { validateChildOrder(); } return start - renderState.mAvailable; }
在fill 之前,先回收一下无用的数据(超出屏幕的itemView),然后使用 while (remainingSpace > 0 && renderState.hasMore(state)) 循环,添加数据直至超出屏幕。
这里主要就是循环复用机制 ,假如之前阅读过listview 循环复用机制,这里和listview是类似的。
这就是RecyclerView 的基础绘画流程
- 从源码解析RecyclerView绘制流程
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 安卓5.1源码解析 : RecyclerView解析从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- Android从源码解析三:View绘制流程
- Android Render(四)supportVersion 27.0.0源码RecyclerView绘制流程解析
- View绘制流程源码解析
- View绘制流程源码解析
- 从源码分析View绘制流程
- Android View绘制流程与源码解析
- ListView源码解析(一) 绘制流程
- 开源项目源码解析-View 绘制流程
- Android源码解析(十八)-->Activity布局绘制流程
- Android源码解析(十九)-->Dialog加载绘制流程
- Android源码解析(二十)-->Dialog取消绘制流程
- Android源码解析(二十一)-->PopupWindow加载绘制流程
- 新SAT作文改革之官方作文解析(10-11分)
- 最简单实现侧边栏的方法----UISplitViewController
- 如何处理win7 64位系统登录进程初始化失败故障
- LCD调试
- Jodd-Java的瑞士军刀 demo
- 从源码解析RecyclerView绘制流程
- O2O刷单“黑市”折射下的泡沫
- Java NIO使用及原理分析(三)
- mysql防止中文乱码
- Hibernate(一)编写第一个小例子
- GRE词汇记忆误区解析
- 通过变量获取资源的id
- PIC32入门篇-----环境搭建
- 月薪没过万,就是交际圈的问题