小紅點用於通知未讀消息,在應用中處處可見。本文將介紹三種實現方案。分別是:多控件方案、單控件繪製方案、容器控件繪製方案。不知道你會更偏向哪一種方案?android
Demo 使用 Kotlin 編寫,Kotlin系列教程能夠點擊這裏canvas
這是自定義控件系列教程的第五篇,系列文章目錄以下:bash
多控件最容易想到的方案:TextView
做爲主體控件,View
做爲附屬小紅點控件相互疊加。效果以下:app
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvMsg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="消息"
android:textSize="20sp"/>
<View
android:layout_width="6dp"
android:layout_height="6dp"
android:background="@drawable/red_shape"
app:layout_constraintEnd_toEndOf="@id/tvMsg"
app:layout_constraintTop_toTopOf="@id/tvMsg" />
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
其中red_shape
是一個紅色圓形shape
資源文件,代碼以下:ide
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="20dp"
android:height="20dp"/>
<solid android:color="#ff0000"/>
</shape>
複製代碼
若要顯示未讀消息數,能夠將View
換成TextView
。佈局
這個方案最大的優勢是簡單直觀,若是項目趕,沒有太多時間深思,用這交差也不錯。post
但它的缺點是增長了控件的數量,若是一個頁面中有3個小紅點,就增長3個控件。字體
有什麼辦法能夠兩個控件合成一個控件?ui
是否是能夠自定義一個TextView
,在右上角繪製一個紅圈。spa
繪製分爲兩步:
Canvas
有現成的 API 繪製圓圈:
public class Canvas extends BaseCanvas {
/**
* Draw the specified circle using the specified paint. If radius is <= 0, then nothing will be
* drawn. The circle will be filled or framed based on the Style in the paint.
*
* @param cx The x-coordinate of the center of the cirle to be drawn
* @param cy The y-coordinate of the center of the cirle to be drawn
* @param radius The radius of the cirle to be drawn
* @param paint The paint used to draw the circle
*/
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
super.drawCircle(cx, cy, radius, paint);
}
}
複製代碼
只需計算出圓心座標和半徑,而後在onDraw()
中調用該 API 便可繪製。
背景的圓心應該是消息數的中心點,背景的半徑依賴於消息數的長短,好比,9 條未讀消息就比 999 條的背景要小一圈。
先繪製背景,再繪製消息數,是爲了避免讓其被背景擋住。
Canvas
有現成的 API 繪製文字:
public class Canvas extends BaseCanvas {
/**
* Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
* based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
super.drawText(text, x, y, paint);
}
}
複製代碼
其中第三個參數y
是指文字基線的縱座標,以下圖所示:
畫文字的關鍵是求出基線在父控件中的縱座標,當前 case 的示意圖以下:
圓圈表明小紅點的背景,紫線是圓圈的直徑,也是文字的中軸線。小紅點繪製在控件的右上角,圓圈的上邊和右邊分別貼住控件的上邊和右邊,因此圓圈頂部切線的縱座標爲 0。問題變成已知半徑raduis
,
top
,
bottom
,求 baseLine 縱座標?(top是負值,bottom爲正值)
分解一下計算步驟:
文字底部的縱座標減掉 bottom 的值就是基線的縱座標:
baseline = radius + (bottom - top) / 2 - bottom
而後只要在自定義控件的onDraw()
中先繪製背景再繪製消息數便可,自定義控件完整代碼以下:
//'自定義TextView'
open class TagTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {
//'消息數字體大小'
var tagTextSize: Float = 0F
set(value) {
field = value
textPaint.textSize = value
}
//'消息數字體顏色'
var tagTextColor: Int = Color.parseColor("#FFFFFF")
set(value) {
field = value
textPaint.color = value
}
//'背景色'
var tagBgColor: Int = Color.parseColor("#FFFF5183")
set(value) {
field = value
bgPaint.color = value
}
//'消息數字體'
var tagTextTypeFace: Typeface? = null
//'消息數'
var tagText: String? = null
//'背景和消息數的間距'
var tagTextPaddingTop: Float = 5f
var tagTextPaddingBottom: Float = 5f
var tagTextPaddingStart: Float = 5f
var tagTextPaddingEnd: Float = 5f
//'消息數字體區域'
private var textRect: Rect = Rect()
//'消息數畫筆'
private var textPaint: Paint = Paint()
//'背景畫筆'
private var bgPaint: Paint = Paint()
init {
//'構建消息數畫筆'
textPaint.apply {
color = tagTextColor
textSize = tagTextSize
isAntiAlias = true
textAlign = Paint.Align.CENTER
style = Paint.Style.FILL
tagTextTypeFace?.let { typeface = tagTextTypeFace }
}
//'構建背景畫筆'
bgPaint.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = tagBgColor
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//'只有當消息數不爲空時才繪製小紅點'
tagText?.takeIf { it.isNotEmpty() }?.let { text ->
textPaint.apply {
//'1.獲取消息數區域大小'
getTextBounds(text, 0, text.length, textRect)
fontMetricsInt.let {
//'背景寬=消息數區域寬+邊距'
val bgWidth = (textRect.right - textRect.left) + tagTextPaddingStart + tagTextPaddingEnd
//'背景高=消息數區域高+邊距'
val bgHeight = tagTextPaddingBottom + tagTextPaddingTop + it.bottom - it.top
//'取寬高中的較大值做爲半徑'
val radius = if (bgWidth > bgHeight) bgWidth / 2 else bgHeight / 2
val centerX = width - radius
val centerY = radius
//'2.繪製背景'
canvas?.drawCircle(centerX, centerY, radius, bgPaint)
//'3.繪製基線'
val baseline = radius + (it.bottom - it.top) / 2 - it.bottom
canvas?.drawText(text, width - radius, baseline, textPaint)
}
}
}
}
}
複製代碼
而後就能像這樣使用自定義控件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<test.taylor.com.taylorcode.ui.custom_view.tag_view.TagTextView
android:id="@+id/ttv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="bug"/>
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
class TagTextViewActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tag_textview_activity)
ttv.tagText = "+99"
ttv.tagTextSize = dip(8F)
ttv.tagTextColor = Color.YELLOW
}
}
複製代碼
把小紅點的顯示細節隱藏在一個自定義View中,這樣佈局文件和業務層代碼會更加簡潔清晰。
但這個方案也有如下缺點:
TextView
、ImageView
和Button
須要顯示小紅點,那就須要分別構建三種類型的自定義View。因而乎就有了第三種方案~~
第三種方案較前兩種略複雜,限於篇幅就留到下一篇接着講。