Android知识点-圆角

先扯两句

不知道多长时间没有登录发文章了,正好今天不太忙进来看一眼,然后看到好多人都已经开始自己公众号了,看看自己的工作年限也不短了,可是感觉好久没有干正事了,还是老老实实打开文档开始写吧。

正文

随着UI越来越美观,原本直来直去的布局样式越来越不受喜欢,圆角样式,这个最简单的优化方案在越来越多的场合下被应用到。而具体怎么样才能展示出圆角的效果,相比大家都有N多中方案,我这里就列举一些自己常用的方案吧。当然,还有一些第三方的框架支持,这里就不列举了,主要是我个人感觉为了一个简单的效果,集成一个库不太划算,尤其是库中很大一部分功能都用不到的时候。而且第三方的框架贡献者实力不等,有很多都要导入源码进一步优化,分析太麻烦了,一向懒汉自称的我是不可能这么勤快去列举的,大家有兴趣可以自行研究。
样式参考就已自如房型列表的item为例吧:
样式参考

方案0

首先说明一下,什么叫做方案0,不是因为程序员,计数一定要从0开始的洁癖,单纯是因为这些方案算是比较常用的,大家遇到类似的需求很快就能想到的。这里只是简单介绍一下,给一些新入门的同学看看,在遇到相应的圆角需求的时候,可以最方便的解决问题。
当然,还是同样的,如果一定要找一个第三方框架,一个参数控制去实现也不是不可以,性能各方面我这里也没有去具体评估过,也没办法说一定谁好谁坏。所以对于一切反对意见,我这里的回应都是:“我错了!你说的对!”

1 圆角背景:xml

圆角的背景,可以实现的效果就是上面图中的下半部分
xml可实现部分

首先我们需要分析这个部分有什么要求:
分析
可以看到有两个地方有要求,分别是:1.白色背景;2.灰色边框。所以对应的xml只需要满足这两点就可以了。

  1. 创建drawable的xml文件
    创建目录
  2. 命名,并将对应的跟元素设置为shape(形状)
    image.png
  3. 配置属性
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--中间的白色实心背景-->
    <solid android:color="#FFFFFF" />
    <!--灰色边框,色值和宽度-->
    <stroke
        android:width="0.5dp"
        android:color="#999999" />
    <!--圆角角度:左下和右下-->
    <corners
        android:bottomLeftRadius="5dp"
        android:bottomRightRadius="5dp" />
</shape>

其中需要注意的是,图中可以看到,需要有圆角的区域只在下半部分,所以这里配置的时候也要只配置下半部,不然图片与背景的交接处就会有相当怪异。
圆角设置错误示例
这里可能有人会说,直接绘制一个白色的全圆角背景放到下面,上面盖上一个圆角的图片不就好了吗?你真是一个小机灵鬼,这个方案当然可行。但是性能稍微有点追求的建议了解一些过度绘制,虽然不是说背景上面叠加一张图片就会对性能造成断崖式影响,但是雪崩之下,可没有哪朵雪花是无辜的。
当然,这里为了看得清晰,页面的背景设置成了黑色,圆角也调整到了20dp,正常情况下也会出现问题,但是不会这么明显,能不能发现就看测试团队和UI团队的审查力度了。
xml正确样式

2 纯图片Glide

现在用的比较多的,或者是我工作这么多年用的比较多的图片框架就是glide,想当初在刚开始用glide的时候还不支持圆角,需要额外添加一堆配置,还要导入第三方的库,现在好了,没有几行代码,基本属于一键配置了。

Glide.with(this).load(R.drawable.bg_scene)
            .apply(
                RequestOptions
                    .bitmapTransform(RoundedCorners(dip2px(this, 20f)))
            ).into(findViewById(R.id.test_img))

其中apply中的内容就是圆角的配置,当然,猜也能猜出来,这里只配置了一个角度,显示的时候底部肯定有问题,应该只配置顶部的两个圆角才对,这里给大家找了个链接:Glide 加载部分圆角图片,作为一个懒汉,没有做测试,哪位如果正好赶上有这个需求可以测试一下,我们直接进入下一个原生方案。

3 CardView

UI样式的大趋势,作为Android的幕后大boss google怎么可能不知道呢,所以官方控件里也有这么一个小朋友可以实现这个功能,那就是CardView,直接上代码:

<FrameLayout 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:id="@+id/item_tv"
    android:layout_width="200dp"
    android:layout_height="wrap_content"
    android:background="#fefefe"
    android:padding="15dp"
    tools:ignore="SpUsage"
    tools:viewBindingIgnore="true">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="5dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/test_img"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/bg_scene" />

            <View
                android:layout_width="match_parent"
                android:layout_height="80dp"
                android:background="20dp" />
        </LinearLayout>
    </androidx.cardview.widget.CardView>
