在實(shí)際使用的過程中,我們經(jīng)常會(huì)接到這樣一些需求,比如環(huán)形計(jì)步器,柱狀圖表,圓形頭像等等,這時(shí)我們通常的思路是去Google 一下,看看 github 上是否有我們需要的這些控件,但是如果網(wǎng)上收不到這樣的控件呢?這時(shí)我們經(jīng)常需要自定義 View 來滿足需求。
第一次看我文章的小伙伴可以關(guān)注一下我,順便關(guān)注一下我的專欄:Android高級(jí)開發(fā)架構(gòu),每天更新各種技術(shù)干貨,分享更多最熱程序員圈內(nèi)事。Android高級(jí)開發(fā)架構(gòu)?zhuanlan.zhihu.com
關(guān)于自定義控件,一般輝遵循以下幾個(gè)套路
方法是用來重新測(cè)量,并設(shè)定控件的大小,我們知道控件的大小是用 width 和 height 兩個(gè)標(biāo)簽來設(shè)定的。通常有三種賦值情況 :
這時(shí)也許你就會(huì)有疑問,既然都已經(jīng)有了這些屬性,那還重寫 onMeasure 干嘛,直接調(diào)用 View 的方法不就行了嗎?但是你想想,比如你設(shè)計(jì)了一個(gè)圓形控件,用戶在 width 和 height 都設(shè)置了 wrap_parent 屬性,同時(shí)又給你傳了一張長方形的圖片,那結(jié)果會(huì)怎么樣?必然得讓你“方”啊。。所以這時(shí)就需要重寫 onMeasure 方法,設(shè)定其寬高相等。
如何重寫OnMeasure()
首先把 onMeasure() 打出來
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
這時(shí)大家不眠會(huì)好奇,明明是重繪大小,那么給我提供寬高就行了呀?這個(gè) int widthMeasureSpec, int heightMeasureSpec ,是個(gè)什么鬼?其實(shí)很好理解,大家都知道計(jì)算機(jī)中數(shù)據(jù)是已二進(jìn)制存儲(chǔ)的。同時(shí),就像我之前講的 View 的大小賦值形式有三種,那么在計(jì)算機(jī)中,要存儲(chǔ)二進(jìn)制數(shù),需要幾位二進(jìn)制呢,答案很明了 -> 兩位。同時(shí)大家也發(fā)現(xiàn),這兩個(gè)參數(shù)都是 int 型的。int 型數(shù)據(jù)在計(jì)算機(jī)中用 32 位存儲(chǔ)。所以聰明的 Google 就把這 30 位劃分為兩部分。第一部分兩位拿來存類型,后面 28 位拿來存數(shù)據(jù)大小。
開始重寫OnMeasure()方法
首先,無論是 width 還是 height ,我們都得先判斷類型,再去計(jì)算大小,so~ 咱先寫個(gè)方法專門用于計(jì)算并返回大小。
private int getMySize(int defaultSize, int measureSpec) {
// 設(shè)定一個(gè)默認(rèn)大小 defaultSize
int mySize = defaultSize;
// 獲得類型
int mode = MeasureSpec.getMode(measureSpec);
// 獲得大小
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: {//如果沒有指定大小,就設(shè)置為默認(rèn)大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果測(cè)量模式是最大取值為size
//我們將大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改變它
mySize = size;
break;
}
}
return mySize;
}
然后,我們?cè)購?onMeasure() 中調(diào)用它
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 分別獲得長寬大小
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
// 這里我已圓形控件舉例
// 所以設(shè)定長寬相等
if (width < height) {
height = width;
} else {
width = height;
}
// 設(shè)置大小
setMeasuredDimension(width, height);
}
在 xml 中應(yīng)用試試效果
<?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"
android:gravity="center"
tools:context=".activities.MainActivity">
<com.entry.android_view_user_defined_first.views.MyView
android:layout_width="100dp"
android:layout_height="100dp"
app:default_size="@drawable/ic_launcher_background"/>
</LinearLayout>
到這里圖就已經(jīng)重繪出來了,讓我們運(yùn)行一下下
我們驚呆了,說好的控件呢??!別急,咱還沒給他上色呢,所以它自然是透明的。所以現(xiàn)在重寫 onDraw() 方法,在 onDraw() 方法中 (這里我為了寫的方便,在 onDraw 方法中直接 new 了對(duì)象 { 嗷我沒有對(duì)象} 但這是一種很容易導(dǎo)致內(nèi)存泄露的行為)
我們通過 canvas (安卓的一個(gè)繪圖類對(duì)象進(jìn)行圖形的繪制)
@Override
protected void onDraw(Canvas canvas) {
// 調(diào)用父View的onDraw函數(shù),因?yàn)閂iew這個(gè)類幫我們實(shí)現(xiàn)了一些
// 基本的而繪制功能,比如繪制背景顏色、背景圖片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我們已經(jīng)將寬高設(shè)置相等了
Log.d(TAG, r + "");
// 圓心的橫坐標(biāo)為當(dāng)前的View的左邊起始位置+半徑
int centerX = r;
// 圓心的縱坐標(biāo)為當(dāng)前的View的頂部起始位置+半徑
int centerY = r;
// 定義灰色畫筆,繪制圓形
Paint bacPaint = new Paint();
bacPaint.setColor(Color.GRAY);
canvas.drawCircle(centerX, centerY, r, bacPaint);
// 定義藍(lán)色畫筆,繪制文字
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setTextSize(60);
canvas.drawText("大傻瓜", 0, r+paint.getTextSize()/2, paint);
}
運(yùn)行一下
大功告成!但是善于思考的可能會(huì)發(fā)現(xiàn):使用這種方式,我們只能使用父類控件的屬性,但是我們有時(shí)需要更多的功能,比如:圖片控件需要改變透明度,卡片控件需要設(shè)定陰影值等等,那么父類控件的屬性顯然不夠用了,這時(shí)我們就要開始實(shí)現(xiàn)自定義布局。
由于自定義布局屬性一般只需要對(duì) onDraw() 進(jìn)行操作。所以 onMeasure() 等方法的重寫我就不再啰嗦了,這里我打算繼承字 view 實(shí)現(xiàn)一個(gè)類似 TextView 的控件。
首先,讓我們現(xiàn)在 res/values/styles 文件中增加一個(gè)自定義布局屬性。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!--定義屬性集合名-->
<declare-styleable name="MyView">
<!--我們定義為 default_size 屬性為 屈指類型 像素 dp 等-->
<attr name="text_size" format="dimension"/>
<attr name="text_color" format="color"/>
<attr name="text_text" format="string"/>
</declare-styleable>
</resources>
這些標(biāo)簽都是什么意思呢?
首先:
MyView 是自定義布局屬性的名字,也就是標(biāo)簽也就是入口,在 onDraw 中,用 context.obtainStyledAttributes(attrs, R.styleable.MyView); 獲得自定義布局屬性的全部子項(xiàng)。
其次:
attr 中的 name 便是你屬性的名字,比如說這個(gè) text_size 、text_color 、text_text 這三個(gè)屬性,在 布局文件中就是:
<com.entry.android_view_user_defined_first.views.MyView
android:layout_width="100dp"
android:layout_height="100dp"
app:text_text="hello world"
app:text_size="20sp"
app:text_color="@color/colorAccent"/>
最后:
format 標(biāo)簽,format 標(biāo)簽指定的是數(shù)據(jù)類型,具體可以看這篇,我在這里就不重復(fù)了 ->
解析和引用
上面我們先定義了屬性,又在布局中對(duì)其賦值,那么實(shí)際中,我們?nèi)绾卧谧远x控件里,獲得它的實(shí)際值呢?讓我們先寫下構(gòu)造方法,在構(gòu)造方法中獲得這些值的大?。?/p>
private int textSize;
private String textText;
private int textColor;
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView);
textSize = array.getDimensionPixelSize(R.styleable.MyView_text_size, 15);
textText = array.getString(R.styleable.MyView_text_text);
textColor = array.getColor(R.styleable.MyView_text_color,Color.BLACK);
array.recycle();
}
重寫onDraw()
由于在構(gòu)造方法中,我們已經(jīng)獲得基本的值,所以在 onDraw() 中,將這些東西繪制出來就行了,這里直接上代碼:
@Override
protected void onDraw(Canvas canvas) {
// 調(diào)用父View的onDraw函數(shù),因?yàn)閂iew這個(gè)類幫我們實(shí)現(xiàn)了一些
// 基本的而繪制功能,比如繪制背景顏色、背景圖片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我們已經(jīng)將寬高設(shè)置相等了
// 圓心的橫坐標(biāo)為當(dāng)前的View的左邊起始位置+半徑
int centerX = r;
// 圓心的縱坐標(biāo)為當(dāng)前的View的頂部起始位置+半徑
int centerY = r;
// 定義灰色畫筆,繪制圓形
Paint bacPaint = new Paint();
bacPaint.setColor(Color.GRAY);
canvas.drawCircle(centerX, centerY, r, bacPaint);
// 定義藍(lán)色畫筆,繪制文字
Paint paint = new Paint();
paint.setColor(textColor);
paint.setTextSize(textSize);
canvas.drawText(textText, 0, r+paint.getTextSize()/2, paint);
}
運(yùn)行: