Androidスライドコンポーネントネスト一般構想、マルチタスクジェスチャー構想、タッチ伝達構想、【例】listviewネストviewpager


Android UIの開発では、このようなニーズによく遭遇します.
スライドをサポートする2つのコンポーネント、例えばlistviewは複数のlistviewをネストし、listviewのitemはviewpagerまたはgallaryですか?あるいはscrollviewネストscrollviewなどです.
一般的には、次の機能をサポートする必要があります.
2層のコンポーネントがスライド可能
2つのコンポーネントを同時にスライドさせないか、2つのコンポーネントを同時にスライドさせて自分で調整できる
下のviewのサブviewとネストされたviewのサブviewに影響しないクリックイベント
上記の機能を実装する際には、いくつかの問題が発生します.
クリックイベントがブロックされている
°viewのスライドがスムーズではありません(ここでは議論しません)
°viewのスライド条件ロジックが設計に合致しない
├どのように関数を書き換えて論理を満たすか分からない
ここでは,このようなニーズを解決するための一般的な考え方と,マルチタスクジェスチャーをサポートするlistviewとviewpagerのニーズ事例を紹介する.
タッチ伝達の考え方:
スライド可能な2つのviewをネストする必要がある場合は、いくつかの問題に注意してください.
Listviewやscrollview、view pagerなどのスライド可能なコンポーネントには、独自のスライドルールがあります.どのようにスライドするかを書き直してもいいです(つまり、タッチした座標を傍受してコードでスライドしないほうがいいです).必要な非スライドビジネスを書けばいいのですが、デフォルトのスライドルールの実行を遮断することはできません.
°viewgroup、viewのイベント配信伝達メカニズムは特に明確にする必要があります.listviewはviewgroupから継承され、一連のタッチイベントが発生したとき、現在のactivityはこのイベントを受け取り、dispatchは最上位viewgroupに与えられます.最上位viewgroupはまずdispatchTouchEventを呼び出します.この関数の内部でonInterceptTouchEvent関数で切断するかどうかを決定します.切断を選択した場合、自分のonTouchEventを実行すると、サブviewはこのTouchEventを受け入れません.カットしない場合、viewGroupはすべてのクリック範囲内のサブviewに配布されます(クリック範囲内のサブviewに配布したくない場合は、より多くのdispatchEventの部分を書き換える必要があります)、すなわち、サブviewのdispatchTouchEventを呼び出し、サブviewの中にtrue(サブViewがこのイベントを消費したことを表す)を返すものがある限り、このviewGroupはこのTouchEventを実行しません.trueが返されない場合、viewGroupのOnTouchEvent()が呼び出され、falseが返されると、このイベントが消費されていないことを示し、onClickが呼び出され、消費されていない場合、viewgroupのdispatch関数はfalseを返します.
この論理は複雑かもしれませんが、viewにonInterceptTouchがない以外は、次のようにまとめることができます.
各viewは、イベントを受信し、そのイベントを遮断しないと判断し、サブviewがこのイベントを消費するか否かを判断し、なければonTouchEventを実行し、すなわち自分が消費しようとし、自分が消費しなければ、そのviewのdispatchはfalseに戻り、そのviewの分岐を含めてイベントを消費していないことを示す.
°コード形式で明確に書くため、ビジネス要件を明確にする必要があります(親viewのマルチタスクジェスチャーを実行するために、親viewで切断するか、サブviewのdispatchでfalseを返すかをためらう場合があります)
子view独占父view事件:this.getParent().requestDisallowInterceptTouchEvent(true);この方法は、親viewがイベントを遮断することを禁止します.すなわち、イベントを必ず受け入れることができ、タッチが完了したときにこれを閉じることを覚えています.
マルチタスクジェスチャーの考え方:
読者にとって、1つのviewのマルチタスクジェスチャーを設計するのはそんなに難しくないと信じています(onTouchEventを書き直して、押して、移動して、持ち上げた座標を記録して相応の演算をします)、しかしこの問題はviewqのネストの上で置いて、あなたは考慮することができます.このような問題に直面する可能性があります.
°子viewがtouchEventを消費した場合、親viewの動作は呼び出されません.
解決:
1.親子ビューでこのイベントを同時に消費したい場合は、親ビューのdispatchを書き換え、onTouchEventを強制的に呼び出す必要があります.
2.このような場合、子viewがこの事件を消費することを望んでいません.2つの案があります.親viewのinterceptを書き直し、ジェスチャーをチェックし、この事件を遮断します.サブviewのdispatchを書き直し、ジェスチャーをチェックしてfalseに戻ります.1つは親view強制遮断であり、1つは子view強制非消費である.
3.現在のステータスがどのviewで消費すべきか分からない場合は、遮断や消費が必要だと判断するまで放置することができます(ビジネスニーズがこのステータスを明確に定義していないため、どのように処理するかを定義する必要はありません).
ソースコードの一部を添付した簡単なケースです.
需要説明:
Listviewはviewpagerをネストし、listviewの各itemはxmlレイアウトによって定義され、レイアウトにはviewpagerと他の部分が含まれています.
サポートされるアクション:
°viewpagerは左右にスライドでき、listviewは上下にスライドでき、同時にスライドすることはなく、自然に、最初にポイントしたviewpagerだけがスライドでき、他のviewpagerはスライドしません.
°listviewはダブル指の縮小操作をサポートします.
°listviewはitemClick操作をサポートします(viewpagerの画像をクリックしません).
°viewpagerの画像はクリック操作をサポートします.
まずlistviewの部分です.
onItemClickは自分のビジネスイベントを定義し、各itemをクリックしてviewpagerの画像viewに消費されなかったときにトリガーします.
dispatchTouchEvent()において、現在の手指指数が2以上である、すなわち両指操作の場合には、onTouchEventを強制的に呼び出し、戻ると、superは呼び出されないと判断する.dispatchTouchEvent()は、イベントがサブビューに入らないということです.
onTouchEvent()では、マルチタスクジェスチャーのビジネスニーズを簡単に実現できます.
@Override
	public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
		// TODO Auto-generated method stub
		if (mSet.get(arg2).size() > 1)
			this.downGranularity(arg2);
	}

	/**
	 *    listview          。
	 * @author ipip
	 *  2014 8 4   10:46:53
	 */
	private class MyListView extends ListView {
		public MyListView(Context context) {
			super(context);
			this.setDivider(null);
			// TODO Auto-generated constructor stub
		}

		/**
		 *   listView     
		 */
		@Override
		public boolean onTouchEvent(MotionEvent ev) {
			switch (ev.getAction() & MotionEvent.ACTION_MASK) {
			case MotionEvent.ACTION_DOWN:
			case MotionEvent.ACTION_POINTER_DOWN:
				if (ev.getPointerCount() == 2)
					dst = measureFingers(ev);
				break;
			case MotionEvent.ACTION_UP:
			case MotionEvent.ACTION_POINTER_UP:

				if (ev.getPointerCount() == 2 && ndst < dst) {
					upGranularity();
					dst = -1;
				}
				break;

			case MotionEvent.ACTION_MOVE:
				if (ev.getPointerCount() >= 2) {
					ndst = measureFingers(ev);
				}
				break;
			}
			if (ev.getPointerCount() >= 2)
				return true;
			return super.onTouchEvent(ev);
		}
		/**
		 * 
		 */
		@Override
		public boolean dispatchTouchEvent(MotionEvent ev) {
			if (ev.getPointerCount() >= 2) {
				return onTouchEvent(ev);
			}
			return super.dispatchTouchEvent(ev);
		}
	}

