淺談Android主題樣式

xuexiangjys發表於2023-03-12

淺談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)
        }
    }
}

自定義SurfaceView實現例子

繼承元件的方式實現自定義控制元件

最簡單的自定義元件的方式,直接繼承需要擴充/修改的控制元件,重寫對應的方法即可。

一般是希望在原有系統控制元件基礎上做一些修飾性的修改(功能增強),而不會做大幅度的改動。

繼承元件實現例子

組合的方式實現自定義控制元件

組合控制元件就是將多個控制元件組合成一個新的控制元件,可以重複使用。

實現組合控制元件的一般步驟如下:

  • 編寫佈局檔案
  • 實現構造方法
  • 初始化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開源之旅】

相關文章