重写ViewGroup并借助ViewDragHelper实现各种拖拽交互效果(二)

本文是《重写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是否能滚动的判断方法。另外代码也添加了滚动的监听。

五、最终效果图:完美





猜你喜欢

转载自blog.csdn.net/lin_dianwei/article/details/79210877