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