Androidタッチイベントの応用詳細


前言
前の記事では、Androidタッチイベントの伝達メカニズムについて説明しましたが、具体的にはここAndroidタッチイベント伝達機構を初めて知った。をご覧ください。Androidにおけるタッチイベントの配信を知っている以上、どのような問題が解決されるのか、私たちが実際に開発している中でどのように適用されるのか、そのことが重要であり、原理を知ることは問題解決のために準備されています。この文章の核心はViewのスライド衝突をどのように解決するかという問題が日常的に開発されています。例えば内部のネストフレームビューは左右にスライドし、外部は一つのSrollViewで含まれています。上下にスライドできます。もしスライド衝突処理を行わないと、外部のスライド方向と内部のスライド方向が一致しないようになります。
目次
よくあるスライド衝突シーン
滑り衝突の処理規則
外部ブロック法
インサイド?ブロック
結び目
よくあるスライド衝突シーン
通常のスライド衝突シーンは、以下の3つに簡単に分けることができます。
シーン1:外部スライド方向と内部スライド方向が一致しない
場面2:外部スライドの方向と内部スライドの方向が一致します。
シーン3:上の2つの場合の入れ子
図のように:

シーン1は主にViewer PagerとFragmentを配合して使用して構成されたページスライド効果で、主流のアプリケーションではほとんどこの効果が使われます。この効果では左右スライドでページを切り替えることができますが、各ページの内部にはListViewがあるため、スライド衝突を起こします。しかし、ViePagerの内部ではこのようなスライド衝突を処理していますので、Viewer Pagerを採用する時はこの問題に注目する必要はありません。そうでなければ、内部と外部の2つの層は1階しかスライドできないという結果になります。
シーン2は複雑な点で、内外の二階が同じ方向にスライドできる時、明らかに論理的な問題があります。指が滑り始めると、システムはユーザーが一体どの層をスライドさせたいのか分かりませんので、指が滑ると問題が発生します。あるいは1階だけスライドするか、内外2階までスライドするかは分かりません。
シーン3は、シーン1とシーン2の入れ子で、より複雑に見えます。例えば外部にはSlideMenuの効果があり、内部にはViewer Pagerがあり、Viewer Pagerの各ページにはListViewがあります。シーン3の摺動衝突は複雑に見えるが、いずれも単一の摺動衝突の重ね合わせなので、一つ一つ解きほぐしていく必要がある。
滑り衝突の処理規則
一般的に、滑りの衝突がどんなに複雑であっても、それは既定の規則があります。これらの規則によって適切な方法を選択して処理することができます。
シーン1については、ユーザが左右にスライドするときは、外部のビューブロックをクリックし、ユーザが上下にスライドするときは、内部のビューブロックをクリックする必要があります。具体的には、スライドが水平にスライドするか、縦にスライドするかによって、誰がブロックするかを判断します。
図のように:

簡単に言えば、水平方向と縦方向の距離差から判断します。Dx>Dyであれば、水平スライド、Dy>Dxであれば、縦スライドです。
シーン2は、スライドの角度、距離差、速度差によって判断できない特殊なものです。この場合は、ある状態にあるときは外部のViewがユーザのスライドに応答し、別の状態にあるときは内部ViewがViewのスライドに応答するように、業務上の突破点を見つける必要がある。
シーン3については、スライドルールも複雑で、シーン2と同じように、業務上の突破点を見つけます。
外部ブロック法
外部ブロック法とは、事件をクリックして親容器のブロックを先に通過することです。もし親容器が必要なら、このイベントをブロックしないと、スライド衝突の問題を解決できます。外部ブロック法は親容器のオンインテットタッチEvent方法を書き換える必要があります。内部で相応のブロックをすればいいです。疑似コードは以下の通りです。

 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
 boolean intercepted = false;
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.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;
 }
 mLastXIntercept = x;
 mLastYIntercept = y;

 return intercepted;
 }

まずはACT ION_DOWNというイベントは、父の容器がfalseに戻らなければならないので、後続のmoveとupのイベントがサブビューに伝えられ、moveイベントによってブロックするかどうかを決定し、父の容器がブロックされたらtrueに戻ります。そうでなければfalseに戻ります。
Viewer Pagerのようなコントロールをカスタマイズし、ListViewの効果を入れ込みます。ソースコードは以下の通りです。

public class HorizontalScrollViewEx extends ViewGroup {
 private static final String TAG = "HorizontalScrollViewEx";

 private int mChildrenSize;
 private int mChildWidth;
 private int mChildIndex;

 //            
 private int mLastX = 0;
 private int mLastY = 0;
 //            (onInterceptTouchEvent)
 private int mLastXIntercept = 0;
 private int mLastYIntercept = 0;

 private Scroller mScroller;  //      
 private VelocityTracker mVelocityTracker; //      

 public HorizontalScrollViewEx(Context context) {
 super(context);
 init();
 }

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

