深度分析FlexboxLayout可伸缩布局

Deep analysis of FlexboxLayout

Posted by Shinelw on April 13, 2017. Viewed times.

Google开源了一种可伸缩弹性的布局FlexboxLayout,它根据CSS的FlexBox仿造而来,提供了更加灵活简单的布局方式。初识FlexboxLayout时,首先联想到的是开源项目流式布局FlowLayout。但相比于FlowLayout,FlexboxLayout在操作上更加简单,并且更加强大,谷歌在apha版本中加入了对于RecyclerView的支持。对于FlexboxLayout,最适合的应用场景在于 标签云图片流 ,如下图。

tagpicflow

同时,FlexboxLayout提供了一个很强大的特性,根据屏幕的大小自动适配自动换行。当我们把手机的屏幕旋转时,就会变成下面的样子。

tagpicflow-rotate

不需要人肉进行复杂的手机适配工作,FlexboxLayout自动的帮我们完成了适配。接下来,我们来看看FlexboxLayout如何实现可伸缩可自动换行的特性。

关于如何在项目中使用FlexboxLayout,请自行参考官方文档

项目框架

我们先看一下FlexboxLayout项目的整个类图,基于版本0.3.0-alpha3。

FlexboxLayout类图

整体上来看,项目基本可以分成三个部分,公共接口/逻辑部分FlexboxLayout实现RecyclerView接入实现

  • 公共接口部分:对于FlexboxLayout和RecyclerView的抽象接口FlexContainer;Layout的属性JustifyContent、FlexDirection、FlexWrap等;Layout的组成FlexLine、FlexItem;以及布局的帮助类FlexboxHelper。
  • FlexboxLayout实现:自定义ViewGroup实现,即FlexboxLayout。
  • RecyclerView支持实现:主要通过扩展RecyclerView.LayoutManager,即FlexboxLayoutManager实现。

公共接口/逻辑部分

公共接口/逻辑部分包括了FlexboxLayout和RecyclerView的抽象接口容器FlexContainer,容器Layout的属性、容器Layout的组成,以及帮助类。在了解FlexContainer之前,我们需要先来了解布局的属性。

Layout属性

1. FlexDirection

FlexboxLayout提出了两个概念:主轴(main axis)和交叉轴(cross axis),主轴和交叉轴相互垂直。FlexDirection决定主轴的方向,共有四个取值。

public @interface FlexDirection {
    /**
     * 主轴方向:水平,从左到右排列子元素
     * 交叉轴方向:垂直,从上到下
     */
    int ROW = 0;
    /**
     * 主轴方向:水平,从右到左排列子元素
     * 交叉轴方向:垂直,从上到下
     */
    int ROW_REVERSE = 1;
    /**
     * 主轴方向:垂直,从上到下排列子元素
     * 交叉轴方向:水平,从左到右
     */
    int COLUMN = 2;
    /**
     * 主轴方向:垂直,从下到上排列子元素
     * 交叉轴方向:垂直,从左到右
     */
    int COLUMN_REVERSE = 3;
}

2. JustifyContent

JustifyContent决定主轴方向上子元素的对齐方式,共有五个取值。

public @interface JustifyContent {
    int FLEX_START = 0; //主轴的起点对齐
    int FLEX_END = 1; //主轴的终点对齐
    int CENTER = 2;  //居中对齐
    int SPACE_BETWEEN = 3; //主轴起点对齐,剩余空间均分用于子元素间的间隔
    int SPACE_AROUND = 4;  // 子元素间
}

举个例子,当FlexDirection=ROW, FlexWrap=NOWRAP的时候,如下:

justify-content

3. FlexWrap

FlexWrap决定是否自动换行和换行的方式,有三种取值。

public @interface FlexWrap {
    int NOWRAP = 0;  //所有元素排列一行
    int WRAP = 1;    //多行显示,自动换行
    int WRAP_REVERSE = 2;  //多行显示,自动换行,但反向(逆交叉轴方向)排列元素
}

4. AlignItems

AlignItems决定一行子元素之间沿着交叉轴(cross axis)的对齐方式,共有5种。

public @interface AlignItems {
    int FLEX_START = 0;  // 交叉轴的起点对齐
    int FLEX_END = 1;    // 交叉轴的终点对齐
    int CENTER = 2;      // 交叉轴居中对齐
    int BASELINE = 3;    // 文本的基线对齐
    int STRETCH = 4;     // 拉伸占满整个交叉轴
}

举个例子,当FlexDirection=ROW, FlexWrap=NOWRAP的时候,如下:

align-item

5. AlignContent

AlignContent决定容器内多根轴线的对齐方式,如果只有一行,则该属性不生效,有6种取值。

public @interface AlignContent {
    int FLEX_START = 0;  // 交叉轴的起点对齐
    int FLEX_END = 1;   //交叉轴的终点对齐
    int CENTER = 2;    // 交叉轴居中对齐
    int SPACE_BETWEEN = 3;  // 轴线之间的间隔均等
    int SPACE_AROUND = 4;   // 每根轴线两侧的间隔都相等
    int STRETCH = 5;  //轴线拉伸沾满整个交叉轴
}

举个例子,当FlexDirection=ROW, FlexWrap=WRAP的时候,如下:

align-content

Layout的组成

FlexContainer容器里有多个FlexLine轴线,每个FlexLine由多个FlexItem组成。针对于每个子元素FlexItem来说,也有自己的一系列属性,包括以下几种:

  • order:子元素排列顺序,数字越大排列越靠后
  • alignSelf:与容器布局的alignItems一样,针对单个子元素,赋值后覆盖容器的alignItems属性
  • flexBasisPercent:宽/高的百分比
  • flexGrow: 子元素的放大比例
  • flexShrink:子元素的压缩比例
  • height:宽
  • width:高
  • maxWidth/minWidth:最大/最小的宽度
  • maxHeight/minHeight:最大/最小的高度
  • wrapBefore:强制进行换行
  • marginStart/marginEnd/marginLeft/marginRight/marginTop/marginBottom: 边距大小

FlexContainer通用容器接口

以上,由容器的组成FlexLine、FlexItem和容器的属性就构成了整个通用的可伸缩容器(FlexContainer),除此之外,FlexContainer作为FlexboxLayout和FlexboxLayoutManager的通用接口,也定义了一些相同的行为函数,包括addView()removeViewAt()getFlexItemAt()等,整个结构如下:

flexcontainer

FlexboxLayout的实现过程

public class FlexboxLayout extends ViewGroup implements FlexContainer{
  ...
}

FlexboxLayout本质上是一个ViewGroup,自定义ViewGroup的实现无非是三个重要的环节:Measure、Layout、Draw,分别对应onMeasure()onLayout()onDraw()三个函数。下面分别通过这三个函数进行整个绘制流程的分析。

onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    //保存每个子元素的排列顺序
    if (mOrderCache == null) {
        mOrderCache = new SparseIntArray(getChildCount());
    }
    //如果测量子元素的顺序order发生了改变,那么久重新进行赋值
    if (mFlexboxHelper.isOrderChangedFromLastMeasurement(mOrderCache)) {
        mReorderedIndices = mFlexboxHelper.createReorderedIndices(mOrderCache);
    }

    //根据FlexDirection的方向分别进行测量
    switch (mFlexDirection) {
        case FlexDirection.ROW:
        case FlexDirection.ROW_REVERSE:
            //ROW ROW_REVERSE横向测量
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
            break;
        case FlexDirection.COLUMN:
        case FlexDirection.COLUMN_REVERSE:
            //COLUMN COLUMN_REVERSE纵向测量
            measureVertical(widthMeasureSpec, heightMeasureSpec);
            break;
        default:
            throw new IllegalStateException(
                    "Invalid value for the flex direction is set: " + mFlexDirection);
    }
}

