重寫ViewGroup並藉助ViewDragHelper實現各種拖拽互動效果(二)

Dway發表於2018-01-30

本文是《重寫ViewGroup並藉助ViewDragHelper實現各種拖拽互動效果(一)》http://blog.csdn.net/lin_dianwei/article/details/79166466 的延續,針對(一)中存在的問題繼續優化。

一、前面SlideUpLayout控制元件在使用時,如果包含有ListView或RecyclerView等列表的時候,可能存在這樣的問題,直接看圖:


其中xml檔案如下:

<com.dway.testwork.viewdrag.SlideUpLayout
        android:id="@+id/slide_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible">

        <FrameLayout
            android:id="@+id/slide_layout_up"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#1f00ffff">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="up"/>
        </FrameLayout>
        <FrameLayout
            android:id="@+id/slide_layout_down"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#1fffff00">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="down"/>
            <ListView
                android:id="@+id/slide_layout_down_list"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </FrameLayout>
        <FrameLayout
            android:id="@+id/slide_layout_slide"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:clickable="true"
            android:background="#1fff00ff">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="slide"/>
        </FrameLayout>

    </com.dway.testwork.viewdrag.SlideUpLayout>

二、原因經分析是事件分派攔截存在著問題,還記得上一篇文章中,事件的攔截是完全交給ViewDragHelper處理的,如下:

@Override  
    public boolean onInterceptTouchEvent(MotionEvent ev) {  
        // 把事件處理交給ViewDragHelper  
        return mHelper.shouldInterceptTouchEvent(ev);  
    }
因為把攔截交給了ViewDragHelper,那麼包含的ListView等就無法消費事件,從而無法正常滾動。所以解決方法就是在這個方法中先對ListView進行判斷看是否需要消費,需要消費事件則把事件交給ListView處理。

三、此處穿插科普下事件分派攔截的原理:(此處是Copy過來的,放這裡幫助理解)

dispatchTouchEvent
用於touch事件的分發;通俗點說,就是決定當前這個touch事件應該交給誰來處理(是當前View還是父View)
當觸控事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會從根元素依次往下傳遞直到最內層子元素或在中間某一元素中事件被攔截或者消費.
如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;這樣該View的onTouchEvent事件也不會得到響應. 如果return false,會將事件返回給父 View 的 onTouchEvent 進行消費。 如果 return super.dispatchTouchEvent(ev),事件會分發給當前 View 的 onInterceptTouchEvent 方法去進行處理。

onInterceptTouchEvent
用於touch事件的攔截;通俗點說,就是決定剛剛 dispatchTouchEvent 拋給我的touch事件應該交給誰來處理(是當前View還是子View)注意跟dispatchTouchEvent的區別
如果 return true,則將事件進行攔截,並將攔截到的事件交由該 View 的 onTouchEvent 進行處理; 如果 return false,則將事件向子View傳遞,再由子View的 dispatchTouchEvent來對這個事件處理; 如果 return super.onInterceptTouchEvent(ev),事件會被攔截,並將事件交由該 View 的 onTouchEvent 進行處理。

onTouchEvent
用於touch事件的處理;通俗點說,就是決定剛剛 onInterceptTouchEvent 拋給我的touch事件進行處理。
如果return false,那麼這個事件會從該 View 向父View傳遞,父 View 的 onTouchEvent 來接收,而且如果父View也是return false,那事件也會向上傳遞由onTouchEvent接收處理. 如果retrun true, 則會接收並消費該事件。 如果retrun super.onTouchEvent(ev) 和返回 false 時相同。


四、有了思路,那麼解決方法就是在攔截事件之前,先判斷下ListView是否需要消費事件,即是否還能繼續往下滾動,即是否已經到頂部了。所以優化後的SlideUpLayout如下:

package com.dway.testwork.viewdrag;

import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * 可上下切換,類似縱向的ViewPager,並且上劃時向下彈出選單效果
 * Created by dway on 2018/1/23.
 */

public class SlideUpLayout extends ViewGroup {

    private View mUpView;
    private View mDownView;
    private View mSlideView;
    //private RecyclerView mListView;

    private ViewDragHelper mHelper;

    //上下滑的程度,0表示在upView,1表示在downView
    private float mSlidePercent = 0;

    private boolean mInLayout = false;
    private boolean mFirstLayout = true;


