me.sweetll.tucao.widget.PasswordEntry.java Source code

Java tutorial

Introduction

Here is the source code for me.sweetll.tucao.widget.PasswordEntry.java

Source

/*
 * Copyright 2016 Google Inc.
 *
 * 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 me.sweetll.tucao.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.support.design.widget.TextInputEditText;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextPaint;
import android.text.method.PasswordTransformationMethod;
import android.util.AttributeSet;
import android.view.animation.Interpolator;

import me.sweetll.tucao.R;
import me.sweetll.tucao.util.AnimUtils;

import static me.sweetll.tucao.util.AnimUtils.lerp;

/**
 * A password entry widget which animates switching between masked and visible text.
 */
public class PasswordEntry extends TextInputEditText {

    static final char[] PASSWORD_MASK = { '\u2022' }; // PasswordTransformationMethod#DOT

    private boolean passwordMasked = false;
    private MaskMorphDrawable maskDrawable;
    private ColorStateList textColor;

    public PasswordEntry(Context context) {
        super(context);
        passwordMasked = getTransformationMethod() instanceof PasswordTransformationMethod;
    }

    public PasswordEntry(Context context, AttributeSet attrs) {
        super(context, attrs);
        passwordMasked = getTransformationMethod() instanceof PasswordTransformationMethod;
    }

    public PasswordEntry(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        passwordMasked = getTransformationMethod() instanceof PasswordTransformationMethod;
    }