 public HorizontalScrollViewEx(Context context, AttributeSet attrs,
  int defStyle) {
 super(context, attrs, defStyle);
 init();
 }

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

 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
 boolean intercepted = false;
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
  intercepted = false;
  if (!mScroller.isFinished()) {
  mScroller.abortAnimation();
  intercepted = true;
  }
  break;
 }
 case MotionEvent.ACTION_MOVE: {
  int deltaX = x - mLastXIntercept;
  int deltaY = y - mLastYIntercept;
  if (Math.abs(deltaX) > Math.abs(deltaY)) {
  intercepted = true;
  } else {
  intercepted = false;
  }
  break;
 }
 case MotionEvent.ACTION_UP: {
  intercepted = false;
  break;
 }
 default:
  break;
 }

 Log.d(TAG, "intercepted=" + intercepted);
 mLastX = x;
 mLastY = y;
 mLastXIntercept = x;
 mLastYIntercept = y;

 return intercepted;
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
 mVelocityTracker.addMovement(event);
 int x = (int) event.getX();
 int y = (int) event.getY();
 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
  if (!mScroller.isFinished()) {
  mScroller.abortAnimation();
  }
  break;
 }
 case MotionEvent.ACTION_MOVE: {
  int deltaX = x - mLastX;
  scrollBy(-deltaX, 0);
  break;
 }
 case MotionEvent.ACTION_UP: {
  int scrollX = getScrollX();
  mVelocityTracker.computeCurrentVelocity(1000);
  float xVelocity = mVelocityTracker.getXVelocity();
  if (Math.abs(xVelocity) >= 50) {
  mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
  } else {
  mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
  }
  mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
  int dx = mChildIndex * mChildWidth - scrollX;
  smoothScrollBy(dx, 0);
  mVelocityTracker.clear();
  break;
 }
 default:
  break;
 }

 mLastX = x;
 mLastY = y;
 return true;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 int measuredWidth = 0;
 int measuredHeight = 0;
 final int childCount = getChildCount();
 measureChildren(widthMeasureSpec, heightMeasureSpec);

 int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
 int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
 if (childCount == 0) {
  setMeasuredDimension(0, 0);
 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
  final View childView = getChildAt(0);
  measuredHeight = childView.getMeasuredHeight();
  setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
 } else if (widthSpecMode == MeasureSpec.AT_MOST) {
  final View childView = getChildAt(0);
  measuredWidth = childView.getMeasuredWidth() * childCount;
  setMeasuredDimension(measuredWidth, heightSpaceSize);
 } else {
  final View childView = getChildAt(0);
  measuredWidth = childView.getMeasuredWidth() * childCount;
  measuredHeight = childView.getMeasuredHeight();
  setMeasuredDimension(measuredWidth, measuredHeight);
 }
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
 int childLeft = 0;
 final int childCount = getChildCount();
 mChildrenSize = childCount;

 for (int i = 0; i < childCount; i++) {
  final View childView = getChildAt(i);
  if (childView.getVisibility() != View.GONE) {
  final int childWidth = childView.getMeasuredWidth();
  mChildWidth = childWidth;
  childView.layout(childLeft, 0, childLeft + childWidth,
   childView.getMeasuredHeight());
  childLeft += childWidth;
  }
 }
 }

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

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

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

この場合のブロック条件は、父容器がスライド中の水平距離の差が垂直距離より大きい場合、ブロックを行います。そうでないとブロックせずに、イベントを伝え続けます。
インサイド?ブロック
内部ブロック法は、親の容器が何のイベントもブロックしないということです。すべてのイベントがサブ要素に伝達されます。もし子供がこのイベントを必要とするなら、そのまま使ってしまいます。そうでなければ、親の容器に任せて処理します。この方法はAndroidのイベント配布メカニズムと一致しないので、request DiscalwInterceptTouchEvent方法に協力して正常に作業する必要があります。使用は外部ブロックより複雑です。疑似コードは以下の通りです

 @Override
 public boolean dispatchTouchEvent(MotionEvent event) {
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
  mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
  break;
 }
 case MotionEvent.ACTION_MOVE: {
  int deltaX = x - mLastX;
  int deltaY = y - mLastY;
  if (           ) {
  mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
  }
  break;
 }
 case MotionEvent.ACTION_UP: {
  break;
 }
 default:
  break;
 }

 mLastX = x;
 mLastY = y;
 return super.dispatchTouchEvent(event);
 }

サブ要素がrequest DiscallowIntercept TouchEvent(false)メソッドを呼び出すと、親要素は必要なイベントをブロックし続けることができます。
前は同じようなViewer Pagerを使っていますが、今はListViewを書き直して、ListViewをカスタマイズできます。ListView Exといいます。そして内部ブロック法のテンプレートコードを修正すればいいです。

public class ListViewEx extends ListView {
 private static final String TAG = "ListViewEx";

 private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;

 //            
 private int mLastX = 0;
 private int mLastY = 0;

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

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

 public ListViewEx(Context context, AttributeSet attrs, int defStyle) {
 super(context, attrs, defStyle);
 }

 public void setHorizontalScrollViewEx2(
  HorizontalScrollViewEx2 horizontalScrollViewEx2) {
 mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
 }

