【Android】如何在布局文件中写圆角效果

前言

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

参考文章

Android 探究 LayoutInflater setFactory

Android 常用换肤方式以及原理分析

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值