View体系与自定义View(六)—— View的滑动冲突

1 常见的滑动冲突场景

常见的滑动冲突场景可以简单分为以下三种:

  • 外部滑动方向和内部滑动方向不一致
  • 外部滑动方向和内部滑动方向一致
  • 上面两种情况的嵌套

滑动冲突的场景

场景1:主要是将ViewPagerFragment配合使用所组成的页面滑动效果。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView。本来这种情况下是有滑动冲突的,但是ViewPager内部处理了这种滑动冲突,如果采用的是ScrollView,那么就必须手动处理滑动冲突,否则造成的后果就是内外两层只能有一层滑动,这是因为两者之间的滑动事件有冲突。除了这种典型的情况外,还存在其他情况,比如外部上下滑动、内部左右滑动等,但是它们属于同一类滑动冲突。

场景2:这种情况就稍微复杂一些,当内外两层都在同一个方向可以滑动的时候,是存在逻辑问题的。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。在实际的开发中,这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。

场景3:场景3是场景1和场景2两种情况的嵌套。比如在许多应用中会有这么一个效果:内层有一个场景1中的滑动效果,然后外层又有一个场景2中的滑动效果。具体说就是,外部有一个SlideMenu效果,然后内部有一个ViewPagerViewPager的每一个页面中又是一个ListView。虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可,而具体的处理方法其实和场景1、场景2相同的。

从本质上来说,这三种滑动冲突的场景的复杂度其实是相同的,因为它们的区别仅仅是滑动策略的不同。

2 滑动冲突的处理规则

一般来说,不管滑动冲突多么复杂,它都有既定的规则,根据这些规则可以选择合适的方法去处理。

对于场景1,它的处理规则是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,具体来说就是:当用户左右滑动时,需要让外部的View拦截滑动事件,当用户上下滑动时,需要让内部View拦截滑动事件。如图所示,根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。有很多可以参考,比如可以依据滑动路径和水平方向所形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方法的速度差来做判断。

滑动过程示意

对于场景2来说,比较特殊,它无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另一种状态时则需要内部View来响应View的滑动,根据这种业务上的需求也能得到相应的处理规则,有了处理规则同样可以进行下一步处理。

对于场景3来说,它的滑动规则就更复杂了,和场景2一样,它也无法直接根据滑动的角度、距离差以及速度差来做判断,同样还是只能从业务上找到突破点,具体方法和场景2一样,都是从业务需求上的出相应的的处理规则。

3 滑动冲突的解决方式

针对冲突,这里给出两种解决滑动冲突的方式:外部拦截法和内部拦截法。

3.1 外部拦截法

外部拦截法是指滑动事件要先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。 外部拦截法需要重写父容器的onInterceptTouchEvent方法,做相应的拦截即可,代码如下所示:

public class CustomView extends ViewGroup {
  
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        intercepted = false;
        break;
      case MotionEvent.ACTION_MOVE:
        if (父容器需要当前滑动事件) {
          intercepted = true;
        } else {
          intercepted = false;
        }
        break;
      case MotionEvent.ACTION_UP:
        intercepted = false;
        break;
      default:
        break;
    }
    return intercepted;
  }
  
}

上述代码是外部拦截的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其它均不需要修改并且也不能修改。这里对上述代码再描述一下,onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVEACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false,最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多的意义。

3.2 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。

要保证父容器不拦截任何事件,也就是在父容器的onInterceptTouchEvent方法中ACTION_DOWN事件不能拦截,这样子元素才能收到事件。 父容器所做的修改如下所示:

public class CustomView extends ViewGroup {
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
      return false;
    } else {
      return true;
    }
  }
}

子元素的dispatchTouchEvent()必须在ACTION_DOWN事件调用parent.requestDisallowInterceptTouchEvent(true),这样才能保证子元素能收到ACTION_MOVE事件,在ACTION_MOVE事件中做逻辑操作。 伪代码如下:

public class CustomView extends View {
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        getParent().requestDisallowInterceptTouchEvent(true);
        break;
      case MotionEvent.ACTION_MOVE:
        if (父容器需要此类滑动事件) {
          getParent().requestDisallowInterceptTouchEvent(false);
        } else {
          getParent().requestDisallowInterceptTouchEvent(true);
        }
        break;
      case MotionEvent.ACTION_UP:

