org.robolectric.shadows.ShadowView Maven / Gradle / Ivy
package org.robolectric.shadows;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.util.AccessibilityUtil;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.TimeUtils;
import java.io.PrintStream;
import java.lang.reflect.Method;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.internal.Shadow.directlyOn;
import static org.robolectric.internal.Shadow.invokeConstructor;
/**
* Shadow for {@link android.view.View}.
*/
@Implements(View.class)
public class ShadowView {
@RealObject
protected View realView;
private View.OnClickListener onClickListener;
private View.OnLongClickListener onLongClickListener;
private View.OnFocusChangeListener onFocusChangeListener;
private View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener;
private boolean wasInvalidated;
private View.OnTouchListener onTouchListener;
protected AttributeSet attributeSet;
public Point scrollToCoordinates = new Point();
private boolean didRequestLayout;
private MotionEvent lastTouchEvent;
private float scaleX = 1.0f;
private float scaleY = 1.0f;
private int hapticFeedbackPerformed = -1;
private boolean onLayoutWasCalled;
private View.OnCreateContextMenuListener onCreateContextMenuListener;
private Rect globalVisibleRect;
/**
* Calls {@code performClick()} on a {@code View} after ensuring that it and its ancestors are visible and that it
* is enabled.
*
* @param view the view to click on
* @return true if {@code View.OnClickListener}s were found and fired, false otherwise.
* @throws RuntimeException if the preconditions are not met.
*/
public static boolean clickOn(View view) {
return shadowOf(view).checkedPerformClick();
}
/**
* Returns a textual representation of the appearance of the object.
*
* @param view the view to visualize
* @return Textual representation of the appearance of the object.
*/
public static String visualize(View view) {
Canvas canvas = new Canvas();
view.draw(canvas);
return shadowOf(canvas).getDescription();
}
/**
* Emits an xml-like representation of the view to System.out.
*
* @param view the view to dump
*/
@SuppressWarnings("UnusedDeclaration")
public static void dump(View view) {
shadowOf(view).dump();
}
/**
* Returns the text contained within this view.
*
* @param view the view to scan for text
* @return Text contained within this view.
*/
@SuppressWarnings("UnusedDeclaration")
public static String innerText(View view) {
return shadowOf(view).innerText();
}
public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
if (context == null) throw new NullPointerException("no context");
this.attributeSet = attributeSet;
invokeConstructor(View.class, realView,
ClassParameter.from(Context.class, context),
ClassParameter.from(AttributeSet.class, attributeSet),
ClassParameter.from(int.class, defStyle));
}
/**
* Build drawable, either LayerDrawable or BitmapDrawable.
*
* @param resourceId Resource id
* @return Drawable
*/
protected Drawable buildDrawable(int resourceId) {
return realView.getResources().getDrawable(resourceId);
}
protected String getQualifiers() {
return shadowOf(realView.getResources().getConfiguration()).getQualifiers();
}
/**
* Non-Android accessor.
*
* @return the resource ID of this view's background
* @deprecated Use FEST assertions instead.
*/
public int getBackgroundResourceId() {
Drawable drawable = realView.getBackground();
return drawable instanceof BitmapDrawable
? shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromResId()
: -1;
}
/**
* Non-Android accessor.
*
* @return the color of this view's background, or 0 if it's not a solid color
* @deprecated Use FEST assertions instead.
*/
public int getBackgroundColor() {
Drawable drawable = realView.getBackground();
return drawable instanceof ColorDrawable ? ((ColorDrawable) drawable).getColor() : 0;
}
@HiddenApi
@Implementation
public void computeOpaqueFlags() {
}
@Implementation
public void setOnFocusChangeListener(View.OnFocusChangeListener l) {
onFocusChangeListener = l;
directly().setOnFocusChangeListener(l);
}
@Implementation
public void setOnClickListener(View.OnClickListener onClickListener) {
this.onClickListener = onClickListener;
directly().setOnClickListener(onClickListener);
}
@Implementation
public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
this.onLongClickListener = onLongClickListener;
directly().setOnLongClickListener(onLongClickListener);
}
@Implementation
public void setOnSystemUiVisibilityChangeListener(View.OnSystemUiVisibilityChangeListener onSystemUiVisibilityChangeListener) {
this.onSystemUiVisibilityChangeListener = onSystemUiVisibilityChangeListener;
directly().setOnSystemUiVisibilityChangeListener(onSystemUiVisibilityChangeListener);
}
@Implementation
public void setOnCreateContextMenuListener(View.OnCreateContextMenuListener onCreateContextMenuListener) {
this.onCreateContextMenuListener = onCreateContextMenuListener;
directly().setOnCreateContextMenuListener(onCreateContextMenuListener);
}
@Implementation
public void draw(android.graphics.Canvas canvas) {
Drawable background = realView.getBackground();
if (background != null) {
shadowOf(canvas).appendDescription("background:");
background.draw(canvas);
}
}
@Implementation
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
onLayoutWasCalled = true;
directlyOn(realView, View.class, "onLayout",
ClassParameter.from(boolean.class, changed),
ClassParameter.from(int.class, left),
ClassParameter.from(int.class, top),
ClassParameter.from(int.class, right),
ClassParameter.from(int.class, bottom));
}
public boolean onLayoutWasCalled() {
return onLayoutWasCalled;
}
@Implementation
public void requestLayout() {
didRequestLayout = true;
directly().requestLayout();
}
public boolean didRequestLayout() {
return didRequestLayout;
}
public void setDidRequestLayout(boolean didRequestLayout) {
this.didRequestLayout = didRequestLayout;
}
public void setViewFocus(boolean hasFocus) {
if (onFocusChangeListener != null) {
onFocusChangeListener.onFocusChange(realView, hasFocus);
}
}
@Implementation
public void invalidate() {
wasInvalidated = true;
directly().invalidate();
}
@Implementation
public boolean onTouchEvent(MotionEvent event) {
lastTouchEvent = event;
return directly().onTouchEvent(event);
}
@Implementation
public void setOnTouchListener(View.OnTouchListener onTouchListener) {
this.onTouchListener = onTouchListener;
directly().setOnTouchListener(onTouchListener);
}
public MotionEvent getLastTouchEvent() {
return lastTouchEvent;
}
/**
* Returns a string representation of this {@code View}. Unless overridden, it will be an empty string.
*
*
* Robolectric extension.
* @return String representation of this view.
*/
public String innerText() {
return "";
}
/**
* Dumps the status of this {@code View} to {@code System.out}
*/
public void dump() {
dump(System.out, 0);
}
/**
* Dumps the status of this {@code View} to {@code System.out} at the given indentation level
* @param out Output stream.
* @param indent Indentation level.
*/
public void dump(PrintStream out, int indent) {
dumpFirstPart(out, indent);
out.println("/>");
}
protected void dumpFirstPart(PrintStream out, int indent) {
dumpIndent(out, indent);
out.print("<" + realView.getClass().getSimpleName());
dumpAttributes(out);
}
protected void dumpAttributes(PrintStream out) {
if (realView.getId() > 0) {
dumpAttribute(out, "id", realView.getContext().getResources().getResourceName(realView.getId()));
}
switch (realView.getVisibility()) {
case View.VISIBLE:
break;
case View.INVISIBLE:
dumpAttribute(out, "visibility", "INVISIBLE");
break;
case View.GONE:
dumpAttribute(out, "visibility", "GONE");
break;
}
}
protected void dumpAttribute(PrintStream out, String name, String value) {
out.print(" " + name + "=\"" + (value == null ? null : TextUtils.htmlEncode(value)) + "\"");
}
protected void dumpIndent(PrintStream out, int indent) {
for (int i = 0; i < indent; i++) out.print(" ");
}
/**
* Non-Android accessor.
*
* @return whether or not {@link #invalidate()} has been called
*/
public boolean wasInvalidated() {
return wasInvalidated;
}
/**
* Clears the wasInvalidated flag
*/
public void clearWasInvalidated() {
wasInvalidated = false;
}
/**
* Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app.
*
* @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible.
* @return Return value of the underlying click operation.
*/
public boolean checkedPerformClick() {
if (!realView.isShown()) {
throw new RuntimeException("View is not visible and cannot be clicked");
}
if (!realView.isEnabled()) {
throw new RuntimeException("View is not enabled and cannot be clicked");
}
AccessibilityUtil.checkViewIfCheckingEnabled(realView);
return realView.performClick();
}
/**
* Non-android accessor.
*
* @return Touch listener, if set.
*/
public View.OnTouchListener getOnTouchListener() {
return onTouchListener;
}
/**
* Non-android accessor.
*
* @return Returns click listener, if set.
*/
public View.OnClickListener getOnClickListener() {
return onClickListener;
}
/**
* Non-android accessor.
*
* @return Returns long click listener, if set.
*/
public View.OnLongClickListener getOnLongClickListener() {
return onLongClickListener;
}
/**
* Non-android accessor.
*
* @return Returns system ui visibility change listener.
*/
public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
return onSystemUiVisibilityChangeListener;
}
/**
* Non-android accessor.
*
* @return Returns create ContextMenu listener, if set.
*/
public View.OnCreateContextMenuListener getOnCreateContextMenuListener() {
return onCreateContextMenuListener;
}
@Implementation
public Bitmap getDrawingCache() {
return ReflectionHelpers.callConstructor(Bitmap.class);
}
@Implementation
public void post(Runnable action) {
ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
}
@Implementation
public void postDelayed(Runnable action, long delayMills) {
ShadowApplication.getInstance().getForegroundThreadScheduler().postDelayed(action, delayMills);
}
@Implementation
public void postInvalidateDelayed(long delayMilliseconds) {
ShadowApplication.getInstance().getForegroundThreadScheduler().postDelayed(new Runnable() {
@Override
public void run() {
realView.invalidate();
}
}, delayMilliseconds);
}
@Implementation
public void removeCallbacks(Runnable callback) {
shadowOf(Looper.getMainLooper()).getScheduler().remove(callback);
}
@Implementation
public void scrollTo(int x, int y) {
try {
Method method = View.class.getDeclaredMethod("onScrollChanged", new Class[]{int.class, int.class, int.class, int.class});
method.setAccessible(true);
method.invoke(realView, x, y, scrollToCoordinates.x, scrollToCoordinates.y);
} catch (Exception e) {
throw new RuntimeException(e);
}
scrollToCoordinates = new Point(x, y);
}
@Implementation
public int getScrollX() {
return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
}
@Implementation
public int getScrollY() {
return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
}
@Implementation
public void setScrollX(int scrollX) {
scrollTo(scrollX, scrollToCoordinates.y);
}
@Implementation
public void setScrollY(int scrollY) {
scrollTo(scrollToCoordinates.x, scrollY);
}
@Implementation
public void setAnimation(final Animation animation) {
directly().setAnimation(animation);
if (animation != null) {
new AnimationRunner(animation);
}
}
private AnimationRunner animationRunner;
private class AnimationRunner implements Runnable {
private final Animation animation;
private long startTime, startOffset, elapsedTime;
AnimationRunner(Animation animation) {
this.animation = animation;
start();
}
private void start() {
startTime = animation.getStartTime();
startOffset = animation.getStartOffset();
Choreographer choreographer = ShadowChoreographer.getInstance();
if (animationRunner != null) {
choreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
}
animationRunner = this;
int startDelay;
if (startTime == Animation.START_ON_FIRST_FRAME) {
startDelay = (int) startOffset;
} else {
startDelay = (int) ((startTime + startOffset) - SystemClock.uptimeMillis());
}
choreographer.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
}
@Override
public void run() {
// Abort if start time has been messed with, as this simulation is only designed to handle
// standard situations.
if ((animation.getStartTime() == startTime && animation.getStartOffset() == startOffset) &&
animation.getTransformation(startTime == Animation.START_ON_FIRST_FRAME ?
SystemClock.uptimeMillis() : (startTime + startOffset + elapsedTime), new Transformation()) &&
// We can't handle infinitely repeating animations in the current scheduling model,
// so abort after one iteration.
!(animation.getRepeatCount() == Animation.INFINITE && elapsedTime >= animation.getDuration())) {
// Update startTime if it had a value of Animation.START_ON_FIRST_FRAME
startTime = animation.getStartTime();
elapsedTime += ShadowChoreographer.getFrameInterval() / TimeUtils.NANOS_PER_MS;
ShadowChoreographer.getInstance().postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
} else {
animationRunner = null;
}
}
}
@Implementation
public boolean isAttachedToWindow() {
return ReflectionHelpers.getField(realView, "mAttachInfo") != null;
}
public void callOnAttachedToWindow() {
invokeReflectively("onAttachedToWindow");
}
public void callOnDetachedFromWindow() {
invokeReflectively("onDetachedFromWindow");
}
private void invokeReflectively(String methodName) {
ReflectionHelpers.callInstanceMethod(realView, methodName);
}
@Implementation
public boolean performHapticFeedback(int hapticFeedbackType) {
hapticFeedbackPerformed = hapticFeedbackType;
return true;
}
@Implementation
public boolean getGlobalVisibleRect(Rect rect, Point globalOffset) {
if (globalVisibleRect == null) {
/*
* The global visible rect is not initialized. The value is not reliable as Robolectric does
* not perform layouts in most cases. Use a substitute concept of visibility if no rect
* had been set explicitly.
*/
rect.setEmpty();
return realView.isShown();
}
if (!globalVisibleRect.isEmpty()) {
rect.set(globalVisibleRect);
if (globalOffset != null) {
rect.offset(-globalOffset.x, -globalOffset.y);
}
return true;
}
rect.setEmpty();
return false;
}
public void setGlobalVisibleRect(Rect rect) {
globalVisibleRect = new Rect();
globalVisibleRect.set(rect);
}
public int lastHapticFeedbackPerformed() {
return hapticFeedbackPerformed;
}
public void setMyParent(ViewParent viewParent) {
directlyOn(realView, View.class, "assignParent", ClassParameter.from(ViewParent.class, viewParent));
}
private View directly() {
return directlyOn(realView, View.class);
}
}