


























设计
左边
退出icon 2424 margin 左12 右12
右边
两个icon都是2424
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
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 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>
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 截断 |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。