1. 程式人生 > >阿里巴巴的Vlayout框架原始碼原理詳解(第一篇流程分析)

阿里巴巴的Vlayout框架原始碼原理詳解(第一篇流程分析)

先看一下阿里對這個框架留下的Demo的效果:


看效果大體的可以猜測這個框架給我們提供了很多佈局規則,據說淘寶首頁就是用這個框架做的。原始碼地址

好接下來我們就沿著這個Demo這條線開始分析實現原理,從而學習人家的架構搭建方式

先看佈局程式碼

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <android.support.v7.widget.RecyclerView
            android:id="@+id/main_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#aaaaaa"
            android:clipToPadding="true"
            android:paddingLeft="0dp"
            android:paddingRight="0dp"
            android:requiresFadingEdge="none"
            android:scrollbars="vertical" />
    </android.support.v4.widget.SwipeRefreshLayout>
..省略不重要程式碼塊
</FrameLayout>

佈局程式碼很簡單就是一個下拉重新整理的控價包裹了RecyclerView,那麼裡面的所有一切佈局規則都是根據RecyclerView的使用規則命名的白,如有對RecyclerView不熟悉的童鞋就看洋神的這篇部落格

RecycleView使用詳解
既然是用RecycleView,那麼看一下RecycleView的Adapter和ViewHoder的實現方式

final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.main_view);
		// layoutManager.setRecycleOffset(300);
		/**
		 * 設定佈局規則
		 */
		recyclerView.setLayoutManager(layoutManager);
		// layoutManager.setReverseLayout(true);
		/**
		 * 只設置間距,而不畫分割線
		 */
		RecyclerView.ItemDecoration itemDecoration = new RecyclerView.ItemDecoration() {
			public void getItemOffsets(Rect outRect, View view,
					RecyclerView parent, RecyclerView.State state) {
				int position = ((LayoutParams) view.getLayoutParams())
						.getViewPosition();
				outRect.set(4, 4, 4, 4);
			}
		};
		/**
		 * 自定義View的快取策略
		 */
		final RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool();
		recyclerView.setRecycledViewPool(viewPool);
		// recyclerView.addItemDecoration(itemDecoration);
		viewPool.setMaxRecycledViews(0, 20);
		/**
		 * 設定資料介面卡
		 */
		final DelegateAdapter delegateAdapter = new DelegateAdapter(
				layoutManager, true);
		recyclerView.setAdapter(delegateAdapter);
這裡Vlayout寫了一個DelegateAdapter 類,資料介面卡全是基於此類,那麼看一看此類是怎麼實現的吧
public abstract class VirtualLayoutAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
  //儲存了一個佈局管理器
    @NonNull
    protected VirtualLayoutManager mLayoutManager;

    public VirtualLayoutAdapter(@NonNull VirtualLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
    }

    public void setLayoutHelpers(List<LayoutHelper> helpers) {
        this.mLayoutManager.setLayoutHelpers(helpers);
    }
/**
 * 獲取所有佈局規則類
 * @return
 */
    @NonNull
    public List<LayoutHelper> getLayoutHelpers() {
        return this.mLayoutManager.getLayoutHelpers();
    }

}

它的父類持有佈局管理類,以及可以獲取加入的佈局管理規則類集合。那麼接著看它自己怎麼實現的,既然是實現了多種規則按照常規處理方式的話肯定會複寫getItemViewType方法,找一下有沒有這個方法
@Override
	public int getItemViewType(int position) {
		// 折半演算法查出對應的子Adapter
		Pair<AdapterDataObserver, Adapter> p = findAdapterByPosition(position);
		if (p == null) {
			return RecyclerView.INVALID_TYPE;
		}
		/**
		 * 通過子Adapter得到Type
		 */
		int subItemType = p.second.getItemViewType(position
				- p.first.mStartPosition);

		if (subItemType < 0) {
			// negative integer, invalid, just return
			return subItemType;
		}
		// Activity填入的為true所以將type作為key和Adapter作為value存入集合mItemTypeAry
		if (mHasConsistItemType) {
			mItemTypeAry.put(subItemType, p.second);
			return subItemType;
		}
		// 第幾個子adapter
		int index = p.first.mIndex;
		// 根據型別和第幾個子adapter重新計算型別,就運算元adapter裡的型別有重複的type也會定義為不同的type
		return (int) getCantor(subItemType, index);
	}


