有時(shí)候我們需要一些特殊的效果或者功能,而系統(tǒng)控件無法滿足我們的需求,這時(shí)候就需要自己定義一個(gè)控件。
自定義view流程
繼承View
要自定義View首先需要需要繼承View或者其子類,如果需要實(shí)現(xiàn)的效果比較復(fù)雜,通常需要繼承View,有時(shí)候我們需要的是系統(tǒng)的控件再加上一些特殊的效果則可以繼承View的子類(如TextView)
如果是要自己設(shè)計(jì)一種布局或者要組合其他控件,這時(shí)候就需要繼承ViewGroup或者LinearLayout、FrameLayout等系統(tǒng)自帶的布局
重寫構(gòu)造方法
自定義View至少需要重寫兩個(gè)構(gòu)造方法
- 在java代碼中直接創(chuàng)建控件所用的構(gòu)造方法
public CustomView(Context context) {
this(context, null);
}
- 在xml文件中定義View所用的構(gòu)造函數(shù)
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
}
自定義xml中的屬性
首先需要新建res/values/custom_view_attrs.xml,并在里面聲明如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="custom_width" format="dimension" />
</declare-styleable>
</resources>
然后就可以在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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.lkllkllkl.customview.MainActivity">
<cn.lkllkllkl.customview.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:custom_width="100dp"/>
</LinearLayout>
接著我們就可以在自定義View的構(gòu)造方法中將該屬性的值取出使用了
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
float customWidth = 0;
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
customWidth = typeArray.getDimension(R.styleable.CustomView_custom_width, 100);
// 需要記得回收
typeArray.recycle();
}
onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
MeasureSpec
這里要明白o(hù)nMeasure的兩個(gè)參數(shù)首先需要明白MeasureSpec這個(gè)類。
MeasureSpec通過將SpecMode(模式)和SpecSize(具體尺寸)打包成一個(gè)int(即widthMeasureSpec、heightMeasureSpec),前2位表示SpecMode,后30位表示尺寸。
SpecMode有三類
UNSPECIFIED: 父容器對View的大小沒有限制,一般我們自定義View用的較少
EXACTLY: 在xml文件中設(shè)置具體大小為多少dp,或者設(shè)置為match_parent則SpecMode為EXACTLY,此模式表示則直接使用SpecSize作為自定義View的尺寸
AT_MOST: 對應(yīng)xml文件中的wrap_content,表示設(shè)置的尺寸大小不能超過SpecSize
通常我們需要view支持wrap_content的時(shí)候才需要重寫onMeasure,如果不重寫則wrap_content效果和match_parent一樣。一般使用如下代碼就夠了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// dp轉(zhuǎn)換成px, 事先定義的默認(rèn)寬高單位為dp
int defaultWidth = dp2px(getContext(), DEFAULT_WIDTH);
int defaultHeight = dp2px(getContext(), DEFAULT_HEIGHT);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
widthSize = Math.min(defaultWidth, widthSize);
heightSize = Math.min(defaultHeight, heightSize);
} else if (widthMode == MeasureSpec.AT_MOST) {
widthSize = Math.min(defaultWidth, widthSize);
} else if (heightMode == MeasureSpec.AT_MOST){
heightSize = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(widthSize, heightSize);
}
一般為了適配不同屏幕需要使用dp為單位,而setMeasuredDimension是使用px為單位的,所以上面的代碼將我們設(shè)置的默認(rèn)寬高轉(zhuǎn)換成px,轉(zhuǎn)換方法如下
public static int dp2px(Context context, float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, context.getResources().getDisplayMetrics());
}
onLayout方法
一般自定義ViewGroup才需要重寫該方法對子View進(jìn)行排放
onDraw方法
通過onDraw方法我們可以將view繪制到屏幕上,如果要讓view支持padding屬性則需要在onDraw中做處理
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/* mPaint為成員變量,畫筆Paint的對象,
* 在構(gòu)造函數(shù)中進(jìn)行初始化,可以設(shè)置一些在canvas上繪制的通用屬性
*/
mPaint.setColor(Color.RED);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//在View所占區(qū)域繪制一條對角線
canvas.drawLine(paddingLeft, paddingTop,
getWidth() - paddingRight, getHeight() - paddingBottom, mPaint);
}
注意事項(xiàng)
onMeasure、onLayout、onDraw三個(gè)方法可能會(huì)對此調(diào)用,特別是onDraw方法,所以最好不要在這些方法中創(chuàng)建對象避免頻繁分配內(nèi)存造成內(nèi)存抖動(dòng),或者做一些耗時(shí)操作導(dǎo)致跳幀
View中有提供post系列方法,所以不需要在View中使用Handler
View中若有創(chuàng)建線程或者動(dòng)畫需要及時(shí)停止,View#onDetachedFromWindow就是一個(gè)很好的時(shí)機(jī)
View中若有滑動(dòng)嵌套的情形,需要處理好滑動(dòng)沖突
reference
《Android開發(fā)藝術(shù)探索》