Android 手势处理实战

手势处理实战

问题提出

  • 显示实战场景
  • ViewPager+ViewFlow\ViewPager
  • NestedScrollView+RecyclerView

核心内容

  • 场景:看图说话
  • Android 手势处理实战_第1张图片
  • 问题分析:同上面图中,我们可以看出可以通过手势处理的控件互相嵌套,需要处理的手势问题可能只是看看都会让自己头大,但是如果我们将手势简单分类,来看看问题会不会简单很多,我们的分类依据是:水平方向滑动手势和垂直方向滑动手势。然后我们按照这种依据将控件分类:水平滑动控件ParentViewPager\ViewFlow\ChildViewPager;垂直滑动控件:NestedScrollView\RecyclerView;这样看问题是不是轻松就转移到了解决ViewPager嵌套ViewFlow和ViewPager的手势冲突,以及NestedScrollView签到RecyclerView的滑动冲突上了呢。下面我们就来意义分解
  • 首先,解决手势冲突问题,你要先明白一个问题,就是Android的手势处理机制,关于这个手势处理机制又可以长篇大论的写一篇博客了,在这我把他总结成一句话就行:手势触发按照从上而下分发传递,然后按照近似从下而上返回的策略,首先事件交给最顶层的“控件”处理(这里排除了Window和Activity的干预),此控件一般为ViewGroup及其衍生类,ViewGroup不做拦截处理,则交由起嵌套的子控件(可以是ViewGroup及其衍生类或者View及其衍生类)处理,如果子控件也不拦截处理则返还给最顶层控件处理,如果子控件拦截处理,则不会再向下或者向上传递此事件。”
  • 通过上面的理论补充,那在实战中怎么考虑问题呢,简单描述一下我的理解吧:如果都是同一方向的手势,两个方向都充写了手势操作处理,这时候按照父控件还是子控件接管用户手势可分为两类,刚好这里都涉及到了,其中,ViewPager嵌套ViewFlow\ViewPager嵌套ViewPager代表了代表了当用户手势在子控件上面是不允许父控件手势处理,而NestedScrollView+RecyclerView属于父控件接管用户手势处理,而子控件不消费用户手势此操作。
  • ViewPager+ViewFlow:问题:滑动ViewFlow出现卡顿不流畅问题,我们的目的是用户操作ViewFlow的时候ViewPager不要干预,按照我们之前的套路,我们通常会重写ViewFlow的OnTouchXXX处理手势,但是像这种ViewFlow控件的手势封装已经很成熟了,我们从问题的本质上去思考,我们是不是只要在ViewFlow滑动的时候不让ViewPager也同步滑动处理就可以达到目的呢?换句话说我们只要将ViewFlow和嵌套他的ViewPager对象关联起来就可以同步控制两个控件在水平方向上的滑动互不干扰,在细致一点来瞄一眼代码吧,这样的方式有点像TabLayout+ViewPager的感觉有没有,其实我在实战中也用了Tablayout但是这里的核心不是它们:

package com.czh.kuihuajingyingwang.customwidgets;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * Created by J_X on 2016/9/27.
 * 用来解决与ViewPager的滑动冲突
 */

