1.前言
通常情況下,我們在做小紅點效果的時候,會有兩種選擇:
- 自定義BadgeView,然后設定給目標View
- xml寫一個View,然后設定shape
有的同學可能會想,能實作不就行了嗎,是的,代碼優不優雅、騷不騷的不重要,代碼和人只要有一個能跑就行…
不過,今天來介紹一種不同的方式來實作小紅點效果,或許會讓你眼前一亮~
2.效果

3.簡介

- 用途:給View添加動態顯示資訊(小紅點提示效果)
- app主題需使用
Theme.MaterialComponents.* - api 要求
18+也就Android 4.3以上(api等級對應關系)
4.實作拆解
4.1TabLayout

- xml:
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#FFFAF0"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include"
app:tabIndicator="@drawable/shape_tab_indicator"
app:tabIndicatorColor="@color/colorPrimary"
app:tabIndicatorFullWidth="false"
app:tabMaxWidth="200dp"
app:tabMinWidth="100dp"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/colorPrimary"
app:tabTextColor="@color/gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kotlin" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flutter" />
</com.google.android.material.tabs.TabLayout>
- kotlin:
private fun initTabLayout() {
// 帶數字小紅點
mBinding.tabLayout.getTabAt(0)?.let {
it.orCreateBadge.apply {
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.red)
badgeTextColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.white)
number = 6
}
}
// 不帶數字小紅點
mBinding.tabLayout.getTabAt(1)?.let {
it.orCreateBadge.apply {
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.red)
badgeTextColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.white)
}
}
}
4.2.TextView

