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:
Adam Powell
2010-08-18 18:14:13 -07:00
parent 8a44bb23c1
commit 2b342f0a76
5 changed files with 596 additions and 34 deletions

View File

@@ -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"

View File

@@ -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.

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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" />