这里因为每个子元素都有自己的排列顺序值,所以需要保存好子元素的顺序值,然后根据顺序进行逐一测量。然后,通过FlexDirection来分别进行横向或者纵向的测量。

这里,我们只从横向分析,即measureHorizontal(),纵向测量过程相同。

private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
    //测量前清空所有内容
    mFlexLines.clear();
    // 计算整个容器中需要有多少根轴线
    // 策略: 1. 如果容器的FlexWrap为NOWRAP时,返回1
    //       2. 如果FlexWrap为WRAP/WRAP_REVERSE时,遍历计算每个子元素的宽度,求和,当容器剩余的宽度不足以放置下一个
    //          子元素时(或者子元素wrapBefore为true),换行,行数+1。
    FlexboxHelper.FlexLinesResult flexLinesResult = mFlexboxHelper
            .calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec);
    mFlexLines = flexLinesResult.mFlexLines;

    // 确定主轴的大小size。遍历每个轴线FlexLine的所有子元素,相加求得。
    //策略:1. 如果子元素的flexGrow/flexShrink属性非0,即需要扩展或收缩时,则需要对子元素进行扩展或收缩来确定大小。
    //     2. 除此之外,则默认为子元素的宽度大小。
    mFlexboxHelper.determineMainSize(widthMeasureSpec, heightMeasureSpec);

    //当AlignItems为BASELINE(文本为基线对齐)时,需要遍历每个轴线每个子元素,以得到每个轴线flexline的高度
    if (mAlignItems == AlignItems.BASELINE) {
        int viewIndex = 0;
        //每行轴线遍历
        for (FlexLine flexLine : mFlexLines) {
            int largestHeightInLine = Integer.MIN_VALUE;
            //轴线flexline内每个子元素遍历
            for (int i = viewIndex; i < viewIndex + flexLine.mItemCount; i++) {
                View child = getReorderedChildAt(i);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (mFlexWrap != FlexWrap.WRAP_REVERSE) {
                    int marginTop = flexLine.mMaxBaseline - child.getBaseline();
                    marginTop = Math.max(marginTop, lp.topMargin);
                    largestHeightInLine = Math.max(largestHeightInLine,
                            child.getHeight() + marginTop + lp.bottomMargin);
                } else {
                    int marginBottom = flexLine.mMaxBaseline - child.getMeasuredHeight() +
                            child.getBaseline();
                    marginBottom = Math.max(marginBottom, lp.bottomMargin);
                    largestHeightInLine = Math.max(largestHeightInLine,
                            child.getHeight() + lp.topMargin + marginBottom);
                }
            }
            flexLine.mCrossSize = largestHeightInLine;
            viewIndex += flexLine.mItemCount;
        }
    }
    //确定交叉轴的大小size。
    //策略:1. 仅当高度模式为MeasureSpec.EXACTLY时,根据AlignContent模式来确定大小。
    //     2. 否则使用所有轴线Flexline的交叉轴大小的总和。
    mFlexboxHelper.determineCrossSize(widthMeasureSpec, heightMeasureSpec,
            getPaddingTop() + getPaddingBottom());
    //确定完交叉轴的大小之后,如果容器的AlignItems=STRETCH或者子元素的AlignSelf=STRETCH时,进行拉伸
    mFlexboxHelper.stretchViews();
    //最后,根据测量出的主轴和交叉轴的大小来设置整个容器FlexboxLayout的width和height
    setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec,
            flexLinesResult.mChildState);
}

以上就是整个Measure的过程,大致流程是:

  1. 保存每个子元素的排列顺序
  2. 根据排列顺序进行每个子元素的测量,得到所需要的轴线FlexLine数
  3. 确定主轴的大小
  4. 确定交叉轴的大小
  5. 设置容器的宽高

onLayout()

