NumberPicker源码分析+自定义View简单实现NumberPicker

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.

简单来说就是可滑动选择数字的控件,具体的效果如下:
《NumberPicker源码分析+自定义View简单实现NumberPicker》

使用

        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

先来上实现的效果:
《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;
    }

}
    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/a199581/article/details/79839137
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