1 2 3 4

临时111

设计
左边
退出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 截断
posted @ 2026-05-15 16:09  y丶innocence  阅读(5)  评论(0)    收藏  举报