从0開始写MyScrollView

从0開始写MyScrollView

上篇文章对ScrollView的详细实现进行了分析。本文依据上篇分析的结果。自己动手写一个ScrollView。

step1 尾随手指滑动,非常easy。重写2个函数就好了

简单的滑动,仅仅要重写onTouchEvent就能够了。然后我们须要内部的LinearLayout高度能够超出MyScrollView,那就在measure过程中进行处理,重写measureChildWithMargins就能够了。


/**
 * Created by fish on 16/8/2.
 */
public class MyScrollView extends FrameLayout {

    private boolean mIsBeingDragged = false;
    /**
     * Position of the last motion event.
     */
    private int mLastMotionY;
    private int mTouchSlop;


    public MyScrollView(Context context) {
        this(context, null);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initScrollView();
    }

    private void initScrollView() {
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
    }


    //让内部的LinearLayout高度能够非常大非常大
    @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,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = (int) event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int delta = (int) (event.getY() - mLastMotionY);
                if (mIsBeingDragged) {
                    scrollBy(0, -delta);
                    mLastMotionY= (int) event.getY();
                } else if (Math.abs(delta) > mTouchSlop) {
                    mIsBeingDragged = true;
                    mLastMotionY= (int) event.getY();
                    scrollBy(0, -delta);
                }
                break;

            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                break;
        }

        return true;
    }
}

step2 增加scrollbar

When you create a custom view you need to do the following to support
scrollbars:
- Enable the scrollbars
- Override the various compute*ScrollOffset, compute*ScrollRange(), etc. to
return sensible values
- Call awakenScrollbars() when you want to display the scrollbars (this is
called by the scroll methods in View as well)
http://markmail.org/thread/n7wv2rvgre3talba

要重写computeVerticalScrollOffset。computeVerticalScrollRange,初始化的时候调用setWillNotDraw(false);(为什么要setWillNotDraw(false)呢。由于默认ViewGroup是不绘制的,仅仅是个容器,可是这里要画滑块。所以得setWillNotDraw(false))
以上几点还不够。还得配置view的style属性。

从上篇文章我们知道ScrollView还配置了com.android.internal.R.attr.scrollViewStyle。 那我们怎样增加这个默认的style呢?我们知道这个style本质上是Widget.ScrollView,所以能够这样, style=”@android:style/Widget.ScrollView”非常关键,直接把style指定。

跟自己定义属性相关的知识能够參考http://blog.csdn.net/lmj623565791/article/details/45022631。写的非常好。

    <com.fish.myscrollviewpractise.MyScrollView
        style="@android:style/Widget.ScrollView"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1">

        <LinearLayout
            android:id="@+id/linear1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        </LinearLayout>
    </com.fish.myscrollviewpractise.MyScrollView>

好了,此时scrollbar已经有了
话说回来。我们有必要搞清楚,为什么这样子就有scrollbar了
先看下scrollbar是什么时候调用的,调用图例如以下
NestedScrollingChild

//View#onDrawScrollBars
scrollBar.setParameters(computeVerticalScrollRange(),
                                            computeVerticalScrollOffset(),
                                            computeVerticalScrollExtent(), true);

在view的onDrawScrollBars内部。须要setParameters。此时调用computeVerticalScrollRange和computeVerticalScrollOffset。这2个函数,我们进行重写。

    @Override
    protected int computeVerticalScrollOffset() {
//        LogUtil.fish("computeVerticalScrollOffset");
//这么写是考虑了OverScroller的情况
        return Math.max(0, super.computeVerticalScrollOffset());
    }

    @Override
    protected int computeVerticalScrollRange() {
//        LogUtil.fish("computeVerticalScrollRange");
        final int count = getChildCount();
        final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        if (count == 0) {
            return contentHeight;
        }

        int scrollRange = getChildAt(0).getBottom();
        final int scrollY = getScrollY();
        final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
//        if (scrollY < 0) {
//            scrollRange -= scrollY;
//        } else if (scrollY > overscrollBottom) {
//            scrollRange += scrollY - overscrollBottom;
//        }

        return overscrollBottom;
    }

