无需自定义 View,巧妙利用 xml 属性,避免 drawable 泛滥
Posted 鸿洋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无需自定义 View,巧妙利用 xml 属性,避免 drawable 泛滥相关的知识,希望对你有一定的参考价值。
本文作者
链接:
https://blog.csdn.net/d745282469/article/details/107673386
本文由作者授权发布。
之前推送过:
主要是为了避免 drawable xml 膨胀,希望在编写布局声明的时候,就能通过属性控制。
本文可以认为上上文中开源库原理的简单的剖析,开始吧。
当我问你,如何为一个View添加圆角效果时,你肯定会说:在drawable文件下新建一个xml文件,在里面写入下面的代码:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:topLeftRadius="10dp"
android:topRightRadius="10dp"/>
</shape>
然后再设置到对应的View的Background属性就可以实现啦。
假如我此时有10个View,每个View的圆角弧度都不一样,怎么办?是不是要创建10个这样子的xml文件?复制粘贴10次?
那还是程序员吗?程序员能受这气?
下面,我将通过分析源码的方式,带你一步步的完成如何在布局文件中实现圆角效果。
通过对setContentView进行追踪,发现在AppCompatDelegateImpl#setContentView中使用了LayoutInflater来完成xml到对象的转换。
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
// 注意这里将布局文件转换成了对象
LayoutInflater.from(this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}
通过对LayoutInflater#inflate进行追踪,在LayoutInflater#createViewFromTag发现了创建View的踪迹,继续跟踪发现实际是通过LayoutInflater.Factory进行创建View的。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// 伪代码
View view = mFactory.onCreateView()
if(view == null){
view = mPrivateFactory.onCreateView()
}
return view;
}
既然如此,接下来只需要设置一个自定义的Factory,即可拦截到View对象的创建过程。
LayoutInflater.from(this).factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
Log.d("dong", "名字:$name")
for (index in 0 until attrs.attributeCount) {
Log.d("dong", "属性名字:${attrs.getAttributeName(index)},属性值:${attrs.getAttributeValue(index)}")
}
return null
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}
}
此时可能会遇到问题,报错将提示A factory has already been set on this LayoutInflater。
是因为现在大部分的Activity都是继承自AppCompatActivity,而AppCompatActivity本身就设置了Factory。我们需要在super.onCreate()之前设置我们自身的Factory。
但是此时会遇到一个问题,那就是AppCompatActivity设置的Factory本身就是为了兼容AppCompatTextView这些控件的,如果我们覆盖了,那么将无法兼容。
可以使用以下写法代替:
override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflater.from(this).factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
// 依然保证AppCompat有效
val view = this@TuyaAirActivity.delegate.createView(parent,name,context,attrs)
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}
}
}
平常,我们想为某个View设置圆角效果,需要创建一个XML文件,然后再设置到View的Background中。
// round.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:topLeftRadius="10dp"
android:topRightRadius="10dp"/>
</shape>
// 布局文件.xml
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round"
android:text="something" />
通过追踪View#setBackgroundResource,发现是将XML文件转换成了Drawable对象,然后再调用View#drawBackground进行绘制的。继续跟踪如何转成Drawable的时候发现了以下的调用路径:
-Context#getDrawable
--Reourse#getDrawableForDensity
---ReourseImpl#loadXmlDrawable
----Drawable#createFromXmlInnerForDensity
-----DrawableInflater#inflateFromTag
其中,在最后的DrawableInflater#inflateFromTag中,可以看到根据不同的标签,会返回不同的Drawable。
该文件的链接:DrawableInflater#inflateFromTag
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/graphics/java/android/graphics/drawable/DrawableInflater.java
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "animated-selector":
return new AnimatedStateListDrawable();
case "shape":
return new GradientDrawable();
...省略部分代码
}
}
根据源码可知,我们只需要针对不同的属性,返回其对应的Drawable即可,例如常用的Shape,就返回GradientDrawable。
首先我们在res/values/attr.xml文件中声明自定义的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DCustomDrawable">
<attr name="d_corners_radius" format="dimension"/>
</declare-styleable>
</resources>
然后在需要使用圆角的控件处,使用这个自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" //一定要定义这里才能使用自定义属性
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CustormDrawableActivity">
<TextView
android:layout_width="50dp"
android:layout_height="50dp"
android:text="Dong"
android:gravity="center"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:d_corners_radius="20dp" /> // 这里可能会划红线,无视它
</android.support.constraint.ConstraintLayout>
最后,我们在Activity中,去设置Factory,将d_corners_radius这个自定义属性解析成Drawable即可:
class CustomDrawableActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflater.from(this).factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
// 兼容AppCompatActivity
val view = this@CustomDrawableActivity.delegate.createView(parent, name, context, attrs)
// 解析xml属性
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.DCustomDrawable)
// 解析自定义的xml属性
val radius = typeArray.getDimension(R.styleable.DCustomDrawable_d_corners_radius, 0f)
if (radius > 0) { // 如果有设置,才执行我们的逻辑
// 生成Drawable
val drawable = GradientDrawable()
drawable.cornerRadius = radius // 设置圆角效果
drawable.setColor(Color.parseColor("#229696")) //这里设置个背景色,比较容易看出圆角效果
view?.background = drawable // 将圆角效果赋值给View
Log.d("dong", "执行这里了,${view == null}")
}
typeArray.recycle()
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custorm_drawable)
}
}
总结
至此,我们完成了在布局文件中实现圆角效果,其他类似的如边框、渐变等等都可以参考这个逻辑进行改写。
然而实际使用了一遍,我们发现这个方案还是有点不足的,那就是无法实时预览,因为我们的方案是需要Activity创建时才能执行的。
这个缺点可以通过自定义一些基础控件,重写构造方法,在构造方法中就执行上面的逻辑,即可完成实时预览。
最后,附上大佬完善后的开源方案:
BackgroundLibrary
https://github.com/JavaNoober/BackgroundLibrary
参考文章
Android 探究 LayoutInflater setFactory
https://blog.csdn.net/lmj623565791/article/details/51503977
Android 常用换肤方式以及原理分析
https://juejin.im/post/6844903670270656525
推荐阅读:
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!
以上是关于无需自定义 View,巧妙利用 xml 属性,避免 drawable 泛滥的主要内容,如果未能解决你的问题,请参考以下文章
Android中View自定义XML属性详解以及R.attr与R.styleable的区别