自定义view之写一个带删除按钮的Edittext

3,213 阅读7分钟

自定义EditText的需求:

最近工作中需要一个可以删除所有字符的EditText,所以自己写了个自定义view继承Edittext,这个实现相对简单,只用到了自定义view中的部分事件。
首先我们来看一下效果,是怎么样的:

从途中可以看到总共分为两个部分,一个是标准的EditText,另一个是右边的我们自定义的图标,在未输入字符之前,图标是隐藏的,输入字符后,图标显示,点击图标即可删除EditText中的所有文字,同时隐藏图标。

继承EditText

首先我们需要编写一个类继承自EditText:

public class ClearEditText extends android.support.v7.widget.AppCompatEditText{

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

    public ClearEditText(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.editTextStyle);
    }

    public ClearEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
       init();
    }
    private void init() {
        Log.d("顺序", "init");
        }
}

继承EditText必须实现其中的构造方法,此处我们重写了三个,事实上只要一个即可,不定义属性时会默认设置为号为editTextStyle属性集。
我们都知道自定义view时候通常会重写onDraw和onMeasure方法,那么这几个方法到底是按怎样的顺序执行呢,我们可以在代码中添加测试代码来实验一下:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("顺序", "onMeasure");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
 @Override
    protected void onDraw(Canvas canvas) {
        Log.d("顺序", "draw");
        super.onDraw(canvas);
    }

然后我们在一个fragment中加载这个view,输出日志

03-07 14:51:10.805 14606-14606/com.saka.customviewdemo D/顺序: init
03-07 14:51:10.820 14606-14606/com.saka.customviewdemo D/顺序: onMeasure
03-07 14:51:10.880 14606-14606/com.saka.customviewdemo D/顺序: draw

可以看到执行的顺序是按构造器—>onmeasure->onDraw来执行的。

设置图标

最简单的方法是让ui切图,切出不同的分辨率,放在drawable中,直接调用。

此处我没有UI,我也不擅长PS,所以我用xml做了一个简单的删除按钮。

首先创建一个vector类型的drawableresource

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="16dp"
    android:height="16dp"
    android:viewportHeight="16"
    android:viewportWidth="16">
    <path
        android:pathData="M 0 8 L 16 8"
        android:strokeColor="#2c2c2c"
        android:strokeWidth="3" />
    <path
        android:pathData="M 8 0 L 8 16"
        android:strokeColor="#2c2c2c"
        android:strokeWidth="3" />

</vector>

这个图标是正方形,边长是16dp线条宽度3dp,然后做了一个十字型,大概是这个样子

然后再自定义一个rotate类型的drawableresource,这个也就是我们要使用的图标资源:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/delete"
    android:fromDegrees="0"
    android:toDegrees="225"
    android:pivotX="50%"
    android:pivotY="50%">

</rotate>

这个rotaterawable设置了旋转角度是从0转到225度,旋转的中心位置在图片X轴和Y轴的中心位置。

添加Drawable

我们实现自定义EditText的思路是占用右边的drawable位置,点击这个drawable即可触发清除字符事件。

此处我们是在构造器中添加的Drawable,通过init()方法来加载右侧图标。那么怎样获取这个位置呢?

首先我来看一下继承关系

java.lang.Object
   ↳     android.view.View
         ↳     android.widget.TextView
               ↳     android.widget.EditText

在TextView中有这样一个属性:android:drawableRight,这个属性是设置控件右边的图标。这个属性对应的java代码是setCompoundDrawablesWithIntrinsicBounds(int,int,int,int),这三个int值的顺序对应的位置是左上右下,其中第三个位置就是drawableright属性。此处应该注意,假如xml布局中设置了drawableright属性,同时java代码中设置了setCompoundDrawablesWithIntrinsicBounds(null,null,null,null),则java代码中的设置会覆盖xml布局中的设置。

既然能设置我们就有办法获取每一个图标。通过查看api我们找到一个方法,Drawable[] getCompoundDrawables (),注意这个方法返回的是一个drawable数组,长度是4,对应的图标位置是左上右下,即使你没有设置任何drawable,这时的四个值都为null。
另一个方法Drawable[] getCompoundDrawablesRelative ()返回的也是一个数组,长度同样是4,对应的图标位置是start,top,end和bottom,注意和上面的方法区分。

