真正開始仔細掰扯自定義view裏面的門門道道是從前段時間開始的事。一直抱着會用就成的我,晃晃悠悠地走到了初級工程師盡頭,總結來就一句話:我不生產代碼,我只是代碼的搬運工。可這樣的日子能過多久呢?
最近危機感特別的敏感,總是感覺隨時都有可能面臨失業的危險,互聯網的冬天,移動端在經歷了巨大的爆熱後已經在不到一年的時間裏面滑向了低谷,個人的境遇也隨之糟糕起來。競爭異常的激烈,幾十幾百個人競爭一個崗位,那麼接下來:找不到工作?轉行?失業?等等問題隨之到來,寒冷的移動端大環境下,個人能撐住現在的狀況多久呢?
那麼,在這慘淡的境況下,我們移動端的小夥伴們的出路在哪裏?轉行?跨行?還是在移動端走進階路,衝擊高級工程師?無論如何我們都不能再像以前那樣:懈怠了。奮鬥的時候來了
一扯廢話就拉不住車了,下面進入正題。
看自定義view的流程(這裏不包括View嵌套等實現手法)
第一:編寫自定義屬性;
第二:編寫自定義View類(一般繼承已有的控件或佈局);
第三:在自定義的view類中初始化自定義的屬性並在邏輯中使用,在這個過程中一般要用到onLayout(),onMeasure(),onDraw()方法,這些方法等我們用的時候再說;
那麼接下來我們就需要找一個輪子自己來造一個並且要弄懂裏面的原理,這樣我們就找一個非常簡單的例子吧:給TextView添加邊框,當然有一個很常用的方法,就是給TextView添加帶背景的邊框:這個背景需要在drawable文件夾下定義。
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffffff" />
<stroke
android:width="@dimen/textview_border"
android:color="@color/colorAccent" />
</shape>
這種實現方法也十分簡單,但是有個嚴重的缺陷就是面對多種樣式代碼重用性不高,比如這個邊框需要紅色,另一個邊框需要黑色的就需要我們把上面這個背景圖的代碼複製一份更改裏面的屬性值。
當然這樣項目中也是基本夠用,但是抱着進階的心態,我們來學習下如何使用自定義屬性來封裝一個新的帶邊框的TextView。
接下來我們來看第一步,自定義屬性,
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="borderColor" format="color" />
<attr name="borderWidth" format="dimension" />
<declare-styleable name="BorderTextView">
<attr name="borderColor" />
<attr name="borderWidth" />
</declare-styleable>
</resources>
上面的代碼基本是固定格式,還有一種寫法也是一樣的:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BorderTextView">
<attr name="borderColor" format="color" />
<attr name="borderWidth" format="dimension" />
</declare-styleable>
第一種寫法將屬性的聲明和屬性所屬的View分離主要是爲了屬性聲明的定義的複用,當自定義的View比較少的時候第二種寫法更加簡潔,當很多自定義View的時候使用第一種代碼的複用性更好。
對於這個例子,其實影響都不大,下面我們來詳細看裏面的代碼: 的name對應自定的View的類名,而attr標籤裏面的name對應屬性名,就和TextView裏面的android:text=”Hello World”中的text對應,format對應屬性的類型,也就是這個屬性支持什麼類型的數據。例如format=”color”就是borderColor這個屬性只支持顏色,不能填入String;dimension是支持尺寸的意思,也就是dp,sp,px之類的。
那麼這個format支持哪些類型呢?
- reference 參考某一資源ID。例如:=”@drawable/圖片ID”;
- color:顏色值。例如:=”#00FF00”;
- boolean:布爾值。例如:=”true”;
- dimension:尺寸值。例如:= “42dp”;
- float:浮點值。例如:= “1.0” ;
- integer:整型值。例如: = “12”;
- string:字符串。例如:=”Hello World”;
- fraction:百分數。例如:= “300%”;
enum:枚舉值。例如:
declare-styleable name="XXXXXX">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>flag:位或運算。
<declare-styleable name="XXXXX">
<attr name="windowSoftInputMode">
<flag name = "stateUnspecified" value = "0" />
<flag name = "stateUnchanged" value = "1" />
</attr>
</declare-styleable>
這先給大家簡單介紹下,大家見識下心中有個數就成。
那自定義屬性就先介紹到這裏。
接下來介紹如何在代碼裏初始化,我們先需要定義一個BorderTextView類繼承自TextView,然後將兩個自定義的屬性聲明一下。
public class BorderTextView extends TextView {
private int mBorderColor;//邊框顏色
private int mBorderWidth;//邊框線的寬度
}
然後我們來重寫父類的構造方法,一共四個,我們常用的是前三個,看代碼:
public BorderTextView(Context context) {
this(context, null);
}
public BorderTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BorderTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
其中:
1. 一個參數的構造方法我們常用在new一個對象出來的時候;
2. 兩個參數構造方法的調用比如說我們在佈局文件中使用xml添加了一個控件實際上調用的是兩個參數的構造方法;
3. 三個參數的構造方法試用於自定義style的時候,較少用到。
-&- 另外一點值得注意的是,我們在一個參數的構造方法中調用了兩個參數的構造方法,兩個參數的構造方法中調用了三個參數的構造方法,這樣做的好處是,我們只需要在三個參數的構造方法中進行一些初始化操作,而不需要在三個構造方法中分別初始化。
-&- 當然你也可以將初始化方法封裝一下,在三個構造方法中分別調用一下,也是可以的。
接下就是今天的重點了:前面自定義的屬性是爲了在xml布文件中調用的,那麼在佈局文件中定義的屬性我們如何在代碼中拿到呢?
這裏基本是固定寫法,直接看代碼:
/**
* 獲得我們所定義的自定義樣式屬性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BorderTextView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.BorderTextView_borderColor:
// 默認顏色設置爲黑色
mBorderColor = a.getColor(attr, defaultColor);
break;
case R.styleable.BorderTextView_borderWidth:
// 默認設置爲16sp,TypeValue也可以把sp轉化爲px
mBorderWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
當然另外一種寫法也是挺好的:
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.BorderTextView, 0, 0);
mBorderWidth = t.getDimensionPixelOffset(R.styleable.BorderTextView_borderWidth, 16);
mBorderColor = t.getColor(R.styleable.rainbowbar_rainbowbar_color, barColor);
a.recycle();
基本都是固定寫法就是對於不同類型的屬性,調用的方法也是不一樣的。這裏推薦一篇博客:鴻洋大神的Android 深入理解Android中的自定義屬性;
接下來考慮到我們自定義的屬性可以在代碼去設置,就需要生成get和set方法,快捷鍵是control+enter。這裏就不多解釋了。
最後我們來介紹今天的重頭戲:根據自定義的屬性來畫一個邊框——也就是畫四條邊。這裏需要用到畫筆Paint,畫布canvas和onDraw()方法;
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//畫TextView的4個邊
canvas.drawLine(0,0,0,getHeight(),mPaint);
canvas.drawLine(0,getHeight(),getWidth(),getHeight(),mPaint);
canvas.drawLine(getWidth(),getHeight(),getWidth(),0,mPaint);
canvas.drawLine(0,0,getWidth(),0,mPaint);
}
這裏我們重寫了父類的onDraw()方法,onDraw()方法主要作用是繪製,在這個方法中我們首先調用了父類的onDraw()方法可以完成對文字的繪製,然後我們調用canvas的drawLine()畫一條直線,這個方法接受五個參數,分別是起點x座標,起點y座標,終點x座標,終點y座標,最後一個是畫筆對象;初始化畫筆時我們設置畫筆的顏色,線條的寬度,和抗鋸齒。
mPaint = new Paint();
mPaint.setColor(mBorderColor);
mPaint.setStrokeWidth(mBorderWidth);
mPaint.setAntiAlias(true);
那麼我們邊框線的起點座標和終點座標是怎麼計算的呢?
這裏首先普及一下關於Android座標的知識點:
1. Android系統原點和X軸,Y軸;
2.Android中的另外一個座標系叫做視圖座標系,它描述的是子視圖在父視圖中的位置:
3. Android的控件座標系原點以及x軸,y軸;
圖片已經十分清楚,這裏就不多解釋了。那麼我相信你就很容易理解邊框的X和Y座標了。
其中getWidth()得到控件的寬度,getHeight()得到控件的高度;
好了接下來我們把這個帶邊框的控件添加到佈局中,如下:
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//畫TextView的4個邊
canvas.drawLine(0,0,0,getHeight(),mPaint);
canvas.drawLine(0,getHeight(),getWidth(),getHeight(),mPaint);
canvas.drawLine(getWidth(),getHeight(),getWidth(),0,mPaint);
canvas.drawLine(0,0,getWidth(),0,mPaint);
現在來在佈局中使用我們自定義的控件:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:border="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jack.mytextview.MainActivity">
<com.jack.mytextview.BorderTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="金雞報曉"
android:layout_centerInParent="true"
android:padding="20dp"
android:layout_margin="10dp"
android:textSize="20sp"
android:textColor="#041ae4"
border:borderColor="#51ff00"
border:borderWidth="5dp"
/>
</RelativeLayout>
其中xmlns:border=”http://schemas.android.com/apk/res-auto”中的border是可以自己定義的命名空間,下面的自定義屬性的前面要用到這個命名控件,而後面是固定的寫法;再然後就是因爲BorderTextView是我們自己定義的控件所以在標籤中要寫完整的路徑。然後我們在Activity中加載這個佈局,來看下運行後的效果:
附加:整個BorderTextView類:
public class BorderTextView extends TextView {
private int mBorderColor;//邊框顏色
private int mBorderWidth;//邊框線的寬度
private Paint mPaint;
private int defaultColor = Color.BLACK;
public BorderTextView(Context context) {
this(context, null);
}
public BorderTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BorderTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 獲得我們所定義的自定義樣式屬性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BorderTextView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.BorderTextView_borderColor:
// 默認顏色設置爲黑色
mBorderColor = a.getColor(attr, defaultColor);
break;
case R.styleable.BorderTextView_borderWidth:
// 默認設置爲16sp,TypeValue也可以把sp轉化爲px
mBorderWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
mPaint = new Paint();
mPaint.setColor(mBorderColor);
mPaint.setStrokeWidth(mBorderWidth);
mPaint.setAntiAlias(true);
}
public int getBorderColor() {
return mBorderColor;
}
public void setBorderColor(int borderColor) {
this.mBorderColor = borderColor;
}
public int getBorderWidth() {
return mBorderWidth;
}
public void setBorderWidth(int borderWidth) {
this.mBorderWidth = borderWidth;
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//畫TextView的4個邊
canvas.drawLine(0,0,0,getHeight(),mPaint);
canvas.drawLine(0,getHeight(),getWidth(),getHeight(),mPaint);
canvas.drawLine(getWidth(),getHeight(),getWidth(),0,mPaint);
canvas.drawLine(0,0,getWidth(),0,mPaint);
}
}