自定義View的學習1

Android本身的控件系統可以實現我們開發中的一些基本需求,可是我們在處理實際業務的時候卻催生出了Android控件系統不能很好的需求。這時,自定義控件應運而生。

在進行自定義View之前我們先來看一下View的座標系。
這裏寫圖片描述
上圖引自劉望舒大神的博客


第1種自定義View的姿勢——直接繼承自View,重寫其onDraw方法

直接繼承自View,重寫其onDraw方法,這個方式主要用來實現一些不規則的效果。比如顯示一個圓。需要注意的是直接繼承自View的控件需要對支持wrap_content和padding做處理。所以本例中也重寫了onMeasure方法。以及在onDraw方法中加入了自身padding的處理。讀者可試着去除onMeasure方法或者onDraw方法中的對padding的處理看看效果

自定義的屬性xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
</resources>

自定義CircleView

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class CircleView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        a.recycle();
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
                radius, mPaint);
    }
}

使用自定義CircleView
佈局文件activity_main1.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical" >

    <com.mafeibiao.testapplication.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:padding="20dp"
        android:background="@color/light_green"/>
</LinearLayout>

MainActivity.java

package com.mafeibiao.testapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main1);
    }
}

效果如下圖

這裏寫圖片描述

注:我們在這裏直接繼承了View並重寫其onMeasure和onDraw方法,我們從上幾篇文章詳細分析了Activity的創建以及顯示。我們在梳理一下,首先程序的入口函數是ActivityThread.main函數,從這個函數開始,然後回調我們MainActivity的attach函數,我們在這裏沒有重寫這個函數,但是該函數內部會創建一個至關重要的對象PhoneWindow,然後會回調我們MainActivity的onCreate函數,我們在MainActivity的onCreate函數中調用了setContentView(R.layout.activity_main1);這個函數內部會創建Android 的頂級View DecorView,把我們的佈局文件R.layout.activity_main1解析成相關View並關聯到DecorView下。然後會調用WindowManager的addView方法把DecorView添加到PhoneWindow上,實際上完成這個過程的是ViewRootImpl,它會對我們的DecorView依次進測量、佈局、繪製等工作,在這些工作的過程中會依次回調我們在View以及其子類中重寫的onMeasure、onLayout、onDraw等方法。以我們上面的CircleView爲例,,我們在佈局文件中定義了一個LinearLayout並在LinearLayout內使用了我們自定義的CircleView,那麼按照上一章講解ViewRootImpl的工作流程。會沿着控件樹從上到下依次調用到我們自定義的CircleView onMeasure(我們重寫了該方法)然後沿着控件樹從下向上依次回調。上文也講過,測量過程是後根遍歷,佈局過程是先根遍歷。(要理解Android View的層級結構是樹結構


第2種自定義View的姿勢——直接繼承自Android中控件View,如TextView或者EditText等。

下面我們來實現漸變的TextView。這個我們效果我們經常在鎖屏應用上看到。
這裏寫圖片描述

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.util.Log;

public class CustomTextView extends AppCompatTextView {
    
    private final static String TAG = CustomTextView.class.getSimpleName();
    private Paint paint1;
    private Paint paint2;
    
    private int mWidth;
    private LinearGradient gradient;
    private Matrix matrix;
    //漸變的速度
    private int deltaX;

    public CustomTextView(Context context) {
        super(context, null);
    }

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs);
    }

    private void initView(Context context, AttributeSet attrs) {
        paint1 = new Paint();
        paint1.setColor(getResources().getColor(android.R.color.holo_blue_dark));
        paint1.setStyle(Paint.Style.FILL);

    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if(mWidth == 0){
            Log.e(TAG,"*********************");
            mWidth = getMeasuredWidth();
            paint2 = getPaint();
            //顏色漸變器
            gradient = new LinearGradient(0, 0, mWidth, 0, new int[]{Color.GRAY,Color.WHITE,Color.GRAY}, new float[]{
                    0.3f,0.5f,1.0f
            }, Shader.TileMode.CLAMP);
            paint2.setShader(gradient);
            
            matrix = new Matrix();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(matrix !=null){
            deltaX += mWidth / 5;
            if(deltaX > 2 * mWidth){
                deltaX = -mWidth;
            }
        }
        //關鍵代碼通過矩陣的平移實現
        matrix.setTranslate(deltaX, 0);
        gradient.setLocalMatrix(matrix);
        postInvalidateDelayed(100);
    }
}

