儘管 Android 系統提供了很多控件,可是有不少酷炫效果仍然是系統原生控件沒法實現的。好在 Android 容許自定義控件,來彌補原生控件的不足。可是在不少初學者看來,自定義 View 彷佛很難掌握。其中有很大一部分緣由是咱們平時看到的自定義 View 使用中,有多種形式,有的寥寥數筆,有的邏輯很複雜,有的直接繼承 View 或 ViewGroup,有的卻直接繼承系統的原生控件,有的能夠直接使用系統定義的屬性,而有的卻自定義了本身的屬性。
實際上實現自定義 View 的方式,從總體上看,只分爲三種:組合控件,繼承控件,自繪控件。而後就是根據須要來添加自定義的屬性,就這麼簡單。android
組合控件,顧名思義,就是將系統原有的控件進行組合,構成一個新的控件。這種方式下,不須要開發者本身去繪製圖上顯示的內容,也不須要開發者重寫 onMeasure,onLayout,onDraw 方法來實現測量、佈局以及 draw 流程。因此,在實現自定義 view 的三種方式中,這一種相對比較簡單。
實際開發中,標題欄就是一個比較常見的例子。由於在一個 app 的各個界面中,標題欄基本上是大同小異,複用率很高。因此常常會將標題欄單獨作成一個自定義 view,在不一樣的界面直接引入便可,而不用每次都把標題欄佈局一遍。本節就自定義一個標題欄,包含標題和返回按鈕兩個控件,來介紹這種組合控件的實現方式。canvas
定義標題欄的佈局文件 custom_title_view.xml,將返回按鈕和標題文本進行組合。這一步用於肯定標題欄的樣子,代碼以下所示:app
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_orange_light"> <Button android:id="@+id/btn_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:text="Back" android:textColor="@android:color/white" /> <TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Title" android:textColor="@android:color/white" android:textSize="20sp" /> </RelativeLayout>
public class CustomTitleView extends FrameLayout implements View.OnClickListener { private View.OnClickListener mLeftOnClickListener; private Button mBackBtn; private TextView mTittleView; public CustomTitleView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.custom_title_view, this); mBackBtn = findViewById(R.id.btn_left); mBackBtn.setOnClickListener(this); mTittleView = findViewById(R.id.title_tv); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_left: if (mLeftOnClickListener != null) { mLeftOnClickListener.onClick(v); } break; } } public void setLeftOnClickListener(View.OnClickListener leftOnClickListener) { mLeftOnClickListener = leftOnClickListener; } public void setTittle(String title){ mTittleView.setText(title); } }
爲了編譯理解和記憶,這裏對該部分作一點說明:ide
在 Activity 的佈局文件 activity_custom_view_compose_demo.xml 中,像使用系統控件同樣使用 CustomTitleView 便可。前說了,CustomTitleView 本身就是繼承的現成的系統佈局,因此它們擁有的屬性特性,CustomTitleView 同樣擁有。函數
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.CustomTitleView android:id="@+id/customview_title" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.demos.customviewdemo.CustomTitleView> </RelativeLayout>
public class CustomViewComposeDemoActivity extends AppCompatActivity { private CustomTitleView mCustomTitleView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_custom_view_compose_demo); mCustomTitleView = findViewById(R.id.customview_title); mCustomTitleView.setTittle("This is Title"); mCustomTitleView.setLeftOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); } }
獲取到 CustomTitleView 實例,設置標題文字,而後自定義"Back"按鈕點擊事件 setLeftOnClickListener()。工具
按照如上的 4 步,就經過組合控件完成了一個比較簡單的自定義標題欄。可見,這種方式是很是簡單的。佈局
經過繼承系統控件(View 子類控件或 ViewGroup 子類控件)來完成自定義 View,通常是但願在原有系統控件基礎上作一些修飾性的修改,而不會作大幅度的改動,如在 TextView 的文字下方添加下劃線,在 LinearLayout 佈局中加一個蒙板等。這種方式每每都會複用系統控件的 onMeasure 和 onLayout 方法,而只須要重寫 onDraw 方法,在其中繪製一些須要的內容。下面會分別繼承 View 類控件和 ViewGroup 類控件來舉例說明。測試
以下示例爲在 TextView 文字下方顯示紅色下劃線,其基本步驟以下:
(1)繼承 View 控件,並重寫 onDraw 方法this
@SuppressLint("AppCompatCustomView") public class UnderlineTextView extends TextView{ public UnderlineTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(Color.RED); paint.setStrokeWidth(5); int width = getWidth(); int height = getBaseline(); canvas.drawLine(0,height,width,height,paint); } }
(2)在佈局文件中調用
就像使用一個普通 TextView 同樣使用 UnderlineTextView。spa
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.UnderlineTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="50dp" android:layout_centerInParent="true" android:text="Hello World!"/> </RelativeLayout>
(3)效果圖
以下示例演示,在 layout 佈局上添加一個淺紅色的半透明蒙板,這種需求在工做中也是很是常見的。
(1)繼承 ViewGroup 類系統控件
public class ForegroundLinearLayout extends LinearLayout{ public ForegroundLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); canvas.drawColor(Color.parseColor("#50FF0000")); } }
(2)在佈局文件中調用
對 ForegroundLinearLayout 的使用,就和使用其父類 LinearLayout 同樣。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.ForegroundLinearLayout android:layout_width="match_parent" android:layout_height="200dp" android:layout_centerInParent="true" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Hello World!" android:textColor="@android:color/black" android:textSize="50dp" /> </com.example.demos.customviewdemo.ForegroundLinearLayout> </RelativeLayout>
(3)效果圖
在寬爲全屏寬度,高爲 200dp 的佈局範圍內,繪製完子其子控件 TextView 後,在上面覆蓋了一層淺紅色的半透明蒙板。
從上面兩個例子可見,繼承系統原有的控件來實現自定義 View,步驟很是簡單,比組合控件簡單多了。可是這一節須要對 Canvas,paint 等繪製方面的知識有必定的瞭解,且還須要對 ViewGroup 的中內容的繪製順序有必定的瞭解,才能在原生控件的基礎上作出想要的效果來。
這三種方法中,自繪控件是最複雜的,由於全部的繪製邏輯和流程都須要本身完成。採用自繪控件這種方式時,若是自定義 View 爲最終的葉子控件,那麼須要直接繼承 View;而不過自定義 View 爲容器類控件,則須要直接繼承 ViewGroup。這裏依然針對直接繼承 View 和 ViewGroup 分別舉例進行說明。
一、自繪葉子 View 控件
這裏經過畫一個直方圖來展現自繪 View 控件的實現。
自繪葉子 View 控件時,最主要工做就是繪製出豐富的內容,這一過程是在重寫的 onDraw 方法中實現的。因爲是葉子 view,它沒有子控件了,因此重寫 onLayout 沒有意義。onMeasure 的方法能夠根據本身的須要來決定是否須要重寫,不少狀況下,不重寫該方法並不影響正常的繪製。
public class HistogramView extends View{ private Paint mPaint; private Path mPath; public HistogramView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPath = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //繪製座標軸 mPaint.reset(); mPath.reset(); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPath.moveTo(100,100); mPath.rLineTo(0,402); mPath.rLineTo(800,0); canvas.drawPath(mPath,mPaint); //繪製文字 mPaint.reset(); mPaint.setTextSize(30); mPaint.setStyle(Paint.Style.FILL); canvas.drawText("Froyo",160,540,mPaint); canvas.drawText("CB",280,540,mPaint); canvas.drawText("ICS",380,540,mPaint); canvas.drawText("J",480,540,mPaint); canvas.drawText("KitKat",560,540,mPaint); canvas.drawText("L",690,540,mPaint); canvas.drawText("M",790,540,mPaint); //繪製直方圖,柱形圖是用較粗的直線來實現的 mPaint.reset(); mPaint.setColor(Color.GREEN); mPaint.setStrokeWidth(80); float[] lines3={ 200,500,200,495, 300,500,300,480, 400,500,400,480, 500,500,500,300, 600,500,600,200, 700,500,700,150, 800,500,800,350, }; canvas.drawLines(lines3,mPaint); } }
(2)在 Activity 界面的佈局文件中引入
和其它自定義控件同樣,直接在佈局文件中引入便可。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.HistogramView android:layout_width="match_parent" android:layout_height="wrap_content"/> </RelativeLayout>
(3)效果圖
這裏經過自定義一個父佈局控件,並添加一個子 view 來做爲例子講解該方法的實現。
(1)直接繼承 ViewGroup 類
自繪 ViewGroup 控件,須要直接繼承 ViewGroup,在該系列第一篇文章中將繪製流程的時候就講過,onLayout 是 ViewGroup 中的抽象方法,其直接繼承者必須實現該方法。因此這裏,onLayout 方法必需要實現的,若是這裏面的方法體爲空,那該控件的子 view 就沒法顯示了。要想準確測量,onMeasure 方法也是要重寫的。下面例子中,只演示了第一個子 view 的測量和佈局,onLayout 方法中的 child.layout,就完成了對子 view 的佈局。
public class CustomLayout extends ViewGroup { public CustomLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { //只測量第一個child View child = getChildAt(0); measureChild(child, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { //只佈局第一個child View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); } } }
(2)在佈局文件中和普通父佈局同樣被引入
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.CustomLayout android:layout_width="match_parent" android:layout_centerInParent="true" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="50dp"/> </com.example.demos.customviewdemo.CustomLayout> </RelativeLayout>
(3)效果圖
上述代碼中 android:layout_centerInParent="true"沒有起效,從佈局上看 TextView 應該是處於屏幕的正中央,可是實際結果卻仍是在左上方顯示。這是由於 CustomLayout 控件,並無實現 android:layout_centerInParent 這個屬性,因此是無效的。關於屬性的問題,正是下一節要介紹的內容。
咱們在使用 Android 原生控件的時候,常常能夠看到在佈局文件中能夠設置不少的屬性值,如:
<TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Title" android:textColor="@android:color/white" android:textSize="20sp" />
這裏能夠根據須要隨時設置 TextView 要顯示的文字,文字顏色,文字大小等各類屬性,給使用者帶來了極大的方便。咱們在使用自定義 View 的時候,也很是但願可以像 TextView 等系統原生控件同樣經過設置屬性值來個性化自定義 View。本節我們在上一節自定義直方圖的基礎上,來介紹自定義屬性的基本使用流程。
在 res/values/下新建資源文件,這裏我們命名爲 attrs.xml,在其中編寫所須要的屬性
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="HistogramView"> <attr name="textColor" format="color"/> <attr name="histogramColor" format="color"/> </declare-styleable> </resources>
這裏"declare-styleable"中的 name 是自行命名的,能夠理解爲這個自定義屬性集合的名稱。代碼中包含了兩個自定義屬性,名稱分別爲"textColor"和"histogramColor",這裏用來設置直方圖中文字的顏色和直方圖的顏色。format 表示的是屬性的格式,這裏均設置爲"color",表示對應的屬性是用來設置顏色值的。對於"format",後面還會詳細講到。其它的就是固定的格式了,直接套用就行。
public class HistogramView extends View{ private Paint mPaint; private Path mPath; private int mTextColor,mHistogramColor; public HistogramView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPath = new Path(); initAttrs(context,attrs); } private void initAttrs(Context context, AttributeSet attrs){ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HistogramView); mTextColor = typedArray.getColor(R.styleable.HistogramView_textColor,Color.BLACK); mHistogramColor = typedArray.getColor(R.styleable.HistogramView_histogramColor,Color.GREEN); typedArray.recycle(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //繪製座標軸 mPaint.reset(); mPath.reset(); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPath.moveTo(100,100); mPath.rLineTo(0,402); mPath.rLineTo(800,0); canvas.drawPath(mPath,mPaint); //繪製文字 mPaint.reset(); mPaint.setTextSize(30); mPaint.setColor(mTextColor); mPaint.setStyle(Paint.Style.FILL); canvas.drawText("Froyo",160,540,mPaint); canvas.drawText("CB",280,540,mPaint); canvas.drawText("ICS",380,540,mPaint); canvas.drawText("J",480,540,mPaint); canvas.drawText("KitKat",560,540,mPaint); canvas.drawText("L",690,540,mPaint); canvas.drawText("M",790,540,mPaint); //繪製直方圖,柱形圖是用較粗的直線來實現的 mPaint.reset(); mPaint.setColor(mHistogramColor); mPaint.setStrokeWidth(80); float[] lines3={ 200,500,200,495, 300,500,300,480, 400,500,400,480, 500,500,500,300, 600,500,600,200, 700,500,700,150, 800,500,800,350, }; canvas.drawLines(lines3,mPaint); } }
將上述代碼和前面第三節中自繪直方圖代碼對比,紅色部分是修改或新增的代碼。初始化屬性的地方,這個過程須要在構造函數中完成。其中,和自定義屬性集創建聯繫,獲取開發者在佈局文件中使用時設置的相應屬性值,若是沒有設置,則會使用默認設置的顏色,分別爲 Color.BLACK 和 Color.GREEN,用完後必定要回收資源。這樣就初始化了文字顏色 mTextColor 值和 mHistogramColor 值,在後面 onDraw 中就使用該值來繪製對應的部分。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.demos.customviewdemo.HistogramView android:layout_width="match_parent" android:layout_height="wrap_content" app:textColor="@android:color/holo_red_light" app:histogramColor="@android:color/holo_blue_bright"/> </RelativeLayout>
這段代碼中,第 3,10,11 行和以往的佈局文件有些不同,這是使用自定義屬性時的固定格式。第 3 行中,若是佈局文件中沒有這一句,必定要加上,這句是聲明命名空間,只有聲明瞭命名空間才能使用自定義屬性。"app"是該命名空間的名稱,這裏是自行命名的,不必定非要用"app"。第 10 行和 11 行,"app:attrName"表示用的是自定義的屬性,固定用法,前面 mTextColor 和 mHistogramColor 值就是從這裏獲取的。
在上面一節中,僅僅只是對文字顏色和直方圖顏色的屬性值作了設置,是爲了演示自定義屬性的使用步驟。在實際開發中,徹底能夠定義更多類型的屬性,如顯示文字的內容,文字的大小,直方圖的寬度等。format 也不僅限定於"color",還有"String","Integer"等,多種多樣。本節就彙總一下平時比較經常使用的一些屬性 format。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SelfAttr"> <!--1.reference:參考某一資源ID--> <attr name="background" format="reference" /> <!--2. color:顏色值--> <attr name = "textColor" format = "color" /> <!--3.boolean:布爾值--> <attr name = "focusable" format = "boolean" /> <!--4.dimension:尺寸值--> <attr name = "layout_width" format = "dimension" /> <!--5. float:浮點值--> <attr name = "fromAlpha" format = "float" /> <!--6.integer:整型值--> <attr name = "lines" format="integer" /> <!--7.string:字符串--> <attr name = "text" format = "string" /> <!--8.fraction:百分數--> <attr name = "pivotX" format = "fraction" /> <!--9.enum:枚舉值。屬性值只能選擇枚舉值中的一個--> <attr name="orientation"> <enum name="horizontal" value="0" /> <enum name="vertical" value="1" /> </attr> <!--10.flag:位或運算。屬性值能夠選擇其中多個值--> <attr name="gravity"> <flag name="top" value="0x01" /> <flag name="bottom" value="0x02" /> <flag name="left" value="0x04" /> <flag name="right" value="0x08" /> <flag name="center_vertical" value="0x16" /> ... </attr> <!--11.混合類型:屬性定義時能夠指定多種類型值--> <attr name = "background_2" format = "reference|color" /> </declare-styleable> </resources>
如上列出了平時工做中在常見的 11 種類型的格式,說是 11 種,但最後一種是前面 10 種的組合而已。看到上述的屬性名稱應該很熟悉吧,都是系統原生控件的屬性名稱。
以下對上述屬性的使用一一舉例演示,能夠對照着來理解,都是平時經常使用的系統控件。
<!--1.reference:參考某一資源ID--> <ImageView android:background = "@drawable/圖片ID"/> <!--2. color:顏色值--> <TextView android:textColor = "#00FF00"/> <!--3.boolean:布爾值--> <Button android:focusable = "true"/> <!--4.dimension:尺寸值--> <Button android:layout_width = "42dp"/> <!--5. float:浮點值--> <alpha android:fromAlpha = "1.0"/> <!--6.integer:整型值--> <TextView android:lines="1"/> <!--7.string:字符串--> <TextView android:text = "我是文本"/> <!--8.fraction:百分數--> <rotate android:pivotX = "200%"/> <!--9.enum:枚舉值--> <LinearLayout android:orientation = "vertical"> </LinearLayout> <!--10.flag:位或運算--> <TextView android:gravity="bottom|left"/> <!--11.混合類型:屬性定義時能夠指定多種類型值--> <ImageView android:background = "@drawable/圖片ID" /> <!--或者--> <ImageView android:background = "#00FF00" />
關於自定義 View 的 3 中實現方式以及自定義屬性的使用,這裏就講完了。讀完後,是否是發現基本的實現流程其實很是簡單。固然,本文爲了說明實現流程,因此舉的例子都比較簡單,但不是說繪製內容也同樣簡單。就好像辦理入學手續很簡單,但讀書這件事卻不那麼容易同樣。要完成一些酷炫的自定義 View,還須要好好地掌握 Canvas,Paint,Path 等工具的使用,以及 View 的繪製流程原理。
Android 自定義View篇(一)View繪製流程
Android 自定義View篇(二)Canvas詳解
Android 自定義View篇(三)Paint詳解