惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Cyberwarzone
Cyberwarzone
SecWiki News
SecWiki News
Webroot Blog
Webroot Blog
L
LINUX DO - 最新话题
V
Vulnerabilities – Threatpost
T
Troy Hunt's Blog
Cloudbric
Cloudbric
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
H
Heimdal Security Blog
S
Schneier on Security
NISL@THU
NISL@THU
The Hacker News
The Hacker News
Attack and Defense Labs
Attack and Defense Labs
A
Arctic Wolf
V2EX - 技术
V2EX - 技术
Security Latest
Security Latest
AWS News Blog
AWS News Blog
Scott Helme
Scott Helme
W
WeLiveSecurity
S
Secure Thoughts
Y
Y Combinator Blog
GbyAI
GbyAI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - Franky
量子位
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
K
Kaspersky official blog
P
Privacy & Cybersecurity Law Blog
T
Tenable Blog
The GitHub Blog
The GitHub Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
J
Java Code Geeks
Vercel News
Vercel News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
小众软件
小众软件
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
C
CERT Recently Published Vulnerability Notes
Security Archives - TechRepublic
Security Archives - TechRepublic
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Palo Alto Networks Blog

博客园 - y丶innocence

RecyclerView 数据多时无法滑动:ConstraintLayout 约束高度修复笔记 Android Kotlin OkHttp3 WebSocket 长连接与 Gson 数据解析系统笔记 Android + Kotlin + OkHttp WebSocket 相关概念与使用流程笔记(TLS/证书 + 鉴权/会话) AI对话导出markdown格式流程 代理转发 分享文件 面向全球的app的excel导出和kotlin IO原理 安卓导出笔记(未整理) java&kotlin listener 0.7 动画 0.4 View 工作流程 0.3 view 滑动冲突 13. Jetpack 0. 安卓开发艺术探索参考资料 12. Material Design 7. 持久化技术 5. Fragment java 基础 4. UI 开发 3. Activity 2.2 Kotlin 面向对象 2.3 Kotlin高级 2.1 Kotlin基础 1. Android简介 [OpenJudge] 反正切函数的应用 (枚举)(数学) [OpenJudge] 摘花生 (模拟)
临时111
y丶innocence · 2026-05-15 · via 博客园 - y丶innocence

设计
左边
退出icon 2424 margin 左12 右12
右边
两个icon都是24
24
margin
12 icon 20 icon 12

目前实现
左icon
marginstart12
width24

space_start
witdh44

textview
layout_width="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/iv_notification"
app:layout_constraintStart_toEndOf="@+id/space_start"
marginstart12

iv_notification
witdh 44
paddintstart12
paddingend8

iv_title_app_jump
width 48
layout_goneMarginEnd4

space_end
width 48

左48dp + 右46dp:用 Barrier 严谨解决

回答你的问题

right_spacer 改为 48dp 就行?

对的,right_spacer = 48dp(镜像 exit)。

count=1 不严谨?

没有 Barrier 时确实不严谨——链式穿透下,title 直接约束到 icon(46dp),左边 exit 是 48dp,差 2dp。

加上右侧 Barrier 后,Barrier 会在 icon(46dp) 和 right_spacer(48dp) 之间取更靠内侧的那个(48dp),自动补上 2dp 差值:

count=1 时:
  notification.start = W-46
  right_spacer.start = W-48   ← 更靠内侧!
  barrier 选 W-48 ✅

  左 = 48dp = 右 = 48dp → 像素级居中 ✅

核心原理

right_spacer(48dp,始终 INVISIBLE)通过 Barrier 提供了一个 "最低保留宽度",等于左侧 exit 的 48dp。无论右侧 icon 怎么组合,barrier 都保证右侧保留区 ≥ 48dp:

count icon 位置 spacer 位置 Barrier 选 左侧需要 操作
2 W-92 W-48 W-92(icon 更内侧) 补到 92dp left_spacer=44dp VISIBLE
1 W-46 W-48 W-48(spacer 更内侧) 48dp 无需补 ✅
0 GONE W-48 W-48 48dp 无需补 ✅

