鴻蒙開源第三方組件—自定義圖片縮放組件PinchImageView-ohos
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺(tái)的手勢(shì)操控組件PinchImageView-ohos(https://github.com/boycy815/PinchImageView), 實(shí)現(xiàn)鴻蒙的功能化遷移和重構(gòu)。代碼已經(jīng)開源到(https://gitee.com/isrc_ohos/pinch-image-view-ohos),歡迎各位開發(fā)者提出寶貴意見。
背景
PinchImageView-ohos是一個(gè)支持多點(diǎn)觸控的ImageView手勢(shì)操控組件,通過識(shí)別單指雙擊、雙指捏合、單指滑動(dòng)等手勢(shì)指令,實(shí)現(xiàn)圖片的放大、縮小、滑動(dòng)等效果。該組件功能豐富且使用簡(jiǎn)單,被廣泛應(yīng)用于各類圖片預(yù)覽類應(yīng)用。
組件效果展示
1、雙指相向或相對(duì)捏合,實(shí)現(xiàn)圖片的縮放變化。

圖1.雙指捏合效果
2、單指雙擊實(shí)現(xiàn)圖片的放大縮小。

圖2 雙擊效果
3、單指雙擊后單指移動(dòng),實(shí)現(xiàn)圖片的放大后平移。

圖3 單指雙擊后單指移動(dòng)效果
Sample解析
Sample部分主要負(fù)責(zé)整體顯示布局的搭建。首先為PinchImageView-ohos組件設(shè)置顯示圖片,然后將組件對(duì)象添加到顯示布局中。下面將詳細(xì)介紹組件的使用方法。
步驟1. 創(chuàng)建整體的顯示布局。
步驟2. 導(dǎo)入相關(guān)類并實(shí)例化PinchImageView-ohos組件對(duì)象。
步驟3. 設(shè)置顯示圖片。
步驟4. 將PinchImageView-ohos組件對(duì)象添加到整體顯示布局中。
- //步驟1 創(chuàng)建整體的顯示布局
- DirectionalLayout directionalLayout = new DirectionalLayout(this);
- //步驟2 導(dǎo)入相關(guān)類并實(shí)例化對(duì)象
- PinchImageViewnew pinchImageView = new PinchImageViewnew(this);
- //步驟3 設(shè)置顯示圖片
- pinchImageView.setPixelMap(this, ResourceTable.Media_1111);
- //步驟4 將pinchImageView添加到整體顯示布局中
- directionalLayout.addComponent(pinchImageView);
- setUIContent(directionalLayout);
Library解析
Library主要為PinchImageView-ohos組件實(shí)現(xiàn)手勢(shì)獲取功能和圖片操控功能。
開發(fā)者通過設(shè)置監(jiān)聽器來捕捉各類手勢(shì),根據(jù)不同的手勢(shì)執(zhí)行不同的圖片操控方法,從而顯示不同的圖片操控效果,如放大、縮小、移動(dòng)。
1、手勢(shì)獲取方法
手勢(shì)獲取對(duì)實(shí)現(xiàn)PinchImageView-ohos組件的功能尤為重要,此處主要通過onTouchEvent()方法來捕捉對(duì)應(yīng)的手勢(shì)。主要用到的手勢(shì)包含PRIMARY_POINT_UP(最后一根手指從屏幕上抬起)、PRIMARY_POINT_DOWN(第一根手指觸摸屏幕)、OTHER_POINT_DOWN(當(dāng)一根或多根手指已經(jīng)觸摸屏幕時(shí),另一個(gè)手指觸摸屏幕 )、OTHER_POINT_UP(一些手指從屏幕上抬起,而一些手指仍留在屏幕上 )、POINT_MOVE(手指在屏幕上移動(dòng))。通過監(jiān)控各類手勢(shì)的操作順序和觸碰時(shí)間等條件,達(dá)到識(shí)別捏合、滑動(dòng)、單擊、雙擊等復(fù)雜手勢(shì)的效果。
onTouchEvent()函數(shù)首先通過TouchEvent.getAction()方法獲取當(dāng)前的手勢(shì),當(dāng)手勢(shì)為:
(1)PRIMARY_POINT_UP
需要判斷圖片之前是否處于縮放模式(此時(shí)圖片處于縮放狀態(tài))。如果是縮放模式,則觸發(fā)結(jié)束縮放動(dòng)畫,后將手勢(shì)狀態(tài)置于自由模式。
- //最后一個(gè)點(diǎn)抬起或者取消,結(jié)束所有模式
- if (action == TouchEvent.PRIMARY_POINT_UP || action == TouchEvent.CANCEL) {
- //如果之前是縮放模式,還需要結(jié)束縮放動(dòng)畫
- if (mPinchMode == PINCH_MODE_SCALE) {
- scaleEnd();//縮放結(jié)束
- }
- //手勢(shì)狀態(tài)置于自由模式
- mPinchMode = PINCH_MODE_FREE;
- }
(2)PRIMARY_POINT_DOWN
需要判斷圖片是否在縮放動(dòng)畫中,若不在,圖片將切換到滾動(dòng)模式(此時(shí)圖片處于可自由移動(dòng)狀態(tài)),并保存觸發(fā)點(diǎn)的位置,用于(5)中的計(jì)算。
- else if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- //在縮放動(dòng)畫過程中不允許啟動(dòng)滾動(dòng)模式
- if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
- //在動(dòng)畫過程中不允許啟動(dòng)滾動(dòng)模式,停止所有動(dòng)畫
- cancelAllAnimator();
- //切換到滾動(dòng)模式
- mPinchMode = PINCH_MODE_SCROLL;
- //保存觸發(fā)點(diǎn)的位置用于(5)中的計(jì)算
- mLastMovePoint.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
- }
- }
(3)OTHER_POINT_DOWN
需要將圖片模式切換到縮放模式,并保存兩個(gè)觸發(fā)點(diǎn)的位置,用于(5)中的計(jì)算。
- else if (action == TouchEvent.OTHER_POINT_DOWN) {
- //在動(dòng)畫過程中不允許啟動(dòng)縮放模式,停止所有動(dòng)畫
- cancelAllAnimator();
- //切換到縮放模式
- mPinchMode = PINCH_MODE_SCALE;
- //保存縮放的兩個(gè)觸發(fā)點(diǎn)的位置,用于(5)中的計(jì)算
- saveScaleContext(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
- }
(4)OTHER_POINT_UP
需要判斷手指抬起后圖片是否處于縮放模式。
如果處于縮放模式下,判斷識(shí)別到的手指是否超過兩個(gè)。
在剩余手指超過兩個(gè)(縮放模式未結(jié)束)的情況下,第一個(gè)觸摸的手指抬起,
那么讓第二個(gè)觸摸的手指和第三個(gè)觸摸的手指所在的點(diǎn)作為縮放控制點(diǎn)。
在剩余手指超過兩個(gè)(縮放模式未結(jié)束)的情況下,第二個(gè)觸摸的手指抬起,
那么讓第一個(gè)觸摸的手指和第三個(gè)觸摸的手指所在的點(diǎn)作為縮放控制點(diǎn)。
如果處于縮放模式下,判斷識(shí)別到的手指只有一個(gè)。此時(shí)不能允許它切換到滾動(dòng)模式,因?yàn)閳D片可能沒有在初始的位置上。
手指抬起后圖片未處于縮放模式時(shí)(屏幕上僅剩余一個(gè)手指),開啟滾動(dòng)模式,并記錄開始滾動(dòng)的點(diǎn)。
- else if (action == TouchEvent.OTHER_POINT_UP) {
- //多個(gè)手指情況下抬起一個(gè)手指,此時(shí)需要是縮放模式才觸發(fā)
- if (mPinchMode == PINCH_MODE_SCALE) {
- //抬起的點(diǎn)如果大于2,那么縮放模式還有效,但是有可能初始點(diǎn)變了,重新測(cè)量初始點(diǎn)
- if (event.getPointerCount() > 2) {
- //如果還沒結(jié)束縮放模式,但是第一個(gè)點(diǎn)抬起了,那么讓第二個(gè)點(diǎn)和第三個(gè)點(diǎn)作為縮放控制點(diǎn)
- 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());
- //如果還沒結(jié)束縮放模式,但是第二個(gè)點(diǎn)抬起了,那么讓第一個(gè)點(diǎn)和第三個(gè)點(diǎn)作為縮放控制點(diǎn)
- } else if (event.getAction() >> 8 == 1) {
- saveScaleContext(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(2).getX(), event.getPointerPosition(2).getY());
- }
- }
- //如果抬起的點(diǎn)等于2,那么此時(shí)只剩下一個(gè)點(diǎn),也不允許進(jìn)入單指模式,因?yàn)榇藭r(shí)可能圖片沒有在正確的位置上
- }
- }
(5)POINT_MOVE
需要判斷當(dāng)前圖片的模式。當(dāng)為滾動(dòng)模式時(shí),執(zhí)行scrollBy()方法來實(shí)現(xiàn)圖片的移動(dòng)效果;當(dāng)它為縮放模式時(shí),計(jì)算兩個(gè)縮放點(diǎn)的距離和縮放點(diǎn)的中心,并執(zhí)行scale()方法實(shí)現(xiàn)圖片的縮放效果。scrollBy()方法和scale()方法的具體邏輯在圖片操控方法中有詳細(xì)介紹,此處就不做過多贅述。
- else if (action == TouchEvent.POINT_MOVE) {
- if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
- //在滾動(dòng)模式下移動(dòng)
- if (mPinchMode == PINCH_MODE_SCROLL) {
- //每次移動(dòng)產(chǎn)生一個(gè)差值累積到圖片位置上
- scrollBy(event.getPointerPosition(0).getX() - mLastMovePoint.position[0], event.getPointerPosition(0).getY() - mLastMovePoint.position[1]);
- //記錄新的移動(dòng)點(diǎn)
- mLastMovePoint.modify(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY());
- //在縮放模式下移動(dòng)
- } else if (mPinchMode == PINCH_MODE_SCALE && event.getPointerCount() > 1) {
- //兩個(gè)縮放點(diǎn)間的距離
- float distance = MathUtils.getDistance(event.getPointerPosition(0).getX(), event.getPointerPosition(0).getY(), event.getPointerPosition(1).getX(), event.getPointerPosition(1).getY());
- //保存縮放點(diǎn)中心
- 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)雙指捏合
雙指捏合,顧名思義是表示兩根手指向相反方向移動(dòng)的操作,該操作可實(shí)現(xiàn)圖片放大縮小的效果。雙指捏合完成圖片縮放的功能是由scale()方法實(shí)現(xiàn)的。
在scale()方法體中需要設(shè)置各種縮放參數(shù):scaleBase是縮放系數(shù)、scaleCenter代表圖片縮放中點(diǎn)、distance指兩指間距離、lineCenter是兩指中點(diǎn)。scaleBase和distance相乘會(huì)得到縮放比例,圖片依舊縮放比例進(jìn)行變化。在縮放過程中,圖片縮放中點(diǎn)scaleCenter會(huì)跟隨兩指中點(diǎn)lineCenter移動(dòng),實(shí)現(xiàn)以兩指中點(diǎn)為中心對(duì)圖片進(jìn)行放大縮小的效果,縮放效果如圖4所示。

圖4 圖片雙指縮放
- private void scale(Point scaleCenter, float scaleBase, float distance, Point lineCenter) {
- if (!isReady()) {
- return;
- }
- //計(jì)算圖片從fit center狀態(tài)到目標(biāo)狀態(tài)的縮放比例
- float scale = scaleBase * distance;
- Matrix matrix = MathUtils.matrixTake();
- //按照?qǐng)D片縮放中心縮放,并且讓縮放中心在縮放點(diǎn)中點(diǎn)上
- matrix.postScale(scale, scale, scaleCenter.position[0], scaleCenter.position[1]);
- //讓圖片的縮放中點(diǎn)跟隨手指縮放中點(diǎn)
- matrix.postTranslate(lineCenter.position[0] - scaleCenter.position[0], lineCenter.position[1] - scaleCenter.position[1]);
- //應(yīng)用變換
- mOuterMatrix.setMatrix(matrix);
- MathUtils.matrixGiven(matrix);
- dispatchOuterMatrixChanged();
- //重繪
- invalidate();
- }
(2)單指雙擊
單指雙擊表示用單根手指雙擊屏幕的操作,該操作可實(shí)現(xiàn)圖片放大縮小的效果,單指雙擊完成圖片縮放的功能是由doubleTap()方法實(shí)現(xiàn)的。
在doubleTap()方法體中我們初始化了一個(gè)縮放動(dòng)畫的對(duì)象mScaleAnimator(),它有兩個(gè)參數(shù)分別為mOuterMatrix(開始矩陣)和animEnd(結(jié)束矩陣)。開始矩陣表示圖片原來的位置與大小;結(jié)束矩陣表示圖片縮放后的位置與大小,是根據(jù)放大比例和雙擊點(diǎn)位置確定的。確定圖片的開始和結(jié)束矩陣后,啟動(dòng)縮放動(dòng)畫,便可以實(shí)現(xiàn)縮放效果,如圖5所示。

圖5 單點(diǎn)觸摸雙擊縮放
- private void doubleTap(float x, float y) {
- ...
- //開始計(jì)算縮放動(dòng)畫的結(jié)果矩陣
- Matrix animEnd = MathUtils.matrixTake(mOuterMatrix);
- //計(jì)算還需縮放的倍數(shù)
- animEnd.postScale(nextScale / currentScale, nextScale / currentScale, x, y);
- //將放大點(diǎn)移動(dòng)到控件中心
- animEnd.postTranslate(displayWidth / 2f - x, displayHeight / 2f - y);
- RectFloat testBound = MathUtils.rectFTake(0,0,mp.getImageInfo().size.width,mp.getImageInfo().size.height);
- ...
- //清理當(dāng)前可能正在執(zhí)行的動(dòng)畫
- cancelAllAnimator();
- //啟動(dòng)矩陣動(dòng)畫
- mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
- mScaleAnimator.start();
- ...
- }
圖片在縮放狀態(tài)下移動(dòng)
單指滑動(dòng)表示手指在屏幕上完成矢量平移,是圖片移動(dòng)的唯一方式。該功能是通過scrollBy()方法實(shí)現(xiàn)的。
以實(shí)現(xiàn)圖片左右移動(dòng)為例,在scrollBy()方法中,需要判斷縮放狀態(tài)下圖片位移的最大距離,有以下幾種不同的情況:
- 圖片移動(dòng)后,左側(cè)邊緣超出控件的左側(cè)邊緣,圖片無法移動(dòng);
- 圖片移動(dòng)后,右側(cè)邊緣超出控件的右側(cè)邊緣,圖片無法移動(dòng);
- 圖片移動(dòng)后,兩側(cè)都未超出控件邊緣的情況下,將以手指觸碰點(diǎn)作為控制點(diǎn),對(duì)圖片進(jìn)行水平移動(dòng)。
圖片上下平移的情況與左右平移類似,這里不做贅述,圖片移動(dòng)效果如圖6所示。

圖6 圖片移動(dòng)的最大距離
- public boolean scrollBy(float xDiff, float yDiff) {
- ...
- if (bound.right - bound.left < displayWidth) {
- xDiff = 0;
- //如果圖片左邊在移動(dòng)后超出控件左邊
- } else if (bound.left + xDiff > 0) {
- //如果在移動(dòng)之前是沒超出的,計(jì)算應(yīng)該移動(dòng)的距離
- if (bound.left < 0) {
- xDiff = -bound.left;
- //否則無法移動(dòng)
- } else {
- xDiff = 0;
- }
- //如果圖片右邊在移動(dòng)后超出控件右邊
- } else if (bound.right + xDiff < displayWidth) {
- //如果在移動(dòng)之前是沒超出的,計(jì)算應(yīng)該移動(dòng)的距離
- if (bound.right > displayWidth) {
- xDiff = displayWidth - bound.right;
- //否則無法移動(dòng)
- } else {
- xDiff = 0;
- }
- }
- ...
- }
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)