兩個(gè)ScrollView在一起的故事

前言

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

知識(shí)要點(diǎn)

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

先來(lái)看看效果圖:

實(shí)現(xiàn)

1、自定義一個(gè)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、將兩個(gè)ScrollView繪制進(jìn)來(lái)。

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

繪制進(jìn)來(lái)我們使用了onMeasure和onLayout

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

這里我們讓TopView和BottomView分別實(shí)現(xiàn)了OnTouchListener,目的就是為了判斷,如果當(dāng)前界面在TopView時(shí),是否可以繼續(xù)往下滑動(dòng),如果當(dāng)前界面在BottomView的時(shí)候,是否可以繼續(xù)往上滑動(dòng)。

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 手滑動(dòng)距離,這個(gè)是控制布局的主要變量
        mTopView.layout(0, (int) mMoveLen, mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen);
        mBottomView.layout(0, mTopView.getMeasuredHeight() + (int) mMoveLen,
                mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen + mBottomView.getMeasuredHeight());
    }

一開始初始化的時(shí)候,就是把TopView和BottomView分別繪制進(jìn)來(lái),把BottomView繪制在TopView的下面。

3、添加事件分發(fā),也就是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:記錄當(dāng)前展示的是哪個(gè)view,0是TopView,1是BottomView
                if (canPullUp && mCurrentViewIndex == 0 && mEvents == 0) {
                    mMoveLen += (ev.getY() - mLastY);//不斷計(jì)算滑動(dòng)的距離
                    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毫秒運(yùn)動(dòng)了多少像素。如果值為500,代表 0.5秒內(nèi)運(yùn)動(dòng)了多少像素
                vt.computeCurrentVelocity(500);
                // 獲取Y方向的速度 可以通過(guò)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,就是用來(lái)計(jì)算手滑動(dòng)的速度,這個(gè)要用來(lái)當(dāng)TopView要過(guò)渡到BottomView的時(shí)候,模擬ScrollView的滾動(dòng)慣性的。

模擬ScrollView滾動(dòng)慣性關(guān)鍵就在于Scroller,我們可以看到MotionEvent.ACTION_UP的時(shí)候,有這樣的方法: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分別是往上滾動(dòng)和往下滾動(dòng)的最大值。

然后重寫computeScroll方法

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

            //往下滑動(dòng)
            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();
            }
        }
    }

這里進(jìn)行了一系列判斷,當(dāng)時(shí)也是累啊,首先是要判斷是往上滑動(dòng)還是往下滑動(dòng),再來(lái)就是判斷最大的滑動(dòng)距離,然后再進(jìn)行重寫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方法進(jìn)行View的滾動(dòng),然后對(duì)View進(jìn)行invalidate,這樣就可以達(dá)到滾動(dòng)的慣性了。

整個(gè)代碼的流程就是這樣的了,實(shí)現(xiàn)了兩個(gè)ScrollView同時(shí)在一個(gè)界面內(nèi),而且可以做到平滑的滑動(dòng),具有ScrollView本身的滑動(dòng)慣性,而且不局限于ScrollView,這要是有滾動(dòng)條的View都可以使用。

總結(jié)

整個(gè)代碼看下來(lái)是不是有點(diǎn)凌亂,我也覺得有點(diǎn)亂,不過(guò)只要有耐心點(diǎn)就可以很快理解了。

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

當(dāng)然到了這里肯定少不了源碼的分享了

Github LayoutContainer源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,939評(píng)論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,186評(píng)論 4 61
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數(shù) View 的位置由它的四個(gè)頂...
    acc8226閱讀 1,378評(píng)論 0 7
  • 休息的一個(gè)下午,而且又落雨天,我們的小貓們正在熟睡呢?你們看它是不是好萌萌噠呢?
    小kit閱讀 314評(píng)論 0 1

友情鏈接更多精彩內(nèi)容