ScrollView源码分析
来源:互联网 发布:java框架面试题及答案 编辑:程序博客网 时间:2024/06/05 00:08
1, 概述
ScrollView直接继承于FrameLayout,当需要展示的内容比较多但并不是重复的item时,就会使用ScrollView。它使内容可以在垂直方向滚动显示,防止显示不全。ScrollView使用起来非常简单,但是要注意的是ScrollView中只能添加一个直接的子View,并且只能在垂直方向上滑动。
2 ScrollView绘制解析
2.1 onMeasure
ScrollView中的onMeasure方法比较简单,直接调用子view的measure方法,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { final View child = getChildAt(0); int height = getMeasuredHeight(); if (child.getMeasuredHeight() < height) { final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); height -= mPaddingTop; height -= mPaddingBottom; int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
看到了吧,只对第一个子view进行测量,然后调用其measure方法,所以一般该子view也是一个ViewGroup.
2.2 onLayout
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; //是否还未添加过window中去 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); }
也是调用父类的onLayout来完成子view的layout。
2.3 draw
public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeGlowTop != null) { final int scrollY = mScrollY; if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth() - mPaddingLeft - mPaddingRight; canvas.translate(mPaddingLeft, Math.min(0, scrollY)); mEdgeGlowTop.setSize(width, getHeight()); if (mEdgeGlowTop.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth() - mPaddingLeft - mPaddingRight; final int height = getHeight(); canvas.translate(-width + mPaddingLeft, Math.max(getScrollRange(), scrollY) + height); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); if (mEdgeGlowBottom.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } } }
依然是调用了父类的draw方法,由此可见,这三个方法基本没有对子view做什么特殊的处理。
3 触摸事件
ScrollView继承自ViewGroup的,所以触摸事件会依次调用dispatchTouchEvent() -> onInterceptTouchEvent() 若返回true,则调用 onTouchEvent方法处理触摸事件。ScrollView并没有重写dispatchTouchEvent方法,所以直接看onInterceptTouchEvent方法。
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); // 如果是移动手势并在处于拖拽阶段,直接返回true if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } /* * Don't try to intercept touch if we can't scroll anyway. */ //如果垂直方向上没有滑动,直接返回false if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { 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); // 滑动的距离 if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { // 如果yDiff大于最小滑动距离,并且是垂直滑动则认为触发了滑动手势。 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(); 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; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* 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; }
由上面分析可知,一般只有在滑动并且滑动距离大于最小值的情况下会返回true,也就是会截取触摸事件(子view就不会处理),调用onTouchEvent方法,触摸事件的大致流程是
ACTION_DOWN ->ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
直接看onTouchEvent方法的ACTION_MOVE事件,
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]; } 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. 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;
ScrollView的onOverScrolled方法如下,
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Treat animating scrolls differently; see #computeScroll() for why. if (!mScroller.isFinished()) { final int oldX = mScrollX; final int oldY = mScrollY; mScrollX = scrollX; mScrollY = scrollY; invalidateParentIfNeeded(); // 刷新界面 onScrollChanged(mScrollX, mScrollY, oldX, oldY); // 发出滑动变化的通知 // 参数,滑动之前和滑动之后x,y的位置 if (clampedY) { mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange()); } } else { super.scrollTo(scrollX, scrollY); } awakenScrollBars(); }
View 的invalidateParentIfNeeded方法会调用invalidate方法来完成界面的刷新。并且调用view的onScrollChanged方法发送通知。
4 发送通知/onScrollChanged
首先看看view中的onScrollChanged方法,
protected void onScrollChanged(int l, int t, int oldl, int oldt) { notifySubtreeAccessibilityStateChangedIfNeeded(); if (AccessibilityManager.getInstance(mContext).isEnabled()) { postSendViewScrolledAccessibilityEventCallback(); } mBackgroundSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewScrollChanged = true; } if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) { mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); } }
说白了, onScrollChange方法是给开发者调用的,是一个回调方法。如何回调呢?
mListenerInfo是view的内部类ListenerInfo对象, mOnScrollChangeListener是ListenerInfo类的一个OnScrollChangeListener接口,定义如下,
public interface OnScrollChangeListener { /** * Called when the scroll position of a view changes. * * @param v The view whose scroll position has changed. * @param scrollX Current horizontal scroll origin. * @param scrollY Current vertical scroll origin. * @param oldScrollX Previous horizontal scroll origin. * @param oldScrollY Previous vertical scroll origin. */ void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); }
一般只要继承ScrollView就可以实现onScrollChange来达到监听的目的。
- 【android】ScrollView源码分析
- Scrollview源码分析
- ScrollView源码分析
- ScrollView 源码分析(二)
- android ScrollView smoothScrollTo源码分析
- Scroll分析-附ScrollView源码分析
- Scroll原理-附ScrollView源码分析
- [Android实例] Scroll原理-附ScrollView源码分析
- [Android实例] Scroll原理-附ScrollView源码分析
- ScrollView分析
- ScrollView分析
- ScrollView分析
- 通过Android 源码分析ScrollView ‘ScrollView can host only one direct child’错误问题
- datagrid-scrollview.js源码
- ScrollView 源码解析
- oschina源码分析之侧滑菜单界面之可以拖动的ScrollView
- (源码分析)ScrollView嵌套ListView/GridView的滑动事件处理
- ScrollView 重点分析
- OpenCV--阈值分割-threshold()
- java实现DH非对称加密工具包
- 使用U盘安装ubuntu 16.04简易教程
- Unity&Shader高级篇-渲染路径(Rendering Paths)
- Intent的概念及应用(二)
- ScrollView源码分析
- 算法提高 c++_ch02_03
- Nuxt.js中使用Element-UI填坑
- 消息队列接口的简单封装
- 单源最短路 SPFA 算法模板
- POJ 2229 Sumsets(DP计数问题)
- 他们的面试风尚一
- 我读《基于WebRTC的无线实时通信QoS-QoE评估与预测》
- zoj2042