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;
}
}