        break;
      default:
        break;
    }
    return super.dispatchTouchEvent(ev);
  }
}

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其它不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其它事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(false);方法时,父元素才能继续拦截所需的事件。

4 示例

在这里插入图片描述

4.1 外部拦截法,解决横竖冲突

重写父控件的onInterceptTouchEvent方法,然后根据具体的需求,来决定父控件是否拦截事件。如果拦截事件返回true,不拦截返回false。如果父控件拦截了事件,则在父控件的onTouchEvent方法中进行相应的事件处理。

public class HorizontalEx extends ViewGroup {

  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;

  private int childIndex;
  private int childCount;

  private int lastXIntercept, lastYIntercept, lastX, lastY;

  private boolean isFirstTouch = true;

  public HorizontalEx(Context context) {
    this(context, null);
  }

  public HorizontalEx(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public HorizontalEx(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    init();
  }

  private void init() {
    mScroller = new Scroller(getContext());
    mVelocityTracker = VelocityTracker.obtain();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    if (childCount == 0) {
      setMeasuredDimension(0, 0);
    } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
      width = childCount * getChildAt(0).getMeasuredWidth();
      height = getChildAt(0).getMeasuredHeight();
      setMeasuredDimension(width, height);
    } else if (widthMode == MeasureSpec.AT_MOST) {
      width = childCount * getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, height);
    } else {
      height = getChildAt(0).getMeasuredHeight();
      setMeasuredDimension(width, height);
    }

  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int left = 0;
    for (int i = 0; i < getChildCount(); i++) {
      final View child = getChildAt(i);
      child.layout(left + l, t, r + left, b);
      left += child.getMeasuredWidth();
    }
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN: 
        lastXIntercept = x;
        lastYIntercept = y;
        intercepted = false; // 一定不要在ACTION_DOWN中返回true,否则会让子View没有机会得到事件
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
          intercepted = true;
        }
        break;
      case MotionEvent.ACTION_MOVE:
        final int deltaX = x - lastXIntercept;
        final int deltaY = y - lastYIntercept;
        if (Math.abs(deltaX) > Math.abs(deltaY)) { // 根据条件判断是否拦截该事件
          intercepted = true;
        } else {
          intercepted = false;
        }
        break;
      case MotionEvent.ACTION_UP:
        // 如果父控件拦截了ACTION_UP,那么子View将得不到UP事件,那么将会影响子View的onClick方法等
        // 但这对父控件时没有影响的,因为如果父控件ACTION_MOVE中拦截了事件,那么ACTION_UP事件必定会交给它处理
        intercepted = false; 
        break;
    }
    lastXIntercept = x;
    lastYIntercept = y;
    return intercepted;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    mVelocityTracker.addMovement(event);
    ViewConfiguration configuration = ViewConfiguration.get(getContext());
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
        }
        break;
      case MotionEvent.ACTION_MOVE:
        if (isFirstTouch) { // 因为父控件拿不到ACTION_DOWN事件,所以使用一个布尔值,当事件第一次来到父控件时,对lastX,lastY赋值
          lastX = x;
          lastY = y;
          isFirstTouch = false;
        }
        final int deltaX = x - lastX;
        scrollBy(-deltaX, 0);
        break;
      case MotionEvent.ACTION_UP:
        int scrollX = getScrollX();
        final int childWidth = getChildAt(0).getWidth();
        mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
        float xVelocity = mVelocityTracker.getXVelocity();
        if (Math.abs(xVelocity) > configuration.getScaledMinimumFlingVelocity()) {
          childIndex = xVelocity < 0 ? childIndex + 1 : childIndex - 1;
        } else {
          childIndex = (scrollX + childWidth / 2) / childWidth;
        }
        childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));
        smoothScrollBy(childIndex * childWidth - scrollX, 0);
        mVelocityTracker.clear();
        isFirstTouch = true;
        break;
    }
    lastX = x;
    lastY = y;
    return true;
  }

  private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);
    invalidate();
  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      invalidate();
    }
  }

  @Override
  protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mVelocityTracker.recycle();
  }
}

