前言
基于安卓平台的手势操控组件PinchImageView-ohos(https://github.com/boycy815/PinchImageView), 实现鸿蒙的功能化迁移和重构。代码已经开源到(https://gitee.com/isrc_ohos/pinch-image-view-ohos),欢迎各位开发者提出宝贵意见。
背景
PinchImageView-ohos是一个支持多点触控的ImageView手势操控组件,通过识别单指双击、双指捏合、单指滑动等手势指令,实现图片的放大、缩小、滑动等效果。该组件功能丰富且使用简单,被广泛应用于各类图片预览类应用。
组件效果展示
1、双指相向或相对捏合,实现图片的缩放变化。
图1.双指捏合效果
2、单指双击实现图片的放大缩小。
图2 双击效果
3、单指双击后单指移动,实现图片的放大后平移。
图3 单指双击后单指移动效果
Sample解析
Sample部分主要负责整体显示布局的搭建。首先为PinchImageView-ohos组件设置显示图片,然后将组件对象添加到显示布局中。下面将详细介绍组件的使用方法。
步骤1. 创建整体的显示布局。
步骤2. 导入相关类并实例化PinchImageView-ohos组件对象。
步骤3. 设置显示图片。
步骤4. 将PinchImageView-ohos组件对象添加到整体显示布局中。
- //步骤1 创建整体的显示布局
- DirectionalLayout directionalLayout = new DirectionalLayout(this);
- //步骤2 导入相关类并实例化对象
- PinchImageViewnew pinchImageView = new PinchImageViewnew(this);
- //步骤3 设置显示图片
- pinchImageView.setPixelMap(this, ResourceTable.Media_1111);
- //步骤4 将pinchImageView添加到整体显示布局中
- directionalLayout.addComponent(pinchImageView);
- setUIContent(directionalLayout);
Library解析
Library主要为PinchImageView-ohos组件实现手势获取功能和图片操控功能。
开发者通过设置监听器来捕捉各类手势,根据不同的手势执行不同的图片操控方法,从而显示不同的图片操控效果,如放大、缩小、移动。
1、手势获取方法
手势获取对实现PinchImageView-ohos组件的功能尤为重要,此处主要通过onTouchEvent()方法来捕捉对应的手势。主要用到的手势包含PRIMARY_POINT_UP(最后一根手指从屏幕上抬起)、PRIMARY_POINT_DOWN(第一根手指触摸屏幕)、OTHER_POINT_DOWN(当一根或多根手指已经触摸屏幕时,另一个手指触摸屏幕 )、OTHER_POINT_UP(一些手指从屏幕上抬起,而一些手指仍留在屏幕上 )、POINT_MOVE(手指在屏幕上移动)。通过监控各类手势的操作顺序和触碰时间等条件,达到识别捏合、滑动、单击、双击等复杂手势的效果。
onTouchEvent()函数首先通过TouchEvent.getAction()方法获取当前的手势,当手势为:
(1)PRIMARY_POINT_UP
需要判断图片之前是否处于缩放模式(此时图片处于缩放状态)。如果是缩放模式,则触发结束缩放动画,后将手势状态置于自由模式。
- //最后一个点抬起或者取消,结束所有模式
- if (action == TouchEvent.PRIMARY_POINT_UP || action == TouchEvent.CANCEL) {
- //如果之前是缩放模式,还需要结束缩放动画
- if (mPinchMode == PINCH_MODE_SCALE) {
- scaleEnd();//缩放结束
- }
- //手势状态置于自由模式
- mPinchMode = PINCH_MODE_FREE;
- }
(2)PRIMARY_POINT_DOWN
需要判断图片是否在缩放动画中,若不在,图片将切换到滚动模式(此时图片处于可自由移动状态),并保存触发点的位置,用于(5)中的计算。
- else if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- //在缩放动画过程中不允许启动滚动模式
- if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
- //在动画过程中不允许启动滚动模式,停止所有动画
- cancelAllAnimator();
- //切换到滚动模式
- mPinchMode = PINCH_MODE_SCROLL;
- //保存触发点的位置用于(5)中的计算
- mLastMovePoint.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
- }
- }
(3)OTHER_POINT_DOWN
需要将图片模式切换到缩放模式,并保存两个触发点的位置,用于(5)中的计算。
- else if (action == TouchEvent.OTHER_POINT_DOWN) {
- //在动画过程中不允许启动缩放模式,停止所有动画
- cancelAllAnimator();
- //切换到缩放模式
- mPinchMode = PINCH_MODE_SCALE;
- //保存缩放的两个触发点的位置,用于(5)中的计算
- saveScaleContext(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
- }
(4)OTHER_POINT_UP
需要判断手指抬起后图片是否处于缩放模式。
如果处于缩放模式下,判断识别到的手指是否超过两个。
在剩余手指超过两个(缩放模式未结束)的情况下,第一个触摸的手指抬起,
那么让第二个触摸的手指和第三个触摸的手指所在的点作为缩放控制点。
在剩余手指超过两个(缩放模式未结束)的情况下,第二个触摸的手指抬起,
那么让第一个触摸的手指和第三个触摸的手指所在的点作为缩放控制点。
如果处于缩放模式下,判断识别到的手指只有一个。此时不能允许它切换到滚动模式,因为图片可能没有在初始的位置上。
手指抬起后图片未处于缩放模式时(屏幕上仅剩余一个手指),开启滚动模式,并记录开始滚动的点。
- else if (action == TouchEvent.OTHER_POINT_UP) {
- //多个手指情况下抬起一个手指,此时需要是缩放模式才触发
- if (mPinchMode == PINCH_MODE_SCALE) {
- //抬起的点如果大于2,那么缩放模式还有效,但是有可能初始点变了,重新测量初始点
- if (event.getPointerCount() > 2) {
- //如果还没结束缩放模式,但是第一个点抬起了,那么让第二个点和第三个点作为缩放控制点
- if (event.getAction() >> 8 == 0) {
- event.getPointerPosition(1).getX();
- saveScaleContext(event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY(), event.getPointerPosition(2).getX(), event.getPointerPosition(2).getY());
- //如果还没结束缩放模式,但是第二个点抬起了,那么让第一个点和第三个点作为缩放控制点
- } else if (event.getAction() >> 8 == 1) {
- saveScaleContext(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(2).getX(), event.getPointerPosition(2).getY());
- }
- }
- //如果抬起的点等于2,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
- }
- }
(5)POINT_MOVE
需要判断当前图片的模式。当为滚动模式时,执行scrollBy()方法来实现图片的移动效果;当它为缩放模式时,计算两个缩放点的距离和缩放点的中心,并执行scale()方法实现图片的缩放效果。scrollBy()方法和scale()方法的具体逻辑在图片操控方法中有详细介绍,此处就不做过多赘述。
- else if (action == TouchEvent.POINT_MOVE) {
- if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
- //在滚动模式下移动
- if (mPinchMode == PINCH_MODE_SCROLL) {
- //每次移动产生一个差值累积到图片位置上
- scrollBy(event.getPointerPosition(0).getX() - mLastMovePoint.position[0], event.getPointerPosition(0).getY() - mLastMovePoint.position[1]);
- //记录新的移动点
- mLastMovePoint.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
- //在缩放模式下移动
- } else if (mPinchMode == PINCH_MODE_SCALE && event.getPointerCount() > 1) {
- //两个缩放点间的距离
- float distance = MathUtils.getDistance(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
- //保存缩放点中心
- float[] lineCenter = MathUtils.getCenterPoint(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
- mLastMovePoint.modify(lineCenter[0], lineCenter[1]);
- //处理缩放
- scale(mScaleCenter, mScaleBase, distance, mLastMovePoint);
- }
- }
- }
2、图片操控方法
图片缩放
(1)双指捏合
双指捏合,顾名思义是表示两根手指向相反方向移动的操作,该操作可实现图片放大缩小的效果。双指捏合完成图片缩放的功能是由scale()方法实现的。
在scale()方法体中需要设置各种缩放参数:scaleBase是缩放系数、scaleCenter代表图片缩放中点、distance指两指间距离、lineCenter是两指中点。scaleBase和distance相乘会得到缩放比例,图片依旧缩放比例进行变化。在缩放过程中,图片缩放中点scaleCenter会跟随两指中点lineCenter移动,实现以两指中点为中心对图片进行放大缩小的效果,缩放效果如图4所示。
图4 图片双指缩放
- private void scale(Point scaleCenter, float scaleBase, float distance, Point lineCenter) {
- if (!isReady()) {
- return;
- }
- //计算图片从fit center状态到目标状态的缩放比例
- float scale = scaleBase * distance;
- Matrix matrix = MathUtils.matrixTake();
- //按照图片缩放中心缩放,并且让缩放中心在缩放点中点上
- matrix.postScale(scale, scale, scaleCenter.position[0], scaleCenter.position[1]);
- //让图片的缩放中点跟随手指缩放中点
- matrix.postTranslate(lineCenter.position[0] - scaleCenter.position[0], lineCenter.position[1] - scaleCenter.position[1]);
- //应用变换
- mOuterMatrix.setMatrix(matrix);
- MathUtils.matrixGiven(matrix);
- dispatchOuterMatrixChanged();
- //重绘
- invalidate();
- }
(2)单指双击
单指双击表示用单根手指双击屏幕的操作,该操作可实现图片放大缩小的效果,单指双击完成图片缩放的功能是由doubleTap()方法实现的。
在doubleTap()方法体中我们初始化了一个缩放动画的对象mScaleAnimator(),它有两个参数分别为mOuterMatrix(开始矩阵)和animEnd(结束矩阵)。开始矩阵表示图片原来的位置与大小;结束矩阵表示图片缩放后的位置与大小,是根据放大比例和双击点位置确定的。确定图片的开始和结束矩阵后,启动缩放动画,便可以实现缩放效果,如图5所示。
图5 单点触摸双击缩放
- private void doubleTap(float x, float y) {
- ...
- //开始计算缩放动画的结果矩阵
- Matrix animEnd = MathUtils.matrixTake(mOuterMatrix);
- //计算还需缩放的倍数
- animEnd.postScale(nextScale / currentScale, nextScale / currentScale, x, y);
- //将放大点移动到控件中心
- animEnd.postTranslate(displayWidth / 2f - x, displayHeight / 2f - y);
- RectFloat testBound = MathUtils.rectFTake(0,0,mp.getImageInfo().size.width,mp.getImageInfo().size.height);
- ...
- //清理当前可能正在执行的动画
- cancelAllAnimator();
- //启动矩阵动画
- mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
- mScaleAnimator.start();
- ...
- }
图片在缩放状态下移动
单指滑动表示手指在屏幕上完成矢量平移,是图片移动的唯一方式。该功能是通过scrollBy()方法实现的。
以实现图片左右移动为例,在scrollBy()方法中,需要判断缩放状态下图片位移的最大距离,有以下几种不同的情况:
- 图片移动后,左侧边缘超出控件的左侧边缘,图片无法移动;
- 图片移动后,右侧边缘超出控件的右侧边缘,图片无法移动;
- 图片移动后,两侧都未超出控件边缘的情况下,将以手指触碰点作为控制点,对图片进行水平移动。
图片上下平移的情况与左右平移类似,这里不做赘述,图片移动效果如图6所示。
图6 图片移动的最大距离
- public boolean scrollBy(float xDiff, float yDiff) {
- ...
- if (bound.right - bound.left < displayWidth) {
- xDiff = 0;
- //如果图片左边在移动后超出控件左边
- } else if (bound.left + xDiff > 0) {
- //如果在移动之前是没超出的,计算应该移动的距离
- if (bound.left < 0) {
- xDiff = -bound.left;
- //否则无法移动
- } else {
- xDiff = 0;
- }
- //如果图片右边在移动后超出控件右边
- } else if (bound.right + xDiff < displayWidth) {
- //如果在移动之前是没超出的,计算应该移动的距离
- if (bound.right > displayWidth) {
- xDiff = displayWidth - bound.right;
- //否则无法移动
- } else {
- xDiff = 0;
- }
- }
- ...
- }