下面我們來實現支付寶上手機號和銀行卡號寫入分段的效果。繼承自EditText

這裏寫圖片描述
這裏寫圖片描述
如上圖,在作爲手機號或者銀行卡時輸入的數字會按照不同規則分段,並且右側出現清空按鈕。很明顯,我們需要自定義一個控件符合上述要求。

style格式attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyEditText">

        <!-- 設置自定義輸入框的模式 當值爲1:普通輸入框模式 2:銀行卡號輸入框模式 3:電話號碼模式 默認爲1-->
        <attr name="editTextMode" format="integer" />
        <!-- 配置自定義控件在銀行卡號模式下分隔位數 ,默認爲4位 -->
        <attr name="splitNumber" format="integer" />

    </declare-styleable>
</resources>

佈局代碼activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="20dp"
        android:text="請綁定持卡人本人的銀行卡" />

    <LinearLayout
        android:id="@+id/ll_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="持卡人"
            />

        <com.mafeibiao.testapplication.MyEditText
            android:id="@+id/et_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="請輸入姓名"
            android:padding="5dp" >
        </com.mafeibiao.testapplication.MyEditText>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_card_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="卡號" />

        <com.mafeibiao.testapplication.MyEditText
            android:id="@+id/et_card_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="請輸入銀行卡號"
            android:padding="5dp"
            android:inputType="number"
            app:editTextMode="2"
            app:splitNumber="4">
        </com.mafeibiao.testapplication.MyEditText>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_phone_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="手機號" />

        <com.mafeibiao.testapplication.MyEditText
            android:id="@+id/et_phone_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="請輸入手機號"
            android:inputType="number"
            android:padding="5dp"
            app:editTextMode="3"
            >
        </com.mafeibiao.testapplication.MyEditText>
    </LinearLayout>

</LinearLayout>

代碼MainActivity.java

package com.mafeibiao.testapplication;

/**
 * Created by mafeibiao on 2017/11/21.
 */


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;


public class MyEditText extends AppCompatEditText {

    // 每隔多少位以空格進行分隔一次,卡號一般都是每4位以空格分隔一次
    public int splitNumber = 4;
    // 自定義輸入框的模式 當值爲true:銀行卡號輸入框模式,false:普通輸入框模式
    private int editTextMode = 1;

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

    public MyEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    // 內容清除圖標
    private Drawable mClearDrawable;

    /**
     * 初始化方法
     */
    private void init(AttributeSet attrs) {
        // 設置單行顯示所有輸入框內容
        setSingleLine();
        // 設置輸入框可獲得焦點
        setFocusable(true);
        setFocusableInTouchMode(true);
        TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
        editTextMode = t.getInt(R.styleable.MyEditText_editTextMode, editTextMode);
        splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
        t.recycle();
        mClearDrawable = this.getResources().getDrawable(R.drawable.clear);
        mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
        initEvent();
    }

    // 輸入框內容改變後onTextChanged方法會調用多次,設置一個變量讓其每次改變之後只調用一次
    private boolean isTextChanged = false;

    /**
     * 處理事件的方法
     */
    private void initEvent() {
        addTextChangedListener(new TextWatcher() {
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (isTextChanged) {
                    isTextChanged = false;
                    return;
                }
                isTextChanged = true;
                // 處理輸入內容空格與位數以及光標位置的邏輯
                handleInputContent(s, start,before,count);
                // 處理清除圖標的顯示與隱藏邏輯
                handleClearIcon(true);
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

    }

    // 卡號內容
    private String content;
    // 卡號最大長度,卡號一般最長19位
    public static final int MAX_CARD_NUMBER_LENGHT = 19;
    //手機號
    public static final int MAX_PHONE_NUMBER_LENGHT = 11;
    // 緩衝分隔後的新內容串
    private String result = "";

    /**
     * 處理輸入內容空格與位數的邏輯
     */
    private void handleInputContent(CharSequence s, int start, int before, int count) {
        content = s.toString();
        // 先緩存輸入框內容
        result = content;
        // 去掉空格,以防止用戶自己輸入空格
        content = content.replace(" ", "");
        switch (editTextMode){
            case 1://普通模式
                break;
            case 2://銀行卡號模式

                // 限制輸入的數字位數最多21位(銀行卡號一般最多21位)
                if (content != null && content.length() <= MAX_CARD_NUMBER_LENGHT) {
                    result = "";
                    int i = 0;
                    // 先把splitNumber倍的字符串進行分隔
                    while (i + splitNumber < content.length()) {
                        result += content.substring(i, i + splitNumber) + " ";
                        i += splitNumber;
                    }
                    // 最後把不夠splitNumber倍的字符串加到末尾
                    result += content.substring(i, content.length());
                } else {
                    // 如果用戶輸入的位數
                    result = result.substring(0, result.length() - 1);
                }

                break;
            case 3://手機號模式

                if (content != null && content.length() <= MAX_PHONE_NUMBER_LENGHT) {
                    int length = s.toString().length();
                    if (length == 3 || length == 8){
                        result += " ";
                    }
                } else {
                    // 如果用戶輸入的位數
                    result = result.substring(0, result.length() - 1);
                }

                break;
        }
        // 獲取光標開始位置
        // 必須放在設置內容之前
        int j = getSelectionStart();
        setText(result);
        // 處理光標位置
        handleCursor(before, j);

    }


    /**
     * 處理光標位置
     *
     * @param before
     * @param j
     */
    private void handleCursor(int before, int j) {
        // 處理光標位置
        try {
            if (j + 1 < result.length()) {
                // 添加字符
                if (before == 0) {
                    // 遇到空格,光標跳過空格,定位到空格後的位置
                    if (j % splitNumber + 1 == 0) {
                        setSelection(j + 1);
                    } else {
                        // 否則,光標定位到內容之後 (光標默認定位方式)
                        setSelection(result.length());
                    }
                    // 回退清除一個字符
                } else if (before == 1) {
                    // 回退到上一個位置(遇空格跳過)
                    setSelection(j);
                }
            } else {
                MyEditText.this.setSelection(result.length());
            }
        } catch (Exception e) {

        }
    }


    /**
     * 處理清除圖標的邏輯
     */
    private void handleClearIcon(boolean focused) {
        if (content != null && content.length() > 0) {
            // 顯示
            if (focused) {
                setEditTextIcon(null, null, mClearDrawable, null);
            } else {
                // 隱藏
                setEditTextIcon(null, null, null, null);
            }
        } else {
            // 隱藏
            setEditTextIcon(null, null, null, null);
        }
    }



    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 獲取用戶點擊的座標,這裏只對X軸做了判斷,
        float x = event.getX();
        // 當用戶擡起手指時,判斷座標是否在圖標交互區域,如果在則清空輸入框內容,同時隱藏圖標自己
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (x > (getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth())) {
                // 清空輸入框內容
                setText("");
                // 隱藏圖標
                setEditTextIcon(null, null, null, null);
            }
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        handleClearIcon(focused);
        //刷新界面,防止有時候出現的不刷新界面情況
        invalidate();
    }

    /**
     * 設置輸入框的左,上,右,下圖標
     *
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    private void setEditTextIcon(Drawable left, Drawable top, Drawable right, Drawable bottom) {

        setCompoundDrawables(left, top, right, bottom);
    }

    /**
     * 重寫onMeasure,主要目的是讓EditText的高度與我們顯示在右側的清空圖標的高度相同,否則輸入的時候可能會動態改變EditText的高度以適應清空圖標的高度
     * 用戶體驗不好
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mClearDrawable.getIntrinsicWidth(), heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mClearDrawable.getIntrinsicHeight());
        }
    }
}
發佈了6 篇原創文章 · 獲贊 2 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章