ScrollView源码分析
前言
Scrollview是我们经常使用的控件,假如一个界面的高度大于屏幕高度的时候,使用它可以很方便的实现一个界面的滑动显示,如果没有它,那么你的布局句会被压缩或者显示不全。一直想看看Scrollview内部怎么实现的,因为学习自定义view和自定义布局最好的老师就是源码,现在就来看看scrollview的源码。
首先先看注释
Scrollview是一个这样的view group,让放在它里面的view可以滑动的。
Scrollview里面只能有一个子view,为了让多个子 view可以滑动,你可以先放一个布局包括他们
Scrollview只支持竖直方向的滑动,如果想要水平滑动请使用HorizontalScrollView
永远不要把RecyclerView或者ListView包括在Scrollview的里面,这样使用会让你的用户体验很差
对于竖直方向的滑动还可以考虑NestedScrollView,有着更加强大灵活的用户接口,和对Material design有着更好的支持
尊重作者劳动
转载请注明出处 https://blog.csdn.net/dreamsever/article/details/80861641
进入源码
首先ScrollView 继承自 FrameLayout,我们看源码就是一个学习的过程,学习他们的写法,为什么这里继承了FrameLayout而不是直接ViewGroup,FrameLayout基本是是我们常用的布局里面最简单的ViewGroup,为什么是FrameLayout而不是直接ViewGroup我还知道,但是Google工程师这么做一定有他的道理,比如FrameLayout不消耗太多性能恰巧又做了一些Scrollview需要的前提工作,是不是这样只有看代码了。ScrollView 虽然继承自 FrameLayout,但是它还是ViewGroup,我们看一个ViewGroup需要关心哪些呢?自定义View我们肯定关心onMeasure,onDraw,因为我们关心的是这个View长什么样子,也许还有一些手势操作,自定义ViewGroup我们关心的就是onMeasure,onLayout方法了,这里是ScrollView肯定也少不了手势相关的逻辑
带着疑问进入源码
对于ScrollView的疑问,我想最大的就是,ScrollView到底如何实现的让布局可以滑动显示
构造方法
//一共四个构造方法,另外三个调用的都是这个
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//初始化
initScrollView();
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
//设置是否延伸内容,用于解决一些留白问题
//当子控件的高度小于ScrollView的高度时,会导致屏幕下面留白
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
a.recycle();
if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
setRevealOnFocusHint(false);
}
}
先看initScrollView()
mScroller,当看到这个名字的时候我就感觉它不是个青铜,这个控件叫ScrollView它叫Scroller,难道滑动就是靠它实现的
private void initScrollView() {
//新建一个OverScroller
mScroller = new OverScroller(getContext());
//设置可获取焦点
setFocusable(true);
//设置后代的获取焦点的情况
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
//如果不绘制,这样设置让性能更好
setWillNotDraw(false);
//ViewConfiguration包含着关于View的各种常量,配置信息
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
//仿误触,低于这个值忽略用户手势操作
mTouchSlop = configuration.getScaledTouchSlop();
//最小速率
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//最大速率
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
//到View的边的时候还能滑动的最大距离
mOverscrollDistance = configuration.getScaledOverscrollDistance();
//
mOverflingDistance = configuration.getScaledOverflingDistance();
//
mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
}
onMeasure
现在构造方法走完了,我们直接去看onMeasure 方法,onMeasure方法直接
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//当mFillViewport 为false的时候直接返回,表示走的是父类FrameLayout的测量方法
//mFillViewport为true重新走一遍测量
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
首先关于mFillViewport
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="@color/color_yellow">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="text"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"/>
</LinearLayout>
</ScrollView>
前者是没有设置mFillViewport,后者是设置了mFillViewport为true,可见mFillViewport为true使LinearLayout 的高这个属性生效了。既然是ScrollView继承自FrameLayout,那么当把ScrollView换成
FrameLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_yellow">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="text"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"/>
</LinearLayout>
</FrameLayout>
发现显示效果和后者是一样的,不应该啊,不设置fillViewport属性也就是默认为false,那么走的onMeasure方法是FrameLayout的onMeasure,为什么显示不一样呢,原来是ScrollView重写了measureChildWithMargins方法,使子控件,也就是LinearLayout显示的高度为 模式为MeasureSpec.UNSPECIFIED
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
onLayout
下面看onLayout方法,onLayout方法没什么说的,基本上是用了父类FrameLayout的布局方法,加入了一些scrollTo操作滑动到指定位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
//是否已经经历了一次布局
if (!isLaidOut()) {
if (mSavedState != null) {
mScrollY = mSavedState.scrollPosition;
mSavedState = null;
} // mScrollY default value is "0"
final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
final int scrollRange = Math.max(0,
childHeight - (b - t - mPaddingBottom - mPaddingTop));
// Don't forget to clamp
if (mScrollY > scrollRange) {
mScrollY = scrollRange;
} else if (mScrollY < 0) {
mScrollY = 0;
}
}
//滑动到指定位置
// Calling this with the present values causes it to re-claim them
scrollTo(mScrollX, mScrollY);
}
手势相关
下面把ScrollView拿掉,仅仅留下里面的布局,运行你会发现,下面的Button也是看不到了,为什么因为TextView太高,把Button顶到屏幕外面了,现在你去触摸滑动屏幕是没有效果的,滑不动。假如这时候外面包一个ScrollView你就可以向上滑动然后看到下面的button了。
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="2000dp"
android:text="text"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"/>
</LinearLayout>
既然是这种效果,那么就要看View的几个手势事件了
事件分发流程ViewRootImpl–>DecorView(DecorView包涵于PhoneWindow)–>Activity–>PhoneWindow–>DecorView,然后一级一级传递给我们的布局以及ziview,事件的传递主要牵涉到一下几个方法:
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
触摸事件先来到dispatchTouchEvent,搜索ScrollView源码发现并没有复写dispatchTouchEvent方法,FrameLayout也没有实现这个方法,也就是处理逻辑和ViewGroup是一样的。
直接去看onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*这个方法仅仅决定是否拦截事件,假如返回true,onTouchEvent事件会调用去处理滑动
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*用户触摸着屏幕并且滑动手指,我们要拦截这个事件
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
/*滑到最上面了
* Don't try to intercept touch if we can't scroll anyway.
*/
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);//向上或者向下的滑动距离
//假如yDiff大于防误触的距离,并且当前嵌套模式不是SCROLL_AXIS_VERTICAL
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
//设置当前是拖动状态
mIsBeingDragged = true;
mLastMotionY = y;
//没有初始化速率追踪去初始化
initVelocityTrackerIfNotExists();
//将事件传递给速率追踪者
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
//设置父控件不要拦截我
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
//是不是在子布局区域里面,不在的话拖动模式为false
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();//回收速率追踪器
break;
}
/*记录按下位置
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
//初始化速率追踪器
initOrResetVelocityTracker();
//将事件传递给速率追踪者
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
mScroller.computeScrollOffset();//计算滑动偏移
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
//开始对相应轴也就是对VERTICAL方向的嵌套滑动,这是特殊情况,先不考虑嵌套滑动的情况
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//对于up和cancel事件
/* Release the drag */
//释放触摸或者拖动事件
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
stopNestedScroll();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
onInterceptTouchEvent事件就是做一件事,决定事件是不是要继续交给自己的onTouchEvent处理,经过断点发现,滑动时onInterceptTouchEvent都返回false,表示所有的点击触摸事件都先给被包含的子View,子View不处理在给ScrollView的onTouchEvent,ScrollView的ACTION_MOVE事件不会经过onInterceptTouchEvent,因为当onTouchEvent消费了事件时,ACTION_DOWN以后的事件将不在经过onInterceptTouchEvent,直接由onTouchEvent处理后续所有的事件。
- 注意:断点时需要把源码版本和手机系统版本对应
onTouchEvent事件
可以看到最后onTouchEvent都返回了true,来者不拒 return true; 这里也就可以解释为什么onInterceptTouchEvent方法看不到ACTION_MOVE事件了,加入子view不处理事件,只有一种可能后续事件都被onTouchEvent返回true消费了。
对于Scroll这个动作来说,滑动分为两种
drag :拖曳操作
fling : 快速滚动
onTouchEvent的ACTION_MOVE事件记录了按下时的y即开始的位置和mActivePointerId--第一个触摸点的位置id
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
//从ev里面copy出一个新的MotionEvent
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
//子控件数量为0什么也不做
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
//滑动未结束使父布局不拦截事件
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
//记录滑动开始位置
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;//滑动距离
//先不考虑嵌套滑动的情况,忽略这个判断
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
//mIsBeingDragged为false并且滑动距离大于最小触发距离,mIsBeingDragged设置为true
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;//制造偏移使滑动不那么突兀
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
//overScrollBy会执行滑动操作,具体如何执行会分两种情况1,拖动状态,2,fling状态
//具体去看onOverScrolled方法
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();//滑动到头了速率监测就不需要了
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
case MotionEvent.ACTION_UP:
//拖动状态并且手指离开了,这时候就要看看滑动速率是不是触发fling状态
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
//如果当前的速率绝对值大于最小触发速率,执行fling滑动
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
//结束拖动状态
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
onOverScrolled
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {//fling状态回调滑动改变
final int oldX = mScrollX;
final int oldY = mScrollY;
mScrollX = scrollX;
mScrollY = scrollY;
invalidateParentIfNeeded();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
}
} else {//直接执行滑动
super.scrollTo(scrollX, scrollY);
}
awakenScrollBars();
}
private void flingWithNestedDispatch(int velocityY) {
final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
(mScrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
if (canFling) {
fling(velocityY);
}
}
}
/**
* Fling the scroll view
* 快速滑动ScrollView
* @param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/cursor is moving down the screen,
* which means we want to scroll towards the top.
*/
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
}
postInvalidateOnAnimation();
}
}
OverScroller.java
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX);
mScrollerY.fling(startY, velocityY, minY, maxY, overY);
}
// mScrollerY.fling
void fling(int start, int velocity, int min, int max, int over) {
mOver = over;
mFinished = false;
mCurrVelocity = mVelocity = velocity;
mDuration = mSplineDuration = 0;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mCurrentPosition = mStart = start;
if (start > max || start < min) {
startAfterEdge(start, min, max, velocity);
return;
}
mState = SPLINE;
double totalDistance = 0.0;
if (velocity != 0) {
mDuration = mSplineDuration = getSplineFlingDuration(velocity);
totalDistance = getSplineFlingDistance(velocity);
}
mSplineDistance = (int) (totalDistance * Math.signum(velocity));
mFinal = start + mSplineDistance;
// Clamp to a valid final position
if (mFinal < min) {
adjustDuration(mStart, mFinal, min);
mFinal = min;
}
if (mFinal > max) {
adjustDuration(mStart, mFinal, max);
mFinal = max;
}
}
看到这里就有点疑惑了,在onTouchEvent方法里面的MotionEvent.ACTION_MOVE事件里面,先调用了overScrollBy,然后里面调用了onOverScrolled,在这里分两种情况一种是拖动状态,直接走super.scrollTo(scrollX, scrollY);,然后是fling状态,最后调用OverScroller的fling方法,SplineOverScroller mScrollerY的fling方法,但是都没有看到ScrollView执行scrollTo或者scrollBy方法
摘录:https://blog.csdn.net/huangbiao86/article/details/23220251
1、它是怎样滑动View的(如何与View关联的)?
2、又是谁触发了它?其实要分析这两个问题,主要还得从View的绘制流程开始分析:
关于View的绘制流程,网上资料众多,基本上相差无几,这里就不再阐述,下面提取下解析Scroller功能的必要的几个View的绘制方法:scrllTo()/scrollBy() —> invalidate()/postInvalidate() —> computeScroll();(这个流程我们可以分析源码得到)。scrllTo()/scrollBy()是view移动的两个方法;它会更新View的新的坐标点,然后调用invalidate/postInvalidate方法刷新view; 滑动完成后再调用computeScroll()方法;computeScroll()是View.java的一个空的方法,需要由我们去实现处理。
也就是说Scroller并不是一个滑动操作者,它只是一个滑动辅助计算类,所以我们现在再来看刚才ScrollView的fling方法
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
//去计算滑动数值
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
}
//刷新调用computeScroll
postInvalidateOnAnimation();
}
}
computeScroll方法才是让你看到滑动变化的地方,你会看到在这里执行了overScrollBy方法,参数是从mScroller里获得的。
@Override
public void computeScroll() {
//计算滑动偏移
if (mScroller.computeScrollOffset()) {
// This is called at drawing time by ViewGroup. We don't want to
// re-show the scrollbars at this point, which scrollTo will do,
// so we replicate most of scrollTo here.
//
// It's a little odd to call onScrollChanged from inside the drawing.
//
// It is, except when you remember that computeScroll() is used to
// animate scrolling. So unless we want to defer the onScrollChanged()
// until the end of the animated scrolling, we don't really have a
// choice here.
//
// I agree. The alternative, which I think would be worse, is to post
// something and tell the subclasses later. This is bad because there
// will be a window where mScrollX/Y is different from what the app
// thinks it is.
//
int oldX = mScrollX;
int oldY = mScrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
//执行滑动
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
//告知滑动改变
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
if (!awakenScrollBars()) {
// Keep on drawing until the animation has finished.
postInvalidateOnAnimation();
}
} else {
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
}
总结
现在我们大致了解了ScrollView的滑动实现,继承FrameLayout实现基本的测量和布局功能,然后在onInterceptTouchEvent里面不拦截子view的事件,但是在onTouchEvent里面每次都返回true,也就是子view不处理,那么后续的事件ScrollView都要处理了,onTouchEvent的down事件记录位置信息,在move事件中做具体的滑动,滑动分为两种:平滑拖动和快速滑动(fling),平滑拖动可以执行scrollTo,fling需要借助mScroller计算位置,然后在computeScroll方法中执行滑动到计算后的位置。
分析有误的地方还请指正。
上一篇: STS / Eclipse更改背景图片