    /**
     * Want to monitor when password mode is set but #setTransformationMethod is final :( Instead
     * override #setText (which it calls through to) & check the transformation method.
     */
    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        boolean isMasked = getTransformationMethod() instanceof PasswordTransformationMethod;
        if (isMasked != passwordMasked) {
            passwordMasked = isMasked;
            passwordVisibilityToggled(isMasked, text);
        }
    }

    @Override
    public void setTextColor(ColorStateList colors) {
        super.setTextColor(colors);
        textColor = colors;
    }

    private void passwordVisibilityToggled(boolean isMasked, CharSequence password) {
        if (maskDrawable == null) {
            // lazily create the drawable that morphs the dots
            if (!ViewCompat.isLaidOut(this) || getText().length() < 1)
                return;
            maskDrawable = new MaskMorphDrawable(getContext(), getPaint(), getBaseline(),
                    getLayout().getPrimaryHorizontal(1), getTextLeft());
            maskDrawable.setBounds(getPaddingLeft(), getPaddingTop(), getPaddingLeft(),
                    getHeight() - getPaddingBottom());
            getOverlay().add(maskDrawable);
        }

        // hide the text during the animation
        setTextColor(Color.TRANSPARENT);
        Animator maskMorph = isMasked ? maskDrawable.createShowMaskAnimator(password)
                : maskDrawable.createHideMaskAnimator(password);
        maskMorph.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                setTextColor(textColor); // restore the proper text color
            }
        });
        maskMorph.start();
    }

    private int getTextLeft() {
        int left = 0;
        if (getBackground() instanceof InsetDrawable) {
            InsetDrawable back = (InsetDrawable) getBackground();
            Rect padding = new Rect();
            back.getPadding(padding);
            left = padding.left;
        }
        return left;
    }

    /**
     * A drawable for animating the switch between a masked and visible password field.
     */
    static class MaskMorphDrawable extends Drawable {

        private static final float NO_PROGRESS = -1f;
        private static final float PROGRESS_CHARACTER = 0f;
        private static final float PROGRESS_MASK = 1f;

        private final TextPaint paint;
        private final float charWidth;
        private final float maskDiameter;
        private final float maskCenterY;
        private final float insetStart;
        private final int baseline;
        private final long showPasswordDuration;
        private final long hidePasswordDuration;
        private final Interpolator fastOutSlowIn;

        private CharSequence password;
        private PasswordCharacter[] characters;
        private float morphProgress;

        MaskMorphDrawable(Context context, TextPaint textPaint, int baseline, float charWidth, int insetStart) {
            this.insetStart = insetStart;
            this.baseline = baseline;
            this.charWidth = charWidth;
            paint = new TextPaint(textPaint);
            Rect maskBounds = new Rect();
            paint.getTextBounds(PASSWORD_MASK, 0, 1, maskBounds);
            maskDiameter = maskBounds.height();
            maskCenterY = (maskBounds.top + maskBounds.bottom) / 2f;
            showPasswordDuration = context.getResources().getInteger(R.integer.show_password_duration);
            hidePasswordDuration = context.getResources().getInteger(R.integer.hide_password_duration);
            fastOutSlowIn = new FastOutSlowInInterpolator();
        }

        Animator createShowMaskAnimator(CharSequence password) {
            return morphPassword(password, PROGRESS_CHARACTER, PROGRESS_MASK, hidePasswordDuration);
        }

        Animator createHideMaskAnimator(CharSequence password) {
            return morphPassword(password, PROGRESS_MASK, PROGRESS_CHARACTER, showPasswordDuration);
        }

        @Override
        public void draw(Canvas canvas) {
            if (characters != null && morphProgress != NO_PROGRESS) {
                final int saveCount = canvas.save();
                canvas.translate(insetStart, baseline);
                for (int i = 0; i < characters.length; i++) {
                    characters[i].draw(canvas, paint, password, i, charWidth, morphProgress);
                }
                canvas.restoreToCount(saveCount);
            }
        }

        @Override
        public void setAlpha(int alpha) {
            if (alpha != paint.getAlpha()) {
                paint.setAlpha(alpha);
                invalidateSelf();
            }
        }

        @Override
        public void setColorFilter(ColorFilter colorFilter) {
            paint.setColorFilter(colorFilter);
        }

        @Override
        public int getOpacity() {
            return PixelFormat.TRANSLUCENT;
        }

        private Animator morphPassword(CharSequence pw, float fromProgress, float toProgress, long duration) {
            password = pw;
            updateBounds();
            characters = new PasswordCharacter[pw.length()];
            String passStr = pw.toString();
            for (int i = 0; i < pw.length(); i++) {
                characters[i] = new PasswordCharacter(passStr, i, paint, maskDiameter, maskCenterY);
            }

            ValueAnimator anim = ValueAnimator.ofFloat(fromProgress, toProgress);
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    morphProgress = (float) valueAnimator.getAnimatedValue();
                    invalidateSelf();
                }
            });

            anim.setDuration(duration);
            anim.setInterpolator(fastOutSlowIn);
            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    characters = null;
                    morphProgress = NO_PROGRESS;
                    password = null;
                    updateBounds();
                    invalidateSelf();
                }
            });
            return anim;
        }

        private void updateBounds() {
            Rect oldBounds = getBounds();
            if (password != null) {
                setBounds(oldBounds.left, oldBounds.top,
                        oldBounds.left + (int) Math.ceil(password.length() * charWidth), oldBounds.bottom);
            } else {
                setBounds(oldBounds.left, oldBounds.top, oldBounds.left, oldBounds.bottom);
            }
        }

    }

    /**
     * Models a character in a password, holding info about it's drawing bounds and how it should
     * move/scale to morph to/from the password mask.
     */
    static class PasswordCharacter {

        private final Rect bounds = new Rect();
        private final float textToMaskScale;
        private final float maskToTextScale;
        private final float textOffsetY;

        PasswordCharacter(String password, int index, TextPaint paint, float maskCharDiameter, float maskCenterY) {
            paint.getTextBounds(password, index, index + 1, bounds);
            // scale the mask from the character width, down to it's own width
            maskToTextScale = Math.max(1f, bounds.width() / maskCharDiameter);
            // scale text from it's height down to the mask character height
            textToMaskScale = Math.min(0f, 1f / (bounds.height() / maskCharDiameter));
            // difference between mask & character center
            textOffsetY = maskCenterY - bounds.exactCenterY();
        }

        /**
         * Progress through the morph:  0 = character, 1 = 
         */
        void draw(Canvas canvas, TextPaint paint, CharSequence password, int index, float charWidth,
                float progress) {
            int alpha = paint.getAlpha();
            float x = charWidth * index;

            // draw the character
            canvas.save();
            float textScale = lerp(1f, textToMaskScale, progress);
            // scale character: shrinks to/grows from the mask's height
            canvas.scale(textScale, textScale, x + bounds.exactCenterX(), bounds.exactCenterY());
            // cross fade between the character/mask
            paint.setAlpha((int) lerp(alpha, 0, progress));
            // vertically move the character center toward/from the mask center
            canvas.drawText(password, index, index + 1, x, lerp(0f, textOffsetY, progress) / textScale, paint);
            canvas.restore();

            // draw the mask
            canvas.save();
            float maskScale = lerp(maskToTextScale, 1f, progress);
            // scale the mask: down from/up to the character width
            canvas.scale(maskScale, maskScale, x + bounds.exactCenterX(), bounds.exactCenterY());
            // cross fade between the mask/character
            paint.setAlpha((int) lerp(0, alpha, progress));
            // vertically move the mask center from/toward the character center
            canvas.drawText(PASSWORD_MASK, 0, 1, x, -lerp(textOffsetY, 0f, progress), paint);
            canvas.restore();

            // restore the paint to how we found it
            paint.setAlpha(alpha);
        }

    }
}