</FrameLayout>

然后看效果:
cardview效果图
可以看到,前面的两个问题,在这里都直接搞定了,背景图也不用设置圆角了,glide也不用设置圆角了,在CardView中添加了app:cardCornerRadius="5dp"后,一切问题都迎刃而解了,是不是特别爽,特别开心!

缺点分析

上面虽然说实现了功能,但是能放到方案0上,就代表还有其他在我个人的能力水平内能看到的更好的方案(具体是不是还是要交给大神们评估),那就先说说上面的这些我所看到的缺点吧。

  1. xml只能对简单的色块等做配置,如果是图片的虽然也可以通过layer-list来完成拼接绘制,但是绘制难度跟在layout的xml中的绘制难度就不是一个量级的(比如位置的相对关系)
  2. glide针对的主要是图片的处理,另外还有一个问题对于加载中的网络图片的支持可以,但是图片加载前的占位图和图片加载后的缺省图支持的就不是很好,一旦图片请求失败,展示个没有圆角的图,测试绝对会来找你的麻烦
  3. CardView看起来可以完美的规避掉上面两种方案的问题,但是可以看到,我们在绘制页面的时候,CardView中需要添加一个子View去绘制其中各个View的相对关系,毕竟CardView的父控件是FrameLayout,又没有重写onLayout方法,所以子View的展示逻辑,可以理解为就是FrameLayout。这样为了实现一个效果就需要多一层View嵌套。且同样,CardView配置圆角的时候,也是四个角同步配置的,如果需要部分配置的时候也是不支持的。
    CardView

那么我们就进入到为大家准备的其他方案。

方案一:自定义View圆角

这个方案就是通过declare-styleable配置圆角角度,然后在执行onDraw的时候,通过裁剪Carvas实现圆角效果,代码如下:

<declare-styleable name="XXXView">
    <!-- 通用圆角配置 -->
    <attr format="dimension" name="radius"/>
    <!-- 右上圆角 -->
    <attr format="dimension" name="rightTopRadius"/>
    <!-- 左上圆角 -->
    <attr format="dimension" name="leftTopRadius"/>
    <!-- 右下圆角 -->
    <attr format="dimension" name="rightBottomRadius"/>
    <!-- 左下圆角 -->
    <attr format="dimension" name="leftBottomRadius"/>
</declare-styleable>

自定义View代码

class XXXView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    private val mPath = Path()
    private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    /**
     * 左上角 圆角大小
     */
    private var leftTopRadius: Float = 0f

    /**
     * 右上角 圆角大小
     */
    private var rightTopRadius: Float = 0f

    /**
     * 左下角 圆角大小
     */
    private var leftBottomRadius: Float = 0f

    /**
     * 右下角 圆角大小
     */
    private var rightBottomRadius: Float = 0f

    /**
     * 全部圆角
     */
    private var radius: Float = 0f

    private val clipRectF = RectF()

    init {
        val obtainStyledAttributes = getContext().obtainStyledAttributes(attrs, R.styleable.XXXView)
        radius= obtainStyledAttributes.getDimension(R.styleable.XXXView_radius, radius)
        if (radius != 0f) {
            //不知道为啥 不能 leftTopRadius=rightTopRadius=rightBottomRadius=leftBottomRadius = radius
            leftTopRadius = radius
            rightTopRadius = radius
            rightBottomRadius = radius
            leftBottomRadius = radius
        } else {
            leftTopRadius = obtainStyledAttributes.getDimension(R.styleable.XXXView_leftTopRadius, leftTopRadius)
            rightTopRadius = obtainStyledAttributes.getDimension(R.styleable.XXXView_rightTopRadius, rightTopRadius)
            leftBottomRadius = obtainStyledAttributes.getDimension(R.styleable.XXXView_leftBottomRadius, leftBottomRadius)
            rightBottomRadius = obtainStyledAttributes.getDimension(R.styleable.XXXView_rightBottomRadius, rightBottomRadius)
        }
        obtainStyledAttributes.recycle()
    }

    override fun onDraw(canvas: Canvas?) {
        //1.
        //dx,dy 成对出现,控制上右下左,四个位置圆角
        val array = floatArrayOf(leftTopRadius, leftTopRadius, rightTopRadius, rightTopRadius, rightBottomRadius, rightBottomRadius, leftBottomRadius, leftBottomRadius);
        clipRectF.set(0f, 0f, width.toFloat(), height.toFloat())
        mPath.addRoundRect(clipRectF, array, Path.Direction.CW)
        //2.
        //3.
        canvas?.clipPath(mPath)
        //4.
        super.onDraw(canvas)
    }
}

