• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    1. 现在,NavigatorBar的onDraw方法有触发了,但重叠问题仍然存在。此时,我第一个猜想是,这个重叠,既然是个残影,即上个绘制没有被清空导致的。那我加上清空画布的代码,是不是就OK了?然后,我就在onDraw方法里面加上清空画布的代码,如下所示:
      // 摘自 NavigatorBar
      override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            // 清空画布,解决TextView.setText时,出现内容重叠的问题。
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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
    • 2
    • 3
    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
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    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)
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
  • 相关阅读:
    线程-API
    取得网络中SQL的服务器列表
    健壮性测试是什么?
    【go语言开发】gorm库连接和操作mysql,实现一个简单的用户注册和登录
    centos7虚拟机部署苍穹私有云环境记录
    STM32的hex文件格式的分析
    Java IO流实现文件复制
    云原生下基于K8S声明式GitOps持续部署工具ArgoCD实战-上
    基于Rook+Ceph的云原生存储架构剖析
    CoM-Px30|RK3358开发初步连载-Andorid系统固件的烧写(三)
  • 原文地址:https://blog.csdn.net/l_o_s/article/details/126055283