简介
在开发过程中,设计常常会有一些比较炫酷的想法,比如两边不一样大小的圆角啦,甚至四角的radius
各不相同,对于这种情况我们该怎么实现呢?
背景圆角
Shape
对于一般的背景,我们可以直接使用shape
,这种方法天生支持设置四角不同的radius
,比如:
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#8358FF" /> <corners android:bottomLeftRadius="10dp" android:bottomRightRadius="20dp" android:topLeftRadius="30dp" android:topRightRadius="40dp" /> </shape>
|
小贴士:shape
在代码层的实现为GradientDrawable
,可以直接在代码层构建圆角背景,顺便推荐一下我写的库:ShapeLayout,可以方便的实现shape
背景,告别xml
内容圆角
很多情况下,设置背景的四边不同圆角并不能满足我们,大多数情况下,我们需要连着里面的内容一起切圆角,这里我们需要先指正一下网上的一个错误写法
有人发文说,可以通过outline.setConvexPath
方法,实现四角不同radius
,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| outline?.setConvexPath( Path().apply { addRoundRect( 0f, 0f, width.toFloat(), height.toFloat(), floatArrayOf( topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomRightRadius, bottomRightRadius, bottomLeftRadius, bottomLeftRadius ), Path.Direction.CCW ) } )
|
经过实测,这样写是不行的,准确的来说,在大部分系统上是不行的(MIUI上可以,我不知道是该夸它兼容性太好了还是该吐槽它啥,我的测试机用的小米,这导致我在最后的测试阶段才发现这个问题)
指出错误方法后,让我们来看看正确解法有哪些
CardView
说到切内容圆角,我们自然而然会去想到CardView
,其实CardView
的圆角也是通过Outline
实现的
有人可能要问了,CardView
不是只支持四角相同radius
吗?别急,且看我灵机一动想出来的神奇嵌套大法
神奇嵌套大法
既然一个CardView
只能设一个radius
,那我多用几个CardView
嵌套是否能解决问题呢?
举个最简单的例子,比如说设计想要上半部分为12dp
的圆角,下半部分没有圆角,我们需要一个辅助View
,让他的顶部和父布局的底部对齐,然后设置成圆角大小的高度或者margin
,接着使用CardView
,让它的底部对齐这个辅助View
的底部,再设置一个圆角大小的padding
,这样,由于CardView
超出了父布局的边界,所以底部的圆角不会显示出来,再由于我们设置了恰好的padding
,所以CardView
里面的内容也能完整展示,可谓完美,实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content">
<Space android:id="@+id/guideline" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="12dp" app:cardElevation="0dp" app:contentPaddingBottom="12dp" app:layout_constraintBottom_toBottomOf="@+id/guideline">
<ImageView android:layout_width="match_parent" android:layout_height="300dp" android:adjustViewBounds="true" android:background="#8358FF" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
|
上面的例子没有嵌套,因为另一边没有圆角,那么如果我们需要上半部分为12dp
的圆角,下半部分为6dp
的圆角,我们可以这样操作
手法和上面的例子一样,不过我们在最外层再嵌套一个CardView
,并且将其圆角设为较小的那个圆角大小6dp
,将里面的CardView
的圆角设置成较大的那个圆角大小12dp
,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="6dp" app:cardElevation="0dp" app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="12dp" app:cardElevation="0dp" app:contentPaddingBottom="12dp" app:layout_constraintBottom_toTopOf="@+id/guideline">
<ImageView android:layout_width="match_parent" android:layout_height="300dp" android:adjustViewBounds="true" android:background="#8358FF" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
|
本质上就是大圆角套小圆角,大圆角的裁切范围更大,会覆盖小圆角裁切的范围,从视觉上看就实现了两边的不同圆角
那么如果我们想进一步实现三边不同圆角或者四边不同圆角呢?原理和上面是一样的,只不过嵌套和占位会变得更加复杂,记住一个原则,小圆角在外,大圆角在内即可,我直接把具体实现贴在下面,各位自取即可:
- 三边不同圆角(左下6dp,左上12dp,右上24dp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content">
<Space android:id="@+id/guideline" android:layout_width="6dp" android:layout_height="match_parent" app:layout_constraintStart_toEndOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="6dp" app:cardElevation="0dp" app:contentPaddingRight="6dp" app:layout_constraintEnd_toEndOf="@+id/guideline" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline2" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="12dp" app:cardElevation="0dp" app:contentPaddingBottom="12dp" app:layout_constraintBottom_toTopOf="@+id/guideline2">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline3" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="24dp" app:layout_constraintEnd_toStartOf="parent" />
<Space android:id="@+id/guideline4" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="24dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="24dp" app:cardElevation="0dp" app:contentPaddingBottom="24dp" app:contentPaddingLeft="24dp" app:layout_constraintBottom_toBottomOf="@+id/guideline4" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline3">
<ImageView android:layout_width="match_parent" android:layout_height="300dp" android:adjustViewBounds="true" android:background="#8358FF" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
|
- 四边不同圆角(左下6dp,左上12dp,右上24dp,右下48dp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content">
<Space android:id="@+id/guideline" android:layout_width="6dp" android:layout_height="match_parent" app:layout_constraintStart_toEndOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="6dp" app:cardElevation="0dp" app:contentPaddingRight="6dp" app:layout_constraintEnd_toEndOf="@+id/guideline" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline2" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="12dp" app:cardElevation="0dp" app:contentPaddingBottom="12dp" app:layout_constraintBottom_toTopOf="@+id/guideline2">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline3" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="24dp" app:layout_constraintEnd_toStartOf="parent" />
<Space android:id="@+id/guideline4" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="24dp" app:layout_constraintTop_toBottomOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="24dp" app:cardElevation="0dp" app:contentPaddingBottom="24dp" app:contentPaddingLeft="24dp" app:layout_constraintBottom_toBottomOf="@+id/guideline4" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline3">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Space android:id="@+id/guideline5" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="48dp" app:layout_constraintEnd_toStartOf="parent" />
<Space android:id="@+id/guideline6" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginBottom="48dp" app:layout_constraintBottom_toTopOf="parent" />
<androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="wrap_content" app:cardBackgroundColor="@android:color/transparent" app:cardCornerRadius="48dp" app:cardElevation="0dp" app:contentPaddingLeft="48dp" app:contentPaddingTop="48dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline5" app:layout_constraintTop_toTopOf="@+id/guideline6">
<ImageView android:layout_width="match_parent" android:layout_height="300dp" android:adjustViewBounds="true" android:background="#8358FF" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
|
自定义ImageView
由于大部分裁切内容的需求,其中的内容都是图片,所以我们也可以直接对图片进行裁切,此时我们就可以自定义ImageView
来将图片裁剪出不同大小的圆角
clipPath
先说这个方法的缺点,那就是无法使用抗锯齿,这一点缺陷注定了它无法被正式使用,但我们还是来看看他是如何实现的
首先,我们需要重写ImageView
的onSizeChanged
方法,为我们的Path
确定路线
1 2 3 4 5 6
| override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) path.reset() path.addRoundRect(0f, 0f, w.toFloat(), h.toFloat(), radii, Path.Direction.CW) }
|
接着我们重写onDraw
方法
1 2 3 4
| override fun onDraw(canvas: Canvas) { canvas.clipPath(path) super.onDraw(rawBitmapCanvas) }
|
网上有的教程说要设置PaintFlagsDrawFilter
,但实际上就算为这个PaintFlagsDrawFilter
设置了Paint.ANTI_ALIAS_FLAG
抗锯齿属性也没用,抗锯齿只在使用了Paint
的情况下才可以生效
PorterDuff
既然clipPath
无法使用抗锯齿,那我们可以换一条路线曲线救国,那就是使用PorterDuff
当然,这种方法也有它的缺点,那就是不能使用硬件加速,但相比无法使用抗锯齿而言,这点缺点也就不算什么了
首先,我们要在构造方法中禁用硬件加速
1 2 3
| init { setLayerType(LAYER_TYPE_SOFTWARE, null) }
|
然后重写onSizeChanged
方法,在这个方法中,我们需要确定Path
,构造出相应大小的Bitmap
和Canvas
,这俩是用来获取原始无圆角的Bitmap
的
1 2 3 4 5 6 7 8
| override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) path.reset() path.addRoundRect(0f, 0f, w.toFloat(), h.toFloat(), radii, Path.Direction.CW)
rawBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) rawBitmapCanvas = Canvas(rawBitmap!!) }
|
接着我们重写onDraw
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
override fun onDraw(canvas: Canvas) { val rawBitmap = rawBitmap ?: return val rawBitmapCanvas = rawBitmapCanvas ?: return
super.onDraw(rawBitmapCanvas)
canvas.drawPath(path, paint) paint.xfermode = xfermode canvas.drawBitmap(rawBitmap, 0f, 0f, paint) paint.xfermode = null }
|
这里,我们调用父类的onDraw
方法,获取到原始无圆角的Bitmap
,然后绘制Path
,再通过PorterDuff
的叠加效果绘制我们刚刚得到的原始Bitmap
,由于PorterDuff.Mode.SRC_IN
的效果是取两层绘制交集,显示上层,所以我们最终便获得了一个带圆角的图片
BitmapShader
有人指出,可以使用BitmapShader
方案,我去实测了一下,确实可以在开启了硬件加速的情况下使用,目前看上去似乎没有什么缺点,在此感谢评论区的大神们,这种方案实现起来也很简单,和上面的差不多
首先重写onSizeChanged
方法
1 2 3 4 5 6 7 8 9 10 11 12
| private var bitmapShader: BitmapShader? = null
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) path.reset() path.addRoundRect(0f, 0f, w.toFloat(), h.toFloat(), radii, Path.Direction.CW)
rawBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) rawBitmapCanvas = Canvas(rawBitmap!!) bitmapShader = BitmapShader(rawBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) paint.shader = bitmapShader }
|
然后onDraw
方法就更简单了
1 2 3 4 5
| override fun onDraw(canvas: Canvas) { val rawBitmapCanvas = rawBitmapCanvas ?: return super.onDraw(rawBitmapCanvas) canvas.drawPath(path, paint) }
|
截图问题
如果想要将View
截图成Bitmap
,在Android 8.0
及以上系统中我们可以使用PixelCopy
,此时使用CardView
或Outline
裁切的圆角不会有任何问题,而在Android 8.0
以下的系统中,通常我们是构建一个带Bitmap
的Canvas
,然后对要截图的View
调用draw
方法达成截图效果,而在这种情况下,使用CardView
或Outline
裁切的圆角便会出现无效的情况(截图出来的Bitmap
中,圆角没了),这种情况的出现似乎也和硬件加速有关,针对这种问题,如果是图片圆角的情况,建议直接使用BitmapShader
方案,这样无论使用哪种方式截图都不会出现问题
这里顺便把截图的代码也贴一下吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| fun View.screenshot(activity: Activity?, onSuccess: (Bitmap) -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != null) { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val locationOfViewInWindow = IntArray(2) this.getLocationInWindow(locationOfViewInWindow)
PixelCopy.request( activity.window, Rect( locationOfViewInWindow[0], locationOfViewInWindow[1], locationOfViewInWindow[0] + width, locationOfViewInWindow[1] + height ), bitmap, { copyResult -> if (copyResult == PixelCopy.SUCCESS) { onSuccess(bitmap) } else { screenshotBackup(onSuccess) } }, Handler(Looper.getMainLooper()) ) } else { screenshotBackup(onSuccess) } }
private fun View.screenshotBackup(onSuccess: (Bitmap) -> Unit) { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) draw(canvas) onSuccess(bitmap) }
|
总结
以上就是我本人目前对Android
实现不同大小的圆角的一些想法和遇到的问题,至于CardView
嵌套会不会带来什么性能问题,我用BitmapShader
方案做了一下对比,不管加载速度,还是内存占用,都没有发现明显差别(甚至用CardView
嵌套速度还快点?),所以各位不用担心性能问题,选适合自己的方案就行了,各位小伙伴有什么更好的解决方案,欢迎在评论区指出,大家一起集思广益