Android technology sharing: how to customize View instead of notification animation?

Keywords: Android

The effect achieved by ObjectAimator in Demo can also be achieved by using a View.

Problems to be solved to implement this custom View:

  1. Override onMeasure to calculate its own size
  2. Text drawing
  3. The picture is loaded and displayed as a circle
    • Optimization involved in image loading (such as size and cache)
  4. Animation effect
    • The message appears
    • The news was pushed up
    • Message close

In this article, we first realize the basic drawing of a message, that is, the first three (except picture cache) and the animation effect in the next article.

The basic data structure of notification message consists of three parts: Avatar, nickname and status (enter / exit); To facilitate expansion, we define a data type to save:

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

Because only one message can be drawn temporarily, we use the member variable mMessage to save the data temporarily.

Complete the measurement of View (onMeasure):
If you want to measure your size, you have to know what you have, right.
Avatar, nickname, status (prompt text for entry / exit), plus the spacing between them.

Take a look at the schematic diagram and feel that the height is calculated based on the height of the prompt text. And the nickname is only 6 words at most (the ellipsis of three points can be roughly regarded as the width of one word)

Then, the height of each message = the height of the incoming and outgoing status text + text padding.
This View can accommodate up to two notifications, so the height of the View = the height of two message s + the padding between them.
View width = the maximum number of characters in this message (I counted 11 in total) + avatar diameter + various padding.

When the width and height are clear, the code is easy to write:

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) {
    // The maximum number of prompt messages is two lines. First calculate the height of one line and add the padding between notifications to get the total height
    messageHeight = fontSize + statusTextPadding.shl(1)
    avatarHeight = messageHeight - avatarPadding.shl(1)

    val width = 11/*Up to 11 words*/ * 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)
    )
    /*
        For the above variables, such as the maximum number of words, word spacing and various padding, it would be better to change the way of dependency injection
    */
}
Copy code

First, implement a simple image loading function, which can be implemented using the open source library. I wrote a simple http loading here.

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()
}
Copy code

Next, you can implement the drawing method. The drawing order is: background - text - picture; Since the length of the message seems to be increasing (in fact, the maximum length has been set in onMeasure), calculate the width of the message again.

override fun onDraw(canvas: Canvas) {
    if (mMessage == null)
        return

    val msg = mMessage!!
    paint.textSize = fontSize.toFloat()
    paint.color = Color.parseColor("#F3F3F3")

    // The 0 of the y-axis of the font is not the top or bottom, but based on something called baseline
    // Therefore, it is necessary to calculate the distance between the baseline and the actual center point, and add this difference when drawing
    val metrics = paint.fontMetrics
    // The calculation formula is (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) "Enter the live broadcasting room" else "Exit the studio"
    val nickname = if (msg.nickname.length > 5) msg.nickname.substring(0, 5) + "..." else msg.nickname

    // The measurement of statusTextWidth can be put at the time of initialization. Anyway, the length is fixed. It is not necessary to measure every time.
    val statusTextWidth = paint.measureText(statusText)
    val nicknameWidth = paint.measureText(nickname)
    // Calculate the actual distance between this message and the left side of the View
    // view width - messageleft = width of message
    val messageLeft = measuredWidth - nicknameWidth - statusTextWidth - statusTextPadding * 3 - avatarPadding.shl(1) - avatarHeight

    // Draw background
    // Add a semicircle on the left
    path.addArc(messageLeft, 0f, messageLeft + avatarPadding + avatarHeight.toFloat(), messageHeight.toFloat(), 90f, 180f)
    // Add a rectangle to connect with the circle above
    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())

    // fill
    paint.style = Paint.Style.FILL
    paint.color = Color.parseColor("#434343")
    canvas.drawPath(path, paint)

    // Draw in and out status text
    paint.color = Color.WHITE
    canvas.drawText(statusText, measuredWidth - statusTextWidth - statusTextPadding, messageHeight.shr(1) + fontCenterOffset, paint)

    // Draw nicknames
    paint.color = Color.parseColor("#BCBCBC")
    canvas.drawText(nickname, measuredWidth - statusTextWidth - statusTextPadding.shl(1) - nicknameWidth, messageHeight.shr(1) + fontCenterOffset, paint)

    // Draw a circular picture, which is implemented here with BitmapShader
    msg.bitmap?.let {
        // After adding a shader, the picture is fixed at the position of 0, 0
        // So I directly moved the canvas here and restored it after painting
        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()
    }
}
Copy code

Finally, a method of adding data is added, and a notification without animation effect is completed.

fun addMessage(avatar: String, nickname: String) {
    mMessage = Message(avatar, nickname, 1)
    // Here, draw the text first and don't wait for the picture, otherwise the picture is too large or the server delay is too high, which will lead to untimely display of the notice
    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 has maintained its own thread switching. Here, you can directly call the main thread to update
    invalidate()
}

Android View source code analysis - → Video address

Author: anyRTC
Link: Nuggets
Source: rare earth Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Posted by woobarb on Fri, 15 Oct 2021 02:40:55 -0700