@@ -108,6 +109,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// Target of Motion events
private View mMotionTarget;
+ // Targets of MotionEvents in split mode
+ private SplitMotionTargets mSplitMotionTargets;
+
// Layout animation
private LayoutAnimationController mLayoutAnimationController;
private Animation.AnimationListener mAnimationListener;
@@ -241,6 +245,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
*/
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
+ /**
+ * When set, this ViewGroup will split MotionEvents to multiple child Views when appropriate.
+ */
+ private static final int FLAG_SPLIT_MOTION_EVENTS = 0x100000;
+
/**
* Indicates which types of drawing caches are to be kept in memory.
* This field should be made private, so it is hidden from the SDK.
@@ -362,6 +371,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
case R.styleable.ViewGroup_descendantFocusability:
setDescendantFocusability(DESCENDANT_FOCUSABILITY_FLAGS[a.getInt(attr, 0)]);
break;
+ case R.styleable.ViewGroup_splitMotionEvents:
+ setMotionEventSplittingEnabled(a.getBoolean(attr, false));
+ break;
}
}
@@ -843,6 +855,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
+ if ((mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == FLAG_SPLIT_MOTION_EVENTS) {
+ return dispatchSplitTouchEvent(ev);
+ }
+
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
@@ -872,7 +888,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
- if (child.dispatchTouchEvent(ev, scrolledXFloat, scrolledYFloat)) {
+ // Single dispatch always picks its target based on the initial down
+ // event's position - index 0
+ if (dispatchTouchEventIfInView(child, ev, 0)) {
mMotionTarget = child;
return true;
}
@@ -952,6 +970,276 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
return target.dispatchTouchEvent(ev);
}
+ /**
+ * This method detects whether the pointer location at pointerIndex within
+ * ev is inside the specified view. If so, the transformed event is dispatched to
+ * child.
+ *
+ * @param child View to hit test against
+ * @param ev MotionEvent to test
+ * @param pointerIndex Index of the pointer within ev to test
+ * @return false if the hit test failed, or the result of
+ * child.dispatchTouchEvent
+ */
+ private boolean dispatchTouchEventIfInView(View child, MotionEvent ev, int pointerIndex) {
+ final float x = ev.getX(pointerIndex);
+ final float y = ev.getY(pointerIndex);
+ final float scrolledX = x + mScrollX;
+ final float scrolledY = y + mScrollY;
+ float localX = scrolledX - child.mLeft;
+ float localY = scrolledY - child.mTop;
+ if (!child.hasIdentityMatrix() && mAttachInfo != null) {
+ // non-identity matrix: transform the point into the view's coordinates
+ final float[] localXY = mAttachInfo.mTmpTransformLocation;
+ localXY[0] = localX;
+ localXY[1] = localY;
+ child.getInverseMatrix().mapPoints(localXY);
+ localX = localXY[0];
+ localY = localXY[1];
+ }
+ if (localX >= 0 && localY >= 0 && localX < (child.mRight - child.mLeft) &&
+ localY < (child.mBottom - child.mTop)) {
+ // It would be safer to clone the event here but we don't for performance.
+ // There are many subtle interactions in touch event dispatch; change at your own risk.
+ child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
+ ev.offsetLocation(localX - x, localY - y);
+ return child.dispatchTouchEvent(ev);
+ }
+ return false;
+ }
+
+ private boolean dispatchSplitTouchEvent(MotionEvent ev) {
+ final SplitMotionTargets targets = mSplitMotionTargets;
+ final int action = ev.getAction();
+ final int maskedAction = ev.getActionMasked();
+ float xf = ev.getX();
+ float yf = ev.getY();
+ float scrolledXFloat = xf + mScrollX;
+ float scrolledYFloat = yf + mScrollY;
+
+ boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
+
+ if (maskedAction == MotionEvent.ACTION_DOWN ||
+ maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
+ final int actionIndex = ev.getActionIndex();
+ final int actionId = ev.getPointerId(actionIndex);
+
+ // Clear out any current target for this ID.
+ // XXX: We should probably send an ACTION_UP to the current
+ // target if present.
+ targets.removeById(actionId);
+
+ // If we're disallowing intercept or if we're allowing and we didn't
+ // intercept
+ if (disallowIntercept || !onInterceptTouchEvent(ev)) {
+ // reset this event's action (just to protect ourselves)
+ ev.setAction(action);
+ // We know we want to dispatch the event down, try to find a child
+ // who can handle it, start with the front-most child.
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
+ || child.getAnimation() != null) {
+ final MotionEvent childEvent = targets.filterMotionEventForChild(ev, child);
+ if (childEvent != null) {
+ try {
+ final int childActionIndex = childEvent.findPointerIndex(actionId);
+ if (dispatchTouchEventIfInView(child, childEvent,
+ childActionIndex)) {
+ targets.add(actionId, child);
+
+ return true;
+ }
+ } finally {
+ childEvent.recycle();
+ }
+ }
+ }
+ }
+
+ // Didn't find a new target. Do we have a "primary" target to send to?
+ final View primaryTarget = targets.getPrimaryTarget();
+ if (primaryTarget != null) {
+ final MotionEvent childEvent =
+ targets.filterMotionEventForChild(ev, primaryTarget);
+ if (childEvent != null) {
+ try {
+ // Calculate the offset point into the target's local coordinates
+ float xc = scrolledXFloat - (float) primaryTarget.mLeft;
+ float yc = scrolledYFloat - (float) primaryTarget.mTop;
+ if (!primaryTarget.hasIdentityMatrix() && mAttachInfo != null) {
+ // non-identity matrix: transform the point into the view's
+ // coordinates
+ final float[] localXY = mAttachInfo.mTmpTransformLocation;
+ localXY[0] = xc;
+ localXY[1] = yc;
+ primaryTarget.getInverseMatrix().mapPoints(localXY);
+ xc = localXY[0];
+ yc = localXY[1];
+ }
+ childEvent.setLocation(xc, yc);
+ if (primaryTarget.dispatchTouchEvent(childEvent)) {
+ targets.add(actionId, primaryTarget);
+ return true;
+ }
+ } finally {
+ childEvent.recycle();
+ }
+ }
+ }
+ }
+ }
+
+ boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
+ (action == MotionEvent.ACTION_CANCEL);
+
+ if (isUpOrCancel) {
+ // Note, we've already copied the previous state to our local
+ // variable, so this takes effect on the next event
+ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+ }
+
+ if (targets.isEmpty()) {
+ // We don't have any targets, this means we're handling the
+ // event as a regular view.
+ ev.setLocation(xf, yf);
+ if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
+ ev.setAction(MotionEvent.ACTION_CANCEL);
+ mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ // if we have targets, see if we're allowed to and want to intercept their
+ // events
+ int uniqueTargetCount = targets.getUniqueTargetCount();
+ if (!disallowIntercept && onInterceptTouchEvent(ev)) {
+ mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
+
+ for (int uniqueIndex = 0; uniqueIndex < uniqueTargetCount; uniqueIndex++) {
+ final View target = targets.getUniqueTargetAt(uniqueIndex);
+
+ // Calculate the offset point into the target's local coordinates
+ float xc = scrolledXFloat - (float) target.mLeft;
+ float yc = scrolledYFloat - (float) target.mTop;
+ if (!target.hasIdentityMatrix() && mAttachInfo != null) {
+ // non-identity matrix: transform the point into the view's coordinates
+ final float[] localXY = mAttachInfo.mTmpTransformLocation;
+ localXY[0] = xc;
+ localXY[1] = yc;
+ target.getInverseMatrix().mapPoints(localXY);
+ xc = localXY[0];
+ yc = localXY[1];
+ }
+
+ ev.setAction(MotionEvent.ACTION_CANCEL);
+ ev.setLocation(xc, yc);
+ if (!target.dispatchTouchEvent(ev)) {
+ // target didn't handle ACTION_CANCEL. not much we can do
+ // but they should have.
+ }
+ }
+ targets.clear();
+ // Don't dispatch this event to our own view, because we already
+ // saw it when intercepting; we just want to give the following
+ // event to the normal onTouchEvent().
+ return true;
+ }
+
+ boolean handled = false;
+ for (int uniqueIndex = 0; uniqueIndex < uniqueTargetCount; uniqueIndex++) {
+ final View target = targets.getUniqueTargetAt(uniqueIndex);
+
+ final MotionEvent targetEvent = targets.filterMotionEventForChild(ev, target);
+ if (targetEvent == null) {
+ continue;
+ }
+
+ try {
+ // Calculate the offset point into the target's local coordinates
+ xf = targetEvent.getX();
+ yf = targetEvent.getY();
+ scrolledXFloat = xf + mScrollX;
+ scrolledYFloat = yf + mScrollY;
+ float xc = scrolledXFloat - (float) target.mLeft;
+ float yc = scrolledYFloat - (float) target.mTop;
+ if (!target.hasIdentityMatrix() && mAttachInfo != null) {
+ // non-identity matrix: transform the point into the view's coordinates
+ final float[] localXY = mAttachInfo.mTmpTransformLocation;
+ localXY[0] = xc;
+ localXY[1] = yc;
+ target.getInverseMatrix().mapPoints(localXY);
+ xc = localXY[0];
+ yc = localXY[1];
+ }
+
+ // finally offset the event to the target's coordinate system and
+ // dispatch the event.
+ targetEvent.setLocation(xc, yc);
+
+ if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
+ targetEvent.setAction(MotionEvent.ACTION_CANCEL);
+ target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
+ targets.removeView(target);
+ uniqueIndex--;
+ uniqueTargetCount--;
+ }
+
+ handled |= target.dispatchTouchEvent(targetEvent);
+ } finally {
+ targetEvent.recycle();
+ }
+ }
+
+ if (maskedAction == MotionEvent.ACTION_POINTER_UP) {
+ final int removeId = ev.getPointerId(ev.getActionIndex());
+ targets.removeById(removeId);
+ }
+
+ if (isUpOrCancel) {
+ targets.clear();
+ }
+
+ return handled;
+ }
+
+ /**
+ * Enable or disable the splitting of MotionEvents to multiple children during touch event
+ * dispatch. This behavior is disabled by default.
+ *
+ *
When this option is enabled MotionEvents may be split and dispatched to different child
+ * views depending on where each pointer initially went down. This allows for user interactions
+ * such as scrolling two panes of content independently, chording of buttons, and performing
+ * independent gestures on different pieces of content.
+ *
+ * @param split true to allow MotionEvents to be split and dispatched to multiple
+ * child views. false to only allow one child view to be the target of
+ * any MotionEvent received by this ViewGroup.
+ */
+ public void setMotionEventSplittingEnabled(boolean split) {
+ // TODO Applications really shouldn't change this setting mid-touch event,
+ // but perhaps this should handle that case and send ACTION_CANCELs to any child views
+ // with gestures in progress when this is changed.
+ if (split) {
+ if ((mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == 0) {
+ mSplitMotionTargets = new SplitMotionTargets();
+ }
+ mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
+ } else {
+ mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
+ mSplitMotionTargets = null;
+ }
+ }
+
+ /**
+ * @return true if MotionEvents dispatched to this ViewGroup can be split to multiple children.
+ */
+ public boolean isMotionEventSplittingEnabled() {
+ return (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == FLAG_SPLIT_MOTION_EVENTS;
+ }
+
/**
* {@inheritDoc}
*/
@@ -3812,4 +4100,263 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
bottomMargin = bottom;
}
}
+
+ private static class SplitMotionTargets {
+ private SparseArray