{"id":19765747,"url":"https://github.com/xuexiangjys/uithemesample","last_synced_at":"2025-04-30T15:31:00.549Z","repository":{"id":101672098,"uuid":"512331210","full_name":"xuexiangjys/UIThemeSample","owner":"xuexiangjys","description":"Android的UI主题使用案例","archived":false,"fork":false,"pushed_at":"2023-12-15T05:47:11.000Z","size":217,"stargazers_count":7,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-06T02:51:08.716Z","etag":null,"topics":["android","samples","ui-design","x-samples"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xuexiangjys.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"custom":"https://gitee.com/xuexiangjys/Resource/blob/master/doc/sponsor.md"}},"created_at":"2022-07-10T02:58:04.000Z","updated_at":"2023-11-06T02:39:39.000Z","dependencies_parsed_at":"2023-06-07T16:45:41.750Z","dependency_job_id":null,"html_url":"https://github.com/xuexiangjys/UIThemeSample","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xuexiangjys%2FUIThemeSample","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xuexiangjys%2FUIThemeSample/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xuexiangjys%2FUIThemeSample/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xuexiangjys%2FUIThemeSample/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xuexiangjys","download_url":"https://codeload.github.com/xuexiangjys/UIThemeSample/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251732014,"owners_count":21634708,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","samples","ui-design","x-samples"],"created_at":"2024-11-12T04:19:18.303Z","updated_at":"2025-04-30T15:31:00.152Z","avatar_url":"https://github.com/xuexiangjys.png","language":"Kotlin","funding_links":["https://gitee.com/xuexiangjys/Resource/blob/master/doc/sponsor.md"],"categories":[],"sub_categories":[],"readme":"# UIThemeSample\n\nAndroid的UI主题使用案例\n\n## 关于我\n\n| 公众号   | 掘金     |  知乎    |  CSDN   |   简书   |   思否  |   哔哩哔哩  |   今日头条\n|---------|---------|--------- |---------|---------|---------|---------|---------|\n| [我的Android开源之旅](https://t.1yb.co/Irse)  |  [点我](https://juejin.im/user/598feef55188257d592e56ed/posts)    |   [点我](https://www.zhihu.com/people/xuexiangjys/posts)       |   [点我](https://xuexiangjys.blog.csdn.net/)  |   [点我](https://www.jianshu.com/u/6bf605575337)  |   [点我](https://segmentfault.com/u/xuexiangjys)  |   [点我](https://space.bilibili.com/483850585)  |   [点我](https://img.rruu.net/image/5ff34ff7b02dd)\n\n## 自定义View\n\n### 完全自定义View实现自定义控件\n\n自定义View、ViewGroup或者SurfaceView：\n\n* 自定义View：主要重写onDraw（绘制）方法。\n\n* 自定义ViewGroup：主要重写：onMeasure（测量）、onLayout（布局）这两个方法。\n\n* 自定义SurfaceView：创建RenderThread，然后调用`SurfaceHolder的.lockCanvas`方法获取画布，再调用`SurfaceHolder的.unlockCanvasAndPost`方法将绘制的画布投射到屏幕上。\n\n```kotlin\n\nclass CustomSurfaceView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : SurfaceView(context, attrs), SurfaceHolder.Callback {\n\n    private var mSurfaceHolder: SurfaceHolder = holder\n    private lateinit var mRenderThread: RenderThread\n    private var mIsDrawing = false\n    \n    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}\n    override fun surfaceCreated(holder: SurfaceHolder) {\n        // 开启RenderThread\n        mIsDrawing = true\n        mRenderThread = RenderThread()\n        mRenderThread.start()\n    }\n\n    override fun surfaceDestroyed(holder: SurfaceHolder) {\n        // 销毁RenderThread\n        mIsDrawing = false\n        mRenderThread.interrupt()\n    }\n\n    /**\n     * 绘制界面的线程\n     */\n    private inner class RenderThread : Thread() {\n\n        override fun run() {\n            // 不停绘制界面\n            while (mIsDrawing) {\n                drawUI()\n                try {\n                    sleep(...) // 刷新间隔\n                } catch (_: InterruptedException) {\n                }\n            }\n        }\n    }\n\n    /**\n     * 界面绘制\n     */\n    private fun drawUI() {\n        val canvas = mSurfaceHolder.lockCanvas()\n        try {\n            drawCanvas(canvas)\n        } catch (e: Exception) {\n            e.printStackTrace()\n        } finally {\n            mSurfaceHolder.unlockCanvasAndPost(canvas)\n        }\n    }\n}\n```\n\n### 继承组件的方式实现自定义控件\n\n最简单的自定义组件的方式，直接继承需要拓展/修改的控件，重写对应的方法即可。\n\n一般是希望在原有系统控件基础上做一些修饰性的修改（功能增强），而不会做大幅度的改动。\n\n### 组合的方式实现自定义控件\n\n\u003e 组合控件就是将多个控件组合成一个新的控件，可以重复使用。\n\n实现组合控件的一般步骤如下：\n\n* 编写布局文件\n* 实现构造方法\n* 初始化UI，加载布局\n* 对外提供修改的接口api\n\n可以看到，组合的方式和我们平时写一个Fragment的流程是很类似的。\n\n## Theme主题\n\n应用于窗体级别，是一整套样式的组合，采取就近原则：Application \u003e Activity \u003e ViewGroup \u003e View。 一般而言，Theme主要应用于Application和Activity这样的窗体，主要放在`/res/values/themes.xml`。\n\n```xml\n\u003cresources\u003e\n    \u003cstyle name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\"\u003e\n        \u003citem name=\"colorPrimary\"\u003e@color/colorPrimary\u003c/item\u003e\n        \u003citem name=\"colorPrimaryDark\"\u003e@color/colorPrimaryDark\u003c/item\u003e\n        \u003citem name=\"colorAccent\"\u003e@color/colorAccent\u003c/item\u003e\n    \u003c/style\u003e\n\u003c/resources\u003e\n```\n\n### Application中的Theme\n\nApplication的主题一般在`Manifest`中，它只对在`Manifest`中未设置Theme的Activity生效。\n\n```xml\n\u003capplication android:theme=\"@style/AppTheme\"\u003e\n\n\u003c/application\u003e\n```\n\n### Activity中的Theme\n\nActivity的主题可以在`Manifest`和代码中调用`setTheme`设置。一般在Activity的onCreate()中，`setContentView`方法之前设置。\n\n1.在`Manifest`中设置。\n\n```xml\n\u003cactivity android:theme=\"@style/DialogTheme\"\u003e\n\n\u003c/activity\u003e\n```\n\n2.代码中调用`setTheme`设置，注意一定要在调用`setContentView(View)`和`inflate(int, ViewGroup)`方法前。\n\n```kotlin\noverride fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setTheme(R.style.AppTheme)\n    setContentView(R.layout.layout_main)\n}\n```\n\n### ViewGroup和View中的Theme\n\nViewGroup和View的主题一般在布局xml中设置，使用`android:theme`设置。\n\n```xml\n\u003cViewGroup \n    android:theme=\"@style/ThemeOverlay.App.Foo\"\u003e\n    \n    \u003cButton android:theme=\"@style/ThemeOverlay.App.Bar\" /\u003e\n    \n\u003c/ViewGroup\u003e\n```\n\n## Style样式\n\n\u003e 仅应用于单个View这种窗体元素级别的外观，主要放在`/res/values/styles.xml`。\n\n### Style的声明\n\n样式的声明，一般放在`/res/values/...`目录下带`styles`的文件中，使用`\u003cstyle name=\"style-name\"\u003e \u003c/style\u003e`进行设置。\n\n```xml\n\u003cstyle name=\"style-name\" parent=\"parent-style-name\"\u003e\n    \u003citem name=\"attr-name1\"\u003evalue1\u003c/item\u003e\n    \u003citem name=\"attr-name2\"\u003evalue2\u003c/item\u003e\n    \u003citem name=\"attr-name3\"\u003evalue3\u003c/item\u003e\n\u003c/style\u003e\n```\n\n### Style的使用\n\n样式一般在布局xml中设置，使用`android:style`设置，不同于主题，样式只能应用于单个View，对于其子View并不会生效。\n\n```xml\n\u003cViewGroup \n    android:style=\"@style/ActionContainerStyle\"\u003e\n    \n    \u003cButton android:style=\"@style/BlueButtonStyle\" /\u003e\n    \n\u003c/ViewGroup\u003e\n```\n\n### Style的优先级顺序\n\n如果我们在多个地方给控件指定了style的属性，那么最终是由谁生效呢？这里我们就以TextView为例，介绍一下Style的生效规则：\n\n* 1.通过文本span将字符设置的样式应用到TextView派生的类。\n* 2.以代码方式动态设置的属性。\n* 3.将单独的属性直接应用到View。\n* 4.将样式应用到View。\n* 5.控件的默认样式，在View构造方法中定义的。\n* 6.控件所处应用、Activity、父布局所应用的主题。\n* 7.应用某些特定于View的样式，例如为TextView设置TextAppearance。\n\n## Attribute属性\n\n\u003e Attribute属性是组成Style的基本单位。如果说主题是各种样式的组合，那么样式就是各种属性的组合，主要放在`/res/values/attrs.xml`。\n\n### Attribute的声明\n\n1.单个属性的定义\n\n```xml\n\u003cresource\u003e\n\n    \u003cattr name=\"attr-name\" format=\"format-type\" /\u003e\n\n\u003c/resource\u003e\n```\n\n2.一组属性的定义\n\n```xml\n\u003cresource\u003e\n\n    \u003cdeclare-styleable name=\"XXXXView\"\u003e\n        \u003cattr name=\"attr-name\" format=\"format-type\" /\u003e\n        \u003cattr name=\"attr-name\" format=\"format-type\" /\u003e\n    \u003c/declare-styleable\u003e\n\n\u003c/resource\u003e\n```\n\n3.属性的赋值\n\n```xml\n\u003cstyle name=\"xx\"\u003e\n\n  \u003citem name=\"attr-name\"\u003evalue\u003c/item\u003e\n\n\u003c/style\u003e\n```\n\n### Attribute的使用\n\n使用`?attr/xxx`或者`?xxx`进行引用。这里xxx是定义的属性名（attr-name）。\n\n```xml\n\u003cTextView\n    android:foreground=\"?attr/selectableItemBackground\"\n    android:textColor=\"?colorAccent\" /\u003e\n```\n\n### Attribute的获取\n\n* 属性集的获取: 使用`context.obtainStyledAttributes`进行整体获取。\n\n```kotlin\nval array = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView, defStyleAttr, defStyleRes)\nsize = array.getInteger(R.styleable.CustomTextView_ctv_size, size)\nisPassword = array.getBoolean(R.styleable.CustomTextView_ctv_is_password, isPassword)\narray.recycle()\n```\n\n* 单个属性的获取: 使用`context.theme.resolveAttribute`进行获取。\n\n```kotlin\nfun Resources.Theme.resolveAttributeToDimension(@AttrRes attributeId: Int, defaultValue: Float = 0F) : Float {\n    val typedValue = TypedValue()\n    return if (resolveAttribute(attributeId, typedValue, true)) {\n        typedValue.getDimension(resources.displayMetrics)\n    } else {\n        defaultValue\n    }\n}\n\nfun Context.resolveDimension(@AttrRes attributeId: Int, defaultValue: Float = 0F) : Float {\n    val typedArray = theme.obtainStyledAttributes(intArrayOf(attributeId))\n    return try {\n        typedArray.getDimension(0, defaultValue)\n    } finally {\n        typedArray.recycle()\n    }\n}\n```\n\n## 如果觉得项目还不错，可以考虑打赏一波\n\n\u003e 你的打赏是我维护的动力，我将会列出所有打赏人员的清单在下方作为凭证，打赏前请留下打赏项目的备注！\n\n![pay.png](https://raw.githubusercontent.com/xuexiangjys/Resource/master/img/pay/pay.png)\n\n## 联系方式\n\n\u003e 更多资讯内容，欢迎扫描关注我的个人微信公众号:【我的Android开源之旅】\n\n![](https://s1.ax1x.com/2022/04/27/LbGMJH.jpg)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxuexiangjys%2Fuithemesample","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxuexiangjys%2Fuithemesample","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxuexiangjys%2Fuithemesample/lists"}