    public SlideUpLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                if(child == mUpView){
                    return Math.max(- mUpView.getMeasuredHeight() + mSlideView.getMeasuredHeight(), Math.min(top, 0));
                }else if(child == mDownView){
                    return Math.max(mSlideView.getMeasuredHeight(), Math.min(top, mUpView.getMeasuredHeight()));
                }else if(child == mSlideView){
                    return Math.max(- mSlideView.getMeasuredHeight(), Math.min(top, 0));
                }
                return 0;
            }

            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return child == mUpView || child == mDownView;
            }

            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                if(releasedChild == mUpView){
                    int upViewHeight = mUpView.getMeasuredHeight();
                    int slideViewHeight = mSlideView.getMeasuredHeight();
                    float offset = (upViewHeight + releasedChild.getTop() - slideViewHeight) * 1.0f / (upViewHeight - slideViewHeight);
                    mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? 0 : -upViewHeight + slideViewHeight);
                    invalidate();
                }else if(releasedChild == mDownView){
                    int downViewHeight = mDownView.getMeasuredHeight();
                    int slideViewHeight = mSlideView.getMeasuredHeight();
                    float offset = (releasedChild.getTop() - slideViewHeight) * 1.0f / downViewHeight;
                    mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? mUpView.getMeasuredHeight() : slideViewHeight);
                    invalidate();
                }
            }

            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                if(changedView == mUpView){
                    mDownView.setTop(top + mUpView.getMeasuredHeight());

                    mSlidePercent = (float) (-top) / mDownView.getMeasuredHeight();
                    if(mSlidePercent > 0.9f){
                        mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
                    }else{
                        mSlideView.setTop(-mSlideView.getMeasuredHeight());
                    }
                    requestLayout();
                    if(mOnSlideListener != null){
                        mOnSlideListener.onSlide(mSlidePercent);
                    }
                }else if(changedView == mDownView){
                    mUpView.setTop(top - mUpView.getMeasuredHeight());

                    mSlidePercent = (float) (mUpView.getMeasuredHeight() - top) / mDownView.getMeasuredHeight();
                    if(mSlidePercent > 0.9f){
                        mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
                    }else{
                        mSlideView.setTop(-mSlideView.getMeasuredHeight());
                    }
                    requestLayout();
                    if(mOnSlideListener != null){
                        mOnSlideListener.onSlide(mSlidePercent);
                    }
                }
            }

            @Override
            public int getViewVerticalDragRange(View child) {
                return child == mUpView ? mUpView.getMeasuredHeight() - mSlideView.getMeasuredHeight() :
                        child == mDownView ? mDownView.getMeasuredHeight() :
                                child == mSlideView ? mSlideView.getMeasuredHeight() : 0;
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(widthSize, heightSize);

        mUpView = getChildAt(0);
        mDownView = getChildAt(1);
        mSlideView = getChildAt(2);
        //mListView = mDownView.findViewById(R.id.rv_select_course);


        //up
        MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
        int widthSpec = MeasureSpec.makeMeasureSpec(
                widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
        int heightSpec = MeasureSpec.makeMeasureSpec(
                heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
        mUpView.measure(widthSpec, heightSpec);

        //slide
        lp = (MarginLayoutParams) mSlideView.getLayoutParams();
        widthSpec = getChildMeasureSpec(widthMeasureSpec,
                lp.leftMargin + lp.rightMargin, lp.width);
        heightSpec = getChildMeasureSpec(heightMeasureSpec,
                lp.topMargin + lp.bottomMargin, lp.height);
        mSlideView.measure(widthSpec, heightSpec);

        //down
        lp = (MarginLayoutParams) mDownView.getLayoutParams();
        widthSpec = MeasureSpec.makeMeasureSpec(
                widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
        heightSpec = MeasureSpec.makeMeasureSpec(
                heightSize - lp.topMargin - lp.bottomMargin - mSlideView.getMeasuredHeight(), MeasureSpec.EXACTLY);
        mDownView.measure(widthSpec, heightSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout = true;
        MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
        int upTop;
        if(mFirstLayout){
            upTop = lp.topMargin;
        }else{
            upTop = mUpView.getTop();
        }
        mUpView.layout(lp.leftMargin, upTop,
                lp.leftMargin + mUpView.getMeasuredWidth(),
                upTop + mUpView.getMeasuredHeight());

        lp = (MarginLayoutParams) mDownView.getLayoutParams();
        int downTop;
        if(mFirstLayout){
            downTop = mUpView.getMeasuredHeight() + lp.topMargin;
        }else{
            downTop = mDownView.getTop();
        }
        mDownView.layout(lp.leftMargin, downTop,
                lp.leftMargin + mDownView.getMeasuredWidth(),
                downTop + mDownView.getMeasuredHeight());

        lp = (MarginLayoutParams) mSlideView.getLayoutParams();
        int slideTop;
        if(mFirstLayout){
            slideTop = - mSlideView.getMeasuredHeight() + lp.topMargin;
        }else{
            slideTop = mSlideView.getTop();
        }
        mSlideView.layout(lp.leftMargin, slideTop,
                lp.leftMargin + mSlideView.getMeasuredWidth(),
                slideTop + mSlideView.getMeasuredHeight());


        mInLayout = false;
        mFirstLayout = false;
    }


    @Override
    public void requestLayout() {
        if(!mInLayout) {
            super.requestLayout();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mFirstLayout = true;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mFirstLayout = true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //return mHelper.shouldInterceptTouchEvent(ev);

        //方法一:還需要根據座標是否是mListView的範圍,進行控制。該方式更直接,理論上效率會更高
        /*if(isInViewArea(mListView, ev.getRawX(), ev.getRawY()) && mListView.canScrollVertically(-1)){
            //listview可以滾動則交給子view決定
            return false;
        }else{
            //已經滾動到頂部,則交給ViewDragHelper處理
            return mHelper.shouldInterceptTouchEvent(ev);
        }*/

        //方法二:把以上的處理改為更一般的情況,遞迴獲取每個子view進行判斷處理
        if(hasViewCanScrollUp(mDownView, ev.getRawX(), ev.getRawY())){
            return false;
        }else{
            return mHelper.shouldInterceptTouchEvent(ev);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mHelper.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll() {
        if (mHelper.continueSettling(true)) {
            invalidate();
        }
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    /**
     * 判斷指定點所在的view是否未到頂可以繼續上劃,如果view為ViewGroup的話還得遞迴往子view判斷進去
     */
    private boolean hasViewCanScrollUp(View view, float x, float y){
        if(view instanceof ViewGroup){
            ViewGroup viewGroup = (ViewGroup) view;
            for(int i=0; i<viewGroup.getChildCount(); i++){
                View child = viewGroup.getChildAt(i);
                if(hasViewCanScrollUp(child, x, y)){
                    return true;
                }
            }
            return isInViewArea(view, x, y) && view.canScrollVertically(-1);
        }else{
            return isInViewArea(view, x, y) && view.canScrollVertically(-1);
        }
    }

    /**
     * 判斷座標是否在view區域內
     */
    private boolean isInViewArea(View view, float x, float y){
        int[] local = new int[2];
        view.getLocationOnScreen(local);
        return x > local[0] && x < local[0]+view.getMeasuredWidth() && y > local[1] && y < local[1]+view.getMeasuredHeight();
    }

    public float getSlidePercent(){
        return mSlidePercent;
    }

    public boolean isSlideUp(){
        return floatCompare(mSlidePercent, 0);
    }

    public boolean isSlideDown(){
        return floatCompare(mSlidePercent, 1);
    }

    public void slideToDown(){
        mHelper.smoothSlideViewTo(mDownView, mDownView.getLeft(), mSlideView.getMeasuredHeight());
        invalidate();
    }

    public void slideToUp(){
        mHelper.smoothSlideViewTo(mUpView, mUpView.getLeft(), 0);
        invalidate();
    }


    private OnSlideListener mOnSlideListener = null;

    /**
     * 外部可設定監聽slide的位置回撥
     */
    public void setOnSlideListener(OnSlideListener listener) {
        mOnSlideListener = listener;
    }

    public interface OnSlideListener{
        /**
         * slide回撥
         * @param percent 取值區間[0, 1],0代表滑到最頂部,1代表滑到最底部
         */
        void onSlide(float percent);
    }


    private boolean floatCompare(float f1, float f2){
        return Math.abs(f1 - f2) < Float.MIN_VALUE;
    }

}
註釋都在程式碼中,有兩種方法,一種比較直接,直接判斷ListView。第二種是把方法一般化,遞迴判斷每一個子view。注意其中view位置的判斷和view是否能滾動的判斷方法。另外程式碼也新增了滾動的監聽。

五、最終效果圖:完美





相關文章