作者:newki
定制圆角与背景的自定义ViewGroup实现
前言
目前线上的一些第三方圆角容器大部分都只支持四周固定圆角,我们一些使用场景只需要顶部圆角,或者底部圆角,或者一个角圆角。
(话说为什么我们的UI这么喜欢各种奇葩圆角,想哭。。。)
对于这些定制化的圆角需求,我们如何自定义实现呢?又有哪些实现方式呢?
之前我们讲过圆角图片的自定义,那么和我们的自定义圆角容器又有哪些区别呢?
带着这些问题,我们一步一步往下看。
技术选型
可能有同学问了,用shape画一个圆角不就行了吗?各种的圆角都能画,实在不行还可以找UI要各种圆角的切图。有必要用自定义ViewGroup来实现吗?
确实在一部分场景中我们可以通过这样设置圆角背景的方式来解决问题,一般设计都有内间距,我们设置了背景,然后再通过设置间距来确保内部的控件不会和父容器交叉重叠。
因为这样设置的背景只是欺骗了视觉,并没有裁剪控件,如果在特定的场景下,如挨着角落布局,或者滚动起来的时候,就会发现内部的控件’超过’了父容器的范围。
一句话说不清楚,大家看下面这张图应该能理解:
我使用自定义的 FrameLayout 设置异性圆角,并且设置异性圆角的图片背景,然后内部添加一个子View,那么子View就不会超过左上角的圆角范围。
如果在这样的特殊场景下,要达到这样的效果,我们就需要自定义View的方式来裁剪父容器,让它真正的就是那样的形状!
一共有 ClipPath
Xfermode
Shader
另外还有一种 Outline
的方式。
之前我们的图片裁剪是利用 Shader
来实现的。现在我们裁剪ViewGroup我们最方便的方式是 Outline
但是我们需要对一些 Outline
实现不了的版本和效果,我们使用 Shader
做一些兼容处理即可。
需求整理
首先在动手之前我们理清一下思路,我们需要哪些功能,以及如何实现。
- 通过策略模式来管理不同版本的裁剪实现
- 通过一个接口来封装逻辑管理这些策略
- 通过实现不同的自定义View来管理接口实现类间接的通过不同的策略来裁剪
- 使用自定义属性来动态的配置需要的属性
- 裁剪完成之后需要接管系统的背景的绘制,由自己实现
- 使用Shader的方式绘制背景
- 对原生背景属性的兼容处理
说明:
根据不同的版本和需求,使用不同的策略来裁剪 ViewGroup,需要考虑到不同的圆角,统一的圆角和圆形的裁剪。
裁剪完成之后在部分方案中我们设置背景还是会覆盖到已裁剪的区域,这时候我们统一处理背景的绘制。
由于系统 View 自带背景的设置,和我们的背景绘制有冲突,我们需要接管系统的 View 的背景绘制,并且需要处理 Xml 中设置背景与 Java 代码中设置背景的兼容性问题。
最后使用 Shader 的方式绘制各种形状的背景绘制。需要注意处理不同的圆角,圆角和圆形的绘制方式。
整体框架的大致构建图如下:
下面跟着我一步一步的来实现吧。
使用策略模式兼容裁剪的方式
其实市面上大部分的裁剪都是使用的 Outline 的方式,这是一种极好的方案。我也是使用这种方案,那我为什么不直接使用第三方库算了。。。 就是因为兼容性问题和一些功能性问题不能解决。
Outline可以绘制圆形和统一圆角,但是它无法设置异形的圆角。并且它只能在5.0以上的系统才能使用。所以我们需要对异形的圆角和低版本做兼容处理。
核心代码如下:
private fun init(view: View, context: Context, attributeSet: AttributeSet?, attrs: IntArray, attrIndexs: IntArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.0版本以上与5.0一下的兼容处理
//判断是否包含自定义圆角
val typedArray = context.obtainStyledAttributes(attributeSet, attrs)
val topLeft = typedArray.getDimensionPixelOffset(attrIndexs[2], 0).toFloat()
val topRight = typedArray.getDimensionPixelOffset(attrIndexs[3], 0).toFloat()
val bottomLeft = typedArray.getDimensionPixelOffset(attrIndexs[4], 0).toFloat()
val bottomRight = typedArray.getDimensionPixelOffset(attrIndexs[5], 0).toFloat()
typedArray.recycle()
roundCirclePolicy = if (topLeft > 0 || topRight > 0 || bottomLeft > 0 || bottomRight > 0) {
//自定义圆角使用兼容方案
RoundCircleLayoutShaderPolicy(view, context, attributeSet, attrs, attrIndexs)
} else {
//使用OutLine裁剪方案
RoundCircleLayoutOutlinePolicy(view, context, attributeSet, attrs, attrIndexs)
}
} else {
// 5.0以下的版本使用兼容方案
roundCirclePolicy = RoundCircleLayoutShaderPolicy(view, context, attributeSet, attrs, attrIndexs)
}
}
我们需要对5.0一下的版本使用 clipPath 的方案裁剪,5.0以上的方案实现 Outline的方案裁剪。
Outline的裁剪:
override fun beforeDispatchDraw(canvas: Canvas?) {
//5.0版本以上,采用ViewOutlineProvider来裁剪view
mContainer.clipToOutline = true
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun afterDispatchDraw(canvas: Canvas?) {
//5.0版本以上,采用ViewOutlineProvider来裁剪view
mContainer.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
if (isCircleType) {
//如果是圆形裁剪圆形
val bounds = Rect()
calculateBounds().roundOut(bounds)
outline.setRoundRect(bounds, bounds.width() / 2.0f)
// outline.setOval(0, 0, mContainer.width, mContainer.height); //两种方法都可以
} else {
//如果是圆角-裁剪圆角
if (mTopLeft > 0 || mTopRight > 0 || mBottomLeft > 0 || mBottomRight > 0) {
//如果是单独的圆角
val path = Path()
path.addRoundRect(
calculateBounds(),
floatArrayOf(mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft),
Path.Direction.CCW
)
//不支持2阶的曲线
outline.setConvexPath(path)
} else {
//如果是统一圆角
outline.setRoundRect(0, 0, mContainer.width, mContainer.height, mRoundRadius)
}
}
}
}
}
</