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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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.Configuration;
|
||||
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.InputConnection;
|
||||
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;
|
||||
@@ -5418,37 +5417,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -43,6 +43,7 @@ import android.view.animation.LayoutAnimationController;
|
||||
import android.view.animation.Transformation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -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 <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}
|
||||
*/
|
||||
@@ -3812,4 +4100,263 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user