All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowView Maven / Gradle / Ivy

The newest version!
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.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.IWindowFocusObserver;
import android.view.IWindowId;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.WindowId;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.*;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.android.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 android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.getField;
import static org.robolectric.util.ReflectionHelpers.setField;

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

  /**
   * This will be removed in Robolectric 3.4 use {@link RuntimeEnvironment#getQualifiers()} instead, however
   * the correct way to configure qualifiers is using {@link Config#qualifiers()} so a constant can be used if this
   * is important to your tests. However, qualifier strings are typically just used to initialize the test environment
   * in a certain configuration. {@link android.content.res.Configuration} changes should be managed through
   * {@link org.robolectric.android.controller.ActivityController#configurationChange(android.content.res.Configuration)}
   * @deprecated
   */
  @Deprecated
  protected String getQualifiers() {
    return RuntimeEnvironment.getQualifiers();
  }

  /**
   * @return the resource ID of this view's background
   * @deprecated Use FEST assertions instead.
   */
  @Deprecated
  public int getBackgroundResourceId() {
    Drawable drawable = realView.getBackground();
    return drawable instanceof BitmapDrawable
        ? shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromResId()
        : -1;
  }

  /**
   * @return the color of this view's background, or 0 if it's not a solid color
   * @deprecated Use FEST assertions instead.
   */
  @Deprecated
  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(" ");
  }

  /**
   * @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();
  }

  /**
   * @return Touch listener, if set.
   */
  public View.OnTouchListener getOnTouchListener() {
    return onTouchListener;
  }

  /**
   * @return Returns click listener, if set.
   */
  public View.OnClickListener getOnClickListener() {
    return onClickListener;
  }

  /**
   * @return Returns long click listener, if set.
   */
  public View.OnLongClickListener getOnLongClickListener() {
    return onLongClickListener;
  }

  /**
   * @return Returns system ui visibility change listener.
   */
  public View.OnSystemUiVisibilityChangeListener getOnSystemUiVisibilityChangeListener() {
    return onSystemUiVisibilityChangeListener;
  }

  /**
   * @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 getAttachInfo() != null;
  }

  private Object getAttachInfo() {
    return getField(realView, "mAttachInfo");
  }

  public void callOnAttachedToWindow() {
    invokeReflectively("onAttachedToWindow");
  }

  public void callOnDetachedFromWindow() {
    invokeReflectively("onDetachedFromWindow");
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  public Object getWindowId() {
    return WindowIdHelper.getWindowId(this);
  }

  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) {
      return directly().getGlobalVisibleRect(rect, globalOffset);
    }

    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) {
    if (rect != null) {
      globalVisibleRect = new Rect();
      globalVisibleRect.set(rect);
    } else {
      globalVisibleRect = null;
    }
  }

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

  public static class WindowIdHelper {
    public static Object getWindowId(ShadowView shadowView) {
      if (shadowView.isAttachedToWindow()) {
        Object attachInfo = shadowView.getAttachInfo();
        if (getField(attachInfo, "mWindowId") == null) {
          IWindowId iWindowId = new MyIWindowIdStub();
          setField(attachInfo, "mWindowId", new WindowId(iWindowId));
          setField(attachInfo, "mIWindowId", iWindowId);
        }
      }

      return shadowView.directly().getWindowId();
    }

    private static class MyIWindowIdStub extends IWindowId.Stub {
      @Override
      public void registerFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
      }

      @Override
      public void unregisterFocusObserver(IWindowFocusObserver iWindowFocusObserver) throws RemoteException {
      }

      @Override
      public boolean isFocused() throws RemoteException {
        return true;
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy