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());
}
}
}