Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择
Android自定义EditText实现带一键清除和密码明文切换按钮,支持多样式选择。
这是一个自定义EditText,带一键清除和密码明文切换按钮(可以传入自定义图片资源),可以自定义边框颜色,还支持四种边框样式的选择。
源码已上传 GitHub:点击打开链接 欢迎fork,start和批评指正哈。
效果图镇楼。
下面撸袖子开讲了。从构造方法开始把,一般情况下构造方法三连击就够用了。因为这里有自定义的 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 PropertyBORDER_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 把。点击打开链接