自己很少做自定義 View 惦积,只有最開始的時候跟著郭神寫了一個小 Demo ,后來隨著見識的越來越多猛频,特別是在開源社區(qū)看到很多優(yōu)秀的漂亮的控件狮崩,都是羨慕的要死蛛勉,但是拉下來的代碼還是看不明白,而且當(dāng)時因為時間因素睦柴,沒有深入學(xué)習(xí)和研究控件和動畫方面的知識诽凌,而是把更多時間花在了 Android 的異步通信和網(wǎng)絡(luò)框架這一塊。
因為想起暑假實習(xí)的時候有個小需求坦敌,當(dāng)時因為忙著主要的業(yè)務(wù)侣诵,一直擱淺沒有做,回到學(xué)校發(fā)現(xiàn)其實不難狱窘。索性從這個人生第一個上架的小控件慢慢深入一點杜顺,順帶復(fù)習(xí) View 的繪制原理。
- 文章來源:itsCoder 的 WeeklyBolg 項目
- itsCoder主頁:http://itscoder.com/
- 作者:謝三弟
- 審閱者:Brucezz
目錄
目標(biāo)效果
需求:實習(xí)公司一個產(chǎn)品训柴,因為很多是臨時用戶哑舒,需要為這些沒有自覺設(shè)置頭像的用戶,給予隨機(jī)頭像幻馁。生成的規(guī)則是根據(jù)用戶用戶名的第一個字符隨機(jī)匹配顏色集洗鸵。
從需求中我們可以知道:
- 該控件需要展示圖片
- 該控件需要按照規(guī)則生成圖像
- 一般頭像都是圓形
大致上可以知道是這樣的。
開搞仗嗦!
繼承 ImageView 開始
我們都知道 Android 自帶了很多控件膘滨,我們自定義控件的出發(fā)點只是官方提供的控件無法滿足業(yè)務(wù)需求的時候。
從我們的需求來看稀拐,該控件是圖片展示類的火邓,所以我們很自然想到了只需要在系統(tǒng) ImageView 上進(jìn)行功能拓展即可,這樣就可以滿足新的需求又不會失去 ImageView 自帶的功能德撬。
public class CharAvatarView extends ImageView {
private static final String TAG = CharAvatarView.class.getSimpleName();
// 顏色畫板集
private static final int[] colors = {
0xff1abc9c, 0xff16a085, 0xfff1c40f, 0xfff39c12, 0xff2ecc71,
0xff27ae60, 0xffe67e22, 0xffd35400, 0xff3498db, 0xff2980b9,
0xffe74c3c, 0xffc0392b, 0xff9b59b6, 0xff8e44ad, 0xffbdc3c7,
0xff34495e, 0xff2c3e50, 0xff95a5a6, 0xff7f8c8d, 0xffec87bf,
0xffd870ad, 0xfff69785, 0xff9ba37e, 0xffb49255, 0xffb49255, 0xffa94136
};
private Paint mPaintBackground;
private Paint mPaintText;
private Rect mRect;
private String text;
private int charHash;
public CharAvatarView(Context context) {
this(context, null);
}
public CharAvatarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CharAvatarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mRect = new Rect();
}
}
在這里我做了一些初始化工作铲咨,并且在其中的一個構(gòu)造函數(shù)中實例化了 Paint
和 Rect
。
關(guān)于 View 的構(gòu)造函數(shù)的區(qū)別:
public CharAvatarView(Context context) {
super(context);
}
public CharAvatarView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CharAvatarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
- 第一種屬于程序內(nèi)實例化時采用蜓洪,之傳入 Context 即可
CharAvatarView avatarView = new CharAvatarView(this);
這樣我們的 View 就新建出來了纤勒,根據(jù)需求添加到布局即可。
第二種用于 layout 文件實例化隆檀,會把 XML 內(nèi)的參數(shù)通過 AttributeSet 帶入到 View 內(nèi)摇天。
第三個主題的 style 信息,也會從 XML 里帶入
為了自定義的 View 兼容 Java 和 Xml 兩種代碼的使用方式恐仑,一般推薦這樣寫構(gòu)造方法:
public CharAvatarView(Context context) {
this(context, null);
}
public CharAvatarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CharAvatarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mRect = new Rect();
}
工作流程
我們的 View 系統(tǒng)是如何將它繪制到屏幕上的呢泉坐?
View 的繪制流程是從 ViewRoot 的
performTraversals
方法開始,它經(jīng)過 measure 裳仆、 layout 和 draw 三個過程才能最終將一個 View 繪制出來腕让,其中 measure 用來測量 View 的寬和高,layout 用來確定 View 在父容器中的放置位置歧斟,而 draw 則負(fù)責(zé)將 View 繪制在屏幕上纯丸。針對 performTraversals 的大致流程如圖:
Measure 過程決定了 View 的寬/高司训, Measure 完成以后,可以通過
getMeasuredWidth
和getMeasuredHeight
方法來獲取到 View 測量后的寬/高液南,在幾乎所有的情況下它都等同于 View 最終的寬/高,但是特殊情況除外勾徽。
Layout 過程 決定了 View 的四個頂點的坐標(biāo)和實際的 View 的寬/高滑凉,完成以后,可以通過getTop
喘帚、getBottom
畅姊、getLeft
、getRight
來拿到 View 的四個頂點的位置吹由,并可以通過getWidth
和getHeight
方法拿到 View 最終的寬/高若未。
Draw 過程則決定了 View 的顯示,只有 draw 方法完成以后 View 的內(nèi)容才能呈現(xiàn)在屏幕上倾鲫。
關(guān)于 View 工作流程的深入我們在以后另外開篇進(jìn)行研究粗合。目前我們已經(jīng)從宏觀了解到了 View 會經(jīng)歷三個過程繪制出來,而且清楚了其中不同方法中的用途乌昔。接下來我們看看 CharAvatarView 在這三個流程中分別做了什么隙疚。
onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec); // 寬高相同
}
讓寬高相同,我在這里是只直接傳入寬度進(jìn)行測量磕道。
這樣會得到一個正方形的 View供屉。
onLayout()
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
我在這里什么也沒有做,因為需求里對 View 的位置沒有什么需要特殊的處理溺蕉。
onDraw()
大部分自定義控件伶丐,最核心的代碼就是在 onDraw()
里了。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (null != text) {
int color = colors[charHash % colors.length];
// 畫圓
mPaintBackground.setColor(color);
canvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2, mPaintBackground);
// 寫字
mPaintText.setColor(Color.WHITE);
mPaintText.setTextSize(getWidth() / 2);
mPaintText.setStrokeWidth(3);
mPaintText.getTextBounds(text, 0, 1, mRect);
// 垂直居中
Paint.FontMetricsInt fontMetrics = mPaintText.getFontMetricsInt();
int baseline = (getMeasuredHeight() - fontMetrics.bottom - fontMetrics.top) / 2;
// 左右居中
mPaintText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, getWidth() / 2, baseline, mPaintText);
}
}
- 首先從顏色數(shù)組里根據(jù) hash 取余得到背景顏色
- 然后畫出背景圓
- 接下來就是寫字
- 最后是對字居中的處理
/**
* @param content 傳入字符內(nèi)容
* 只會取內(nèi)容的第一個字符,如果是字母轉(zhuǎn)換成大寫
*/
public void setText(String content) {
if (content == null) {
throw new NullPointerException("字符串內(nèi)容不能為空");
}
this.text = String.valueOf(content.toCharArray()[0]);
this.text = text.toUpperCase();
charHash = this.text.hashCode();
// 重繪
invalidate();
}
這是暴露給外部的方法疯特,我們也是在這里得到要畫的字符哗魂。
使用
在 gradle 依賴?yán)锾砑?
compile 'com.github.xcc3641:charavatarview:0.1'
<com.hugo.charavatarview.CharAvatarView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="@+id/avatar"/>
CharAvatarView mAvatarView;
mAvatarView = (CharAvatarView) findViewById(R.id.avatar);
mAvatarView.setText("謝三弟");
運行:
人生第一個自定義 View 就完成了。
上傳到可以參考司機(jī)的這篇文章碼農(nóng)必知之上傳開源庫到 jcenter辙芍,配置好各種參數(shù)啡彬。以后更新版本就執(zhí)行一行代碼就行啦。
./gradlew install // 只需要第一次執(zhí)行
./gradlew bintrayUpload
開源地址:GitHub 地址