1.前言
通常情况下,我们在做小红点效果的时候,会有两种选择:
自定义 BadgeView,然后设置给目标 View
xml 写一个 View,然后设置 shape
有的同学可能会想,能实现不就行了吗,是的,代码优不优雅、骚不骚的不重要,代码和人只要有一个能跑就行
...
不过,今天来介绍一种不同的方式来实现小红点效果,或许会让你眼前一亮~
2.效果
3.简介
用途:给 View 添加动态显示信息(小红点提示效果)
app 主题需使用Theme.MaterialComponents.*
api 要求18+
也就 Android 4.3 以上(api等级对应关系)
4.实现拆解
4.1TabLayout
<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>
复制代码
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
<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" />
复制代码
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
<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>
复制代码
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
<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>
复制代码
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
<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" />
复制代码
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 整理
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.相关文档
9.最后
写作不易,如果对你有一丢丢帮助或启发,感谢点赞支持 ^ - ^
评论