欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择

程序员文章站 2022-07-03 20:20:00
Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择。 这是一个自定义EditText,带一键清除和密码明文切换按钮(可以传入自定义图片资源),可...

Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择。

这是一个自定义EditText,带一键清除和密码明文切换按钮(可以传入自定义图片资源),可以自定义边框颜色,还支持四种边框样式的选择。

源码已上传 GitHub:点击打开链接 欢迎fork,start和批评指正哈。

效果图镇楼。

Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择

下面撸袖子开讲了。从构造方法开始把,一般情况下构造方法三连击就够用了。因为这里有自定义的 attrs 值,所以这里用到了二个参数的构造方法。

首先初始化画笔,抗锯齿就不说了,说下Paint.FILTER_BITMAP_FLAG 这个属性,它表示用双线性过滤来绘制Bitmap,有啥用呢? 图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让图像绘制出来时显得更加平滑。

接下来是自定义属性的初始化,如果在布局xml中有指定自定义属性的值,那么在这里会被读取。如果没有指定,那么会使用默认的值。

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

    public PowerfulEditText(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public PowerfulEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //抗锯齿和位图滤波
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

        //读取xml文件中的配置
        if (attrs != null) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PowerfulEditText);
            for (int i = 0; i < array.getIndexCount(); i++) {
                int attr = array.getIndex(i);

                switch (attr) {
                    case R.styleable.PowerfulEditText_clearDrawable:
                        mClearResId = array.getResourceId(attr, DEFAULT_CLEAR_RES);
                        break;

                    case R.styleable.PowerfulEditText_visibleDrawable:
                        mVisibleResId = array.getResourceId(attr, DEFAULT_VISIBLE_RES);
                        break;

                    case R.styleable.PowerfulEditText_invisibleDrawable:
                        mInvisibleResId = array.getResourceId(attr, DEFAULT_INVISIBLE_RES);
                        break;

                    case R.styleable.PowerfulEditText_BtnWidth:
                        mBtnWidth = array.getDimensionPixelSize(attr, DEFAULT_BUTTON_WIDTH);
                        break;

                    case R.styleable.PowerfulEditText_BtnSpacing:
                        mBtnPadding = array.getDimensionPixelSize(attr, DEFAULT_BUTTON_PADDING);
                        break;

                    case R.styleable.PowerfulEditText_borderStyle:
                        mBorderStyle = array.getString(attr);
                        break;

                    case R.styleable.PowerfulEditText_styleColor:
                        mStyleColor = array.getColor(attr, DEFAULT_STYLE_COLOR);
                        break;
                }
            }
            array.recycle();
        }

        //初始化按钮显示的Bitmap
        mBitmapClear = createBitmap(context, mClearResId, DEFAULT_CLEAR_RES);
        mBitmapVisible = createBitmap(context, mVisibleResId, DEFAULT_VISIBLE_RES);
        mBitmapInvisible = createBitmap(context, mInvisibleResId, DEFAULT_INVISIBLE_RES);
        //如果自定义,则使用自定义的值,否则使用默认值
        if (mBtnPadding == 0) {
            mBtnPadding = DEFAULT_BUTTON_PADDING;
        }
        if (mBtnWidth == 0) {
            mBtnWidth = DEFAULT_BUTTON_WIDTH;
        }
        //给文字设置一个padding,避免文字和按钮重叠了
        mTextPaddingRight = mBtnPadding * 4 + mBtnWidth * 2;

        //按钮出现和消失的动画
        mGoneAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATOR_TIME);
        mVisibleAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATOR_TIME);

        //是否是密码样式
        isPassword =
                getInputType() == (InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT);

    }

下面是View绘制的三大流程,measure,layout,draw。这里对layout没有做特殊主要处理,主要是mesaure和draw。

首先是 measure。因为在控件里绘制了按钮,为了避免和text重叠,所以在measure时需要给显示的text设置padding。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //设置右内边距, 防止清除按钮和文字重叠
        setPadding(getPaddingLeft(), getPaddingTop(), mTextPaddingRight, getPaddingBottom());
    }

draw流程中主要的任务是绘制边框和按钮。

