无需自定义 View,巧妙利用 xml 属性,避免 drawable 泛滥

Posted 鸿洋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无需自定义 View,巧妙利用 xml 属性,避免 drawable 泛滥相关的知识,希望对你有一定的参考价值。


本文作者


链接:

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

本文由作者授权发布。


之前推送过:



主要是为了避免 drawable xml 膨胀,希望在编写布局声明的时候,就能通过属性控制。


本文可以认为上上文中开源库原理的简单的剖析,开始吧。


1
前言


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


那还是程序员吗?程序员能受这气?


下面,我将通过分析源码的方式,带你一步步的完成如何在布局文件中实现圆角效果。


2
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();
}


3
如何拦截创建对象的过程


通过对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
        }

    }

}


4
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。


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


参考文章

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的区别

Android自定义View

attrs.xml自定义属性学习笔记(自定义View2)

使用自定义 View 或 SurfaceView 时如何避免 NullPointerException

Android 自定义View

Android——自定义View