UI技巧:优雅实现红点效果!

刘望舒

共 6581字,需浏览 14分钟

 · 2022-06-08

 安卓进阶涨薪训练营,让一部分人先进大厂

大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。


详情见文章:皇叔的最新作来啦!



作者:yechaoa
https://blog.csdn.net/yechaoa/article/details/117339632


前言

今天介绍一种特别的方式优雅实现小红点效果:「BadgeDrawable」,保证让你眼前一亮!


效果展示


BadgeDrawable简介

  • 用途:给View添加动态显示信息(小红点提示效果)
  • app主题需使用Theme.MaterialComponents.*
  • api 要求18+ 也就Android 4.3以上(api等级对应关系)

API使用说明

API描述
backgroundColor背景色
badgeTextColor文本颜色
alpha透明度
number显示的提示数字
maxCharacterCount最多显示字符数量(99+包括‘+’号)
badgeGravity显示位置
horizontalOffset水平方向偏移量
verticalOffset垂直方向偏移量
isVisible是否显示

实例说明

主要包括多个场景下的红点显示,如Textview、Button、TabLayout等上的红点。

实例1:TextView

// 布局文件XML
        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)
        }
    })
}

实例2:Button

// 布局文件XML
    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">

            android:id="@+id/mb_badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button小红点示例" />



// 逻辑代码
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)
        }
    })
}

实例3:ImageView

// 布局文件XML
    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">

            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" />



// 逻辑代码
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)
        }
    })
}

实例4:TabLayout

// 布局文件XML
        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">

                    android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Android" />

                    android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Kotlin" />

                    android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Flutter" />

    

// 逻辑代码
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)
            }
        }
    }

实例5:BottomNavigationView

 // 布局文件XML
    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


源码解析

来一段最简单的代码示例看看:

BadgeDrawable.create(this@BadgeDrawableActivity).apply {
    // ...
    BadgeUtils.attachBadgeDrawable(this, mBinding.mbBadge, mBinding.flBtn)
}

不难发现,有两个关键点:

  1. BadgeDrawable.create
  2. BadgeUtils.attachBadgeDrawable

源码分析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

源码分析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+。

至此,关于BadgeDrawable的使用和源码解析介绍完毕。





为了失联,欢迎关注我防备的小号


浏览 40
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报