The effect achieved by ObjectAimator in Demo can also be achieved by using a View.
Problems to be solved to implement this custom View:
- Override onMeasure to calculate its own size
- Text drawing
- The picture is loaded and displayed as a circle
- Optimization involved in image loading (such as size and cache)
- 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.