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

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

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

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

圖4 圖片雙指縮放
- private void scale(Point scaleCenter, float scaleBase, float distance, Point lineCenter) {
- if (!isReady()) {
- return;
- }
- //計算圖片從fit center狀態(tài)到目標狀態(tài)的縮放比例
- 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]);
- //應(yīng)用變換
- mOuterMatrix.setMatrix(matrix);
- MathUtils.matrixGiven(matrix);
- dispatchOuterMatrixChanged();
- //重繪
- invalidate();
- }
(2)單指雙擊
單指雙擊表示用單根手指雙擊屏幕的操作,該操作可實現(xiàn)圖片放大縮小的效果,單指雙擊完成圖片縮放的功能是由doubleTap()方法實現(xiàn)的。
在doubleTap()方法體中我們初始化了一個縮放動畫的對象mScaleAnimator(),它有兩個參數(shù)分別為mOuterMatrix(開始矩陣)和animEnd(結(jié)束矩陣)。開始矩陣表示圖片原來的位置與大小;結(jié)束矩陣表示圖片縮放后的位置與大小,是根據(jù)放大比例和雙擊點位置確定的。確定圖片的開始和結(jié)束矩陣后,啟動縮放動畫,便可以實現(xiàn)縮放效果,如圖5所示。

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

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