兩個ScrollView在一起的故事

lpwngw228s 7年前發布 | 5K 次閱讀 Android開發 移動開發 ScrollView

前言

很多時候,作為程序猿的我們都會接到產品的奇奇怪怪的需求,比如我們正要說的,要在同一個界面使用兩個ScrollView,這個兩個ScrollView不是并列的,而是在垂直方向的哦。這時你的心里肯定有千萬只草泥馬飛奔而過,這坑爹需求,怎么可能兩個ScrollView同時使用啊,一個界面里有兩個ScrollView不是滑動各種沖突了啊。別急,這篇文章就是為了幫你解決這樣的變態需求的,且聽我一一道來。

知識要點

首先,你必須掌握如何自定義View,然后還要熟悉事件的傳遞機制,再來就是怎么使用Scroller,最后還要知道VelocityTracker(計算手勢滑動的速度),掌握這些知識之后,看起這篇文章來將水到渠成了,如果還不是很了解或者熟悉的同學,就先自行Google或者度娘下了,如果有需要,我也會把相關的文章寫下來的。

先來看看效果圖:

實現

1、自定義一個Relativelayout。

public class LayoutContainer extends RelativeLayout {
    public LayoutContainer(Context context) {
        super(context);
    }

    public LayoutContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LayoutContainer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

然后我們一步一步往里面添加代碼了。

2、將兩個ScrollView繪制進來。

我們把這兩個View分別定義為mTopView和mBottomView。

繪制進來我們使用了onMeasure和onLayout

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //isMeasured的存在是只onMeasure一次,不讓requestLayout調用的時候再次onMeasure
        if (!isMeasured) {
            isMeasured = true;
            mViewHeight = getMeasuredHeight();
            mViewWidth = getMeasuredWidth();
            mTopView = getChildAt(0);
            mBottomView = getChildAt(1);
            mBottomView.setOnTouchListener(bottomViewTouchListener);
            mTopView.setOnTouchListener(topViewTouchListener);
        }
    }

這里我們讓TopView和BottomView分別實現了OnTouchListener,目的就是為了判斷,如果當前界面在TopView時,是否可以繼續往下滑動,如果當前界面在BottomView的時候,是否可以繼續往上滑動。

private OnTouchListener topViewTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ScrollView sv = (ScrollView) v;
            //判斷是否在最底部
            if (sv.getScrollY() == (sv.getChildAt(0).getMeasuredHeight() - sv.getMeasuredHeight()) && mCurrentViewIndex == 0) {
                canPullUp = true;
            } else {
                canPullUp = false;
            }
            return mCanScroll;
        }
    };

    private OnTouchListener bottomViewTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ScrollView sv = (ScrollView) v;
            //判斷是否在最頂部
            if (sv.getScrollY() == 0 && mCurrentViewIndex == 1) {
                canPullDown = true;
            } else {
                canPullDown = false;
            }
            return mCanScroll;
        }
    };
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //mMoveLen 手滑動距離,這個是控制布局的主要變量
        mTopView.layout(0, (int) mMoveLen, mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen);
        mBottomView.layout(0, mTopView.getMeasuredHeight() + (int) mMoveLen,
                mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen + mBottomView.getMeasuredHeight());
    }

一開始初始化的時候,就是把TopView和BottomView分別繪制進來,把BottomView繪制在TopView的下面。

3、添加事件分發,也就是dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                if (vt == null) {
                    vt = VelocityTracker.obtain();
                } else {
                    vt.clear();
                }
                mLastY = ev.getY();
                mLastX = ev.getX();
                mTempLastY = ev.getY();
                vt.addMovement(ev);
                mEvents = 0;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
                mEvents = -1;
                break;
            case MotionEvent.ACTION_MOVE:
                vt.addMovement(ev);
                //mCurrentViewIndex:記錄當前展示的是哪個view,0是TopView,1是BottomView
                if (canPullUp && mCurrentViewIndex == 0 && mEvents == 0) {
                    mMoveLen += (ev.getY() - mLastY);//不斷計算滑動的距離
                    if (mMoveLen > 0) {
                        mMoveLen = 0;
                        mCurrentViewIndex = 0;
                    } else if (mMoveLen < -mViewHeight) {
                        mMoveLen = -mViewHeight;
                        mCurrentViewIndex = 1;
                    }
                    if (mMoveLen < -5) {
                        // 防止事件沖突
                        mCanScroll = true;
                    } else {
                        mCanScroll = false;
                    }
                } else if (canPullDown && mCurrentViewIndex == 1 && mEvents == 0) {
                    mMoveLen += (ev.getY() - mLastY);
                    if (mMoveLen < -mViewHeight) {
                        mMoveLen = -mViewHeight;
                        mCurrentViewIndex = 1;
                    } else if (mMoveLen > 0) {
                        mMoveLen = 0;
                        mCurrentViewIndex = 0;
                    }
                    if (mMoveLen > 5 - mViewHeight) {
                        // 防止事件沖突
                        mCanScroll = true;
                    } else {
                        mCanScroll = false;
                    }
                } else {
                    mCanScroll = false;
                    mEvents++;
                }
                mLastY = ev.getY();
                requestLayout();
                break;
            case MotionEvent.ACTION_UP:
                double deltaX = Math.sqrt((ev.getX() - mLastX) * (ev.getX() - mLastX) + (ev.getY() - mTempLastY) * (ev.getY() - mTempLastY));
                if (deltaX < 10) {
                    mIsClick = true;
                }
                if (mIsClick) {
                    mIsClick = false;
                    break;
                }
                mLastY = ev.getY();
                vt.addMovement(ev);
                //你想要指定的得到的速度單位,如果值為1,代表1毫秒運動了多少像素。如果值為500,代表 0.5秒內運動了多少像素
                vt.computeCurrentVelocity(500);
                // 獲取Y方向的速度 可以通過getXVelocity()和getYVelocity()獲得橫向和豎向的速率
                int initialVelocity = (int) vt.getYVelocity();
                if (mMoveLen != 0 && mMoveLen != mTempMoveLen) {
                    fling(-initialVelocity);
                }
                mTempMoveLen = mMoveLen;
                try {
                    vt.recycle();
                    vt = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
        super.dispatchTouchEvent(ev);
        return super.dispatchTouchEvent(ev);
    }

