Allow ViewGroup to split MotionEvents to multiple targets during dispatch.

Use the layout xml attribute splitMotionEvents="true" or the ViewGroup
method setMotionEventSplittingEnabled(true) to enable motion event
splitting. Rules for splitting are as follows:

* Splitting is enabled per ViewGroup. When splitting is enabled any
  MotionEvent dispatched to that ViewGroup can potentially be split
  into several and dispatched to children independently.

* Each pointer is assigned a target child view when the ACTION_DOWN or
  ACTION_POINTER_DOWN event is received. That will be the pointer's
  target until it goes up, the target returns false from onTouchEvent,
  or the MotionEvents are intercepted.

* Multiple pointers may be assigned to the same target. All pointer
  data sent to a target are bundled into a single MotionEvent. Child
  views do not need to be aware that splitting has occurred.

Change-Id: I993f838e2f6b455da9812f4742a016dfcd1c4cc9
diff --git a/api/current.xml b/api/current.xml
index bcc80ac..d826416 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -8919,6 +8919,17 @@
  visibility="public"
 >
 </field>
+<field name="splitMotionEvents"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="16843567"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="src"
  type="int"
  transient="false"
@@ -199360,6 +199371,17 @@
  visibility="protected"
 >
 </method>
+<method name="isMotionEventSplittingEnabled"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
 <method name="measureChild"
  return="void"
  abstract="false"
@@ -199823,6 +199845,19 @@
 <parameter name="animationListener" type="android.view.animation.Animation.AnimationListener">
 </parameter>
 </method>
+<method name="setMotionEventSplittingEnabled"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="split" type="boolean">
+</parameter>
+</method>
 <method name="setOnHierarchyChangeListener"
  return="void"
  abstract="false"
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index f31a248..c13bb8c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -16,7 +16,6 @@
 
 package android.view;
 
-import android.graphics.Camera;
 import com.android.internal.R;
 import com.android.internal.view.menu.MenuBuilder;
 
@@ -25,6 +24,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
+import android.graphics.Camera;
 import android.graphics.Canvas;
 import android.graphics.Interpolator;
 import android.graphics.LinearGradient;
@@ -69,7 +69,6 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.ScrollBarDrawable;
 
-import java.lang.ref.SoftReference;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -5419,37 +5418,6 @@
     }
 
     /**
-     * This method detects whether the given event is inside the view and, if so,
-     * handles it via the dispatchEvent(MotionEvent) method.
-     *
-     * @param ev The event that is being dispatched.
-     * @param parentX The x location of the event in the parent's coordinates.
-     * @param parentY The y location of the event in the parent's coordinates.
-     * @return true if the event was inside this view, false otherwise.
-     */
-    boolean dispatchTouchEvent(MotionEvent ev, float parentX, float parentY) {
-        float localX = parentX - mLeft;
-        float localY = parentY - mTop;
-        if (!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;
-            getInverseMatrix().mapPoints(localXY);
-            localX = localXY[0];
-            localY = localXY[1];
-        }
-        if (localX >= 0 && localY >= 0 && localX < (mRight - mLeft) && localY < (mBottom - 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.
-            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            ev.setLocation(localX, localY);
-            return dispatchTouchEvent(ev);
-        }
-        return false;
-    }
-
-    /**
      * Utility method to determine whether the given point, in local coordinates,
      * is inside the view, where the area of the view is expanded by the slop factor.
      * This method is called while processing touch-move events to determine if the event
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index e2f9c15..e2e3333 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -43,6 +43,7 @@
 import android.view.animation.Transformation;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * <p>
@@ -108,6 +109,9 @@
     // 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;
@@ -242,6 +246,11 @@
     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.
      * {@hide}
@@ -362,6 +371,9 @@
                 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 @@
      */
     @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 @@
                     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;
                         }
@@ -953,6 +971,276 @@
     }
 
     /**
+     * This method detects whether the pointer location at <code>pointerIndex</code> within
+     * <code>ev</code> is inside the specified view. If so, the transformed event is dispatched to
+     * <code>child</code>.
+     *
+     * @param child View to hit test against
+     * @param ev MotionEvent to test
+     * @param pointerIndex Index of the pointer within <code>ev</code> to test
+     * @return <code>false</code> if the hit test failed, or the result of
+     *         <code>child.dispatchTouchEvent</code>
+     */
+    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.
+     *
+     * <p>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 <code>true</code> to allow MotionEvents to be split and dispatched to multiple
+     *              child views. <code>false</code> 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}
      */
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -3812,4 +4100,263 @@
             bottomMargin = bottom;
         }
     }
