小紅點(diǎn)用于通知未讀消息,在應(yīng)用中到處可見赴涵。本文將介紹三種實(shí)現(xiàn)方案。分別是:多控件方案订讼、單控件繪制方案、容器控件繪制方案扇苞。不知道你會更偏向哪種方案欺殿?
Demo 使用 Kotlin 編寫,Kotlin系列教程可以點(diǎn)擊這里
這是自定義控件系列教程的第五篇鳖敷,系列文章目錄如下:
多控件方案
多控件最容易想到的方案:TextView
作為主體控件棍潘,View
作為附屬小紅點(diǎn)控件相互疊加。效果如下:
布局文件如下:
<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
資源文件崖媚,代碼如下:
<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>
若要顯示未讀消息數(shù)亦歉,可以將View
換成TextView
。
這個方案最大的優(yōu)點(diǎn)是簡單直觀畅哑,如果項(xiàng)目趕肴楷,沒有太多時間深思,用這交差也不錯荠呐。
但它的缺點(diǎn)是增加了控件的數(shù)量赛蔫,如果一個頁面中有3個小紅點(diǎn),就增加3個控件泥张。
有什么辦法可以兩個控件合成一個控件呵恢?
單控件繪制方案
是不是可以自定義一個TextView
,在右上角繪制一個紅圈媚创。
繪制分為兩步:
- 繪制紅色背景
- 繪制消息數(shù)
繪制背景
Canvas
有現(xiàn)成的 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);
}
}
只需計(jì)算出圓心坐標(biāo)和半徑渗钉,然后在onDraw()
中調(diào)用該 API 即可繪制。
背景的圓心應(yīng)該是消息數(shù)的中心點(diǎn)钞钙,背景的半徑依賴于消息數(shù)的長短晌姚,比如,9 條未讀消息就比 999 條的背景要小一圈歇竟。
繪制消息數(shù)
先繪制背景挥唠,再繪制消息數(shù),是為了不讓其被背景擋住焕议。
Canvas
有現(xiàn)成的 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);
}
}
其中第三個參數(shù)y
是指文字基線的縱坐標(biāo)宝磨,如下圖所示:
畫文字的關(guān)鍵是求出基線在父控件中的縱坐標(biāo)弧关,當(dāng)前 case 的示意圖如下:
圓圈代表小紅點(diǎn)的背景,紫線是圓圈的直徑唤锉,也是文字的中軸線世囊。小紅點(diǎn)繪制在控件的右上角,圓圈的上邊和右邊分別貼住控件的上邊和右邊窿祥,所以圓圈頂部切線的縱坐標(biāo)為 0株憾。問題變成已知半徑
raduis
,top
晒衩,bottom
嗤瞎,求 baseLine 縱坐標(biāo)?(top是負(fù)值听系,bottom為正值)
分解一下計(jì)算步驟:
- raduis:紫線的縱坐標(biāo)
- (bottom - top) / 2:文字區(qū)域總高度的一半
- radius + (bottom - top) / 2:文字底部的縱坐標(biāo)
文字底部的縱坐標(biāo)減掉 bottom 的值就是基線的縱坐標(biāo):
baseline = radius + (bottom - top) / 2 - bottom
然后只要在自定義控件的onDraw()
中先繪制背景再繪制消息數(shù)即可贝奇,自定義控件完整代碼如下:
//'自定義TextView'
open class TagTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {
//'消息數(shù)字體大小'
var tagTextSize: Float = 0F
set(value) {
field = value
textPaint.textSize = value
}
//'消息數(shù)字體顏色'
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
}
//'消息數(shù)字體'
var tagTextTypeFace: Typeface? = null
//'消息數(shù)'
var tagText: String? = null
//'背景和消息數(shù)的間距'
var tagTextPaddingTop: Float = 5f
var tagTextPaddingBottom: Float = 5f
var tagTextPaddingStart: Float = 5f
var tagTextPaddingEnd: Float = 5f
//'消息數(shù)字體區(qū)域'
private var textRect: Rect = Rect()
//'消息數(shù)畫筆'
private var textPaint: Paint = Paint()
//'背景畫筆'
private var bgPaint: Paint = Paint()
init {
//'構(gòu)建消息數(shù)畫筆'
textPaint.apply {
color = tagTextColor
textSize = tagTextSize
isAntiAlias = true
textAlign = Paint.Align.CENTER
style = Paint.Style.FILL
tagTextTypeFace?.let { typeface = tagTextTypeFace }
}
//'構(gòu)建背景畫筆'
bgPaint.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = tagBgColor
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//'只有當(dāng)消息數(shù)不為空時才繪制小紅點(diǎn)'
tagText?.takeIf { it.isNotEmpty() }?.let { text ->
textPaint.apply {
//'1.獲取消息數(shù)區(qū)域大小'
getTextBounds(text, 0, text.length, textRect)
fontMetricsInt.let {
//'背景寬=消息數(shù)區(qū)域?qū)?邊距'
val bgWidth = (textRect.right - textRect.left) + tagTextPaddingStart + tagTextPaddingEnd
//'背景高=消息數(shù)區(qū)域高+邊距'
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>
- 在 Activity 中引用并設(shè)置參數(shù):
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
}
}
把小紅點(diǎn)的顯示細(xì)節(jié)隱藏在一個自定義View中,這樣布局文件和業(yè)務(wù)層代碼會更加簡潔清晰靠胜。
但這個方案也有以下缺點(diǎn):
- 控件類型綁定:若當(dāng)前界面分別有一個
TextView
掉瞳、ImageView
和Button
需要顯示小紅點(diǎn),那就需要分別構(gòu)建三種類型的自定義View浪漠。 - 控件需留 padding:小紅點(diǎn)是控件的一部分陕习,為了不讓小紅點(diǎn)與控件本體內(nèi)容重疊,控件需給小紅點(diǎn)留有 padding址愿,即控件占用空間會變大衡查,在布局文件中可能引起連鎖反應(yīng),使得其他控件位置也需要跟著微調(diào)必盖。
于是乎就有了第三種方案~~
容器控件繪制方案
第三種方案較前兩種略復(fù)雜拌牲,限于篇幅就留到下一篇接著講。