ITPub博客

首页 > 移动开发 > ANDROID > 产品需求驱动下的技术演进:动态缩放View的不同方案

产品需求驱动下的技术演进:动态缩放View的不同方案

原创 ANDROID 作者:山有木xi 时间:2025-08-26 16:37:22 0 删除 编辑

前言:

在不同阶段,哪怕是 “临时方案”,也有它存在的价值——它帮助我们快速推进了项目,并为后续优化打下了实践基础。

 

说起动态修改 view的大小,好像很简单,第一反应应该是修改 LayoutParams 下的宽高 。 需求很简单,但是真正实现起来才会发现有很多问题需要解决:交互体验、边界值计算、性能优化等等。

 

于是,在产品需求和技术现实的双重驱动下,尝试了不同的方案。每一个方案都不是 “完美解”,但它们都解决了当时我们最急迫的问题,也让我们对“方案选择”有了更深的理解: 技术不在于炫技,而在于能否在合适的时机,满足最合适的需求。

初代方案:直接修改 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字体被裁剪 、 图片被拉伸或压缩 等等问题

 

 

第一个上线方案: View生成ImageView

在我接触项目之前一直采用的这个方案,不可否认的是这个方案其实用 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 中

 

 

第二个优化方案: scaleX / scaleY + 解决滑动 边界 冲突问题

核心思路:

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,实现每个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/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle OCA(Java),IBM高级工程师认证,中国软件行业人才(高级c语言),高级Android工程师,对数据库并发与性能调优也有一定了解

注册时间:2019-04-25

  • 博文量
    147
  • 访问量
    390763