自定義view之寫一個帶刪除按鈕的Edittext

saka發表於2017-03-07

自定義EditText的需求:

最近工作中需要一個可以刪除所有字元的EditText,所以自己寫了個自定義view繼承Edittext,這個實現相對簡單,只用到了自定義view中的部分事件。
首先我們來看一下效果,是怎麼樣的:

自定義view之寫一個帶刪除按鈕的Edittext

從途中可以看到總共分為兩個部分,一個是標準的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,然後做了一個十字型,大概是這個樣子

自定義view之寫一個帶刪除按鈕的Edittext

然後再自定義一個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中一些基本流程,要深入進去需要掌握的遠遠多於這些,下一節將繼續我們的學習。

相關文章