NumberPicker分析(二)
NumberPicker
继承自LinearLayout
。一般而言,无论是继承自View
,还是继承自ViewGroup
,必然会经过如下的几个阶段:
onMeasure
onLayout
onDraw
onMeasure
在onMeasure
方法测量当前控件大小,为正式布局提供建议。测量完成以后,要通过 setMeasuredDimen ion(int,int)
函数设置给系统
在NumberPicker
中的构造方法中,通过调试断点可知(结合上一节中的Widget.Holo.NumberPicker
的style
也可知),初始化时:
mMinHeight
没有设置值(默认为-1
, 表示SIZE_UNSPECIFIED
)mMaxHeight
有设置值(在本人测试机器上为495
)mMinWidth
有设置值(在本人测试机器上为176
)mMaxWidth
没有设置值(默认为-1
, 表示SIZE_UNSPECIFIED
)
之后在tryComputeMaxWidth()
方法中,给mMaxWidth
又设置了值,mMaxWidth = mMinWidth
此时mMaxWidth
和 mMaxHeight
就都有值了
NumberPicker
的onMeasure
方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mHasSelectorWheel) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// Try greedily to fit the max width and height.
/**
* 1.widthMeasureSpec是父View传递给当前View的一个建议值
* 2.mMaxWidth = 176
*/
final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
/**
* 1.heightMeasureSpec是父View传递给当前View的一个建议值
* 2.mMaxHeight = 495
*/
final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
// Flag if we are measured with width or height less than the respective min.
// 在这里还需要考虑最小值限制的问题
/**
* 1.mMinWidth = 176, getMeasuredWidth() = 176
* 2.按resolveSizeAndStateRespectingMinSize方法,需要创建一个新的widthMeasureSpec
*/
final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
widthMeasureSpec);
/**
* 1.mMinHeight = -1, getMeasuredWidth() = 495
* 2.按resolveSizeAndStateRespectingMinSize方法,直接返回heightMeasureSpec
*/
final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
makeMeasureSpec
方法和resolveSizeAndStateRespectingMinSize
方法,定义如下:
/**
* Makes a measure spec that tries greedily to use the max value.
*
* @param measureSpec The measure spec.
* @param maxSize The max value for the size.
* @return A measure spec greedily imposing the max size.
*/
private int makeMeasureSpec(int measureSpec, int maxSize) {
if (maxSize == SIZE_UNSPECIFIED) {
return measureSpec;
}
final int size = MeasureSpec.getSize(measureSpec);
final int mode = MeasureSpec.getMode(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return measureSpec;
case MeasureSpec.AT_MOST:
return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
case MeasureSpec.UNSPECIFIED:
return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
default:
throw new IllegalArgumentException("Unknown measure mode: " + mode);
}
}
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Tries to respect the min size, unless a different size
* is imposed by the constraints.
*
* @param minSize The minimal desired size.
* @param measuredSize The currently measured size.
* @param measureSpec The current measure spec.
* @return The resolved size and state.
*/
private int resolveSizeAndStateRespectingMinSize(
int minSize, int measuredSize, int measureSpec) {
if (minSize != SIZE_UNSPECIFIED) {
final int desiredWidth = Math.max(minSize, measuredSize);
return resolveSizeAndState(desiredWidth, measureSpec, 0);
} else {
return measuredSize;
}
}
在layout布局中,设置layout_width="wrap_content"
, layout_height="wrap_content"
,相当于是 MeasureSpec.AT_MOST
,所以最终调用的都是如下的方法:
MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY)
onLayout
onLayout
布局所有的子控件
NumberPicker
的onLayout
方法主要用来:
- 居中布局
EditText
(mInputText
) - 初始化选择轮 -
initializeSelectorWheel
- 初始换边缘Fading效果 -
initializeFadingEdges
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (!mHasSelectorWheel) {
super.onLayout(changed, left, top, right, bottom);
return;
}
final int msrdWdth = getMeasuredWidth();
final int msrdHght = getMeasuredHeight();
// Input text centered horizontally.
final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
if (changed) {
// need to do all this when we know our size
initializeSelectorWheel();
initializeFadingEdges();
//分割线位置
mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
- mSelectionDividerHeight;
mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
+ mSelectionDividersDistance;
}
}
initializeSelectorWheel
方法
initializeSelectorWheel
方法中,计算和初始化了许多值
其中的关系,可以表示如下:
并且在这里设置了mCurrentScrollOffset = mInitialScrollOffset;
onDraw
在NumberPicker
的构造方法中有如下的设置:
// By default Linearlayout that we extend is not drawn. This is
// its draw() method is not called but dispatchDraw() is called
// directly (see ViewGroup.drawChild()). However, this class uses
// the fading edge effect implemented by View and we need our
// draw() method to be called. Therefore, we declare we will draw.
setWillNotDraw(!mHasSelectorWheel);
从前面的文章可知,mHasSelectorWheel
为ture
,所以这里相当于是setWillNotDraw(false)
setWillNotDraw
public void setWillNotDraw (boolean willNotDraw)
If this view doesn’t do any drawing on its own, set this flag to allow further optimizations. By default, this flag is not set on View, but could be set on some View subclasses such as ViewGroup. Typically, if you override
onDraw(android.graphics.Canvas)
you should clear this flag.
如果这个视图自己不做任何绘图,设置这个标志以允许进一步优化。 默认情况下,此标志未在View
上设置,但可以在某些 View 子类(例如ViewGroup
)上设置。 通常,如果您覆盖onDraw(android.graphics.Canvas)
您应该清除此标志。
设置setWillNotDraw(false)
后,ViewGroup
的onDraw
方法就可以被调用了
具体的onDraw
方法如下:
@Override
protected void onDraw(Canvas canvas) {
....
float x = (mRight - mLeft) / 2;
float y = mCurrentScrollOffset;
// draw the virtual buttons pressed state if needed
....
// draw the selector wheel
int[] selectorIndices = mSelectorIndices;
for (int i = 0; i < selectorIndices.length; i++) {
int selectorIndex = selectorIndices[i];
String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
// Do not draw the middle item if input is visible since the input
// is shown only if the wheel is static and it covers the middle
// item. Otherwise, if the user starts editing the text via the
// IME he may see a dimmed version of the old value intermixed
// with the new one.
if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
(i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
//绘制文字
canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
}
y += mSelectorElementHeight;
}
// draw the selection dividers
// 绘制分割线
if (showSelectorWheel && mSelectionDivider != null) {
// draw the top divider
int topOfTopDivider = mTopSelectionDividerTop;
int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
mSelectionDivider.draw(canvas);
// draw the bottom divider
int bottomOfBottomDivider = mBottomSelectionDividerBottom;
int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
mSelectionDivider.draw(canvas);
}
}
概括来说就是遍历mSelectorIndices
(第一次值为[4, 0, 1]
),绘制text,绘制分割线。注意这里的y
:
float y = mCurrentScrollOffset; //初始值
...
y += mSelectorElementHeight; //每次循环后
结合上面的示意图,即可明白其中的含义