public class MyViewFlow extends ViewFlow {
private ViewPager mPager;

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


public void setViewPager(ViewPager viewPager) {
    mPager = viewPager;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (mPager != null)
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPager.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
                mPager.requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_CANCEL:
                mPager.requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_MOVE:
                mPager.requestDisallowInterceptTouchEvent(true);
                break;
        }
    return super.onInterceptTouchEvent(ev);
}
}
  • ViewPager+ViewPager:按照惯性思维,在上面的问题解决之后,我们通常会想要按照上面的套路来解决这个问题,but再思考一下ViewPager+ViewPager的组合和上面有什么不一样的地方,那就是ViewPager+ViewPager两者是完全一样的控件,也就是说他们的手势处理默认默认情况下一模一样;实际情况是,当两个ViewPager互相嵌套的时候他们的水平滑动手势操作并没互相干扰,Why?我们来瞄一眼ViewPager的源码:

 @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.
     */

    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    // Always take care of the touch gesture being complete.
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        // Release the drag.
        if (DEBUG) Log.v(TAG, "Intercept done!");
        resetTouch();
        return false;
    }

    // Nothing more to do here if we have decided whether or not we
    // are dragging.
    if (action != MotionEvent.ACTION_DOWN) {
        if (mIsBeingDragged) {
            if (DEBUG) Log.v(TAG, "Intercept returning true!");
            return true;
        }
        if (mIsUnableToDrag) {
            if (DEBUG) Log.v(TAG, "Intercept returning false!");
            return false;
        }
    }

    switch (action) {
        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);
            final float x = ev.getX(pointerIndex);
            final float dx = x - mLastMotionX;
            final float xDiff = Math.abs(dx);
            final float y = ev.getY(pointerIndex);
            final float yDiff = Math.abs(y - mInitialMotionY);
            if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

            if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
                    && canScroll(this, false, (int) dx, (int) x, (int) y)) {
                // Nested view has scrollable area under this point. Let it be handled there.
                mLastMotionX = x;
                mLastMotionY = y;
                mIsUnableToDrag = true;
                return false;
            }
            if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                if (DEBUG) Log.v(TAG, "Starting drag!");
                mIsBeingDragged = true;
                requestParentDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
                mLastMotionX = dx > 0
                        ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
                mLastMotionY = y;
                setScrollingCacheEnabled(true);
            } else if (yDiff > mTouchSlop) {
                // The finger has moved enough in the vertical
                // direction to be counted as a drag...  abort
                // any attempt to drag horizontally, to work correctly
                // with children that have scrolling containers.
                if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                mIsUnableToDrag = true;
            }
            if (mIsBeingDragged) {
                // Scroll to follow the motion event
                if (performDrag(x)) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
            break;
        }

        case MotionEvent.ACTION_DOWN: {
            /*
             * Remember location of down touch.
             * ACTION_DOWN always refers to pointer index 0.
             */
            mLastMotionX = mInitialMotionX = ev.getX();
            mLastMotionY = mInitialMotionY = ev.getY();
            mActivePointerId = ev.getPointerId(0);
            mIsUnableToDrag = false;

            mIsScrollStarted = true;
            mScroller.computeScrollOffset();
            if (mScrollState == SCROLL_STATE_SETTLING
                    && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                // Let the user 'catch' the pager as it animates.
                mScroller.abortAnimation();
                mPopulatePending = false;
                populate();
                mIsBeingDragged = true;
                requestParentDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
            } else {
                completeScroll(false);
                mIsBeingDragged = false;
            }

            if (DEBUG) {
                Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                        + " mIsBeingDragged=" + mIsBeingDragged
                        + "mIsUnableToDrag=" + mIsUnableToDrag);
            }
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
    }

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    /*
     * The only time we want to intercept motion events is if we are in the
     * drag mode.
     */
    return mIsBeingDragged;
}
  • 排除Debug模式,返回的是一个mIsBeingDragged值,只有在水平滑动满足条件的时候会变成true,你可能会有疑问为什么如果滑动父类的子类按理说是不能滑动的呀,在细看一眼那个mIsBeingDragged=true;的位置你会发现这样一句: requestParentDisallowInterceptTouchEvent(true);到这里我们应该就明白了,为什么ViewPager+ViewPager我们不需要再去自己按照ViewPager+ViewFlow的方式去处理了吧,因为默认的情况下ViewPager已经替我们做了这项工作了。所以我们手势方面我们直接嵌套就是了。
  • NestedScrollView+RecyclerView:这个问题说白了就是ScrollView+ListView的问题,不过这里用的是双方的升级版本而已。针对NestedScrollView+RecyclerView官方有提供一些其他的处理方案,需要一定的版本支持,这里只是简单贴一下代码,其他的套路和ScrollView+ListView类似就不再赘述了,因为这两个控件的主要问题还是出现在高度计算的问题上,这个都是老生常谈百度有的是:

    LinearLayoutManager mLinearLayoutManager = new LinearLayoutManager(getActivity());
    mLinearLayoutManager.setSmoothScrollbarEnabled(true);
    mLinearLayoutManager.setAutoMeasureEnabled(true);
    recycleView_for_supply.setHasFixedSize(true);
    recycleView_for_supply.setNestedScrollingEnabled(false);
    recycleView_for_supply.setLayoutManager(mLinearLayoutManager);`

你可能感兴趣的:(Android进阶)