布景

家人们,太卷啦,谁懂呀,今日老板给让咱们完成如下作用的一个东东。

被迫内卷之 Android 自定义View进阶(一)
我看的第一眼,这不是很简单吗?作为一个优异的Androider,这种工作当然是让设计师直接给我一张完好的图,然后放到页面就好了,竣工!

可是,当我让设计师这姿态给我切图的时分,做iOS开发的小伙伴问我:你这样做的话要是后续内容有更新咋办呢?
我:那还不简单,换张图就好了呀。
iOS的小伙伴: 那上面有两个能够点击的当地咋办?
我:我。。。凉拌
iOS的小伙伴:小张(设计师)把上面的左面中间的图标给我,右下角的图给我,其他当地我自己画了。
我的心里OS:能不能不要这么卷呀,直接放张图多好,要啥按钮。。。。
这个时分产品经理说:这个内容随时都要变的,要后端能够直接操控这个内容。
我:。。。。要不你来?

当然这句话怂怂的我没敢说出来。这个作用表面看起来没啥难度,可是其实暗藏玄。

  1. 左面的“会员特权”那一块,全体布景是一个不规则图画,并且是从左到右的一个渐变作用。右边的布景全体是一个带圆角的纯色布景,右下角有个图标。
  2. 右边的布景却是不杂乱,可是文本后边或许存在(也或许不存在)一个能够点击的文本按钮,当文本最终一行能够按钮共存(长度能放得下两个内容)的时分,按钮在文本最终一行后边(如第2点),当最终一行长度不能和按钮共存的时分,按钮需求换行(如第5点)。

有的小伙伴或许说,这有啥难的,第2点上 FlexboxLayout 不就好了。可是仔细想想 FlexboxLayout 其实不行。因为 FlexboxLayout 核算的时分控件整个的宽度,而不是 TextView 最终一行的宽度。因此 FlexboxLayout 没有办法把文本按钮放到 TextView 的最终一行,除非 TextView 只要一行。

没办法了,只能选择自定义 View 来打破。咱们来把上面的问题逐个打破。作为一名合格的 Androider ,咱们不惹事,可是咱们不能怕事儿。

全体思路

  1. 最外层的自定义View继承 ConstraintLayout ,经过重写其 onDraw 办法自定义制作其中的左右部分的布景,左右部分的布景均经过 Canvas.drawPath() 办法进行制作,所以需求提前准备好两个部分的Path。
  2. 左半部分的图标经过TextView + drawableTop 放到自定义的 ConstraintLayout 的左面,右下角的图标同理经过 ImageView 放上去,为减少工作量,这两个当地的图标+文字均在 Xml 布局时放上去。
  3. 右边的文本内容+文本按钮全体为一个 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) 来接触这个约束。加上这行代码之后咱们在来看看作用。

被迫内卷之 Android 自定义View进阶(一)
嗯嗯,没毛病,布景图现已竣工。接下来就要自定义右边那个类似 FlexboxLayout 的控件了。因为篇幅关系,我把接下来的完成放到了下一篇《被迫内卷之 Android 自定义View进阶(二)》。