垂直スライドRecyclerViewネスト水平スライドRecyclerView水平スライド不感を修正

6250 ワード

Androidアプリケーションでは、ListView、RecyclerViewなど、垂直にスクロールしたViewを使用してコンテンツを表示することがほとんどです.しかし、垂直にスクロールしたViewの内容を水平にスクロールしたい場合もあります.垂直にスクロールしたビューに水平にスクロールしたビューを直接使用すると、スクロール操作がスムーズではありません.
たとえば、次の例を示します.
なぜこの問題が発生したのでしょうか.
上図のレイアウトは1つのRecyclerViewで垂直スクロールのLinearLayoutManagerレイアウトマネージャを使用していますが、中の各Itemは別のRecyclerViewで水平スクロールのLinearLayoutManagerを使用しています.Androidシステムのイベント配信では、最上位のViewが垂直にスクロールできる場合でも、ユーザーが水平にドラッグすると、最上位のViewがクリックイベントをブロックします.次はRecyclerViewです.JAvaのonInterceptTouchEventに関するコード:
@Override public boolean onInterceptTouchEvent(MotionEvent e) { ...
switch (action) { case MotionEvent.ACTION_DOWN: ...
case MotionEvent.ACTION_MOVE: {
    ...

    if (mScrollState != SCROLL_STATE_DRAGGING) {
      boolean startScroll = false;
      if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
        ...
        startScroll = true;
      }
      if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
        ...
        startScroll = true;
      }
      if (startScroll) {
        setScrollState(SCROLL_STATE_DRAGGING);
      }
  }
} break;
  ...

} return mScrollState == SCROLL_STATE_DRAGGING; }
上のif判断に注意してください.
if(canScrollVertically && Math.abs(dy) > mTouchSlop) {...}
RecyclerViewは,ユーザがドラッグする角度を判断するのではなく,ドラッグする距離がスクロールの最小サイズより大きいか否かを判断するために用いられる.垂直スクロールしかできないViewの場合は、このような実装は問題ありません.水平スクロールのRecyclerViewをもう1つ入れると問題が発生します.
この問題は、次のように修正できます.
if(canScrollVertically && Math.abs(dy) > mTouchSlop && (canScrollHorizontally || Math.abs(dy) > Math.abs(dx))) {...}
次は完全な実装ですjava :
public class BetterRecyclerView extends RecyclerView{ private static final int INVALID_POINTER = -1; private int mScrollPointerId = INVALID_POINTER; private int mInitialTouchX, mInitialTouchY; private int mTouchSlop; public BetterRecyclerView(Contextcontext) { this(context, null); }
public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs) { this(context, attrs, 0); }
public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) { super(context, attrs, defStyle); final ViewConfigurationvc = ViewConfiguration.get(getContext()); mTouchSlop = vc.getScaledTouchSlop(); }
@Override public void setScrollingTouchSlop(int slopConstant) { super.setScrollingTouchSlop(slopConstant); final ViewConfigurationvc = ViewConfiguration.get(getContext()); switch (slopConstant) { case TOUCH_SLOP_DEFAULT: mTouchSlop = vc.getScaledTouchSlop(); break; case TOUCH_SLOP_PAGING: mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc); break; default: break; } }
@Override public boolean onInterceptTouchEvent(MotionEvent e) { final int action = MotionEventCompat.getActionMasked(e); final int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
  case MotionEvent.ACTION_DOWN:
    mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
    mInitialTouchX = (int) (e.getX() + 0.5f);
    mInitialTouchY = (int) (e.getY() + 0.5f);
    return super.onInterceptTouchEvent(e);

  case MotionEventCompat.ACTION_POINTER_DOWN:
    mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
    mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
    mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
    return super.onInterceptTouchEvent(e);

  case MotionEvent.ACTION_MOVE: {
    final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
    if (index < 0) {
      return false;
    }

    final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
    final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
    if (getScrollState() != SCROLL_STATE_DRAGGING) {
      final int dx = x - mInitialTouchX;
      final int dy = y - mInitialTouchY;
      final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
      final boolean canScrollVertically = getLayoutManager().canScrollVertically();
      boolean startScroll = false;
      if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
        startScroll = true;
      }
      if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
        startScroll = true;
      }
      return startScroll && super.onInterceptTouchEvent(e);
    }
    return super.onInterceptTouchEvent(e);
  }

  default:
    return super.onInterceptTouchEvent(e);
}

} }
その他の質問
ユーザがRecyclerViewをすばやくスライドさせる場合、RecyclerViewはその最終位置を決定するのに時間がかかる.ユーザがサブの水平RecyclerViewをすばやくスライドしている場合、サブRecyclerViewがスライドしている間、ユーザが垂直にスライドしている場合、垂直にスライドすることはできません.サブRecyclerViewがこの垂直スライドイベントを依然として処理しているためである.
したがって、高速スライド後のスクロールが静止した状態では、サブViewはスライドイベントに応答するべきではありません.RecyclerViewのonInterceptTouchEvent()コードをもう一度見てみましょう.
@Override public boolean onInterceptTouchEvent(MotionEvent e) { ...
switch (action) {
    case MotionEvent.ACTION_DOWN:
        ...

        if (mScrollState == SCROLL_STATE_SETTLING) {
            getParent().requestDisallowInterceptTouchEvent(true);
            setScrollState(SCROLL_STATE_DRAGGING);
        }

        ...
}
return mScrollState == SCROLL_STATE_DRAGGING;

}
RecyclerViewのステータスがSCROLL_である場合STATE_SETTLING(クイックスライド後からスライド静止までの状態)の場合、RecyclerViewは親コントロールにイベントをブロックしないように伝えます.
同じように、一方向だけ固定すれば、このような処理は問題ありません.
このネストの場合、親RecyclerViewは垂直スクロールイベントのみをブロックする必要があります.したがって、親RecyclerViewはこのように変更できます.
public class FeedRootRecyclerView extends BetterRecyclerView{ public FeedRootRecyclerView(Contextcontext) { this(context, null); }
public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs) { this(context, attrs, 0); }
public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) { super(context, attrs, defStyle); }
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {/* do nothing */} }
次の図は、最終的な結果です.
サンプルアイテムをダウンロードできる場合は、サンプルアイテムでkotlinが使用されていることに注意してください.kotlinプラグインを構成する必要があります.
原文:http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/