果然不出所料,確實有這個方法,這個方法的主要意思就是從已經儲存了子Adpter的集合中用折半演算法取出子adapter和它註冊的資料觀察者AdapterDataObserver,然後通過子Adapter的getItemViewType獲得子View的型別(通俗的講就是實現多級不同的列表),如果沒有覆寫此方法的話type預設為-1,此時直接返回-1,那麼RecycleView的快取策略就不會區分快取itemView,這裡的mHasConsistItemType如果不設定的話預設為true,那麼所有的型別必須以子Adpter的type為依據,也就是說,此時如果有兩個子Adpter,那麼如果它們區分了不同的type,但裡面有相同的type值,佈局確不一樣,那麼現在就危險了,因為滑動的時候,快取儲存的資料很可能會覆蓋前面的,也就是說可能會出現itemView和position的錯位問題,這個時候需把mHasConsistItemType改為false,那麼這個時候type會根據第幾個子Adpter的索引和子Adapter的type做運算所得,所以,肯定保證子Adapter中的itemView保證快取獨立不會被覆蓋。

接著看一下它怎麼設定子Adpter的:

/*
	 * 設定ziView的Adapter
	 */
	public void setAdapters(@Nullable List<Adapter> adapters) {
		clear();
		if (adapters == null) {
			adapters = Collections.emptyList();
		}
		List<LayoutHelper> helpers = new LinkedList<>();
		boolean hasStableIds = true;
		mTotal = 0;
		Pair<AdapterDataObserver, Adapter> pair;
		for (Adapter adapter : adapters) {
			// every adapter has an unique index id
			// 建立AdapterDataObserver資料觀察者
			// AdapterDataObserver的上一個mTotal末尾為startPosition
			// 每次index累加1
			AdapterDataObserver observer = new AdapterDataObserver(mTotal,
					mIndexGen == null ? mIndex++ : mIndexGen.incrementAndGet());
			adapter.registerAdapterDataObserver(observer);
			hasStableIds = hasStableIds && adapter.hasStableIds();
			LayoutHelper helper = adapter.onCreateLayoutHelper();
			helper.setItemCount(adapter.getItemCount());
			mTotal += helper.getItemCount();
			helpers.add(helper);
			pair = Pair.create(observer, adapter);
			// mIndex第幾個子Adapter
			mIndexAry.put(observer.mIndex, pair);
			mAdapters.add(pair);

		}
		// (!hasObservers()肯定返回false,因為recycleView預設會建立資料觀察者
		if (!hasObservers()) {
			// 所有子adapter的hasStableIds都為true,才為true
			super.setHasStableIds(hasStableIds);
		}
		super.setLayoutHelpers(helpers);
	}

這個方法就是設定子Adpter了,在用這個框架的時候為RecycleView設定為DelegateAdapter ,還要再呼叫這個方法為它設定子Adpter,而子Adpter真正實現了資料轉化為View場景,這裡的mTotal 表示的是資料來源的數量,它等於所有子View的getCount之和,AdapterDataObserver 是為每個子Adpter新增資料觀察者,最後建立子Adpter的佈局管理類,並把它們快取起來,用來二分查詢等。

接下來看一下RecycleView.Adapter最重要的兩個實現方法,資料繫結和和建立ViewHodler

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
		Pair<AdapterDataObserver, Adapter> pair = findAdapterByPosition(position);
		if (pair == null) {
			return;
		}

		pair.second.onBindViewHolder(holder, position
				- pair.first.mStartPosition);
		pair.second.onBindViewHolderWithOffset(holder, position
				- pair.first.mStartPosition, position);
	}