边框的多样式选择也是在这里实现的。通过画布Canvas的 drawxxx() 方法,就可以绘制出这些效果。这里用到了三个。画线:drawLine(...); 画矩形:drawRect(...); 画圆角矩形: drawRoundRect(...)。其中画圆角矩形时,可能会遇到一些小麻烦,就是圆角上的线比四条边上的线粗。所以有一些特殊处理,具体见使用canvas.drawRoundRect()时,解决四个圆角的线比较粗的问题

这里还给按钮实现了消失和出现时的缩放动画效果。控件获取焦点且text不为空时,会显示放大出现的动画;失去焦点时,会显示缩小后消失的动画。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPaint.setStyle(Paint.Style.STROKE);

        //使用自定义颜色。如未定义,则使用默认颜色
        if (mStyleColor != -1) {
            mPaint.setColor(mStyleColor);
        } else {
            mPaint.setColor(DEFAULT_STYLE_COLOR);
        }

        //控件获取焦点时,加粗边框
        if (isFocused()) {
            mPaint.setStrokeWidth(DEFAULT_FOCUSED_STROKE_WIDTH);
        } else {
            mPaint.setStrokeWidth(DEFAULT_UNFOCUSED_STROKE_WIDTH);
        }

        //绘制清空和明文显示按钮
        drawBorder(canvas);

        //绘制边框
        drawButtons(canvas);
    }

    private void drawBorder(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();

        switch (mBorderStyle) {
            //矩形样式
            case STYLE_RECT:
                setBackground(null);
                canvas.drawRect(0, 0, width, height, mPaint);
                break;

            //圆角矩形样式
            case STYLE_ROUND_RECT:
                setBackground(null);
                float roundRectLineWidth = 0;
                if (isFocused()) {
                    roundRectLineWidth = DEFAULT_FOCUSED_STROKE_WIDTH / 2;
                } else {
                    roundRectLineWidth = DEFAULT_UNFOCUSED_STROKE_WIDTH / 2;
                }
                mPaint.setStrokeWidth(roundRectLineWidth);
                if (Build.VERSION.SDK_INT >= 21) {
                    canvas.drawRoundRect(
                            roundRectLineWidth/2, roundRectLineWidth/2, width - roundRectLineWidth/2, height - roundRectLineWidth/2,
                            DEFAULT_ROUND_RADIUS, DEFAULT_ROUND_RADIUS,
                            mPaint);
                } else {
                    canvas.drawRoundRect(
                            new RectF(roundRectLineWidth/2, roundRectLineWidth/2, width - roundRectLineWidth/2, height - roundRectLineWidth/2),
                            DEFAULT_ROUND_RADIUS, DEFAULT_ROUND_RADIUS,
                            mPaint);
                }
                break;

            //半矩形样式
            case STYLE_HALF_RECT:
                setBackground(null);
                canvas.drawLine(0, height, width, height, mPaint);
                canvas.drawLine(0, height / 2, 0, height, mPaint);
                canvas.drawLine(width, height / 2, width, height, mPaint);
                break;

            //动画特效样式
            case STYLE_ANIMATOR:
                setBackground(null);
                if (isAnimatorRunning) {
                    canvas.drawLine(width / 2 - mAnimatorProgress, height, width / 2 + mAnimatorProgress, height, mPaint);
                    if (mAnimatorProgress == width / 2) {
                        isAnimatorRunning = false;
                    }
                } else {
                    canvas.drawLine(0, height, width, height, mPaint);
                }
                break;
        }
    }

    private void drawButtons(Canvas canvas) {
        if (isBtnVisible) {
            //播放按钮出现的动画
            if (mVisibleAnimator.isRunning()) {
                float scale = (float) mVisibleAnimator.getAnimatedValue();
                drawClearButton(scale, canvas);
                if (isPassword) {
                    drawVisibleButton(scale, canvas, isPasswordVisible);
                }
                invalidate();
            //绘制静态的按钮
            } else {
                drawClearButton(1, canvas);
                if (isPassword) {
                    drawVisibleButton(1, canvas, isPasswordVisible);
                }
            }
        } else {
            //播放按钮消失的动画
            if (mGoneAnimator.isRunning()) {
                float scale = (float) mGoneAnimator.getAnimatedValue();
                drawClearButton(scale, canvas);
                if (isPassword) {
                    drawVisibleButton(scale, canvas, isPasswordVisible);
                }
                invalidate();
            }
        }
    }

这里说的控件中的按钮,其实都只是显示一个图片而已,并不能直接设置事件监听。那么如何实现点击效果,是通过判断在控件中点击的位置来实现的。如果点击了控件中按钮图片显示的区域,说明该按钮应该响应事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {

            boolean clearTouched =
                    ( getWidth() - mBtnPadding - mBtnWidth < event.getX() )
                            && (event.getX() < getWidth() - mBtnPadding)
                            && isFocused();
            boolean visibleTouched =
                    (getWidth() - mBtnPadding * 3 - mBtnWidth * 2 < event.getX())
                            && (event.getX() < getWidth() - mBtnPadding * 3 - mBtnWidth)
                            && isPassword && isFocused();

            if (clearTouched) {
                setError(null);
                setText("");
                return true;
            } else if (visibleTouched) {
                if (isPasswordVisible) {
                    isPasswordVisible = false;
                    setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT);
                    setSelection(getText().length());
                    invalidate();
                } else {
                    isPasswordVisible = true;
                    setInputType(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
                    setSelection(getText().length());
                    invalidate();
                }
                return true;
            }
        }
        return super.onTouchEvent(event);
    }

一些控件焦点变化时的处理是在回调方法 onFocusChanged 中完成的。
    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);

        //播放按钮出现和消失动画
        if (focused && getText().length() > 0) {
            if (!isBtnVisible) {
                isBtnVisible = true;
                startVisibleAnimator();
            }
        } else {
            if (isBtnVisible) {
                isBtnVisible = false;
                startGoneAnimator();
            }
        }

        //实现动画特效样式
        if (focused && mBorderStyle.equals(STYLE_ANIMATOR)) {
            isAnimatorRunning = true;
            mAnimator = ObjectAnimator.ofInt(this, BORDER_PROGRESS, 0, getWidth() / 2);
            mAnimator.setDuration(ANIMATOR_TIME);
            mAnimator.start();
        }
    }

接下来,说下 动画特效的样式实现,就是控件获取焦点时,边框会有一个从中间向外面扩展的动画。这里用到了自定义属性动画。该动画在上面 onFocusChanged 中启动,然后通过变量mAnimatorProgress 来记录当前动画的进度。
    private boolean isAnimatorRunning = false;
    private int mAnimatorProgress = 0;

    //自定义属性动画
    private static final Property BORDER_PROGRESS
            = new Property(Integer.class, "borderProgress") {
        @Override
        public Integer get(PowerfulEditText powerfulEditText) {
            return powerfulEditText.getBorderProgress();
        }

        @Override
        public void set(PowerfulEditText powerfulEditText, Integer value) {
            powerfulEditText.setBorderProgress(value);
        }
    };


    protected void setBorderProgress(int borderProgress) {
        mAnimatorProgress = borderProgress;
        postInvalidate();
    }

    protected int getBorderProgress() {
        return mAnimatorProgress;
    },>,>

当mAnimatorProgress等于控件宽度的一半,说明动画结束。
            //动画特效样式
            case STYLE_ANIMATOR:
                setBackground(null);
                if (isAnimatorRunning) {
                    canvas.drawLine(width / 2 - mAnimatorProgress, height, width / 2 + mAnimatorProgress, height, mPaint);
                    if (mAnimatorProgress == width / 2) {
                        isAnimatorRunning = false;
                    }
                } else {
                    canvas.drawLine(0, height, width, height, mPaint);
                }
                break;

最后,说下该控件的使用把。一共有7个自定义属性。


    
        
        
        
        
        
        
        
        
        
        
        
    

使用示例如下:

    

其中,边框样式的对应规则如下。

1、矩形样式: app:borderStyle="rectangle"

2、半矩形样式: app:borderStyle="halfRect"

3、圆角矩形样式: app:borderStyle="roundRect"

4、动画特效样式: app:borderStyle="animator"

控件抖动的效果也是利用动画实现的。调用view实例的以下方法即可实现抖动。

mPEditText.startShakeAnimation()

其他就不多啰嗦了。完整源码请戳本人 GitHub 把。点击打开链接