Android | Tangram动态页面之路(四)vlayout原理

本系列文章主要介绍天猫团队开源的Tangram框架的使用心得和原理,由于Tangram底层基于vlayout,所以也会简单讲解,该系列将按以下大纲进行介绍:

  1. 需求背景

  2. Tangram和vlayout介绍

  3. Tangram的使用

  4. vlayout原理

  5. Tangram原理

  6. Tangram二次封装

本文将对Tangram的底层实现vlayout进行讲解。

基于vlayout最新源码

vlayout

Tangram和vlayout介绍这篇文章提到过,

vlayout自定义了一个VirtualLayoutManager,它继承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它负责具体的布局逻辑;VirtualLayoutManager管理了一系列LayoutHelper,将具体的布局能力交给LayoutHelper来完成,每一种LayoutHelper提供一种布局方式,框架内置提供了几种常用的布局类型,包括:网格布局、线性布局、瀑布流布局、悬浮布局、吸边布局等。这样实现了混合布局的能力,并且支持扩展外部,注册新的LayoutHelper,实现特殊的布局方式。

引用自苹果核 - Tangram 的基础 —— vlayout(Android)

大致意思是这样,

VLayoutActivity中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//VLayoutActivity.java
void onCreate(Bundle savedInstanceState) {
if (FLOAT_LAYOUT) {
//创建布局方式layoutHelper,FloatLayoutHelper是浮动可拖拽布局,比如微信现在的浮窗功能
FloatLayoutHelper layoutHelper = new FloatLayoutHelper();
//设置初始位置为右下角
layoutHelper.setAlignType(FixLayoutHelper.BOTTOM_RIGHT);
//设置偏移量,位置是右下角时,分别是marginRight和marginBottom
layoutHelper.setDefaultLocation(100, 400);
//设置宽高
LayoutParams layoutParams = new LayoutParams(150, 150);
//创建子适配器,添加进适配器集合
adapters.add(new SubAdapter(this, layoutHelper, 1, layoutParams));
}
}

来到子适配器SubAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//继承DelegateAdapter.Adapter
class SubAdapter extends DelegateAdapter.Adapter<MainViewHolder> {
private LayoutHelper mLayoutHelper;

public SubAdapter(Context context, LayoutHelper layoutHelper, int count, LayoutParams layoutParams) {
this.mContext = context;
this.mLayoutHelper = layoutHelper;
this.mCount = count;
this.mLayoutParams = layoutParams;
}

@Override
public LayoutHelper onCreateLayoutHelper() {
//把传进来的布局方式LayoutHelper返回
return mLayoutHelper;
}

//创建ViewHolder
public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MainViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false));
}

//绑定ViewHolder
protected void onBindViewHolderWithOffset(MainViewHolder holder, int position, int offsetTotal) {
((TextView) holder.itemView.findViewById(R.id.title)).setText(Integer.toString(offsetTotal));
}
}

delegateAdapter.setAdapters(adapters)时,取出适配器指定的布局方式,进行透传,

1
2
3
4
5
6
7
8
9
//DelegateAdapter.java
public void setAdapters(List<Adapter> adapters) {
List<LayoutHelper> helpers = new LinkedList<>();
for (Adapter adapter : adapters) {
LayoutHelper helper = adapter.onCreateLayoutHelper();
helpers.add(helper);
}
super.setLayoutHelpers(helpers);
}

来到VirtualLayoutManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//VirtualLayoutManager.java
void setLayoutHelpers(@Nullable List<LayoutHelper> helpers) {
//设置每个布局方式LayoutHelper的管辖范围start和end
//假设第1个模块是ColumnLayoutHelper,有3个元素,则管辖范围是[0,2]
//第2个模块是OnePlusNLayoutHelper,有4个元素,则管辖范围是[3,6]
if (helpers != null) {
int start = 0;
Iterator<LayoutHelper> it1 = helpers.iterator();
while (it1.hasNext()) {
LayoutHelper helper = it1.next();
if (helper.getItemCount() > 0) {
helper.setRange(start, start + helper.getItemCount() - 1);
} else {
helper.setRange(-1, -1);
}
start += helper.getItemCount();
}
}
//内部进行赋值和排序,RangeLayoutHelperFinder可以根据位置查找对应的LayoutHelper
this.mHelperFinder.setLayouts(helpers);
requestLayout();
}

