一.什么是自定義View
自定義view可以分為三類:
1.把系統(tǒng)內置的控件組合起來生成一個新的控件咖杂;
2.繼承系統(tǒng)現有的控件,然后加入新的功能蚊夫;
3.自己繪制控件诉字,繼承系統(tǒng)的View類,通過View中的回調方法實現繪制知纷。
本文所說的自定義View就是第三種方式壤圃。
二.為什么使用自定義View
自定義View可以實現系統(tǒng)View滿足不了的需求,根據我們開發(fā)中不同的需求去實習我們自己的View琅轧。通過自定義View我們還可以實現一些炫酷的效果伍绳,提升產品的體驗。
三.自定義View的2個關鍵方法
在學習自定義View之前我們先了解幾個自定義View的函數乍桂,這樣有助于我們更好的理解自定義View冲杀。
1.前言
在介紹兩個方法之前我們先看一下MeasureSpec這個類
MeasureSpec從字面意思上理解為“測量規(guī)格”,它決定了一個View的尺寸睹酌。
MeasureSpec是View源碼中的一個靜態(tài)內部類权谁。
getMode(int measureSpec)
和getSize(int measureSpec)
方法的參數mesaureSpec是一個32位的int值,前兩位為View的測量模式值憋沿,后30位為View的測量尺寸旺芽。
getMode(int measureSpec)
方法是獲取View的測量模式。
getSize(int measureSpec)
方法是獲取View測量的尺寸辐啄。
源碼如下:
public static class MeasureSpec {
//需要移動的位數
private static final int MODE_SHIFT = 30;
//相當于把0x3左移30位
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//View的測量模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//View的測量模式
public static final int EXACTLY = 1 << MODE_SHIFT;
//View的測量模式
public static final int AT_MOST = 2 << MODE_SHIFT;
//把size和mode合成一個32為的int
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//獲取View的測量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//獲取測量的尺寸
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
從源碼中可以看出測量模式有三種類型
測量模式 | 表示的意思 |
---|---|
UNSPECIFIED | 父容器沒有對當前View有任何限制甥绿,要多大就多大,這種情況一般用于系統(tǒng)內部则披,表示一種測量狀態(tài) |
EXACTLY | 父容器已經檢查出View所需要的精確大小共缕,這時候View的最終大小就是getSize 中返回的值 |
AT_MOST | 父容器制定了一個View可用的大小,但View大小不能大于這個值 |
測量模式跟布局時用到的
wrap_content
士复、match_parent
以及固定的尺寸的對應關系如下:
EXACTLY對應為:match_parent
和固定尺寸
AT_MOST對應為:wrap_content
前面說了這么多图谷,下面我們看一下onMeasure
的用處吧:
2.onMeasure方法
該方法的作用是測量當前View的在屏幕上占用的尺寸,我們看一下View中的源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//此方法是設置View的測量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}```
其中```setMeasuredDimension ```方法是設置View測量的值獲取View值的方法是```getDefaultSize ``` ,我們再看一下它的源碼:
```java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//獲取測量模式
int specMode = MeasureSpec.getMode(measureSpec);
//獲取測量尺寸
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//返回的大小等于測量尺寸
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
從
getDefaultSize
方法中可用看出View的寬高都由specSize決定翩活,可以得出:
在View中使用wrap_content
就相當于是使用match_parent
,所以直接繼承系統(tǒng)View的類的控件需要重寫onMeasure
便贵。
2.onDraw方法
這個方法就比較好理解了菠镇,它是把已經測量好的View畫在屏幕上,開發(fā)者根據自己的需求繪制不同的功能承璃。
onDraw
方法是通過View的draw
方法調用的利耍,分析源碼可以看出draw
方法繪制過程一般分為以下幾步:
- 繪制背景
background.draw(canvas)
- 繪制自己,調用
onDraw
方法 - 繪制childern盔粹,調用
dispatchDrae
方法 - 繪制裝飾 隘梨,
onDrawScrollBars
方法
源碼如下:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// 第一步 繪制背景
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
//第二步 繪制自己
if (!dirtyOpaque) onDraw(canvas);
//第三步 繪制childern
dispatchDraw(canvas);
//第四步 繪制裝飾
onDrawScrollBars(canvas);
//以下省略...
return;
}
}
下面我們就來寫一個自定義的View吧:
四.自定義View的流程
1.自定義View的屬性(非必需)
2.在View的構造方法中獲得我們自定義的屬性(非必需)
3.重寫onMesure
4.重寫onDraw
以一個簡單的圓形視圖RoundView的控件為例吧。
第一步
給這個視圖設置個自定義的屬性舷嗡,在values
目錄下轴猎,創(chuàng)建一個名為attrs_round_view.xml
的文件,文件內容如下:
<resources>
<declare-styleable name="RoundView">
<attr name="round_color" format="boolean" />
</declare-styleable>
</resources>
第二步
在自定義RoundView的構造函數中獲取color屬性:
//畫圓形的畫筆
private Paint mPaint;
//初始化畫筆
private void initPaint() {
mPaint = new Paint();
//設置畫筆的顏色 默認為黑色
mPaint.setColor(mColor);
}
public RoundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//他們兩個是相同的
//TypedArray array=context.getTheme().obtainStyledAttributes(attrs,R.styleable.RecyclerView,defStyleAttr,0);
//獲取RoundView中的所有自定義屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
//獲取顏色的屬性
mColor = array.getColor(R.styleable.RoundView_round_color, Color.BLACK);
//初始化畫筆
initPaint();
//釋放TypedArray
array.recycle();
}
第三步
重寫onMeasure,計算View的布局进萄。設置布局的默認大小為200
//視圖的默認大小
private final static int DEFAULT_SIZE = 200;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取測量之后的寬度
int width = measureDimension(widthMeasureSpec);
//獲取測量之后的高度
int height = measureDimension(heightMeasureSpec);
//設置測量之后的大小
setMeasuredDimension(width, height);
}
/**
* 獲取計算之后的值
* @param measureSpec
* @return
*/
private int measureDimension(int measureSpec) {
//獲取測量方式
int specMode = MeasureSpec.getMode(measureSpec);
//獲取測量大小
int specSize = MeasureSpec.getSize(measureSpec);
//設置默認大小
int result = DEFAULT_SIZE;
//如果是wrap_content就選取最小的值作為最后測量的大小
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(DEFAULT_SIZE, specSize);
//如果是match_parent或者是固定大小就返回測量的大小
} else if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
}
return result;
}
第四步
通過onDraw
方法繪制圓形捻脖,首先獲取布局中設置的padding屬性,再根據寬高計算出實際圓形的半徑中鼠,并繪制可婶。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//獲取布局的padding大小
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//通過獲取布局的寬高和padding大小計算實際的寬高
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//計算圓的半徑
int radius = Math.min(width, height) / 2;
//以圓形的原點坐標,畫出圓形
canvas.drawCircle(paddingLeft + radius, paddingTop + radius, radius, mPaint);
}
最后的效果如下圖所示:
附上:RoundVIew代碼