
1. 先把“居中”这件事拆清楚gravity 与 layout_gravity 是两码事在 Android UI 设计里“文本居中对齐”大概是新手问得最多、也最容易自我怀疑的问题。明明在 XML 里写了android:gravitycenter文本却没有居中或者预览图里看着居中跑到真机上偏了又或者换了控件类型同样的写法完全不生效。这些问题的根源往往不是某一行代码写错了而是没有把 Android 的对齐体系拆明白。Android 里控制“对齐”的属性一共有两套很多初级开发者在入门阶段容易把它们混为一谈。第一套叫android:gravity它控制的是View 内部内容的对齐方式也就是文本、子元素在控件内部怎么摆放。第二套叫android:layout_gravity它控制的是View 自身在其父容器中的对齐方式是控件整体往父布局的哪个位置靠。理解到这一层文本居中的很多困惑就已经解决了一半。1.1 内容对齐与布局对齐一句话区分用大白话说android:gravity管的是“家里东西怎么摆”android:layout_gravity管的是“这个家放在整栋楼的什么位置”。举个例子一个宽 200dp、高 100dp 的 TextView如果设置android:gravitycenter文本会在这 200x100 的区域中间显示如果只设置android:layout_gravitycenter意思是这个 TextView 整体在父容器里居中但文本在 TextView 内部仍然是默认的左上角对齐。很多人的困惑就在这里看到一个控件没有居中第一反应是去调layout_gravity但真正出问题的是gravity。反过来也一样文本本身居中但控件位置不对问题又出在layout_gravity。建议在实际写布局时把这两种属性分清楚写之前先问一句我到底是想让“文本在控件里居中”还是想让“控件在布局里居中”目标不同用的属性就完全不同。1.2 XML 与代码设置方式的区别XML 里的写法很直观TextView android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter android:text文本内容 android:textSize16sp /代码里的写法对应的是setGravity()和setLayoutGravity()textView.gravity Gravity.CENTER textView.layoutParams (textView.layoutParams as LinearLayout.LayoutParams).apply { gravity Gravity.CENTER }注意一点layout_gravity是LayoutParams上的属性所以代码里赋值时需要先强制转换成具体的LayoutParams类型。LinearLayout 的布局参数里带gravity字段RelativeLayout 的布局参数里带的是addRule所以你不能用一套代码通吃所有布局容器这也是动态创建界面时容易踩的坑。除了gravityAndroid 5.0API 21之后还引入了android:textAlignment属性专门控制文本的对齐方式。它的取值包括textStart、textCenter、textEnd等。这里有个细节值得注意textAlignment和gravity同时存在时gravity在垂直方向仍然有效但水平方向的表现可能被textAlignment覆盖。我的经验是能用gravity的尽量用gravity因为它的行为在各个版本上更稳定textAlignment在低版本设备和 RTL 语言场景下偶尔会出现预期外的表现。1.3 不同控件的默认行为差异Android 里不同的控件对文本居中的默认处理并不一样这是很多人在写样式时“凭经验”翻车的原因。控件默认文本对齐方式备注TextView默认文本左对齐、垂直顶部对齐需要显式设置 gravity 才能居中Button默认文本水平居中、垂直居中但受背景和 padding 影响视觉效果可能不正EditText默认文本左对齐、垂直居中hint 同理原生控件有默认 padding 和背景CheckBox / RadioButton默认文本靠右排列与选择框一起组合不是文本独立居中AppCompatButton与 Button 类似居中实现依赖内部 CompoundDrawable 和 padding更复杂一些这个表格是我在实际开发中反复验证过的。最典型的是 Button很多人以为 Text 在 Button 里天然居中但换一个自定义背景后文字偏偏就偏了。这里涉及 Button 内部的默认内边距、最小宽度、最小高度以及系统样式里的stateListAnimator。后续我会专门展开讲。2. 不同控件的文本居中实现清单理解了基本概念接下来就要落到具体的控件上了。不同控件有不同的居中策略下面按高频场景整理一份可以直接“抄作业”的清单。2.1 TextView单行与多行两种处理思路单行文本相对简单在 XML 里写两行属性即可TextView android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter android:singleLinetrue android:text单行文本居中 /wrap_content高度下文本垂直方向通常没什么偏移问题。但真正容易出问题的是多行文本的垂直居中。比如一个 TextView 固定高度 200dp里面有三行文字想要整体在这 200dp 里垂直居中直接设置android:gravitycenter看起来是对的但如果 TextView 同时设置了lineSpacingExtra或lineSpacingMultiplier行间距会把文本的视觉中心往下推。一个稳妥的做法是先把行间距确定好再调整高度。同时可以考虑用android:includeFontPaddingfalse去掉字体上下的默认内边距。这个东西在 iOS 和 Android 上的表现差异很大Android 的 TextView 默认会为字体预留上下空间在需要精确定位文本的场景比如把文本对齐到一张图标的中心时includeFontPadding往往是最终决定成败的属性。多行文本垂直居中的另一个技巧是设置android:gravitycenter_vertical并配合android:minLines。如果你明确知道文本最多占两行就在布局里声明android:minLines2这样高度不会在文本从一行变两行时突然跳动文本始终在那个固定高度区域内垂直居中。2.2 Button默认样式的坑与解决方式Button 的文本居中是所有控件里最逼疯人的一个。原生 Button 在没有自定义样式时文本确实是居中的。但只要你给 Button 设置了一个自定义背景尤其是带圆角的 shape drawable文本就可能开始偏。这背后有两个原因第一Button 在系统 Material 主题下有默认的insetTop和insetBottom这个参数会在按钮内部加上下内边距导致文本向上偏第二如果自定义背景的padding和 Button 的padding叠加就会造成视觉重心偏移。我在实战中通常会这么做Button android:layout_widthwrap_content android:layout_height48dp android:backgrounddrawable/bg_btn_rounded android:gravitycenter android:insetTop0dp android:insetBottom0dp android:paddingTop0dp android:paddingBottom0dp android:text确定 /insetTop和insetBottom是 API 26 后 Button 才有的属性低版本上如果你用的仍是 AppCompatButton需要动态设置if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { button.insetTop 0 button.insetBottom 0 }你的minHeight、minWidth也要检查一下。系统 Button 默认会有最小值这会撑大按钮的实际尺寸文本在更大的空间里“看起来”偏上。设置android:minHeight0dp和android:minWidth0dp能有效规避这类问题。还有一种更彻底的方案如果你对 Button 的自定义程度要求很高直接用 TextView 叠加背景和点击效果。很多 UI 设计稿里的“按钮”其实只是带背景的文本用 TextView 实现反而能完全掌控 padding 和 gravity不会被系统的 inset 逻辑干扰。但代价是你要自己处理点击水波纹在用 Material 主题时还得额外注意?attr/selectableItemBackground的用法。2.3 EditText 与 SearchView提示文本、输入文本都要居中EditText 让人头疼的点在于提示文本hint、输入文本、光标三者的对齐方式可能各不相同。下面是一个居中搜索框的常见写法EditText android:layout_widthmatch_parent android:layout_height48dp android:gravitycenter_vertical android:hint请输入搜索关键词 android:paddingStart16dp android:paddingEnd48dp android:backgrounddrawable/bg_search_input /注意水平方向我不建议把 EditText 的文本设置成居中因为用户输入长文本时居中排布会严重影响阅读体验。真实的搜索框通常都是“文本左对齐、垂直居中”左侧放图标右侧放清除按钮。如果你确实需要把 placeholder 显示为居中比如某些表单场景的短输入框可以用android:gravitycenter但要知晓输入内容一旦超过一行体验就会变差。SearchView 的居中则更特殊。SearchView 内部其实是一个 LinearLayout 套了一个搜索图标和一个 AutoCompleteTextView直接设置 gravity 没效果。如果你需要搜索框文字居中显示一个偏 hack 的方式是在 OnQueryTextListener 里去拿内部的 AutoCompleteTextView 再设置 gravitysearchView.findViewByIdAutoCompleteTextView( androidx.appcompat.R.id.search_src_text ).gravity Gravity.CENTER这种写法属于“迫不得已”升级 AndroidX 版本后内部 id 是否改动需要验证所以我的建议是如果产品对搜索框文本居中要求很高直接在布局里用 EditText 自己拼一个别依赖 SearchView。2.4 自定义 ViewonDraw 里的文本居中如果你在自定义 View 里用 Canvas 画文字gravity那一套就完全失效了。Canvas 画文本默认的基准线baseline在左下角任何文字内容都是从 baseline 往上生长的。要让文本在指定的矩形区域内居中需要手动计算val bounds Rect() paint.getTextBounds(text, 0, text.length, bounds) val x (width - bounds.width()) / 2f val y height / 2f - (bounds.top bounds.bottom) / 2f canvas.drawText(text, x, y, paint)这里的bounds.top是负数bounds.bottom是正数(bounds.top bounds.bottom) / 2表示文本整体中心相对 baseline 的偏移。用控件中心点height / 2f减去这个偏移就能得到正确的 baseline 位置。这个方法是我在自定义图表、标签、水印时反复使用的。getTextBounds拿到的是纯文本的边界不包含文字阴影或描边效果如果你的自定义 View 给文字加了描边、阴影视觉中心还会进一步偏移需要再加上paint.setShadowLayer的偏移量做补偿。另外StaticLayout在画多行文本时会自动处理行高和对齐建议优先用它而不是手动逐行计算。3. 进阶场景图标加文本、多行省略、跑马灯与约束布局控件本身的居中过关后真正的硬骨头都在组合场景里。图标加文本这种组合、多行文本配合省略号、跑马灯与居中的冲突、ConstraintLayout 里的特殊约束策略这些才是日常 UI 设计里最难搞的部分。3.1 图标与文本组合如何整体居中UI 设计稿里经常有一个“图标 文字”的模块整体居中比如首页的底部导航、分类入口图标。这里有两种实现路线我分别说下各自的问题。第一种是TextView加DrawableTextView android:layout_widthwrap_content android:layout_heightwrap_content android:drawableTopdrawable/ic_main android:drawablePadding6dp android:gravitycenter android:text首页 /这种写法最方便但坑在于drawableTop的图标和文本是分开对齐的系统会把图标放在上方、文本放在下方它们的中心线并不严格重合。如果图标尺寸比例特殊或者文本是高矮不一的字符比如有上标下标视觉上就会看到图标和文字错位。更可控的是用LinearLayout组合LinearLayout android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:gravitycenter android:orientationvertical ImageView android:layout_width24dp android:layout_height24dp android:srcdrawable/ic_main / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:gravitycenter android:text首页 / /LinearLayout这个方案里LinearLayout 整体作为一个控件参与父布局的居中内部的图标和文本各自受控你可以在它们之间自由调整间距。缺点是层级多了一层但换来的是精确控制在 UI 还原度要求高的项目里这套方案最稳。3.2 多行文本居中加省略号的兼容方案多行文本配合ellipsize一直是个老大难。android:ellipsizeend在单行模式下效果很好但一旦maxLines大于 1系统对省略号的处理就看机型了。尤其是中文文本部分 ROM 上会截断到很奇怪的位置。比较成熟的做法有两套。第一套是在 TextView 上设置TextView android:layout_width0dp android:layout_heightwrap_content android:ellipsizeend android:gravitycenter android:maxLines2 android:text这是一段很长的文本需要被截断并显示省略号最多显示两行。 android:textAlignmentviewStart /注意多行文本如果要求“整体居中”同时又要求省略号在第二行末尾gravitycenter会让第二行的内容居中显示省略号也跟着居中这往往不符合产品预期。产品通常要的是“两行文本整体居中但每行内部左对齐省略号出现在第二行最右边”。解决思路是分层控制把 TextView 放在一个容器里让容器负责整体居中TextView 自己只负责“左对齐 上限两行 末尾省略”LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter TextView android:layout_widthwrap_content android:layout_heightwrap_content android:gravitystart android:maxLines2 android:ellipsizeend android:text... / /LinearLayout这里TextView的宽度用了wrap_content当文本超长时TextView 的实际宽度会被约束在 LinearLayout 的宽度内而容器整体居中。这样既保证了文本块整体居中又保证了每行内左对齐、省略号显示在末尾。这个技巧在列表卡片、通知栏、评论回复等场景里非常实用。3.3 跑马灯效果与居中的冲突跑马灯Marquee的效果是把文本从右往左滚动显示它的前提条件是TextView 有固定宽度文本超宽并且ellipsizemarquee。一旦文本是“居中”的跑马灯就永远不会触发因为系统判断文本宽度小于控件宽度时不滚动。需求里如果既要求跑马灯、又要求文本在静止时居中这本身是有冲突的。常规的处理手法是把 TextView 宽度设为wrap_content放在容器里居中跑马灯滚动时文本从左侧挤出视觉上不太好看或者用自定义的跑马灯效果做一个无限循环的横向位移动画。实际项目中我倾向于另一种做法在 TextView 里设置android:ellipsizemarquee并固定宽度文本正常从左侧开始滚动不让它居中。因为用户看到跑马灯时注意力在滚动的内容上对起始位置是否居中并不敏感。产品设计要真要求“先居中再滚动”那就得在代码里监听文本宽度和控件宽度动态切换对齐方式成本高且容易出现闪烁。从投入产出比看我通常建议产品经理重新考虑这个效果的必要性。3.4 ConstraintLayout 中的居中策略ConstraintLayout 的居中策略和 LinearLayout 不同。LinearLayout 靠layout_gravity控制子 View 的位置而 ConstraintLayout 是把控件的两个边界约束到父容器的对应边界再设置偏置让控件稳定在中心TextView android:layout_widthwrap_content android:layout_heightwrap_content app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent android:gravitycenter android:text居中文本 /这里的constraintTop_toTopOfparent与constraintBottom_toBottomOfparent一起使用配合默认的bias0.5控件就会在垂直方向居中。如果只约束了 Top 和 Bottom但没有同时设置constraintHeight的类型控件高度是wrap_content时实际计算逻辑会有差异建议高度用wrap_content时还是把四个方向的约束都写上比较省心。ConstrainLayout 里还有一个常见的坑android:gravitycenter只作用于文本在 TextView 内部的居中不能把 TextView 自身移动到 ConstraintLayout 中间。两者的关系又回到第 1 节说的那套逻辑区分清楚之后就不容易乱了。许多项目里ConstraintLayout 还承担了“按百分比对齐”的职责。你可以通过layout_constraintVertical_bias和layout_constraintHorizontal_bias微调文本的位置。比如文本整体偏上 30% 的位置可以写app:layout_constraintVertical_bias0.3这在做一个视觉引导页、弹窗标题布局时很常见。微调的幅度通常不大但视觉效果差异明显。3.5 textAlignment 与 RTL 适配前面提到过android:textAlignment它在适配阿拉伯语、希伯来语等 RTL从右往左语言时非常重要。如果你写死了android:gravitycenter通常不会出问题因为 center 在任何语言方向下都是安全的。但如果你用了android:gravityleft或android:gravityright在 RTL 语言下就会出现文本方向不符合当地阅读习惯的问题。Google 官方推荐的做法是使用start和end替代left和rightTextView android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitystart android:text文本 /start在 LTR 语言下是左边在 RTL 下是右边这样不需要额外判断就能适配。居中对齐不受 RTL 影响但如果你在同一个布局里还包含图标图标的方向就需要注意了。比如一个“箭头 文本”的组合RTL 下箭头应该翻转方向这时用android:autoMirroredtrue可以让 Drawable 自动镜像。4. 文本居中实战坑问题与排查技巧积累了很多居中问题的排查经验后我把最高频的几个问题整理成了一份速查表。如果你在开发中遇到文本居中异常按这个思路逐一排查大部分问题都能定位到根因。4.1 Button 文本视觉不居中的检查路线Button 文本看着不居中先别急着改代码按照下面这个顺序排查检查项说明minHeight/minWidth系统默认值会改变按钮实际尺寸先归零测试insetTop/insetBottomAPI 26 的默认 inset 可能是文本视觉偏移的元凶背景 shape 的 padding背景内部自带 padding 时会和 Button 的 padding 叠加stateListAnimatorMaterial 按钮有 elevation 动画会导致视觉重心变化字体和字号不同字体家族对文本行高的处理不同建议用同一字体对比测试排查时可以打开开发者选项中的“显示布局边界”直接观察 Button 的实际边界和内容边界是否重合。4.2 TextView 文字看上去偏高或偏低这种情况最常出现在中文文本上。Android 的默认字体家族Roboto、思源黑体在字体度量上为上下留了较大空间导致固定高度下文本视觉中心比几何中心偏上或偏下。比较有效的两个属性是android:includeFontPaddingfalse和android:lineSpacingExtra。去掉fontPadding后文本会更紧凑但也会导致某些字形比如带重音的拉丁字符被裁剪在需要支持多语言的应用里要谨慎。另一个思路是用TextView的setLineSpacing调整textView.setLineSpacing(0f, 1.1f)如果你的需求是让文本精确对齐到某一根参考线上靠 padding 调是最直观的但注意padding是四个方向统一生效的不能单独只调垂直方向又不影响其它控件。这种情况下更适合把 TextView 放进一个高度固定的容器里通过gravitycenter_vertical做内部居中。4.3 Emoji 和特殊字体导致的居中偏移Text 中混入 emoji 时TextView 的文本测量结果会产生变化。emoji 字符的宽高和普通文字不一样在部分设备上还会触发字体回退导致包含 emoji 的行比纯文字行更高。如果 TextView 高度是wrap_content文本包含 emoji 后控件高度变大居中位置随之变化视觉上就会出现“跳动”。这种情况比较实用的处理办法是给 TextView 设置固定的lineHeight或在 XML 里用android:lineHeight指定行高保证包含 emoji 和纯文字的行高一致。同时尽量为 emoji 场景选择兼容性好的字体现在 Android 系统默认 emoji 字体已经比较统一了但定制 ROM 上偶尔还是会出现旧的彩色 emoji 渲染。还有一个容易被忽略的点Paint.FontMetrics在不同字体下返回值不同如果你在自定义 View 中根据fontMetrics计算居中位置换字体后计算基准就变了。建议在自定义 View 中总是基于Paint.getFontMetrics()动态计算不要写死任何数值。4.4 用约束布局嵌套替代多层 LinearLayout文本居中问题排查到最后十有五六是布局层级问题。很多团队在进行 UI 设计还原时习惯大量嵌套 LinearLayout每个 LinearLayout 都设置 gravity 和 layout_gravity代码不仅慢而且层级越深居中判断越容易出错。我个人的习惯是能用 ConstraintLayout 的地方优先用 ConstraintLayout尽量减少嵌套。文本本身居中用gravity控件在父布局中居中用layout_constraintBottom_toBottomOf等约束两层逻辑分得很清楚排查时只需要看一个布局文件不用在多层 LinearLayout 之间来回找。当然这也不是绝对的。如果团队偏好 LinearLayout只要把第 1 节的“两套对齐体系”理清楚同样能写出清晰的布局。关键在于“每层布局的 gravity 职责要单一”同一个 LinearLayout 里不要同时出现layout_gravity和内部多元素互相对齐的复杂逻辑否则越改越乱。最后分享一个我这几年积累下来的小习惯在任何文本居中的 UI 设计任务里我都会先把控件背景设置成明显的对比色用“文本中心点”和“控件中心点”做一次肉眼对齐验证。背景色一加偏 1dp 都能看出来验证完再移除背景。这个“土办法”不需要打开开发者工具改一行 XML 就能看到效果效率非常高。文本居中的问题说到底不难但需要细心和耐心一个像素一个像素地调才能还原出设计稿里那种“看起来就是舒服”的效果。