• android 自定义view: 跑马灯-光圈


    本系列自定义View全部采用kt

    **系统: **mac

    android studio: 4.1.3

    **kotlin version:**1.5.0

    gradle: gradle-6.5-bin.zip

    本篇效果:

    8140FE3CF87738708E0C5D0E4F59704F

    前沿

    最近在bilibili看到一个跑马灯光圈效果挺好, 参考着思路写了一下.

    bilibili地址,美中不足的是这是html代码 QaQ

    实现思路

    • 将效果分为3层
      • 第一层: 背景
      • 第二层: 跑马灯光圈
      • 第三层: 展示区

    如图所示:

    Nov-28-2022 17-19-34

    tips: 图片截取自上方bilibili视频

    换到android中直接将view当作背景层, 在利用Canvas绘制跑马灯层即可

    将View圆角化

    // 设置view圆角
    outlineProvider = object : ViewOutlineProvider() {
      override fun getOutline(view: View, outline: Outline) {
        // 设置圆角率为
        outline.setRoundRect(0, 0, view.width, view.height, RADIUS)
      }
    }
    clipToOutline = true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这段代码网上找的,源码还没有看, 有机会再看吧.

    image-20221128173221355

    来看看当前效果:

    CD09F6ED6DBE6895E487C703B7DB64F0

    自定义跑马灯光圈

    这几个字可能有点抽象,所以来看看要完成的效果:

    Nov-28-2022 17-45-34

    接下来只需要吧黄框外面和里面的的去掉就完成了旋转的效果:

    去掉外面:

    Nov-28-2022 17-47-38

    去掉里面:

    Nov-28-2022 17-47-32

    这都是html效果,接下来看看android怎么写:

    class ApertureView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
        companion object {
            val DEF_WIDTH = 200.dp
            val DEF_HEIGHT = DEF_WIDTH
            private val RADIUS = 20.dp
        }
    
        private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    
        private val rectF by lazy {
            val left = 0f + RADIUS / 2f
            val top = 0f + RADIUS / 2f
            val right = left + DEF_WIDTH - RADIUS
            val bottom = top + DEF_HEIGHT - RADIUS
            RectF(left, top, right, bottom)
        }
    
        override fun onDraw(canvas: Canvas) {
            val left = rectF.left + rectF.width() / 2f
            val right = rectF.right + rectF.width()
            val top = rectF.top + rectF.height() / 2f
            val bottom = rectF.bottom + rectF.height() / 2f
    
            // 绘制渐变view1
            paint.color = Color.GREEN
            canvas.drawRect(left, top, right, bottom, paint)
    
            // 绘制渐变view2
            paint.color = Color.RED
            canvas.drawRect(left, top, -right, -bottom, paint)
    
        }
    }
    
    • 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

    这里就是计算偏移量等,都比较简单:

    542DD72464B89550F97E8BAD9EFE6FD5

    因为咋们是view,并且已经测量了view的宽和高,所以超出的部分就不展示了

    跑马灯动起来

    这段代码比较简单,直接开一个animator即可

     private val animator by lazy {
       val animator = ObjectAnimator.ofFloat(this, "currentSpeed", 0f, 360f)
       animator.repeatCount = -1
       animator.interpolator = null
       animator.duration = 2000L
       animator
     }
    
    var currentSpeed = 0f
      set(value) {
        field = value
        invalidate()
      }
            
    override fun onDraw(canvas: Canvas) {
    
      // withSave 保存画布
      canvas.withSave {
        
      // 画布中心点旋转
      canvas.rotate(currentSpeed, width / 2f, height / 2f)
        // 绘制渐变view1 绘制渐变view2
        ...
      }
    }
    
    • 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
    14162A8D36FFE0BEB6CD9B9D5A67446F

    '去掉’里面

    去除里面部分有2种方式

    • 方式一: 利用 clipOutPath() 来clip掉中间区域, 这个api对版本有要求
    • 方式二: 重新绘制一个 RoundRect() 来覆盖掉中间区域

    方式一:

    private val path by lazy {
        Path().also { it.addRoundRect(rectF, RADIUS, RADIUS, Path.Direction.CCW) }
    }
    
    override fun onDraw(canvas: Canvas) {
    
        // withSave 保存画布
        canvas.withSave {
          canvas.clipOutPath(path)
             // 画布中心点旋转
          canvas.rotate(currentSpeed, width / 2f, height / 2f)
          
          // 绘制渐变view1 ..view2...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    方式二:

    override fun onDraw(canvas: Canvas) {
      // withSave 保存画布
      canvas.withSave {
    
        // 画布中心点旋转
        canvas.rotate(currentSpeed, width / 2f, height / 2f)
    
        // 绘制渐变view1
    
        // 绘制渐变view2
    
      }
    
      paint.color = Color.BLACK
      canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    来看看当前效果:

    B9B3733C51780A7AFB53CBA080582B20

    但是现在看起来还是有一点生硬, 可以让view渐变一下

    private val color1 by lazy {
      LinearGradient(width * 1f,height / 2f,width * 1f,height * 1f,
        intArrayOf(Color.TRANSPARENT, Color.RED), floatArrayOf(0f, 1f),
        Shader.TileMode.CLAMP
      )
    }
    
    private val color2 by lazy {
      LinearGradient( width / 2f,height / 2f,width / 2f, 0f,
        intArrayOf(Color.TRANSPARENT, Color.GREEN), floatArrayOf(0f, 1f),
        Shader.TileMode.CLAMP
      )
    }
    
    override fun onDraw(canvas: Canvas) {
    //
      canvas.withSave {
        canvas.rotate(currentSpeed, width / 2f, height / 2f)
       ...
        // 绘制渐变view1
        paint.shader = color1
        canvas.drawRect(left1, top1, right1, bottom1, paint)
        paint.shader = null
    
        // 绘制渐变view2
        paint.shader = color2
        canvas.drawRect(left1, top1, -right1, -bottom1, paint)
        paint.shader = null
      }
    
      // 中间rect
      canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)
    }
    
    • 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

    这样一来,就更有感觉了

    效果图:

    FBFD3920C18DA5E6821CA08C9CFB8052

    基本效果就完成了,那么如何给其他view也可以轻松的添加这个炫酷的边框呢?

    很显然,view是办不到的,所以我们只能自定义viewgroup

    代码没有改变,只是在自定义viewgroup时,onDraw() 不会回调, 因为viewgroup主要就是用来管理view的,所以要想绘制viewgroup最好是重写dispatchDraw()方法,

    在dispatchDraw()方法中,需要注意的是 super.dispatchDraw(canvas), 这个super中会绘制children,

    所以为了避免 view被跑马灯背景覆盖,需要将super.dispatchDraw(canvas) 写到最后一行

    #ApertureViewGroup.kt
    
    override fun dispatchDraw(canvas: Canvas) {
            val left1 = width / 2f
            val top1 = height / 2f
    
            val right1 = left1 + width
            val bottom1 = top1 + width
            canvas.withSave {
                canvas.rotate(currentSpeed, width / 2f, height / 2f
                // 绘制渐变view1
                paint.shader = color1
                canvas.drawRect(left1, top1, right1, bottom1, paint)
                paint.shader = null
    
                if (mColor2 != -1) {
                    // 绘制渐变view2
                    paint.shader = color2
                    canvas.drawRect(left1, top1, -right1, -bottom1, paint)
                    paint.shader = null
                }
            }
    
            paint.color = mMiddleColor
            canvas.drawRoundRect(rectF, mBorderAngle, mBorderAngle, paint)
    
    				// 一定要写到最后一行,否则children会被跑马灯覆盖掉
            super.dispatchDraw(canvas)
        }
    
    • 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

    最后在调用的时候直接:

    
    
        <XXXX View
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center" />
    com.example.customviewproject.f.f2.ApertureViewGroup>
    
    • 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

    本篇代码比较简单,不过这个思路确实挺好玩的!

    最终效果:

    A051CC6A0481AE320B2371E271889D04

    完整代码

    原创不易,您的点赞就是对我最大的帮助!

  • 相关阅读:
    证书过期问题: https://registry.npm.taobao.org npm ERR! code CERT_HAS_EXPIRED npm ERR
    探索信号处理:低通滤波器的原理与应用
    学习MySQL的第三天:函数(基础篇)
    步入TypeScript中的其它类型,js要认怂了吗
    合肥中科深谷嵌入式项目实战——基于ARM语音识别的智能家居系统(二)
    Pandas数据结构
    ⭐每天一道leetcode:21.合并两个有序链表(简单;双指针)
    安装 TypeScript 并编译成JS
    爱上开源之golang入门至实战第四章函数(Func)(九)
    顺丰小哥派件装载问题——典型的01背包问题
  • 原文地址:https://blog.csdn.net/weixin_44819566/article/details/128085734