从0开始写MyScrollView

从0开始写MyScrollView

上篇文章对ScrollView的具体实现进行了分析,本文根据上篇分析的结果,自己动手写一个ScrollView。

step1 跟随手指滑动,很简单,重写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地址

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值