NumberPicker分析(三)
这一节主要用来分析NumberPicker
的事件处理及滚动
NumberPicker
继承自LinearLayout
,是一个ViewGroup
,ViewGroup
事件处理的顺序大致如下:
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
另外,源码中实现滚轮的滚动,使用到了Scroller
,以及 View的scrollTo
、scrollBy
方法,也需要对其有一定的了解
View的scrollTo和scrollBy
scrollBy
方法
public void scrollBy(int x, int y)
scrollBy
是在现有位置的基础上移动
scrollTo
方法
public void scrollTo(int x, int y)
scrollTo
则是在初始位置的基础上移动
scrollTo
和scrollBy
移动的时候,没有动画,要实现动画的过程,可借助Scroller
Scroller
Scroller
是专门处理滚动效果的工具类
其使用方式是:
1.初始化
如NumberPicker
中的mFlingScroller
和mAdjustScroller
// 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
类只能在自定义的View
或ViewGroup
中 使用,因为只有它们有invalidate
函数
3.在computeScroll
(computeScroll
是View
类中函数)中处理计算出的数值
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
在调用
startScroll
函数后,就会在Scroller
内部用一个线程来计算,从起始位置沿X轴移动dx
,沿Y轴方向移动dy
,每毫秒控件应该在的位置。用户可以通过scroller.getCurrX
、scroller.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
对象传递给VelocityTracker
的addMovement(MotionEvent)
方法,然后分析MotionEvent
对象在单位时间类发生的位移来计算速度。你可以使用getXVelocity()
或getXVelocity()
获得横向和竖向的速率,但是使用它们之前请先调用computeCurrentVelocity(int)
来初始化速率的单位 。
对上面的知识有基本了解后,继续分析
滚动事件分析
暂时把dispatchTouchEvent
和 onInterceptTouchEvent
放一旁,从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分析
scrollBy
是View
中的方法,NumberPicker
重写了scrollBy
方法,如下:
@Override
public 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 += y
,mCurrentScrollOffset
累加移动的距离
2.如何理解 mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight
?
在上一节NumberPicker分析(二)中,可知最开始mCurrentScrollOffset = mInitialScrollOffset
mSelectorTextGapHeight
可以理解为文字间的间距,如下图:
所以mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight
可以理解为:
mInitialScrollOffset + 累加的y - mInitialScrollOffset > mSelectorTextGapHeight
即
累加的y>mSelectorTextGapHeight
所以如果累计移动的距离,大于了mSelectorTextGapHeight
,则会进入while
循环中:
// 如果累计移动的距离,大于了mSelectorTextGapHeight,表示控件往下滑动了大于mSelectorTextGapHeight的距离
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
// 调整mCurrentScrollOffset
mCurrentScrollOffset -= mSelectorElementHeight;
// 重新计算SelectorIndices
decrementSelectorIndices(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 end
if (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 visible
if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
updateInputTextView();
}
if (notifyChange) {
notifyChange(previous, current);
}
// 再初始化SelectorWheelIndices
initializeSelectorWheelIndices();
// 重绘
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个判断:
- 如果用户滑动的速度很快,手指抬起时,滚轮flinging,需要一个减速过程才停止下来
- 如果手指离开时,滚轮速度不快,也需要对齐滚轮中的字符串
case MotionEvent.ACTION_UP: {
...
// VelocityTracker追踪滑动速度
VelocityTracker velocityTracker = mVelocityTracker;
// 计算滑动速度
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
// 获取Y轴速度
int initialVelocity = (int) velocityTracker.getYVelocity();
// 大于mMinimumFlingVelocity,开始fling
if (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 value
int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
if (deltaY != 0) {
mPreviousScrollerY = 0;
// 如果滚动的距离大于mSelectorElementHeight / 2
if (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 / 2
,mInitialScrollOffset - mCurrentScrollOffset
得到的为负值,所以deltaY += mSelectorElementHeight
2.如果往上滚动,滚动的距离大于mSelectorElementHeight / 2
,mInitialScrollOffset - mCurrentScrollOffset
得到的为正值,所以deltaY += -mSelectorElementHeight
computeScroll
computeScroll
是View
中的方法,使用了Scroller
,则需要重写该方法
@Override
public 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