onLayout()主要通过FlexboxLayout容器的JustifyContent属性来定位容器的位置,通过子元素的各种属性定位在容器内的位置。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //得到flexboxlayout的布局方向
    int layoutDirection = ViewCompat.getLayoutDirection(this);
    boolean isRtl;
    //根据不同的布局方向FlexDirection分别进行定位
    switch (mFlexDirection) {
        case FlexDirection.ROW:
            isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
            layoutHorizontal(isRtl, left, top, right, bottom);
            break;
        case FlexDirection.ROW_REVERSE:
            isRtl = layoutDirection != ViewCompat.LAYOUT_DIRECTION_RTL;
            layoutHorizontal(isRtl, left, top, right, bottom);
            break;
        case FlexDirection.COLUMN:
            isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
            if (mFlexWrap == FlexWrap.WRAP_REVERSE) {
                isRtl = !isRtl;
            }
            layoutVertical(isRtl, false, left, top, right, bottom);
            break;
        case FlexDirection.COLUMN_REVERSE:
            isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
            if (mFlexWrap == FlexWrap.WRAP_REVERSE) {
                isRtl = !isRtl;
            }
            layoutVertical(isRtl, true, left, top, right, bottom);
            break;
        default:
            throw new IllegalStateException("Invalid flex direction is set: " + mFlexDirection);
    }
}

这里针对FlexDirection = ROW情况来进行分析,即layoutHorizontal()。其余三种情况整体过程相似。