完整 XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:minHeight="?attr/actionBarSize"
        app:contentInsetStart="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/toolbar_content"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize">

            <!-- ====== 左侧:退出按钮(48dp,始终可见) ====== -->
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_toolbar_exit"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:padding="12dp"
                android:src="@android:drawable/ic_menu_close_clear_cancel"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <!-- ====== 左侧占位符(44dp = 92-48,count=2 时 VISIBLE) ====== -->
            <!--
                count=2 时右侧 icon 总宽=92dp,左侧 exit=48dp
                补 44dp 使左侧也达到 92dp,与右侧对称
            -->
            <Space
                android:id="@+id/left_spacer"
                android:layout_width="44dp"
                android:layout_height="0dp"
                android:visibility="gone"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toEndOf="@id/iv_toolbar_exit"
                app:layout_constraintTop_toTopOf="parent" />

            <!-- ====== 左侧 Barrier ====== -->
            <!--
                方向 = end:取 exit 和 left_spacer 中更靠右的
                count=2 → spacer 可见, barrier = 48+44 = 92dp
                count≠2 → spacer GONE 被忽略, barrier = 48dp
            -->
            <androidx.constraintlayout.widget.Barrier
                android:id="@+id/barrier_start"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:barrierDirection="end"
                app:constraint_referenced_ids="iv_toolbar_exit,left_spacer" />

            <!-- ====== 标题 ====== -->
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:gravity="center"
                android:singleLine="true"
                android:text="A very very long title to test center"
                android:textSize="18sp"
                android:textStyle="bold"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@id/barrier_end"
                app:layout_constraintStart_toEndOf="@id/barrier_start"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Title" />

            <!-- ====== 右侧 Barrier ====== -->
            <!--
                方向 = start:取三者中最靠左的 start 边缘
                GONE 的 view 被忽略,right_spacer(INVISIBLE) 始终参与

                count=2 → min(W-92, W-46, W-48) = W-92
                count=1 → min(W-46, W-48) = W-48  ← spacer 自动补 2dp!
                count=0 → W-48
            -->
            <androidx.constraintlayout.widget.Barrier
                android:id="@+id/barrier_end"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:barrierDirection="start"
                app:constraint_referenced_ids="iv_notification,iv_title_app_jump,right_spacer" />

            <!-- ====== 右侧 icon 1(46dp) ====== -->
            <ImageView
                android:id="@+id/iv_notification"
                android:layout_width="46dp"
                android:layout_height="46dp"
                android:padding="11dp"
                android:src="@android:drawable/ic_dialog_info"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@id/iv_title_app_jump"
                app:layout_constraintTop_toTopOf="parent" />

            <!-- ====== 右侧 icon 2(46dp) ====== -->
            <ImageView
                android:id="@+id/iv_title_app_jump"
                android:layout_width="46dp"
                android:layout_height="46dp"
                android:padding="11dp"
                android:src="@android:drawable/ic_menu_share"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <!-- ====== 右侧占位符(48dp = 镜像 exit,始终 INVISIBLE) ====== -->
            <!--
                始终 INVISIBLE → 始终参与 Barrier 计算(GONE 才会被忽略)
                保证 Barrier 右边界 ≥ 48dp = 左侧 exit 宽度
                
                作用:
                  count=1 → icon=46dp < spacer=48dp → barrier 选 spacer → 补齐 2dp
                  count=0 → 唯一参考 → barrier=48dp
                  count=2 → icon=92dp > spacer=48dp → barrier 选 icon → spacer 被"覆盖"
            -->
            <Space
                android:id="@+id/right_spacer"
                android:layout_width="48dp"
                android:layout_height="0dp"
                android:visibility="invisible"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </com.google.android.material.appbar.MaterialToolbar>

    <!-- ==================== 测试按钮 ==================== -->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_toggle_notification"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Toggle Notification Icon"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_toggle_app_jump"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:text="Toggle AppJump Icon"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_toggle_notification" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_show_both"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Show Both"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_toggle_app_jump" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_hide_both"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:text="Hide Both"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_show_both" />

</androidx.constraintlayout.widget.ConstraintLayout>

完整 Kotlin

