背景
家人们,太卷啦,谁懂呀,今天老板给让我们实现如下效果的一个东东。
我看的第一眼,这不是很简单吗?作为一个优秀的Androider,这种事情当然是让设计师直接给我一张完整的图,然后放到页面就好了,完工!
但是,当我让设计师这样子给我切图的时候,做iOS开发的小伙伴问我:你这样做的话要是后续内容有更新咋办呢?
我:那还不简单,换张图就好了呀。
iOS的小伙伴: 那上面有两个可以点击的地方咋办?
我:我。。。凉拌
iOS的小伙伴:小张(设计师)把上面的左边中间的图标给我,右下角的图给我,其他地方我自己画了。
我的内心OS:能不能不要这么卷呀,直接放张图多好,要啥按钮。。。。
这个时候产品经理说:这个内容随时都要变的,要后端可以直接控制这个内容。
我:。。。。要不你来?
当然这句话怂怂的我没敢说出来。这个效果表面看起来没啥难度,但是其实暗藏玄🐔。
- 左边的“会员特权”那一块,整体背景是一个不规则图像,并且是从左到右的一个渐变效果。右边的背景整体是一个带圆角的纯色背景,右下角有个图标。
- 右边的背景倒是不复杂,但是文本后面可能存在(也可能不存在)一个可以点击的文本按钮,当文本最后一行可以按钮共存(长度能放得下两个内容)的时候,按钮在文本最后一行后面(如第2点),当最后一行长度不能和按钮共存的时候,按钮需要换行(如第5点)。
有的小伙伴可能说,这有啥难的,第2点上 FlexboxLayout 不就好了。但是仔细想想 FlexboxLayout 其实不行。因为 FlexboxLayout 计算的时候控件整个的宽度,而不是 TextView 最后一行的宽度。因此 FlexboxLayout 没有办法把文本按钮放到 TextView 的最后一行,除非 TextView 只有一行。
没办法了,只能选择自定义 View 来突破。我们来把上面的问题逐个突破。作为一名合格的 Androider ,我们不惹事,但是我们不能怕事儿。
整体思路
- 最外层的自定义View继承 ConstraintLayout ,通过重写其 onDraw 方法自定义绘制其中的左右部分的背景,左右部分的背景均通过 Canvas.drawPath() 方法进行绘制,所以需要提前准备好两个部分的Path。
- 左半部分的图标通过TextView + drawableTop 放到自定义的 ConstraintLayout 的左边,右下角的图标同理通过 ImageView 放上去,为减少工作量,这两个地方的图标+文字均在 Xml 布局时放上去。
- 右边的文本内容+文本按钮整体为一个 RecyclerView,RecyclerView 的 Item 则通过自定义 ViewGroup 实现对位置进行随心所欲的控制。
实现最外层的自定义View
作为一个996的Androider,看图写 Path 简直是信手拈来。而我,在 ChatGPT 的帮助下,只用了2个小时就把 Path 给搞定了,是不是很厉害。(此处应有狗头)由于没有什么技术难度,我就直接贴代码了。
// 构建左半部分的背景的 Path
val triangleWidth = 6.dp
val triangleHeight = 16.dp
val radius = 16.dp
val leftRectWidth = 100.dp
shaderPath.reset()
shaderPath.moveTo(radius, 0f)
// 最上面的那条线
shaderPath.lineTo(leftRectWidth, 0f)
// 最后边的线+三角形
shaderPath.lineTo(leftRectWidth, (viewHeight - triangleHeight) / 2f)
shaderPath.lineTo(leftRectWidth + triangleWidth, viewHeight / 2f)
shaderPath.lineTo(leftRectWidth, (viewHeight + triangleHeight) / 2f)
// 剩下的线+圆角
shaderPath.lineTo(leftRectWidth, viewHeight.toFloat())
shaderPath.lineTo(radius, viewHeight.toFloat())
shaderPath.arcTo(0f, viewHeight - radius, radius, viewHeight.toFloat(), 90f, 90f, false)
shaderPath.lineTo(0f, radius)
shaderPath.arcTo(0f, 0f, radius, radius, 180f, 90f, false)
左边的部分搞定了,该右边了。
val radius = 16.dp
val leftRectWidth = 100.dp
rightPath.reset()
rightPath.moveTo(leftRectWidth, 0f)
rightPath.lineTo(viewWidth.toFloat() - radius, 0f)
// 右上圆角
rightPath.arcTo(
viewWidth.toFloat() - radius,
0f,
viewWidth.toFloat(),
radius,
-90f,
90f,
false
)
rightPath.lineTo(viewWidth.toFloat(), viewHeight.toFloat() - radius)
// 右下圆角
rightPath.arcTo(
viewWidth.toFloat() - radius,
viewHeight.toFloat() -radius,
viewWidth.toFloat(),
viewHeight.toFloat(),
0f,
90f,
false
)
rightPath.lineTo(100.dp, viewHeight.toFloat())
rightPath.close()
我们需要在 onSizeChange 方法中调用以上代码重新构建以上两个 Path,这样我们的自定义 View 就可以随着用户屏幕的变化而随之变化。
除此之外,为了实现渐变效果,我们还应该创建一个 Shader。
private fun createShader(): Shader {
val startColor = Color.parseColor("#FEDD83")
val endColor = Color.parseColor("#F2CA5C")
return LinearGradient(
0f,
0f,
106.dp,
0f,
startColor,
endColor,
Shader.TileMode.CLAMP
)
}
OK,万事俱备,只欠东风。接下来我们只需要在 onDraw 里面把这两个 Path 画出来就好啦。
override fun onDraw(canvas: Canvas) {
// 先绘制右边的Path
canvas.drawPath(rightPath, paint)
// 渐变效果的Shader
paint.shader = shader
// 绘制左边的Path
canvas.drawPath(shaderPath, paint)
// 渐变效果取消,防止重复调用时影响到右边的Path
paint.shader = null
}
好了,我们来运行看一哈。咦?啥也没有!为啥会这样?我几个小时的努力呀!!!别慌,我来问下 ChatGPT。原来ViewGroup以及其子类为了更好的性能,默认情况下都不会触发其 onDraw 方法,那样咋办呢?我们可以在自定义 View 的构造函数中调用 setWillNotDraw(false) 来接触这个限制。加上这行代码之后我们在来看看效果。
嗯嗯,没毛病,背景图已经完工。接下来就要自定义右边那个类似 FlexboxLayout 的控件了。由于篇幅关系,我把接下来的实现放到了下一篇《被迫内卷之 Android 自定义View进阶(二)》。