想象一下你拿着放大镜贴很近的看一副巨大的清明上河图, 那放大镜里能够看到的内容是颇有限的,
而随着放大镜的上下左右移动,就能够看到不一样的内容了
android中手机屏幕就至关于这个放大镜, 而看到的内容是画在一个无限大的画布上~
画的内容有限, 而手机屏幕能够看到的东西更有限~ 可是背景画布是无限的
若是把放大镜的移动比做scroll操做,那么能够理解,这个scroll的距离是无限制的~
只不过scroll到有图的地方才能看到内容
参考ScrollView理解, 当child内容过长时,有一部份内容是"看不到"的,至关于"在屏幕以外",
而随着咱们的拖动滚动,则慢慢看到剩下的内容,至关于咱们拿着放大镜向下移动~
而代码中的这个scroll方法系统提供了两个:
scrollTo和scrollBy
源码以下
html
/**
* Set the scrolled position of your view. This will cause a call to
*
{@link #onScrollChanged(int, int, int, int)}
and the view will be
* invalidated.
*
@param
x the x position to scroll to
*
@param
y the y position to scroll to
*/
public
void
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged( mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy( int x, int y) {
}
mScrollX 表示离视图起始位置的x水平方向的偏移量
android
mScrollY表示离视图起始位置的y垂直方向的偏移量
能够经过getScrollX() 和getScrollY()方法分别得到
两个方法的区别就是to参数是绝对值,by是相对于当前滚动到的位置的增量值
好比:
mScrollX=100, mScrollY=100
scrollTo(20, 20) -> mScrollX=20, mScrollY=20;
scrollBy(20, 20) -> mScrollX=120,mScrollY=120;
注意:
这里mScrollX和mScrollY的值是偏移量,是相对于视图起始位置的偏移量~
因此任何view,不管布局是怎么样的,只要是刚初始化未通过scroll的,偏移量都是0~
即mScrollX/Y是相对于本身初始位置的偏移量,而不是相对于其容器的位置坐标
-----------------------------------------------------------------------------
下面是就ScrollView的源码拆开了的分析,并加入了一些补充扩展,
主要内容包括
1.最基本的随着touch滚动的效果
2.fling效果,即滑动后抬起手后继续关心滚动的效果
3.over scroll效果,即拖动超出边界的处理
上述123系统都有提供相关实现方法,可是ScrollView默认只有1,2的实现效果,
over scroll须要咱们自行进行必定处理后才能够看到~
下面就ScrollView的源码进行分析,且提供三个自定义ScrollView(难度依次递进)实现上面的三种效果,已打包成demo
后面源码分析时,系统是乱七八糟直接写一块儿时,分析的被比较细也比较乱,
demo中三个自定义ScrollView至关于按照难度梯度抽取出来的,
即view2是在view1基础上修改添加功能的,view3是在view2基础上修改添加功能的
能够从demo下手帮助理解其中原理
-----------------------------------------------------------------------------
scroll至关于一个拖动,咱们能够用scrollTo/By控制其滚动到某个位置,
那通常ScrollView控件这种都是随着咱们的手势生效的,内部原理是如何的呢~
下面来研究下系统ScrollView控件源码里面的具体实现~
系统考虑的东西比较多,研究起来较为复杂,因此先就核心部分拆开一点点研究~
手的拖动确定是跟touch即触摸事件挂钩了~直接定位到ScrollView中的该方法
(onTouchEvent干什么用的就不扫盲了)
首先是ACTION_DOWN
ios
case MotionEvent.ACTION_DOWN: {
mIsBeingDragged = getChildCount() != 0;
if (!mIsBeingDragged) {
return false;
}
/*
* 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 = ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
有三段代码,第二三段带注释,下面是介绍:
1.ScrollView没有child则不作处理,这个不解释,都没child滚个蛋啊
若是有child则设置标志位mIsBeingDragged即"开始拖动"(看英文就能够理解了)
2.看注释理解~若是还在滑动用户触碰了屏幕,则马上中止滑动
mScroller是一个OverScroller对象,是处理滚动的,
类介绍里提到这个功能和Scroller类差很少,大部分状况下能够替代之,
区别能够简单的理解为OverScroller容许超出边界,后面会介绍Scroller类~
至于mFlingStrictSpan无视之
3.看注释理解~记住点击的位置
scrollerView,处理垂直滚动,这里就只记录Y坐标了
mActivePointerId是用来处理多点触控时的稳定性的,这里先记住做用就好了
-----------------------------------------------------------------------------
而后是ACTION_MOVE,这个是重点,随着move咱们但愿控件也能随着咱们的手的拖动滚动到所需位置
算法
case MotionEvent.ACTION_MOVE:
if (mIsBeingDragged ) {
// Scroll to follow the motion event
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float y = ev.getY(activePointerIndex);
final int deltaY = ( int) ( mLastMotionY - y);
mLastMotionY = y;
final int oldX = mScrollX;
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (overScrollBy(0, deltaY, 0, mScrollY,
0, range, 0, mOverscrollDistance, true)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
onScrollChanged( mScrollX, mScrollY , oldX, oldY);
if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight());
if (! mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight());
if (! mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if ( mEdgeGlowTop != null
&& (! mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
invalidate();
}
}
}
break;
系统注释仍是很良心的,通常以功能点为单位注释,这里仍是根据注释去看
分两段:随着触摸事件的滚动,还有顶部底部边缘阴影的处理
阴影处理的先忽视
这里的触摸点获取不是直接event.getY,而是经过下面两句代码获取的
spring
final int activePointerIndex = ev.findPointerIndex( mActivePointerId);
final float y = ev.getY(activePointerIndex);
上面已经介绍过了mActivityPointerId的保证多点触碰稳定性的做用,
包括onTouchEvent里面的ACTION_POINTER_DOWN/UP也是为了处理多点状况的,
为了避免发散太多就不细介绍了(其实我也不是研究太透彻)
知道这样处理能防止多点触控的干扰,能够稳定获取到咱们须要的触摸的y坐标就好了
根据如今的触摸坐标y和上次位置的y坐标mLastMotionY算出差值,即此次移动的距离deltaY
最后以获取到的这些数据进行滚动操做~
ScrollView中在这里使用的是overScrollBy方法,该方法是其父类view的方法,定位过去看下
其实若是要简单处理的话直接掉scrollTo方法就能够了~参见demo中MyScrollView1
若是以为ScrollView里逻辑没法理解,那就能够先把上面demo的view1研究懂之后再继续下文
注意,scroll因为是没有限制的,便可以滚动到任何位置,显然不符合咱们的须要,
因此咱们要限制滚动范围,只在有内容的绘制部分滚动
因为ScrollView只是纵向Y轴上滚动,因此只限定y上滚动范围便可,
以下图示,红框是scrollview,蓝框是child,滚动范围应该是箭头所示部分~
即0 到 child.height-scrollview,height
而demo里view1中也添加了这么一段(demo是横向滚动)
app
// Clamp values if at the limits and record
final
int
left = 0;
final
int
right = getScrollRangeX();
// 防止滚动超出边界
if
(scrollX > right) {
scrollX = right;
}
else
if
(scrollX < left) {
scrollX = left;
}
-----------------------------------------------------------------------------
由于scrollView考虑的比较多,因此处理麻烦点,按照源码追踪到view中的overScrollerBy方法
less
/**
* Scroll the view with standard behavior for scrolling beyond the normal
* content boundaries. Views that call this method should override
* {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
* results of an over -scroll operation.
*
* Views can use this method to handle any touch or fling -based scrolling.
*
* @param deltaX Change in X in pixels
* @param deltaY Change in Y in pixels
* @param scrollX Current X scroll value in pixels before applying deltaX
* @param scrollY Current Y scroll value in pixels before applying deltaY
* @param scrollRangeX Maximum content scroll range along the X axis
* @param scrollRangeY Maximum content scroll range along the Y axis
* @param maxOverScrollX Number of pixels to overscroll by in either direction
* along the X axis.
* @param maxOverScrollY Number of pixels to overscroll by in either direction
* along the Y axis.
* @param isTouchEvent true if this scroll operation is the result of a touch event.
* @return true if scrolling was clamped to an over -scroll boundary along either
* axis, false otherwise.
*/
@SuppressWarnings({"UnusedParameters"})
protected boolean overScrollBy( int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = mOverScrollMode;
final boolean canScrollHorizontal =
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}
int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
maxOverScrollY = 0;
}
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -maxOverScrollY;
final int bottom = maxOverScrollY + scrollRangeY;
boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}
boolean clampedY = false;
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
return clampedX || clampedY;
}
方法的做用:
总结起来就是计算over scroll时scrollX/Y的值~ 并将其记录在onOverScrolled方法里
参数意义:
前8个分两组,1357是针对x轴的,2468则是y轴的,这里scrollView只纵向滚动,因此只处理y轴
比较难理解的是后俩参数
1.scrollRange
是某方向上滚动的范围,能够参考上面的图片,只不过要把padding部分考虑进去
下面是系统获取范围的方法
ide
private int int scrollRange = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);
scrollRange = Math. max(0,
child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
}
return scrollRange;
}
不难理解,最大距离的状况就是childview移动到了最顶部,而后滑动到最底部,上面这个方法算的就是这个最大距离
2.maxOverScroll
为越界滚动最大距离,即在以前范围的基础上再加上这个越界最大距离~
ScrollView在这里设置的是系统默认值0
好比之前纵向的滚动范围是0~300,那若是这个值设为50,则最终over scroll的范围就是-50~350,方法内算法以下
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -maxOverScrollY;
final int bottom = maxOverScrollY + scrollRangeY;
top=-maxOverScrollY ~ bottom=maxOverScrollY+scrollRangY
带入咱们假设的值,那就是0~300, 注意,这个300是Y坐标300~
这里假设咱们的maxOverScrollY不是系统默认的Y而是 50,
那虽然滚动范围不变仍是500-200=300~ 可是实际上能够滚动的范围是大于300的~
效果相似于ios那种,listview到达顶部之后继续拖还能够移动~ 也能够脑补下拉刷新listview的效果
在拖到顶部之后,还能够overScroll继续拖动,
而继续拖动的最大距离就是maxOverScrollY,如图左边小箭头的长度
底部同理
那整个边界范围就变了~
从原来的y = 0 ~ 300 变成了
-maxOverScrollY ~ scrollRangeY + maxOverScrollY
看图很好理解,就变成了
y = -50 ~ 350
横向同理
下面代码就是判断了
若是如今滚动的新坐标超过了over的极限值,则将极限值赋值给新坐标~
简单而言就是滑动到极限越界距离之后就卡住他,让他"划不动",
这里英文clamp为"钳住"的意思,英语好的能够帮助快速理解~
x/y方向达到极限距离的同时会分别记录下一个标志符,最后|做为返回值,
即任何一个方向上有划不动的状况时则返回true,不然false
最后经过onOverScrolled暴露给子类,这里view里面的onOverScrolled方法是empty不作任何处理的~里面注释也是卖萌,"我是故意的~"
上面一大串overScrollBy方法源码分析这里总结一下
该方法就至关于在scrollTo/By的基础上添加了对overScroll状况的处理, 但父类view中只处理数据,没有实际的scroll操做,父类view处理完数据后将其记录在onOverScrolled方法中,
子类继承onOverScrolled方法再根据获得的数据scrollTo/By处理便可~
举个简单的例子帮助理解,好比这个view至关于一个大加工厂,
那overScrollBy方法至关于一个加工车间,好比是作串串的(竹签上有海带素鸡一类的那种)- -
把原材料加工好成串串后,直接就丢到一个储藏的仓库里~不作任何其余操做
而继承view的子类ScrollView就至关于来拿货的销售商,来了之后无论生产过程,直接去这个储藏的仓库里,
把货拿出来而后该卖的卖,该本身吃的本身吃,该二次加工的二次加工~进行具体的操做~
这个仓库就能够理解为onOverScrolled方法~
能够理解为一个监听,好比scrollview提供一个onScroll监听,父类只用该方法记录数据,
而子类复写之就能够获取到所需数据,如当前滚动到位置,根据须要处理了
回到源码ScrollView的onOverScrolled方法
其实简单处理的话直接在onOverScrolled里面scrollTo就能够了,可是系统考虑到scrollbar,animating scroll等状况因此处理的比较复杂
若是下面这段代码没法理解,能够先跳过本段,直接到ACTION_UP部分,
能够在看完后面OverScroll实现(demo中view3)介绍,研究懂后再回头看这段代码
源码分析
@Override
protected void onOverScrolled( int scrollX, int scrollY,
boolean clampedX, boolean // Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
mScrollX = scrollX;
mScrollY = scrollY;
invalidateParentIfNeeded();
if ( mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
awakenScrollBars();
}
这里是if分红两部分的,注释说明为,对于还在进行中的滚动要区别处理,
区分处理的缘由能够参考computeScroll()方法
@Override
public void 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());
}
}
}
awakenScrollBars();
// Keep on drawing until the animation has finished.
postInvalidate();
} else {
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
}
注释翻译下
这个方法会在viewgroup draw绘制的时候调用,
咱们不想在这个时候再次显示scrollbar滚动条,这个scrollTo方法会处理,
因此咱们在这里复制了scrollTo方法的大部份内容
下面代码和onTouch里的MOVE中代码同样(上文有引用过这部分代码)
回到onOverScrolled方法中,结合看意思就是
若是mScroller.isFinished滚动动画已经结束了,那正常scrollTo方法滚动
若是未结束,那调用一个不会显示scrollBar滚动条的scrollTo方法
(上面注释说过,处理缘由就是为了方法滚动条再次显示,
且代码大部分复制scrollTo,即至关于一个不显示滚动条的scrollTo方法)
且若是未滚动完成,还要加个判断, 若是是滚动到不能再滚动了,即clampedY=true
则进行回弹操做mScroller.springBack
再次引用上面车间加工串串的例子加深理解
demo中本身处理就至关于,卖串串的本身加工串串(计算数据), 直接卖(scrollTo)~
系统的ScrollView呢,就是把串串交给加工车间view精加工一下(overScrollBy),
最后从onOverScrolled仓库中取加工好的串串再去卖(scrollTo)
-----------------------------------------------------------------------------
回到onTouch,以后是ACTION_UP操做
按照日常滚屏的习惯,UP抬起时,通常还会有一个惯性继续滚动一段距离~
(这里咱们把滚动叫作scroll,这个有惯性的甩~抛~的动做叫作fling~)
首先用VelocityTracket获取y向的速度,根据速度去处理fling
(按照一般思惟,速度越快,甩的越远~)
case MotionEvent. ACTION_UP:
if ( mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = ( int) velocityTracker.getYVelocity(mActivePointerId );
if (getChildCount() > 0) {
if ((Math. abs(initialVelocity) > mMinimumVelocity)) {
} else {
if ( mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
invalidate();
}
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
这里有判断,当速度超过一个最小阀值的时候,就视为一个fling~则调用fling()方法
不然视为普通的scroll,那判断这个时候是否须要回弹操做,有的话invalidate刷新页面
下面是核心方法~甩~
/**
* Fling the scroll view
*
* @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);
final boolean movingDown = velocityY > 0;
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling" );
}
invalidate();
}
}
诸如这类"高级"的滚动,都使用了Scroller类处理~详见
fling里面实际上调用的是mScroller的fling方法,咱们定位到OverScroller类该方法
内部原理太多,这里只解释方法的做用和参数的意义了
方法做用:根据一个fling手势开始fling行为~移动的距离取决于fling的初始速度~
参数传进来的值为
mScrollX, mScrollY, 0, velocityY, 0, 0, 0, Math. max(0, bottom - height), 0, height/2
一共10个参数分两组,奇数为x轴处理,偶为y
2 4 6 8 10五个参数分别介绍(x向的同理)
startY 开始甩的y坐标 - 值为当前滚动位置mScrollY
velocityY y轴上的速度 - 值为velocityTracker计算出来的速度值
minY y轴上能够到的最小坐标 - 值为0
maxY y抽上能够到的最大坐标 - 值为child的高度减去viewgroup除padding之外的高度~
(这个值和以前计算的scrollRangeY同样,只不过那个是长度,这个是坐标~
想理解能够参考以前的图片)
overY 越界滑动的范围 - 值为viewgroup除padding高度之外的一半
fling里面的具体速度距离的算法...略过~
插一句
若是是没有over的scroll或者fling,那直接用Scroller类就能够了,系统考虑的比较全,
因此ScrollView里面用的是OverScroller类~
一样的fling方法Scroller就没有最后两个和over相关的参数
下面是一个不考虑over状况用Scroller实现fling效果的demo
-----------------------------------------------------------------------------
回到over scroll的处理
上面提到,系统ScrollView源码中是有处理over scroll的,只不过ScrollView中的对应参数overDistance为0,形成了ScrollView没有over scroll的效果
这里咱们能够本身尝试实现下~
其实很是简单,demo12中ouTouch的MOVE里面的scrollTo方法改为和ScrollView同样的,
即利用父类view的overScrollBy方法转一圈~
只不过调用overScrollBy方法时,记得传入的maxOverScrollX/Y不能是0,否则就没意义了
这样拖动的时候就能够over scroll了~
既然有over scroll那咱们确定须要回弹效果,即松手后自动滚动回来~
前面说过这种高级的要用Scroller类,而牵涉到over的,天然就用到OverScroller类了~
fling方法和Scroller同样,多一个overX方法,即fling时over scroll的最大距离,
demo里我设定成了maxOverScrollX的一半,能够自行调整
fling的回弹是自动的,但咱们fling行为是有个最小速度判断的,于是在UP时还要加个,
没有fling时,若是开始了一个回弹效果,则刷新视图~
如下是over scroll的优化效果
over scroll从体验上来讲,咱们但愿是有一个阻塞的效果的,
即若是普通状态下,手指移动100距离,那view也滚动100距离,
可是over scroll时,手指移动100距离,view滚动距离应该按比例下降~
这样一种效果才更加"真实"
此外demo中还在onOverScrolled回调用添加了一个拉断效果,
即当拖动到over scroll的极限距离时,虽然没有UP可是强制进行回弹操做,
至关于模拟了一个"拉断"的效果
-----------------------------------------------------------------------------
缺陷, 没有考虑measure,因此MyScrollView中子类size有点问题,demo中暂时写死了child宽度,
measure的原理介绍会在后面有时间整理出~但愿你们持续关注
照例,回复能够免积分下载~