Spinner类setSelection执行流程源码解析

Spinner.setSelection(int)方法的执行顺序大致如下步骤所示

  1. AbsSpinner.setSelection(int)
  2. AdapterView.setNextSelectedPositionInt(int)
  3. Spinner.onMeasure(int,int)
  4. Spinner.onLayout(boolean,int,int,int,int)
  5. Spinner.layout(int,boolean)

好了,我们来跟着源码一步步来揭开setSelection方法的面纱。
Step1

@Override
public void setSelection(int position) {
    //设置选中的postion
    setNextSelectedPositionInt(position);
    //重新计算布局
    requestLayout();
    //重绘界面
    invalidate();
}

源码很简单,三步走,1、设置position,方法名中加了Next是因为position的状态只有在layout的时候才会真正的被更新。2、重新measure、layout布局。3、重绘布局。

Step2
让我们看看setNextSelectedPositionInt方法做了什么。

void setNextSelectedPositionInt(int position) {
    //将position赋值给mNextSelectedPosition变量同时获取相应的itemId
    mNextSelectedPosition = position;
    mNextSelectedRowId = getItemIdAtPosition(position);
    //以下代码对于Spinner没有用处,暂时不管
    if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
        mSyncPosition = position;
        mSyncRowId = mNextSelectedRowId;
    }
}

Step3

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
        final int measuredWidth = getMeasuredWidth();
        setMeasuredDimension(Math.min(Math.max(measuredWidth,
                measureContentWidth(getAdapter(), getBackground())),
                MeasureSpec.getSize(widthMeasureSpec)),
                getMeasuredHeight());
    }
}

该方法首先调用了super.onMeasure(int,int),也就是AbsSpinner.onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize;
    int heightSize;
    // mSpinnerPadding为一个Rect实例用来记录padding值,mSelection*Padding的值为0,因此mSpinnerPadding所记录的就是Spinner本身的padding值
    mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft
            : mSelectionLeftPadding;
    mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop
            : mSelectionTopPadding;
    mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight
            : mSelectionRightPadding;
    mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom
            : mSelectionBottomPadding;

    // mDataChanged是用来记录数据源是否改变,此处为false
    if (mDataChanged) {
        handleDataChanged();
    }

    int preferredHeight = 0;
    int preferredWidth = 0;
    boolean needsMeasuring = true;

    // 此处获得的selectedPosition就是Step2中的mNextSelectedPosition
    int selectedPosition = getSelectedItemPosition();
    if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {

        // mRecycler中保存着所有已经被measure的view
        View view = mRecycler.get(selectedPosition);
        if (view == null) {
            // 调用adapter的getView方法获取一个view
            view = mAdapter.getView(selectedPosition, null, this);
        // 暂时不明白干什么用的,不过影响不大
            if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
                view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
            }
        }

        if (view != null) {
            // 设置缓存
            mRecycler.put(selectedPosition, view);
        // 如果LayoutParams为null那么生成一个默认布局参数,mBlockLayoutRequests如果使能,那么调用requestLayout会无效
            if (view.getLayoutParams() == null) {
                mBlockLayoutRequests = true;
                view.setLayoutParams(generateDefaultLayoutParams());
                mBlockLayoutRequests = false;
            }
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            // 根据child的尺寸与padding值计算尺寸
            preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
            preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;

            needsMeasuring = false;
        }
    }

    if (needsMeasuring) {
        // 如果view为null那么直接使用padding值
        preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
        }
    }
    // 与最小尺寸比较,取较大值
    preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
    preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
    // 根据mode解析尺寸,state状态此处暂时不管
    heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
    widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
    // 设置尺寸
    setMeasuredDimension(widthSize, heightSize);
    //保存measureSpec,这两个东东在接下来的layout过程中会用到
    mHeightMeasureSpec = heightMeasureSpec;
    mWidthMeasureSpec = widthMeasureSpec;
}

回到Spinner.onMeasure(int, int)方法中来,super.onMeasure之后还有几句代码

// mPopup是一个接口,Spinner通过该接口统一了Dialog和DropDown这两种显示模式
if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
    final int measuredWidth = getMeasuredWidth();
    // 高度保持不变,对宽度重新处理了一下
    setMeasuredDimension(Math.min(Math.max(measuredWidth,
            measureContentWidth(getAdapter(), getBackground())),
            MeasureSpec.getSize(widthMeasureSpec)),
            getMeasuredHeight());
}

好吧,我们来看一下Math.min…这么一长串东西里面到底是些什么
Math.min中有两个参数
参数1.Math.max(measuredWidth,measureContentWidth(getAdapter(), getBackground()))
参数2.MeasureSpec.getSize(widthMeasureSpec))

参数2很好理解,获取了父布局给定的宽度,我们看看参数1,一个Math.max函数,也包含两个参数
参数1.measuredWidth
参数2.measureContentWidth(getAdapter(), getBackground())
参数1为AbsSpinner.onMeasure(int,int)中设置好的宽度,我们看看measureContentWidth干了什么

