寫在前面
Android已經爲我們提供了豐富的組件庫,讓我們可以實現各種UI效果。但是如果如此衆多的組件還不能滿足我們的需求,怎麼辦呢?別急,android組件也爲我們提供了非常方便的拓展方法,通過對現有系統組件的繼承,可以方便地實習那我們自己的功能。
自定義View作爲Android的一項重要技能,一直以來被初學者認爲是代表高手的象徵,這篇文章就帶大家瞭解下自定義View的過程。
自定義View的分類
- 繼承 View重寫 onDraw 方法
這種方式主要用於顯示不規則的效果哦,即這種效果不方便用佈局組合來實現,往往需要靜態或者動態的顯示一些不規則的圖形採用這種方式需要自己支持wrap_content,並且padding也需要自己處理。 - 繼承ViewGroup派生特殊的Layout
這種方法主要用於實現自定義佈局,即除了LinearLayout,RelativeLayout等系統佈局之外的一種重新定義的全新的佈局,當某種效果很像
幾種View組合在一起的時候就可以採用這種方法。
這種方法稍微複雜一些,需要合適的處理ViewGroup的測量和佈局這倆個過程 - 繼承特定的View(比如TextView)
這種方法一般用於擴展某種已有的View功能。這種方法不需要自己支持wrap_content,padding等。 - 繼承特定的ViewGroup(比如LinearLayout等)
當某種效果很像幾種View組合在一起的時候就可以採用這種方法。這種方法不需要自己處理ViewGroup的測量和佈局這倆個過程。
自定義View的注意事項
- 讓View支持wrap_content
這是因爲直接繼承View或ViewGroup的控件,如果不在onMeasure中處理wrap_content,那麼外界在佈局中使用wrap_content時就無法達到預期效果 - 讓View支持padding
直接繼承View的控件,如果不再draw方法中處理padding,那麼這個屬性是無法起作用的。直接繼承ViewGroup的控件需要在onMeasure和onLayout中考慮padding和子元素的margin對其造成的影響,不然將導致pading和子元素的margin失效 - 不要在View中使用Handler
這是因爲View內部本身就提供了post系列方法,完全可以替代Handler的作用。除非你很明確要用Handler來發送消息。 - View中如果有線程和動畫,及時停止
如果有線程和動畫需要停止的時候,onDetachedFromWindow就惡意做到。這是因爲當包含此View的Activity退出或者當前View被remove時,View的onDetachedFromWindow方法就會被調用。相對的,當包含此View的Activity啓動時onAttachedToWindow會被調用。同時,View不可見時,我們也需要停止線程和動畫,如果不及時停止,可能會導致內存泄漏。 - 如果有滑動嵌套時,當然要處理好滑動衝突的問題。
注意事項
在自定義View中,通常有下列比較重要的方法:
- onFinishInflate():從xml中加載組件後調用
- onSizeChanged():當組件的大小發生變化時調用
- onMeasure():測量組件時調用,是View支持wrap_content屬性
- onLayout():確定組件顯示位置時調用
-onTouchEvent():界面上有觸摸事件時調用
當然,創建自定義View的時候,不一定要全部重寫上述方法,只需按照需要重寫即可。
通常,有以下三種方法實現自定義View
- 對現有控件進行擴展
- 通過組合實現新的控件
- 重寫View實現全新控件
下面就用代碼展示下自定義View的基本步驟:
- 新建BasicCustomView繼承View
完整代碼如下
public class BasicCustomView extends View {
private Paint mPaint;
public BasicCustomView(Context context) {
super(context);
initView();
}
public BasicCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public BasicCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
canvas.translate(width/2,height/2);
canvas.drawCircle(0,0,100,mPaint);
}
}
首先驗證自定義View是否支持layout_margin,padding,wrap_content等屬性,驗證代碼如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_custom_view_basic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.ahuang.viewandgroup.activity.CustomViewBasicActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.example.ahuang.viewandgroup.View.BasicCustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:background="#111fff"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.example.ahuang.viewandgroup.View.BasicCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#111fff"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.example.ahuang.viewandgroup.View.BasicCustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#111fff"
android:padding="20dp"/>
</LinearLayout>
</LinearLayout>
上圖證明圖我們的自定義View
1.支持layout_margin屬性
2.不支持padding屬性
3.證明不支持wrap_content
讓View支持wrap_content
之所以不支持wrap_content屬性,是因爲我們的自定義View沒有重寫onMeasure()方法,View默認的onMeasure()方法只支持EXACTLY模式,所以可以指定控件的具體寬高值或者match_parent屬性,如果要自定義的view支持wrap_content屬性,就必須重寫onMeasure()方法。
加入代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}
/**
* 獲得測量的寬度
* @param widthMeasureSpec
* @return
*/
private int measureWidth(int widthMeasureSpec){
int width = 0;
int mode=MeasureSpec.getMode(widthMeasureSpec); //獲得測量模式
int size=MeasureSpec.getSize(widthMeasureSpec); //獲得測量值
if (mode==MeasureSpec.EXACTLY){ //精準測量模式
width=size;
}else {
width=300;
if (mode==MeasureSpec.AT_MOST){
width=Math.min(width,size);
}
}
return width;
}
/**
* 獲得測量的高度
* @param heightMeasureSpec
* @return
*/
private int measureHeight(int heightMeasureSpec){
int height = 0;
int mode=MeasureSpec.getMode(heightMeasureSpec); //獲得測量模式
int size=MeasureSpec.getSize(heightMeasureSpec); //獲得測量值
if (mode==MeasureSpec.EXACTLY){ //精準測量模式
height=size;
}else {
height=300;
if (mode==MeasureSpec.AT_MOST){
height=Math.min(width,size);
}
}
return height;
}
可以看到,重寫onMeasure()方法後,VIew已經支持wrap_content了。
讓View支持padding屬性
修改onDraw()方法如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft=getPaddingLeft();
final int paddingRight=getPaddingRight();
final int paddingTop=getPaddingTop();
final int paddingBottom=getPaddingBottom();
int width = getWidth()-(paddingLeft+paddingRight);
int height = getHeight()-(paddingTop+paddingBottom);
canvas.translate(width/2,height/2);
canvas.drawCircle(0,0,100,mPaint);
}
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.example.ahuang.viewandgroup.View.BasicCustomView
android:layout_width="150dp"
android:layout_height="150dp"
android:background="#111fff"
android:paddingLeft="30dp"
android:paddingTop="30dp"/>
</LinearLayout>
我們看到已經支持padding屬性了.