 @Override
 public boolean dispatchTouchEvent(MotionEvent event) {
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
  mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
  break;
 }
 case MotionEvent.ACTION_MOVE: {
  int deltaX = x - mLastX;
  int deltaY = y - mLastY;
  Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
  if (Math.abs(deltaX) > Math.abs(deltaY)) {
  mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
  }
  break;
 }
 case MotionEvent.ACTION_UP: {
  break;
 }
 default:
  break;
 }

 mLastX = x;
 mLastY = y;
 return super.dispatchTouchEvent(event);
 }
}

また、ListView Exを含む外部レイアウトを修正し、オンインテットTouchEventイベントでブロックしません。

public class HorizontalScrollViewEx2 extends ViewGroup {
 private static final String TAG = "HorizontalScrollViewEx2";

 private int mChildrenSize;
 private int mChildWidth;
 private int mChildIndex;
 //            
 private int mLastX = 0;
 private int mLastY = 0;

 //            (onInterceptTouchEvent)
 private int mLastXIntercept = 0;
 private int mLastYIntercept = 0;

 private Scroller mScroller;
 private VelocityTracker mVelocityTracker;

 public HorizontalScrollViewEx2(Context context) {
 super(context);
 init();
 }

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

 public HorizontalScrollViewEx2(Context context, AttributeSet attrs,
  int defStyle) {
 super(context, attrs, defStyle);
 init();
 }

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

 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
 int x = (int) event.getX();
 int y = (int) event.getY();
 int action = event.getAction();
 if (action == MotionEvent.ACTION_DOWN) {
  mLastX = x;
  mLastY = y;
  if (!mScroller.isFinished()) {
  mScroller.abortAnimation();
  return true;
  }
  return false;
 } else {
  return true;
 }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
 Log.d(TAG, "onTouchEvent action:" + event.getAction());
 mVelocityTracker.addMovement(event);
 int x = (int) event.getX();
 int y = (int) event.getY();
 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
  if (!mScroller.isFinished()) {
  mScroller.abortAnimation();
  }
  break;
 }
 case MotionEvent.ACTION_MOVE: {
  int deltaX = x - mLastX;
  int deltaY = y - mLastY;
  Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
  scrollBy(-deltaX, 0);
  break;
 }
 case MotionEvent.ACTION_UP: {
  int scrollX = getScrollX();
  int scrollToChildIndex = scrollX / mChildWidth;
  Log.d(TAG, "current index:" + scrollToChildIndex);
  mVelocityTracker.computeCurrentVelocity(1000);
  float xVelocity = mVelocityTracker.getXVelocity();
  if (Math.abs(xVelocity) >= 50) {
  mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
  } else {
  mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
  }
  mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
  int dx = mChildIndex * mChildWidth - scrollX;
  smoothScrollBy(dx, 0);
  mVelocityTracker.clear();
  Log.d(TAG, "index:" + scrollToChildIndex + " dx:" + dx);
  break;
 }
 default:
  break;
 }

 mLastX = x;
 mLastY = y;
 return true;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 int measuredWidth = 0;
 int measuredHeight = 0;
 final int childCount = getChildCount();
 measureChildren(widthMeasureSpec, heightMeasureSpec);

 int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
 int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
 if (childCount == 0) {
  setMeasuredDimension(0, 0);
 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
  final View childView = getChildAt(0);
  measuredHeight = childView.getMeasuredHeight();
  setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
 } else if (widthSpecMode == MeasureSpec.AT_MOST) {
  final View childView = getChildAt(0);
  measuredWidth = childView.getMeasuredWidth() * childCount;
  setMeasuredDimension(measuredWidth, heightSpaceSize);
 } else {
  final View childView = getChildAt(0);
  measuredWidth = childView.getMeasuredWidth() * childCount;
  measuredHeight = childView.getMeasuredHeight();
  setMeasuredDimension(measuredWidth, measuredHeight);
 }
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
 Log.d(TAG, "width:" + getWidth());
 int childLeft = 0;
 final int childCount = getChildCount();
 mChildrenSize = childCount;

 for (int i = 0; i < childCount; i++) {
  final View childView = getChildAt(i);
  if (childView.getVisibility() != View.GONE) {
  final int childWidth = childView.getMeasuredWidth();
  mChildWidth = childWidth;
  childView.layout(childLeft, 0, childLeft + childWidth,
   childView.getMeasuredHeight());
  childLeft += childWidth;
  }
 }
 }

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

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

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

このブロッキング規則も親容器の滑り中の水平距離差と垂直距離差の比較である。
結び目
全体としては、スライド衝突のシーンは3種類に分けられ、内外の部の方向が一致しない、内外の部の方向が一致している、前の二つのケースが入れ子されています。どのように解決すれば、どんなに複雑な滑り衝突であっても、分割することができます。一定の規則に従って、第一の状況はスライド距離差、速度差、角度差によって解決できます。業務ニーズに応じた処理規則を作成し、処理規則があれば次の処理を行うことができます。
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。