問題の説明
ウォーターフォールフローを記述します。垂直RecyclerViewが水平RecyclerViewをネストしている場合、水平RecyclerViewがスライドされると、垂直RecyclerViewがジッターします。
イベント配信概要
dispatchTouchEvent
戻り値true:すべてのイベントがビュー内でダイジェストされた
ことを示します戻り値false:イベントがこのレイヤーで配信されなくなり、消費のために上位コントロールのonTouchEventメソッドに渡されることを示します
super.dispatchTouchEvent(ev):デフォルトイベントはこのレイヤーに配信されます処理のためのレイヤーイベントインターセプトonInterceptTouchEventメソッド
onInterceptTouchEvent
trueを返す:イベントをインターセプトし、インターセプトしたイベントをこのレイヤーのコントロールのonTouchEventに渡して処理します。return
false:イベントをインターセプトせず、イベントを子ビューに正常に配信できます
。return super.onInterceptTouchEvent(ev):デフォルトはnoイベントをインターセプトし、ビューの次のレイヤーのdispatchTouchEventにイベントを渡します。
onTouchEvent
trueを返す:onTouchEventがイベントを処理した後にイベントが消費された
ことを示します。fasle:イベントが応答されなかったことを示します。その後、ビューのonTouchEventメソッドがtrueを
返すまで、イベントは上位ビューのonTouchEventメソッドに渡されます。ev):イベントが応答しないことを示し、結果はfalseを返すのと同じです
問題分析
水平方向のRecyclerViewをスライドすると、イベントは垂直方向のRecyclerViewから渡されます。スライドジェスチャーが垂直方向のRecyclerViewスライドイベントをトリガーすると、イベントがインターセプトされ、水平方向のRecyclerViewはスライドしませんが、垂直方向はRecyclerViewは上下にジッターします。
ソースコードのRecyclerViewスライドトリガー部分
public boolean onInterceptTouchEvent(MotionEvent e){ if(this.mLayoutFrozen){ return false; } else if(this.dispatchOnItemTouchIntercept(e)){ this.cancelTouch(); trueを返します。 } else if(this.mLayout == null){ return false; } else { boolean canScrollHorizontally = this.mLayout.canScrollHorizontally(); boolean canScrollVertically = this.mLayout.canScrollVertically(); if(this.mVelocityTracker == null){ this.mVelocityTracker = VelocityTracker.obtain(); }
this.mVelocityTracker.addMovement(e);
int action = e.getActionMasked();
int actionIndex = e.getActionIndex();
switch(action){ ケース0: ... ケース1: ... // ここから 开case :// 2:// 2:// 2 里的2 ACTION_MOVE = 2 int index = e.findPointerIndex(this.mScrollPointerId); if(index <0){ Log.e( "RecyclerView"、 "エラー処理スクロール; idのポインターインデックス" + this.mScrollPointerId + "が見つかりません。モーションイベントはスキップされましたか?"); falseを返します。 }
int x =(int)(e.getX(index)+ 0.5F);
int y =(int)(e.getY(index)+ 0.5F);
if(this.mScrollState!= 1){ int dx = x-this.mInitialTouchX; int dy = y-this.mInitialTouchY; boolean startScroll = false; if(canScrollHorizontally && Math.abs(dx)> this.mTouchSlop){ this.mLastTouchX = x; startScroll = true; }
if(canScrollVertically && Math.abs(dy)> this.mTouchSlop){ this.mLastTouchY = y; startScroll = true; }
if(startScroll){this.setScrollState (1); } } break; //ここで ケースを終了3: ... } return this.mScrollState == 1; } }
上記のRecyclerViewソースコードを見ると、次のことがわかります。
if(canScrollHorizontally && Math.abs(dx)> this.mTouchSlop){ this.mLastTouchX = x; startScroll = true; }
if(canScrollVertically && Math.abs(dy)> this.mTouchSlop){ this.mLastTouchY = y; startScroll = true; }
これら2つの条件が満たされると、startScrollがtrueに設定され、this.setScrollState(1);が呼び出されます。
void setScrollState(int state){ if(state!= this.mScrollState){// mScrollState认值认值0 this.mScrollState = state; if(state!= 2){ this.stopScrollersInternal(); }
this.dispatchOnScrollStateChanged(state);
}
}
mScroStateのデフォルト値を1に設定し、最後にonInterceptTouchEventが戻ります
this.mScrollState == 1を返します。
それは本当です。スライディングトリガーのソースコードを理解したら、RecyclerViewをここで変更できます。
修正方法
RecyclerViewスライドメソッドをトリガーする条件を見てみましょう
if(canScrollHorizontally && Math.abs(dx)> this.mTouchSlop){ this.mLastTouchX = x; startScroll = true; }
if(canScrollVertically && Math.abs(dy)> this.mTouchSlop){ this.mLastTouchY = y; startScroll = true; }
条件1:水平スライドが可能で、水平スライド距離の絶対値がスライドmTouchSlopトリガーのトリガーのしきい値より大きい場合
条件2:垂直スライドが可能で、垂直スライド距離の絶対値がスライドmTouchSlopトリガーのしきい値より大きい場合
問題は何ですか?
問題は、スライド距離の絶対値がしきい値より大きい限りです。この例を組み合わせると、垂直のRecyclerViewが外部で受け取った垂直のスライド距離コンポーネントの絶対値がしきい値mTouchSlopより大きい限り、2番目の条件がトリガーされてtrueが返され、それがインターセプトされます。
ユーザーの水平方向のスライドの距離成分が垂直方向よりも大きい場合でも、水平方向のRecyclerViewには引き渡されません。これにより、垂直方向のRecyclerViewジッターの問題が発生します。問題の
解決方法。
問題を認識している場合は、次の判断を追加するだけです。
if(canScrollHorizontally && Math.abs(dx)> this.mTouchSlop
&& Math.abs(dx)> Math.abs(dy)){ startScroll = true; }
if(canScrollVertically && Math.abs(dy)> this.mTouchSlop
&& Math.abs(dy)> Math.abs(dx)){ startScroll = true; }
水平方向にスライドする場合は、水平方向の成分が垂直方向の成分よりも大きいかどうか、またはその逆かどうかを判断します。このようにして、45度のスライド分離を実現できます。ユーザーと水平角度が45度未満の場合、スライドは水平RecyclerViewに渡されて処理され、逆も同様です。
ソースコードは次のように添付されています。
package com.newsweekly.livepi.mvp.ui.widget; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; public class BetterRecyclerView extends RecyclerView { private int mScrollPointerId; private int mInitialTouchX, mInitialTouchY; private int mTouchSlop; public BetterRecyclerView (@NonNull Context context) { super(context); init(); } public BetterRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public BetterRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { ViewConfiguration vc = ViewConfiguration.get(getContext()); this.mTouchSlop = vc.getScaledTouchSlop(); } @Override public void setScrollingTouchSlop(int slopConstant) { ViewConfiguration vc = ViewConfiguration.get(this.getContext()); switch (slopConstant) { case 0: this.mTouchSlop = vc.getScaledTouchSlop(); case 1: this.mTouchSlop = vc.getScaledPagingTouchSlop(); break; default: Log.w("RecyclerView", "setScrollingTouchSlop(): bad argument constant " + slopConstant + "; using default value"); } super.setScrollingTouchSlop(slopConstant); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally(); boolean canScrollVertically = getLayoutManager().canScrollVertically(); int action = e.getActionMasked(); int actionIndex = e.getActionIndex(); switch (action) { //ACTION_DOWN case 0: mScrollPointerId = e.getPointerId(0); this.mInitialTouchX = (int) (e.getX() + 0.5F); this.mInitialTouchY = (int) (e.getY() + 0.5F); return super.onInterceptTouchEvent(e); //ACTION_MOVE case 2: int index = e.findPointerIndex(this.mScrollPointerId); if (index < 0) { Log.e("RecyclerView", "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } int x = (int) (e.getX(index) + 0.5F); int y = (int) (e.getY(index) + 0.5F); if (getScrollState() != 1) { int dx = x - this.mInitialTouchX; int dy = y - this.mInitialTouchY; boolean startScroll = false; if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop && Math.abs(dx) > Math.abs(dy)) { startScroll = true; } if (canScrollVertically && Math.abs(dy) > this.mTouchSlop && Math.abs(dy) > Math.abs(dx)) { startScroll = true; } Log.d("MyRecyclerView", "canX:" + canScrollHorizontally + "--canY" + canScrollVertically + "--dx:" + dx + "--dy:" + dy + "--startScorll:" + startScroll + "--mTouchSlop" + mTouchSlop); return startScroll && super.onInterceptTouchEvent(e); } return super.onInterceptTouchEvent(e); //ACTION_POINTER_DOWN case 5: this.mScrollPointerId = e.getPointerId(actionIndex); this.mInitialTouchX = (int) (e.getX(actionIndex) + 0.5F); this.mInitialTouchY = (int) (e.getY(actionIndex) + 0.5F); return super.onInterceptTouchEvent(e); } return super.onInterceptTouchEvent(e); } }