Creating custom and compound Views in Android - Tutorial(翻譯)
譯前的:
之前做了三篇學習筆記,從知乎上面看到了這篇英文的推薦,總的來說可以是一篇導讀,沒有相關的學習,看這篇,可以作爲一個學習脈絡導向;有相關的學習底子,可以作爲一個基礎夯實、思維理清。沒想到一翻譯就是四個多小時…英語渣,很多詞句都不太準確,幸好有之前的學習基礎打底…
1. Custom Views 自定義視圖
1.1 Default views 默認視圖
Android框架提供了多種默認視圖,但是開發者們也可以在他們的應用中創造、使用他們自己定義的視圖。視圖的基類(父類)是View
。
View
的子類有三大塊——ImageView
、TextView
、ViewGroup
。
1.2Android在視圖層次結構上的繪製
一旦一個Activity
獲得焦點,它必然會向Android系統提供它佈局的根結點,之後Android系統會開始繪製步驟。
繪製(Drawing)是從佈局的根結點開始的,佈局層次的繪製順序爲聲明的順序,例如,父view的繪製先於它的子view,而子view的繪製順序也是按照聲明的順序。
繪製佈局(layout)有兩個階段:
- 計算階段(measuring)——實現了
measrue(int,int)
方法並且是在控件層次中自上而下的遍歷順序(and is a top-down traversal of the view hierarchy)。每個視圖存儲它自己的尺寸數據。 - 佈局階段(layout)——實現了
layout(int,int,int,int)
方法,也同樣是自上而下的順序。在這個階段,每個佈局管理器通過在上一階段計算好的尺寸數據,來安置他們所有的子view的位置。
note:
計算階段和佈局階段通常都是一起發生的。
佈局管理器可以多次調用計算階段進行多次計算。例如LinearLayout
支持weight
屬性(根據權重,分配view之間的空餘空間),像RelativeLayout
多次計算子view來解決佈局文件中設置的約束。
一個view或者activity的計算階段和佈局階段可以通過調用requestLayout()
被重新觸發。
在計算和佈局階段之後,view們都會自己繪製自己。這個操作可以被定義在View
類中的invalidate()
方法觸發(譯者注:invalidate()
方法適用於ui線程中,而在非ui線程中,要重新調用onDraw()
方法,只能用postInvalidate()
方法)。
更詳細深入的說明,參見Android Graphical Architecture。
1.3 創造view的理由
視圖是典型的用於提供用戶體驗的組件,不可能總是使用默認提供的視圖。自定義view的使用允許開發者展現最優的展示效果。例如,自定義佈局可以讓開發者在他的特定需求下作出最優的佈局管理。
1.4 view的職責
view需要測量尺寸、對子view的位置進行佈局、繪製自身及其子view(例如ViewGroup
),除此之外,還需要保存ui的狀態和負責觸摸事件的處理。
1.5 自定義view的步驟
自定義視圖是典型的複合視圖或者自己定義的視圖。一般自定義視圖的方式爲:
1. view的組合
2. 自定義視圖
- 擴展現有的view
- 擴展父類
View
1.6 在佈局文件中使用新的view
在佈局文件中,自定義視圖和複合視圖的聲明,需要使用完全限定的名稱,即包名+類名這樣的結構。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
<de.vogella.android.ownview.MyDrawView
android:id="@+id/myDrawView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
小技巧:
和android命名空間類似,你也可以再佈局文件中聲明你自己的命名空間。
1.7 創建screenshots (Images) of views
每個View
類都支持創建一個內容爲當前顯示的圖片對象。下面的代碼就是例子。
# Build the Drawing Cache
view.buildDrawingCache();
# Create Bitmap
Bitmap cache = view.getDrawingCache();
# Save Bitmap
saveBitmap(cache);
view.destroyDrawingCache();
2 複合視圖
複合視圖(也叫組合控件)是指基於現有的view和預先定義好的view在ViewGroup
中進行定義。
複合視圖也允許你增加自定義的api和查詢狀態。
爲了實現這樣的控制,你需要定義一個佈局文件並將其和複合視圖進行聯繫。複合視圖的實現中,你要預先定義好視圖之間的交互。首先定義一個佈局文件,擴展相應的ViewGroup
的子類,在這個子類的實現中,你需要將其和之前的佈局文件聯繫起來,再實現相關的視圖邏輯。
小技巧:
由於顯示的原因,你或許想將你的複合視圖重寫爲擴展了View
的自定義視圖,這或許能夠有效的降低你的視圖的佈局層次。這樣情況下如果你正確的實現了,視圖層次的遍歷會更少,繪製視圖會變得更快。
3 創建自定義視圖
3.1 創建自定義視圖
通過繼承View
類或者它的子類,你可以實現自己的view。
繪製view是通過使用onDraw()
方法,在這個方法中,你可以獲得一個可以進行繪製操作的Canvas
類,例如繪製直線、圓形、文本或者位圖。如果view需要被重繪,你應該調用invalidate()
方法。
小技巧:
如果你定義你自己的視圖,在定義之前看一下ViewConfiguration
這個類,它裏面包含着數個定義視圖的常量。
繪製視圖通常會用到2D Canvas API
3.2 測量(Measurement)
佈局管理器調用onMeasure()
方法,視圖從佈局管理器中獲取到佈局屬性參數。一個佈局管理器負責確定它所有子view的尺寸。
視圖必須調用setMeasureDimenstion(int,int)
方法來設置最後的結果。
3.3 定義自定義佈局管理(Layout managers)
你可以實現通過繼承ViewGroup
類來實現自定義的佈局管理。這使得你能夠實現更加有效的佈局管理或者實現當前安卓平臺上缺少的一些佈局。
一個自定義佈局管理需要重寫onMeasrue()
和onLayout
方法,指定其子view的計算策略。舉例來說,it can leave out the time consuming support of layout_weight of the LinearLayout class.(這句話沒看懂什麼意思…)
通過重寫ViewGroup學習onMeasure()和onLayout()方法
小技巧:
可以使用ViewGroup
類中的measureChildWithMargins()
方法來計算子view的尺寸。
在繼承了ViewGroup
類的自定義類中用內部類存儲額外的佈局參數,是一種很好的方式。例如ViewGroup.LayoutParams
的實現了一些通常的佈局參數,而LinearLayout.LayoutParams
則是內含一些特定使用在該佈局上的參數,像layout_weight
這個屬性。
4 生命週期
4.1 和窗口有關的生命週期事件
視圖顯示,那麼該視圖應當是包含于于一個佈局層次之中,而該佈局應當是隸屬於當前的窗口的。view是擁有多個和生命週期有關的鉤子函數。一旦當前窗口可被獲得,方法onAttachedToWindow()
就會被調用。
在view從其父view中被移除(前提是這個父view隸屬於一個窗口),方法onDetachedFromWindow()
會被調用。通常發生於這樣的情景:如果一個activity被回收(例如通過調用finish()
方法)或者一個在ListView
中的view被回收,方法onDetachedFromWindow()
可以用來停止動畫、清理被view使用的資源。
4.2 遍歷生命週期事件
這些事件包括 動畫(Animate)、測量(Measure)、佈局(Layout)和繪製(Draw)。
所有的view都必須瞭解如何測量和佈局自己。view可以通過調用requestLayout()
方法來使得父view重新測量、佈局自己(即父view需要重新調用父view的onMeasure()
和onLayout
方法)。這個操作獲取也會導致佈局中的其他view同樣調用requestLayout()
向他們的父view要求重新測量和佈局。
小技巧:
這樣的遞歸調用也是爲什麼不建議你嵌套多層佈局,如果很多層次的都需要重新計算,測量和佈局方法調用代價將很高。
方法onMeasure()
用於定義自身及其子view的尺寸。在函數接收之前,必須通過setMeasuredDimension()
這個方法來設置尺寸。
方法onLayout()
基於方法調用onMeasure()
的結果來放置子view。這種調用通常只發生一次。 This call happens typically once, which onMeasure() can happens once.(翻譯感覺有問題…)
4.3 Activity的生命週期
view沒有和activity的聲明週期事件相聯繫的途徑。如果view希望獲知這些事件的信息,你應該在相關的view中聲明一個接口,並且在activity中相應的事件中調用接口。
5 爲你的自定義view定義額外的屬性
你可以爲你的複合視圖或者自定義視圖定義額外的屬性。在res/values路徑下,新建名爲attrs.xml的屬性文件。下面的代碼示例如何定義一個名爲ColorOptionView
的自定義視圖。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ColorOptionsView">
<attr name="titleText" format="string" localization="suggested" />
<attr name="valueColor" format="color" />
</declare-styleable>
</resources>
爲了能夠在你的佈局文件中使用你自定義出來的屬性,你需要在xml文件首部去定義他們。下面的代碼通過xmlns:custom
來聲明命名空間。這些屬性也被分配給了視圖。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<!-- define new name space for your attributes -->
xmlns:custom="http://schemas.android.com/apk/res/com.vogella.android.view.compoundview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<!-- Assume that this is your new component. It uses your new attributes -->
<com.vogella.android.view.compoundview.ColorOptionsView
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
custom:titleText="Background color"
custom:valueColor="@android:color/holo_green_light"
/>
</LinearLayout>
而下面的代碼演示如何在代碼中獲取這些屬性。
package com.vogella.android.view.compoundview;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
public class ColorOptionsView extends View {
private View mValue;
private ImageView mImage;
public ColorOptionsView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.Options, 0, 0);
String titleText = a.getString(R.styleable.Options_titleText);
int valueColor = a.getColor(R.styleable.Options_valueColor,
android.R.color.holo_blue_light);
a.recycle();
// more stuff
}
}
6 練習
這個就不翻譯了…
7 Canvas API
7.1 綜述
canves API允許繪製多種幾何效果。
需要在位圖上進行繪製。Canvas
類提供了多種在位圖上的繪圖方法,並且搭配Paint
類,可以有多種不同的風格樣式。
7.2 Canvas 類
Canvas
對象包含了作爲畫布的位圖。同樣也提供了繪製的方法。例如drawARGB()
方法來畫不同的顏色(好彆扭)、drawBitmap()
方法來繪製位圖、drawText()
方法來繪製文本,drawRoundRect()
來繪製圓角矩形等等等等。
7.3 Paint 類
使用Paint
類來在Canvas
上繪圖。
使用Paint
類來指定顏色、字體和特定效果的繪圖操作(我想這裏指的應該是不同的style)。
方法setStyle()
來指定是否僅僅繪製外框線(Paint.Style.STROKE
)、填充(Paint.Style.FILL
)和二者都有的樣式(Paint.Style.STROKE_AND_FILL
)。
使用Paint
類的setAlpha()
來設置透明度。
通過渲染器(Shader
)類,可以是的Patin
用多種顏色進行填充。
7.4 Shader 類
渲染器用來爲Paint
對象定義將要被繪製的內容,例如你可以使用BitmapShader
類來定義要被渲染位圖。舉例來講,你可以用來繪製圓形的圖片。簡單爲你的Paint
對象定義一個BitmapShader
並且使用drawRoundRect()
方法來畫出圓角矩形的圖片。
其他的由android平臺提供的渲染器是LinearGradient
、RadiaGradient
和SweepGradient
,用於繪製漸變色。
使用一個Shader
並通過setShader()
方法將其賦予給你的Paint
對象。
通過Shader
類的tile mode來定義當要渲染區域大於渲染器中原本的位圖或者內容區域時,這些多餘的區域的填充類型。常量Shader.TileMode.CLAMP
定義使用渲染元素的邊界部分來填充額外的區域。常量Shader.TileMode.MIRROR
定義使用鏡像的方式來填充而Shader.TileMode.REPEAT
定義單純使用重複的內容進行多餘部分的填充。
8 持有視圖數據
大多數標準視圖能夠保存他們的狀態,因此可以被系統持有。android系統調用onSaveInstanceState()
和onRestoreInstanceState(Parcable)
方法來保存和獲取視圖狀態。
約定俗成的是使用View.BasedSaveStage
的派生類作爲view中的靜態內部類來保存數據。
android系統基於ID搜索佈局中的視圖,傳遞存有它狀態的Bundle
對象給view。
你應當在使用離開的時候保存和復位用戶界面的狀態。
9 相關鏈接
- 原文鏈接
- 之前的學習筆記
Android自定義view學習筆記01
Android自定義view學習筆記02
Android自定義view學習筆記03