private void layoutHorizontal(boolean isRtl, int left, int top, int right, int bottom) {
    ...
    //遍历所有的轴线Flexline
    for (int i = 0, size = mFlexLines.size(); i < size; i++) {
        FlexLine flexLine = mFlexLines.get(i);
        if (hasDividerBeforeFlexLine(i)) {
            childBottom -= mDividerHorizontalHeight;
            childTop += mDividerHorizontalHeight;
        }
        float spaceBetweenItem = 0f;
        //通过JustfyContent得到该FlexLine距离子元素四周的距离
        switch (mJustifyContent) {
            case JustifyContent.FLEX_START:
                childLeft = paddingLeft;
                childRight = width - paddingRight;
                break;
            ...
        }
        //遍历每个flexLine的子元素,定位每个子元素的位置
        for (int j = 0; j < flexLine.mItemCount; j++) {
            ...
            //通过换行的方式来进行逐一子元素定位
            if (mFlexWrap == FlexWrap.WRAP_REVERSE) {
                    if (isRtl) {
                        mFlexboxHelper.layoutSingleChildHorizontal(child, flexLine,
                                Math.round(childRight) - child.getMeasuredWidth(),
                                childBottom - child.getMeasuredHeight(), Math.round(childRight),
                                childBottom);
                    } else {
                        mFlexboxHelper.layoutSingleChildHorizontal(...);
                    }
                } else {
                    if (isRtl) {
                        mFlexboxHelper.layoutSingleChildHorizontal(...);
                    } else {
                        mFlexboxHelper.layoutSingleChildHorizontal(...);
                    }
                }
            ...
    }
}

遍历每个FlexLine,得到距离子元素四周的距离,然后遍历每个FlexLine的子元素,定位每个子元素的位置,即mFlexboxHelper.layoutSingleChildHorizontal()

void layoutSingleChildHorizontal(View view, FlexLine flexLine, int left, int top, int right,
        int bottom) {
    FlexItem flexItem = (FlexItem) view.getLayoutParams();
    int alignItems = mFlexContainer.getAlignItems();
    //如果每个子元素的AlignSelf不是默认值,将值覆赋值给FlexLine的AlignItems
    if (flexItem.getAlignSelf() != AlignSelf.AUTO) {
        alignItems = flexItem.getAlignSelf();
    }
    int crossSize = flexLine.mCrossSize;
    //根据AlignItems来进行分类处理
    switch (alignItems) {
        case AlignItems.FLEX_START:
        case AlignItems.STRETCH:
            //分别设置子元素在FlexWrap为WRAP_REVERSE和WRAP情况下的位置
            if (mFlexContainer.getFlexWrap() != FlexWrap.WRAP_REVERSE) {
                view.layout(left, top + flexItem.getMarginTop(), right,
                        bottom + flexItem.getMarginTop());
            } else {
                view.layout(left, top - flexItem.getMarginBottom(), right,
                        bottom - flexItem.getMarginBottom());
            }
            break;
        case AlignItems.BASELINE:
            ...
            break;
        case AlignItems.FLEX_END:
            ...
            break;
        case AlignItems.CENTER:
            ...
            break;
    }
}

以上,完成了整个FlexboxLayout的Layout定位过程,遍历每个FlexLine和每个子元素FlexItem。

onDraw()

在完成了测量和定位的工作之后,最后进行了绘制的工作。

protected void onDraw(Canvas canvas) {
     ...
     int layoutDirection = ViewCompat.getLayoutDirection(this);
     boolean isRtl;
     boolean fromBottomToTop = false;
     //根据FlexDirection方向进行绘制
     switch (mFlexDirection) {
         case FlexDirection.ROW:
             isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
             if (mFlexWrap == FlexWrap.WRAP_REVERSE) {
                 fromBottomToTop = true;
             }
             //绘制过程中通过DividerMode属性判断是否需要绘制分隔线
             drawDividersHorizontal(canvas, isRtl, fromBottomToTop);
             break;
         case FlexDirection.ROW_REVERSE:
             ...
         case FlexDirection.COLUMN:
             ...
         case FlexDirection.COLUMN_REVERSE:
             ...
     }
 }

至此,就完成了整个FlexLayout的工作流程。

RecyclerView接入的实现过程

关于对RecyclerView的支持,主要是通过扩展RecyclerView.LayoutManager来实现,最关键的类是FlexboxLayoutManager。同时,对于item之间的间隔线,也对RecyclerView.ItemDecoration进行了扩展,即FlexboxItemDecoration

对于扩展LayoutManager来说,学习过RecyclerView的同学来说,最重要的也是三个环节:

  1. generateDefaultLayoutParams()
  2. onLayoutChildren()
  3. canScrollVertically()/canScrollHorizontally()

实现generateDefaultLayoutParams()

public RecyclerView.LayoutParams generateDefaultLayoutParams() {
	return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public static class LayoutParams extends RecyclerView.LayoutParams implements FlexItem {...}

我们看到,LayoutParams继承自RecyclerView.LayoutParams,并且实现了FlexItem接口,让每个子元素(即列表项)都具有其AlignSelf、flexGrow、flexShrink等通用属性。

实现onLayoutChildren()

onLayoutChildren()方法是LayoutManager的入口,在RecyclerView初始化和数据源改变的时候都会被调用。它的整个功能类似于上述FlexboxLayout(ViewGroup)的onLayout(),实现定位的工作。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 定位策略:
    // 1. 找到锚点坐标和锚点位置。如果没有找到,坐标从零开始
    // 2. 从锚定位置到可见区域,计算需要填充的所有轴线flexlins
    // 3. 从锚点位置填充到终点(end)位置
    // 4. 从锚点位置向布到起始(start)位置

    mRecycler = recycler;
    mState = state;
    int childCount = state.getItemCount();
    if (childCount == 0 && state.isPreLayout()) {
        return;
    }
    //处理容器的FlexDirection
    resolveLayoutDirection();
    //当FlexWrap != FlexWrap.NOWRAP时,需要对垂直滚动和横向滚动进行处理
    ensureOrientationHelper();
    //当进行填空时,短暂保存Layout的状态值
    ensureLayoutState();
    //缓存Item的MeasureSpec,用一个64位的long类型表示,前32位表示height,后32位拜师width。
    mFlexboxHelper.ensureMeasureSpecCache(childCount);
    //缓存每个Item的width和height,用一个64位的long类型表示,前32位表示height,后32位拜师width
    mFlexboxHelper.ensureMeasuredSizeCache(childCount);
    //将view索引映射到每个FlexLine上,对应mIndexToFlexLine,为int数组。
    //举个例子:FlexLine(0): itemCount 3 ,FlexLine(1): itemCount 2。则数组为[0, 0, 0, 1, 1, ...]
    mFlexboxHelper.ensureIndexToFlexLine(childCount);
    mLayoutState.mShouldRecycle = false;
    //初始化锚点对象
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) {
        mAnchorInfo.reset();
        updateAnchorInfoForLayout(state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    }
    detachAndScrapAttachedViews(recycler);
    //按照锚点的布局方向进行处理
    if (mAnchorInfo.mLayoutFromEnd) {
        updateLayoutStateToFillStart(mAnchorInfo, false, true);
    } else {
        updateLayoutStateToFillEnd(mAnchorInfo, false, true);
    }
    //更新所有的FlexLine信息,包括了每根FlexLine的主轴交叉轴大小和子元素数量
    updateFlexLines(childCount);
    int startOffset;
    int endOffset;
    //开始填充
    if (mAnchorInfo.mLayoutFromEnd) {
        // 从锚点位置填充到终点(end)位置
        int filledToEnd = fill(recycler, state, mLayoutState);
        startOffset = mLayoutState.mOffset;
        updateLayoutStateToFillEnd(mAnchorInfo, true, false);
        // 从锚点位置填充到起始(Start)位置
        int filledToStart = fill(recycler, state, mLayoutState);
        endOffset = mLayoutState.mOffset;
    } else {
        // 从锚点位置填充到起始(start)位置
        int filledToEnd = fill(recycler, state, mLayoutState);
        endOffset = mLayoutState.mOffset;
        updateLayoutStateToFillStart(mAnchorInfo, true, false);
        // 从锚点位置填充到终点(end)位置
        int filledToStart = fill(recycler, state, mLayoutState);
        startOffset = mLayoutState.mOffset;
    }
    ...
}

这里我们需要重点理解定位的策略。关于布局的锚点信息mAnchorInfo,是这样解释的:

A class that holds the information about an anchor position like from what pixels layout should start.

也就是说,锚点用来保存布局的方向mLayoutFromEnd、列表项的索引mPosition,轴线在容器中的索引位置mFlexLinePosition,起始绘制偏移量mCoordinate等信息。

然后需要做的就是,以锚点的位置为起点,分别向终点和起始的方向进行填充子元素(列表项)。

举个例子,当FlexDirection=ROW,FlexWrap=WRAP时,则主轴为水平方向,沿着交叉轴滚动。此时,mLayoutFromEnd = fasle,即布局方向从起始点开始。此时,填充的示意图如下图:

anchor-fill

由于mIndexToFlexLine保存了每个FlexLine的子元素数量和在整个容器中的位置,所以在填充的过程中每行能容纳的子元素确定,如图主轴为水平方向,则填充方向为往起始方向(start)填充最左上角,往终点方向(end)填充值最右下角。虽然填充方向为start、end两个方向同时进行,但是一般来说,初始的锚点都在起始点或者终点。

在此过程中,由fill()进行FlexItem的具体填充过程。

填充的过程,主要是填充由layoutState定义的剩余空间mAvailable。整个过程除了RecyclerView默认的LinearLayoutManager相似的fill部分之外,还需要特别考虑两个滚动方向:

  1. 沿着交叉轴:当FlexWrap设置为WRAP或WRAP_REVERSE时,布局需要沿着交叉轴滚动;
  2. 沿着主轴: 当FlexWrap设置为NOWRAP时,如果有溢出的子元素,即使FlexDirection为ROW,也需要沿着主轴滚动。
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState) {
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        //当屏幕可见没有剩余空间时,添加可滚动的偏移
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    int start = layoutState.mAvailable;
    int remainingSpace = layoutState.mAvailable;
    int consumed = 0;
    //填充所有的FlexItem
    while ((remainingSpace > 0 || mLayoutState.mInfinite) &&
            layoutState.hasMore(state, mFlexLines)) {
        FlexLine flexLine = mFlexLines.get(layoutState.mFlexLinePosition);
        layoutState.mPosition = flexLine.mFirstIndex;
        consumed += layoutFlexLine(flexLine, layoutState);
        layoutState.mOffset += flexLine.getCrossSize() * layoutState.mLayoutDirection;
        remainingSpace -= flexLine.getCrossSize();
    }
    layoutState.mAvailable -= consumed;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        layoutState.mScrollingOffset += consumed;
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    return start - layoutState.mAvailable;
}