這個方法為資料繫結,看的出來最終的繫結資料還是用的子adpter的onBindViewHolder來實現,這裡的子Adpter,框架為它擴充套件了一個onBindViewHolderWithOffset方法,稍後再介紹它的用法。
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
			int viewType) {

		if (mHasConsistItemType) {
			//如果子類沒有設定viewtype
			Adapter adapter = mItemTypeAry.get(viewType);
			if (adapter != null) {
				return adapter.onCreateViewHolder(parent, viewType);
			}

			return null;
		}

		// reverse Cantor Function
		//Math.floor四捨五入,Math.sqrt求平方根
		int w = (int) (Math.floor(Math.sqrt(8 * viewType + 1) - 1) / 2);
		int t = (w * w + w) / 2;

		int index = viewType - t;
		int subItemType = w - index;

		Adapter adapter = findAdapterByIndex(index);
		if (adapter == null) {
			return null;
		}

		return adapter.onCreateViewHolder(parent, subItemType);
	}

這個方法就是建立ViewHoder了,預設mHasConsistItemType為true時,直接從快取類裡取出子Adpter並呼叫它的子Adpter來建立ViewHodler,如果子Adpter沒有覆寫getViewType方法的話,它將利用反向康托爾演算法計算出近似值取出子Adpter,並進行建立ViewHodler

其實這個演算法就是

private static long getCantor(long k1, long k2) {
		return (k1 + k2) * (k1 + k2 + 1) / 2 + k2;
	}

此演算法的逆向

好了,看完DelegateAdapter的這幾個重要的方法,我們暫且已經能得出此介面卡並沒有實現真正意義上的獲得子itemView,它只做一箇中轉工作,所有的工作全部交給子Adpter來做,也就是所有工作交給客戶端自己來確定到底使用什麼樣的佈局,當然這也是介面卡模式的好處,也就是說此DelegateAdapter採用了委託者模式,所有的資料繫結,佈局工作它不管,全部委託給子Adpter來處理。其實原理和通用的介面卡原理一樣,介面卡只做管理和公共互動部分,將資料和ui繫結交給被委託者處理,從而實現程式碼分離。

好了既然資料怎麼繫結怎麼分離的我們看完了,那麼接下來就看此框架到底是怎麼排版的,如果你熟練使用RecycleView的話一定知道RecycleView沒有實現怎麼佈局子View而是把工作都交給了LayoutManager的子類來處理,像LinearLayoutManager水平或垂直排版子View等。這裡vlayout框架用了VirtualLayoutManager。好的,那麼就進入看看它到底是幹什麼的

public class VirtualLayoutManager extends ExposeLinearLayoutManagerEx implements
		LayoutManagerHelper
它繼承與ExposeLinearLayoutManagerEx ,並實現了LayoutManagerHelper介面,看來它們就是重點了,接著進入ExposeLinearLayoutManagerEx 看一下
class ExposeLinearLayoutManagerEx extends LinearLayoutManager 

哈哈,這不是我們所熟悉的LinearLayoutManager 嗎,一般要自定義LayoutMannager的話基本上重寫一些方法就ok
如果要自定義佈局屬性的話重寫它
public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
			return new LayoutParams(c, attrs);
		}
//預設佈局屬性必須重寫
public abstract LayoutParams generateDefaultLayoutParams();
		
		public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
			return 0;
		}
//垂直滾動時呼叫
		public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
			return 0;
		}
//水平滾動時呼叫
	public int scrollHorizontallBy(int dy, Recycler recycler, State state) {
			return 0;
		}

		//只有這個方法返回true的時候才可以呼叫scrollHorizontallBy方法
		public boolean canScrollHorizontally() {
			return false;
		}

		//是否可以垂直滾動,只有它返回true時,方法scrollVerticallyBy才會呼叫
		public boolean canScrollVertically() {
			return false;
		}

		//滾動到確定的位置
		public void scrollToPosition(int position) {
			
			}
		}
//平滑滾動到某個位置
		public void smoothScrollToPosition(RecyclerView recyclerView,
				State state, int position) {
		
		}

