content: /**
   * Copyright 2016 Ali Muzaffar
   * <p/>
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * <p/>
   * http://www.apache.org/licenses/LICENSE-2.0
   * <p/>
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.alimuzaffar.lib.pin;
  
  import android.animation.Animator;
  import android.animation.AnimatorSet;
  import android.animation.ValueAnimator;
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Typeface;
  import android.graphics.drawable.Drawable;
  import android.text.InputFilter;
  import android.text.InputType;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.util.TypedValue;
  import android.view.ActionMode;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.View;
  import android.view.animation.OvershootInterpolator;
  import android.view.inputmethod.InputMethodManager;
  
  import androidx.annotation.Nullable;
  import androidx.appcompat.widget.AppCompatEditText;
  import androidx.core.content.ContextCompat;
  import androidx.core.view.ViewCompat;
  
  public class PinEntryEditText extends AppCompatEditText {
      private static final String XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
  
      public static final String DEFAULT_MASK = "\u25CF";
  
      protected String mMask = null;
      protected StringBuilder mMaskChars = null;
      protected String mSingleCharHint = null;
      protected int mAnimatedType = 0;
      protected float mSpace = 24; //24 dp by default, space between the lines
      protected float mCharSize;
      protected float mNumChars = 4;
      protected float mTextBottomPadding = 8; //8dp by default, height of the text from our lines
      protected int mMaxLength = 4;
      protected RectF[] mLineCoords;
      protected float[] mCharBottom;
      protected Paint mCharPaint;
      protected Paint mLastCharPaint;
      protected Paint mSingleCharPaint;
      protected Drawable mPinBackground;
      protected Rect mTextHeight = new Rect();
      protected boolean mIsDigitSquare = false;
  
      protected OnClickListener mClickListener;
      protected OnPinEnteredListener mOnPinEnteredListener = null;
  
      protected float mLineStroke = 1; //1dp by default
      protected float mLineStrokeSelected = 2; //2dp by default
      protected Paint mLinesPaint;
      protected boolean mAnimate = false;
      protected boolean mHasError = false;
      protected ColorStateList mOriginalTextColors;
      protected int[][] mStates = new int[][]{
              new int[]{android.R.attr.state_selected}, // selected
              new int[]{android.R.attr.state_active}, // error
              new int[]{android.R.attr.state_focused}, // focused
              new int[]{-android.R.attr.state_focused}, // unfocused
      };
  
      protected int[] mColors = new int[]{
              Color.GREEN,
              Color.RED,
              Color.BLACK,
              Color.GRAY
      };
  
      protected ColorStateList mColorStates = new ColorStateList(mStates, mColors);
  
      public PinEntryEditText(Context context) {
          super(context);
      }
  
      public PinEntryEditText(Context context, AttributeSet attrs) {
          super(context, attrs);
          init(context, attrs);
      }
  
      public PinEntryEditText(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init(context, attrs);
      }
  
      public void setMaxLength(final int maxLength) {
          mMaxLength = maxLength;
          mNumChars = maxLength;
  
          setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
  
          setText(null);
          invalidate();
      }
  
      public void setMask(String mask) {
          mMask = mask;
          mMaskChars = null;
          invalidate();
      }
  
      public void setSingleCharHint(String hint) {
          mSingleCharHint = hint;
          invalidate();
      }
  
      private void init(Context context, AttributeSet attrs) {
          float multi = context.getResources().getDisplayMetrics().density;
          mLineStroke = multi * mLineStroke;
          mLineStrokeSelected = multi * mLineStrokeSelected;
          mSpace = multi * mSpace; //convert to pixels for our density
          mTextBottomPadding = multi * mTextBottomPadding; //convert to pixels for our density
  
          TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PinEntryEditText, 0, 0);
          try {
              TypedValue outValue = new TypedValue();
              ta.getValue(R.styleable.PinEntryEditText_pinAnimationType, outValue);
              mAnimatedType = outValue.data;
              mMask = ta.getString(R.styleable.PinEntryEditText_pinCharacterMask);
              mSingleCharHint = ta.getString(R.styleable.PinEntryEditText_pinRepeatedHint);
              mLineStroke = ta.getDimension(R.styleable.PinEntryEditText_pinLineStroke, mLineStroke);
              mLineStrokeSelected = ta.getDimension(R.styleable.PinEntryEditText_pinLineStrokeSelected, mLineStrokeSelected);
              mSpace = ta.getDimension(R.styleable.PinEntryEditText_pinCharacterSpacing, mSpace);
              mTextBottomPadding = ta.getDimension(R.styleable.PinEntryEditText_pinTextBottomPadding, mTextBottomPadding);
              mIsDigitSquare = ta.getBoolean(R.styleable.PinEntryEditText_pinBackgroundIsSquare, mIsDigitSquare);
              mPinBackground = ta.getDrawable(R.styleable.PinEntryEditText_pinBackgroundDrawable);
              ColorStateList colors = ta.getColorStateList(R.styleable.PinEntryEditText_pinLineColors);
              if (colors != null) {
                  mColorStates = colors;
              }
          } finally {
              ta.recycle();
          }
  
          mCharPaint = new Paint(getPaint());
          mLastCharPaint = new Paint(getPaint());
          mSingleCharPaint = new Paint(getPaint());
          mLinesPaint = new Paint(getPaint());
          mLinesPaint.setStrokeWidth(mLineStroke);
  
          TypedValue outValue = new TypedValue();
          context.getTheme().resolveAttribute(R.attr.colorControlActivated,
                  outValue, true);
          int colorSelected = outValue.data;
          mColors[0] = colorSelected;
  
          int colorFocused = isInEditMode() ? Color.GRAY : ContextCompat.getColor(context, R.color.pin_normal);
          mColors[1] = colorFocused;
  
          int colorUnfocused = isInEditMode() ? Color.GRAY : ContextCompat.getColor(context, R.color.pin_normal);
          mColors[2] = colorUnfocused;
  
          setBackgroundResource(0);
  
          mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", 4);
          mNumChars = mMaxLength;
  
          //Disable copy paste
          super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
              public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                  return false;
              }
  
              public void onDestroyActionMode(ActionMode mode) {
              }
  
              public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                  return false;
              }
  
              public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                  return false;
              }
          });
          // When tapped, move cursor to end of text.
          super.setOnClickListener(new OnClickListener() {
              @Override
              public void onClick(View v) {
                  setSelection(getText().length());
                  if (mClickListener != null) {
                      mClickListener.onClick(v);
                  }
              }
          });
  
          super.setOnLongClickListener(new OnLongClickListener() {
              @Override
              public boolean onLongClick(View v) {
                  setSelection(getText().length());
                  return true;
              }
          });
  
          //If input type is password and no mask is set, use a default mask
          if ((getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD) == InputType.TYPE_TEXT_VARIATION_PASSWORD && TextUtils.isEmpty(mMask)) {
              mMask = DEFAULT_MASK;
          } else if ((getInputType() & InputType.TYPE_NUMBER_VARIATION_PASSWORD) == InputType.TYPE_NUMBER_VARIATION_PASSWORD && TextUtils.isEmpty(mMask)) {
              mMask = DEFAULT_MASK;
          }
  
          if (!TextUtils.isEmpty(mMask)) {
              mMaskChars = getMaskChars();
          }
  
          //Height of the characters, used if there is a background drawable
          getPaint().getTextBounds("|", 0, 1, mTextHeight);
  
          mAnimate = mAnimatedType > -1;
      }
  
      @Override
      public void setInputType(int type) {
          super.setInputType(type);
  
          if ((type & InputType.TYPE_TEXT_VARIATION_PASSWORD) == InputType.TYPE_TEXT_VARIATION_PASSWORD
                  || (type & InputType.TYPE_NUMBER_VARIATION_PASSWORD) == InputType.TYPE_NUMBER_VARIATION_PASSWORD) {
              // If input type is password and no mask is set, use a default mask
              if (TextUtils.isEmpty(mMask)) {
                  setMask(DEFAULT_MASK);
              }
          } else {
              // If input type is not password, remove mask
              setMask(null);
          }
  
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
          mOriginalTextColors = getTextColors();
          if (mOriginalTextColors != null) {
              mLastCharPaint.setColor(mOriginalTextColors.getDefaultColor());
              mCharPaint.setColor(mOriginalTextColors.getDefaultColor());
              mSingleCharPaint.setColor(getCurrentHintTextColor());
          }
          int availableWidth = getWidth() - ViewCompat.getPaddingEnd(this) - ViewCompat.getPaddingStart(this);
          if (mSpace < 0) {
              mCharSize = (availableWidth / (mNumChars * 2 - 1));
          } else {
              mCharSize = (availableWidth - (mSpace * (mNumChars - 1))) / mNumChars;
          }
          mLineCoords = new RectF[(int) mNumChars];
          mCharBottom = new float[(int) mNumChars];
          int startX;
          int bottom = getHeight() - getPaddingBottom();
          int rtlFlag;
          final boolean isLayoutRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
          if (isLayoutRtl) {
              rtlFlag = -1;
              startX = (int) (getWidth() - ViewCompat.getPaddingStart(this) - mCharSize);
          } else {
              rtlFlag = 1;
              startX = ViewCompat.getPaddingStart(this);
          }
          for (int i = 0; i < mNumChars; i++) {
              mLineCoords[i] = new RectF(startX, bottom, startX + mCharSize, bottom);
              if (mPinBackground != null) {
                  if (mIsDigitSquare) {
                      mLineCoords[i].top = getPaddingTop();
                      mLineCoords[i].right = startX + mLineCoords[i].width();
                  } else {
                      mLineCoords[i].top -= mTextHeight.height() + mTextBottomPadding * 2;
                  }
              }
  
              if (mSpace < 0) {
                  startX += rtlFlag * mCharSize * 2;
              } else {
                  startX += rtlFlag * (mCharSize + mSpace);
              }
              mCharBottom[i] = mLineCoords[i].bottom - mTextBottomPadding;
          }
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          if (mIsDigitSquare) {
              int widthMode = MeasureSpec.getMode(widthMeasureSpec);
              int heightMode = MeasureSpec.getMode(heightMeasureSpec);
              int measuredWidth = 0;
              int measuredHeight = 0;
              // If we want a square or circle pin box, we might be able
              // to figure out the dimensions outselves
              // if width and height are set to wrap_content or match_parent
              if (widthMode == MeasureSpec.EXACTLY) {
                  measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
                  measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
              } else if (heightMode == MeasureSpec.EXACTLY) {
                  measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
                  measuredWidth = (int) ((measuredHeight * mNumChars) + (mSpace * mNumChars - 1));
              } else if (widthMode == MeasureSpec.AT_MOST) {
                  measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
                  measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
              } else if (heightMode == MeasureSpec.AT_MOST) {
                  measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
                  measuredWidth = (int) ((measuredHeight * mNumChars) + (mSpace * mNumChars - 1));
              } else {
                  // Both unspecific
                  // Try for a width based on our minimum
                  measuredWidth = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
  
                  // Whatever the width ends up being, ask for a height that would let the pie
                  // get as big as it can
                  measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
              }
  
              setMeasuredDimension(
                      resolveSizeAndState(measuredWidth, widthMeasureSpec, 1), resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
          } else {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          }
      }
  
      @Override
      public void setOnClickListener(OnClickListener l) {
          mClickListener = l;
      }
  
      @Override
      public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
          throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          //super.onDraw(canvas);
          CharSequence text = getFullText();
          int textLength = text.length();
          float[] textWidths = new float[textLength];
          getPaint().getTextWidths(text, 0, textLength, textWidths);
  
          float hintWidth = 0;
          if (mSingleCharHint != null) {
              float[] hintWidths = new float[mSingleCharHint.length()];
              getPaint().getTextWidths(mSingleCharHint, hintWidths);
              for (float i : hintWidths) {
                  hintWidth += i;
              }
          }
          for (int i = 0; i < mNumChars; i++) {
              //If a background for the pin characters is specified, it should be behind the characters.
              if (mPinBackground != null) {
                  updateDrawableState(i < textLength, i == textLength);
                  mPinBackground.setBounds((int) mLineCoords[i].left, (int) mLineCoords[i].top, (int) mLineCoords[i].right, (int) mLineCoords[i].bottom);
                  mPinBackground.draw(canvas);
              }
              float middle = mLineCoords[i].left + mCharSize / 2;
              if (textLength > i) {
                  if (!mAnimate || i != textLength - 1) {
                      canvas.drawText(text, i, i + 1, middle - textWidths[i] / 2, mCharBottom[i], mCharPaint);
                  } else {
                      canvas.drawText(text, i, i + 1, middle - textWidths[i] / 2, mCharBottom[i], mLastCharPaint);
                  }
              } else if (mSingleCharHint != null) {
                  canvas.drawText(mSingleCharHint, middle - hintWidth / 2, mCharBottom[i], mSingleCharPaint);
              }
              //The lines should be in front of the text (because that's how I want it).
              if (mPinBackground == null) {
                  updateColorForLines(i <= textLength);
                  canvas.drawLine(mLineCoords[i].left, mLineCoords[i].top, mLineCoords[i].right, mLineCoords[i].bottom, mLinesPaint);
              }
          }
      }
  
      private CharSequence getFullText() {
          if (TextUtils.isEmpty(mMask)) {
              return getText();
          } else {
              return getMaskChars();
          }
      }
  
      private StringBuilder getMaskChars() {
          if (mMaskChars == null) {
              mMaskChars = new StringBuilder();
          }
          int textLength = getText().length();
          while (mMaskChars.length() != textLength) {
              if (mMaskChars.length() < textLength) {
                  mMaskChars.append(mMask);
              } else {
                  mMaskChars.deleteCharAt(mMaskChars.length() - 1);
              }
          }
          return mMaskChars;
      }
  
  
      private int getColorForState(int... states) {
          return mColorStates.getColorForState(states, Color.GRAY);
      }
  
      /**
       * @param hasTextOrIsNext Is the color for a character that has been typed or is
       *                        the next character to be typed?
       */
      protected void updateColorForLines(boolean hasTextOrIsNext) {
          if (mHasError) {
              mLinesPaint.setColor(getColorForState(android.R.attr.state_active));
          } else if (isFocused()) {
              mLinesPaint.setStrokeWidth(mLineStrokeSelected);
              mLinesPaint.setColor(getColorForState(android.R.attr.state_focused));
              if (hasTextOrIsNext) {
                  mLinesPaint.setColor(getColorForState(android.R.attr.state_selected));
              }
          } else {
              mLinesPaint.setStrokeWidth(mLineStroke);
              mLinesPaint.setColor(getColorForState(-android.R.attr.state_focused));
          }
      }
  
      protected void updateDrawableState(boolean hasText, boolean isNext) {
          if (mHasError) {
              mPinBackground.setState(new int[]{android.R.attr.state_active});
          } else if (isFocused()) {
              mPinBackground.setState(new int[]{android.R.attr.state_focused});
              if (isNext) {
                  mPinBackground.setState(new int[]{android.R.attr.state_focused, android.R.attr.state_selected});
              } else if (hasText) {
                  mPinBackground.setState(new int[]{android.R.attr.state_focused, android.R.attr.state_checked});
              }
          } else {
              if (hasText) {
                  mPinBackground.setState(new int[]{-android.R.attr.state_focused, android.R.attr.state_checked});
              } else {
                  mPinBackground.setState(new int[]{-android.R.attr.state_focused});
              }
          }
      }
  
      public void setError(boolean hasError) {
          mHasError = hasError;
          invalidate();
      }
  
      public boolean isError() {
          return mHasError;
      }
  
      /**
       * Request focus on this PinEntryEditText
       */
      public void focus() {
          requestFocus();
  
          // Show keyboard
          InputMethodManager inputMethodManager = (InputMethodManager) getContext()
                  .getSystemService(Context.INPUT_METHOD_SERVICE);
          inputMethodManager.showSoftInput(this, 0);
      }
  
      @Override
      public void setTypeface(@Nullable Typeface tf) {
          super.setTypeface(tf);
          setCustomTypeface(tf);
      }
  
      @Override
      public void setTypeface(@Nullable Typeface tf, int style) {
          super.setTypeface(tf, style);
          setCustomTypeface(tf);
      }
  
      private void setCustomTypeface(@Nullable Typeface tf) {
          if (mCharPaint != null) {
              mCharPaint.setTypeface(tf);
              mLastCharPaint.setTypeface(tf);
              mSingleCharPaint.setTypeface(tf);
              mLinesPaint.setTypeface(tf);
          }
      }
  
      public void setPinLineColors(ColorStateList colors) {
          mColorStates = colors;
          invalidate();
      }
  
      public void setPinBackground(Drawable pinBackground) {
          mPinBackground = pinBackground;
          invalidate();
      }
  
      @Override
      protected void onTextChanged(CharSequence text, final int start, int lengthBefore, final int lengthAfter) {
          setError(false);
          if (mLineCoords == null || !mAnimate) {
              if (mOnPinEnteredListener != null && text.length() == mMaxLength) {
                  mOnPinEnteredListener.onPinEntered(text);
              }
              return;
          }
  
          if (mAnimatedType == -1) {
              invalidate();
              return;
          }
  
          if (lengthAfter > lengthBefore) {
              if (mAnimatedType == 0) {
                  animatePopIn();
              } else {
                  animateBottomUp(text, start);
              }
          }
      }
  
      private void animatePopIn() {
          ValueAnimator va = ValueAnimator.ofFloat(1, getPaint().getTextSize());
          va.setDuration(200);
          va.setInterpolator(new OvershootInterpolator());
          va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
              @Override
              public void onAnimationUpdate(ValueAnimator animation) {
                  mLastCharPaint.setTextSize((Float) animation.getAnimatedValue());
                  PinEntryEditText.this.invalidate();
              }
          });
          if (getText().length() == mMaxLength && mOnPinEnteredListener != null) {
              va.addListener(new Animator.AnimatorListener() {
                  @Override
                  public void onAnimationStart(Animator animation) {
                  }
  
                  @Override
                  public void onAnimationEnd(Animator animation) {
                      mOnPinEnteredListener.onPinEntered(getText());
                  }
  
                  @Override
                  public void onAnimationCancel(Animator animation) {
                  }
  
                  @Override
                  public void onAnimationRepeat(Animator animation) {
                  }
              });
          }
          va.start();
      }
  
      private void animateBottomUp(CharSequence text, final int start) {
          mCharBottom[start] = mLineCoords[start].bottom - mTextBottomPadding;
          ValueAnimator animUp = ValueAnimator.ofFloat(mCharBottom[start] + getPaint().getTextSize(), mCharBottom[start]);
          animUp.setDuration(300);
          animUp.setInterpolator(new OvershootInterpolator());
          animUp.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
              @Override
              public void onAnimationUpdate(ValueAnimator animation) {
                  Float value = (Float) animation.getAnimatedValue();
                  mCharBottom[start] = value;
                  PinEntryEditText.this.invalidate();
              }
          });
  
          mLastCharPaint.setAlpha(255);
          ValueAnimator animAlpha = ValueAnimator.ofInt(0, 255);
          animAlpha.setDuration(300);
          animAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
              @Override
              public void onAnimationUpdate(ValueAnimator animation) {
                  Integer value = (Integer) animation.getAnimatedValue();
                  mLastCharPaint.setAlpha(value);
              }
          });
  
          AnimatorSet set = new AnimatorSet();
          if (text.length() == mMaxLength && mOnPinEnteredListener != null) {
              set.addListener(new Animator.AnimatorListener() {
  
                  @Override
                  public void onAnimationStart(Animator animation) {
                  }
  
                  @Override
                  public void onAnimationEnd(Animator animation) {
                      mOnPinEnteredListener.onPinEntered(getText());
                  }
  
                  @Override
                  public void onAnimationCancel(Animator animation) {
                  }
  
                  @Override
                  public void onAnimationRepeat(Animator animation) {
  
                  }
              });
          }
          set.playTogether(animUp, animAlpha);
          set.start();
      }
  
      public void setAnimateText(boolean animate) {
          mAnimate = animate;
      }
  
      public void setOnPinEnteredListener(OnPinEnteredListener l) {
          mOnPinEnteredListener = l;
      }
  
      public interface OnPinEnteredListener {
          void onPinEntered(CharSequence str);
      }
  }