前言
当我问你,如何为一个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次?
那还是程序员吗?程序员能受这气?
下面,我将通过分析源码的方式,带你一步步的完成如何在布局文件中实现圆角效果。
XML如何到实体对象的
通过对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
}
}
}
Background设置的XML如何应用到View的
平常,我们想为某个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
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。
通过自定义属性完成Drawable创建
首先我们在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