概述
NumberPicker介绍
A widget that enables the user to select a number from a predefined
range. There are two flavors of this widget and which one is presented
to the user depends on the current theme.
简单来说就是可滑动选择数字的控件,具体的效果如下:
使用
mNumberPicker = findViewById(R.id.system_number_picker);
mNumberPicker.setMaxValue(5);
mNumberPicker.setMinValue(1);
mNumberPicker.setWrapSelectorWheel(true);//是否循环显示
源码分析
因为NumberPicker的外表会因为Theme的不同而不同,因此我们可以不用特别关心初始化外观的那部分代码。
NumberPicker继承自LinearLayout,而它有需要重写onDraw方法,因此下面的这句代码是必须的:
// 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);
按着view的绘制流程来看源码,NumberPicker的并没有重写onMeasure,因此直接看onLayout方法:
@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();
//对中间的editext进行layout操作
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();//初始化显示的三个数字的位置信息,以备onDraw使用
initializeFadingEdges();//布局上方和下方的背景虚化的部分
//两根分割线的top和bottom位置
mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
- mSelectionDividerHeight;
mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
+ mSelectionDividersDistance;
}
}
onLayout方法里有一个mHasSelectorWheel
,他起什么作用呢?
Flag whether this widget has a selector wheel.
仅仅是一个标记位,具体的赋值是在NumberPicker的构造函数里:
mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
我们可以在这句代码前设个断点,然后debug,我的测试机是Android4.4的,通过debug发现这个值为true,因此他不会直接调用super.onLayout(changed, left, top, right, bottom);
,而是自己处理layout操作。
onLayout方法里调用了initializeSelectorWheel()
:
private void initializeSelectorWheel() {
initializeSelectorWheelIndices();
int[] selectorIndices = mSelectorIndices;
//根据文字的数量以及gap来计算所需布局的高
int totalTextHeight = selectorIndices.length * mTextSize;
float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
float textGapCount = selectorIndices.length;
mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
// Ensure that the middle item is positioned the same as the text in
// mInputText
//初始化了mInitialScrollOffset和mCurrentScrollOffset两个属性,
// 在scrollby和ondraw里绘制文字时使用,以达到轮子的效果。
int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
mInitialScrollOffset = editTextTextPosition
- (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
mCurrentScrollOffset = mInitialScrollOffset;
updateInputTextView();
}
mSelectorIndices是一个数组,private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
,再来看SELECTOR_WHEEL_ITEM_COUNT的值:private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
,这就很明显了,mSelectorIndices里面保存的就是在屏幕上显示的那三个数字,而initializeSelectorWheel()
的作用就是获取到屏幕显示的三个数字的位置信息,以备onDraw时使用。那接下来就去看onDraw方法。
@Override
protected void onDraw(Canvas canvas) {
其他代码...
final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true;
float x = (mRight - mLeft) / 2;
//记录当前滑动的距离,mCurrentScrollOffset会在onTouchEvent和scrollby里面会重新被赋值。
float y = mCurrentScrollOffset;
省略绘制上下两个imagebutton按下状态的代码...
//重要的代码:用来绘制显示的文字。他的y值就是当前滑动的偏移量,即:mCurrentScrollOffset
// 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;
}
省略绘制分割线的代码...
}
onDraw里的mCurrentScrollOffset,代表滑动的偏移量,当手指滑动NumberPicker的时候,他会被赋值。因此,onDraw方法的作用就是根据当前滑动偏移量来绘制屏幕上显示的三个数字,以及分割线等。
至此,绘制流程走完了,接下来我们关注下他是如何滑动的。看onTouchEvent
方法会发现,他是直接调用了scrollBy方法来滑动布局,而NumberPicker重写了scrollBy方法:
@Override
public void scrollBy(int x, int y) {
int[] selectorIndices = mSelectorIndices;
//mWrapSelectorWheel作用:如果为true,可循环显示所有的数字,如果为false,当向下或向上滑动到最后一个数字的时候,不可再向下或向上滑动。可以通过`setWrapSelectorWheel`试一试效果。
if (!mWrapSelectorWheel && y > 0
&& selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
mCurrentScrollOffset = mInitialScrollOffset;//mCurrentScrollOffset的赋值
return;
}
if (!mWrapSelectorWheel && y < 0
&& selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
mCurrentScrollOffset = mInitialScrollOffset;//mCurrentScrollOffset的赋值
return;
}
mCurrentScrollOffset += y;//mCurrentScrollOffset的赋值
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
mCurrentScrollOffset -= mSelectorElementHeight;
decrementSelectorIndices(selectorIndices);//向下滑动
setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);//设置NumberPicker的当前值。
if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
mCurrentScrollOffset = mInitialScrollOffset;//mCurrentScrollOffset的赋值
}
}
while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
mCurrentScrollOffset += mSelectorElementHeight;
incrementSelectorIndices(selectorIndices);//向上滑动
setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
mCurrentScrollOffset = mInitialScrollOffset;//mCurrentScrollOffset的赋值
}
}
}
把向下滑动的代码也贴出来:
private void decrementSelectorIndices(int[] selectorIndices) {
for (int i = selectorIndices.length - 1; i > 0; i--) {
selectorIndices[i] = selectorIndices[i - 1];
}
int nextScrollSelectorIndex = selectorIndices[1] - 1;
if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
nextScrollSelectorIndex = mMaxValue;
}
selectorIndices[0] = nextScrollSelectorIndex;
ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}
入参其实就是mSelectorIndices,即界面显示的那三个数字,把方法里的循环走一遍就会发现他的作用。向上滑动的代码基本差不多,这就不再贴出。对于NumberPicker的滑动操作,还有两个Scroller:
/**
* The {@link Scroller} responsible for flinging the selector.
*/
private final Scroller mFlingScroller;
/**
* The {@link Scroller} responsible for adjusting the selector.
*/
private final Scroller mAdjustScroller;
一个负责随手势滑动NumberPicker,一个负责滑动后的调整,既然有Scroller,那肯定需要重写computeScroll()
方法,computeScroll内部还是调用了上面说的scrollBy方法,这不再赘述。
总结下,无论要显示几个数,显示在屏幕上的数字只有三个,都是通过drawText方法绘制上去的,而这三个数字的位置随手势滑动的距离变化而变化。知道他的大体流程之后,我们可以自己实现一个简单的NumberPicker。
自定义View简单实现NumberPicker
先来上实现的效果:
/**
* by shenmingliang1
* 2018.03.28 17:44.
*/
public class MyNumberPicker extends LinearLayout {
private static final int DEFAULT_HEIGHT_OF_ITEM = 50;
private static final int TEXT_GAP = 20;
private int mCurrentScrollOffset;
private int mInitOffset;
private int mWidth;
private int[] mNumbers = new int[]{1, 2, 3};
private int mTouchSlop;
private Paint mTextPaint = new Paint();
private Paint mDividerPaint = new Paint();
public MyNumberPicker(Context context) {
this(context, null);
}
public MyNumberPicker(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyNumberPicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
setOrientation(VERTICAL);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setTextSize(50);
mDividerPaint.setColor(Color.BLUE);
mDividerPaint.setStrokeWidth(5);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT_OF_ITEM * 3 + TEXT_GAP * 3);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mWidth = getMeasuredWidth();
mInitOffset = mCurrentScrollOffset = DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP;
setVerticalFadingEdgeEnabled(true);
setFadingEdgeLength((DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP) * 2);
}
/***
*必须要重写这个方法,否则边缘虚化的效果出不来。
* @return 虚化的力度
*/
@Override
protected float getTopFadingEdgeStrength() {
return 1f;
}
/***
*必须要重写这个方法,否则边缘虚化的效果出不来。
* @return 虚化的力度
*/
@Override
protected float getBottomFadingEdgeStrength() {
return 1f;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int x = mWidth / 2;
int y = mCurrentScrollOffset - TEXT_GAP;
//不断将数字绘制出来
for (int i = 0; i < mNumbers.length; i++) {
canvas.drawText(String.valueOf(mNumbers[i]), x, y, mTextPaint);
y += DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP;
}
//绘制两根分割线
canvas.drawLine(0, DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP, mWidth,
DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP, mDividerPaint);
canvas.drawLine(0, DEFAULT_HEIGHT_OF_ITEM * 2 + 2 * TEXT_GAP,
mWidth, DEFAULT_HEIGHT_OF_ITEM * 2 + 2 * TEXT_GAP, mDividerPaint);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
private int mLastY = 0;
private int mLastX = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
scrollBy(0, y - mLastY);
mCurrentScrollOffset += (y - mLastY);
invalidate();
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
default:
break;
}
return true;
}
@Override
public void scrollBy(int x, int y) {
if (y > 0) {
if (mCurrentScrollOffset - mInitOffset > DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP) {
mCurrentScrollOffset = mInitOffset;
decrementNumbers();
}
} else {
if (mInitOffset - mCurrentScrollOffset > DEFAULT_HEIGHT_OF_ITEM + TEXT_GAP) {
mCurrentScrollOffset = mInitOffset;
incrementNumbers();
}
}
invalidate();
}
private void incrementNumbers() {
int[] num = mNumbers;
int first = num[0];
for (int i = 0; i < num.length - 1; i++) {
num[i] = num[i + 1];
}
num[num.length - 1] = first;
mNumbers = num;
}
private void decrementNumbers() {
int[] num = mNumbers;
int next = num[mNumbers.length - 1];
for (int i = num.length - 1; i > 0; i--) {
num[i] = num[i - 1];
}
num[0] = next;
mNumbers = num;
}
}
最后
以上就是如意猫咪为你收集整理的NumberPicker源码分析+自定义View简单实现NumberPickerNumberPicker介绍使用源码分析自定义View简单实现NumberPicker的全部内容,希望文章能够帮你解决NumberPicker源码分析+自定义View简单实现NumberPickerNumberPicker介绍使用源码分析自定义View简单实现NumberPicker所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复