> 文章列表 > NumberPicker分析(三)

NumberPicker分析(三)

NumberPicker分析(三)

NumberPicker分析(三)

这一节主要用来分析NumberPicker的事件处理及滚动
NumberPicker继承自LinearLayout,是一个ViewGroupViewGroup事件处理的顺序大致如下:

  1. dispatchTouchEvent
  2. onInterceptTouchEvent
  3. onTouchEvent

事件处理顺序
另外,源码中实现滚轮的滚动,使用到了Scroller ,以及 View的scrollToscrollBy方法,也需要对其有一定的了解

View的scrollTo和scrollBy

scrollBy方法

public void scrollBy(int x, int y)

scrollBy是在现有位置的基础上移动

scrollTo方法

public void scrollTo(int x, int y)

scrollTo则是在初始位置的基础上移动

scrollToscrollBy移动的时候,没有动画,要实现动画的过程,可借助Scroller

Scroller

Scroller是专门处理滚动效果的工具类

其使用方式是:
1.初始化

NumberPicker中的mFlingScrollermAdjustScroller

// create the fling and adjust scrollers
mFlingScroller = new Scroller(getContext(), null, true);
mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));

2.调用startScroll

public void startScroll(int startX, int startY, int dx, int dy, int duration
  • startX, startY - 开始移动时的x,y坐标
  • dx - 沿x轴移动的距离
  • dy - 沿y轴移动的距离
  • duration - 整个移动过程所耗费的时间

该方法,根据插值器和起始、终止位置来计算当前应该移动到的位置,并反馈给用户,其只做数值计算,不会真正的移动View

需要注意的是,在调用startScroll函数后,需要调用invalidate函数来重绘View。由此可见,Scroller类只能在自定义的ViewViewGroup中 使用,因为只有它们有invalidate函数

3.在computeScroll(computeScrollView类中函数)中处理计算出的数值

    @Overridepublic void computeScroll() {super.computeScroll();if (mScroller.computeScrollOffset()) {scrollTo(mScroller.getCurrX(), mScroller.getCurrY());invalidate();}}

在调用startScroll函数后,就会在Scroller内部用一个线程来计算,从起始位置沿X轴移动dx,沿Y轴方向移动dy,每毫秒控件应该在的位置。用户可以通过scroller.getCurrXscroller.getCurrY函数来获取当前计算得到的位置信息。
computeScrollOffset()方法,当Scroller还在计算中,表示当前控件还在滚动中,就会返回true。当Scroller计算结束,就会返回false
要想移动控件,就必须使用scrollTo函数,所以要每计算出一个新位置就让View重绘一次。所以步骤3也要调用invalidate函数

另外还用到了其fling方法:

public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)

用于带速度的滑动,行进的距离将取决于投掷的初始速度。可以用于实现类似 RecycleView 的滑动效果

  • startX - 开始滑动点的x坐标
  • startY - 开始滑动点的y坐标
  • velocityX - 水平方向的初始速度,单位为每秒多少像素(px/s)
  • velocityY - 垂直方向的初始速度,单位为每秒多少像素(px/s)
  • minX - x坐标最小的值,最后的结果不会低于这个值;
  • maxX - x坐标最大的值,最后的结果不会超过这个值;
  • minY - y坐标最小的值,最后的结果不会低于这个值;
  • maxY - y坐标最大的值,最后的结果不会超过这个值;

VelocityTracker

VelocityTracker 是一个跟踪触摸事件滑动速度的帮助类,用于实现flinging以及其它类似的手势。它的原理是把触摸事件 MotionEvent 对象传递给VelocityTrackeraddMovement(MotionEvent)方法,然后分析MotionEvent 对象在单位时间类发生的位移来计算速度。你可以使用getXVelocity()getXVelocity()获得横向和竖向的速率,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。

对上面的知识有基本了解后,继续分析

滚动事件分析

暂时把dispatchTouchEventonInterceptTouchEvent 放一旁,从onTouchEvent方法入手,可能比较易懂点

onTouchEvent方法

先看MotionEvent.ACTION_MOVE这个Action

