type
status
date
slug
summary
tags
category
icon
password
背景实现步骤思路1. 实现放大和拖动的功能2. 添加双指捏合操作3. 初始化处理4. 边界处理5. 添加 fling 效果6. 解决缩放动画过渡不自然的问题7. Over Zoom 处理8. 嵌套在滚动控件内时事件冲突处理9. 规范代码,细节调优总结思考分解技术的时效性彩蛋参考文档
背景
ZoomImageView 是一个自定义的 ImageView 控件,用于实现对图片的手势缩放、双击缩放以及放大后的平移查看等功能。在我之前的 MeetPhoto 项目中,图片预览功能使用了一个开源 ZoomImageView 控件(这个控件基于 PhotoView 实现,因其代码量较少而选择它)。但我发现这个控件在某些方面的用户体验并不理想,所以我决定对其进行优化,便是这篇博客的由来。
其中两个体验问题见下图
问题一:高清图切换底清图时被重置
问题二:双击缩放到最小动画不自然
实现步骤
思路
本篇博客中 ZoomImageView 的实现思路可以简化为下面这个算式
- imageMatrix:ImageView 的图像矩阵,更新绘图
- suppMatrix:活动矩阵(支持矩阵),所有的操作(平移、缩放)作用于它
- originMatrix:原始矩阵,图像初始化时的矩阵
1. 实现放大和拖动的功能
首先设置
scaleType="matrix"
,这样 ImageView 绘图时才使用图像矩阵 imageMatrix
,添加手势检测器 GestureDetector
,帮助我们捕获到双击和拖动事件,然后处理这些事件。方便矩阵操作,对其添加扩展函数。
2. 添加双指捏合操作
添加
ScaleGestureDetector
,修改 ImageView#onTouch
,同时响应 GestureDetector
和 ScaleGestureDetector
,在缩放事件里添加矩阵缩放的操作。3. 初始化处理
ImageView 初始化过程中,以类似
fitCenter
(保证图片完整显示,图片高或宽按比例放缩到 View 的高或宽,居中显示)显示模式初始化 originMatrix
矩阵。由于图片切换时只会切换
originMatrix
,对应缩放、平移的操作都记录在 suppMatrix
,初始显示效果是一样的 “fitCenter”
——图片尺寸放大相应的 MSCALE_X
值同比例变小,最终的 imageMatrix
显示效果并不会改变,基于此便解决了第一个体验问题。如下图,在图片放大过程中切换同比例的高清图,显示效果是一致的。4. 边界处理
在把
suppMatrix* originMatrix
赋值给 imageMatrix
之前,先对边界进行矫正,左上右下移动超出边界时通过 suppMatrix
平移抵消掉,矫正之后再应用于 imageMatrix
更新绘图。5. 添加 fling 效果
图片放大可拖动时,快速滑动响应
onFling
事件,基于 Android Scroller 滚动特性进行处理。6. 解决缩放动画过渡不自然的问题
动画执行前后 pivot 支点位置变化和边界矫正的原因,导致开头第二个体验不好的问题,修改动画的实现来解决这个问题。
动画开始前,计算终止时的
endMatrix
,对 endMatrix
矫正后求得 endPivotPointF
支点的位置,这样动画行进过程中,便可同步改变 pivot
的位置,同时动画执行过程中,跳过边界矫正。这样做的副作用很明显,我们已经计算出了可用来绘图的 Matrix,却又要反回去计算出 suppMatrix,导致额外的计算量,如果过程不优雅,那结果一定不优雅,这种实现思路并不好。
7. Over Zoom 处理
在双指捏是允许
zoom < minZoom
,并在手指抬起时,通过动画还原到 minZoom
。8. 嵌套在滚动控件内时事件冲突处理
事件冲突总体处理起来很轻松,
down
事件时设置parent
.requestDisallowInterceptTouchEvent(
true
)
优先由 ZoomImageView 响应处理,当 ZoomImageView 滑动到边界时设置parent
.requestDisallowInterceptTouchEvent(
false
)
,在交由父级控件处理。另外双指捏和与父级滚动控件谁先响应的冲突,这里直接简单处理,触屏手指数
>1
都由 ZoomImageView 响应。9. 规范代码,细节调优
对一些变量命名、代码注释等进行规范,最终完整代码如下
替换掉 MeetPhoto 中原来的控件,看下最终效果,撒花。。。✿✿ヽ(°▽°)ノ✿
总结
我们像上面那样一步一步地实现了一个自己的 ZoomImageView 控件,包含原始控件的功能,并解决掉最开始提到的两个体验问题,但依然存在许多问题(如何解决就留给 ZoomImageView 下篇再来讲述吧)。我把这些问题总结如下
- 性能方面,空间、时间复杂度没有考虑(例如第 6 步中重复计算的问题),对大图可能导致内存溢出也未做处理
- 手势交互是否足够灵敏?可以调整触摸滑动的最小距离(通过
getTouchSlop()
获取)来改善用户体验。动画过程中拖动等并行操作未做处理。
- 不支持 over scale (过度放大)、over scroll,因为整体思路和边界矫正并不合理,导致很难扩展 over scroll 功能
- 不支持关闭,不支持 scaleType 等等(不需要的时候就先不要吧)
- 双指捏合同时移动出现抖动的问题
对比下面四个图片预览的功能,第一个第二个都能明显感到抖动,iPhone 体验最好,OPPO 上体验还是不错的,Pixel4 真的没想到这么拉,这个功能体验如果排序的话:
iPhone > OPPO > Pixel4 > Demo
。达到 iPhone 照片丝般顺滑的体验很有挑战,但我们至少可以实现媲美 OPPO 相册上的体验。然而,这篇博客介绍的方法已不再适用。
思考
分解
当我们遇到一个大问题的挑战时,可能当下并不具备解决它的能力,不妨试试分解它。我们把一个大问题拆分成若干个小问题,逐个击破,最终组合解决方案。就像通关游戏一样,我们需要先通过一个一个小的关卡,才能挑战最终的 boss。这种方式往往行之有效,并且在分解之后我们掌握了一个一个小的技能。
如果我们想了解发动机的工作原理,我们需要把发动机拆开,拆开需要那些工具?我们需要扳手、螺丝刀、需要起重设备和滑轮,然后我们把它拆开,不仅了解发动机的工作原理,在这个过程中,还会学会扳手、螺丝刀等工具的使用。
这篇博客也一样,为了解决 ZoomImageView 的问题,我们要弄清楚 Scroller 滑动特性、事件分发和 GestureDetector 、对 Matrix 运算也了解一二,学会了 geogebra 工具的使用。我们不只是得到了一个 ZoomImageView 控件,更有意义的是
积木变成了粘土
。现在这些粘土可以按照我们的意愿重新组合成新的积木了,我们可以拿来一个手势放大的视频控件,或是在鸿蒙、Flutter、QT 等上面实现一个 ”ZoomImageView”,在遇到头像裁剪、图片编辑等技术问题时,这些粘土一样可以派上用场。这些便是对分解的另一种解释。
技术的时效性
多年之前使用 PhotoView 时就曾想对其进行重构,这个念头终于有一天奇怪地又带点遗憾地实现了,不免让人唏嘘。某项技术随着时间的推移变得不再引入注目,放到时代背景下,连同它所依附的行业都变得无足轻重,开发者所具备的经验和技能其实作为知识资产,和金融资产何其相似。也许我们需要像金融投资者那样管理自己的技能组合。除了年终时盘算自己一年下来可怜的收入,是否也可以盘算一下,哪些技能和经验,未来潜在的收益率更大。
彩蛋
2007年乔布斯在初代 iPhone 发布会上演示图片手势放大的片段