淺談Android主題樣式
文章末尾有附帶例子的原始碼連結, 感興趣的可以下載原始碼研究, 味道更佳.
在講Android主題之前, 讓我們先回顧一下Android中自定義View的實現方法.
自定義View
完全自定義View實現自定義控制元件
自定義View、ViewGroup或者SurfaceView:
- 自定義View:主要重寫onDraw(繪製)方法。自定義View實現例子
- 自定義ViewGroup:主要重寫:onMeasure(測量)、onLayout(佈局)這兩個方法。自定義ViewGroup實現例子
- 自定義SurfaceView:建立RenderThread,然後呼叫
SurfaceHolder的.lockCanvas
方法獲取畫布,再呼叫SurfaceHolder的.unlockCanvasAndPost
方法將繪製的畫布投射到螢幕上。
class CustomSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : SurfaceView(context, attrs), SurfaceHolder.Callback {
private var mSurfaceHolder: SurfaceHolder = holder
private lateinit var mRenderThread: RenderThread
private var mIsDrawing = false
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceCreated(holder: SurfaceHolder) {
// 開啟RenderThread
mIsDrawing = true
mRenderThread = RenderThread()
mRenderThread.start()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// 銷燬RenderThread
mIsDrawing = false
mRenderThread.interrupt()
}
/**
* 繪製介面的執行緒
*/
private inner class RenderThread : Thread() {
override fun run() {
// 不停繪製介面
while (mIsDrawing) {
drawUI()
try {
sleep(...) // 重新整理間隔
} catch (_: InterruptedException) {
}
}
}
}
/**
* 介面繪製
*/
private fun drawUI() {
val canvas = mSurfaceHolder.lockCanvas()
try {
drawCanvas(canvas)
} catch (e: Exception) {
e.printStackTrace()
} finally {
mSurfaceHolder.unlockCanvasAndPost(canvas)
}
}
}
繼承元件的方式實現自定義控制元件
最簡單的自定義元件的方式,直接繼承需要擴充/修改的控制元件,重寫對應的方法即可。
一般是希望在原有系統控制元件基礎上做一些修飾性的修改(功能增強),而不會做大幅度的改動。
組合的方式實現自定義控制元件
組合控制元件就是將多個控制元件組合成一個新的控制元件,可以重複使用。
實現組合控制元件的一般步驟如下:
- 編寫佈局檔案
- 實現構造方法
- 初始化UI,載入佈局
- 對外提供修改的介面api
可以看到,組合的方式和我們平時寫一個Fragment的流程是很類似的。
Theme主題
應用於窗體級別,是一整套樣式的組合,採取就近原則:Application > Activity > ViewGroup > View。 一般而言,Theme主要應用於Application和Activity這樣的窗體,主要放在/res/values/themes.xml
。
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Application中的Theme
Application的主題一般在Manifest
中,它只對在Manifest
中未設定Theme的Activity生效。
<application android:theme="@style/AppTheme">
</application>
Activity中的Theme
Activity的主題可以在Manifest
和程式碼中呼叫setTheme
設定。一般在Activity的onCreate()中,setContentView
方法之前設定。
1.在Manifest
中設定。
<activity android:theme="@style/DialogTheme">
</activity>
2.程式碼中呼叫setTheme
設定,注意一定要在呼叫setContentView(View)
和inflate(int, ViewGroup)
方法前。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme)
setContentView(R.layout.layout_main)
}
ViewGroup和View中的Theme
ViewGroup和View的主題一般在佈局xml中設定,使用android:theme
設定。
<ViewGroup
android:theme="@style/ThemeOverlay.App.Foo">
<Button android:theme="@style/ThemeOverlay.App.Bar" />
</ViewGroup>
Style樣式
僅應用於單個View這種窗體元素級別的外觀,主要放在/res/values/styles.xml
。
Style的宣告
樣式的宣告,一般放在/res/values/...
目錄下帶styles
的檔案中,使用<style name="style-name"> </style>
進行設定。
<style name="style-name" parent="parent-style-name">
<item name="attr-name1">value1</item>
<item name="attr-name2">value2</item>
<item name="attr-name3">value3</item>
</style>
Style的使用
樣式一般在佈局xml中設定,使用android:style
設定,不同於主題,樣式只能應用於單個View,對於其子View並不會生效。
<ViewGroup
android:style="@style/ActionContainerStyle">
<Button android:style="@style/BlueButtonStyle" />
</ViewGroup>
Style的優先順序順序
如果我們在多個地方給控制元件指定了style的屬性,那麼最終是由誰生效呢?這裡我們就以TextView為例,介紹一下Style的生效規則:
- 1.透過文字span將字元設定的樣式應用到TextView派生的類。
- 2.以程式碼方式動態設定的屬性。
- 3.將單獨的屬性直接應用到View。
- 4.將樣式應用到View。
- 5.控制元件的預設樣式,在View構造方法中定義的。
- 6.控制元件所處應用、Activity、父佈局所應用的主題。
- 7.應用某些特定於View的樣式,例如為TextView設定TextAppearance。
具體程式碼可參考: StyleRuleFragment
Attribute屬性
Attribute屬性是組成Style的基本單位。如果說主題是各種樣式的組合,那麼樣式就是各種屬性的組合,主要放在/res/values/attrs.xml
。
Attribute的宣告
1.單個屬性的定義
<resource>
<attr name="attr-name" format="format-type" />
</resource>
2.一組屬性的定義
<resource>
<declare-styleable name="XXXXView">
<attr name="attr-name" format="format-type" />
<attr name="attr-name" format="format-type" />
</declare-styleable>
</resource>
3.屬性的賦值
<style name="xx">
<item name="attr-name">value</item>
</style>
Attribute的使用
使用?attr/xxx
或者?xxx
進行引用。這裡xxx是定義的屬性名(attr-name)。
<TextView
android:foreground="?attr/selectableItemBackground"
android:textColor="?colorAccent" />
Attribute的獲取
- 屬性集的獲取: 使用
context.obtainStyledAttributes
進行整體獲取。
val array = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView, defStyleAttr, defStyleRes)
size = array.getInteger(R.styleable.CustomTextView_ctv_size, size)
isPassword = array.getBoolean(R.styleable.CustomTextView_ctv_is_password, isPassword)
array.recycle()
- 單個屬性的獲取: 使用
context.theme.resolveAttribute
進行獲取。
fun Resources.Theme.resolveAttributeToDimension(@AttrRes attributeId: Int, defaultValue: Float = 0F) : Float {
val typedValue = TypedValue()
return if (resolveAttribute(attributeId, typedValue, true)) {
typedValue.getDimension(resources.displayMetrics)
} else {
defaultValue
}
}
fun Context.resolveDimension(@AttrRes attributeId: Int, defaultValue: Float = 0F) : Float {
val typedArray = theme.obtainStyledAttributes(intArrayOf(attributeId))
return try {
typedArray.getDimension(0, defaultValue)
} finally {
typedArray.recycle()
}
}
最後
以上內容的全部原始碼我都放在了github上, 感興趣的小夥伴可以下下來研究和學習.
專案地址: https://github.com/xuexiangjys/UIThemeSample
我是xuexiangjys,一枚熱愛學習,愛好程式設計,勤于思考,致力於Android架構研究以及開源專案經驗分享的技術up主。獲取更多資訊,歡迎微信搜尋公眾號:【我的Android開源之旅】