package com.example.chartresearch

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.example.chartresearch.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var vb: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        vb = ActivityMainBinding.inflate(layoutInflater)
        setContentView(vb.root)

        vb.ivNotification.isVisible = true
        vb.ivTitleAppJump.isVisible = true
        syncSpacers()

        vb.btnToggleNotification.setOnClickListener {
            vb.ivNotification.isGone = !vb.ivNotification.isGone
            syncSpacers()
        }

        vb.btnToggleAppJump.setOnClickListener {
            vb.ivTitleAppJump.isGone = !vb.ivTitleAppJump.isGone
            syncSpacers()
        }

        vb.btnShowBoth.setOnClickListener {
            vb.ivNotification.visibility = View.VISIBLE
            vb.ivTitleAppJump.visibility = View.VISIBLE
            syncSpacers()
        }

        vb.btnHideBoth.setOnClickListener {
            vb.ivNotification.visibility = View.GONE
            vb.ivTitleAppJump.visibility = View.GONE
            syncSpacers()
        }
    }

    /**
     * 只需要控制左侧占位符!
     * right_spacer 始终 INVISIBLE,由 Barrier 自动处理
     *
     * count=2 → 右侧92dp,左侧需补44dp → left_spacer VISIBLE
     * count=1 → 右侧 Barrier 自动取 max(46,48)=48dp = 左侧48dp → 无需补
     * count=0 → 右侧 Barrier 取 right_spacer=48dp = 左侧48dp → 无需补
     */
    private fun syncSpacers() {
        val count = listOf(vb.ivNotification, vb.ivTitleAppJump).count { it.isVisible }
        vb.leftSpacer.visibility = if (count == 2) View.VISIBLE else View.GONE
        // right_spacer 永远是 INVISIBLE,不需要动
    }
}

三种场景验证

count=2(两个 icon 都可见)

  左侧 Barrier:
    exit.end = 48, left_spacer.end = 48+44 = 92
    barrier = max(48, 92) = 92 ✅

  右侧 Barrier:
    notification.start = W-92
    app_jump.start     = W-46
    right_spacer.start = W-48
    barrier = min(W-92, W-46, W-48) = W-92 ✅

  ┌──────────────────────────────────────────────┐
  │ [✕][sp]       Title 居中        [🔔][↗]     │
  │ 48  44                           46  46      │
  │ ←92dp→                          ←92dp→       │
  │             像素级居中 ✅                      │
  └──────────────────────────────────────────────┘


count=1(只有 notification 可见)

  左侧 Barrier:
    left_spacer GONE → 忽略
    barrier = exit.end = 48 ✅

  右侧 Barrier:
    notification.start = W-46
    app_jump GONE → 忽略
    right_spacer.start = W-48    ← 比 icon 更靠内侧 2dp!
    barrier = min(W-46, W-48) = W-48 ✅

  ┌──────────────────────────────────────────────┐
  │ [✕]          Title 居中       (sp)[🔔]      │
  │ 48dp                          2dp  46dp      │
  │ ←48dp→                       ←48dp→          │
  │             像素级居中 ✅                      │
  │      (barrier 在 W-48 处,icon 和 spacer      │
  │       都在 barrier 右侧,不遮挡 title)        │
  └──────────────────────────────────────────────┘


count=0(都隐藏)

  左侧 Barrier:
    barrier = exit.end = 48 ✅

  右侧 Barrier:
    两个 icon 都 GONE → 忽略
    barrier = right_spacer.start = W-48 ✅

  ┌──────────────────────────────────────────────┐
  │ [✕]          Title 居中            (sp)      │
  │ 48dp                               48dp      │
  │ ←48dp→                            ←48dp→     │
  │             像素级居中 ✅                      │
  └──────────────────────────────────────────────┘

方案总结

项目 说明
居中精度 三种情况 全部像素级居中
Kotlin 核心 1 行(只控制 left_spacer)
right_spacer XML 中 invisible永远不需要动,Barrier 自动利用
关键改进 Barrier 保证右侧保留 ≥ 48dp = 左侧 exit,count=1 时自动补齐 2dp 差值
left_spacer 44dp = 两 icon 总宽(92) − exit 宽(48),只 count=2 时显示
防遮挡 Barrier 是标题的硬边界,title 用 0dp + ellipsize=end 截断