前言
在不同阶段,哪怕是"临时方案",也有它存在的价值------它帮助我们快速推进了项目,并为后续优化打下了实践基础。
说起动态修改view的大小,好像很简单,第一反应应该是修改LayoutParams下的宽高。需求很简单,但是真正实现起来才会发现有很多问题需要解决:交互体验、边界值计算、性能优化等等。
于是,在产品需求和技术现实的双重驱动下,尝试了不同的方案。每一个方案都不是"完美解",但它们都解决了当时我们最急迫的问题,也让我们对"方案选择"有了更深的理解:技术不在于炫技,而在于能否在合适的时机,满足最合适的需求。

初代方案:直接修改view的LayoutParams
大致实现思路如下:
kotlin
val layoutParams = mView.getLayoutParams()
layoutParams.width = 100
layoutParams.height = 100
setLayoutParams(layoutParams)
在这个方案下出现了非常多问题
1、setLayoutParams 会触发 requestLayout(),实际测量和绘制 不是立即完成 , 如果紧接着读取 view.width 或 view.height,可能还是旧值
2、不同父布局对 LayoutParams 类型要求不同,例如约束布局是ConstraintLayout.LayoutParams、线性布局是LinearLayout.LayoutParams,需要做很长的适配工作
3、原始布局可能使用wrap_content或0dp/match_constraint(ConstraintLayout),直接赋值固定宽高后,可能破坏布局约束逻辑,ConstraintLayout 中,如果原来宽度为 0dp(match_constraint),改成固定宽高可能导致布局错位
4、也是最大的一个问题,子View与内容自适应问题,改变父View尺寸后,子View或文字可能需要重新适配,不处理子View,可能出现TextView字体被裁剪、图片被拉伸或压缩等等问题
第一个上线方案:View生成ImageView
在我接触项目之前一直采用的这个方案,不可否认的是这个方案其实用imageView隐藏了很多的坑,但是因为我们的业务需要比较频繁的刷新view,所以还是留下了性能和清晰度的问题
核心思想:
1、把原始 View 绘制成一张 Bitmap,再用一个 ImageView 来承载这张图。
2、之后只需要对 ImageView 做缩放,就能达到"缩放 View"的视觉效果。
大致实现流程:
kotlin
var bitmap = view.drawToBitmap()
bitmap = Bitmap.createScaledBitmap(
bitmap,
(bitmap.width * progress / 100f).toInt(),
(bitmap.height * progress / 100f).toInt(),
false
)
mView.setImageBitmap(bitmap)
这样可以完全避免对子 View、文字、边界逻辑的修改,属于"一步到位"的替代方案。
但是也有一些缺陷,例如
1、性能与内存消耗
(1) View → Bitmap → 内存占用高
(2) 频繁截图或缩放会触发 GC,造成卡顿
2、清晰度问题
(1) Bitmap 有固定分辨率,放大会模糊
(2) 有需要时在,要在捕获时提高分辨率,但这又会增加内存开销
3、View 的一些硬件加速特效(如elevation阴影)不会出现在 Bitmap 中
第二个优化方案:scaleX / scaleY + 解决滑动边界冲突问题
核心思路:
1、通过修改view的sceleX/Y直接实现缩放效果,用于解决第一代方案的性能和清晰度问题
2、配合 setPivotX / setPivotY 调整缩放参考点,解决拖拽边界和视觉错位问题
kotlin
// 设置缩放参考点(pivot)
view.pivotX = view.width / 2f // 中心为基准
view.pivotY = view.height / 2f
// 缩放
view.scaleX = 0.8f
view.scaleY = 0.8f
这个方案的重点是解决滑动冲突,因为在滑动时默认计算的是view的原大小,而scaleX/Y都是视觉上的效果,就会出现视觉上view并没有拖动到边界,而实际上已经拖不动的问题,所以这个时候view.pivotX/Y就显得很重要了,通过pivot可以将视觉上上的view在接触到边界后的视觉位置继续移动
在ViewDragHelper的onViewPositionChanged 中实现以下操作
kotlin
fun adjustPivotForRightBoundary(changedView: View, dx: Float, parentWidth: Int) {
val scaleX = changedView.scaleX
val scaledWidth = changedView.width * scaleX
val leftBoundary = changedView.left + scaledWidth
if (leftBoundary > parentWidth) {
// 计算视觉偏移
val overScroll = leftBoundary - parentWidth
// 修正 pivotX,确保缩放后的视觉边缘贴合父布局
var newPivotX = changedView.pivotX - overScroll / scaleX
// 限制 pivotX 在 [0, width] 范围内
newPivotX = newPivotX.coerceIn(0f, changedView.width.toFloat())
changedView.pivotX = newPivotX
} else if (changedView.left < 0) {
// 左边界处理
val overScroll = -changedView.left.toFloat()
var newPivotX = changedView.pivotX + overScroll / scaleX
newPivotX = newPivotX.coerceIn(0f, changedView.width.toFloat())
changedView.pivotX = newPivotX
}
}
....//解决pivotY的思路参考上面的逻辑
第三个临时方案:递归修改所有view大小
在第二个方案遇到的一些问题中,在想,我们能不能通过手动处理所有的view,实现每个view的一个缩放效果,所以就有了这个临时方案
设计思路
1、递归遍历整个 View 树:从根布局开始,逐个访问子 View,对每个子 View 的 LayoutParams 修改宽高
2、处理 TextView 字体、ImageView圆角等:对 TextView,除了修改宽高,还要用 setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize) 调整文字大小
3、保持父布局正确识别大小:修改 LayoutParams 后调用 requestLayout() 确保布局更新
4、可选动画/比例缩放:可以用一个统一的缩放比例,对整个布局和文字做增量缩放
核心代码:
kotlin
fun resizeAllChildViews(viewGroup: ViewGroup, scale: Float) {
for (i in 0 until viewGroup.childCount) {
val childView: View = viewGroup.getChildAt(i)
// 存储每个视图的初始大小
if (childView is ViewGroup) {
resizeAllChildViews(childView, scale) // 递归调用处理子 ViewGroup
}
if (childView is TextView) {
// 如果是 TextView,调整字号
val newTextSize = (initialTextSizes[childView] ?: 0f) * scale
childView.setTextSize(TypedValue.COMPLEX_UNIT_PX, newTextSize)
}
val newMarginStart = ((initialMarginStartSizes[childView] ?: 0) * scale).toInt()
val newMarginTop = ((initialMarginTopSizes[childView] ?: 0) * scale).toInt()
val newMarginEnd = ((initialMarginEndSizes[childView] ?: 0) * scale).toInt()
val newMarginBottom = ((initialMarginBottomSizes[childView] ?: 0) * scale).toInt()
val newPaddingStart = ((initialPaddingStartSizes[childView] ?: 0) * scale).toInt()
val newPaddingTop = ((initialPaddingTopSizes[childView] ?: 0) * scale).toInt()
val newPaddingEnd = ((initialPaddingEndSizes[childView] ?: 0) * scale).toInt()
val newPaddingBottom = ((initialPaddingBottomSizes[childView] ?: 0) * scale).toInt()
val layoutParams = childView.layoutParams as ViewGroup.MarginLayoutParams
val initialWidth = initialSizes[childView]?.first ?: 0
val initialHeight = initialSizes[childView]?.second ?: 0
val newHeight = (initialHeight * scale).toInt()
if (childView is TextView) {
// 针对textView要计算避免最后一个字不显示
val paint = Paint()
val newTextSize = (initialTextSizes[childView] ?: 0f) * scale
paint.textSize = newTextSize // 设置字号,可以根据需要设置
val textWidth = paint.measureText(childView.text.toString())
// 添加额外的空间,考虑到 TextView 的 padding 和 margin
val extraSpace = 2 * childView.paddingStart + 2 * childView.paddingEnd +
newMarginStart + newMarginEnd
layoutParams.width = (textWidth + extraSpace).toInt()
} else {
val newWidth = (initialWidth * scale).toInt()
layoutParams.width = newWidth
}
childView.setPadding(newPaddingStart, newPaddingTop, newPaddingEnd, newPaddingBottom)
layoutParams.height = newHeight
layoutParams.setMargins(newMarginStart, newMarginTop, newMarginEnd, newMarginBottom)
childView.layoutParams = layoutParams
childView.requestLayout() // 请求布局以确保更新生效
}
}
这个方案的重点是解决方案二的一些问题
1、方案2是视觉上的缩放,会导致用户点击了视觉上的"空白位置"实际上是view的真实位置触发了相关的点击事件
2、解决旋转、位移等需要用到真实布局大小和位置信息的逻辑出现的问题
但是这个方案有一些很棘手和需要消耗大量精力的问题
1、不同类型的 View 可能需要不同处理:TextView、ImageView、RecyclerView 等
2、大布局 + 多层 View 树 + 频繁缩放 → 可能导致 UI 卡顿/掉帧
3、如果后续新增自定义 View 或特殊控件,需要额外适配
4、缩放时的动画很诡异,会出现一节一节的缩放的情况
5、View的绘制时机会出现冲突等问题,例如有些view是子view撑开父view的大小,有些是父view限制了子view的大小,让子TextView自动换行,等等一系列操作都需要很复杂的适配
第四个视觉最优方案:管理不同的缩放
在第三个临时方案的处理中,我们处理了所有的子view的一个缩放效果,但是因为是手动处理,系统对于view的处理就被我强行替代了,那有没有可能让系统帮我们执行缩放呢?这就是我们的第四个方案
1、在第三个方案的基础上不再强求指定所有的view的大小
2、针对textView修改size/换行处理
3、对于宽度和高度是wrap_content和match_parent的不进行手动缩放
4、封装所有子view
封装可缩放接口
kotlin
interface Scalable {
fun applyScale(scale: Float)
}
在单独特性的子view中单独进行适配,例如TextView中触发applyScale执行修改textSize
这样我们只需要调用不同view的applyScale
kotlin
private fun traverseChildren(viewGroup: ViewGroup, action: (View) -> Unit) {
for (i in 0 until viewGroup.childCount) {
val child = viewGroup.getChildAt(i)
action(child)
if (child is ViewGroup) traverseChildren(child, action)
}
}
回顾一下
其实在工程实践里面很少有最优解,更多的是"阶段解",我们的每个版本都是为了在当前的条件下一步一步的解决问题。方案的取舍,其实是需求、体验、性能、维护、成本之间的平衡,所以,动态修改 View 大小的过程,对我们来说不仅仅是"解决一个 UI 问题",更像是一次团队的认知升级: 从"怎么做"到"为什么这么做",从"快点搞定"到"找到适合长期演进的方案"。