庖丁解牛之ScrollView |
您所在的位置:网站首页 › 暂停这是怎么了 › 庖丁解牛之ScrollView |
新媒体管家 点击上方“程序员大咖”,选择“置顶公众号” 关键时刻,第一时间送达! 前言 ScrollView可以说是Android里最简单的滑动控件,但是其中也蕴含了很多的知识点。今天尝试通过ScrollView的源码来了解ScrollView内部的细节。本文在介绍ScrollView时会忽略以下内容:嵌套滑动,崩溃保存,Accessibility。 ScrollView是一种控件,继承自 FrameLayout,他的子控件远远大于ScrollView本身,所以ScrollView展现出来的只有子控件的一部分,通过滑动的形式来呈现出子控件的内容。 基本用法与功能剖析 先来回顾下ScrollView的基本用法,超级简单。我们通常在ScrollView内部放一个LinearLayout,然后在LinearLayout放各种元素,ScrollView滚动时就可以看到这些元素。附带一句,LinearLayout的width通常是match_parent(也可以是warp_content,这里有个坑,我们暂且不管,后面会提)。
从测试的角度来看下,ScrollView的功能是怎么样的? 首先滑动的时候有2种情况,如果滑的慢,ScrollView的滑动会随着手指的离开而停止(简单滑动);如果滑的快,在手指离开后,ScrollView还会再滑一段时间(这段时间内的状态我们称为fling)。 第二,fling的时候,手指碰一下,就立刻停止fling 第三,ScrollView到顶部的时候,下拉有光影效果。底部同理 子窗口大小超出父窗口 我们知道,一般情况下子view都是没有父view大的,因为measure的时候子view的大小会受到父view的制约,那什么情况下,子view会超出父view大小呢? 要想子view超出父view大小,大概有2种方式,一种是父view对子view的要求为MeasureSpec.EXACTLY,子view的size设置为某个固定值,另一种是父view对子view的要求为UNSPECIFIED,然后子view就可以随便搞了。可以参考getChildMeasureSpec代码就能大概看出来。 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { //此时为case1,resultSize可能大于specSize resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: //此时为case2,parent不做限制,大小就可以乱来了 if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } EXACTLY+固定值 对于case1,我们举个例子,可以这么写
此时TextView的就比parent的大,这是一种方式让子view超出了父view的大小。 ScrollView重写了android.widget.ScrollView#measureChildWithMargins UNSPECIFIED 而ScrollView的child能比ScrollView本身还大,用的是第二种方法,量的时候把specMode改为UNSPECIFIED,具体代码如下所示,关键看这句 final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED); 直接把childHeightMeasureSpec变为了MeasureSpec.UNSPECIFIED,此时parent传过来的高度其实已经毫无意义了。而子view的高度一般写为wrap_conten,就可以非常大了。 @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 childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 嵌套滑动(NestedScrolling) 本文虽然不介绍嵌套滑动,但是嵌套滑动的相关代码频繁出现在onTouchevent里面,所以还是要简单说下。 NestedScrolling 提供了一套父 View 和子 View 滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口。 更多知识可以参考 http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0822/3342.html https://segmentfault.com/a/1190000002873657 ScrollView默认支持了嵌套滑动,既可作为父view,也可作为子view 我们在看代码的时候暂时忽略和嵌套滑动相关的(带nest的函数) 简单滑动 手指在屏幕上滑动会触发ACTION_DOWN,ACTION_MOVE, ACTION_MOVE没人处理,就交给ScrollView处理。 这里我们看到个变量mIsBeingDragged,这个代表的是ScrollView是否正在被拖拽,手指抬起,mIsBeingDragged就会变为false,初始化的时候也为false。看L4可知如果deltaY(滑动的距离)超过mTouchSlop,那就表示触发了ScrollView的滑动,mIsBeingDragged 置为true,mTouchSlop是一个固定阈值。然后会执行L17 overScrollBy进行滚动。 case MotionEvent.ACTION_MOVE: ... 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 (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } overScrollBy这是View的方法,会触发onOverScrolled回调。此时只是普通的滑动,所以走L18,就是调super.scrollTo,根据手指滑动的距离进行移动。非常简单。 @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(); } fling(惯性滑动) 怎么实现手指离开之后,还能滑动一段距离呢? onTouchEvent里有这么段代码 case MotionEvent.ACTION_UP: if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } mActivePointerId = INVALID_POINTER; endDrag(); } break; 只要速度超过mMinimumVelocity,那就会调用flingWithNestedDispatch(),实际上就是调用mScroller.fling()。mScroller.fling是一个OverScroller,OverScroller的相关知识可以参考 View的滚动与Scroller fling的时候点击一下,立刻停止 这是怎么做到的?总的来说,是通过onInterceptTouchEvent和onTouchEvent的配合,调用 mScroller.abortAnimation();来停止滚动的。 分2种case来讨论: case1 ScrollView内部的LinearLayout的width为match_parent 此时随便点一下就点到了LinearLayout内部。 先来看fling时的状态,此时手指已经抬起,endDrag()被调用,mIsBeingDragged为false。此时点击一下,会到onInterceptTouchEvent()方法。此时在LinearLayout内部,所以inChild返回true,会走到mIsBeingDragged = !mScroller.isFinished();,因为在fling,所以mScroller.isFinished()必定false,所以mIsBeingDragged为true,那么down事件就被拦截起来了。 下一步会走到onTouchEvent里。 case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); 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. */ mIsBeingDragged = !mScroller.isFinished(); if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } startNestedScroll(SCROLL_AXIS_VERTICAL); break; } 再来看onTouchEvent如何处理down事件,有下面这段代码,如果在fling,那么立刻终止,达到目的。 /* * 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; } } case2 ScrollView内部的LinearLayout的width较小,点击到LinearLayout外部 此时inChild返回false,那么onInterceptTouchEvent返回false,不拦截。但是注意,此时点到了LinearLayout外部,那么这个down事件,没有child去处理,所以还是交给ScrollView来处理,还是会走到onTouchEvent内,一样会调用mScroller.abortAnimation();方法 R.attr.scrollViewStyle是什么 在构造函数里,我们可以看到这么一段代码,默认给ScrollView,配置了scrollViewStyle,这有什么意义呢?其实就是设置了scrollbars和fadingEdge为vertical。看下边代码 public ScrollView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.scrollViewStyle); } attrs.xml内有 themes.xml内有 @style/Widget.ScrollView styles.xml内有
vertical vertical
其他 因为用了OverScroller,所以mScrollY可能是负值; Scrollview到顶部的时候下拉的晕影效果,主要是用EdgeEffect实现; 来自:CSDN-litefish http://blog.csdn.net/litefish/article/details/52127930程序员大咖整理发布,转载请联系作者获得授权
|
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |