記事ディレクトリ
Android がスライディングの競合を解決
スライディングコンフリクトの生成
スライディング競合を解決する方法について議論する前に、どのようなシナリオでスライディング競合が発生するかを知る必要があります。2 つのコンポーネント (またはそれ以上) の間でスライドの競合が発生し、これら 2 つのコンポーネントが包含関係 (親コントロールと子コントロール) にある場合、親コントロールと子コントロールはスライド イベントをインターセプトできるが、インターセプトしなかった場合、それらのスライドの競合は処理されます。の場合、現時点で 1 つのコントロールのみがスライディング イベントに応答できます。通常、この状況を親コントロールと子コントロールの間のスライディング競合と呼びます。主なシナリオは 3 つあります。
スライディング紛争シーン 1
親コントロールのスライド方向は、子コントロールのスライド方向とは異なります。このシナリオの解決策は比較的簡単です。たとえば、通常は ViewPager+RecyclerView を使用します (viewPager が左右にスライドし、RecyclerView が上にスライドすると仮定します)。これはスライディングであり、競合のシナリオ 1 をスライドさせますが、viewPager 内でスライディング競合を解決する方法を実現するのに役立つため、このアーキテクチャを使用する場合にはスライディング競合は発生しませんでした。また、ViewPager を使用せずに、スライド可能な他の親コントロールを使用し、内部が RecyclerView または ListView リストである場合、この時点でスライドの競合が発生します。
スライディング競合シナリオ 1 に対する具体的な解決策は、外部インターセプト方法です。これについては、以下で詳しく説明します。
外部インターセプト方式
いわゆる外部インターセプト メソッドは、最初にイベント シーケンス全体を親コントロールによってインターセプトする必要があることを意味します。このイベントが必要な場合はインターセプトし、必要ない場合は子コントロールに渡します。加工用に。(イベント配信の仕組みがわからない場合は、Android のイベント配信の仕組みについて学んでください)。親コントロールの onInterceptTouchEvent() メソッドを書き換えるだけで、外部が ViewPager (左右にスワイプ) であると仮定して、イベントをインターセプトするかどうかを決定できます。ここでは、ViewPager がスライディングの競合、つまり実際の ViewPager ソースを処理しないと仮定しています。コードはスライド競合の問題を解決しています)、内部は RecyclerView (上下にスライド)、ViewPager の onInterceptTouchEvent() メソッドを書き換えます。具体的なコード ロジックは次のとおりです。
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
public class MyViewPager extends ViewPager {
private float startX;
private float startY;
//使用directionSign来作为拦截的标志,0代表没有初始化,1代表父控件拦截,其他情况代表子控件拦截
private int directionSign = 0; //只有在滑动开始时需要进行判断由谁进行拦截,之后的这一次事件中无需判断
public MyViewPager(@NonNull Context context) {
super(context);
}
//onInterceptTouchEvent返回true表示该viewPager拦截事件(不再向子控件中进行分发),返回false代表不拦截,进行分发事件(交由子控件RecyclerView处理)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean sign = false; //该标志标示是否拦截事件
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//按下屏幕事件
sign = false; //此处必须为false,如果为true,则后期滑动的事件将默认会被viewPager拦截,且不在调用onInterceptTouchEvent()方法
//进行记录当前的坐标
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//滑动屏幕事件,具体解决滑动冲突的思路在此
if(checkFatherNeed(ev)){
//检查父控件ViewPager是否需要拦截,如果需要则进行修改sign = true
sign = true;
}else{
sign = false;
}
break;
case MotionEvent.ACTION_UP:
//松开屏幕事件
sign = false; //此处为true或false意义不大,一般置为false
break;
}
return sign;
}
private boolean checkFatherNeed(MotionEvent event) {
if(directionSign == 0){
//没有初始化,对directionSign进行初始化
float currentX = event.getX();
float currentY = event.getY();
if(Math.abs(currentX - startX) > Math.abs(currentY-startY)){
//代表水平滑动内容多,进行拦截事件
directionSign = 1;
return true;
}else{
//代表水平滑动内容多,交给子控件拦截事件
directionSign = 2;
return false;
}
}else if(directionSign == 1){
//父控件拦截
return true;
}else{
//子控件拦截
return false;
}
}
}
一般的な解決策は、指のスライドの方向を判断して (水平方向のスライド距離と垂直方向のスライド距離を比較できます)、誰がイベントを傍受して処理するかを決定することです。(左右にスワイプすると ViewPager に、上下にスライドすると RecyclerView に移動します)
スライディング紛争シーン 2
スライド競合の 2 番目のシナリオは、外部スライド方向が内部スライド方向と同じである場合です。たとえば、上下にスライドする RecyclerView が、上下にスライドする ScrollView 内にネストされています。このシナリオに対する具体的な解決策は、内部インターセプト方式です。
内部インターセプト方式
内部インターセプトメソッドとは、子コントロールのdispatcherTouchEvent配布メソッドを書き換える必要があり、イベントの状態を判断して親コントロールまたは子コントロールに処理権を振り分けることができます。(たとえば、特定の状態にあるときは、子コントロールでイベントを処理し、他の状態は親コントロールに渡されるようにします)。
具体的な実装アイデアは次のとおりです。
子コントロールのdispatcherTouchEventメソッドを書き換え、親コントロールのonInterceptTouchEventメソッドを書き換えて、指が一瞬押されたとき、つまりACTION_DOWNイベントが発生したときに、親コントロールがイベントをインターセプトしないようにするには、parent.requestDisallowInterceptTouchEvent(true) を実行して、親コントロールがイベントをインターセプトしないようにします。parent.requestDisallowInterceptTouchEvent(false) を呼び出すと、親コントロールはイベントをインターセプトできます。
具体的なコードは次のとおりです。
チャイルドコントロール
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class MyRecyclerview extends RecyclerView {
float x;
float y;
public MyRecyclerview(@NonNull Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
float x = ev.getX();
float y = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true); //父控件之后不再拦截事件
break;
case MotionEvent.ACTION_MOVE:
if(父控件需要此类事件){
getParent().requestDisallowInterceptTouchEvent(false); //父控件可以进行拦截事件
}
break;
}
return super.dispatchTouchEvent(ev);
}
}
親コントロール
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
public class MyViewPager extends ViewPager {
public MyViewPager(@NonNull Context context) {
super(context);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
return false; //不拦截
}else{
return true; //除了按下事件其他进行全部拦截(前提是子控件的parent.requestDisallowInterceptTouchEvent(false))
}
}
}
スライディング紛争シーン 3
スライディング紛争の 3 番目のシナリオは、上記 2 つの状況が組み合わされたり、同時に発生したりする場合であり、解決策は、内部インターセプトと外部インターセプトを併用することです。