这些XML属性你在用了吗

作者:大东Pd

链接:

https://blog.csdn.net/d745282469/article/details/107673386

       当我问你,如何为一个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

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。

通过自定义属性完成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

https://github.com/JavaNoober/BackgroundLibrary

关注我获取更多知识或者投稿

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值