這里可以看到最后是return super.dispatchTouchEvent(ev),這是為了讓子View也可以接收到事件。

這里的VelocityTracker,就是用來計算手滑動的速度,這個要用來當TopView要過渡到BottomView的時候,模擬ScrollView的滾動慣性的。

模擬ScrollView滾動慣性關鍵就在于Scroller,我們可以看到MotionEvent.ACTION_UP的時候,有這樣的方法:fling(-initialVelocity);

public void fling(int velocityY) {
        if (getChildCount() > 0) {
            mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, -700, 700);
            awakenScrollBars(mScroller.getDuration());
            invalidate();
        }
    }

這里的 -700和700分別是往上滾動和往下滾動的最大值。

然后重寫computeScroll方法

/**
     * 當scrollY > 0時,是往下滑動,當scrollY < 0時,是往上滑動
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset() && mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
            int scrollX = mScroller.getCurrX();
            int scrollY = mScroller.getCurrY();
            //往上滑動
            if (scrollY > 0 && oldY < 0) {
                oldY = 0;
            }
            if (scrollY > 0 && scrollY < oldY) {
                oldY = 0;
            }

            //往下滑動
            if (scrollY < 0 && scrollY > oldY) {
                oldY = 0;
            }
            if (scrollY < 0 && oldY > 0) {
                oldY = 0;
            }
            if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
                scrollTo(scrollX, scrollY);
            }
            if (scrollY > 0) {
                mMoveLen = mMoveLen - scrollY + oldY;
            }
            if (scrollY < 0) {
                mMoveLen = mMoveLen - scrollY + oldY;
            }
            if (mMoveLen > 0) {
                mMoveLen = 0;
            }
            if (mMoveLen <= (-mScreenH + getStatusBarHeight(getContext()))) {
                mMoveLen = -mScreenH + getStatusBarHeight(getContext());
            }
            oldY = scrollY;
            if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
                requestLayout();
            }
        }
    }

這里進行了一系列判斷,當時也是累啊,首先是要判斷是往上滑動還是往下滑動,再來就是判斷最大的滑動距離,然后再進行重寫scrollTo方法。

@Override
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                invalidate();
            }
        }
    }

這里使用到的是onScrollChanged方法進行View的滾動,然后對View進行invalidate,這樣就可以達到滾動的慣性了。

整個代碼的流程就是這樣的了,實現了兩個ScrollView同時在一個界面內,而且可以做到平滑的滑動,具有ScrollView本身的滑動慣性,而且不局限于ScrollView,這要是有滾動條的View都可以使用。

總結

整個代碼看下來是不是有點凌亂,我也覺得有點亂,不過只要有耐心點就可以很快理解了。

首先,一個自定義RelativeLayout,然后把兩個包含ScrollView的子View同時繪制在這個自定義的RelativeLayout內,并且是上下排列,然后重寫dispatchTouchEvent方法,對事件進行處理,主要是在TopView和BottomView過渡的時候進行處理,要判斷是TopView往BottomView過渡還是BottomView往TopView過渡,這個是關鍵點,最后就使用到Scroller進行滾動慣性的模擬,需要重寫View的computeScroll和scrollTo兩個方法,然后在滾動的時候需要判斷是否超出了滾動的距離,還有滾動的方向。

 

 

來自:http://www.jianshu.com/p/0fa34bbd0b65

 

 本文由用戶 lpwngw228s 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!