package carbon.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.text.Editable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewOutlineProvider;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.TextView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import carbon.Carbon;
import carbon.R;
import carbon.animation.AnimUtils;
import carbon.animation.AnimatedColorStateList;
import carbon.animation.AnimatedView;
import carbon.animation.StateAnimator;
import carbon.drawable.DefaultColorStateList;
import carbon.drawable.DefaultNormalColorStateList;
import carbon.drawable.VectorDrawable;
import carbon.drawable.ripple.RippleDrawable;
import carbon.drawable.ripple.RippleView;
import carbon.internal.AllCapsTransformationMethod;
import carbon.internal.RevealAnimator;
import carbon.internal.SimpleTextWatcher;
import carbon.internal.TypefaceUtils;
import carbon.shadow.Shadow;
import carbon.shadow.ShadowGenerator;
import carbon.shadow.ShadowShape;
import carbon.shadow.ShadowView;
public class EditText extends android.widget.EditText
implements ShadowView, RippleView, TouchMarginView, StateAnimatorView, AnimatedView, TintedView, ValidStateView, AutoSizeTextView, RevealView, VisibleView {
private Field mIgnoreActionUpEventField;
private Object editor;
TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private int DIVIDER_PADDING;
private int cursorColor;
private Pattern pattern;
private int matchingView;
private BitmapShader dashPathShader;
private boolean underline = true;
private boolean valid = true;
private List<OnValidateListener> validateListeners = new ArrayList<>();
public EditText(Context context) {
super(context, null);
initEditText(null, android.R.attr.editTextStyle);
}
public EditText(Context context, AttributeSet attrs) {
super(Carbon.getThemedContext(context, attrs, R.styleable.EditText, android.R.attr.editTextStyle, R.styleable.EditText_carbon_theme), attrs);
initEditText(attrs, android.R.attr.editTextStyle);
}
public EditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(Carbon.getThemedContext(context, attrs, R.styleable.EditText, defStyleAttr, R.styleable.EditText_carbon_theme), attrs, defStyleAttr);
initEditText(attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(Carbon.getThemedContext(context, attrs, R.styleable.EditText, defStyleAttr, R.styleable.EditText_carbon_theme), attrs, defStyleAttr, defStyleRes);
initEditText(attrs, defStyleAttr);
}
private static int[] rippleIds = new int[]{
R.styleable.EditText_carbon_rippleColor,
R.styleable.EditText_carbon_rippleStyle,
R.styleable.EditText_carbon_rippleHotspot,
R.styleable.EditText_carbon_rippleRadius
};
private static int[] animationIds = new int[]{
R.styleable.EditText_carbon_inAnimation,
R.styleable.EditText_carbon_outAnimation
};
private static int[] touchMarginIds = new int[]{
R.styleable.EditText_carbon_touchMargin,
R.styleable.EditText_carbon_touchMarginLeft,
R.styleable.EditText_carbon_touchMarginTop,
R.styleable.EditText_carbon_touchMarginRight,
R.styleable.EditText_carbon_touchMarginBottom
};
private static int[] tintIds = new int[]{
R.styleable.EditText_carbon_tint,
R.styleable.EditText_carbon_tintMode,
R.styleable.EditText_carbon_backgroundTint,
R.styleable.EditText_carbon_backgroundTintMode,
R.styleable.EditText_carbon_animateColorChanges
};
private static int[] elevationIds = new int[]{
R.styleable.EditText_carbon_elevation,
R.styleable.EditText_carbon_tintMode,
};
private static int[] autoSizeTextIds = new int[]{
R.styleable.EditText_carbon_autoSizeText,
R.styleable.EditText_carbon_autoSizeMinTextSize,
R.styleable.EditText_carbon_autoSizeMaxTextSize,
R.styleable.EditText_carbon_autoSizeStepGranularity
};
private void initEditText(AttributeSet attrs, int defStyleAttr) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.EditText, defStyleAttr, R.style.carbon_EditText);
int ap = a.getResourceId(R.styleable.EditText_android_textAppearance, -1);
if (ap != -1)
setTextAppearanceInternal(ap);
for (int i = 0; i < a.getIndexCount(); i++) {
int attr = a.getIndex(i);
if (!isInEditMode() && attr == R.styleable.EditText_carbon_fontPath) {
String path = a.getString(attr);
Typeface typeface = TypefaceUtils.getTypeface(getContext(), path);
setTypeface(typeface);
} else if (attr == R.styleable.EditText_carbon_fontFamily) {
int textStyle = a.getInt(R.styleable.EditText_android_textStyle, 0);
Typeface typeface = TypefaceUtils.getTypeface(getContext(), a.getString(attr), textStyle);
setTypeface(typeface);
} else if (attr == R.styleable.EditText_android_textAllCaps) {
setAllCaps(a.getBoolean(attr, true));
}
}
setCursorColor(a.getColor(R.styleable.EditText_carbon_cursorColor, 0));
setPattern(a.getString(R.styleable.EditText_carbon_pattern));
DIVIDER_PADDING = (int) getResources().getDimension(R.dimen.carbon_paddingHalf);
setMatchingView(a.getResourceId(R.styleable.EditText_carbon_matchingView, 0));
setUnderline(a.getBoolean(R.styleable.EditText_carbon_underline, true));
Carbon.initRippleDrawable(this, a, rippleIds);
Carbon.initTint(this, a, tintIds);
Carbon.initElevation(this, a, elevationIds);
Carbon.initAnimations(this, a, animationIds);
Carbon.initTouchMargin(this, a, touchMarginIds);
setCornerRadius(a.getDimension(R.styleable.EditText_carbon_cornerRadius, 0));
Carbon.initHtmlText(this, a, R.styleable.EditText_carbon_htmlText);
Carbon.initAutoSizeText(this, a, autoSizeTextIds);
a.recycle();
int underlineWidth = getResources().getDimensionPixelSize(R.dimen.carbon_1dip);
Bitmap dashPathBitmap = Bitmap.createBitmap(underlineWidth * 4, underlineWidth, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(dashPathBitmap);
paint.setColor(0xffffffff);
paint.setAlpha(255);
c.drawCircle(dashPathBitmap.getHeight() / 2.0f, dashPathBitmap.getHeight() / 2.0f, dashPathBitmap.getHeight() / 2.0f, paint);
dashPathShader = new BitmapShader(dashPathBitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
initSelectionHandle();
addTextChangedListener(new SimpleTextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
validateInternalEvent();
}
});
validateInternalEvent();
if (getElevation() > 0)
AnimUtils.setupElevationAnimator(stateAnimator, this);
setSelection(length());
}
private void initSelectionHandle() {
try {
final Field fEditor = android.widget.TextView.class.getDeclaredField("mEditor");
fEditor.setAccessible(true);
editor = fEditor.get(this);
mIgnoreActionUpEventField = editor.getClass().getDeclaredField("mIgnoreActionUpEvent");
mIgnoreActionUpEventField.setAccessible(true);
final Field fSelectHandleLeft = editor.getClass().getDeclaredField("mSelectHandleLeft");
final Field fSelectHandleRight = editor.getClass().getDeclaredField("mSelectHandleRight");
final Field fSelectHandleCenter = editor.getClass().getDeclaredField("mSelectHandleCenter");
fSelectHandleLeft.setAccessible(true);
fSelectHandleRight.setAccessible(true);
fSelectHandleCenter.setAccessible(true);
VectorDrawable leftHandle = new VectorDrawable(getResources(), R.raw.carbon_selecthandle_left);
leftHandle.setTint(Carbon.getThemeColor(getContext(), R.attr.colorAccent));
fSelectHandleLeft.set(editor, leftHandle);
VectorDrawable rightHandle = new VectorDrawable(getResources(), R.raw.carbon_selecthandle_right);
rightHandle.setTint(Carbon.getThemeColor(getContext(), R.attr.colorAccent));
fSelectHandleRight.set(editor, rightHandle);
VectorDrawable middleHandle = new VectorDrawable(getResources(), R.raw.carbon_selecthandle_middle);
middleHandle.setTint(Carbon.getThemeColor(getContext(), R.attr.colorAccent));
fSelectHandleCenter.set(editor, middleHandle);
} catch (final Exception ignored) {
}
}
public void setCursorColor(int cursorColor) {
this.cursorColor = cursorColor;
try {
Field mHighlightPaintField = android.widget.TextView.class.getDeclaredField("mHighlightPaint");
mHighlightPaintField.setAccessible(true);
mHighlightPaintField.set(this, new Paint() {
@Override
public void setColor(int color) {
if (getSelectionStart() == getSelectionEnd()) {
super.setColor(cursorColor);
} else {
super.setColor(color);
}
}
});
} catch (Exception e) {
}
}
public int getCursorColor() {
return cursorColor;
}
public boolean hasUnderline() {
return underline;
}
public void setUnderline(boolean drawUnderline) {
this.underline = drawUnderline;
}
public void validate() {
validateInternal();
postInvalidate();
}
private void validateInternal() {
String s = getText().toString();
// dictionary suggestions vs s.length()>0
/*try {
Field mTextField = getText().getClass().getDeclaredField("mText");
mTextField.setAccessible(true);
s = new String((char[])mTextField.get(getText()));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}*/
boolean drawPatternError = false, drawMatchingViewError = false;
if (pattern != null)
drawPatternError = !pattern.matcher(s).matches();
if (matchingView != 0) {
View view = getRootView().findViewById(matchingView);
if (view instanceof TextView) {
TextView matchingTextView = (TextView) view;
if (!matchingTextView.getText().toString().equals(getText().toString()))
drawMatchingViewError = true;
}
}
valid = !drawMatchingViewError && !drawPatternError;
refreshDrawableState();
}
private void validateInternalEvent() {
validateInternal();
fireOnValidateEvent();
postInvalidate();
}
public void addOnValidateListener(OnValidateListener listener) {
validateListeners.add(listener);
}
public void removeOnValidateListener(OnValidateListener listener) {
validateListeners.remove(listener);
}
public void clearOnValidateListeners() {
validateListeners.clear();
}
private void fireOnValidateEvent() {
for (OnValidateListener validateListener : validateListeners)
validateListener.onValidate(valid);
}
RevealAnimator revealAnimator;
@Override
public Animator createCircularReveal(int x, int y, float startRadius, float finishRadius) {
if (Carbon.IS_LOLLIPOP && renderingMode == RenderingMode.Auto) {
Animator circularReveal = ViewAnimationUtils.createCircularReveal(this, x, y, startRadius, finishRadius);
circularReveal.setDuration(Carbon.getDefaultRevealDuration());
return circularReveal;
} else {
revealAnimator = new RevealAnimator(x, y, startRadius, finishRadius);
revealAnimator.setDuration(Carbon.getDefaultRevealDuration());
revealAnimator.addUpdateListener(animation -> {
RevealAnimator reveal = ((RevealAnimator) animation);
reveal.radius = (float) reveal.getAnimatedValue();
reveal.mask.reset();
reveal.mask.addCircle(reveal.x, reveal.y, Math.max((Float) reveal.getAnimatedValue(), 1), Path.Direction.CW);
postInvalidate();
});
revealAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
revealAnimator = null;
}
@Override
public void onAnimationEnd(Animator animation) {
revealAnimator = null;
}
});
return revealAnimator;
}
}
/**
* Changes text transformation method to caps.
*
* @param allCaps if true, TextView will automatically capitalize all characters
*/
public void setAllCaps(boolean allCaps) {
if (allCaps) {
setTransformationMethod(new AllCapsTransformationMethod(getContext()));
} else {
setTransformationMethod(null);
}
}
@Override
public void setTextColor(ColorStateList colors) {
super.setTextColor(animateColorChanges && !(colors instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(colors, textColorAnimatorListener) : colors);
}
@Override
public void setTextAppearance(@NonNull Context context, int resid) {
super.setTextAppearance(context, resid);
setTextAppearanceInternal(resid);
}
public void setTextAppearance(int resid) {
super.setTextAppearance(getContext(), resid);
setTextAppearanceInternal(resid);
}
private void setTextAppearanceInternal(int resid) {
TypedArray appearance = getContext().obtainStyledAttributes(resid, R.styleable.TextAppearance);
if (appearance != null) {
for (int i = 0; i < appearance.getIndexCount(); i++) {
int attr = appearance.getIndex(i);
if (!isInEditMode() && attr == R.styleable.TextAppearance_carbon_fontPath) {
String path = appearance.getString(attr);
Typeface typeface = TypefaceUtils.getTypeface(getContext(), path);
setTypeface(typeface);
} else if (attr == R.styleable.TextAppearance_carbon_fontFamily) {
int textStyle = appearance.getInt(R.styleable.TextAppearance_android_textStyle, 0);
Typeface typeface = TypefaceUtils.getTypeface(getContext(), appearance.getString(attr), textStyle);
setTypeface(typeface);
} else if (attr == R.styleable.TextAppearance_android_textAllCaps) {
setAllCaps(appearance.getBoolean(attr, true));
}
}
appearance.recycle();
}
}
public void drawInternal(@NonNull Canvas canvas) {
super.draw(canvas);
int paddingBottom = getPaddingBottom();
if (isFocused() && isEnabled()) {
paint.setStrokeWidth(2 * getResources().getDimension(R.dimen.carbon_1dip));
} else {
paint.setStrokeWidth(getResources().getDimension(R.dimen.carbon_1dip));
}
if (underline) {
//if (isEnabled()) {
//paint.setShader(null);
paint.setColor(backgroundTint.getColorForState(getDrawableState(), backgroundTint.getDefaultColor()));
canvas.drawLine(getScrollX() + getPaddingLeft(), getHeight() - paddingBottom + DIVIDER_PADDING, getScrollX() + getWidth() - getPaddingRight(), getHeight() - paddingBottom + DIVIDER_PADDING, paint);
/* } else {
Matrix matrix = new Matrix();
matrix.postTranslate(0, getHeight() - paddingBottom + DIVIDER_PADDING - paint.getStrokeWidth() / 2.0f);
dashPathShader.setLocalMatrix(matrix);
//paint.setShader(dashPathShader);
canvas.drawRect(getPaddingLeft(), getHeight() - paddingBottom + DIVIDER_PADDING - paint.getStrokeWidth() / 2.0f,
getWidth() - getPaddingRight(), getHeight() - paddingBottom + DIVIDER_PADDING + paint.getStrokeWidth() / 2.0f, paint);
}*/
}
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Over)
rippleDrawable.draw(canvas);
}
@Override
public void setValid(boolean valid) {
this.valid = valid;
}
public boolean isValid() {
return valid;
}
@Override
public boolean isEmpty() {
return getText().length() == 0;
}
public String getPattern() {
return pattern.pattern();
}
public void setPattern(final String pattern) {
if (pattern == null) {
this.pattern = null;
} else {
this.pattern = Pattern.compile(pattern);
}
}
public void setMatchingView(int viewId) {
this.matchingView = viewId;
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (!focused)
validateInternalEvent();
}
// -------------------------------
// corners
// -------------------------------
private float cornerRadius;
private Path cornersMask;
/**
* Gets the corner radius. If corner radius is equal to 0, rounded corners are turned off.
*
* @return corner radius, equal to or greater than 0.
*/
public float getCornerRadius() {
return cornerRadius;
}
/**
* Sets the corner radius. If corner radius is equal to 0, rounded corners are turned off.
*
* @param cornerRadius
*/
public void setCornerRadius(float cornerRadius) {
this.cornerRadius = cornerRadius;
if (getWidth() > 0 && getHeight() > 0)
updateCorners();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed)
return;
if (getWidth() == 0 || getHeight() == 0)
return;
updateCorners();
if (rippleDrawable != null)
rippleDrawable.setBounds(0, 0, getWidth(), getHeight());
}
private void updateCorners() {
if (cornerRadius > 0) {
cornerRadius = Math.min(cornerRadius, Math.min(getWidth(), getHeight()) / 2.0f);
if (Carbon.IS_LOLLIPOP && renderingMode == RenderingMode.Auto) {
setClipToOutline(true);
setOutlineProvider(ShadowShape.viewOutlineProvider);
} else {
cornersMask = new Path();
cornersMask.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), cornerRadius, cornerRadius, Path.Direction.CW);
cornersMask.setFillType(Path.FillType.INVERSE_WINDING);
}
} else if (Carbon.IS_LOLLIPOP) {
setOutlineProvider(ViewOutlineProvider.BOUNDS);
}
}
@SuppressLint("MissingSuperCall")
@Override
public void draw(@NonNull Canvas canvas) {
if (cornerRadius > 0 && getWidth() > 0 && getHeight() > 0 && !Carbon.IS_LOLLIPOP || renderingMode == RenderingMode.Software) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
drawInternal(canvas);
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Over)
rippleDrawable.draw(canvas);
paint.setXfermode(Carbon.CLEAR_MODE);
canvas.drawPath(cornersMask, paint);
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
} else {
drawInternal(canvas);
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Over)
rippleDrawable.draw(canvas);
}
}
// -------------------------------
// ripple
// -------------------------------
private RippleDrawable rippleDrawable;
private Transformation t = new Transformation();
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
Animation a = getAnimation();
if (a != null) {
a.getTransformation(event.getEventTime(), t);
float[] loc = new float[]{event.getX(), event.getY()};
t.getMatrix().mapPoints(loc);
event.setLocation(loc[0], loc[1]);
}
if (rippleDrawable != null && event.getAction() == MotionEvent.ACTION_DOWN)
rippleDrawable.setHotspot(event.getX(), event.getY());
return super.dispatchTouchEvent(event);
}
@Override
public RippleDrawable getRippleDrawable() {
return rippleDrawable;
}
@Override
public void setRippleDrawable(RippleDrawable newRipple) {
if (rippleDrawable != null) {
rippleDrawable.setCallback(null);
if (rippleDrawable.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable(rippleDrawable.getBackground());
}
if (newRipple != null) {
newRipple.setCallback(this);
newRipple.setBounds(0, 0, getWidth(), getHeight());
if (newRipple.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable((Drawable) newRipple);
}
rippleDrawable = newRipple;
}
@Override
protected boolean verifyDrawable(@NonNull Drawable who) {
return super.verifyDrawable(who) || rippleDrawable == who;
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).invalidate();
}
@Override
public void invalidate(@NonNull Rect dirty) {
super.invalidate(dirty);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate(dirty);
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).invalidate(dirty);
}
@Override
public void invalidate(int l, int t, int r, int b) {
super.invalidate(l, t, r, b);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate(l, t, r, b);
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).invalidate(l, t, r, b);
}
@Override
public void invalidate() {
super.invalidate();
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).invalidate();
}
@Override
public void postInvalidateDelayed(long delayMilliseconds) {
super.postInvalidateDelayed(delayMilliseconds);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidateDelayed(delayMilliseconds);
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).postInvalidateDelayed(delayMilliseconds);
}
@Override
public void postInvalidateDelayed(long delayMilliseconds, int left, int top, int right, int bottom) {
super.postInvalidateDelayed(delayMilliseconds, left, top, right, bottom);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidateDelayed(delayMilliseconds, left, top, right, bottom);
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).postInvalidateDelayed(delayMilliseconds, left, top, right, bottom);
}
@Override
public void postInvalidate() {
super.postInvalidate();
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidate();
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).postInvalidate();
}
@Override
public void postInvalidate(int left, int top, int right, int bottom) {
super.postInvalidate(left, top, right, bottom);
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).postInvalidate(left, top, right, bottom);
if (getElevation() > 0 || getCornerRadius() > 0)
((View) getParent()).postInvalidate(left, top, right, bottom);
}
@Override
public void setBackground(Drawable background) {
setBackgroundDrawable(background);
}
@Override
public void setBackgroundDrawable(Drawable background) {
if (background instanceof RippleDrawable) {
setRippleDrawable((RippleDrawable) background);
return;
}
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Background) {
rippleDrawable.setCallback(null);
rippleDrawable = null;
}
super.setBackgroundDrawable(background);
updateTint();
}
// -------------------------------
// elevation
// -------------------------------
private float elevation = 0;
private float translationZ = 0;
private Shadow ambientShadow, spotShadow;
private ColorStateList shadowColor;
private PorterDuffColorFilter shadowColorFilter;
private RectF shadowMaskRect = new RectF();
@Override
public float getElevation() {
return elevation;
}
@Override
public void setElevation(float elevation) {
if (Carbon.IS_LOLLIPOP) {
if (shadowColor == null && renderingMode == RenderingMode.Auto) {
super.setElevation(elevation);
super.setTranslationZ(translationZ);
} else {
super.setElevation(0);
super.setTranslationZ(0);
}
} else if (elevation != this.elevation && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.elevation = elevation;
}
@Override
public float getTranslationZ() {
return translationZ;
}
public void setTranslationZ(float translationZ) {
if (translationZ == this.translationZ)
return;
if (Carbon.IS_LOLLIPOP) {
if (shadowColor == null && renderingMode == RenderingMode.Auto) {
super.setTranslationZ(translationZ);
} else {
super.setTranslationZ(0);
}
} else if (translationZ != this.translationZ && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.translationZ = translationZ;
}
@Override
public ShadowShape getShadowShape() {
if (cornerRadius == getWidth() / 2 && getWidth() == getHeight())
return ShadowShape.CIRCLE;
if (cornerRadius > 0)
return ShadowShape.ROUND_RECT;
return ShadowShape.RECT;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
}
@Override
public boolean hasShadow() {
return getElevation() + getTranslationZ() >= 0.01f && getWidth() > 0 && getHeight() > 0;
}
@Override
public void drawShadow(Canvas canvas) {
float alpha = getAlpha() * Carbon.getDrawableAlpha(getBackground()) / 255.0f * Carbon.getBackgroundTintAlpha(this) / 255.0f;
if (alpha == 0)
return;
if (!hasShadow())
return;
float z = getElevation() + getTranslationZ();
if (ambientShadow == null || ambientShadow.elevation != z || ambientShadow.cornerRadius != cornerRadius) {
ambientShadow = ShadowGenerator.generateShadow(this, z / getResources().getDisplayMetrics().density / 4);
spotShadow = ShadowGenerator.generateShadow(this, z / getResources().getDisplayMetrics().density);
}
int saveCount = 0;
boolean maskShadow = getBackground() != null && alpha != 1;
boolean r = revealAnimator != null && revealAnimator.isRunning();
if (maskShadow) {
saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
} else if (r) {
saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.clipRect(
getLeft() + revealAnimator.x - revealAnimator.radius, getTop() + revealAnimator.y - revealAnimator.radius,
getLeft() + revealAnimator.x + revealAnimator.radius, getTop() + revealAnimator.y + revealAnimator.radius);
}
paint.setAlpha((int) (Shadow.ALPHA * alpha));
Matrix matrix = getMatrix();
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(this.getLeft(), this.getTop());
canvas.concat(matrix);
ambientShadow.draw(canvas, this, paint, shadowColorFilter);
canvas.restore();
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(this.getLeft(), this.getTop() + z / 2);
canvas.concat(matrix);
spotShadow.draw(canvas, this, paint, shadowColorFilter);
canvas.restore();
if (saveCount != 0) {
canvas.translate(this.getLeft(), this.getTop());
canvas.concat(matrix);
paint.setXfermode(Carbon.CLEAR_MODE);
}
if (maskShadow) {
shadowMaskRect.set(0, 0, getWidth(), getHeight());
canvas.drawRoundRect(shadowMaskRect, cornerRadius, cornerRadius, paint);
}
if (r) {
canvas.drawPath(revealAnimator.mask, paint);
}
if (saveCount != 0) {
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}
}
@Override
public void setElevationShadowColor(ColorStateList shadowColor) {
this.shadowColor = shadowColor;
shadowColorFilter = shadowColor != null ? new PorterDuffColorFilter(shadowColor.getColorForState(getDrawableState(), shadowColor.getDefaultColor()), PorterDuff.Mode.MULTIPLY) : Shadow.DEFAULT_FILTER;
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public void setElevationShadowColor(int color) {
shadowColor = ColorStateList.valueOf(color);
shadowColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY);
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public ColorStateList getElevationShadowColor() {
return shadowColor;
}
// -------------------------------
// touch margin
// -------------------------------
private Rect touchMargin;
@Override
public void setTouchMargin(int left, int top, int right, int bottom) {
touchMargin = new Rect(left, top, right, bottom);
}
@Override
public void setTouchMarginLeft(int margin) {
touchMargin.left = margin;
}
@Override
public void setTouchMarginTop(int margin) {
touchMargin.top = margin;
}
@Override
public void setTouchMarginRight(int margin) {
touchMargin.right = margin;
}
@Override
public void setTouchMarginBottom(int margin) {
touchMargin.bottom = margin;
}
@Override
public Rect getTouchMargin() {
return touchMargin;
}
public void getHitRect(@NonNull Rect outRect) {
if (touchMargin == null) {
super.getHitRect(outRect);
return;
}
outRect.set(getLeft() - touchMargin.left, getTop() - touchMargin.top, getRight() + touchMargin.right, getBottom() + touchMargin.bottom);
}
// -------------------------------
// state animators
// -------------------------------
private StateAnimator stateAnimator = new StateAnimator(this);
@Override
public StateAnimator getStateAnimator() {
return stateAnimator;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
if (stateAnimator != null)
stateAnimator.setState(getDrawableState());
ColorStateList textColors = getTextColors();
if (textColors instanceof AnimatedColorStateList)
((AnimatedColorStateList) textColors).setState(getDrawableState());
if (tint != null && tint instanceof AnimatedColorStateList)
((AnimatedColorStateList) tint).setState(getDrawableState());
if (backgroundTint != null && backgroundTint instanceof AnimatedColorStateList)
((AnimatedColorStateList) backgroundTint).setState(getDrawableState());
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
drawableState[drawableState.length - 1] = valid ? -R.attr.carbon_state_invalid : R.attr.carbon_state_invalid;
return drawableState;
}
// -------------------------------
// animations
// -------------------------------
private Animator inAnim = null, outAnim = null;
private Animator animator;
public Animator animateVisibility(final int visibility) {
if (visibility == View.VISIBLE && (getVisibility() != View.VISIBLE || animator != null)) {
if (animator != null)
animator.cancel();
if (inAnim != null) {
animator = inAnim;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator a) {
animator.removeListener(this);
animator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
animator.removeListener(this);
animator = null;
}
});
animator.start();
}
setVisibility(visibility);
} else if (visibility != View.VISIBLE && (getVisibility() == View.VISIBLE || animator != null)) {
if (animator != null)
animator.cancel();
if (outAnim == null) {
setVisibility(visibility);
return null;
}
animator = outAnim;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator a) {
if (((ValueAnimator) a).getAnimatedFraction() == 1)
setVisibility(visibility);
animator.removeListener(this);
animator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
animator.removeListener(this);
animator = null;
}
});
animator.start();
}
return animator;
}
public Animator getAnimator() {
return animator;
}
public Animator getOutAnimator() {
return outAnim;
}
public void setOutAnimator(Animator outAnim) {
if (this.outAnim != null)
this.outAnim.setTarget(null);
this.outAnim = outAnim;
if (outAnim != null)
outAnim.setTarget(this);
}
public Animator getInAnimator() {
return inAnim;
}
public void setInAnimator(Animator inAnim) {
if (this.inAnim != null)
this.inAnim.setTarget(null);
this.inAnim = inAnim;
if (inAnim != null)
inAnim.setTarget(this);
}
// -------------------------------
// tint
// -------------------------------
ColorStateList tint;
PorterDuff.Mode tintMode;
ColorStateList backgroundTint;
PorterDuff.Mode backgroundTintMode;
boolean animateColorChanges;
ValueAnimator.AnimatorUpdateListener tintAnimatorListener = animation -> {
updateTint();
ViewCompat.postInvalidateOnAnimation(EditText.this);
};
ValueAnimator.AnimatorUpdateListener backgroundTintAnimatorListener = animation -> {
updateBackgroundTint();
ViewCompat.postInvalidateOnAnimation(EditText.this);
};
ValueAnimator.AnimatorUpdateListener textColorAnimatorListener = animation -> setHintTextColor(getHintTextColors());
@Override
public void setTint(ColorStateList list) {
if (list != null) {
tint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, tintAnimatorListener) : list;
} else {
tint = null;
}
updateTint();
}
@Override
public void setTint(int color) {
if (color == 0) {
setTint(new DefaultNormalColorStateList(getContext()));
} else {
setTint(ColorStateList.valueOf(color));
}
}
@Override
public ColorStateList getTint() {
return tint;
}
private void updateTint() {
Drawable[] drawables = getCompoundDrawables();
if (tint != null && tintMode != null) {
int color = tint.getColorForState(getDrawableState(), tint.getDefaultColor());
for (Drawable d : drawables)
if (d != null)
d.setColorFilter(new PorterDuffColorFilter(color, tintMode));
} else {
for (Drawable d : drawables)
if (d != null)
d.setColorFilter(null);
}
}
@Override
public void setTintMode(@NonNull PorterDuff.Mode mode) {
this.tintMode = mode;
updateTint();
}
@Override
public PorterDuff.Mode getTintMode() {
return tintMode;
}
@Override
public void setBackgroundTint(ColorStateList list) {
this.backgroundTint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, backgroundTintAnimatorListener) : list;
updateBackgroundTint();
}
@Override
public void setBackgroundTint(int color) {
if (color == 0) {
setBackgroundTint(new DefaultColorStateList(getContext()));
} else {
setBackgroundTint(ColorStateList.valueOf(color));
}
}
@Override
public ColorStateList getBackgroundTint() {
return backgroundTint;
}
private void updateBackgroundTint() {
if (getBackground() == null)
return;
if (backgroundTint != null && backgroundTintMode != null) {
int color = backgroundTint.getColorForState(getDrawableState(), backgroundTint.getDefaultColor());
getBackground().setColorFilter(new PorterDuffColorFilter(color, backgroundTintMode));
} else {
getBackground().setColorFilter(null);
}
}
@Override
public void setBackgroundTintMode(@Nullable PorterDuff.Mode mode) {
this.backgroundTintMode = mode;
updateBackgroundTint();
}
@Override
public PorterDuff.Mode getBackgroundTintMode() {
return backgroundTintMode;
}
public boolean isAnimateColorChangesEnabled() {
return animateColorChanges;
}
public void setAnimateColorChangesEnabled(boolean animateColorChanges) {
this.animateColorChanges = animateColorChanges;
if (tint != null && !(tint instanceof AnimatedColorStateList))
setTint(AnimatedColorStateList.fromList(tint, tintAnimatorListener));
if (backgroundTint != null && !(backgroundTint instanceof AnimatedColorStateList))
setBackgroundTint(AnimatedColorStateList.fromList(backgroundTint, backgroundTintAnimatorListener));
if (!(getTextColors() instanceof AnimatedColorStateList))
setTextColor(AnimatedColorStateList.fromList(getTextColors(), textColorAnimatorListener));
}
// -------------------------------
// auto size
// -------------------------------
private AutoSizeTextMode autoSizeText = AutoSizeTextMode.None;
private float minTextSize, maxTextSize, autoSizeStepGranularity;
private float[] autoSizeStepPresets;
private RectF textRect = new RectF();
private RectF availableSpaceRect = new RectF();
private float spacingMult = 1.0f;
private float spacingAdd = 0.0f;
private int maxLines = -1;
@NonNull
public AutoSizeTextMode getAutoSizeText() {
return autoSizeText;
}
public void setAutoSizeText(@NonNull AutoSizeTextMode autoSizeText) {
this.autoSizeText = autoSizeText;
adjustTextSize();
}
@Override
public void setText(final CharSequence text, BufferType type) {
super.setText(text, type);
adjustTextSize();
}
@Override
public void setTextSize(float size) {
super.setTextSize(size);
adjustTextSize();
}
@Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
this.maxLines = maxLines;
adjustTextSize();
}
@Override
public void setSingleLine() {
super.setSingleLine();
adjustTextSize();
}
@Override
public void setSingleLine(boolean singleLine) {
super.setSingleLine(singleLine);
if (!singleLine)
super.setMaxLines(-1);
adjustTextSize();
}
@Override
public void setLines(int lines) {
super.setLines(lines);
adjustTextSize();
}
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
adjustTextSize();
}
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
spacingMult = mult;
spacingAdd = add;
}
private void initAutoSize() {
if (autoSizeText == AutoSizeTextMode.Uniform && minTextSize > 0 && maxTextSize > 0) {
autoSizeStepPresets = new float[(int) Math.ceil((maxTextSize - minTextSize) / autoSizeStepGranularity) + 1];
for (int i = 0; i < autoSizeStepPresets.length - 1; i++)
autoSizeStepPresets[i] = minTextSize + autoSizeStepGranularity * i;
autoSizeStepPresets[autoSizeStepPresets.length - 1] = maxTextSize;
}
}
public float getMinTextSize() {
return minTextSize;
}
public void setMinTextSize(float minTextSize) {
this.minTextSize = minTextSize;
autoSizeStepPresets = null;
adjustTextSize();
}
public float getMaxTextSize() {
return maxTextSize;
}
public float getAutoSizeStepGranularity() {
return autoSizeStepGranularity;
}
public void setAutoSizeStepGranularity(float autoSizeStepGranularity) {
this.autoSizeStepGranularity = autoSizeStepGranularity;
autoSizeStepPresets = null;
adjustTextSize();
}
public void setMaxTextSize(float maxTextSize) {
this.maxTextSize = maxTextSize;
autoSizeStepPresets = null;
adjustTextSize();
}
private void adjustTextSize() {
if (autoSizeText == AutoSizeTextMode.None || minTextSize <= 0 || maxTextSize <= 0 || getMeasuredWidth() == 0 || getMeasuredHeight() == 0)
return;
if (autoSizeStepPresets == null)
initAutoSize();
availableSpaceRect.right = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
availableSpaceRect.bottom = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, binarySearch(availableSpaceRect));
}
private float binarySearch(RectF availableSpace) {
int lastBest = 0;
int lo = 0;
int hi = autoSizeStepPresets.length - 1;
int mid;
// for (int i = 0; i < autoSizeStepPresets.length; i++) {
// if (testSize(autoSizeStepPresets[i], availableSpace)) {
// lastBest = i;
// } else {
// break;
// }
// }
while (lo <= hi) {
mid = (lo + hi) / 2;
boolean fits = testSize(autoSizeStepPresets[mid], availableSpace);
if (fits) {
lastBest = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return autoSizeStepPresets[lastBest];
}
public boolean testSize(float suggestedSize, RectF availableSpace) {
paint.setTextSize(suggestedSize);
paint.setTypeface(getTypeface());
String text = getText().toString();
if (maxLines == 1) {
textRect.bottom = paint.getFontSpacing();
textRect.right = paint.measureText(text);
return availableSpace.width() >= textRect.right && availableSpace.height() >= textRect.bottom;
} else {
StaticLayout layout = new StaticLayout(text, paint, (int) availableSpace.right, Layout.Alignment.ALIGN_NORMAL, spacingMult, spacingAdd, true);
if (maxLines != -1 && layout.getLineCount() > maxLines)
return false;
return availableSpace.width() >= layout.getWidth() && availableSpace.height() >= layout.getHeight();
}
}
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
super.onTextChanged(text, start, before, after);
adjustTextSize();
}
@Override
protected void onSizeChanged(int width, int height, int oldwidth, int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
adjustTextSize();
}
// -------------------------------
// rendering mode
// -------------------------------
private RenderingMode renderingMode = RenderingMode.Auto;
@Override
public void setRenderingMode(RenderingMode mode) {
this.renderingMode = mode;
setElevation(elevation);
setTranslationZ(translationZ);
updateCorners();
}
@Override
public RenderingMode getRenderingMode() {
return renderingMode;
}
}