美文网首页
Android技术分享:如何自定义View代替通知动画?

Android技术分享:如何自定义View代替通知动画?

作者: 笨笨11 | 来源:发表于2021-10-15 18:15 被阅读0次

    在Demo中通过ObjectAimator实现的效果,使用一个View同样可以实现。

    实现这个自定义View需要解决的问题:

    1. 重写onMeasure计算自己的大小
    2. 文本绘制
    3. 图片加载展示为圆形
      • 图片加载涉及到的优化(如大小、缓存)
    4. 动画效果
      • 消息出现
      • 消息被顶上去
      • 消息关闭

    本篇文章我们先实现一条消息的基本绘制,也就是前三条(除图片缓存)下一篇文章中再加上动画效果。

    通知消息基本数据结构由3个部分组成:头像、昵称、状态(进入/退出); 为了便于拓展,我们定义一个数据类型来保存:

    data class Message(
        val avatar: String,
        val nickname: String,
        val status: Int,// 1=join,2=leave
        val shader: BitmapShader? = null,
        val bitmap: Bitmap? = null
    )
    复制代码
    

    因为暂时只实现一条消息的绘制,我们暂时用成员变量mMessage将数据保存起来。

    完成View的测量(onMeasure):
    想要测量自身大小,得要先知道自己都有什么东西占地方,对吧。
    头像、昵称、状态(进入/退出的提示文字),这些再加上它们之间的间距。

    观察一下这个示意图,感觉高度以提示文本的高度为基准来计算就可以了。 并且昵称最多只有6个字(三个点的省略号可以粗略算是一个字的宽度)

    那么每条message的高度=进出状态文本的高度+文本上下padding。
    本View最多容纳两条通知,所以View的高度=两条message的高度+它们之间的padding。
    View的宽度=本条message最多的字符数(我数了一下一共11个)+头像直径+各种padding。

    宽高都明确了,代码也就好写了:

    private val fontSize = context.resource.getDimensionPixelSize(R.dimen.sp12)
    private val statusTextPadding = context.resource.getDimensionPixelSize(R.dimen.dp5)
    private val avatarPadding = context.resource.getDimensionPixelSize(R.dimen.dp2)
    private val messagePadding = context.resource.getDimensionPixelSize(R.dimen.dp8)
    
    private var messageHeight = 0
    private var avatarHeight = 0
    
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 提示消息最多两行,先计算好一行的高度,加上通知之间的padding就是总高度
        messageHeight = fontSize + statusTextPadding.shl(1)
        avatarHeight = messageHeight - avatarPadding.shl(1)
    
        val width = 11/*最多一共11个字*/ * fontSize + avatarPadding.shl(1) + statusTextPadding.shl(1) + avatarHeight
        val height = messageHeight.shl(1) + messagePadding
    
        setMeasuredDimension(
            MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
            MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        )
        /*
            以上的变量,如最多几个字、字间距、各种padding,改为依赖注入的方式会更好
        */
    }
    复制代码
    

    先实现一个简单的图片加载功能,可以使用开源库来实现,我这里写了个简单的http加载。

    private fun loadImage(uri: String, callback: (BitmapShader?, Boolean) -> Unit) {
        Thread {
            try {
                var http = URL(uri).openConnection() as HttpURLConnection
                http.connectTimeout = 5000
                http.readTimeout = 5000
                http.requestMethod = "GET"
                http.connect()
    
                var iStream = http.inputStream
                val options = BitmapFactory.Options()
                options.inJustDecodeBounds = true
    
                BitmapFactory.decodeStream(iStream, null, options)
                val outWidth = options.outWidth
                val outHeight = options.outHeight
    
                val minDimension = outWidth.coerceAtMost(outHeight)
                options.inSampleSize = floor((minDimension.toFloat() / avatarHeight).toDouble()).toInt()
                options.inPreferredConfig = Bitmap.Config.RGB_565
                options.inJustDecodeBounds = false
    
                iStream.close()
    
                http = URL(uri).openConnection() as HttpURLConnection
                http.connectTimeout = 5000
                http.readTimeout = 5000
                http.requestMethod = "GET"
                http.connect()
                iStream = http.inputStream
    
                val bitmap = BitmapFactory.decodeStream(iStream, null, options) ?: throw IOException("bitmap is null")
                iStream.close()
    
                post { callback.invoke(bitmap, true) }
            } catch (e: IOException) {
                callback.invoke(null, false)
                e.printStackTrace()
            } catch (e: SocketTimeoutException) {
            }
        }.start()
    }
    复制代码
    

    接下来就可以实现绘制方法了,绘制顺序为:背景——文本——图片;由于消息长短看起来像是变长的(实际上在onMeasure里已经定好了最大长度),所以要再计算一次这条message的宽度。

    override fun onDraw(canvas: Canvas) {
        if (mMessage == null)
            return
    
        val msg = mMessage!!
        paint.textSize = fontSize.toFloat()
        paint.color = Color.parseColor("#F3F3F3")
    
        // 字体的y轴的0并不是最上方或最下方,而是基于一个叫baseline的东西
        // 所以需要先计算出baseline距离实际中心点的距离,在绘制时加上这个差值
        val metrics = paint.fontMetrics
        // 计算公式为(bottom - top) / 2 - bottom
        // = abs(top) / 2 - bottom / 2 
        // = (abs(top) - bottom) / 2
        val fontCenterOffset = (abs(metrics.top) - metrics.bottom) / 2
    
        val statusText = if (msg.status == 1) "进入直播间" else "退出直播间"
        val nickname = if (msg.nickname.length > 5) msg.nickname.substring(0, 5) + "..." else msg.nickname
    
        // statusTextWidth的测量可以放到初始化的时候,反正长度固定,没必要每次都测量。
        val statusTextWidth = paint.measureText(statusText)
        val nicknameWidth = paint.measureText(nickname)
        // 计算这条消息实际与View左边距离多远
        // view宽度 - messageLeft = message的宽度
        val messageLeft = measuredWidth - nicknameWidth - statusTextWidth - statusTextPadding * 3 - avatarPadding.shl(1) - avatarHeight
    
        // 绘制背景
        // 添加一个左侧的半圆
        path.addArc(messageLeft, 0f, messageLeft + avatarPadding + avatarHeight.toFloat(), messageHeight.toFloat(), 90f, 180f)
        // 添加一个长方形,与上面的圆连接起来
        path.moveTo(messageLeft + avatarHeight.shr(1).toFloat(), 0f)
        path.lineTo(measuredWidth.toFloat(), 0f)
        path.lineTo(measuredWidth.toFloat(), messageHeight.toFloat())
        path.lineTo(messageLeft + avatarHeight.shr(1).toFloat(), messageHeight.toFloat())
    
        // 填充
        paint.style = Paint.Style.FILL
        paint.color = Color.parseColor("#434343")
        canvas.drawPath(path, paint)
    
        // 绘制进出状态的文字
        paint.color = Color.WHITE
        canvas.drawText(statusText, measuredWidth - statusTextWidth - statusTextPadding, messageHeight.shr(1) + fontCenterOffset, paint)
    
        // 绘制昵称
        paint.color = Color.parseColor("#BCBCBC")
        canvas.drawText(nickname, measuredWidth - statusTextWidth - statusTextPadding.shl(1) - nicknameWidth, messageHeight.shr(1) + fontCenterOffset, paint)
    
        // 绘制圆形图片,这里用BitmapShader实现
        msg.bitmap?.let {
            // 加了shader之后图片就固定在0,0的位置了
            // 所以我这里直接移动了画布,绘制前完成后再恢复回去
            canvas.save()
            paint.shader = msg.shader
            val translateOffset = (messageHeight - it.width).shr(1)
            canvas.translate(messageLeft + translateOffset, translateOffset.toFloat())
            canvas.drawCircle(it.width.shr(1).toFloat(), it.width.shr(1).toFloat()/*messageHeight.shr(1).toFloat()*/, avatarHeight.shr(1).toFloat(), paint)
            paint.shader = null
            canvas.restore()
        }
    }
    复制代码
    

    最后增加一个添加数据的方法,一个没有动画效果的通知就完成了。

    fun addMessage(avatar: String, nickname: String) {
        mMessage = Message(avatar, nickname, 1)
        // 这里先将文本绘制上去,不等待图片,否则图片过大或服务器延迟过高会导致通知显示不及时
        invalidate()
        loadImage(avatar) { bitmap, success ->
            if (!success)
                return@loadImage
    
            val shader = BitmapShader(bitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
            mMessage?.let {
                it.bitmap = bitmap
                it.shader = shader
            }
        }
    
        // loadImage已经自己维护好线程切换了,这里直接主线程调用更新即可
        invalidate()
    }
    

    Android View源码解析 ——→视频地址

    作者:anyRTC
    链接:https://juejin.cn/post/7018756818776113160
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

          本文标题:Android技术分享:如何自定义View代替通知动画?

          本文链接:https://www.haomeiwen.com/subject/viimoltx.html