好,OnLayoutChild肯定會被實現,因為只是自定義LayoutManager最重要的方法,找一下
public void onLayoutChildren(RecyclerView.Recycler recycler,
			RecyclerView.State state) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
			Trace.beginSection(TRACE_LAYOUT);
		}
		// 不是在滾動的話並且資料集結構改變了
		if (mNoScrolling && state.didStructureChange()) {
			mSpaceMeasured = false;
			mSpaceMeasuring = true;
		}
		//佈局之前的回撥方法
		runPreLayout(recycler, state);

		try {
			super.onLayoutChildren(recycler, state);
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			// MaX_VALUE means invalidate scrolling offset - no scroll
			runPostLayout(recycler, state, Integer.MAX_VALUE); // hack to
																// indicate its
																// an initial
																// layout
		}
//此處省略若干行
		
	}

不出所料這個方法的確被實現,現在可以大體的看出來,此框架應該是把LayoutManager該乾的佈局那部分給拆出去了,那麼驗證猜測繼續
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        這裡省略若干行,基本上是一些引數判斷和賦值,比如是否資料改變了,是否有滑動偏移值
        int startOffset;
        int endOffset;
        onAnchorReady(state, mAnchorInfo);
        //釋放回收adapter
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mIsPreLayout = state.isPreLayout();
        mLayoutState.mOnRefresLayout = true;
//從底部開始佈局
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStartExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEndExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        } else {
            // fill towards end
            updateLayoutStateToFillEndExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
//開始填充資料
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStartExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
//此處再省略若干行
        }
    }

假如沒有設定錨點或者沒有設定從底部開始佈局子View或者沒有設定mLayoutFromEnd=true,那麼預設從頂部開始佈局,在開始填充之前先對以前的子View進行全部回收,那麼繼續看fill填充
 protected 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.SCOLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutStateExpose(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        while (remainingSpace > 0 && layoutState.hasMore(state)) {
            layoutChunkResultCache.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResultCache);
            if (layoutChunkResultCache.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResultCache.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 (!layoutChunkResultCache.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResultCache.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResultCache.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResultCache.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutStateExpose(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResultCache.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrderExpose();
        }
        return start - layoutState.mAvailable;
    }

這個方法的核心思想就是計算RecycleView的可以使用的空間大小,根據這個限制不斷的排版子View與新增子View,直到排到邊界為止,最終通過layoutChunk安排子View的位置
protected void layoutChunk(RecyclerView.Recycler recycler,
			RecyclerView.State state, LayoutState layoutState,
			com.alibaba.android.vlayout.layout.LayoutChunkResult result) {
		final int position = layoutState.mCurrentPosition;
		mTempLayoutStateWrapper.mLayoutState = layoutState;
		// 得到LayoutHelper進行真正的佈局,找到adapter對應的LayoutHelper交給它進行佈局
		LayoutHelper layoutHelper = mHelperFinder == null ? null
				: mHelperFinder.getLayoutHelper(position);
		if (layoutHelper == null)
			layoutHelper = mDefaultLayoutHelper;

		layoutHelper.doLayout(recycler, state, mTempLayoutStateWrapper, result,
				this);

	此處省略若干行…
	}
進入這個方法之後發現它沒有實現真正的layout的而是把佈局交給子Adapter繫結的LayoutHelper 來進行佈局,也就是真正的確定子View的位置排列的並不是LayoutManager,而是LayoutHelper 類,再回到例子程式碼
if (FLOAT_LAYOUT) {
			FloatLayoutHelper layoutHelper = new FloatLayoutHelper();
			layoutHelper.setAlignType(FixLayoutHelper.BOTTOM_RIGHT);
			layoutHelper.setDefaultLocation(100, 400);
			LayoutParams layoutParams = new LayoutParams(150, 150);
			adapters.add(new SubAdapter(this, layoutHelper, 1, layoutParams));
		}
每次載入子Adpter的時候都為它繫結一個LayoutHelper,實現真正意義的位置確定,今天就分析到這