/* MonkeyTalk - a cross-platform functional testing tool
Copyright (C) 2012 Gorilla Logic, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package com.gorillalogic.fonemonkey.automators;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.SystemClock;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import com.gorillalogic.fonemonkey.Log;
import com.gorillalogic.fonemonkey.TouchListener;
import com.gorillalogic.monkeytalk.automators.AutomatorConstants;
/**
* @author sstern
*
*/
public class ViewAutomator extends AutomatorBase implements OnTouchListener, /* OnClickListener, */
OnLongClickListener {
private Map<Integer, String> idMap = null;
protected Object component;
protected String monkeyId;
static String componentType = AutomatorConstants.TYPE_VIEW;
private static Class<?> componentClass = View.class;
static {
Log.log("Initializing ViewAutomator");
AutomationManager.registerClass(componentType, componentClass, ViewAutomator.class);
}
/**
* You must set the target component
*
* @param v
*/
public void setComponent(Object o) {
component = o;
}
@Override
public Class<?> getComponentClass() {
return componentClass;
}
@Override
public String getMonkeyID() {
if (monkeyId == null) {
monkeyId = getDefaultMonkeyID();
}
return monkeyId;
}
@Override
public String getComponentType() {
return componentType;
}
@Override
public Object getComponent() {
return component;
}
public View getView() {
return (View) getComponent();
}
public List<String> getRawMonkeyIdCandidates() {
List<String> list = new ArrayList<String>();
View v = getView();
if (v == null) {
return list;
}
CharSequence desc = v.getContentDescription();
if (desc != null) {
list.add(desc.toString());
}
String id = getId();
if (id != null) {
list.add(id);
}
Object tag = v.getTag();
if (tag != null && tag instanceof String) {
list.add(tag.toString());
}
return list;
}
public List<String> getIdentifyingValues() {
List<String> list = super.getIdentifyingValues();
list.addAll(this.getRawMonkeyIdCandidates());
return list;
}
@Override
public boolean canAutomate(String componentType, String monkeyId) {
return getView().isShown() && super.canAutomate(componentType, monkeyId);
}
protected String getDefaultMonkeyID() {
View v = getView();
if (v == null) {
return "";
}
String id = getRawMonkeyId();
if (id != null) {
return id;
}
String o = formatOrdinalFor(v);
return o == null ? "" : o;
}
protected String getRawMonkeyId() {
List<String> candidates = getRawMonkeyIdCandidates();
for (String candidate : candidates) {
if (candidate != null && candidate.trim().length() > 0) {
return candidate;
}
}
return null;
}
private boolean ordInit = false;
private int ordinal;
@Override
public int getOrdinal() {
if (!ordInit || ordinal == -1) {
ordinal = findOrdinalFor(this.getView());
ordInit = true;
}
return ordinal;
}
static String formatOrdinalFor(View v) {
int index = findOrdinalFor(v);
return index > 1 ? "#" + String.valueOf(index) : null;
}
static int findOrdinalFor(View v) {
IAutomator ia = AutomationManager.findAutomator(v);
if (ia != null && !(ia instanceof ViewAutomator)) {
return -1;
}
ViewAutomator auto = (ViewAutomator) ia;
if (!auto.isAutomatable()) {
return -1;
}
Class<?> targetComponentType = auto.getComponentClass();
return findOrdinalFor(v, targetComponentType);
}
public static int findOrdinalFor(View target, Class<?> targetComponentType) {
int[] ordinal = new int[] { 0 };
for (View r : AutomationManager.getRoots()) {
if (r instanceof ViewGroup) {
ViewGroup root = (ViewGroup) r;
if (_findOrdinalFor(root, target, targetComponentType, ordinal)) {
return ordinal[0] + 1;
}
}
}
return -1;
}
public static boolean _findOrdinalFor(ViewGroup root, View target,
Class<?> targetComponentType, int[] ordinal) {
for (int i = 0; i < root.getChildCount(); ++i) {
View v = root.getChildAt(i);
ViewAutomator auto = (ViewAutomator) AutomationManager.findAutomator(v);
if (!v.isShown() || auto.isHiddenByParent()) {
continue;
}
if (v.equals(target))
return true;
// if (target.getClass().isInstance(v))
if (auto.getComponentClass().equals(targetComponentType)) {
if (auto.isAutomatable()) {
ordinal[0]++;
}
}
if (v instanceof ViewGroup) {
if (_findOrdinalFor((ViewGroup) v, target, targetComponentType, ordinal))
return true;
}
}
return false;
}
/**
* Return false if this component is hidden from automation
*
* @return
*/
protected boolean isAutomatable() {
return true;
}
@Override
public String play(final String action, final String... args) {
if (action.equalsIgnoreCase(AutomatorConstants.ACTION_TAP)) {
AutomationManager.runOnUIThread(new Runnable() {
public void run() {
if (args.length == 2) {
int x = getIntegerArg(action, args[0], 0);
int y = getIntegerArg(action, args[1], 1);
tap(x, y);
} else {
tap();
}
}
});
return null;
}
if (action.equalsIgnoreCase(AutomatorConstants.ACTION_LONG_PRESS)) {
AutomationManager.runOnUIThread(new Runnable() {
public void run() {
getView().performLongClick();
}
});
return null;
}
// if (action.equalsIgnoreCase(AutomatorConstants.ACTION_GET)) {
// assertArgCount(AutomatorConstants.ACTION_GET, args, 1);
// if (args.length == 1) {
// return getValue();
// } else {
// return getValue(args[1]);
// }
// }
if (action.equalsIgnoreCase(AutomatorConstants.ACTION_DRAG)) {
if (args.length < 4) {
throw new IllegalArgumentException(action
+ " requires at least 4 arguments. Found " + args.length);
}
int[] points = new int[args.length];
int i = 0;
for (String arg : args) {
try {
points[i++] = Integer.valueOf(arg);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(AutomatorConstants.ACTION_DRAG
+ " requires numeric arguments. Found " + arg);
}
}
drag(points);
return null;
}
if (action.equalsIgnoreCase(AutomatorConstants.ACTION_PINCH)) {
if (args.length < 1 || args[0].length() < 1) {
throw new IllegalArgumentException(AutomatorConstants.ACTION_PINCH
+ " requires at least 1 argument numeric argument");
}
float arg;
try {
arg = Float.valueOf(args[0]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(AutomatorConstants.ACTION_PINCH
+ " requires numeric arguments. Found " + args[0]);
}
pinch(arg);
return null;
}
if (action.equalsIgnoreCase(AutomatorConstants.ACTION_SWIPE)) {
if (args.length < 1 || args[0].length() < 1) {
throw new IllegalArgumentException(AutomatorConstants.ACTION_SWIPE
+ " requires at least 1 argument: Down, Up, Left, or Right");
}
swipe(args[0]);
return null;
}
if (action.equalsIgnoreCase(AutomatorConstants.TOUCH_DOWN)) {
touch(0, args);
return null;
} else if (action.equalsIgnoreCase(AutomatorConstants.TOUCH_UP)) {
touch(1, args);
return null;
} else if (action.equalsIgnoreCase(AutomatorConstants.TOUCH_MOVE)) {
touch(2, args);
return null;
}
return super.play(action, args);
}
protected void tap() {
tap(getView().getWidth() / 2, getView().getHeight() / 2);
}
private static int PADDING = 50;
// private static Object syncObject = new Object();
protected void tap(int x, int y) {
tap(x, y, 25);
}
protected void tap(int x, int y, int duration) {
View v = getView();
int dx = 0;
int dy = 0;
int top = v.getScrollY();
int bottom = top + v.getHeight();
int left = v.getScrollX();
int right = left + v.getWidth();
if (y < top) {
dy = y - (top + PADDING);
} else if (y > bottom) {
dy = y - (bottom - PADDING);
}
if (x < left) {
dx = x - (left + PADDING);
} else if (x > right) {
dx = x - (right - PADDING);
}
if (dx != 0 || dy != 0) {
v.scrollTo(left + dx, top + dy);
left = v.getScrollX();
top = v.getScrollY();
}
long start = SystemClock.uptimeMillis();
final MotionEvent down = MotionEvent.obtain(start, start, MotionEvent.ACTION_DOWN,
x - left, y - top, 0);
final MotionEvent up = MotionEvent.obtain(start, start + duration, MotionEvent.ACTION_UP, x
- left, y - top, 0);
// Can't sleep on UIThread to wait for ACTION_UP event, sandwich TouchEvents with a
// Thread.sleep in between to allow for long presses.
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
ViewAutomator.this.getView().dispatchTouchEvent(down);
}
});
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
// do nothing
}
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
ViewAutomator.this.getView().dispatchTouchEvent(up);
}
});
}
protected void enterText(final String s) {
enterText(s, false);
}
// KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
final static KeyCharacterMap characterMap = KeyCharacterMap
.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
protected void enterText(final String s, final boolean hitDone) {
AutomationManager.runOnUIThread(new Runnable() {
public void run() {
KeyEvent[] keys = characterMap.getEvents(s.toCharArray());
if (keys == null) {
Log.log("Unable to find chars for \"" + s + "\" in builtin keyboard keymap");
// special characters not found in charactermap, so
// outputting via zapping rather than simulating keyboard
if (getView() instanceof TextView) {
((TextView) getView()).setText(s);
}
} else {
for (KeyEvent key : keys) {
getView().dispatchKeyEvent(key);
}
}
if (hitDone) {
KeyEvent down = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
getView().dispatchKeyEvent(down);
KeyEvent up = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER);
getView().dispatchKeyEvent(up);
InputMethodManager imm = (InputMethodManager) getView().getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
}
}
});
}
private void drag(final int... points) {
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
long start = SystemClock.uptimeMillis();
MotionEvent down = MotionEvent.obtain(start, start, MotionEvent.ACTION_DOWN,
points[0], points[1], 0);
ViewAutomator.this.getView().dispatchTouchEvent(down);
for (int i = 0; i < points.length; i += 2) {
MotionEvent move = MotionEvent.obtain(start, start + i * 100,
MotionEvent.ACTION_MOVE, points[i], points[i + 1], 0);
ViewAutomator.this.getView().dispatchTouchEvent(move);
}
MotionEvent up = MotionEvent.obtain(start, start + points.length * 100,
MotionEvent.ACTION_UP, points[points.length - 2],
points[points.length - 1], 0);
ViewAutomator.this.getView().dispatchTouchEvent(up);
}
});
}
private void swipe(String direction) {
int startX, startY, endX, endY;
boolean isUp = direction.equalsIgnoreCase("up");
boolean isVertical = isUp || direction.equalsIgnoreCase("down");
if (isVertical) {
startX = this.getView().getWidth() / 2;
endX = startX;
if (isUp) {
startY = this.getView().getHeight() - 1;
endY = 1;
} else {
startY = 1;
endY = this.getView().getHeight() - 1;
}
} else {
startY = this.getView().getHeight() / 2;
endY = startY;
if (direction.equalsIgnoreCase("left")) {
startX = this.getView().getWidth() - 1;
endX = 1;
} else {
startX = 1;
endX = this.getView().getWidth() - 1;
;
}
}
long start = SystemClock.uptimeMillis();
final MotionEvent down = MotionEvent.obtain(start, start, MotionEvent.ACTION_DOWN, startX,
startY, 0);
final MotionEvent move = MotionEvent.obtain(start, start + 500, MotionEvent.ACTION_MOVE,
(startX + endX) / 2, (startY + endY) / 2, 0);
// By making the time between the MOVE and UP events short (start+999 vs
// start+1000), we simulate a "fling"
final MotionEvent move1 = MotionEvent.obtain(start, start + 999, MotionEvent.ACTION_MOVE,
endX, endY, 0);
final MotionEvent up = MotionEvent.obtain(start, start + 1000, MotionEvent.ACTION_UP, endX,
endY, 0);
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
dispatchEvent(down);
dispatchEvent(move);
dispatchEvent(move1);
dispatchEvent(up);
}
});
}
public void dispatchEvent(final MotionEvent event) {
ViewAutomator.this.getView().dispatchTouchEvent(event);
// ViewAutomator.this.getView().onTouchEvent(event);
}
// Used by WebViewAutomator and the ScrollerAutomator.
protected void scroll(int x, int y) {
View view = getView();
view.scrollBy(x, y);
}
// Android doesn't recognize pinches starting to close to the edge (what sdk calls "edgeSlop")
// We adjust accordingly. This value may need to be dynamic. Currently hardcoding to 50.
private static int edgeSlop = 50;
public void pinch(final float scale) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
throw new IllegalArgumentException(
"Pinch playback not supported for Android "
+ Build.VERSION.RELEASE
+ ". MonkeyTalk only supports pinch playback for Android 2.3 Gingerbread and later.");
}
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
int width = getView().getWidth();
int slopWidth = width - edgeSlop;
int startDist = (int) (scale < 1 ? slopWidth : slopWidth / scale);
int endDist = (int) (scale < 1 ? (slopWidth) * scale : slopWidth);
int zoomDir = startDist - endDist > 0 ? 1 : -1;
int y = getView().getHeight() / 2;
int x = width / 2;
int startX1 = x + startDist / 2;
int startX2 = x - startDist / 2;
int endX1 = x + endDist / 2;
int endX2 = x - endDist / 2;
long downTime = SystemClock.uptimeMillis();
int pointers = 2;
int[] pointerIds = { 1, 0 };
int metaState = 0;
int xPrecision = 1;
int yPrecision = 1;
int deviceId = 0;
int edgeFlags = 0;
int pressure = 1;
int size = 1;
int source = 0;
int flags = 0;
Log.log("pinch " + startX1 + " " + startX2 + " " + endX1 + " " + endX2);
// try {
// Method method;
// method = MotionEvent.class.getMethod("obtainNano", long.class, long.class,
// long.class,
// int.class, int.class, int[].class, float[].class, int.class,
// float.class, float.class, int.class, int.class);
//
// } catch (NoSuchMethodException e) {
// Method method = MotionEvent.class.getMethod("obtain", long.class, long.class,
// int.class, int.class, int[].class, PointerCoords[].class, int.class,
// float.class, float.class, int.class, int.class, int.class, int.class);
// }
MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
1, startX1, y, pressure, size, metaState, xPrecision, yPrecision, deviceId,
edgeFlags);
dispatchEvent(event);
PointerCoords p1 = new PointerCoords();
p1.x = startX1;
p1.pressure = 1;
p1.y = y;
PointerCoords p2 = new PointerCoords();
p2.x = startX2;
p2.pressure = 1;
p2.y = y;
PointerCoords[] pointerCoords = { p2, p1 };
event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_POINTER_1_DOWN,
pointers, pointerIds, pointerCoords, metaState, 1, 1, deviceId, edgeFlags,
source, flags);
dispatchEvent(event);
p1 = new PointerCoords();
p1.pressure = 1;
p1.x = startX1 - zoomDir * edgeSlop;
p1.y = y;
p2 = new PointerCoords();
p2.x = startX2 + zoomDir * edgeSlop;
p2.pressure = 1;
p2.y = y;
pointerCoords = new PointerCoords[] { p2, p1 };
event = MotionEvent.obtain(downTime, downTime + 1000, MotionEvent.ACTION_MOVE,
pointers, pointerIds, pointerCoords, metaState, 1, 1, deviceId, edgeFlags,
source, flags);
dispatchEvent(event);
p1 = new PointerCoords();
p1.pressure = 1;
p1.x = endX1;
p1.y = y;
p2 = new PointerCoords();
p2.x = endX2;
p2.pressure = 1;
p2.y = y;
pointerCoords = new PointerCoords[] { p2, p1 };
event = MotionEvent.obtain(downTime, downTime + 1000, MotionEvent.ACTION_MOVE,
pointers, pointerIds, pointerCoords, metaState, 1, 1, deviceId, edgeFlags,
source, flags);
dispatchEvent(event);
event = MotionEvent.obtain(downTime, downTime + 1000,
MotionEvent.ACTION_POINTER_1_UP, pointers, pointerIds, pointerCoords,
metaState, 1, 1, deviceId, edgeFlags, source, flags);
dispatchEvent(event);
event = MotionEvent.obtain(downTime, downTime + 1000, MotionEvent.ACTION_UP, 1,
endX1, y, pressure, size, metaState, xPrecision, yPrecision, deviceId,
edgeFlags);
dispatchEvent(event);
}
});
}
private static long __downTime = SystemClock.uptimeMillis();
private void touch(int actionInt, String... args) {
float x = Float.parseFloat(args[0]);
float y = Float.parseFloat(args[1]);
touch(actionInt, x, y);
}
private void touch(int actionInt, float x, float y) {
if (actionInt == 0) {
ViewAutomator.__downTime = SystemClock.uptimeMillis();
}
long eventTime = System.currentTimeMillis();
final MotionEvent event = MotionEvent.obtain(ViewAutomator.__downTime, eventTime,
actionInt, x, y, 0);
AutomationManager.runOnUIThread(new Runnable() {
@Override
public void run() {
ViewAutomator.this.getView().dispatchTouchEvent(event);
}
});
}
@Override
public boolean installDefaultListeners() {
if (isHiddenByParent()) {
return true;
}
boolean didInstall = super.installDefaultListeners();
// // manage touch listeners
// if (!hasOnTouchListener(v)) {
// Log.log("ViewAutomator::installDefaultListeners - View "
// + v
// +
// " does not seem to have our onTouchListener installed -- installing...");
// v.setOnTouchListener(new TouchListener());
// didInstall = true;
// } else {
// logWarn("OnTouchListener", v);
// }
return didInstall;
}
//
// private static boolean hasOnTouchListener(View v) {
// // We need to get a handle to the actual parent View
// Class klass = v.getClass();
// while (!klass.equals(View.class)) {
// klass = klass.getSuperclass();
// }
//
// try {
// // Obviously this is not the best way to test if a listener
// // exists. But it's all we've got ...
// Field f = klass.getDeclaredField("mOnTouchListener");
// f.setAccessible(true);
// return f.get(v) != null;
// } catch (Exception e) {
// Log.log(e);
// }
// return false;
// }
private static String getDirectionFromFlick(Object[] args) {
String direction = "left";
try {
int startX = Integer.parseInt((String) args[0]);
int startY = Integer.parseInt((String) args[1]);
int endX = Integer.parseInt((String) args[2]);
int endY = Integer.parseInt((String) args[3]);
int deltaX = endX - startX;
int deltaY = endY - startY;
if (deltaX < 0 && deltaY < 0) {
// left or down
if (deltaX < deltaY) {
direction = "Left";
} else {
direction = "Up";
}
}
if (deltaX < 0 && deltaY >= 0) {
// left or up
if (deltaX < (0 - deltaY)) {
direction = "Left";
} else {
direction = "Down";
}
}
if (deltaX >= 0 && deltaY < 0) {
// right or down
if (deltaX > (0 - deltaY)) {
direction = "Right";
} else {
direction = "Up";
}
}
if (deltaX >= 0 && deltaY >= 00) {
// right or up
if (deltaX > deltaY) {
direction = "Right";
} else {
direction = "Down";
}
}
} catch (NumberFormatException e) {
Log.log("getDirectionFromFlick(): " + e.getMessage());
}
return direction;
}
/**
* @return true if hidden from recording by a parent (composite component).
*/
public boolean isHiddenByParent() {
// Log.log("Checking if " + view + " is hidden");
return _isHiddenByParent(getView(), getView().getParent());
}
private boolean _isHiddenByParent(View view, ViewParent parent) {
// Log.log("Checking if " + view + " is hidden by " + parent);
if (parent == null) {
// Log.log(view + " is not hidden");
return false;
}
IAutomator automator = AutomationManager.findAutomator(parent);
if (automator == null) {
return false;
}
if (automator.hides(view)) {
return true;
}
// Log.log("Found automator " + automator);
return (automator == null) ? false : _isHiddenByParent(view, parent.getParent());
}
@Override
public void record(String operation, String... args) {
if (!isAutomatable()) {
return;
}
if (operation.equals(AutomatorConstants.ACTION_SWIPE)) {
if (args.length < 1) {
throw new IllegalArgumentException("SWIPE action must have at least one argument");
}
String direction = null;
if (Character.isDigit(((String) args[0]).charAt(0))) {
direction = ViewAutomator.getDirectionFromFlick(args);
} else {
direction = (String) args[0];
}
String[] sargs = { direction };
super.record(operation, sargs);
} else {
super.record(operation, args);
}
}
/**
* Chains listeners such that the automtor's implementation of the supplied listener will be
* called before the listener associated with the associted view. The automator must implement
* the supplied interface. Assumes listener name is of the form OnXxxListener. Also assumes
* there is a setter on the associated view called setOnXxxListener, and a private field called
* mOnXxxListener.
*
* @param c
*/
// protected void chainListenerFor(Class<?> c) {
//
// String listenerClass = c.getName();
// Matcher m = onListener.matcher(listenerClass);
// m.find();
// String listenerName = "On" + m.group(1) + "Listener";
// String listenerField = "m" + listenerName;
// chainListenerFor(c, listenerName);
// }
/*
* (non-Javadoc)
*
* @see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent)
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
TouchListener.handleMotionEvent(v, event);
return false;
}
protected String getId() {
return getFieldMap().get(getView().getId());
}
public Map<Integer, String> getFieldMap() {
if (idMap != null) {
return idMap;
}
idMap = new HashMap<Integer, String>();
Class<?> r;
String rClass = this.getView().getContext().getApplicationContext().getPackageName()
+ ".R$id";
try {
r = Class.forName(rClass);
} catch (ClassNotFoundException e1) {
Log.log("Unable to load " + rClass + ": " + e1.getMessage());
return idMap;
}
for (Field f : r.getFields()) {
int val;
try {
val = f.getInt(null);
} catch (Exception e) {
throw new IllegalStateException("Unable to get value for " + f.getName() + ": "
+ e.getMessage());
}
idMap.put(val, f.getName());
}
return idMap;
}
/*
* (non-Javadoc)
*
* @see android.view.View.OnClickListener#onClick(android.view.View)
*/
// @Override
// public void onClick(View v) {
// AutomationManager.record(AutomatorConstants.ACTION_TAP, getView(), (String[]) null);
//
// }
/*
* (non-Javadoc)
*
* @see android.view.View.OnLongClickListener#onLongClick(android.view.View)
*/
@Override
public boolean onLongClick(View v) {
AutomationManager.record(AutomatorConstants.ACTION_LONG_PRESS, getView());
return false;
}
@Override
protected Rect getBoundingRectangle() {
View v = getView();
if (v == null) {
return super.getBoundingRectangle();
}
// Log.log("getBoundingRectangle() for view type=" + v.getClass().getSimpleName()
// + " left=" + v.getLeft()
// + " top=" + v.getTop()
// + " width=" + v.getWidth()
// + " height=" + v.getHeight()
// + " measuredWidth=" + v.getMeasuredWidth()
// + " measuredHeight=" + v.getMeasuredHeight()
// + " paddingTop=" + v.getPaddingTop()
// + " paddingLeft=" + v.getPaddingLeft()
// + " paddingRight=" + v.getPaddingRight()
// + " paddingBottom=" + v.getPaddingBottom()
// );
int w = v.getWidth();
int h = v.getHeight();
int x = v.getLeft();
int y = v.getTop();
ViewParent parent = v.getParent();
while (parent != null) {
if (parent instanceof View) {
View pv = (View) parent;
// Log.log("getBoundingRectangle() for parent view type=" +
// pv.getClass().getSimpleName()
// + " left=" + pv.getLeft()
// + " top=" + pv.getTop()
// + " width=" + pv.getWidth()
// + " height=" + pv.getHeight()
// + " measuredWidth=" + pv.getMeasuredWidth()
// + " measuredHeight=" + pv.getMeasuredHeight()
// + " paddingTop=" + pv.getPaddingTop()
// + " paddingLeft=" + pv.getPaddingLeft()
// + " paddingRight=" + pv.getPaddingRight()
// + " paddingBottom=" + pv.getPaddingBottom()
// );
x += pv.getLeft();
y += pv.getTop();
}
parent = parent.getParent();
}
// x+=v.getPaddingLeft();
// y+=v.getPaddingTop();
// w-=(v.getPaddingLeft()+v.getPaddingRight());
// h-=(v.getPaddingTop()+v.getPaddingBottom());
return new Rect(x, y, x + w, y + h);
}
}