com.mukesh.OtpView.java Source code

Java tutorial

Introduction

Here is the source code for com.mukesh.OtpView.java

Source

/*
 * Copyright 2018 Mukesh Solanki
 *
 * 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.mukesh;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatEditText;
import android.text.InputFilter;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.method.MovementMethod;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;

public class OtpView extends AppCompatEditText {

    private static final boolean DBG = false;
    private static final int BLINK = 500;
    private static final int DEFAULT_COUNT = 4;
    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
    private static final int[] SELECTED_STATE = new int[] { android.R.attr.state_selected };
    private static final int[] FILLED_STATE = new int[] { R.attr.state_filled };
    private static final int VIEW_TYPE_RECTANGLE = 0;
    private static final int VIEW_TYPE_LINE = 1;
    private int viewType;
    private int otpViewItemCount;
    private int otpViewItemWidth;
    private int otpViewItemHeight;
    private int otpViewItemRadius;
    private int otpViewItemSpacing;
    private final Paint paint;
    private final TextPaint animatorTextPaint = new TextPaint();
    private ColorStateList lineColor;
    private int cursorLineColor = Color.BLACK;
    private int lineWidth;
    private final Rect textRect = new Rect();
    private final RectF itemBorderRect = new RectF();
    private final RectF itemLineRect = new RectF();
    private final Path path = new Path();
    private final PointF itemCenterPoint = new PointF();
    private ValueAnimator defaultAddAnimator;
    private boolean isAnimationEnable = false;
    private Blink blink;
    private boolean isCursorVisible;
    private boolean drawCursor;
    private float cursorHeight;
    private int cursorWidth;
    private int cursorColor;
    private int itemBackgroundResource;
    private Drawable itemBackground;
    private boolean hideLineWhenFilled;
    private boolean rtlTextDirection;
    private OnOtpCompletionListener onOtpCompletionListener;

    public OtpView(Context context) {
        this(context, null);
    }

    public OtpView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.otpViewStyle);
    }

    public OtpView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final Resources res = getResources();
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        animatorTextPaint.set(getPaint());
        final Resources.Theme theme = context.getTheme();
        TypedArray typedArray = theme.obtainStyledAttributes(attrs, R.styleable.OtpView, defStyleAttr, 0);
        viewType = typedArray.getInt(R.styleable.OtpView_viewType, VIEW_TYPE_RECTANGLE);
        otpViewItemCount = typedArray.getInt(R.styleable.OtpView_itemCount, DEFAULT_COUNT);
        otpViewItemHeight = (int) typedArray.getDimension(R.styleable.OtpView_itemHeight,
                res.getDimensionPixelSize(R.dimen.otp_view_item_size));
        otpViewItemWidth = (int) typedArray.getDimension(R.styleable.OtpView_itemWidth,
                res.getDimensionPixelSize(R.dimen.otp_view_item_size));
        otpViewItemSpacing = typedArray.getDimensionPixelSize(R.styleable.OtpView_itemSpacing,
                res.getDimensionPixelSize(R.dimen.otp_view_item_spacing));
        otpViewItemRadius = (int) typedArray.getDimension(R.styleable.OtpView_itemRadius, 0);
        lineWidth = (int) typedArray.getDimension(R.styleable.OtpView_lineWidth,
                res.getDimensionPixelSize(R.dimen.otp_view_item_line_width));
        lineColor = typedArray.getColorStateList(R.styleable.OtpView_lineColor);
        isCursorVisible = typedArray.getBoolean(R.styleable.OtpView_android_cursorVisible, true);
        cursorColor = typedArray.getColor(R.styleable.OtpView_cursorColor, getCurrentTextColor());
        cursorWidth = typedArray.getDimensionPixelSize(R.styleable.OtpView_cursorWidth,
                res.getDimensionPixelSize(R.dimen.otp_view_cursor_width));
        itemBackground = typedArray.getDrawable(R.styleable.OtpView_android_itemBackground);
        hideLineWhenFilled = typedArray.getBoolean(R.styleable.OtpView_hideLineWhenFilled, false);
        rtlTextDirection = typedArray.getBoolean(R.styleable.OtpView_rtlTextDirection, false);
        typedArray.recycle();
        if (lineColor != null) {
            cursorLineColor = lineColor.getDefaultColor();
        }
        updateCursorHeight();
        checkItemRadius();
        setMaxLength(otpViewItemCount);
        paint.setStrokeWidth(lineWidth);
        setupAnimator();
        super.setCursorVisible(false);
        setTextIsSelectable(false);
    }

    @Override
    public void setTypeface(Typeface tf, int style) {
        super.setTypeface(tf, style);
    }

    @Override
    public void setTypeface(Typeface tf) {
        super.setTypeface(tf);
        if (animatorTextPaint != null) {
            animatorTextPaint.set(getPaint());
        }
    }

    private void setMaxLength(int maxLength) {
        setFilters(maxLength >= 0 ? new InputFilter[] { new InputFilter.LengthFilter(maxLength) } : NO_FILTERS);
    }

    private void setupAnimator() {
        defaultAddAnimator = ValueAnimator.ofFloat(0.5f, 1f);
        defaultAddAnimator.setDuration(150);
        defaultAddAnimator.setInterpolator(new DecelerateInterpolator());
        defaultAddAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float scale = (float) animation.getAnimatedValue();
                int alpha = (int) (255 * scale);
                animatorTextPaint.setTextSize(getTextSize() * scale);
                animatorTextPaint.setAlpha(alpha);
                postInvalidate();
            }
        });
    }

    private void checkItemRadius() {
        if (viewType == VIEW_TYPE_LINE) {
            float halfOfLineWidth = ((float) lineWidth) / 2;
            if (otpViewItemRadius > halfOfLineWidth) {
                throw new IllegalArgumentException(
                        "The itemRadius can not be greater than lineWidth when viewType is line");
            }
        } else if (viewType == VIEW_TYPE_RECTANGLE) {
            float halfOfItemWidth = ((float) otpViewItemWidth) / 2;
            if (otpViewItemRadius > halfOfItemWidth) {
                throw new IllegalArgumentException("The itemRadius can not be greater than itemWidth");
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        int boxHeight = otpViewItemHeight;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            int boxesWidth = (otpViewItemCount - 1) * otpViewItemSpacing + otpViewItemCount * otpViewItemWidth;
            width = boxesWidth + ViewCompat.getPaddingEnd(this) + ViewCompat.getPaddingStart(this);
            if (otpViewItemSpacing == 0) {
                width -= (otpViewItemCount - 1) * lineWidth;
            }
        }
        height = heightMode == MeasureSpec.EXACTLY ? heightSize : boxHeight + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        if (start != text.length()) {
            moveSelectionToEnd();
        }
        if (text.length() == otpViewItemCount && onOtpCompletionListener != null) {
            onOtpCompletionListener.onOtpCompleted(text.toString());
        }
        makeBlink();
        if (isAnimationEnable) {
            final boolean isAdd = lengthAfter - lengthBefore > 0;
            if (isAdd && defaultAddAnimator != null) {
                defaultAddAnimator.end();
                defaultAddAnimator.start();
            }
        }
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        if (focused) {
            moveSelectionToEnd();
            makeBlink();
        }
    }

    @Override
    protected void onSelectionChanged(int selStart, int selEnd) {
        super.onSelectionChanged(selStart, selEnd);
        if (getText() != null && selEnd != getText().length()) {
            moveSelectionToEnd();
        }
    }

    private void moveSelectionToEnd() {
        if (getText() != null) {
            setSelection(getText().length());
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if (lineColor == null || lineColor.isStateful()) {
            updateColors();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        updatePaints();
        drawOtpView(canvas);
        canvas.restore();
    }

    private void updatePaints() {
        paint.setColor(cursorLineColor);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(lineWidth);
        getPaint().setColor(getCurrentTextColor());
    }

    private void drawOtpView(Canvas canvas) {
        int nextItemToFill;
        if (rtlTextDirection) {
            nextItemToFill = otpViewItemCount - 1;
        } else {
            if (getText() != null) {
                nextItemToFill = getText().length();
            } else {
                nextItemToFill = 0;
            }
        }
        for (int i = 0; i < otpViewItemCount; i++) {
            boolean itemSelected = isFocused() && nextItemToFill == i;
            boolean itemFilled = i < nextItemToFill;
            int[] itemState = null;
            if (itemFilled) {
                itemState = FILLED_STATE;
            } else if (itemSelected) {
                itemState = SELECTED_STATE;
            }
            paint.setColor(itemState != null ? getLineColorForState(itemState) : cursorLineColor);
            updateItemRectF(i);
            updateCenterPoint();
            canvas.save();
            if (viewType == VIEW_TYPE_RECTANGLE) {
                updateOtpViewBoxPath(i);
                canvas.clipPath(path);
            }
            drawItemBackground(canvas, itemState);
            canvas.restore();
            if (itemSelected) {
                drawCursor(canvas);
            }
            if (viewType == VIEW_TYPE_RECTANGLE) {
                drawOtpBox(canvas, i);
            } else if (viewType == VIEW_TYPE_LINE) {
                drawOtpLine(canvas, i);
            }
            if (DBG) {
                drawAnchorLine(canvas);
            }
            if (rtlTextDirection) {
                int reversedPosition = otpViewItemCount - i;
                if (getText().length() >= reversedPosition) {
                    drawInput(canvas, i);
                } else if (!TextUtils.isEmpty(getHint()) && getHint().length() == otpViewItemCount) {
                    drawHint(canvas, i);
                }
            } else {
                if (getText().length() > i) {
                    drawInput(canvas, i);
                } else if (!TextUtils.isEmpty(getHint()) && getHint().length() == otpViewItemCount) {
                    drawHint(canvas, i);
                }
            }
        }
        if (isFocused() && getText() != null && getText().length() != otpViewItemCount
                && viewType == VIEW_TYPE_RECTANGLE) {
            int index = getText().length();
            updateItemRectF(index);
            updateCenterPoint();
            updateOtpViewBoxPath(index);
            paint.setColor(getLineColorForState(SELECTED_STATE));
            drawOtpBox(canvas, index);
        }
    }

    private void drawInput(Canvas canvas, int i) {
        if (isPasswordInputType(getInputType())) {
            drawCircle(canvas, i);
        } else {
            drawText(canvas, i);
        }
    }

    private int getLineColorForState(int... states) {
        return lineColor != null ? lineColor.getColorForState(states, cursorLineColor) : cursorLineColor;
    }

    private void drawItemBackground(Canvas canvas, int[] backgroundState) {
        if (itemBackground == null) {
            return;
        }
        float delta = (float) lineWidth / 2;
        int left = Math.round(itemBorderRect.left - delta);
        int top = Math.round(itemBorderRect.top - delta);
        int right = Math.round(itemBorderRect.right + delta);
        int bottom = Math.round(itemBorderRect.bottom + delta);
        itemBackground.setBounds(left, top, right, bottom);
        itemBackground.setState(backgroundState != null ? backgroundState : getDrawableState());
        itemBackground.draw(canvas);
    }

    private void updateOtpViewBoxPath(int i) {
        boolean drawRightCorner = false;
        boolean drawLeftCorner = false;
        if (otpViewItemSpacing != 0) {
            drawLeftCorner = drawRightCorner = true;
        } else {
            if (i == 0 && i != otpViewItemCount - 1) {
                drawLeftCorner = true;
            }
            if (i == otpViewItemCount - 1 && i != 0) {
                drawRightCorner = true;
            }
        }
        updateRoundRectPath(itemBorderRect, otpViewItemRadius, otpViewItemRadius, drawLeftCorner, drawRightCorner);
    }

    private void drawOtpBox(Canvas canvas, int i) {
        if (getText() != null && hideLineWhenFilled && i < getText().length()) {
            return;
        }
        canvas.drawPath(path, paint);
    }

    private void drawOtpLine(Canvas canvas, int i) {
        if (getText() != null && hideLineWhenFilled && i < getText().length()) {
            return;
        }
        boolean drawLeft;
        boolean drawRight;
        drawLeft = drawRight = true;
        if (otpViewItemSpacing == 0 && otpViewItemCount > 1) {
            if (i == 0) {
                drawRight = false;
            } else if (i == otpViewItemCount - 1) {
                drawLeft = false;
            } else {
                drawLeft = drawRight = false;
            }
        }
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(((float) lineWidth) / 10);
        float halfLineWidth = ((float) lineWidth) / 2;
        itemLineRect.set(itemBorderRect.left - halfLineWidth, itemBorderRect.bottom - halfLineWidth,
                itemBorderRect.right + halfLineWidth, itemBorderRect.bottom + halfLineWidth);

        updateRoundRectPath(itemLineRect, otpViewItemRadius, otpViewItemRadius, drawLeft, drawRight);
        canvas.drawPath(path, paint);
    }

    private void drawCursor(Canvas canvas) {
        if (drawCursor) {
            float cx = itemCenterPoint.x;
            float cy = itemCenterPoint.y;
            float y = cy - cursorHeight / 2;
            int color = paint.getColor();
            float width = paint.getStrokeWidth();
            paint.setColor(cursorColor);
            paint.setStrokeWidth(cursorWidth);
            canvas.drawLine(cx, y, cx, y + cursorHeight, paint);
            paint.setColor(color);
            paint.setStrokeWidth(width);
        }
    }

    private void updateRoundRectPath(RectF rectF, float rx, float ry, boolean l, boolean r) {
        updateRoundRectPath(rectF, rx, ry, l, r, r, l);
    }

    private void updateRoundRectPath(RectF rectF, float rx, float ry, boolean tl, boolean tr, boolean br,
            boolean bl) {
        path.reset();
        float l = rectF.left;
        float t = rectF.top;
        float r = rectF.right;
        float b = rectF.bottom;
        float w = r - l;
        float h = b - t;
        float lw = w - 2 * rx;
        float lh = h - 2 * ry;
        path.moveTo(l, t + ry);
        if (tl) {
            path.rQuadTo(0, -ry, rx, -ry);
        } else {
            path.rLineTo(0, -ry);
            path.rLineTo(rx, 0);
        }
        path.rLineTo(lw, 0);
        if (tr) {
            path.rQuadTo(rx, 0, rx, ry);
        } else {
            path.rLineTo(rx, 0);
            path.rLineTo(0, ry);
        }
        path.rLineTo(0, lh);
        if (br) {
            path.rQuadTo(0, ry, -rx, ry);
        } else {
            path.rLineTo(0, ry);
            path.rLineTo(-rx, 0);
        }
        path.rLineTo(-lw, 0);
        if (bl) {
            path.rQuadTo(-rx, 0, -rx, -ry);
        } else {
            path.rLineTo(-rx, 0);
            path.rLineTo(0, -ry);
        }
        path.rLineTo(0, -lh);
        path.close();
    }

    private void updateItemRectF(int i) {
        float halfLineWidth = ((float) lineWidth) / 2;
        float left = getScrollX() + ViewCompat.getPaddingStart(this) + i * (otpViewItemSpacing + otpViewItemWidth)
                + halfLineWidth;
        if (otpViewItemSpacing == 0 && i > 0) {
            left = left - (lineWidth) * i;
        }
        float right = left + otpViewItemWidth - lineWidth;
        float top = getScrollY() + getPaddingTop() + halfLineWidth;
        float bottom = top + otpViewItemHeight - lineWidth;
        itemBorderRect.set(left, top, right, bottom);
    }

    private void drawText(Canvas canvas, int i) {
        Paint paint = getPaintByIndex(i);
        paint.setColor(getCurrentTextColor());
        if (rtlTextDirection) {
            int reversedPosition = otpViewItemCount - i;
            int reversedCharPosition;
            if (getText() == null) {
                reversedCharPosition = reversedPosition;
            } else {
                reversedCharPosition = reversedPosition - getText().length();
            }
            if (reversedCharPosition <= 0 && getText() != null) {
                drawTextAtBox(canvas, paint, getText(), Math.abs(reversedCharPosition));
            }
        } else if (getText() != null) {
            drawTextAtBox(canvas, paint, getText(), i);
        }
    }

    private void drawHint(Canvas canvas, int i) {
        Paint paint = getPaintByIndex(i);
        paint.setColor(getCurrentHintTextColor());
        if (rtlTextDirection) {
            int reversedPosition = otpViewItemCount - i;
            int reversedCharPosition = reversedPosition - getHint().length();
            if (reversedCharPosition <= 0) {
                drawTextAtBox(canvas, paint, getHint(), Math.abs(reversedCharPosition));
            }
        } else {
            drawTextAtBox(canvas, paint, getHint(), i);
        }
    }

    private void drawTextAtBox(Canvas canvas, Paint paint, CharSequence text, int charAt) {
        paint.getTextBounds(text.toString(), charAt, charAt + 1, textRect);
        float cx = itemCenterPoint.x;
        float cy = itemCenterPoint.y;
        float x = cx - Math.abs((float) textRect.width()) / 2 - textRect.left;
        float y = cy + Math.abs((float) textRect.height()) / 2 - textRect.bottom;
        canvas.drawText(text, charAt, charAt + 1, x, y, paint);
    }

    private void drawCircle(Canvas canvas, int i) {
        Paint paint = getPaintByIndex(i);
        float cx = itemCenterPoint.x;
        float cy = itemCenterPoint.y;
        if (rtlTextDirection) {
            int reversedItemPosition = otpViewItemCount - i;
            int reversedCharPosition = reversedItemPosition - getHint().length();
            if (reversedCharPosition <= 0) {
                canvas.drawCircle(cx, cy, paint.getTextSize() / 2, paint);
            }
        } else {
            canvas.drawCircle(cx, cy, paint.getTextSize() / 2, paint);
        }
    }

    private Paint getPaintByIndex(int i) {
        if (getText() != null && isAnimationEnable && i == getText().length() - 1) {
            animatorTextPaint.setColor(getPaint().getColor());
            return animatorTextPaint;
        } else {
            return getPaint();
        }
    }

    private void drawAnchorLine(Canvas canvas) {
        float cx = itemCenterPoint.x;
        float cy = itemCenterPoint.y;
        paint.setStrokeWidth(1);
        cx -= paint.getStrokeWidth() / 2;
        cy -= paint.getStrokeWidth() / 2;
        path.reset();
        path.moveTo(cx, itemBorderRect.top);
        path.lineTo(cx, itemBorderRect.top + Math.abs(itemBorderRect.height()));
        canvas.drawPath(path, paint);
        path.reset();
        path.moveTo(itemBorderRect.left, cy);
        path.lineTo(itemBorderRect.left + Math.abs(itemBorderRect.width()), cy);
        canvas.drawPath(path, paint);
        path.reset();
        paint.setStrokeWidth(lineWidth);
    }

    private void updateColors() {
        boolean shouldInvalidate = false;
        int color = lineColor != null ? lineColor.getColorForState(getDrawableState(), 0) : getCurrentTextColor();
        if (color != cursorLineColor) {
            cursorLineColor = color;
            shouldInvalidate = true;
        }
        if (shouldInvalidate) {
            invalidate();
        }
    }

    private void updateCenterPoint() {
        float cx = itemBorderRect.left + Math.abs(itemBorderRect.width()) / 2;
        float cy = itemBorderRect.top + Math.abs(itemBorderRect.height()) / 2;
        itemCenterPoint.set(cx, cy);
    }

    private static boolean isPasswordInputType(int inputType) {
        final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
        return variation == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
                || variation == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
                || variation == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
    }

    @Override
    protected MovementMethod getDefaultMovementMethod() {
        return DefaultMovementMethod.getInstance();
    }

    /**
     * Sets the line color for all the states (normal, selected,
     * focused) to be this color.
     *
     * @param color A color value in the form 0xAARRGGBB.
     * Do not pass a resource ID. To get a color value from a resource ID, call
     * {@link android.support.v4.content.ContextCompat#getColor(Context, int) getColor}.
     * @attr ref R.styleable#OtpView_lineColor
     * @see #setLineColor(ColorStateList)
     * @see #getLineColors()
     */
    public void setLineColor(@ColorInt int color) {
        lineColor = ColorStateList.valueOf(color);
        updateColors();
    }

    /**
     * Sets the line color.
     *
     * @attr ref R.styleable#OtpView_lineColor
     * @see #setLineColor(int)
     * @see #getLineColors()
     */
    public void setLineColor(ColorStateList colors) {
        if (colors == null) {
            throw new IllegalArgumentException("Color cannot be null");
        }

        lineColor = colors;
        updateColors();
    }

    /**
     * Gets the line colors for the different states (normal, selected, focused) of the OtpView.
     *
     * @attr ref R.styleable#OtpView_lineColor
     * @see #setLineColor(ColorStateList)
     * @see #setLineColor(int)
     */
    public ColorStateList getLineColors() {
        return lineColor;
    }

    /**
     * <p>Return the current color selected for normal line.</p>
     *
     * @return Returns the current item's line color.
     */
    @ColorInt
    public int getCurrentLineColor() {
        return cursorLineColor;
    }

    /**
     * Sets the line width.
     *
     * @attr ref R.styleable#OtpView_lineWidth
     * @see #getLineWidth()
     */
    public void setLineWidth(@Px int borderWidth) {
        lineWidth = borderWidth;
        checkItemRadius();
        requestLayout();
    }

    /**
     * @return Returns the width of the item's line.
     * @see #setLineWidth(int)
     */
    public int getLineWidth() {
        return lineWidth;
    }

    /**
     * Sets the count of items.
     *
     * @attr ref R.styleable#OtpView_itemCount
     * @see #getItemCount()
     */
    public void setItemCount(int count) {
        otpViewItemCount = count;
        setMaxLength(count);
        requestLayout();
    }

    /**
     * @return Returns the count of items.
     * @see #setItemCount(int)
     */
    public int getItemCount() {
        return otpViewItemCount;
    }

    /**
     * Sets the radius of square.
     *
     * @attr ref R.styleable#OtpView_itemRadius
     * @see #getItemRadius()
     */
    public void setItemRadius(@Px int itemRadius) {
        otpViewItemRadius = itemRadius;
        checkItemRadius();
        requestLayout();
    }

    /**
     * @return Returns the radius of square.
     * @see #setItemRadius(int)
     */
    public int getItemRadius() {
        return otpViewItemRadius;
    }

    /**
     * Specifies extra space between two items.
     *
     * @attr ref R.styleable#OtpView_itemSpacing
     * @see #getItemSpacing()
     */
    public void setItemSpacing(@Px int itemSpacing) {
        otpViewItemSpacing = itemSpacing;
        requestLayout();
    }

    /**
     * @return Returns the spacing between two items.
     * @see #setItemSpacing(int)
     */
    @Px
    public int getItemSpacing() {
        return otpViewItemSpacing;
    }

    /**
     * Sets the height of item.
     *
     * @attr ref R.styleable#OtpView_itemHeight
     * @see #getItemHeight()
     */
    public void setItemHeight(@Px int itemHeight) {
        otpViewItemHeight = itemHeight;
        updateCursorHeight();
        requestLayout();
    }

    /**
     * @return Returns the height of item.
     * @see #setItemHeight(int)
     */
    public int getItemHeight() {
        return otpViewItemHeight;
    }

    /**
     * Sets the width of item.
     *
     * @attr ref R.styleable#OtpView_itemWidth
     * @see #getItemWidth()
     */
    public void setItemWidth(@Px int itemWidth) {
        otpViewItemWidth = itemWidth;
        checkItemRadius();
        requestLayout();
    }

    /**
     * @return Returns the width of item.
     * @see #setItemWidth(int)
     */
    public int getItemWidth() {
        return otpViewItemWidth;
    }

    /**
     * Specifies whether the text animation should be enabled or disabled.
     * By the default, the animation is disabled.
     *
     * @param enable True to start animation when adding text, false to transition immediately
     */
    public void setAnimationEnable(boolean enable) {
        isAnimationEnable = enable;
    }

    /**
     * Specifies whether the line (border) should be hidden or visible when text entered.
     * By the default, this flag is false and the line is always drawn.
     *
     * @param hideLineWhenFilled true to hide line on a position where text entered,
     * false to always show line
     * @attr ref R.styleable#OtpView_hideLineWhenFilled
     */
    public void setHideLineWhenFilled(boolean hideLineWhenFilled) {
        this.hideLineWhenFilled = hideLineWhenFilled;
    }

    @Override
    public void setTextSize(float size) {
        super.setTextSize(size);
        updateCursorHeight();
    }

    @Override
    public void setTextSize(int unit, float size) {
        super.setTextSize(unit, size);
        updateCursorHeight();
    }

    public void setOtpCompletionListener(OnOtpCompletionListener otpCompletionListener) {
        this.onOtpCompletionListener = otpCompletionListener;
    }

    //region ItemBackground

    /**
     * Set the item background to a given resource. The resource should refer to
     * a Drawable object or 0 to remove the item background.
     *
     * @param resId The identifier of the resource.
     * @attr ref R.styleable#OtpView_android_itemBackground
     */
    public void setItemBackgroundResources(@DrawableRes int resId) {
        if (resId != 0 && itemBackgroundResource != resId) {
            return;
        }
        itemBackground = ResourcesCompat.getDrawable(getResources(), resId, getContext().getTheme());
        setItemBackground(itemBackground);
        itemBackgroundResource = resId;
    }

    /**
     * Sets the item background color for this view.
     *
     * @param color the color of the item background
     */
    public void setItemBackgroundColor(@ColorInt int color) {
        if (itemBackground instanceof ColorDrawable) {
            ((ColorDrawable) itemBackground.mutate()).setColor(color);
            itemBackgroundResource = 0;
        } else {
            setItemBackground(new ColorDrawable(color));
        }
    }

    /**
     * Set the item background to a given Drawable, or remove the background.
     *
     * @param background The Drawable to use as the item background, or null to remove the
     * item background
     */
    public void setItemBackground(Drawable background) {
        itemBackgroundResource = 0;
        itemBackground = background;
        invalidate();
    }
    //endregion

    //region Cursor

    /**
     * Sets the width (in pixels) of cursor.
     *
     * @attr ref R.styleable#OtpView_cursorWidth
     * @see #getCursorWidth()
     */
    public void setCursorWidth(@Px int width) {
        cursorWidth = width;
        if (isCursorVisible()) {
            invalidateCursor(true);
        }
    }

    /**
     * @return Returns the width (in pixels) of cursor.
     * @see #setCursorWidth(int)
     */
    public int getCursorWidth() {
        return cursorWidth;
    }

    /**
     * Sets the cursor color.
     *
     * @param color A color value in the form 0xAARRGGBB.
     * Do not pass a resource ID. To get a color value from a resource ID, call
     * {@link android.support.v4.content.ContextCompat#getColor(Context, int) getColor}.
     * @attr ref R.styleable#OtpView_cursorColor
     * @see #getCursorColor()
     */
    public void setCursorColor(@ColorInt int color) {
        cursorColor = color;
        if (isCursorVisible()) {
            invalidateCursor(true);
        }
    }

    /**
     * Gets the cursor color.
     *
     * @return Return current cursor color.
     * @see #setCursorColor(int)
     */
    public int getCursorColor() {
        return cursorColor;
    }

    @Override
    public void setCursorVisible(boolean visible) {
        if (isCursorVisible != visible) {
            isCursorVisible = visible;
            invalidateCursor(isCursorVisible);
            makeBlink();
        }
    }

    @Override
    public boolean isCursorVisible() {
        return isCursorVisible;
    }

    @Override
    public void onScreenStateChanged(int screenState) {
        super.onScreenStateChanged(screenState);
        if (screenState == View.SCREEN_STATE_ON) {
            resumeBlink();
        } else if (screenState == View.SCREEN_STATE_OFF) {
            suspendBlink();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        resumeBlink();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        suspendBlink();
    }

    private boolean shouldBlink() {
        return isCursorVisible() && isFocused();
    }

    private void makeBlink() {
        if (shouldBlink()) {
            if (blink == null) {
                blink = new Blink();
            }
            removeCallbacks(blink);
            drawCursor = false;
            postDelayed(blink, BLINK);
        } else {
            if (blink != null) {
                removeCallbacks(blink);
            }
        }
    }

    private void suspendBlink() {
        if (blink != null) {
            blink.cancel();
            invalidateCursor(false);
        }
    }

    private void resumeBlink() {
        if (blink != null) {
            blink.unCancel();
            makeBlink();
        }
    }

    private void invalidateCursor(boolean showCursor) {
        if (drawCursor != showCursor) {
            drawCursor = showCursor;
            invalidate();
        }
    }

    private void updateCursorHeight() {
        int delta = 2 * dpToPx();
        cursorHeight = otpViewItemHeight - getTextSize() > delta ? getTextSize() + delta : getTextSize();
    }

    private class Blink implements Runnable {
        private boolean cancelled;

        @Override
        public void run() {
            if (cancelled) {
                return;
            }

            removeCallbacks(this);

            if (shouldBlink()) {
                invalidateCursor(!drawCursor);
                postDelayed(this, BLINK);
            }
        }

        private void cancel() {
            if (!cancelled) {
                removeCallbacks(this);
                cancelled = true;
            }
        }

        private void unCancel() {
            cancelled = false;
        }
    }
    //endregion

    private int dpToPx() {
        return (int) ((float) 2 * getResources().getDisplayMetrics().density + 0.5f);
    }
}