- xml:
<TextView
android:id="@+id/tv_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="小紅點示例"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_layout" />
- kotlin:
private fun initTextView() {
// 在視圖樹變化
mBinding.tvBadge.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
BadgeDrawable.create(this@BadgeDrawableActivity).apply {
badgeGravity = BadgeDrawable.TOP_END
number = 6
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.colorPrimary)
isVisible = true
BadgeUtils.attachBadgeDrawable(this, mBinding.tvBadge)
}
mBinding.tvBadge.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
4.3.Button
- xml:
<FrameLayout
android:id="@+id/fl_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_badge">
<com.google.android.material.button.MaterialButton
android:id="@+id/mb_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button小紅點示例" />
</FrameLayout>
- kotlin:
private fun initButton() {
mBinding.mbBadge.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
@SuppressLint("UnsafeOptInUsageError")
override fun onGlobalLayout() {
BadgeDrawable.create(this@BadgeDrawableActivity).apply {
badgeGravity = BadgeDrawable.TOP_START
number = 6
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.red)
// MaterialButton本身有間距,不設定為0dp的話,可以設定badge的偏移量
verticalOffset = 15
horizontalOffset = 10
BadgeUtils.attachBadgeDrawable(this, mBinding.mbBadge, mBinding.flBtn)
}
mBinding.mbBadge.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
關于MaterialButton的使用及決議可查看:Android MaterialButton使用詳解,告別shape、selector
4.4.ImageView
- xml:
<FrameLayout
android:id="@+id/fl_img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fl_btn">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/siv_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="Image小紅點示例"
android:src="@mipmap/ic_avatar" />
</FrameLayout>
- kotlin:
private fun initImageView() {
mBinding.sivBadge.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
@SuppressLint("UnsafeOptInUsageError")
override fun onGlobalLayout() {
BadgeDrawable.create(this@BadgeDrawableActivity).apply {
badgeGravity = BadgeDrawable.TOP_END
number = 99999
// badge最多顯示字符,默認999+ 是4個字符(帶'+'號)
maxCharacterCount = 3
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.red)
BadgeUtils.attachBadgeDrawable(this, mBinding.sivBadge, mBinding.flImg)
}
mBinding.sivBadge.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
關于ShapeableImageView的使用及決議可查看:Android ShapeableImageView使用詳解,告別shape、三方庫
4.5.BottomNavigationView
- xml:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:itemBackground="@color/colorPrimary"
app:itemIconTint="@color/white"
app:itemTextColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
- kotlin:
private fun initNavigationView() {
mBinding.navigationView.getOrCreateBadge(R.id.navigation_home).apply {
backgroundColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.red)
badgeTextColor = ContextCompat.getColor(this@BadgeDrawableActivity, R.color.white)
number = 9999
}
}
TabLayout和BottomNavigationView原始碼中直接提供了創建
BadgeDrawable的api,未提供的使用BadgeUtils,
5.常用API整理
| API | 描述 |
|---|---|
| backgroundColor | 背景色 |
| badgeTextColor | 文本顏色 |
| alpha | 透明度 |
| number | 顯示的提示數字 |
| maxCharacterCount | 最多顯示字符數量(99+包括‘+’號) |
| badgeGravity | 顯示位置 |
| horizontalOffset | 水平方向偏移量 |
| verticalOffset | 垂直方向偏移量 |
| isVisible | 是否顯示 |
6.原始碼決議
來一段最簡單的代碼示例看看:
BadgeDrawable.create(this@BadgeDrawableActivity).apply {
// ...
BadgeUtils.attachBadgeDrawable(this, mBinding.mbBadge, mBinding.flBtn)
}
不難發現,有兩個關鍵點:
- BadgeDrawable.create
- BadgeUtils.attachBadgeDrawable
下面繼續跟一下,看看原始碼里究竟是做了什么
6.1.BadgeDrawable.create
create實際呼叫的是構造方法:
private BadgeDrawable(@NonNull Context context) {
this.contextRef = new WeakReference<>(context);
ThemeEnforcement.checkMaterialTheme(context);
Resources res = context.getResources();
badgeBounds = new Rect();
shapeDrawable = new MaterialShapeDrawable();
badgeRadius = res.getDimensionPixelSize(R.dimen.mtrl_badge_radius);
badgeWidePadding = res.getDimensionPixelSize(R.dimen.mtrl_badge_long_text_horizontal_padding);
badgeWithTextRadius = res.getDimensionPixelSize(R.dimen.mtrl_badge_with_text_radius);
textDrawableHelper = new TextDrawableHelper(/* delegate= */ this);
textDrawableHelper.getTextPaint().setTextAlign(Paint.Align.CENTER);
this.savedState = new SavedState(context);
setTextAppearanceResource(R.style.TextAppearance_MaterialComponents_Badge);
}
構造方法里有這么一行:ThemeEnforcement.checkMaterialTheme(context); 檢測Material主題,如果不是會直接拋出例外
private static void checkTheme(
@NonNull Context context, @NonNull int[] themeAttributes, String themeName) {
if (!isTheme(context, themeAttributes)) {
throw new IllegalArgumentException(
"The style on this component requires your app theme to be "
+ themeName
+ " (or a descendant).");
}
}
這也是上面為什么說主題要使用Theme.MaterialComponents.*
然后創建了一個文本繪制幫助類,TextDrawableHelper
比如設定文本居中:textDrawableHelper.getTextPaint().setTextAlign(Paint.Align.CENTER);
其他的就是text屬性的獲取和設定,跟我們平時設定一毛一樣,比較好理解,
繪制文本之后怎么顯示出來呢?繼續跟attachBadgeDrawable,
6.2.BadgeUtils.attachBadgeDrawable
public static void attachBadgeDrawable(@NonNull BadgeDrawable badgeDrawable, @NonNull View anchor, @Nullable FrameLayout customBadgeParent) {
setBadgeDrawableBounds(badgeDrawable, anchor, customBadgeParent);
if (badgeDrawable.getCustomBadgeParent() != null) {
badgeDrawable.getCustomBadgeParent().setForeground(badgeDrawable);
} else {
if (USE_COMPAT_PARENT) {
throw new IllegalArgumentException("Trying to reference null customBadgeParent");
}
anchor.getOverlay().add(badgeDrawable);
}
}
這里先是判斷badgeDrawable.getCustomBadgeParent() != null,這個parent view的型別就是FrameLayout,不為空的情況下,層級前置,
為空的情況下先是判斷了if (USE_COMPAT_PARENT),這里其實是對api level的判斷
static {
USE_COMPAT_PARENT = VERSION.SDK_INT < 18;
}
核心代碼:
anchor.getOverlay().add(badgeDrawable);
如果有同學做過類似全域添加View的需求,這行代碼就看著比較熟悉了,
ViewOverlay,視圖疊加,也可以理解為浮層,在不影響子view的情況下,可以添加、洗掉View,這個api就是android 4.3加的,這也是為什么前面說api 要求18+,
ok,至此關于BadgeDrawable的使用和原始碼決議就介紹完了,
7.Github
https://github.com/yechaoa/MaterialDesign
歡迎去主頁或Github,查看更多關于MaterialDesign組件的分享,
8.相關檔案
- BadgeDrawable
- BadgeUtils
- ViewOverlay
9.最后
寫作不易,如果對你有一丟丟幫助或啟發,感謝點贊支持 ^ - ^
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/413493.html
標籤:其他
下一篇:pthread的奇怪行為
