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来达到监听的目的。

0 0
原创粉丝点击