此处为了简单起见,我们直接在代码中设置右侧图标,覆盖xml布局中的设置,同时设置图标不可见。

 private RotateDrawable drawableRotate;

 private void init() {
        Log.d("顺序", "init");        
        setIconVisible(false, getCompoundDrawables());
    }

    private void setIconVisible(boolean b, Drawable[] drawables) {
        if (b) {            
            setCompoundDrawablesWithIntrinsicBounds(drawables[0], drawables[1], getResources().getDrawable(R.drawable.mydelete), drawables[3]);
            drawableRotate = (RotateDrawable) getCompoundDrawables()[2];
        } else {
            setCompoundDrawablesWithIntrinsicBounds(drawables[0], drawables[1], null, drawables[3]);
        }
    }

这样,我们的图标就引入了EditText中,只是它现在是隐藏的,我们无法看到他。

设置图标可见与不可见

我们的目标是在有文字时显示图标,没有文字时隐藏图标,这个时候我们最好的方法是实现TextWatcher方法,它一共有三个

public void beforeTextChanged(CharSequence s, int start,
                                  int count, int after);
public void onTextChanged(CharSequence s, int start, int before, int count);

public void afterTextChanged(Editable s);

我们重点关注第二个方法,这个方法在更改s的时候会用这个回调来通知你,在s中,从start位置开始的count个字符刚刚替换了before开始的的旧文本。
这里我们就可以利用这几个参数来计算此时的状态:

@Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (s.length() == 0 && before > 0) {
            //从有文字删除到无文字的时候
            startAnimatorSetResver();
            return;
        }
        if (start == 0 && s.length() > 0) {
            //从无文字到有文字
            setIconVisible(true, getCompoundDrawables());
            setAnimator();
            startAnimatorSet();
        }
    }

注解中已经说明了两个方法的作用,动画函数稍后讲。此时试试你就可以显示和隐藏图标了。

添加消失和出现的动画

private ValueAnimator alphaAnimator = ValueAnimator.ofInt(0, 255);
private ValueAnimator rotateAnimator = ValueAnimator.ofInt(0, 10000);

此处我们设置了两个动画,一个是用来设置通明度变化,一个是用来设置旋转角度的。drawable有两个属性可以用来设置,一个是setLevel(),这个level就是设置的旋转角度,范围是1-10000(假如你是用的是ScaleDrawable,这个level控制就是你的图片的大小)。另一个就是setAlpha(),这个alpha就是透明度,范围是0-255。

private void setAnimator() {
        alphaAnimator.setDuration(1000);
        alphaAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        alphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                drawableRotate.setAlpha((Integer) animation.getAnimatedValue());
            }
        });

        rotateAnimator.setDuration(1000);
        rotateAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        rotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                drawableRotate.setLevel((Integer) animation.getAnimatedValue());
            }
        });
    }

然后我们定义一个同时启动动画的集合:

    private void startAnimatorSet() {
        AnimatorSet setVisible = new AnimatorSet();
        setVisible.playTogether(alphaAnimator, rotateAnimator);
        setVisible.start();
    }

这样,我们设置图片显示的动画就完成了。当你输入字符时,就可以看到图标慢慢旋转出现了。

同理可以设置图标消失的动画,不详细写出了,可以看demo(我的代码水平有点懒,没有优化)。

设置点击事件

其实我们此处并不是真正的设置点击事件,而是通过判断用户的触摸行为来模拟点击事件:

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_UP:
                if (getCompoundDrawables()[2] != null) {
                    if (getWidth() - getTotalPaddingRight() < event.getX() &&
                            getWidth() - getPaddingRight() > event.getX()) {
                        this.setText("");
                        Log.d("点击了图片", "图片");
                    }
                }
                break;
        }
        return super.onTouchEvent(event);
    }

我们并不是重写onTouchEvent事件,我们只是在onTouchEvent中添加了一个在手指离开屏幕时是否正在图片上的判断,然后将内容设置为空,经过次操作以后,继续原有的onTouchEvent流程。

判断手指离开屏幕的位置的方法是这样的,api中有这样的方法:getWidth返回的是控件的宽度,getTotalPadingRight返回的是空间右边的padding,包含了drawable,getPaddingRight返回的是view右边的padding,要是包含滚动条,滚动条的宽度也在pading内。

至此我们的自定义EditText就完成了,可以使用。
这篇文章只是简单的降解了一下自定义view中一些基本流程,要深入进去需要掌握的远远多于这些,下一节将继续我们的学习。