| package com.xtremelabs.robolectric.shadows; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import com.xtremelabs.robolectric.res.ViewLoader; |
| import com.xtremelabs.robolectric.util.Implementation; |
| import com.xtremelabs.robolectric.util.Implements; |
| import com.xtremelabs.robolectric.util.RealObject; |
| |
| import java.io.PrintStream; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import static com.xtremelabs.robolectric.Robolectric.shadowOf; |
| import static java.lang.Boolean.TRUE; |
| |
| /** |
| * Shadow implementation of {@code View} that simulates the behavior of this class. Supports listeners, focusability |
| * (but not focus order), resource loading, visibility, tags, and tracks the size and shape of the view. |
| */ |
| @SuppressWarnings({"UnusedDeclaration"}) |
| @Implements(View.class) |
| public class ShadowView { |
| @RealObject protected View realView; |
| |
| private int id; |
| ShadowView parent; |
| private Context context; |
| private boolean selected; |
| private View.OnClickListener onClickListener; |
| private Object tag; |
| private boolean enabled = true; |
| private int visibility = View.VISIBLE; |
| int left; |
| int top; |
| int right; |
| int bottom; |
| private int paddingLeft; |
| private int paddingTop; |
| private int paddingRight; |
| private int paddingBottom; |
| private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0); |
| private Map<Integer, Object> tags = new HashMap<Integer, Object>(); |
| private boolean clickable; |
| protected boolean focusable; |
| boolean focusableInTouchMode; |
| private int backgroundResourceId = -1; |
| protected View.OnKeyListener onKeyListener; |
| private boolean isFocused; |
| private View.OnFocusChangeListener onFocusChangeListener; |
| private boolean wasInvalidated; |
| private View.OnTouchListener onTouchListener; |
| protected AttributeSet attributeSet; |
| |
| public void __constructor__(Context context) { |
| this.context = context; |
| } |
| |
| public void __constructor__(Context context, AttributeSet attributeSet) { |
| this.attributeSet = attributeSet; |
| __constructor__(context); |
| applyIdAttribute(); |
| applyVisibilityAttribute(); |
| applyEnabledAttribute(); |
| } |
| |
| @Implementation |
| public void setId(int id) { |
| this.id = id; |
| } |
| |
| @Implementation |
| public void setClickable(boolean clickable) { |
| this.clickable = clickable; |
| } |
| |
| /** |
| * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior. |
| * |
| * @param focusable the new status of the {@code View}'s focusability |
| */ |
| @Implementation |
| public void setFocusable(boolean focusable) { |
| this.focusable = focusable; |
| if (!focusable) { |
| setFocusableInTouchMode(false); |
| } |
| } |
| |
| @Implementation |
| public final boolean isFocusableInTouchMode() { |
| return focusableInTouchMode; |
| } |
| |
| /** |
| * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior. |
| * |
| * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability |
| */ |
| @Implementation |
| public void setFocusableInTouchMode(boolean focusableInTouchMode) { |
| this.focusableInTouchMode = focusableInTouchMode; |
| if (focusableInTouchMode) { |
| setFocusable(true); |
| } |
| } |
| |
| @Implementation |
| public boolean isFocusable() { |
| return focusable; |
| } |
| |
| @Implementation |
| public int getId() { |
| return id; |
| } |
| |
| /** |
| * Simulates the inflating of the requested resource. |
| * |
| * @param context the context from which to obtain a layout inflater |
| * @param resource the ID of the resource to inflate |
| * @param root the {@code ViewGroup} to add the inflated {@code View} to |
| * @return the inflated View |
| */ |
| @Implementation |
| public static View inflate(Context context, int resource, ViewGroup root) { |
| return ShadowLayoutInflater.from(context).inflate(resource, root); |
| } |
| |
| /** |
| * Finds this {@code View} if it's ID is passed in, returns {@code null} otherwise |
| * |
| * @param id the id of the {@code View} to find |
| * @return the {@code View}, if found, {@code null} otherwise |
| */ |
| @Implementation |
| public View findViewById(int id) { |
| if (id == this.id) { |
| return realView; |
| } |
| |
| return null; |
| } |
| |
| @Implementation |
| public View getRootView() { |
| ShadowView root = this; |
| while (root.parent != null) { |
| root = root.parent; |
| } |
| return root.realView; |
| } |
| |
| @Implementation |
| public ViewGroup.LayoutParams getLayoutParams() { |
| return layoutParams; |
| } |
| |
| @Implementation |
| public void setLayoutParams(ViewGroup.LayoutParams params) { |
| layoutParams = params; |
| } |
| |
| @Implementation |
| public final ViewParent getParent() { |
| return parent == null ? null : (ViewParent) parent.realView; |
| } |
| |
| @Implementation |
| public final Context getContext() { |
| return context; |
| } |
| |
| @Implementation |
| public Resources getResources() { |
| return context.getResources(); |
| } |
| |
| @Implementation |
| public void setBackgroundResource(int backgroundResourceId) { |
| this.backgroundResourceId = backgroundResourceId; |
| } |
| |
| @Implementation |
| public int getVisibility() { |
| return visibility; |
| } |
| |
| @Implementation |
| public void setVisibility(int visibility) { |
| this.visibility = visibility; |
| } |
| |
| @Implementation |
| public void setSelected(boolean selected) { |
| this.selected = selected; |
| } |
| |
| @Implementation |
| public boolean isSelected() { |
| return this.selected; |
| } |
| |
| @Implementation |
| public boolean isEnabled() { |
| return this.enabled; |
| } |
| |
| @Implementation |
| public void setEnabled(boolean enabled) { |
| this.enabled = enabled; |
| } |
| |
| @Implementation |
| public void setOnClickListener(View.OnClickListener onClickListener) { |
| this.onClickListener = onClickListener; |
| } |
| |
| @Implementation |
| public boolean performClick() { |
| if (onClickListener != null) { |
| onClickListener.onClick(realView); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| @Implementation |
| public void setOnKeyListener(View.OnKeyListener onKeyListener) { |
| this.onKeyListener = onKeyListener; |
| } |
| |
| @Implementation |
| public Object getTag() { |
| return this.tag; |
| } |
| |
| @Implementation |
| public void setTag(Object tag) { |
| this.tag = tag; |
| } |
| |
| @Implementation |
| public final int getHeight() { |
| return bottom - top; |
| } |
| |
| @Implementation |
| public final int getWidth() { |
| return right - left; |
| } |
| |
| @Implementation |
| public final int getMeasuredWidth() { |
| return getWidth(); |
| } |
| |
| @Implementation |
| public final void layout(int l, int t, int r, int b) { |
| left = l; |
| top = t; |
| right = r; |
| bottom = b; |
| |
| // todo: realView.onLayout(); |
| } |
| |
| @Implementation |
| public void setPadding(int left, int top, int right, int bottom) { |
| paddingLeft = left; |
| paddingTop = top; |
| paddingRight = right; |
| paddingBottom = bottom; |
| } |
| |
| @Implementation |
| public int getPaddingTop() { |
| return paddingTop; |
| } |
| |
| @Implementation |
| public int getPaddingLeft() { |
| return paddingLeft; |
| } |
| |
| @Implementation |
| public int getPaddingRight() { |
| return paddingRight; |
| } |
| |
| @Implementation |
| public int getPaddingBottom() { |
| return paddingBottom; |
| } |
| |
| @Implementation |
| public Object getTag(int key) { |
| return tags.get(key); |
| } |
| |
| @Implementation |
| public void setTag(int key, Object value) { |
| tags.put(key, value); |
| } |
| |
| @Implementation |
| public final boolean requestFocus() { |
| return requestFocus(View.FOCUS_DOWN); |
| } |
| |
| @Implementation |
| public final boolean requestFocus(int direction) { |
| setViewFocus(true); |
| return true; |
| } |
| |
| public void setViewFocus(boolean hasFocus) { |
| this.isFocused = hasFocus; |
| if (onFocusChangeListener != null) { |
| onFocusChangeListener.onFocusChange(realView, hasFocus); |
| } |
| } |
| |
| @Implementation |
| public boolean isFocused() { |
| return isFocused; |
| } |
| |
| @Implementation |
| public boolean hasFocus() { |
| return isFocused; |
| } |
| |
| @Implementation |
| public void clearFocus() { |
| setViewFocus(false); |
| } |
| |
| @Implementation |
| public void setOnFocusChangeListener(View.OnFocusChangeListener listener) { |
| onFocusChangeListener = listener; |
| } |
| |
| @Implementation |
| public void invalidate() { |
| wasInvalidated = true; |
| } |
| |
| @Implementation |
| public void setOnTouchListener(View.OnTouchListener onTouchListener) { |
| this.onTouchListener = onTouchListener; |
| } |
| |
| @Implementation |
| public boolean dispatchTouchEvent(MotionEvent event) { |
| if (onTouchListener != null) { |
| return onTouchListener.onTouch(realView, event); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string. |
| * |
| * Robolectric extension. |
| */ |
| 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 |
| */ |
| 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()); |
| if (id > 0) { |
| out.print(" id=\"" + shadowOf(context).getResourceLoader().getNameForId(id) + "\""); |
| } |
| } |
| |
| protected void dumpIndent(PrintStream out, int indent) { |
| for (int i = 0; i < indent; i++) out.print(" "); |
| } |
| |
| /** |
| * @return left side of the view |
| */ |
| @Implementation |
| public int getLeft() { |
| return left; |
| } |
| |
| /** |
| * @return top coordinate of the view |
| */ |
| @Implementation |
| public int getTop() { |
| return top; |
| } |
| |
| /** |
| * @return right side of the view |
| */ |
| @Implementation |
| public int getRight() { |
| return right; |
| } |
| |
| /** |
| * @return bottom coordinate of the view |
| */ |
| @Implementation |
| public int getBottom() { |
| return bottom; |
| } |
| |
| /** |
| * @return whether the view is clickable |
| */ |
| @Implementation |
| public boolean isClickable() { |
| return clickable; |
| } |
| |
| /** |
| * Non-Android accessor. |
| * |
| * @return the resource ID of this views background |
| */ |
| public int getBackgroundResourceId() { |
| return backgroundResourceId; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setLeft(int left) { |
| this.left = left; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setTop(int top) { |
| this.top = top; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setRight(int right) { |
| this.right = right; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setBottom(int bottom) { |
| this.bottom = bottom; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setPaddingLeft(int paddingLeft) { |
| this.paddingLeft = paddingLeft; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setPaddingTop(int paddingTop) { |
| this.paddingTop = paddingTop; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setPaddingRight(int paddingRight) { |
| this.paddingRight = paddingRight; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setPaddingBottom(int paddingBottom) { |
| this.paddingBottom = paddingBottom; |
| } |
| |
| /** |
| * Non-Android accessor. |
| */ |
| public void setFocused(boolean focused) { |
| isFocused = focused; |
| } |
| |
| /** |
| * Non-Android accessor. |
| * |
| * @return true if this object and all of its ancestors are {@code View.VISIBLE}, returns false if this or |
| * any ancestor is not {@code View.VISIBLE} |
| */ |
| public boolean derivedIsVisible() { |
| View parent = realView; |
| while (parent != null) { |
| if (parent.getVisibility() != View.VISIBLE) { |
| return false; |
| } |
| parent = (View) parent.getParent(); |
| } |
| return true; |
| } |
| |
| /** |
| * 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. |
| */ |
| public boolean checkedPerformClick() { |
| if (!derivedIsVisible()) { |
| 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"); |
| } |
| |
| return realView.performClick(); |
| } |
| |
| public void applyViewNodeAttributes(ViewLoader.ViewNode viewNode) { |
| applyFocus(viewNode); |
| } |
| |
| private void applyIdAttribute() { |
| Integer id = attributeSet.getAttributeResourceValue("android", "id", 0); |
| if (getId() == 0) { |
| setId(id); |
| } |
| } |
| |
| private void applyVisibilityAttribute() { |
| String visibility = attributeSet.getAttributeValue("android", "visibility"); |
| if (visibility != null) { |
| if (visibility.equals("gone")) { |
| setVisibility(View.GONE); |
| } else if (visibility.equals("invisible")) { |
| setVisibility(View.INVISIBLE); |
| } |
| } |
| } |
| |
| private void applyEnabledAttribute() { |
| setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true)); |
| } |
| |
| private void applyFocus(ViewLoader.ViewNode viewNode) { |
| checkFocusOverride(viewNode); |
| |
| if (!anyParentHasFocus(realView)) { |
| Boolean focusRequested = viewNode.getAttributeAsBool("android:focus"); |
| if (TRUE.equals(focusRequested) || realView.isFocusableInTouchMode()) { |
| realView.requestFocus(); |
| } |
| } |
| } |
| |
| private void checkFocusOverride(ViewLoader.ViewNode viewNode) { |
| if (viewNode.hasRequestFocusOverride()) { |
| View root = realView; |
| View parent = (View) root.getParent(); |
| while (parent != null) { |
| root = parent; |
| parent = (View) root.getParent(); |
| } |
| root.clearFocus(); |
| } |
| } |
| |
| private boolean anyParentHasFocus(View view) { |
| while (view != null) { |
| if (view.hasFocus()) return true; |
| view = (View) view.getParent(); |
| } |
| return false; |
| } |
| } |