实现canScrollVertically()/canScrollHorizontally()

最后,需要扩展canScrollVertically()/canScrollHorizontally()来实现滚动功能.

@Override
public boolean canScrollHorizontally() {
    return !isMainAxisDirectionHorizontal();
}

@Override
public boolean canScrollVertically() {
    return isMainAxisDirectionHorizontal();
}

这里只允许水平或垂直一个方向进行滚动。

  1. 当主轴方向水平时,沿交叉轴(垂直)滚动
  2. 当主轴方向垂直时,沿交叉轴(水平)滚动
public boolean isMainAxisDirectionHorizontal() {
    return mFlexDirection == FlexDirection.ROW || mFlexDirection == FlexDirection.ROW_REVERSE;
}

FlexboxLayout VS FlexboxLayoutManager(RecyclerView)

接下来,我们对FlexboxLayout和FlexboxLayoutManager(RecyclerView)进行对比。

Attribute / Feature FlexboxLayout FlexboxLayoutManager (RecyclerView)
flexDirection Check Check
flexWrap Check Check (except wrap_reverse)
justifyContent Check Check
alignItems Check Check
alignContent Check -
layout_order Check -
layout_flexGrow Check Check
layout_flexShrink Check Check
layout_alignSelf Check Check
layout_flexBasisPercent Check Check
layout_(min/max)Width Check Check
layout_(min/max)Height Check Check
layout_wrapBefore Check Check
Divider Check Check
View recycling - Check
Scrolling *1 Check