次は
viewpagerのコード:
まず私のonTouchEvent()を説明します.私のviewpagerは1行に4枚の画像を収容できるので(アダプタにgetPageWidth()を書き換える)、画像数が4未満の場合、スライドイベントを処理しません.そうしないと、画像が瞬時に点滅する現象が発生します(実はこれは面白いですが、原理は分かりません).
dispatchでは,まず両指操作の有無を判断する.次に、1回目の操作の位置および移動毎の判断を記録する.
public class MyViewPager extends ViewPager {

	public MyViewPager(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}

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

	private float xDown;//            。
	private float xMove;//            。
	private float yDown;//            。
	private float yMove;//            。
	private boolean viewPagerScrolling = false;
	private boolean fatherScrolling = false;

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (this.getChildCount() < 4)
			return false;
		return super.onTouchEvent(ev);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		// TODO Auto-generated method stub
		if (ev.getPointerCount() >= 2)
			return false;

		switch (ev.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_POINTER_DOWN:
			xDown = ev.getRawX();
			yDown = ev.getRawY();
			fatherScrolling = false;
			break;
		case MotionEvent.ACTION_MOVE:
			xMove = ev.getRawX();
			yMove = ev.getRawY();
			if (fatherScrolling) {
				return false;
			}
			if (viewPagerScrolling) {
				return super.dispatchTouchEvent(ev);
			}

			if (Math.abs(yMove - yDown) < 10 && Math.abs(xMove - xDown) > 3) {
				this.getParent().requestDisallowInterceptTouchEvent(true);
				viewPagerScrolling = true;
			} else if (Math.abs(yMove - yDown) >= 10) {
				fatherScrolling = true;
				return false;
			} else
				return false;
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_POINTER_UP:
			viewPagerScrolling = false;
			if (ev.getPointerCount() == 1)
				this.getParent().requestDisallowInterceptTouchEvent(false);
			break;
		}
		return super.dispatchTouchEvent(ev);
	}
}