MotionEvent.ACTION_MOVE

            case MotionEvent.ACTION_MOVE: {if (mIgnoreMoveEvents) {break;}float currentMoveY = event.getY();if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);if (deltaDownY > mTouchSlop) {removeAllCallbacks();// Scroll State变化了onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);}} else {int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));// 滚动一段距离scrollBy(0, deltaMoveY);// 重绘invalidate();}mLastDownOrMoveEventY = currentMoveY;} break;

假设在初始状态开始缓慢的滚动NumberPicker

1.mScrollState初始值为OnScrollListener.SCROLL_STATE_IDLE,所以会进入第一个if判断里面
2.如果滑动值大于mTouchSlop(系统所能识别出的,被认为是滑动的最小距离),则进入第二个if里面
在这个if里面,会将mScrollState设置为OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
3.所以,如果继续滑动话,就会进入else这个判断,开始scroll

scrollBy(0, deltaMoveY);
invalidate();

scrollBy分析

scrollByView中的方法,NumberPicker重写了scrollBy方法,如下:

    @Overridepublic void scrollBy(int x, int y) {int[] selectorIndices = mSelectorIndices;int startScrollOffset = mCurrentScrollOffset;...// mCurrentScrollOffset来时累加滚动距离mCurrentScrollOffset += y;// 处理向下滚动while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {mCurrentScrollOffset -= mSelectorElementHeight;decrementSelectorIndices(selectorIndices);setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);...}// 处理向上滚动while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {mCurrentScrollOffset += mSelectorElementHeight;incrementSelectorIndices(selectorIndices);setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);...}if (startScrollOffset != mCurrentScrollOffset) {onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);}}

其中有2个while循环(有些类似),如何理解,以第一个while为例

1.mCurrentScrollOffset += ymCurrentScrollOffset累加移动的距离

2.如何理解 mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight
在上一节NumberPicker分析(二)中,可知最开始mCurrentScrollOffset = mInitialScrollOffset
mSelectorTextGapHeight可以理解为文字间的间距,如下图:
mSelectorTextGapHeight
所以mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight可以理解为:

mInitialScrollOffset + 累加的y - mInitialScrollOffset > mSelectorTextGapHeight

累加的y>mSelectorTextGapHeight

所以如果累计移动的距离,大于了mSelectorTextGapHeight,则会进入while循环中:

// 如果累计移动的距离,大于了mSelectorTextGapHeight,表示控件往下滑动了大于mSelectorTextGapHeight的距离
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {// 调整mCurrentScrollOffsetmCurrentScrollOffset -= mSelectorElementHeight;// 重新计算SelectorIndicesdecrementSelectorIndices(selectorIndices);// 更新当前值setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {mCurrentScrollOffset = mInitialScrollOffset;}
}/*** Decrements the <code>selectorIndices</code> whose string representations* will be displayed in the selector.*/
private void decrementSelectorIndices(int[] selectorIndices) {for (int i = selectorIndices.length - 1; i > 0; i--) {selectorIndices[i] = selectorIndices[i - 1];}int nextScrollSelectorIndex = selectorIndices[1] - 1;//判断减1后是否小于最小值if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {nextScrollSelectorIndex = mMaxValue;}selectorIndices[0] = nextScrollSelectorIndex;//缓存ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}/*** Sets the current value of this NumberPicker.** @param current The new value of the NumberPicker.* @param notifyChange Whether to notify if the current value changed.*/
private void setValueInternal(int current, boolean notifyChange) {if (mValue == current) {return;}// Wrap around the values if we go past the start or endif (mWrapSelectorWheel) {current = getWrappedSelectorIndex(current);} else {current = Math.max(current, mMinValue);current = Math.min(current, mMaxValue);}int previous = mValue;mValue = current;// If we're flinging, we'll update the text view at the end when it becomes visibleif (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {updateInputTextView();}if (notifyChange) {notifyChange(previous, current);}// 再初始化SelectorWheelIndicesinitializeSelectorWheelIndices();// 重绘invalidate();
}

上面的代码可理解为:
a.往下滑动了大于mSelectorTextGapHeight的距离
b.后移selectorIndices数组,如最开始selectorIndices[4, 0, 1],后移一位变成[4, 4, 0] (上面的循环方法,i == 0 时暂时不处理)
c.由于是往下滑动,数组的第一个元素就必须是selectorIndices[1] - 1(原来的第一个值减去1,即当前的第二个值减去1),即变成[3, 4, 0]
d.根据最新的selectorIndices,更新当前值mValue
e.再根据当前值mValue,计算selectorIndices
f.重绘

重绘时调用onDraw方法,此时mCurrentScrollOffset累加上了移动距离,所以绘制的文字位置也发生了变化
文字位置发生了变化

MotionEvent.ACTION_UP

考虑一个问题,如果滑动结束后,滚轮中的字符串没有居中对齐,是不是还需要继续处理?
所以,在手指抬起来的MotionEvent.ACTION_UP事件中,还需要处理继续滚动。这里有大致有2个判断:

  1. 如果用户滑动的速度很快,手指抬起时,滚轮flinging,需要一个减速过程才停止下来
  2. 如果手指离开时,滚轮速度不快,也需要对齐滚轮中的字符串
            case MotionEvent.ACTION_UP: {...// VelocityTracker追踪滑动速度VelocityTracker velocityTracker = mVelocityTracker;// 计算滑动速度velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);// 获取Y轴速度int initialVelocity = (int) velocityTracker.getYVelocity();// 大于mMinimumFlingVelocity,开始flingif (Math.abs(initialVelocity) > mMinimumFlingVelocity) {fling(initialVelocity);onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);} else {int eventY = (int) event.getY();int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);long deltaTime = event.getEventTime() - mLastDownEventTime;if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {if (mPerformClickOnTap) {...} else {...}} else {// 调整滚轮ensureScrollWheelAdjusted();}// 更新滚动状态onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);}mVelocityTracker.recycle();mVelocityTracker = null;} break;

fling

fling方法用于带初速滑动,当滚轮往下滚动时,velocityY>0,往上滚动,velocityY<0

这里使用的mFlingScroller

    /*** Flings the selector with the given <code>velocityY</code>.*/private void fling(int velocityY) {mPreviousScrollerY = 0;if (velocityY > 0) {mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);} else {mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);}invalidate();}

ensureScrollWheelAdjusted

ensureScrollWheelAdjusted方法用于,调整滚轮,确保最后的状态没有偏移,且中间元素居中显示

这里使用的是mAdjustScroller

    /*** Ensures that the scroll wheel is adjusted i.e. there is no offset and the* middle element is in the middle of the widget.** @return Whether an adjustment has been made.*/private boolean ensureScrollWheelAdjusted() {// adjust to the closest valueint deltaY = mInitialScrollOffset - mCurrentScrollOffset;if (deltaY != 0) {mPreviousScrollerY = 0;// 如果滚动的距离大于mSelectorElementHeight / 2if (Math.abs(deltaY) > mSelectorElementHeight / 2) {deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;}// 调整滚动mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);// 重绘invalidate();return true;}return false;}

1.如果往下滚动,滚动的距离大于mSelectorElementHeight / 2mInitialScrollOffset - mCurrentScrollOffset得到的为负值,所以deltaY += mSelectorElementHeight
2.如果往上滚动,滚动的距离大于mSelectorElementHeight / 2mInitialScrollOffset - mCurrentScrollOffset得到的为正值,所以deltaY += -mSelectorElementHeight

computeScroll

computeScrollView中的方法,使用了Scroller,则需要重写该方法

    @Overridepublic void computeScroll() {Scroller scroller = mFlingScroller;if (scroller.isFinished()) {scroller = mAdjustScroller;if (scroller.isFinished()) {return;}}// 必须调用此方法scroller.computeScrollOffset();int currentScrollerY = scroller.getCurrY();if (mPreviousScrollerY == 0) {mPreviousScrollerY = scroller.getStartY();}// 又进入了`scrollBy`方法scrollBy(0, currentScrollerY - mPreviousScrollerY);mPreviousScrollerY = currentScrollerY;if (scroller.isFinished()) {onScrollerFinished(scroller);} else {// 重绘invalidate();}} /*** Callback invoked upon completion of a given <code>scroller</code>.*/private void onScrollerFinished(Scroller scroller) {if (scroller == mFlingScroller) {// 调整位置ensureScrollWheelAdjusted();updateInputTextView();onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);} else {if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {updateInputTextView();}}}

其它

参考:

  • 让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI