本文對應項目的碼雲位址: https://gitee.com/wanchuanxy/AndroidHeroesTest/tree/master/3/SystemWidget
Android給我們提供了豐富的元件庫來建立豐富的UI效果,同時也提供了非常友善的拓展方法。通過繼承Android的系統元件,我們可以非常友善地拓展現有功能,在系統元件的基礎上建立新的功能,甚至可以直接自定義一個控件,實作Android系統控件所沒有的功能。自定義控件作為Android中一個非常重要的功能,一直以來都被初學者認為是代表高手的象征。其實,自定義View并沒有想象中的那麼難,與其說是在自定義一個View,不如說是在設計一個圖形,隻有站在一個設計者的角度上,才可以更好地建立自定義View。我們不能機械地記憶所有繪圖的API,而是要讓這些API為你所用,結合現實中繪圖的方法,甚至是PhotoShop的技巧,才能設計出更好的自定義View。
适當地使用自定義View,可以豐富應用程式的體驗效果,但濫用自定義View則會帶來适得其反的效果。一個讓使用者覺得熟悉得控件,才是一個好的控件。如果一味追求炫酷的效果而建立自定義View,則會讓使用者覺得華而不實。而且,在系統原生控件可以實作功能的基礎上,系統也提供了主題、圖檔資源、各種風格來建立豐富的UI。這些控件都是經過了Android一代代版本疊代後的産物。即使這樣,在如今的版本中,依然存在不少Bug,更不要提我們自定義的View了。特别是現在Android ROM的多樣性,導緻Android的适配變得越來越複雜,很難保證自定義View在其他手機上也能達到你想要的效果。
當然,了解Android系統自定義View的過程,可以幫助我們了解系統的繪圖機智。同時,在适當的情況下也可以通過自定義View來幫助我們建立更佳靈活的布局。
在自定義View時,我們通常會去重寫onDraw()方法來揮着View的顯示内容。如果該View還需要使用wrap_content屬性,那麼還必須寫onMeasure()方法。另外,通過自定義attrs屬性,還可以設定新的屬性配置值。
在View中通常有以下一些比較重要的回調方法。
- onFinishInflate():從XML加載元件後回調。
- onSizeChanged():元件大小改變時回調。
- onMeasure():回調該方法來進行測量。
- onLayout():回調該方法來确定顯示的位置。
- onTouchEvent():監聽到觸摸事件時回調。
當然,建立自定義View的時候,并不需要重寫所有的方法,隻需要重寫特定條件的回調方法即可。這也是Android控件架構靈活性的展現。
通常情況下,有以下三種方法來實作自定義的控件。
- 對現有控件進行拓展
- 通過組合來實作新的控件
- 重寫View來實作全新的控件
3.6.1 對現有控件進行拓展
這是一個非常重要的自定義View方法,它可以在原生控件的基礎上進行拓展,增加新的功能、修改顯示的UI等。一般來說,我們可以再原生控件的基礎上進行拓展,增加新的功能、修改顯示的UI等。一般來說,我們可以在onDraw()方法中對原生控件行為進行拓展。
下面以一個TextView為例,來看看如何使用拓展原生控件的方法建立新的控件。比如想讓一個TextView的背景更佳豐富,給其多繪制幾層背景,如下圖所示。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuUzYwYWMhRWOldTNzQDN3U2YlFDOkN2MhZTOhVzM0MWMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
我們先來分析一下如何實作這個效果,原生的TextView使用onDraw()方法繪制要顯示的文字。當繼承了系統的TextView之後,如果不重寫其onDraw()方法,則不會修改TextView的任何效果。可以認為在自定義的TextView中調用TextView類的onDraw()方法來繪制顯示的文字,代碼如下所示。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
程式調用super.onDraw(canvas)方法來實作原生控件的功能,但是在動用super.onDraw()方法之前和之後,我們都可以實作自己的邏輯,分别在系統繪制文字前後,完成自己的操作,即如下所示。
@Override
protected void onDraw(Canvas canvas) {
//在調父類方法前,實作自己的邏輯,對TextView來說即是在繪制文本内容前
super.onDraw(canvas);
//在調父類方法後,實作自己的邏輯,對TextView來說即是在繪制文本内容後
}
以上就是通過改變控件的繪制行為建立自定義View的思路。有了上面的分析,我們就可以很輕松地實作上圖所示的自定義TextView了。我們在構造方法中完成必要對象的初始化工作,如初始化畫筆等,代碼如下所示。
mPaint1 = new Paint();
mPaint1.setColor(Color.BLUE);
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
而代碼中最重要的部分則是在onDraw()方法中,為了改變原生的繪制行為,在系統調用super.onDraw(canvas)方法前,也就是在繪制文字之前,繪制兩個不同大小的矩形,形成一個重疊效果,再讓系統調用super.onDraw(canvas)方法,執行繪制文字的工作。這樣,我們就通過改變控件繪制行為,建立了一個新的控件,代碼如下所示。
//繪制外層矩形
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
//繪制内層矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
//繪制文字前平移10像素
canvas.translate(10,0);
//父類完成的方法,即繪制文本
super.onDraw(canvas);
canvas.restore();
此View全文(一些細節解析包含在注釋中了):
package com.imooc.systemwidget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
//思路:1.準備畫筆,2.繪制。完
public class MyTextView extends TextView {
private Paint mPaint1, mPaint2;//聲明畫筆對象
public MyTextView(Context context) {
super(context);
initView();
}//三個重載構造函數
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyTextView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() { //準備畫筆
mPaint1 = new Paint();//準備畫筆1
mPaint1.setColor(getResources().getColor(
android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();//準備畫筆2
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制外層矩形 drawRect乃繪制矩形的方法
// 參數:public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
// 按ctrl點選方法,自己看
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
// 繪制内層矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
// 繪制文字前平移10像素
// public void translate(float dx, float dy)
//translate用于平移坐标系
canvas.translate(10, 0);
// 父類完成的方法,即繪制文本
super.onDraw(canvas);
canvas.restore();
Log.d("MyTextView", "MyTextView_onDraw: ");
}
}
下面再來看一個稍微複雜一點的TextView。在前面一個執行個體中,我們直接使用了Canvas對象來進行圖像的繪制,然後利用Android的繪圖機制,可以繪制出更複雜豐富的圖像。比如可以利用LinearGradient Shader 和Matrix來實作一個動态的文字閃動效果,程式運作效果如下圖所示。
- 要想實作這個效果,可以充分利用Android中Paint對象的Shader渲染器。
- 通過設定一個不斷變化的LinearGradient,并使用帶有該屬性的Paint對象來繪制要顯示的文字。
- 首先,在onSizeChanged()方法中進行一些對象的初始化工作,并根據View的寬度設定一個LinearGradient漸變渲染器,代碼如下所示。 (注意細節的注釋)
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(TAG, "onSizeChanged: ");
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
mPaint = getPaint();
mLinearGradient = new LinearGradient( //gradient.梯度|LinearGradient是一個漸變渲染器!!!
0,
0,
mViewWidth,
0,
new int[]{
Color.BLUE, 0xffffffff,
Color.RED},
null,
Shader.TileMode.CLAMP);
// Create a shader that draws a linear gradient along a line.
// @param x0 The x-coordinate for the start of the gradient line
// @param y0 The y-coordinate for the start of the gradient line
// @param x1 The x-coordinate for the end of the gradient line
// @param y1 The y-coordinate for the end of the gradient line
// @param colors The colors to be distributed along the gradient line 用于設定漸變中的色彩
// @param positions May be null. The relative positions [0..1] of
// each corresponding color in the colors array. If this is null,
// the the colors are distributed evenly along the gradient line.顔色沿着梯度線均勻分布
// @param tile The Shader tiling mode
//
// public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
// Shader.TileMode tile)
mPaint.setShader(mLinearGradient);//設定渲染器
mGradientMatrix = new Matrix();
// Log.d(TAG, "onSizeChanged: ");
// String i ="88888888888";
}
}
}
其中最關鍵的就是使用getPaint()方法擷取目前繪制TextView的Paint對象,并給這個Paint對象設定原生TextView沒有的LinearGradient屬性。最後,在onDraw()方法中,通過矩形的方式來不斷平移漸變效果,進而在繪制文字時,産生動态的閃動效果,代碼如下所示。 (注意細節的注釋)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw:000000 "+i);
if (mGradientMatrix != null) {
mTranslate += mViewWidth / 5;
// Log.d(TAG, "onDraw: ");
if (mTranslate > 2 * mViewWidth) {
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate, 0);//平移矩陣
mLinearGradient.setLocalMatrix(mGradientMatrix);//通過設定平移的矩陣來平移漸變效果
postInvalidateDelayed(100);//延時0.1s
// Log.d(TAG, "onDraw2222: ");
}
}
}
onDraw()思路簡析:
本View.java全文:
package com.imooc.systemwidget;
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.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
public class ShineTextView extends TextView {
private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private Paint mPaint;
private int mViewWidth = 0;
private int mTranslate = 0;
private final String TAG = "ShineTextView";
private String i = "0" ;
public ShineTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(TAG, "onSizeChanged: ");
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
mPaint = getPaint();
mLinearGradient = new LinearGradient( //gradient.梯度|LinearGradient是一個漸變渲染器!!!
0,
0,
mViewWidth,
0,
new int[]{
Color.BLUE, 0xffffffff,
Color.RED},
null,
Shader.TileMode.CLAMP);
// Create a shader that draws a linear gradient along a line.
// @param x0 The x-coordinate for the start of the gradient line
// @param y0 The y-coordinate for the start of the gradient line
// @param x1 The x-coordinate for the end of the gradient line
// @param y1 The y-coordinate for the end of the gradient line
// @param colors The colors to be distributed along the gradient line 用于設定漸變中的色彩
// @param positions May be null. The relative positions [0..1] of
// each corresponding color in the colors array. If this is null,
// the the colors are distributed evenly along the gradient line.顔色沿着梯度線均勻分布
// @param tile The Shader tiling mode
//
// public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
// Shader.TileMode tile)
mPaint.setShader(mLinearGradient);//設定渲染器
mGradientMatrix = new Matrix();
// Log.d(TAG, "onSizeChanged: ");
// String i ="88888888888";
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw:000000 "+i);
if (mGradientMatrix != null) {
mTranslate += mViewWidth / 5;
// Log.d(TAG, "onDraw: ");
if (mTranslate > 2 * mViewWidth) {
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate, 0);//平移矩陣
mLinearGradient.setLocalMatrix(mGradientMatrix);//通過設定平移的矩陣來平移漸變效果
postInvalidateDelayed(100);//延時0.1s
// Log.d(TAG, "onDraw2222: ");
}
}
}
/*
postInvalidateDelayed(100);線程重新整理,0.1s一刷,每刷調用onDraw一次****反複不停地調用onDraw()
07-16 07:19:59.949 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:19:59.949 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.065 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.065 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.181 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.181 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.298 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.298 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.414 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.414 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.532 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.532 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.648 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.649 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.764 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.764 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.881 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.881 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.997 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.997 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:01.115 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:01.115 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
*/
内容參考自
Blankj