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

大致实现思路如下:
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字体被裁剪 、 图片被拉伸或压缩 等等问题
在我接触项目之前一直采用的这个方案,不可否认的是这个方案其实用 imageView隐藏了很多的坑,但是因为我们的业务需要比较频繁的刷新view,所以还是留下了性能和清晰度的问题
核心思想:
1、把原始 View 绘制成一张 Bitmap,再用一个 ImageView 来承载这张图。
2、之后只需要对 ImageView 做缩放,就能达到“缩放 View”的视觉效果。
大致实现流程:
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 中
核心思路:
1、 通过修改 view的sceleX/Y直接实现缩放效果,用于解决第一代方案的性能和清晰度问题
2、 配合 setPivotX / setPivotY 调整缩放参考点,解决拖拽边界和视觉错位问题
// 设置缩放参考点(pivot) view.pivotX = view.width / 2f // 中心为基准 view.pivotY = view.height / 2f // 缩放 view.scaleX = 0.8f view.scaleY = 0.8f
这个方案的重点是解决滑动冲突,因为在滑动时默认计算的是 view的原大小,而scaleX/Y都是视觉上的效果,就会出现视觉上view并没有拖动到边界,而实际上已经拖不动的问题,所以这个时候 view.pivot X/Y就显得很重要了,通过pivot可以将视觉上上的view在接触到边界后的视觉位置继续移动
在 ViewDragHelper 的 onViewPositionChanged 中实现以下操作
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的一个缩放效果,所以就有了这个临时方案
设计思路
1、 递归遍历整个 View 树: 从根布局开始,逐个访问子 View , 对每个子 View 的 LayoutParams 修改宽高
2、 处理 TextView 字体 、 ImageView圆角等: 对 TextView ,除了修改宽高,还要用 setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize) 调整文字大小
3、 保持父布局正确识别大小 : 修改 LayoutParams 后调用 requestLayout() 确保布局更新
4、 可选动画/比例缩放 : 可以用一个统一的缩放比例,对整个布局和文字做增量缩放
核心代码:
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
封装可缩放接口
interface Scalable {
fun applyScale(scale: Float)
}
在单独特性的子 view中单独进行适配,例如TextView中触发applyScale执行修改textSize
这样我们只需要调用不同 view的applyScale
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)
}
}
其实在工程实践里面很少有最优解,更多的是 “阶段解”,我们的每个版本都是为了在当前的条件下一步一步的解决问题。方案的取舍,其实是需求、体验、性能、维护、成本之间的平衡
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3094251/ ,如需转载,请注明出处,否则将追究法律责任。