+
+    private static class SplitMotionTargets {
+        private SparseArray<View> mTargets;
+        private View[] mUniqueTargets;
+        private int mUniqueTargetCount;
+        private long mDownTime;
+        private MotionEvent.PointerCoords[] mPointerCoords;
+        private int[] mPointerIds;
+
+        private static final int INITIAL_UNIQUE_MOTION_TARGETS_SIZE = 5;
+        private static final int INITIAL_BUCKET_SIZE = 5;
+
+        public SplitMotionTargets() {
+            mTargets = new SparseArray<View>();
+            mUniqueTargets = new View[INITIAL_UNIQUE_MOTION_TARGETS_SIZE];
+            mPointerIds = new int[INITIAL_BUCKET_SIZE];
+            mPointerCoords = new MotionEvent.PointerCoords[INITIAL_BUCKET_SIZE];
+            for (int i = 0; i < INITIAL_BUCKET_SIZE; i++) {
+                mPointerCoords[i] = new MotionEvent.PointerCoords();
+            }
+        }
+
+        public void clear() {
+            mTargets.clear();
+            Arrays.fill(mUniqueTargets, null);
+            mUniqueTargetCount = 0;
+        }
+
+        public void add(int pointerId, View target) {
+            mTargets.put(pointerId, target);
+
+            final int uniqueCount = mUniqueTargetCount;
+            boolean addUnique = true;
+            for (int i = 0; i < uniqueCount; i++) {
+                if (mUniqueTargets[i] == target) {
+                    addUnique = false;
+                }
+            }
+            if (addUnique) {
+                if (mUniqueTargets == null) {
+                    mUniqueTargets = new View[INITIAL_UNIQUE_MOTION_TARGETS_SIZE];
+                }
+                if (mUniqueTargets.length == uniqueCount) {
+                    View[] newTargets =
+                        new View[uniqueCount + INITIAL_UNIQUE_MOTION_TARGETS_SIZE];
+                    System.arraycopy(mUniqueTargets, 0, newTargets, 0, uniqueCount);
+                    mUniqueTargets = newTargets;
+                }
+                mUniqueTargets[uniqueCount] = target;
+                mUniqueTargetCount++;
+            }
+        }
+
+        public int getIdCount() {
+            return mTargets.size();
+        }
+
+        public int getUniqueTargetCount() {
+            return mUniqueTargetCount;
+        }
+
+        public View getUniqueTargetAt(int index) {
+            return mUniqueTargets[index];
+        }
+
+        public View get(int id) {
+            return mTargets.get(id);
+        }
+
+        public int indexOfId(int id) {
+            return mTargets.indexOfKey(id);
+        }
+
+        public int indexOfTarget(View target) {
+            return mTargets.indexOfValue(target);
+        }
+
+        public int idAt(int index) {
+            return mTargets.keyAt(index);
+        }
+
+        public View targetAt(int index) {
+            return mTargets.valueAt(index);
+        }
+
+        public View getPrimaryTarget() {
+            if (!isEmpty()) {
+                return mUniqueTargets[0];
+            }
+            return null;
+        }
+
+        public boolean hasTarget(View target) {
+            final View[] unique = mUniqueTargets;
+            final int uniqueCount = mUniqueTargetCount;
+            for (int i = 0; i < uniqueCount; i++) {
+                if (unique[i] == target) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public boolean isEmpty() {
+            return mUniqueTargetCount == 0;
+        }
+
+        public void removeById(int id) {
+            final int index = mTargets.indexOfKey(id);
+            removeAt(index);
+        }
+        
+        public void removeView(View view) {
+            int i = 0;
+            while (i < mTargets.size()) {
+                if (mTargets.valueAt(i) == view) {
+                    mTargets.removeAt(i);
+                } else {
+                    i++;
+                }
+            }
+            removeUnique(view);
+        }
+
+        public void removeAt(int index) {
+            if (index < 0 || index >= mTargets.size()) {
+                return;
+            }
+
+            final View removeView = mTargets.valueAt(index);
+            mTargets.removeAt(index);
+            if (mTargets.indexOfValue(removeView) < 0) {
+                removeUnique(removeView);
+            }
+        }
+
+        private void removeUnique(View removeView) {
+            View[] unique = mUniqueTargets;
+            int uniqueCount = mUniqueTargetCount;
+            for (int i = 0; i < uniqueCount; i++) {
+                if (unique[i] == removeView) {
+                    unique[i] = unique[--uniqueCount];
+                    unique[uniqueCount] = null;
+                    break;
+                }
+            }
+
+            mUniqueTargetCount = uniqueCount;
+        }
+
+        /**
+         * Return a new (obtain()ed) MotionEvent containing only data for pointers that should
+         * be dispatched to child. Don't forget to recycle it!
+         */
+        public MotionEvent filterMotionEventForChild(MotionEvent ev, View child) {
+            int action = ev.getAction();
+            final int maskedAction = action & MotionEvent.ACTION_MASK;
+
+            // Only send pointer up events if this child was the target. Drop it otherwise.
+            if (maskedAction == MotionEvent.ACTION_POINTER_UP &&
+                    get(ev.getPointerId(ev.getActionIndex())) != child) {
+                return null;
+            }
+
+            int pointerCount = 0;
+            final int idCount = getIdCount();
+            for (int i = 0; i < idCount; i++) {
+                if (targetAt(i) == child) {
+                    pointerCount++;
+                }
+            }
+
+            int actionId = -1;
+            boolean needsNewIndex = false; // True if we should fill in the action's masked index
+
+            // If we have a down event, it wasn't counted above.
+            if (maskedAction == MotionEvent.ACTION_DOWN) {
+                pointerCount++;
+                actionId = ev.getPointerId(0);
+                mDownTime = ev.getDownTime();
+            } else if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
+                pointerCount++;
+
+                actionId = ev.getPointerId(ev.getActionIndex());
+
+                if (indexOfTarget(child) < 0) {
+                    // The new action should be ACTION_DOWN if this child isn't currently getting
+                    // any events.
+                    action = MotionEvent.ACTION_DOWN;
+                } else {
+                    // Fill in the index portion of the action later.
+                    needsNewIndex = true;
+                }
+            } else if (maskedAction == MotionEvent.ACTION_POINTER_UP) {
+                actionId = ev.getPointerId(ev.getActionIndex());
+                if (pointerCount == 1) {
+                    // The new action should be ACTION_UP if there's only one pointer left for
+                    // this target.
+                    action = MotionEvent.ACTION_UP;
+                } else {
+                    // Fill in the index portion of the action later.
+                    needsNewIndex = true;
+                }
+            }
+
+            if (pointerCount == 0) {
+                return null;
+            }
+
+            // Fill the buckets with pointer data!
+            final int eventPointerCount = ev.getPointerCount();
+            int bucketIndex = 0;
+            int newActionIndex = -1;
+            for (int evp = 0; evp < eventPointerCount; evp++) {
+                final int id = ev.getPointerId(evp);
+
+                // Add this pointer to the bucket if it is new or targeted at child
+                if (id == actionId || get(id) == child) {
+                    // Expand scratch arrays if needed
+                    if (mPointerCoords.length <= bucketIndex) {
+                        int[] pointerIds = new int[pointerCount];
+                        MotionEvent.PointerCoords[] pointerCoords =
+                                new MotionEvent.PointerCoords[pointerCount];
+                        for (int i = mPointerCoords.length; i < pointerCoords.length; i++) {
+                            pointerCoords[i] = new MotionEvent.PointerCoords();
+                        }
+
+                        System.arraycopy(mPointerCoords, 0,
+                                pointerCoords, 0, mPointerCoords.length);
+                        System.arraycopy(mPointerIds, 0, pointerIds, 0, mPointerIds.length);
+
+                        mPointerCoords = pointerCoords;
+                        mPointerIds = pointerIds;
+                    }
+
+                    mPointerIds[bucketIndex] = id;
+                    ev.getPointerCoords(evp, mPointerCoords[bucketIndex]);
+
+                    if (needsNewIndex && id == actionId) {
+                        newActionIndex = bucketIndex;
+                    }
+
+                    bucketIndex++;
+                }
+            }
+
+            // Encode the new action index if we have one
+            if (newActionIndex >= 0) {
+                action = (action & MotionEvent.ACTION_MASK) |
+                        (newActionIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+            }
+
+            MotionEvent result = MotionEvent.obtain(mDownTime, ev.getEventTime(),
+                    action, pointerCount, mPointerIds, mPointerCoords, ev.getMetaState(),
+                    ev.getXPrecision(), ev.getYPrecision(), ev.getDeviceId(), ev.getEdgeFlags(),
+                    ev.getSource());
+            return result;
+        }
+    }
 }
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index c65631d..e611b2b 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1453,6 +1453,17 @@
             <enum name="blocksDescendants" value="2" />
         </attr>
 
+        <!-- Sets whether this ViewGroup should split MotionEvents
+             to separate child views during touch event dispatch.
+             If false (default), touch events will be dispatched to
+             the child view where the first pointer went down until
+             the last pointer goes up.
+             If true, touch events may be dispatched to multiple children.
+             MotionEvents for each pointer will be dispatched to the child
+             view where the initial ACTION_DOWN event happened.
+             See {@link android.view.ViewGroup#setSplitMotionEvents(boolean)}
+             for more information. -->
+        <attr name="splitMotionEvents" format="boolean" />
     </declare-styleable>
 
     <!-- A {@link android.view.ViewStub} lets you lazily include other XML layouts
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index c89b4c0..ea5c158 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1324,6 +1324,7 @@
   <public type="attr" name="imeSubtypeLocale" />
   <public type="attr" name="imeSubtypeMode" />
   <public type="attr" name="imeSubtypeExtraValue" />
+  <public type="attr" name="splitMotionEvents" />
 
   <public type="anim" name="animator_fade_in" />
   <public type="anim" name="animator_fade_out" />