Android自定义控件 -- 仿书签(Kotlin)

作者:opLW
最近做毕设时自己设计的书签,无奈最终效果和想象中的不太一样(有点丑感觉),可能我需要一个美工妹子😭。

目录

1.书签 – 简介
2.阴影、背景图案的处理
3.对原始padding的支持
4.总结

1.书签 – 简介
  • 最终功能展示
    在这里插入图片描述

    PS: 通过点击控件可在正常状态和下沉状态之间进行切换。
    下沉状态下图片的阴影各项指标将为正常状态的一半,同时字体变得模糊。

  • 方案 想到如下两个方案,经过考虑使用继承TextView来实现。
    • 继承View
      • 做法 直接继承自View,然后重写ViewonDraw方法,绘制阴影、背景图案、字体等。
      • 不足 除了绘制阴影、不规则图案外,还需要考虑字体绘制带来的难点,包括字体大小、gravity等,难度相当之大。
    • 继承TextView
      • 做法 继承自TextView,重写onDraw方法,绘制阴影、背景图案
      • 优点 对于字体的处理等就不需要考虑,交给TextView处理即可,大大降低了难度。只需考虑如何绘制字体外的阴影和背景图案。

2.阴影、背景图案的处理
  • 总体思路
    • 问题 为了保留TextView的大多数功能,使用继承自View的方案。需要考虑的是如何在不影响TextView核心内容的情况下绘制阴影、背景图案。
    • 思考 其实阴影、背景图案都是在包含字体的框框外部进行处理,很容易可以联想到padding。因为padding就是内容与控件四周的距离,padding越大,预留给我们进行处理的空间也就越大。所以只需要计算出需要显示的阴影、背景图案的大小,预留足够的padding进行绘制即可。
  • padding的计算
    • 介绍
      在这里插入图片描述

      1.黑色边框为整个控件的边界;
      2.黑色线条和蓝色线条之间的空间为阴影需要的大小,(由于左右两侧还需要考虑切角的大小,所以作此标志);
      3.蓝色线条和红色线条之间的空间为需要裁剪的大小;
      4.最内的红色边框为不进行任何设置时的边界,即普通TextView不设置padding的边界。

    • 计算留给阴影的padding
      • 示例代码
         shadowSizeL = Math.max(0f, -bShadowDx) + Math.max(0f, bShadowRadius)
         shadowSizeT = Math.max(0f, -bShadowDy) + Math.max(0f, bShadowRadius)
         shadowSizeR = Math.max(0f, bShadowDx) + Math.max(0f, bShadowRadius)
         shadowSizeB = Math.max(0f, bShadowDy) + Math.max(0f, bShadowRadius)
        
      • 注意点
        • 对值为负的处理 使用Math.max函数限制参数最小值为0。
        • 对shadowDx和shadowDy为负时的处理 当这两个值为负时,代表阴影向左、向上偏移。而留给阴影的空间为正值,所以对这两个方向的计算,需要添加“-”号,如shadowSizeL、shadowSizeT的计算。
        • 预留空间给bShadowRadius 实践证明,即使阴影没有发生偏移,阴影本身的radius也会使得控件周围有一圈阴影,所以为了显示这一圈的阴影,需要考虑bShadowRadius的大小。
    • 计算留给切角的padding
      • 示例代码 没什么特别之处,就是简单的限制了切角的大小。
        clipSizeL = Math.min(measuredWidth / 4, Math.max(leftClipX, 0)).toFloat()
        clipSizeR = Math.min(measuredWidth / 4, Math.max(rightClipX, 0)).toFloat()
        
  • padding的设置时机 考虑到需要对左右切角的大小进行限制,避免切角太大。而限制因素是控件本身的宽度,所以我们将padding的设置置于onMeasure方法中,因为在此方法中可以获得控件初步测量得到的大小:measureWidth
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      setPaddingToDrawBG()
    
      super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }
    
    private fun setPaddingToDrawBG() {
      val left = calculatePaddingLeft().toInt()
      val top = calculatePaddingTop().toInt()
      val right = calculatePaddingRight().toInt()
      val bottom = calculatePaddingBottom().toInt()
    
      setPadding(left, top, right, bottom)
    }
    
    private fun calculatePaddingLeft(): Float {
      clipSizeL = Math.min(measuredWidth / 4, Math.max(leftClipX, 0)).toFloat()
      shadowSizeL = Math.max(0f, -bShadowDx) + Math.max(0f, bShadowRadius)
      return clipSizeL + shadowSizeL + paddingL
    }
    ......
    
  • 设置影响最终绘制效果的参数
    • 参数如下:
      /**
       * 划分阴影和裁剪区域,即前面提到的蓝色边框。
       */
      private val clipBorderL: Float
          get() {
              return shadowSizeL
          }
      private val clipBorderR: Float
          get() {
              return width - shadowSizeR
          }
      /**
       * 内容物的左边界,边界右侧为不受裁剪影响的区域。
       * 同理的还有contentBorderT、contentBorderR、contentBorderB。
       * 下述四者组成了前面提到的红色边框。
       */
      private val contentBorderL: Float
          get() {
              return shadowSizeL + clipSizeL
          }
      private val contentBorderT: Float
          get() {
              return shadowSizeT
          }
      private val contentBorderR: Float
          get() {
              return width - shadowSizeR - clipSizeR
          }
      private val contentBorderB: Float
         get() {
              return height - shadowSizeB
          }
      
      • Kotlin语言的val属性,默认自带get方法,上述语法为重写自带的get方法;
    • 为何不在计算padding时设置这些参数,而采取重写get方法的方式?
      • 最后onDraw要使用到的各项参数,需要根据控件最终的widthheight进行计算,而最终的宽高需要算上留给阴影、背景图案的padding,因此不能在计算padding时设置;
      • Kotlin中访问属性时,实质上调用的是该属性的get方法,所以最终访问该属性时,会调用到重写后的get方法,此时属性的值会再次进行计算,从而保证onDraw方法中用到的属性值受控件最终大小的限制。