// 使用
public void initView(List<String> data1, List<String> data2, List<String> data3) {
  ListView listView1 = new ListView(this);
  ArrayAdapter<String> adapter1 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data1);
  listView1.setAdapter(adapter1);

  ListView listView2 = new ListView(this);
  ArrayAdapter<String> adapter2 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data2);
  listView2.setAdapter(adapter2);

  ListView listView3 = new ListView(this);
  ArrayAdapter<String> adapter3 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data3);
  listView3.setAdapter(adapter3);

  ViewGroup.LayoutParams params
    = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                 ViewGroup.LayoutParams.MATCH_PARENT);

  mHorizontalEx.addView(listView1, params);
  mHorizontalEx.addView(listView2, params);
  mHorizontalEx.addView(listView3, params);
}
4.2 内部拦截法,解决横竖冲突

内部拦截法主要依赖父控件的requestDisallowInterceptTouchEvent方法,它设置父控件的一个标志(FLAG_DISALLOW_INTERCEPT)这个标志可以决定父控件是否拦截事件,如果设置了这个标志则不拦截,如果没有这个标志,它会调用父控件的onInterceptTouchEvent()来询问父控件是否拦截,这个标志对ACTION_DOWN事件无效。

以下是ViewGroup.dispatchTouchEvent源码:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
      if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
      }

      final boolean intercepted;
      if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
          intercepted = onInterceptTouchEvent(ev);
          ev.setAction(action); 
        } else {
          intercepted = false;
        }
      } else {
        intercepted = true;
      }
  }
}

如果想要使用内部拦截法拦截事件:

  1. 重写父控件的onInterceptTouchEvent,在ACTION_DOWN的时候返回false,否则的话,子View调用requestDisallowInterceptTouchEvent是无效的。如果其他事件都返回true,这样就把是否拦截事件的权利交给了子View
  2. 在子ViewdispatchTouchEvent中来决定是否让父控件拦截事件:
    • 先要在ACTION_DOWN的时候使用使用mHorizontalEx2.requestDisallowInterceptTouchEvent(true);,否则的话,下一个事件就交给父控件了
    • 然后在ACTION_MOVE中根据业务逻辑是否调用mHorizontalEx2.requestDisallowInterceptTouchEvent(false);来决定父控件是否拦截事件
public class HorizontalEx2 extends ViewGroup {

  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;

  private boolean isFirstTouch = true;

  private int lastX, lastY;
  private int childIndex;

  public HorizontalEx2(Context context) {
    this(context, null);
  }

  public HorizontalEx2(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public HorizontalEx2(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    init();
  }

  private void init() {
    mScroller = new Scroller(getContext());
    mVelocityTracker = VelocityTracker.obtain();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int height = MeasureSpec.getMode(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    if (childCount == 0) {
      setMeasuredDimension(0, 0);
    } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
      height = getChildAt(0).getMeasuredHeight();
      width = childCount * getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, height);
    } else if (widthMode == MeasureSpec.AT_MOST) {
      width = childCount * getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, height);
    } else {
      height = getChildAt(0).getMeasuredHeight();
      setMeasuredDimension(width, height);
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int leftOffset = 0;
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      child.layout(l + leftOffset, t, r + leftOffset, b);
      leftOffset += child.getMeasuredWidth();
    }
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
      if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
        return true;
      }
      return false;
    } else {
      return true;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    mVelocityTracker.addMovement(event);
    ViewConfiguration configuration = ViewConfiguration.get(getContext());
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
        }
        break;
      case MotionEvent.ACTION_MOVE:
        if (isFirstTouch) {
          isFirstTouch = false;
          lastY = y;
          lastX = x;
        }
        final int deltaX = x - lastX;
        scrollBy(-deltaX, 0);
        break;
      case MotionEvent.ACTION_UP:
        isFirstTouch = true;
        int scrollX = getScrollX();
        mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
        float mVelocityX = mVelocityTracker.getXVelocity();
        if (Math.abs(mVelocityX) > configuration.getScaledMinimumFlingVelocity()) {
          childIndex = mVelocityX < 0 ? childIndex + 1 : childIndex - 1;
        } else {
          childIndex = (scrollX + getChildAt(0).getWidth() / 2) / getChildAt(0).getWidth();
        }
        childIndex = Math.min(getChildCount() - 1, Math.max(0, childIndex));
        smoothScrollBy(childIndex * getChildAt(0).getWidth() - scrollX, 0);
        mVelocityTracker.clear();
        break;
    }
    lastX = x;
    lastY = y;
    return true;
  }

  private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);
    invalidate();
  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }

  @Override
  protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mVelocityTracker.recycle();
  }
}

