鲜为人知的完美动态设置view的style以及属性的技术方案

4,575 阅读4分钟

目前动态创建view中的痛点

一直以来 在android的世界中 我们都是通过xml来编写布局,这是目前最主流的方案,当然你也可以为了性能,全部以代码的形式来写你的布局 Kotlin高效绘制布局

除此之外,我们还经常遇到需要动态创建view的场景,这些动态创建的view和xml里面的view相比,一直以来都有着一些缺陷:

xml中的一些属性设置,很多时候在代码里并没有开放接口

举例:

android:textCursorDrawable

比如说 上述的属性 ,你想通过代码来给他设置 那是不可能的,因为textview 没有开放这个api。

这样类似的api有不少,大部分的解决方案是 通过反射来找到对应的field再给他赋值

然而即使如此,也依旧有一些xml中可以使用的属性 但是你通过反射也找不到对应赋值的地方

例如:

android:scrollbars="vertical"

这个困难非常尴尬,这不得不让我们在动态创建view的时候 又重新使用了inflate xml的老旧技术,在主线程中重新经历一次 inflate xml 反射 构建view对象的过程。

除此之外,对于动态view的创建来说,无法设置对应的style也是一个痛点,设想一下你xml中的textview 都使用了同一套style,结果你动态创建的textview 无法配套设置style,只能手动一个个写set方法 这是多么痛苦的一件事情(关键有些属性你还写不进去)

目前的解决方案

随着kotlin动态创建view的技术越发流行,lottie的作者 Airbnb开源了一个动态设置style库的方案 Airbnb的开源库

有兴趣的可以尝试一下,可以大部分的情况下解决前文中我们提到的痛点,但是这个方案的缺陷是在某些场景下兼容的还是不太好,同时作为一个新库,大家的接受程度普遍都不太高,还要占据一定的包大小。所以该方案并不完美。

推荐的方案一

对于谷歌大部分的view来说, 其构造函数通常有4个,我们以edittext为例:

image.png

通常而言,我们对第一个构造函数很熟悉,因为我们最常用的创建view的方法就是 new View(Context)

第二个构造函数也不会感到陌生,因为一般情况下我们在xml中设置一个view的时候 往往走的都是第二个构造函数

而对于第三个第四个 构造函数 就不太了解。其实这里第四个构造函数 是可以用来设置theme style的,

既然你可以设置style 那自然很多属性 也可以直接采用。

举例:


<style name="test_thme" parent="android:Widget.EditText">
    <item name="android:scrollbarSize">@dimen/dp2</item>
    <item name="android:background">@null</item>
    <item name="android:textColor">@color/color_333333</item>
    <item name="android:maxHeight">@dimen/dp61</item>
    <item name="android:textColorHint">@color/color_C6C6C6</item>
    <item name="android:textCursorDrawable">@drawable/space_forum_input_edit_cursor</item>
    <item name="android:gravity">left</item>
    <item name="android:textSize">@dimen/dp14</item>
    <item name="android:scrollbars">vertical</item>
    <item name="android:scrollbarStyle">outsideOverlay</item>
    <item name="android:scrollbarThumbVertical">@drawable/space_lib_vigour_scrollbar_handle_vertical</item>
</style>

这里千万要注意的是 一定不要忘记设置 parent="android:Widget.EditText"

你对那个view使用属性 这里parent就设置成他的组件,否则会出现一些奇怪的bug

最后在动态创建的时候 使用第四种构造函数 即可

EditText(context, null, 0, R.style.test_thme)

推荐方案二

对于上述的方案来说 只能解决一部分问题,因为在android中 还有很多view 没有四个构造函数,只有3个构造函数,碰到这种view 应该如何设置他的属性 是个问题, 但其实这种方案 也是有解的:

<style name="test_theme" parent="android:Widget.EditText">
    <item name="android:scrollbarSize">@dimen/dp2</item>
    <item name="android:background">@null</item>
    <item name="android:textColor">@color/color_333333</item>
    <item name="android:maxHeight">@dimen/dp61</item>
    <item name="android:textColorHint">@color/color_C6C6C6</item>
    
    <item name="android:gravity">left</item>
    <item name="android:textSize">@dimen/dp14</item>
    <item name="android:scrollbars">vertical</item>
    <item name="android:scrollbarStyle">outsideOverlay</item>
 
</style>

<style name="test">
    <item name="editTextStyle">@style/test_theme</item>
</style>

这里我们注意的是要看 最后一个style 这个名为test的style 里面有一个editTextStyle 这个东西对于android的view系统来说 极为重要,我们所有系统中提供的view 都会有一个对应的style 这是系统为了方便自己设置所留下的一个钩子 大家可以看看是不是所有的view都有对应的style,

有了这个钩子方案 我们只要包装一下context的theme 即可

首先构造一个扩展函数

/**
 * 自定义view时可用这个来包裹一个真正的自定义style属性
 */
fun Context.toTheme(@StyleRes themeResId: Int): ContextThemeWrapper = ContextThemeWrapper(this, themeResId)

然后使用我们包装过的context 来初始化这个view即可。

EditText(context.toTheme(R.style.space_forum_post_detail_bottom_input), null, R.attr.editTextStyle)