此时有一个问题不太理解,为什么滚动停止了。滚动栏就消失了?答案在下边,state会变为ScrollabilityCache.OFF,就不会仅仅滚动栏了。


    protected final void onDrawScrollBars(Canvas canvas) {
        // scrollbars are drawn only when the animation is running
        final ScrollabilityCache cache = mScrollCache;
        if (cache != null) {

            int state = cache.state;

            if (state == ScrollabilityCache.OFF) {
            //滚好了就会走到这里。那就不调用               onDrawVerticalScrollBar,所以不绘制滚动栏
                return;
            }
            。。。
             scrollBar.setParameters(computeHorizontalScrollRange(),
                                  ![]()          computeHorizontalScrollOffset(),
                                            computeHorizontalScrollExtent(), false);
            。

。 onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom); 。。。

step3 滚完不要立马停下来,依据惯性再滚一会

速度达到一定程度。才会有惯性滚动,所以我们要检測速度,增加VelocityTracker。假设不熟悉VelocityTracker能够參考VelocityTracker

我们增加了

private VelocityTracker mVelocityTracker;

private Scroller mScroller;

在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)) {
                        mScroller.startScroll(getScrollX(), getScrollY(), 0, initialVelocity > 0 ? -300 : 300, 4000);
                        invalidate();
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;
    private void endDrag() {
        mIsBeingDragged = false;
        recycleVelocityTracker();
    }

step4 Scroller改为OverScroller

依据官方建议把Scroller改为OverScroller,增加fling代码。


看下边代码,把overY设置为height / 2。

overY代表能够超出边界多大距离,height / 2事实上这是比較大的一个值,滑的时候会导致超过边界较多距离。而原生是ScrollView不会超过边界非常多距离,这是为什么?
假设我们想要超过边界的距离小一点全然能够把这个值改小,比方改为100,这个地方写height / 2我也认为非常奇怪,暂且无论。

 mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height / 2);

step5 滚动的时候考虑边界,增加onScrollChanged

之前,我们直接用scrollTo,没有考虑边界的问题。
此时其有用overScrollBy比較合适。overScrollBy()会考虑边界以及over区域。

overScrollBy()是view的方法。会回调onOverScrolled()。所以我们还须要重写onOverScrolled().onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY)这个函数是在overScrollBy内部调用的,overScrollBy会依据边界值以及over值计算出合适的scrollX和scrollY,而clampedX和clampedY代表着scrollX和scrollY的值是否被裁剪过(超出上下限就会被裁剪),假设被裁剪过overScrollBy的返回值就是true。否则就是false。
主要代码例如以下所看到的:

   @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {

            int oldX = getScrollX();
            int oldY = getScrollY();
            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(getScrollX(), getScrollY(), oldX, oldY);

            }

            postInvalidate();
        }
    }

    @Override
    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 = getScrollX();
            final int oldY = getScrollY();
            setScrollX(scrollX);
            setScrollY(scrollY);
//            invalidateParentIfNeeded();
            //源代码里有这句,可是我认为不是必需写。

onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); if (clampedY) { mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange()); } } else { super.scrollTo(scrollX, scrollY); } awakenScrollBars(); }

主要解释3点,
第一。onOverScrolled()的2个分支是怎么回事?普通滑动调用的是下边super.scrollTo(scrollX, scrollY);fling走的是上边,假设超出边界须要用mScroller.springBack来复位。


第二,onOverScrolled里面为什么调用awakenScrollBars(),这句话的作用是要求绘制的时候加上scrollBar,曾经我们不写这句话是由于scrollTo()方法内部包括了这句话
第三,onOverScrolled里面有这句话onScrollChanged。事实上是不是必需的,由于在computeScroll是会调用的。所以反复了。可是呢,写这个也有一点优点,那就是我们监控onScrollChanged的时候,假设发现同样的值出现了2次,那我们就知道这是出于惯性滑动的状态(fling)

step6 move事件也用overScrollBy处理