// 使用
public void initView(List<String> data1, List<String> data2, List<String> data3) {
  ListViewEx listView1 = new ListViewEx(this);
  ArrayAdapter<String> adapter1 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data1);
  listView1.setAdapter(adapter1);
  listView1.setHorizontalEx2(mHorizontalEx2);

  ListViewEx listView2 = new ListViewEx(this);
  ArrayAdapter<String> adapter2 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data2);
  listView2.setAdapter(adapter2);
  listView2.setHorizontalEx2(mHorizontalEx2);

  ListViewEx listView3 = new ListViewEx(this);
  ArrayAdapter<String> adapter3 = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data3);
  listView3.setAdapter(adapter3);
  listView3.setHorizontalEx2(mHorizontalEx2);

  ViewGroup.LayoutParams params
    = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                 ViewGroup.LayoutParams.MATCH_PARENT);

  mHorizontalEx2.addView(listView1, params);
  mHorizontalEx2.addView(listView2, params);
  mHorizontalEx2.addView(listView3, params);
}
4.3 外部拦截法,解决同向滑动冲突

public abstract class RefreshLayoutBase<T extends View> extends ViewGroup {

  public static final int STATUS_LOADING = 1;
  public static final int STATUS_RELEASE_TO_REFRESH = 2;
  public static final int STATUS_PULL_TO_REFRESH = 3;
  public static final int STATUS_IDLE = 4;
  public static final int STATUS_LOAD_MORE = 5;
  public static final int SCROLL_DURATION = 500;

  private int mScreenWidth, mScreenHeight;

  protected Scroller mScroller;

  private int mTouchSlop;

  protected ViewGroup headView;
  private ProgressBar headProgressBar;
  private TextView headTv;
  protected ViewGroup footView;
  private ProgressBar footProgressBar;
  private TextView footTv;

  private T contentView;

  protected int mInitScrollY = 0;

  private int mLastXIntercepted;
  private int mLastYIntercepted;
  private int lastX;
  private int lastY;

  private boolean isFirstTouch = true;


  protected int currentStatus = STATUS_IDLE;

  private OnRefreshListener mOnRefreshListener;


  public RefreshLayoutBase(Context context) {
    this(context, null);
  }

  public RefreshLayoutBase(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics metrics = new DisplayMetrics();
    wm.getDefaultDisplay().getRealMetrics(metrics);
    mScreenWidth = metrics.widthPixels;
    mScreenHeight = metrics.heightPixels;

    mScroller = new Scroller(context);

    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    initView();

    setPadding(0, 0, 0, 0);
  }

  private void initView() {
    setHeaderView();
    setFooterView();
  }

  public void setContentView(T view) {
    addView(view, 1);
  }

