浅谈 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 开源之旅】
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于