package co.smartreceipts.android.widget;
import android.content.Context;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatEditText;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.EditText;
import co.smartreceipts.android.R;
/**
* <p>
* Extends the default Android {@link EditText} behavior to allow for different network
* states within this box via a right-aligned icon. Users of this class should maintain responsibility
* for driving the different network states of this class via the following methods:
* <ul>
* <p/>
* </ul>
* </p>
* <p>
* Please note that this class overrides the <pre>@attr ref android.R.styleable#TextView_drawableEnd</pre>
* attribute. Manually calling any of the setCompoundDrawable methods may break the behavior of this class.
* </p>
*/
public class NetworkRequestAwareEditText extends AppCompatEditText {
/**
* Tracks the various network states this layout can exist within. All states must be driven externally
*/
public enum State {
/**
* Initial/default state before another gets set
*/
Unprepared,
/**
* Indicates that this view is ready to submit a network request
*/
Ready,
/**
* Indicates that we are currently loading
*/
Loading,
/**
* Indicates that we successfully performed a network request
*/
Success,
/**
* Indicates that the network request failed
*/
Failure
}
public interface RetryListener {
/**
* Callback method that is called whenever the user wish to retry the network request
*/
void onUserRetry();
}
private State mState = State.Unprepared;
private CharSequence mOriginalHint;
private CharSequence mFailedHint;
private RetryListener mRetryListener;
private boolean mUserRetryActionEnabled;
public NetworkRequestAwareEditText(Context context) {
super(context);
init();
}
public NetworkRequestAwareEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public NetworkRequestAwareEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mOriginalHint = getHint();
mFailedHint = getHint();
mUserRetryActionEnabled = true;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
final Drawable drawableEnd;
if (!mUserRetryActionEnabled) {
drawableEnd = null;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
drawableEnd = getCompoundDrawablesRelative()[2];
} else {
drawableEnd = getCompoundDrawables()[2];
}
if (drawableEnd != null) {
final int start;
if (ViewCompat.LAYOUT_DIRECTION_LTR == ViewCompat.getLayoutDirection(this)) {
start = getWidth() - ViewCompat.getPaddingEnd(this) - drawableEnd.getIntrinsicWidth();
} else {
start = ViewCompat.getPaddingEnd(this);
}
final boolean wasTapped = event.getX() > start && event.getX() < start + drawableEnd.getIntrinsicWidth();
if (wasTapped) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (mRetryListener != null) {
mRetryListener.onUserRetry();
}
return true;
} else {
return super.onTouchEvent(event);
}
}
}
return super.onTouchEvent(event);
}
/**
* @return the current network state of this view
*/
@NonNull
public State getCurrentState() {
return mState;
}
/**
* Updates the current state of this view
*
* @param state the desired "new" state
*/
public synchronized void setCurrentState(@NonNull State state) {
if (mState == state) {
// Exit early if nothing is changing
return;
}
mState = state;
final Drawable[] drawables;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
drawables = getCompoundDrawablesRelative();
} else {
drawables = getCompoundDrawables();
}
final Drawable drawableStart = drawables[0];
final Drawable drawableTop = drawables[1];
final Drawable drawableBottom = drawables[3];
final Drawable drawableEnd;
if (mUserRetryActionEnabled) {
switch (state) {
case Ready:
drawableEnd = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_refresh, getContext().getTheme());
setHint(mOriginalHint);
break;
case Loading:
drawableEnd = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_refresh_in_progress, getContext().getTheme());
setHint(mOriginalHint);
break;
case Failure:
drawableEnd = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_refresh, getContext().getTheme());
setHint(mFailedHint);
break;
case Success:
drawableEnd = null;
setHint(mOriginalHint);
break;
default:
drawableEnd = null;
setHint(mOriginalHint);
break;
}
} else {
drawableEnd = null;
setHint(mOriginalHint);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, drawableTop, drawableEnd, drawableBottom);
} else {
setCompoundDrawablesWithIntrinsicBounds(drawableStart, drawableTop, drawableEnd, drawableBottom);
}
if (drawableEnd instanceof Animatable) {
((Animatable)drawableEnd).start();
}
}
public void setRetryListener(@Nullable RetryListener retryListener) {
mRetryListener = retryListener;
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if (TextUtils.isEmpty(text)) {
setCurrentState(State.Ready);
} else {
setCurrentState(State.Success);
}
}
/**
* @return the hint that appears if we enter the Failed state
*/
public CharSequence getFailedHint() {
return mFailedHint;
}
/**
* Sets the hint that appears if we failed to complete the network request
*
* @param failedHint the new failed hint
*/
public void setFailedHint(CharSequence failedHint) {
mFailedHint = failedHint;
}
/**
* Sets the hint that appears if we failed to complete the network request
*
* @param failedHint the new failed hint
*/
public void setFailedHint(@StringRes int failedHint) {
mFailedHint = getContext().getString(failedHint);
}
/**
* Allows us to selectively enable/disable the ability for user's to retry
*
* @param enabled {@code true} if this behavior should be enabled. {@code false} otherwise
*/
public void setUserRetryActionEnabled(boolean enabled) {
mUserRetryActionEnabled = enabled;
invalidate();
}
/**
* @return {@code true} if user retry actions are enabled, {@code false} otherwise
*/
public boolean isUserRetryActionEnabled() {
return mUserRetryActionEnabled;
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState, mState);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mState = savedState.getState();
} else {
super.onRestoreInstanceState(state);
}
}
/**
* Utility class that allows us to persist {@link State} information
* across config changes
*/
static class SavedState extends BaseSavedState {
private final State mState;
public SavedState(@NonNull Parcelable superState, @NonNull State currentState) {
super(superState);
mState = currentState;
}
public SavedState(@NonNull Parcel in) {
super(in);
final State state = (State) in.readSerializable();
mState = state != null ? state : State.Unprepared;
}
@NonNull
public State getState() {
return mState;
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeSerializable(mState);
}
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(@NonNull Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}