前言
- 自定義View是Android開(kāi)發(fā)者必須了解的基礎(chǔ)
- 今天,我將手把手教你寫一個(gè)自定義View呀狼,并理清自定義View所有應(yīng)該的注意點(diǎn)
Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過(guò)程
Carson帶你學(xué)Android:自定義View Layout過(guò)程
Carson帶你學(xué)Android:自定義View Draw過(guò)程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析
目錄
1. 自定義View的分類
自定義View一共分為兩大類等曼,具體如下圖:
2. 具體介紹 & 使用場(chǎng)景
對(duì)于自定義View的類型介紹及使用場(chǎng)景如下圖:
3. 使用注意點(diǎn)
在使用自定義View時(shí)有很多注意點(diǎn)(坑)里烦,希望大家要非常留意:
3.1 支持特殊屬性
- 支持wrap_content
如果不在onMeasure()
中對(duì)wrap_content
作特殊處理,那么wrap_content
屬性將失效
具體原因請(qǐng)看文章:為什么你的自定義View wrap_content不起作用禁谦?
- 支持padding & margin
如果不支持胁黑,那么padding
和margin
(ViewGroup情況)的屬性將失效
- 對(duì)于繼承View的控件,padding是在draw()中處理
- 對(duì)于繼承ViewGroup的控件州泊,padding和margin會(huì)直接影響measure和layout過(guò)程
3.2 多線程應(yīng)直接使用post方式
View的內(nèi)部本身提供了post系列的方法别厘,完全可以替代Handler的作用,使用起來(lái)更加方便拥诡、直接触趴。
3.3 避免內(nèi)存泄露
主要針對(duì)View中含有線程或動(dòng)畫的情況:當(dāng)View退出或不可見(jiàn)時(shí),記得及時(shí)停止該View包含的線程和動(dòng)畫渴肉,否則會(huì)造成內(nèi)存泄露問(wèn)題冗懦。
啟動(dòng)或停止線程/ 動(dòng)畫的方式:
- 啟動(dòng)線程/ 動(dòng)畫:使用
view.onAttachedToWindow()
,因?yàn)樵摲椒ㄕ{(diào)用的時(shí)機(jī)是當(dāng)包含View的Activity啟動(dòng)的時(shí)刻- 停止線程/ 動(dòng)畫:使用
view.onDetachedFromWindow()
仇祭,因?yàn)樵摲椒ㄕ{(diào)用的時(shí)機(jī)是當(dāng)包含View的Activity退出或當(dāng)前View被remove的時(shí)刻
3.4 處理好滑動(dòng)沖突
當(dāng)View帶有滑動(dòng)嵌套情況時(shí)披蕉,必須要處理好滑動(dòng)沖突,否則會(huì)嚴(yán)重影響View的顯示效果。
4. 具體實(shí)例
接下來(lái)没讲,我將用自定義View中最常用的繼承View來(lái)說(shuō)明自定義View的具體應(yīng)用和需要注意的點(diǎn)
4.1 繼承VIew的介紹
在下面的例子中眯娱,我將講解:
- 如何實(shí)現(xiàn)一個(gè)基本的自定義View(繼承VIew)
- 如何自身支持wrap_content & padding屬性
- 如何為自定義View提供自定義屬性(如顏色等等)
- 實(shí)例說(shuō)明:畫一個(gè)實(shí)心圓
4.2 具體步驟
- 創(chuàng)建自定義View類(繼承View類)
- 布局文件添加自定義View組件及顯示
- 注意點(diǎn)設(shè)置(支持wrap_content & padding屬性自定義屬性等等)
下面我將逐個(gè)步驟進(jìn)行說(shuō)明:
步驟1:創(chuàng)建自定義View類(繼承View類)
/**
* CircleView.java
* 作用:繪制自定義View的具體內(nèi)容
* 需復(fù)寫方法:onDraw()
* 復(fù)寫邏輯:具體繪制邏輯
*/
public class CircleView extends View {
// 設(shè)置畫筆變量
Paint mPaint1;
// 自定義View有四個(gè)構(gòu)造函數(shù)
// 如果View是在Java代碼里面new的,則調(diào)用第一個(gè)構(gòu)造函數(shù)
public CircleView(Context context){
super(context);
// 在構(gòu)造函數(shù)里初始化畫筆的操作
init();
}
// 如果View是在.xml里聲明的爬凑,則調(diào)用第二個(gè)構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來(lái)的
public CircleView(Context context,AttributeSet attrs){
super(context, attrs);
init();
}
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CircleView(Context context,AttributeSet attrs,int defStyleAttr ){
super(context, attrs,defStyleAttr);
init();
}
// API21之后才使用
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
// 畫筆初始化
private void init() {
// 創(chuàng)建畫筆
mPaint1 = new Paint ();
// 設(shè)置畫筆顏色為藍(lán)色
mPaint1.setColor(Color.BLUE);
// 設(shè)置畫筆寬度為10px
mPaint1.setStrokeWidth(5f);
//設(shè)置畫筆模式為填充
mPaint1.setStyle(Paint.Style.FILL);
}
// 關(guān)鍵點(diǎn):復(fù)寫onDraw()進(jìn)行繪制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 獲取控件的高度和寬度
int width = getWidth();
int height = getHeight();
// 設(shè)置圓的半徑 = 寬,高最小值的2分之1
int r = Math.min(width, height)/2;
// 畫出圓(藍(lán)色)
// 圓心 = 控件的中央,半徑 = 寬,高最小值的2分之1
canvas.drawCircle(width/2,height/2,r,mPaint1);
}
}
特別注意:
- View的構(gòu)造函數(shù)一共有4個(gè)徙缴,具體使用請(qǐng)看:深入理解View的構(gòu)造函數(shù)和
理解View的構(gòu)造函數(shù) - 對(duì)于繪制內(nèi)容為何在復(fù)寫onDraw()里實(shí)現(xiàn),具體請(qǐng)看我寫的文章:自定義View Draw過(guò)程- 最易懂的自定義View原理系列(4)
步驟2:在布局文件中添加自定義View類的組件及顯示
/**
* 1. 在布局文件中添加自定義View類的組件
* activity_main.xml
*/
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity">
<!-- 注意添加自定義View組件的標(biāo)簽名:包名 + 自定義View類名-->
<!-- 控件背景設(shè)置為黑色-->
<scut.carson_ho.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#000000"
</RelativeLayout>
/**
* 2. 設(shè)置顯示
* MainActivity.java
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
至此嘁信,一個(gè)基本的自定義View已經(jīng)實(shí)現(xiàn)了于样,運(yùn)行效果如下圖。
步驟3:注意點(diǎn)設(shè)置(支持wrap_content & padding屬性自定義屬性等等)
接下來(lái)繼續(xù)看自定義View關(guān)于屬性自定義的問(wèn)題:
- 如何手動(dòng)支持wrap_content屬性
- 如何手動(dòng)支持padding屬性
- 如何為自定義View提供自定義屬性(如顏色等等)
a. 手動(dòng)支持wrap_content屬性
先來(lái)看wrap_content & match_parent屬性的區(qū)別
// 視圖的寬和高被設(shè)定成剛好適應(yīng)視圖內(nèi)容的最小尺寸
android:layout_width="wrap_content"
// 視圖的寬和高延伸至充滿整個(gè)父布局
android:layout_width="match_parent"
// 在Android API 8之前叫作"fill_parent"
如果不手動(dòng)設(shè)置支持wrap_content
屬性潘靖,那么wrap_content
屬性是不會(huì)生效(顯示效果同match_parent
)
具體原因 & 解決方案請(qǐng)看我寫的文章:為什么你的自定義View wrap_content不起作用穿剖?
b. 支持padding屬性
padding
屬性:用于設(shè)置控件內(nèi)容相對(duì)控件邊緣的邊距;
區(qū)別與margin屬性(同樣稱為:邊距):控件邊緣相對(duì)父控件的邊距(父控件控制)卦溢,具體區(qū)別如下:
如果不手動(dòng)設(shè)置支持padding屬性糊余,那么padding屬性在自定義View中是不會(huì)生效的。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity">
<scut.carson_ho.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
/** 添加Padding屬性单寂,但不會(huì)生效 **/
android:padding="20dp"
/>
</RelativeLayout>
解決方案
繪制時(shí)考慮傳入的padding屬性值(四個(gè)方向)啄刹。
在自定義View類的復(fù)寫onDraw()進(jìn)行設(shè)置
/**
* 復(fù)寫的onDraw()
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 獲取傳入的padding值
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
// 獲取繪制內(nèi)容的高度和寬度(考慮了四個(gè)方向的padding值)
int width = getWidth() - paddingLeft - paddingRight ;
int height = getHeight() - paddingTop - paddingBottom ;
// 設(shè)置圓的半徑 = 寬,高最小值的2分之1
int r = Math.min(width, height)/2;
// 畫出圓(藍(lán)色)
// 圓心 = 控件的中央,半徑 = 寬,高最小值的2分之1
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,r,mPaint1);
}
c. 提供自定義屬性
除了常見(jiàn)的以android:開(kāi)頭的系統(tǒng)屬性(如下所示),很多場(chǎng)景下自定義View還需要系統(tǒng)所沒(méi)有的屬性凄贩,即自定義屬性。
// 基本是以android開(kāi)頭
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:padding="30dp"
實(shí)現(xiàn)自定義屬性的步驟如下:
- 在values目錄下創(chuàng)建自定義屬性的xml文件
- 在自定義View的構(gòu)造方法中解析自定義屬性的值
- 在布局文件中使用自定義屬性
下面我將對(duì)每個(gè)步驟進(jìn)行具體介紹
步驟1:在values目錄下創(chuàng)建自定義屬性的xml文件
/**
* attrs_circle_view.xml
*/
<?xml version="1.0" encoding="utf-8"?>
<resources>
// 自定義屬性集合:CircleView
// 在該集合下,設(shè)置不同的自定義屬性
<declare-styleable name="CircleView">
// 在attr標(biāo)簽下設(shè)置需要的自定義屬性
// 此處定義了一個(gè)設(shè)置圖形的顏色:circle_color屬性,格式是color,代表顏色
// 格式有很多種,如資源id(reference)等等
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
對(duì)于自定義屬性類型 & 格式如下:
<-- 1. reference:使用某一資源ID -->
<declare-styleable name="名稱">
<attr name="background" format="reference" />
</declare-styleable>
// 使用格式
// 1. Java代碼
private int ResID;
private Drawable ResDraw;
ResID = typedArray.getResourceId(R.styleable.SuperEditText_background, R.drawable.background); // 獲得資源ID
ResDraw = getResources().getDrawable(ResID); // 獲得Drawble對(duì)象
// 2. xml代碼
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
app:background="@drawable/圖片ID" />
<-- 2. color:顏色值 -->
<declare-styleable name="名稱">
<attr name="textColor" format="color" />
</declare-styleable>
// 格式使用
<TextView
android:layout_width="42dip"
android:layout_height="42dip"
android:textColor="#00FF00" />
<-- 3. boolean:布爾值 -->
<declare-styleable name="名稱">
<attr name="focusable" format="boolean" />
</declare-styleable>
// 格式使用
<Button
android:layout_width="42dip"
android:layout_height="42dip"
android:focusable="true" />
<-- 4. dimension:尺寸值 -->
<declare-styleable name="名稱">
<attr name="layout_width" format="dimension" />
</declare-styleable>
// 格式使用:
<Button
android:layout_width="42dip"
android:layout_height="42dip" />
<-- 5. float:浮點(diǎn)值 -->
<declare-styleable name="AlphaAnimation">
<attr name="fromAlpha" format="float" />
<attr name="toAlpha" format="float" />
</declare-styleable>
// 格式使用
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.7" />
<-- 6. integer:整型值 -->
<declare-styleable name="AnimatedRotateDrawable">
<attr name="frameDuration" format="integer" />
<attr name="framesCount" format="integer" />
</declare-styleable>
// 格式使用
<animated-rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:frameDuration="100"
android:framesCount="12"
/>
<-- 7. string:字符串 -->
<declare-styleable name="MapView">
<attr name="apiKey" format="string" />
</declare-styleable>
// 格式使用
<com.google.android.maps.MapView
android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />
<-- 8. fraction:百分?jǐn)?shù) -->
<declare-styleable name="RotateDrawable">
<attr name="pivotX" format="fraction" />
<attr name="pivotY" format="fraction" />
</declare-styleable>
// 格式使用
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="200%"
android:pivotY="300%"
/>
<-- 9. enum:枚舉值 -->
<declare-styleable name="名稱">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
// 格式使用
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
<-- 10. flag:位或運(yùn)算 -->
<declare-styleable name="名稱">
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0" />
<flag name="stateUnchanged" value="1" />
<flag name="stateHidden" value="2" />
<flag name="stateAlwaysHidden" value="3" />
<flag name="stateVisible" value="4" />
<flag name="stateAlwaysVisible" value="5" />
<flag name="adjustUnspecified" value="0x00" />
<flag name="adjustResize" value="0x10" />
<flag name="adjustPan" value="0x20" />
<flag name="adjustNothing" value="0x30" />
</attr>
</declare-styleable>袱讹、
// 使用
<activity
android:name=".StyleAndThemeActivity"
android:label="@string/app_name"
android:windowSoftInputMode="stateUnspecified | stateUnchanged | stateHidden" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<-- 特別注意:屬性定義時(shí)可以指定多種類型值 -->
<declare-styleable name="名稱">
<attr name="background" format="reference|color" />
</declare-styleable>
// 使用
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
android:background="@drawable/圖片ID|#00FF00" />
步驟2:在自定義View的構(gòu)造方法中解析自定義屬性的值
/**
* 此處是需要解析circle_color屬性的值
* 該構(gòu)造函數(shù)需要重寫
*/
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs,0);
// 原來(lái)是:super(context,attrs);
init();
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加載自定義屬性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
// 解析集合中的屬性circle_color屬性
// 該屬性的id為:R.styleable.CircleView_circle_color
// 將解析的屬性傳入到畫圓的畫筆顏色變量當(dāng)中(本質(zhì)上是自定義畫圓畫筆的顏色)
// 第二個(gè)參數(shù)是默認(rèn)設(shè)置顏色(即無(wú)指定circle_color情況下使用)
mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
// 解析后釋放資源
a.recycle();
init();
}
步驟3:在布局文件中使用自定義屬性
/**
* activity_main.xml
*/
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!--必須添加schemas聲明才能使用自定義屬性-->
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity"
>
<!-- 注意添加自定義View組件的標(biāo)簽名:包名 + 自定義View類名-->
<!-- 控件背景設(shè)置為黑色-->
<scut.carson_ho.diy_view.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
android:padding="30dp"
<!--設(shè)置自定義顏色-->
app:circle_color="#FF4081"
/>
</RelativeLayout>
至此疲扎,一個(gè)較為規(guī)范的自定義View已經(jīng)完成了。
完整代碼下載
Carson_Ho的github:自定義View的具體應(yīng)用
5. 總結(jié)
- 本文對(duì)自定義View的具體應(yīng)用和注意點(diǎn)進(jìn)行了全面分析
- Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過(guò)程
Carson帶你學(xué)Android:自定義View Layout過(guò)程
Carson帶你學(xué)Android:自定義View Draw過(guò)程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析
歡迎關(guān)注Carson_Ho的簡(jiǎn)書
不定期分享關(guān)于安卓開(kāi)發(fā)的干貨捷雕,追求短椒丧、平、快救巷,但卻不缺深度壶熏。