上のキー:
			if (fatherScrolling) {
				return false;
			}
			if (viewPagerScrolling) {
				return super.dispatchTouchEvent(ev);
			}

			if (Math.abs(yMove - yDown) < 10 && Math.abs(xMove - xDown) > 3) {
				this.getParent().requestDisallowInterceptTouchEvent(true);
				viewPagerScrolling = true;
			} else if (Math.abs(yMove - yDown) >= 10) {
				fatherScrolling = true;
				return false;
			} else
				return false;

この点で明らかな横移動があり、垂直方向が所定の数値内である場合、viewpagerの横スライド操作を実行すると判定すると、viewPagerScrolingをtrueとして付与すると、次のすべてのmoveがsuperをデフォルトで呼び出す.dispatchTouchEvent()は、親viewのブロックを拒否し、一連のイベントの終了までデフォルトのスライド方式を適用します.垂直方向の移動が範囲を超え、かつ前の横方向の移動が顕著でない場合、親viewのlistviewがスライドすると判定し、fatherscrollingをtureとして付与すると、その後moveのたびにfalseが返され、すなわち消費されない後のすべてのイベントが返される.
もちろん、すべての指が持ち上げられた後、これらの状態はリセットされました.
その後、viewpagerの各imageViewは簡単にonClickListener()を設定することができ、親viewとおじいさんviewはクリックイベントを遮断しません.
簡単に説明します.なぜ、itemClickとimageViewのclickが遮断されていないのですか.
祖先viewはdown-upを消費せず、down-move-upを消費しないという事件を遮断していないことが前提です.簡単に言えば、押すとすぐに持ち上げるようなclickのイベントは、2層のviewも消費されず、デフォルトでスムーズにサブviewに伝えることができます.
Noなぜviewpagerで一定の横移動があるかどうかを判断するのですか?
viewpagerにこのイベントを直接消費させると、父viewはこのイベントを消費する機会がなくなり、私は一定の移動を基礎にしてこそ、縦に滑るのか横に滑るのかを判断することができます.
まずサブviewを消費させて、縦方向に移動しすぎるとlistview消費に渡すことができると言います.では、実は私はやはり横方向の移動の距離を判断しなければなりません.もし横方向に100 px移動したら、それから縦方向に15 px移動したのでviewpager消費と業務需要が異なることはありません.同じように横方向の距離を計算する以上、最善の方法は先に待つべきで、タイミングが成熟したときにそのイベントのviewをイベントチェーンが終わるまでロックします.
何でlistviewでdispatchでinterceptではなく双指かどうかを判断したの?
ああ、実は全部いいです...
以下は効果図です.新版adtのScreen Recordingで録画します.kitkatから始まるadtは録画をサポートしているようです.
---------------------------------------------------------------------
2014/8/6、修正補足:
原文のキーコードが変更されました.
if (fatherScrolling) {
				return false;
			}
			if (viewPagerScrolling) {
				return super.dispatchTouchEvent(ev);
			}
			float dx = Math.abs(xMove - xDown), dy = Math.abs(yMove - yDown);
			if (dx > 3 && dx > dy && this.getChildCount() >= 4) {
				this.getParent().requestDisallowInterceptTouchEvent(true);
				viewPagerScrolling = true;
			} else if (dy > 3 && dy > dx) {
				fatherScrolling = true;
				return false;
			} else
				return false;
は大量の試みとLog観察を経て、既存の論理が私たちのユーザー体験に影響を与える可能性が高いことを発見した.
従来の方法ではスライドに応答しない場合がある.
新しい方法は,一方の方向のオフセットが3より大きく,他方のオフセットより大きいことを検出すると,垂直にスライドするか横にスライドするかを決定し,ユーザの2つの方向のオフセット,すなわちどのスライド方式がより傾向にあるかに完全に依存する.