  private void setHeaderView() {
    headView = (ViewGroup) View.inflate(getContext(), R.layout.fresh_head_view, null);
    headView.setBackgroundColor(Color.RED);
    headProgressBar = headView.findViewById(R.id.head_progress_bar);
    headTv = headView.findViewById(R.id.head_tv);
    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mScreenHeight / 4);
    headView.setLayoutParams(layoutParams);
    headView.setPadding(0, mScreenHeight / 4 - dp2px(100), 0, 0);
    addView(headView);
  }

  public OnRefreshListener getOnRefreshListener() {
    return mOnRefreshListener;
  }

  public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
    this.mOnRefreshListener = onRefreshListener;
  }

  private void setFooterView() {
    footView = (ViewGroup) View.inflate(getContext(), R.layout.fresh_foot_view, null);
    footView.setBackgroundColor(Color.BLUE);
    footProgressBar = headView.findViewById(R.id.foot_progress_bar);
    footTv = footView.findViewById(R.id.foot_tv);
    addView(footView);
  }


  public void showFooter() {
    if (currentStatus != STATUS_LOAD_MORE) return;
    currentStatus = STATUS_LOAD_MORE;
    mScroller.startScroll(0, getScrollY(), 0, footView.getMeasuredHeight(), SCROLL_DURATION);
    invalidate();
  }


  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int finalHeight = 0;
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      measureChild(child, widthMeasureSpec, heightMeasureSpec);
      finalHeight += child.getMeasuredHeight();
    }

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
      width = getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, finalHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
      width = getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, height);
    } else {
      setMeasuredDimension(width, finalHeight);
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int topOffset = 0;
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      child.layout(getPaddingLeft(), getPaddingTop() + topOffset, r, getPaddingTop() + child.getMeasuredHeight() + topOffset);
      topOffset += child.getMeasuredHeight();
    }
    mInitScrollY = headView.getMeasuredHeight() + getPaddingTop();
    scrollTo(0, mInitScrollY);
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        mLastXIntercepted = x;
        mLastYIntercepted = y;
        break;
      case MotionEvent.ACTION_MOVE:
        final int deltaY = y - mLastYIntercepted;
        if (isTop() && deltaY > 0 && Math.abs(deltaY) > mTouchSlop) {
          intercepted = true; // 下拉
        }
        break;
      case MotionEvent.ACTION_UP:
        break;
    }

    mLastXIntercepted = x;
    mLastYIntercepted = y;

    return intercepted;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
        }
        lastX = x;
        lastY = y;
        break;
      case MotionEvent.ACTION_MOVE:
        if (isFirstTouch) {
          isFirstTouch = false;
          lastX = x;
          lastY = y;
        }
        final int deltaY = y - lastY;
        if (currentStatus != STATUS_LOADING) {
          changeScrollY(deltaY);
        }

        break;
      case MotionEvent.ACTION_UP:
        isFirstTouch = true;
        doRefresh();
        break;
    }


    return true;
  }

  protected void doRefresh() {
    if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
      mScroller.startScroll(0, getScrollY(), 0, mInitScrollY - getScrollY(), SCROLL_DURATION);
      currentStatus = STATUS_IDLE;
    } else if (currentStatus == STATUS_PULL_TO_REFRESH) {
      mScroller.startScroll(0, getScrollY(), 0, 0 - getScrollY(), SCROLL_DURATION);
      if (mOnRefreshListener != null) {
        currentStatus = STATUS_LOADING;
        mOnRefreshListener.refresh();
      }
    }
    invalidate();
  }

  public void refreshComplete() {
    mScroller.startScroll(0, getScrollY(), 0, mInitScrollY - getScrollY(), SCROLL_DURATION);
    currentStatus = STATUS_IDLE;
    invalidate();
  }

  protected void changeScrollY(int deltaY) {
    int curY = getScrollY();
    if (deltaY > 0) {
      if (curY - deltaY > getPaddingTop()) {
        scrollBy(0, -deltaY);
      }
    } else {
      if (curY - deltaY <= mInitScrollY) {
        scrollBy(0, -deltaY);
      }
    }

    curY = getScrollY();
    int slop = mInitScrollY / 2;
    if (curY > 0 && curY <= slop) {
      currentStatus = STATUS_PULL_TO_REFRESH;
    } else if (curY > 0 && curY >= slop) {
      currentStatus = STATUS_RELEASE_TO_REFRESH;
    }

  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }

  abstract boolean isTop();

  abstract boolean isBottom();

  public interface OnRefreshListener {
    void refresh();
  }

  private int dp2px(int dp) {
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics metrics = new DisplayMetrics();
    wm.getDefaultDisplay().getRealMetrics(metrics);
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
  }
}

RefreshLayoutBase是一个抽象类,需要子类继承isTopisBottom方法:

public class RefreshListView extends RefreshLayoutBase<ListView> {

  private ListView listView;
  private OnLoadListener loadListener;

  public RefreshListView(Context context) {
    super(context);
  }

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

  public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  public ListView getListView() {
    return listView;
  }

