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

1,951 阅读6分钟

本系列文章主要介绍天猫团队开源的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中,

//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

//继承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)时,取出适配器指定的布局方式,进行透传,

//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

//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

//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,中间两列不指定比重,则平分剩余空间,

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

效果如下,

来看layoutViews方法,

//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,

//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统一高度,为最小高度

//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

//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的布局代码就不看了,大概就是根据位置和偏移量计算具体位置,我们重点关注下他的触摸事件实现,

//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(组件复用的问题),

//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中使用数学的小场景

参考文章

本文使用 mdnice 排版