这是为了解决一个问题,曾经拉到顶部了,还能够继续下拉

            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 (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;

                    //把deltaY弄小一点,这事实上无所谓的
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y;

//                    final int oldY = getScrollY();
                    final int range = getScrollRange();
//                    final int overscrollMode = getOverScrollMode();

                    // Calling overScrollBy will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollBy(0, deltaY, 0, getScrollY(), 0, range, 0, mOverscrollDistance, true)) {
                        //被裁剪了说明滑到头了。此时清除mVelocityTracker,是为了up的时候计算不出速度。速度为0,就没有fling了
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }


                }

step7 边缘拉的时候增加晕影效果

ScrollView边缘拉的时候有晕影效果。这是怎么做到的呢?
EdgeEffect。增加此效果,主要四步
第一步,在View初始化的时候。会调用setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);
我们重写此函数,在内部构造mEdgeGlowTop和mEdgeGlowTop

  //在view的init里面被调用
    @Override
    public void setOverScrollMode(int mode) {
        if (mode != OVER_SCROLL_NEVER) {
            if (mEdgeGlowTop == null) {
                Context context = getContext();
                mEdgeGlowTop = new EdgeEffect(context);
                mEdgeGlowBottom = new EdgeEffect(context);
            }
        } else {
            mEdgeGlowTop = null;
            mEdgeGlowBottom = null;
        }
        super.setOverScrollMode(mode);
    }

第二步,在computeScroll内增加mEdgeGlowTop.onAbsorb。onAbsorb是初始化一堆參数为后面的draw做准备

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {

            int oldX = getScrollX();
            int oldY = getScrollY();
            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(getScrollX(), getScrollY(), 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());
                    }
                }

            }

            postInvalidate();
        }
    }

第三步,重写onDraw()。增加绘制mEdgeGlowTop和mEdgeGlowBottom的代码。此处代码抄自ScrollView。
第四步,在endDrag的时候进行release。这是和onAbsorb相应的。清除各种数据

        if (mEdgeGlowTop != null) {
            mEdgeGlowTop.onRelease();
            mEdgeGlowBottom.onRelease();
        }

第五步,在onTouchevent的move事件里,对下拉。上拉做响应,调用mEdgeGlowTop.onPull,呈现出拖拽效果

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();
                        }
                    }

step8 增加onInterceptTouchEvent

这部分代码不难理解,可是实际调用的机会比較少,主要实现2个功能。child处理了down,我能够抢个move(假设够大的话);配合onTouchevent实现fling时点击停止。

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * 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;
        }

        /*
         * Don't try to intercept touch if we can't scroll anyway.
         */
        if (getScrollY() == 0 && !canScrollVertically(1)) {
            return false;
        }

        switch (action & MotionEvent.ACTION_MASK) {
            //down事件child处理的。我有权截获move事件
            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);
                if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);

                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }

            //配合完毕fling时。点击停止滚动
            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();

                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /* Release the drag */
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                recycleVelocityTracker();
                if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                    postInvalidateOnAnimation();
                }
                break;

        }

        /*
        * The only time we want to intercept motion events is if we are in the
        * drag mode.
        */
        return mIsBeingDragged;

    }

step9 增加cancel事件处理。增加requestDisallowInterceptTouchEvent

cancel事件。就是收到前驱事件,后边的事件被parent抢走了,此时触发cancel,进行重置处理。
requestDisallowInterceptTouchEvent就是请求parent放过事件,都给我吧。
相关代码例如以下

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept) {
            recycleVelocityTracker();
        }
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
          //onTouchEvent
          //假设cancel了就结束滚动
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
                    if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                        postInvalidateOnAnimation();
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }

OK。此时大功告成,一个可用的ScrollView已经完毕了,功能有滚时显示滑块。普通滑动。惯性滑动,fling时点击停止,滚动能够超出边界并回弹,到达边界是有晕影效果等功能。


github地址

posted on 2018-02-08 10:55  yjbjingcha  阅读(211)  评论(0编辑  收藏  举报

导航