3.对原始padding的支持
  • 方法 通过自定义参数paddingL等实现对原始padding的支持,如计算padding时用到的函数:
    private fun calculatePaddingLeft(): Float {
    	clipSizeL = Math.min(measuredWidth / 4, Math.max(leftClipX, 0)).toFloat()
        shadowSizeL = Math.max(0f, -bShadowDx) + Math.max(0f, bShadowRadius)
        // 这里使用自定义参数paddingL而不是paddingLeft
        return clipSizeL + shadowSizeL + paddingL 
     }
    
  • 使用自定义参数的原因
    • 受父布局的影响,一般ViewonMeasure方法会被多次调用(具体原因尚不是很清晰😝),所以在onMeasure中的setPaddingToDrawBG()方法会被多次调用,即会多次设置padding

    • 假设这里不使用自定义的参数paddingL等,而是使用View自带的属性paddingLeft,那么上一次设置的padding值会影响到当前padding的设置,最终导致padding的值为一个累加的效果。

  • paddingL的赋值时机 在控件初始化时进行记录,即Kotlin中的init方法被调用时。
    init {
       ...... // 其他参数的设置
       paddingL = paddingLeft
       paddingT = paddingTop
       paddingR = paddingRight
       paddingB = paddingBottom
    }
       ```
    
  • 对原始padding支持的前后对比
    在这里插入图片描述
    在这里插入图片描述

4.总结
  • padding的灵活使用,使得控件在满足大部分TextView功能的前提下,又含有阴影、背景图案、原始padding等功能。
  • 暂时不支持android:singleLine="true"属性,设置之后导致阴影、背景图案丢失(后面再研究如何支持😁)。
  • 对于阴影、背景图案的绘制,这里不做详细介绍,详情可见BookMark,欢迎赐教❤。

万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。

opLW原创七言律诗,转载请注明出处

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值