  public void setListView(ListView listView) {
    this.listView = listView;
    setContentView(listView);

    this.listView.setOnScrollListener(new AbsListView.OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {

      }

      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (currentStatus == STATUS_IDLE && getScrollY() <= mInitScrollY && isBottom()) {
          showFooter();
          if (loadListener != null) {
            loadListener.load();
          }
        }
      }
    });

  }

  public OnLoadListener getLoadListener() {
    return loadListener;
  }

  public void setOnLoadListener(OnLoadListener loadListener) {
    this.loadListener = loadListener;
  }

  @Override
  boolean isTop() {
    return listView.getFirstVisiblePosition() == 0 && getScrollY() <= headView.getMeasuredHeight();
  }

  @Override
  boolean isBottom() {
    return listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1;
  }

  public interface OnLoadListener {
    void load();
  }
}

4.4 内部拦截法解决同向滑动
public class RefreshLayoutBase2 extends ViewGroup {

  private static List<String> datas;

  static {
    datas = new ArrayList<>();
    for (int i = 0; i < 40; i++) {
      datas.add("数据-" + i);
    }
  }

  private Scroller mScroller;

  public int mInitScrollY;

  private ViewGroup headView;

  private int lastY;

  private ListViewEx2 lv;

  public RefreshLayoutBase2(Context context) {
    this(context, null);
  }

  public RefreshLayoutBase2(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public RefreshLayoutBase2(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    mScroller = new Scroller(context);

    setHeadView(context);
    setContentView(context);
  }

  private void setHeadView(Context context) {
    headView = (ViewGroup) View.inflate(context, R.layout.fresh_head_view, null);
    headView.setBackgroundColor(Color.RED);
    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, 300);
    addView(headView, params);
  }

  private void setContentView(Context context) {
    lv = new ListViewEx2(context, this);
    lv.setBackgroundColor(Color.BLUE);
    ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, datas);
    lv.setAdapter(adapter);
    addView(lv, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
  }


  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int finalHeight = 0;
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      measureChild(child, widthMeasureSpec, heightMeasureSpec);
      finalHeight += child.getMeasuredHeight();
    }
    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
      width = getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, finalHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
      width = getChildAt(0).getMeasuredWidth();
      setMeasuredDimension(width, height);
    } else {
      setMeasuredDimension(width, finalHeight);
    }

  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int topOffset = 0;
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      child.layout(getPaddingLeft(), getPaddingTop() + topOffset, r, getPaddingTop() + child.getMeasuredHeight() + topOffset);
      topOffset += child.getMeasuredHeight();
    }
    mInitScrollY = headView.getMeasuredHeight() + getPaddingTop();
    scrollTo(0, mInitScrollY);
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) return false;
    return true;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        lastY = y;
        break;
      case MotionEvent.ACTION_MOVE:
        final int deltaY = y - lastY;
        if (deltaY >= 0 && lv.isTop() && getScrollY() - deltaY >= getPaddingTop()) {
          scrollBy(0, -deltaY);
        }
        break;
      case MotionEvent.ACTION_UP:
        this.postDelayed(() -> {
          mScroller.startScroll(0, getScrollY(), 0, mInitScrollY - getScrollY());
          invalidate();
        }, 2000);
        break;
    }
    lastY = y;
    return true;
  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }
}


public class ListViewEx2 extends ListView {

  private RefreshLayoutBase2 refreshLayoutBase2;

  public ListViewEx2(Context context, RefreshLayoutBase2 refreshLayoutBase2) {
    super(context);
    this.refreshLayoutBase2 = refreshLayoutBase2;
  }

  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        refreshLayoutBase2.requestDisallowInterceptTouchEvent(true);
        break;
      case MotionEvent.ACTION_MOVE:
        if (isTop() && refreshLayoutBase2.getScrollY() <= refreshLayoutBase2.mInitScrollY) {
          refreshLayoutBase2.requestDisallowInterceptTouchEvent(false);
        }
        break;
      case MotionEvent.ACTION_UP:

        break;
    }

    return super.dispatchTouchEvent(ev);
  }

  public boolean isTop() {
    return getFirstVisiblePosition() == 0;
  }
}

参考

https://www.cnblogs.com/andy-songwei/p/11076612.html
https://blog.csdn.net/a992036795/article/details/51735501

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值