package mobi.sherif.widgywidgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.Layout; import android.text.StaticLayout; import android.text.TextUtils; import android.util.AttributeSet; import android.widget.EditText; import android.widget.PopupWindow; import android.widget.TextView; public class EditTextWithCustomError extends EditText { private boolean hasFallen = false; private ErrorPopup mPopup; private Drawables mDrawables; private CharSequence mError; /** * still unused */ @SuppressWarnings("unused") private boolean mErrorWasChanged; /** * This flag is set if the TextView tries to display an error before it * is attached to the window (so its position is still unknown). * It causes the error to be shown later, when onAttachedToWindow() * is called. */ private boolean mShowErrorAfterAttach; Drawable mErrorIcon; Drawable mErrorBackgroundAbove; Drawable mErrorBackground; int mErrorTextColor; public EditTextWithCustomError(Context context) { super(context); init(context, null); } public EditTextWithCustomError(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public EditTextWithCustomError(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EditTextWithCustomError); if (a.hasValue(R.styleable.EditTextWithCustomError_ErrorDefaultIcon)) { mErrorIcon = a.getDrawable(R.styleable.EditTextWithCustomError_ErrorDefaultIcon); } if(mErrorIcon == null) { } if (a.hasValue(R.styleable.EditTextWithCustomError_ErrorDefaultBackground)) { mErrorBackground = a.getDrawable(R.styleable.EditTextWithCustomError_ErrorDefaultBackground); } if(mErrorBackground == null) { } if (a.hasValue(R.styleable.EditTextWithCustomError_ErrorDefaultBackgroundAbove)) { mErrorBackgroundAbove = a.getDrawable(R.styleable.EditTextWithCustomError_ErrorDefaultBackgroundAbove); } if(mErrorBackgroundAbove == null) { } if (a.hasValue(R.styleable.EditTextWithCustomError_ErrorTextColor)) { mErrorTextColor = a.getColor(R.styleable.EditTextWithCustomError_ErrorTextColor, Color.rgb(50, 50, 50)); } } /** * Sets the right-hand compound drawable of the TextView to the "error" * icon and sets an error message that will be displayed in a popup when * the TextView has focus. The icon and error message will be reset to * null when any key events cause changes to the TextView's text. If the * <code>error</code> is <code>null</code>, the error message and icon * will be cleared. */ @Override public void setError(CharSequence error) { if (error == null) { setError(null, null); } else { Drawable dr = mErrorIcon; if(dr!=null) dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); setError(error, dr); } } private void setTheError(CharSequence error) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { error = TextUtils.stringOrSpannedString(error); mError = error; if(true) return; // Class<?> c = TextView.class; // Field f = c.getDeclaredField("mError"); // f.setAccessible(true); // f.set(this, error); } private void setTheErrorWasChanged(boolean value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { mErrorWasChanged = value; if(true) return; // Class<?> c = TextView.class; // Field f = c.getDeclaredField("mErrorWasChanged"); // f.setAccessible(true); // f.setBoolean(this, value); } /** * Sets the right-hand compound drawable of the TextView to the specified * icon and sets an error message that will be displayed in a popup when * the TextView has focus. The icon and error message will be reset to * null when any key events cause changes to the TextView's text. The * drawable must already have had {@link Drawable#setBounds} set on it. * If the <code>error</code> is <code>null</code>, the error message will * be cleared (and you should provide a <code>null</code> icon as well). */ @Override public void setError(CharSequence error, Drawable icon) { try { setTheError(error); setTheErrorWasChanged(true); } catch(Exception ex) { ex.printStackTrace(); hasFallen = true; //let us fallback super.setError(error, icon); return; } final Drawables dr = mDrawables; if (dr != null) { setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, dr.mDrawableBottom); } else { setCompoundDrawables(null, null, icon, null); } if (error == null) { if (mPopup != null) { if (mPopup.isShowing()) { mPopup.dismiss(); } mPopup = null; } } else { if (isFocused()) { showError(); } } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!hasFallen && mShowErrorAfterAttach) { showError(); mShowErrorAfterAttach = false; } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (!hasFallen && mError != null) { hideError(); } } private void hideError() { if (mPopup != null) { if (mPopup.isShowing()) { mPopup.dismiss(); } } mShowErrorAfterAttach = false; } private void showError() { if (getWindowToken() == null) { mShowErrorAfterAttach = true; return; } if (mPopup == null) { // LayoutInflater inflater = LayoutInflater.from(getContext()); final TextView err = new TextView(getContext()); err.setTextColor(mErrorTextColor); final float scale = getResources().getDisplayMetrics().density; mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f)); mPopup.setFocusable(false); // The user is entering text, so the input method is needed. We // don't want the popup to be displayed on top of it. mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); } TextView tv = (TextView) mPopup.getContentView(); chooseSize(mPopup, mError, tv); tv.setText(mError); mPopup.showAsDropDown(this, getErrorX(), getErrorY()); mPopup.fixDirection(mPopup.isAboveAnchor()); } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); if (focused) { if (mError != null) { showError(); } } else { if (mError != null) { hideError(); } } } /** * Returns the Y offset to make the pointy top of the error point * at the middle of the error icon. */ private int getErrorX() { /* * The "25" is the distance between the point and the right edge * of the background */ final float scale = getResources().getDisplayMetrics().density; final Drawables dr = mDrawables; return getWidth() - mPopup.getWidth() - getPaddingRight() - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); } /** * Returns the Y offset to make the pointy top of the error point * at the bottom of the error icon. */ private int getErrorY() { /* * Compound, not extended, because the icon is not clipped * if the text height is smaller. */ int vspace = getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop(); final Drawables dr = mDrawables; int icontop = getCompoundPaddingTop() + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; /* * The "2" is the distance between the point and the top edge * of the background. */ return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() - 2; } private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { int wid = tv.getPaddingLeft() + tv.getPaddingRight(); int ht = tv.getPaddingTop() + tv.getPaddingBottom(); /* * Figure out how big the text would be if we laid it out to the * full width of this view minus the border. */ int cap = getWidth() - wid; if (cap < 0) { cap = 200; // We must not be measured yet -- setFrame() will fix it. } Layout l = new StaticLayout(text, tv.getPaint(), cap, Layout.Alignment.ALIGN_NORMAL, 1, 0, true); float max = 0; for (int i = 0; i < l.getLineCount(); i++) { max = Math.max(max, l.getLineWidth(i)); } /* * Now set the popup size to be big enough for the text plus the border. */ pop.setWidth(wid + (int) Math.ceil(max)); pop.setHeight(ht + l.getHeight()); } class Drawables { final Rect mCompoundRect = new Rect(); Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; int mDrawablePadding; } private class ErrorPopup extends PopupWindow { private boolean mAbove = false; private TextView mView; ErrorPopup(TextView v, int width, int height) { super(v, width, height); mView = v; } @SuppressWarnings("deprecation") void fixDirection(boolean above) { mAbove = above; if (above) { if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) mView.setBackground(mErrorBackgroundAbove); else mView.setBackgroundDrawable(mErrorBackgroundAbove); } else { if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) mView.setBackground(mErrorBackground); else mView.setBackgroundDrawable(mErrorBackground); } } @Override public void update(int x, int y, int w, int h, boolean force) { super.update(x, y, w, h, force); boolean above = isAboveAnchor(); if (above != mAbove) { fixDirection(above); } } } @Override protected boolean setFrame(int l, int t, int r, int b) { boolean result = super.setFrame(l, t, r, b); if (!hasFallen && mPopup != null) { TextView tv = (TextView) mPopup.getContentView(); chooseSize(mPopup, mError, tv); mPopup.update(this, getErrorX(), getErrorY(), mPopup.getWidth(), mPopup.getHeight()); } return result; } public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { super.setCompoundDrawables(left, top, right, bottom); if(hasFallen) return; Drawables dr = mDrawables; final boolean drawables = left != null || top != null || right != null || bottom != null; if (!drawables) { // Clearing drawables... can we free the data structure? if (dr != null) { if (dr.mDrawablePadding == 0) { mDrawables = null; } else { // We need to retain the last set padding, so just clear // out all of the fields in the existing structure. if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); dr.mDrawableLeft = null; if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); dr.mDrawableRight = null; if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); dr.mDrawableBottom = null; dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } } else { if (dr == null) { mDrawables = dr = new Drawables(); } if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { dr.mDrawableLeft.setCallback(null); } dr.mDrawableLeft = left; if (dr.mDrawableTop != top && dr.mDrawableTop != null) { dr.mDrawableTop.setCallback(null); } dr.mDrawableTop = top; if (dr.mDrawableRight != right && dr.mDrawableRight != null) { dr.mDrawableRight.setCallback(null); } dr.mDrawableRight = right; if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { dr.mDrawableBottom.setCallback(null); } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; state = getDrawableState(); if (left != null) { left.setState(state); left.copyBounds(compoundRect); left.setCallback(this); dr.mDrawableSizeLeft = compoundRect.width(); dr.mDrawableHeightLeft = compoundRect.height(); } else { dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; } if (right != null) { right.setState(state); right.copyBounds(compoundRect); right.setCallback(this); dr.mDrawableSizeRight = compoundRect.width(); dr.mDrawableHeightRight = compoundRect.height(); } else { dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; } if (top != null) { top.setState(state); top.copyBounds(compoundRect); top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; } if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); bottom.setCallback(this); dr.mDrawableSizeBottom = compoundRect.height(); dr.mDrawableWidthBottom = compoundRect.width(); } else { dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } // I do not think this is needed anymore // invalidate(); // requestLayout(); } }