LayoutHelper被赋值好后,进行布局,这里暂不深究View的测量布局绘制流程,来到VirtualLayoutManager.onLayoutChildren

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//VirtualLayoutManager.java
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//预布局,也就是调用每个ayoutHelper的beforeLayout
runPreLayout(recycler, state);
super.onLayoutChildren(recycler, state);
}

//ExposeLinearLayoutManagerEx.java
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
fill(recycler, mLayoutState, state, false);
}

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
layoutChunk(recycler, state, layoutState, layoutChunkResultCache);
}

//VirtualLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState,
com.alibaba.android.vlayout.layout.LayoutChunkResult result) {
//RangeLayoutHelperFinder根据位置查找对应的的布局方式LayoutHelper
final int position = layoutState.mCurrentPosition;
LayoutHelper layoutHelper = mHelperFinder == null ? null : mHelperFinder.getLayoutHelper(position);
layoutHelper.doLayout(recycler, state, mTempLayoutStateWrapper, result, this);
}

//BaseLayoutHelper.java
void doLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutStateWrapper layoutState, LayoutChunkResult result,
LayoutManagerHelper helper) {
//触发每个具体的LayoutHelper进行测量和布局
layoutViews(recycler, state, layoutState, result, helper);
}

具体的测量和布局的实现layoutViews,我们举两个比较典型的布局方式分析,ColumnLayoutHelperFloatLayoutHelper

举例ColumnLayoutHelper列布局

设置比重,第一列和第四列占比33,中间两列不指定比重,则平分剩余空间,

1
layoutHelper.setWeights(new float[]{33f, Float.NaN, Float.NaN, 33f});

效果如下,

来看layoutViews方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//ColumnLayoutHelper.java
void layoutViews(RecyclerView.Recycler recycler, RecyclerView.State state,
VirtualLayoutManager.LayoutStateWrapper layoutState,
LayoutChunkResult result, LayoutManagerHelper helper) {
final int count = getAllChildren(mViews, recycler, layoutState, result, helper);
//1. 计算每个child的margin

//2. 用总宽度和百分比为child分配宽高,没有设置百分比的child先存储进mEqViews
for (int i = 0; i < count; i++) {
View view = mViews[i];
VirtualLayoutManager.LayoutParams params = (VirtualLayoutManager.LayoutParams) view.getLayoutParams();
int heightSpec = helper.getChildMeasureSpec(
helper.getContentHeight() - helper.getPaddingTop() - helper.getPaddingBottom(),
uniformHeight > 0 ? uniformHeight : params.height, true);
if (mWeights != null && i < mWeights.length && !Float.isNaN(mWeights[i]) && mWeights[i] >= 0) {
//根据百分比计算宽度
int resizeWidth = (int) (mWeights[i] * 1.0f / 100 * availableWidth + 0.5f);
//根据宽度和比例计算高度
if (!Float.isNaN(params.mAspectRatio)) {
int specialHeight = (int) (resizeWidth / params.mAspectRatio + 0.5f);
heightSpec = View.MeasureSpec
.makeMeasureSpec(specialHeight, View.MeasureSpec.EXACTLY);
}
helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(resizeWidth, View.MeasureSpec.EXACTLY), heightSpec);
//记录已使用宽度
usedWidth += resizeWidth;
//记录最小高度
minHeight = Math.min(minHeight, view.getMeasuredHeight());
} else {
mEqViews[eqSize++] = view;
}
}
}

3.将剩余宽度平分给没有设置百分比的child,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ColumnLayoutHelper.java
for (int i = 0; i < eqSize; i++) {
View view = mEqViews[i];
VirtualLayoutManager.LayoutParams params = (VirtualLayoutManager.LayoutParams) view.getLayoutParams();
int heightSpec;
int resizeWidth = (int) ((availableWidth - usedWidth) * 1.0f / eqSize + 0.5f);
//根据宽度和比例计算高度
if (!Float.isNaN(params.mAspectRatio)) {
int specialHeight = (int) (resizeWidth / params.mAspectRatio + 0.5f);
heightSpec = View.MeasureSpec
.makeMeasureSpec(specialHeight, View.MeasureSpec.EXACTLY);
} else {
heightSpec = helper.getChildMeasureSpec(
helper.getContentHeight() - helper.getPaddingTop() - helper.getPaddingBottom(),
uniformHeight > 0 ? uniformHeight : params.height, true);
}
helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(resizeWidth, View.MeasureSpec.EXACTLY),
heightSpec);
//记录最小高度
minHeight = Math.min(minHeight, view.getMeasuredHeight());
}

4.为所有child统一高度,为最小高度

1
2
3
4
5
6
7
8
//ColumnLayoutHelper.java
for (int i = 0; i < count; i++) {
View view = mViews[i];
if (view.getMeasuredHeight() != minHeight) {
helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(view.getMeasuredWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(minHeight, View.MeasureSpec.EXACTLY));
}
}

5.测量完成,进行布局,最终交给RecyclerView.LayoutManager进行处理,即layoutDecorated

1
2
3
4
5
6
7
8
//ColumnLayoutHelper.java
for (int i = 0; i < count; i++) {
View view = mViews[i];
int top = mTempArea.top, bottom = mTempArea.bottom;
int right = left + orientationHelper.getDecoratedMeasurementInOther(view);
layoutChildWithMargin(view, left, top, right, bottom, helper);
left = right;
}

举例FloatLayoutHelper浮动可拖拽布局

FloatLayoutHelper的布局代码就不看了,大概就是根据位置和偏移量计算具体位置,我们重点关注下他的触摸事件实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//FloatLayoutHelper.java
View.OnTouchListener touchDragListener = new View.OnTouchListener() {
boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isDrag = false;
//按下,让父view RecyclerView不要拦截事件
(v.getParent()).requestDisallowInterceptTouchEvent(true);
lastPosX = (int) event.getX();
lastPosY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(event.getX() - lastPosX) > mTouchSlop
|| Math.abs(event.getY() - lastPosY) > mTouchSlop) {
isDrag = true;
}
if (isDrag) {
//...
//不断更新坐标,实现移动效果
v.setTranslationX(curTranslateX);
v.setTranslationY(curTranslateY);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//抬起或取消,播放吸边动画,即自动弹回两侧
doPullOverAnimation(v);
//让父view RecyclerView恢复拦截事件
(v.getParent()).requestDisallowInterceptTouchEvent(false);
break;
}
}
}

效果如下,

RecyclerView复用和Cantor函数

RecyclerView最终使用的是管理子适配器集合的DelegateAdapter,通常情况下,我们是没法保证各个子适配器间的viewType能不冲突的,所以这里只分析hasConsistItemType=false的情况,具体原因见FAQ(组件复用的问题),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//DelegateAdapter.java

@Override
public int getItemViewType(int position) {
Pair<AdapterDataObserver, Adapter> p = findAdapterByPosition(position);
//子适配器的viewType作为subItemType
int subItemType = p.second.getItemViewType(position - p.first.mStartPosition);
//布局方式LayoutHelper的所在位置作为index
int index = p.first.mIndex;
//Cantor运算转成一个数
return (int) Cantor.getCantor(subItemType, index);
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//Cantor逆运算,把一个数转回subItemType和index
Cantor.reverseCantor(viewType, cantorReverse);
int index = (int)cantorReverse[1];
int subItemType = (int)cantorReverse[0];
//根据index找到具体的子适配器
Adapter adapter = findAdapterByIndex(index);
//由子适配器来创建具体的view
return adapter.onCreateViewHolder(parent, subItemType);
}

这边有点晦涩,画了张图,需要细品~

这样,自然就可以利用RecyclerView自带的复用机制帮我们管理view的复用了,

关于cantor函数:

设idx1,type1;idx2,type2,

当 idx1 != idx2 或 type1 != type2,

viewType1 = cantor(idx1,type1)

viewType2 = cantor(idx2,type2) 时

满足 viewType1 != viewType2

同时支持逆运算:

viewType1 => idx1,type1

viewType2 => idx2,type2

感兴趣的话可以看vlayout中使用数学的小场景

参考文章