我们可以看到,FlexboxLayoutManager实现了大部分FlexboxLayout的属性,但是由于RecyclerView的某些特性,FlexboxLayoutManager没有实现。比如:

  1. RecyclerView通过addView()进行添加列表项时,没必要设置order属性。
  2. RecyclerView不支持AlignContent多轴线对齐方式,若为FLEX_END,并且FlexWrap=WRAP,则需要从底部垂直往上滚动,这显然是不合理的。同理于flexWrap为WRAP_REVERSE的情况。

另一方面,FlexLayoutManager(RecyclerView)有独特的优势,即支持View回收和滚动。

对于View回收,FlexboxLayout本质上ViewGroup,进行了Measure、Layout、Draw的操作,并不能支持视图回收;而FlexboxLayoutManager基于RecyclerView的特性,在ViewHolder视图复用、视图缓存回收等方面天然支持。

对于页面滚动,*1表示FlexboxLayout可以通过包装ScrollView来实现,但是因为FlexboxLayout不支持view回收,大量的子元素view会占据极大的内存空间。而FlexboxLayoutManager基于RecyclerView,天然支持滚动。

综上,我们可以得到FlexboxLayout和FlexLayoutManager(RecyclerView)不同的使用场景:

  • FlexboxLayout: 适合少量的view,比如标签云,不考虑大量view的情况。
  • FlexLayoutManager(RecyclerView): 适合大量的view,比如图片流,视图回收能保证其良好的性能。

参考资料:


知识共享许可协议
本文采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,转载请务必注明作者以及原文出处链接。