自定义ViewGroup圆角(onDraw替换为dispatchDraw)

override fun dispatchDraw(canvas: Canvas) {
        //1.
        //dx,dy 成对出现,控制上右下左,四个位置圆角
        val array = floatArrayOf(leftTopRadius, leftTopRadius, rightTopRadius, rightTopRadius, rightBottomRadius, rightBottomRadius, leftBottomRadius, leftBottomRadius);
        mPath.addRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), array, Path.Direction.CW)
        //2.
        //3.
        canvas.clipPath(mPath)
        //4.
        super.dispatchDraw(canvas)
    }

注:
1. canvas裁剪需要放置在super之前
2. 频繁刷新UI,会导致频繁在onDraw执行canvas裁剪,出现黑色闪屏

方案二:ViewOutlineProvider

其实我较多的时候使用的还是方案一,直到一次的需求需要频繁刷新UI,才发现了黑色闪屏的问题,查找其他方案的时候,才知道的ViewOutlineProvider,参考链接:Material Design :ViewOutlineProvider

demo代码如下:

<declare-styleable name="XXXView">
    <!-- 圆角角度 -->
    <attr format="dimension" name="radius"/>
</declare-styleable>

圆角View

class XXXView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    private var radius = 0f

    init {
        val obtainStyledAttributes =
            getContext().obtainStyledAttributes(attrs, R.styleable.XXXView)
        radius =
            obtainStyledAttributes.getDimension(R.styleable.XXXView_radius, radius)
        obtainStyledAttributes.recycle()
        clipToOutline = true
        outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View?, outline: Outline?) {
                view?.let {
                    outline?.setRoundRect(
                        0 + paddingLeft,
                        0 + paddingTop,
                        it.width - paddingRight,
                        it.height - paddingBottom,
                        radius
                    )
                }
            }
        }
    }

    // 业务代码逻辑
    // ...
}

布局:

<com.example.demo.widget.radius.RadiusView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:padding="15dp"
    app:radius="50dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!--图片自己网上随便找一张就可以-->
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/test_img"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:adjustViewBounds="true"
            android:scaleType="centerInside"
            android:src="@drawable/bg_scene" />
        <View
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:background="@android:color/holo_purple" />
    </LinearLayout>
</com.example.demo.widget.radius.RadiusView>

效果如图:
ViewOutlineProvider效果图

注:
1. 代码中圆角的位置是会收到padding影响的,如果不想收到影响,去掉setRoundRect中的四个padding参数即可,具体依据具体需求调整
2. 由于ViewOutlineProvider api的限制,暂时能配置的都是四个角统一角度,如果想要分别配置,该方案不适用

方案三 切图覆盖

可以看到,上面的所有方案在特定的情况下都会有所限制,所以在相应的场景下,产品经历还是要求我们实现对应的圆角效果,我们又不能让他闭嘴的时候,只能再想其他方案。
这里的建议就是切图覆盖,也就是让UI切一个颜色与背景色相同的圆角边框,覆盖到需要圆角展示的View上方,假装实现了一个圆角的效果。

圆角图片:
圆角图片

绘制布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/item_tv"
    android:layout_width="200dp"
    android:layout_height="200dp"
    tools:viewBindingIgnore="true">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/test_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:adjustViewBounds="true"
        android:scaleType="centerCrop"
        android:src="@drawable/bg_scene" />

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/mask" />
</FrameLayout>

圆角效果:
圆角效果

优点:基本可以兼容产品的所有圆角样式需求
缺点:需要多绘制一层View,不够优雅

总结

上面列举的就是我现阶段想到的所有圆角效果的实现方案,在工作场景中,其实大多数情况也都够用了,当然,大家如果有其他的方案,也欢迎分享一下,大家共同进步。
这里也说过了,是没有列举第三方的方案的,都是一些原生的实现方案,虽然说用第三方的方案很爽,但是大多数情况下,还是建议自己学习一下具体的实现,较少一些不必要的引用。
另外,关于最后一条要说几句,其实这个方案是一次看网课有学生提问,老师提到的方案,结果是学生感觉这种方案是在忽悠他,最终不欢而散。其实说实话,这个方案,我个人也不是那么建议使用的,只是有很多情况我们并没有办法很快的实现产品或者UI想要的一些效果,不得不使用一些折中的方案,也算是工作中的一种妥协和无奈。
这里不是建议大家一点不用,但是最好应付过关后,在空闲时间思考一下,还有没有其他好的解决方案丰富自己。
好了,祝大家新年快乐,开年第一天的摸鱼……还有什么可以做的呢,容我想想,哈哈哈!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值