• 从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地址

  • 相关阅读:
    JVM010JVM有哪些垃圾收集器
    MySQL005MySQL复制的原理是什么
    MySQL002MVCC解决的问题是什么
    MySQL007MySQL索引结构有哪些,各自的优劣是什么
    JVM011如何解决线上gc频繁的问题
    MySQL003MVCC实现原理是什么
    MySQL004MySQL的隔离级别有哪些
    MySQL006MySQL聚簇索引和非聚簇索引的区别
    MySQL001什么是MVCC
    MySQL008MySQL锁的类型有哪些
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/8430568.html
Copyright © 2020-2023  润新知