[轉(zhuǎn)] Android自定義View,你摸的透透的了?
本文轉(zhuǎn)載自ClericYi的Android自定義View,你摸的透透的了?

前言
View,有很多的名稱。不論是你熟知的布局,還是控件,他們?nèi)慷祭^承自View。

文內(nèi)部分圖片轉(zhuǎn)載自Carson_Ho大佬的文章
思維導(dǎo)圖

工作流程
measure
其實(shí)通過layout中的第二張圖我們已經(jīng)知道了控件大小的計(jì)算了。
-
height=bottom-top -
width=right-left
對(duì)于ViewGroup而言,就是對(duì)容器內(nèi)子控件的遍歷和計(jì)算了。
因?yàn)橹苯永^承自View的控件使用wrap_cotent和match_parent是顯示出來的效果是相同的。需要我們使用MeasureSpec中的getMode()方法來對(duì)當(dāng)前的模式進(jìn)行區(qū)分和比較。
| 模式 | 狀態(tài) |
|---|---|
| UNSPECIFIED | 未指定模式,View想多大就多大,父容器不做限制,一般用于系統(tǒng)內(nèi)部的測(cè)量 |
| AT_MOST | 最大模式,對(duì)應(yīng)wrap_content,View的大小不大于SpecSize的值 |
| EXACTLY | 精確模式,對(duì)應(yīng)match_parent,View的大小為SpecSize的值 |
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//用于獲取設(shè)定的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 用于獲取設(shè)定的長(zhǎng)度
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 類似這樣的判斷,后面不過多復(fù)述
// 用于判斷是不是wrap_content
// 如果不進(jìn)行處理,效果會(huì)是match_parent
if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(20, 20);
}
}
復(fù)制代碼
layout
在確定位置時(shí),我們有一個(gè)非常需要主要的地方—— 坐標(biāo)系。Android系統(tǒng)的坐標(biāo)系和平時(shí)畫的坐標(biāo)系并不相同。

所以相對(duì)應(yīng)的,我們的位置計(jì)算方法自然和我們?cè)瓉淼恼檬窍喾吹摹?/p>

4個(gè)頂點(diǎn)的位置分別由4個(gè)值決定:
-
top:子View上邊界到所在容器上邊界的距離。 -
left:子View左邊界到所在容器左邊界的距離。 -
bottom:子View下邊界到所在容器上邊界的距離。 -
right:子View右邊界到所在容器左邊界的距離。
所有的計(jì)算都是相對(duì)于所在容器才能夠開始的。
draw
一共有6個(gè)步驟:
- 如果需要,則繪制背景 -- drawBackground(canvas);
- 保存當(dāng)前canvas層 -- saveCount = canvas.getSaveCount();
- 繪制View的內(nèi)容 -- if (!dirtyOpaque) onDraw(canvas);
- 繪制子View -- dispatchDraw(canvas);
- 如果需要,則繪制View的褪色邊緣,類似于陰影效果 -- canvas.restoreToCount(saveCount);
- 繪制裝飾,比如滾動(dòng)條 -- onDrawForeground(canvas);
關(guān)于開發(fā)者需要重寫的方法一般是第三步繪制View的內(nèi)容對(duì)應(yīng)的
onDraw()。
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// 在畫布上進(jìn)行類似這樣的操作
canvas.drawLine(0, height/2, width,height/2, paint);
}
復(fù)制代碼
入門自定義View
在日常項(xiàng)目的布局文件中我們經(jīng)常會(huì)使用到xmlns:app="http://schemas.android.com/apk/res-auto"這樣標(biāo)簽,其實(shí)他就是用來引入我們自定義的標(biāo)簽使用的。
- 在
res/values目錄下創(chuàng)建attrs
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DefaultView">
<attr name="color" format="color"/>
</declare-styleable>
</resources>
復(fù)制代碼
- 在
DefaultView(Context context, @Nullable AttributeSet attrs)中獲取。以下是整個(gè)完整代碼。
/**
* author: ClericYi
* time: 2020-01-30
*/
public class DefaultView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mColor = Color.RED;
public DefaultView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
initDraw();
}
private void initAttrs(Context context, @Nullable AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DefaultView);
// 從styleable中獲取的名字是系統(tǒng)會(huì)生成的,一般是 類名_name 的形式
mColor = array.getColor(R.styleable.DefaultView_color, Color.GREEN);
// 獲取完資源后即使回收
array.recycle();
}
private void initDraw() {
paint.setColor(mColor);
paint.setStrokeWidth(3f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
canvas.drawLine(0, height/2, width,height/2, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(20, 20);
}
}
}
復(fù)制代碼
基礎(chǔ)的性能優(yōu)化
首先的話我們先了解如何去知道一個(gè)View是否被過度繪制了?
其實(shí)在我們手機(jī)中的開發(fā)模式已經(jīng)存在這個(gè)選項(xiàng)了。
| 開啟前 | 開啟后 |
|---|---|
![]() 開啟前
|
![]() 開啟后
|
下方給出繪制的次數(shù)對(duì)應(yīng)圖

那如何做到性能優(yōu)化呢?
在這個(gè)問題之前,需要了解什么是過度繪制,你可以理解為同一位置的控件不斷的疊加而產(chǎn)生的無用數(shù)據(jù),那我們就來說說集中解決方案吧。
方案1: 減少嵌套層數(shù)。
| 使用線性布局 | 使用約束布局 |
|---|---|
![]() |
![]() |
因?yàn)橹皇且粋€(gè)案例,想說的意思,如果多個(gè)LinearLayout嵌套實(shí)現(xiàn)的效果,如果能被一個(gè)ConstraintLayout直接實(shí)現(xiàn),那么就用后者替代,因?yàn)椴粫?huì)這樣在同一個(gè)區(qū)域重復(fù)出現(xiàn)
方案2: 去除默認(rèn)的背景

這個(gè)解決方案其實(shí)針對(duì)的背景會(huì)被自動(dòng)繪制的問題,如果我們把這個(gè)層次消去,從繪制角度老說也是一種提升了。正如圖示一般直接減少了一層的繪制。
在代碼中的具體表現(xiàn),通過對(duì)style.xml中的Theme進(jìn)行修改:
<item name="android:windowBackground">@null</item>
復(fù)制代碼
總結(jié)

以上就是我的學(xué)習(xí)成果,如果有什么我沒有思考到的地方或是文章內(nèi)存在錯(cuò)誤,歡迎與我分享。



