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

在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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容