Android setText 出现文本重叠的问题

一、今天在做NavigatorBar(导航栏,系统级应用)时,遇到一个问题,在调用TextView.setText方法后,会出现文本重叠的问题,如下图所示:

在这里插入图片描述


二、我的实现场景比较特殊,我的导航栏是无Activity做为容器的,在Service中,直接通过WindowManager.addView()添加NavigatorBar,而NavigatorBar本身是一个自定义View,直接继承自FrameLayout实现的。所以,我的NavigatorBar的getParent()返回的是ViewRootImpl,而不是传统的DecorView。

我的整体排查思路如下:

  1. TextView的实例是否有过变化?,经log查看,确实是同个实例。
  2. 是否有2个TextView存在,导致重叠? 经log查看,只有一个TextView。
  3. 在setText之后,打印text,看看打出来的text是不是最新的?经log查看,是最新的text,出现重叠是旧的text没被清除而留下来的残影。
  4. 调用invalidate()方法,有效? 不起作用。
  5. 尝试调用WindowManager的updateViewLayout,不起作用。(除非修改长宽,但这个会导致闪烁,用户体验不佳)
  6. 改成动态添加TextView,每次设置文本,都先remove现有的,再创建个新的。结果,仍然不起作用。
  7. 在网上搜索相同问题,查出来的文章,有的说,给根布局设置背景,比如,这篇:Android 绘制产生重影(重叠)。后面,经实验,设置个非透明的颜色,比如黑色,白色,都可以,但透明、半透明、不设置,都不行。如果可以给根布局设置纯色背景,那问题到此可以解决。
  8. 但像我这种业务场景的话就不行,原因是一旦某个app设置了跟我背景色不一致的颜色,就会出现颜色相左的问题 【详见图1】 ,如图所示,一条黑条横在页面中间。既然不行,那就继续排查。

在这里插入图片描述


  1. 重新屡了一下思路: 首先,Navigator是自定义View。自定义View就会走onDraw(),那么,我是不是可以考虑在onDraw里面做什么清除操作,或者自已绘制文本,不用TextView,就没这问题了?然后,我就重写Navigator(FrameLayout的子类)的onDraw()方法,并加log。但发现,onDraw方法,并没有被触发。后面,想起来,以前做继承自ViewGroup的自定义View时,也遇到过onDraw没被调用过,原因是ViewGroup默认是不触发draw的,源码如下所示:
 // 摘自 ViewGroup。
 private void initViewGroup() {
        // ViewGroup doesn't draw by default
        if (!isShowingLayoutBounds()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
        .... 省略其它
 }

  1. FrameLayout直接继承自ViewGroup,默认也是没有调onDraw方法。问题搞清楚了,我们现在只需要加这行代码,就可以触发ViewGroup/FrameLayout/NavigatorBar的onDraw()方法:
   // 摘自 NavigatorBar
   init {
        setWillNotDraw(false)
   }

  1. 现在,NavigatorBar的onDraw方法有触发了,但重叠问题仍然存在。此时,我第一个猜想是,这个重叠,既然是个残影,即上个绘制没有被清空导致的。那我加上清空画布的代码,是不是就OK了?然后,我就在onDraw方法里面加上清空画布的代码,如下所示:
  // 摘自 NavigatorBar
  override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 清空画布,解决TextView.setText时,出现内容重叠的问题。
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
    }
  1. 现在,一切都OK了,残影也不见了。但还有个问题,就是虽然根布局的background设置了透明色,但显示出来的,仍然是有个黑色背景(也可能是其它背景,这个颜色猜测是与主题相关也可能无关)。【如果你的到这一步,没有这个问题,可以直接忽略后面的内容】
  2. 这个时候,就好办了,先把onDraw注释掉,发现,什么都不绘制,也会存在这条黑条。那么,就可以证明,这个黑色背景,不是我们的自定义View产生的。此时,我就去google查WindowManager背景色的相关资料,最后在stackoverflow的这个提问Android: Transparend background when using WindowManager里找到了答案,只需要在WindowManager.addView时,给WindowManager.LayoutParams设置format为透明即可,代码如下:
val layoutParams = WindowManager.LayoutParams()
// 设置背景透明
layoutParams.format = PixelFormat.TRANSLUCENT
  1. 【追加】后面经过测试,只要加了第13步的代码,即可解决设置背景透明后,文本会重叠的问题。第10、11步不是必要的。但留着做为参考,在某些类似的场景,也可以做为借鉴思路。

经验证,问题成功解决,关键代码就3行,一行触发onDraw,一行在onDraw里面清空画布,一行设置window为透明。这样,就可以给根布局设置透明背景,或不设置背景,也能解决文本重叠的问题。


最后,分享一下相关代码,共2个类,业务相关的已删除,只保留该博文主题相关的核心代码:

1. NavigatorBarManager.kt

/**
 * @author lyf
 * @date 2022/7/26 20:20
 * @describe 导航栏添加类,封装WindowManager添加View的逻辑
 */
object NavigatorBarManager {

    // navigatorBar显示动画
    private const val animation = android.R.style.Animation_Dialog

    /**
     * 显示navigatorBar
     * @param navigatorBar vnavigatorBar的View
     */
    fun showNavigatorBar(navigatorBar: View, height: Int = 60) {

        val windowManager = navigatorBar.context.getWindowManager()
        val layoutParams = WindowManager.LayoutParams()

        layoutParams.gravity = Gravity.BOTTOM
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT
        layoutParams.height = height
        layoutParams.packageName = navigatorBar.context.packageName
        layoutParams.y = +navigatorBar.context.resources.getDimension(R.dimen.p30).toInt()
        layoutParams.windowAnimations = animation
        
        // 设置背景透明
        layoutParams.format = PixelFormat.TRANSLUCENT
        // 用这个级别的level,可以不挡到toast
        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            .or(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
            .or(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
            .or(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR)
            .or(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH)

        windowManager.addView(navigatorBar, layoutParams)
    }

    fun removeNavigatorBar(navigatorBar: View) {
        navigatorBar.context.getWindowManager().removeView(navigatorBar)
    }

    private fun Context.getWindowManager() =
        getSystemService(Context.WINDOW_SERVICE) as WindowManager

}

2、NavigatorBar.kt

/**
 * @author lyf
 * @date 2022/7/26 20:20
 * @describe 底部导航栏
 */
class NavigatorBar @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    companion object {
        private val TAG = NavigatorBar::class.java.simpleName
    }

    private var guideTv: TextView? = null

    init {

        // 自定义View,重写ViewGroup及其子类时,默认是不会去调onDraw方法。
        // 加上这个,就会触发onDraw,然后,在onDraw里面,清空画布,
        // 就可以解决,TextView.setText时,出现内容重叠的问题。
        setWillNotDraw(false)
        LayoutInflater.from(context).inflate(R.layout.layout_navigator_bar, this)
        guideTv = findViewById(R.id.tv_guide)
        setOnSystemUiVisibilityChangeListener {
            val isFullScreen =
                it.and(View.SYSTEM_UI_FLAG_FULLSCREEN) == View.SYSTEM_UI_FLAG_FULLSCREEN

            val isHideNavigator =
                it.and(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            visibility = if (isFullScreen || isHideNavigator) View.GONE else View.VISIBLE
            Log.d(TAG, "UiVisibility=$it , isFullScreen=$isFullScreen")
        }
    }

    fun setGuideText(guideText: String?) {
        guideTv?.text = guideText.orEmpty()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 清空画布,解决TextView.setText时,出现内容重叠的问题。
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY)
    }

}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值