int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
    if (adapter == null) {
        return 0;
    }

    int width = 0;
    View itemView = null;
    int itemType = 0;
    // 不对子视图大小进行约束
    final int widthMeasureSpec =
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    final int heightMeasureSpec =
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

    // 下面四句代码主要是为了计算for循环中的start和end值,使end-start的值尽量靠近MAX_ITEMS_MEASURED,MAX_ITEMS_MEASURED默认为15
    int start = Math.max(0, getSelectedItemPosition());
    final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
    final int count = end - start;
    start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
    // 此for循环主要是为了尽可能多的计算子视图以获取最大的宽度
    for (int i = start; i < end; i++) {
        final int positionType = adapter.getItemViewType(i);
        if (positionType != itemType) {
            itemType = positionType;
            itemView = null;
        }
        itemView = adapter.getView(i, itemView, this);
        if (itemView.getLayoutParams() == null) {
            itemView.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT));
        }
        itemView.measure(widthMeasureSpec, heightMeasureSpec);
        width = Math.max(width, itemView.getMeasuredWidth());
    }

    // 如果存在背景图片,那么将背景图的padding值计算进宽度中
    if (background != null) {
        background.getPadding(mTempRect);
        width += mTempRect.left + mTempRect.right;
    }

    return width;
}

至此,measure的过程就结束了。

Step4

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 仅仅调用getHeight方法获取了高度保存在mLayoutHeight变量中
    super.onLayout(changed, l, t, r, b);
    mInLayout = true;
    layout(0, false);
    mInLayout = false;
}

mInLayout变量会在另一篇文章中进行讨论,暂时不去理会。我们先看看layout(innt,boolean)方法

@Override
void layout(int delta, boolean animate) {
    int childrenLeft = mSpinnerPadding.left;
    // 注:mRight、mLeft的值是在layout的过程中被系统通过调用setRight和setLeft进行设置的
    int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
    // 此处mDataChanged为false
    if (mDataChanged) {
        handleDataChanged();
    }

    // itemCount为0那么移除所有view,然后返回
    if (mItemCount == 0) {
        resetList();
        return;
    }
    // 将mNextSelectedPosition保存为mSelectedPosition
    if (mNextSelectedPosition >= 0) {
        setSelectedPositionInt(mNextSelectedPosition);
    }
    // 缓存在mRecycler中,以便在之后的measure和下面的makeView方法中使用
    recycleAllViews();

    // 该方法其实就是ViewGroup中removeAllViews()方法的实现,只不过removeAllViews()多调用了requestLayout()和invalidate()方法
    removeAllViewsInLayout();

    // 将mSelectedPosition设置为首位置
    mFirstPosition = mSelectedPosition;

    if (mAdapter != null) {
    // 创建视图,makeView其实干了好多事情,后面再细说
        View sel = makeView(mSelectedPosition, true);
        int width = sel.getMeasuredWidth();
        int selectedOffset = childrenLeft;
        // 根据layoutDirection获取绝对gravity
        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
        // 根据gravity计算横向偏移量
        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
                break;
            case Gravity.RIGHT:
                selectedOffset = childrenLeft + childrenWidth - width;
                break;
        }
        sel.offsetLeftAndRight(selectedOffset);
    }

    // 清空所有视图缓存
    mRecycler.clear();
    // 刷新界面
    invalidate();
    // 检查selection是否改变
    checkSelectionChanged();
    // 重新初始化一些状态
    mDataChanged = false;
    mNeedSync = false;
    setNextSelectedPositionInt(mSelectedPosition);
}

至此,整个setSelection方法的大致流程就已经走完了,想不到,仅仅设置一个selection就做了这么多事情。

上面提到makeView方法,现在来看看这个方法到底干了些什么。源码如下:

private View makeView(int position, boolean addChild) {
    View child;
    // 如果数据源未改变,那么优先从mRecycler中获取视图。
    if (!mDataChanged) {
        child = mRecycler.get(position);
        if (child != null) {
            setUpChild(child, addChild);
            return child;
        }
    }

    // mRecycler返回null,那么调用adapter的getView方法生成一个view
    child = mAdapter.getView(position, null, this);

    setUpChild(child, addChild);

    return child;
}

makeView方法在返回之前都调用了setUpChild方法,我们再来看看这个方法

private void setUpChild(View child, boolean addChild) {

    // 获取LayoutParam,如果为null那么生成一个默认参数
    ViewGroup.LayoutParams lp = child.getLayoutParams();
    if (lp == null) {
        lp = generateDefaultLayoutParams();
    }
    // 如果addChild为true,那么将该view添加到当前的ViewGroup中。addViewInLayout为ViewGroup的一个方法,用以在layout过程中添加视图。
    if (addChild) {
        addViewInLayout(child, 0, lp);
    }

    // 设置child的selected以及enable状态
    child.setSelected(hasFocus());
    if (mDisableChildrenWhenDisabled) {
        child.setEnabled(isEnabled());
    }

    // 构造measure参数,这里使用到的mHeightMeasureSpec和mWidthMeasureSpec就是在AbsSpinner.onMeasure方法中保存的两个东东
    int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
            mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
    int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
            mSpinnerPadding.left + mSpinnerPadding.right, lp.width);

    // 计算child大小
    child.measure(childWidthSpec, childHeightSpec);

    int childLeft;
    int childRight;

    int childTop = mSpinnerPadding.top
            + ((getMeasuredHeight() - mSpinnerPadding.bottom -
                    mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
    int childBottom = childTop + child.getMeasuredHeight();

    int width = child.getMeasuredWidth();
    childLeft = 0;
    childRight = childLeft + width;
    // 计算视图位置
    child.layout(childLeft, childTop, childRight, childBottom);
}

总结:makeView做了四件事情,1.生成一个view;2.添加到Spinner中;3.计算view的大小;4.计算视图位置。几乎把一个视图的绘制流程走完了。

好了,setSelection的源码解析到这里就差不多了,接下来我会再扩展一下写一篇itemSelected回调的文章。

以上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值