Android實(shí)現(xiàn)對(duì)圖片放大、平移和旋轉(zhuǎn)的功能
先來(lái)看看要實(shí)現(xiàn)的效果圖

在講解中,需要大家提前了解一些關(guān)于圖片繪制的原理的相關(guān)知識(shí)。
關(guān)于實(shí)現(xiàn)的流程
1、自定義View
2、獲得操作圖片的Bitmap
3、復(fù)寫(xiě)View的onTouchEvent()方法中的ACTION_DOWN,ACTION_POINTER_DOWN,ACTION_MOVE,ACTION_POINTER_UP以及ACTION_UP事件。
4、定義相應(yīng)圖片變化的Matrix矩陣,通過(guò)手勢(shì)操作的變化來(lái)設(shè)置相應(yīng)的Matrix。
5、完成最終的Matrix設(shè)置時(shí),通過(guò)invalidate()方法重新繪制頁(yè)面。
那么接下來(lái)我們根據(jù)以上流程一步一步實(shí)現(xiàn)代碼。
代碼演示
/**
* 作者:ZhouYou
* 日期:2016/8/23.
*/
public class TouchImageView extends View {
// 繪制圖片的邊框
private Paint paintEdge;
// 繪制圖片的矩陣
private Matrix matrix = new Matrix();
// 手指按下時(shí)圖片的矩陣
private Matrix downMatrix = new Matrix();
// 手指移動(dòng)時(shí)圖片的矩陣
private Matrix moveMatrix = new Matrix();
// 資源圖片的位圖
private Bitmap srcImage;
// 多點(diǎn)觸屏?xí)r的中心點(diǎn)
private PointF midPoint = new PointF();
// 觸控模式
private int mode;
private static final int NONE = 0; // 無(wú)模式
private static final int TRANS = 1; // 拖拽模式
private static final int ZOOM = 2; // 縮放模式
// 是否超過(guò)邊界
private boolean withinBorder;
public TouchImageView(Context context) {
this(context, null);
}
public TouchImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paintEdge = new Paint();
paintEdge.setColor(Color.BLACK);
paintEdge.setAlpha(170);
paintEdge.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
srcImage = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_avatar_1);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] points = getBitmapPoints(srcImage, matrix);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
// 畫(huà)邊框
canvas.drawLine(x1, y1, x2, y2, paintEdge);
canvas.drawLine(x2, y2, x4, y4, paintEdge);
canvas.drawLine(x4, y4, x3, y3, paintEdge);
canvas.drawLine(x3, y3, x1, y1, paintEdge);
// 畫(huà)圖片
canvas.drawBitmap(srcImage, matrix, null);
}
// 手指按下屏幕的X坐標(biāo)
private float downX;
// 手指按下屏幕的Y坐標(biāo)
private float downY;
// 手指之間的初始距離
private float oldDistance;
// 手指之間的初始角度
private float oldRotation;
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mode = TRANS;
downX = event.getX();
downY = event.getY();
downMatrix.set(matrix);
break;
case MotionEvent.ACTION_POINTER_DOWN: // 多點(diǎn)觸控
mode = ZOOM;
oldDistance = getSpaceDistance(event);
oldRotation = getSpaceRotation(event);
downMatrix.set(matrix);
midPoint = getMidPoint(event);
break;
case MotionEvent.ACTION_MOVE:
// 縮放
if (mode == ZOOM) {
moveMatrix.set(downMatrix);
float deltaRotation = getSpaceRotation(event) - oldRotation;
float scale = getSpaceDistance(event) / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);
moveMatrix.postRotate(deltaRotation, midPoint.x, midPoint.y);
withinBorder = getMatrixBorderCheck(srcImage, event.getX(), event.getY());
if (withinBorder) {
matrix.set(moveMatrix);
invalidate();
}
}
// 平移
else if (mode == TRANS) {
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - downX, event.getY() - downY);
withinBorder = getMatrixBorderCheck(srcImage, event.getX(), event.getY());
if (withinBorder) {
matrix.set(moveMatrix);
invalidate();
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
mode = NONE;
break;
default:
break;
}
return true;
}
/**
* 獲取手指的旋轉(zhuǎn)角度
*
* @param event
* @return
*/
private float getSpaceRotation(MotionEvent event) {
double deltaX = event.getX(0) - event.getX(1);
double deltaY = event.getY(0) - event.getY(1);
double radians = Math.atan2(deltaY, deltaX);
return (float) Math.toDegrees(radians);
}
/**
* 獲取手指間的距離
*
* @param event
* @return
*/
private float getSpaceDistance(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
/**
* 獲取手勢(shì)中心點(diǎn)
*
* @param event
*/
private PointF getMidPoint(MotionEvent event) {
PointF point = new PointF();
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
return point;
}
/**
* 將matrix的點(diǎn)映射成坐標(biāo)點(diǎn)
*
* @return
*/
protected float[] getBitmapPoints(Bitmap bitmap, Matrix matrix) {
float[] dst = new float[8];
float[] src = new float[]{
0, 0,
bitmap.getWidth(), 0,
0, bitmap.getHeight(),
bitmap.getWidth(), bitmap.getHeight()
};
matrix.mapPoints(dst, src);
return dst;
}
/**
* 檢查邊界
*
* @param x
* @param y
* @return true - 在邊界內(nèi) | false - 超出邊界
*/
private boolean getMatrixBorderCheck(Bitmap bitmap, float x, float y) {
if (bitmap == null) return false;
float[] points = getBitmapPoints(bitmap, moveMatrix);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
+ Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
+ Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
+ Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
return true;
}
return false;
}
}
我已經(jīng)在代碼中針對(duì)可能遇到的問(wèn)題做了詳細(xì)的注釋。
1. Matrix
// 繪制圖片的矩陣 private Matrix matrix = new Matrix(); // 手指按下時(shí)圖片的矩陣 private Matrix downMatrix = new Matrix(); // 手指移動(dòng)時(shí)圖片的矩陣 private Matrix moveMatrix = new Matrix();
首先我定義了三個(gè)Matrix變量,目的在于通過(guò)不同手勢(shì)的操控圖片的Matrix最終由繪制圖片的Matrix所接收,因此需要在不同的操作中使用不同的Matrix進(jìn)行圖形變換的數(shù)據(jù)傳遞,從而在渲染頁(yè)面的時(shí)候?qū)⒆罱K的Matrix再傳遞回繪圖的Matrix。
2. PointF
// 多點(diǎn)觸屏?xí)r的中心點(diǎn) private PointF midPoint = new PointF();
因?yàn)槿绻轻槍?duì)圖片的旋轉(zhuǎn)和放大操作,需要通過(guò)兩個(gè)手指進(jìn)行控制,因此我們需要知道在多個(gè)手指觸摸屏幕時(shí)的中心點(diǎn)坐標(biāo)。
3. 觸控模式
// 觸控模式 private int mode; private static final int NONE = 0; // 無(wú)模式 private static final int TRANS = 1; // 拖拽模式 private static final int ZOOM = 2; // 縮放模式
在onTouchEvent()事件中,會(huì)根據(jù)不同的事件變換觸控的模式,從而進(jìn)行不同圖片變換的操作。
4. onTouchEvent()
首先,我們是自定義的View,因此如果要對(duì)該事件進(jìn)行消費(fèi)的話(huà),需要將返回值設(shè)置為true。
(1)ACTION_DOWN - 該事件是單點(diǎn)觸屏的事件,也就是說(shuō)如果一個(gè)手指按下屏幕的時(shí)候就會(huì)回調(diào)這個(gè)事件。那么我們?cè)谠撌录芯蛯⒂|控模式設(shè)置為拖拽模式(TRANS),記錄下按下屏幕的xy坐標(biāo),并在這個(gè)事件中將繪圖的Matrix復(fù)制給按下屏幕的Matrix。
case MotionEvent.ACTION_DOWN: mode = TRANS; downX = event.getX(); downY = event.getY(); downMatrix.set(matrix); break;
(2)ACTION_POINTER_DOWN - 這個(gè)事件發(fā)生在超過(guò)一個(gè)手指觸摸屏幕的時(shí)候。我們?cè)谶@個(gè)事件中即可針對(duì)多點(diǎn)觸屏的操作進(jìn)行初始化設(shè)置。在該事件中,我們將觸控模式重新設(shè)置為(ZOOM),初始化兩指之間觸摸屏幕的距離以及兩指之間的旋轉(zhuǎn)角度,初始化兩指之間的中心點(diǎn)坐標(biāo)。最后把繪圖的Matrix復(fù)制給按下屏幕的Matrix。
case MotionEvent.ACTION_POINTER_DOWN: // 多點(diǎn)觸控
mode = ZOOM;
oldDistance = getSpaceDistance(event);
oldRotation = getSpaceRotation(event);
midPoint = getMidPoint(event);
downMatrix.set(matrix);
break;
(3)ACTION_MOVE - 到了移動(dòng)的事件中,根據(jù)之前的觸控模式進(jìn)行判斷。首先,將按下事件的Matrix復(fù)制給移動(dòng)事件的Matrix。如果是(ZOOM)模式,我們將會(huì)根據(jù)事件獲得手指旋轉(zhuǎn)角度的差值,以及手指之間距離的差值。根據(jù)這兩個(gè)差值,以及在ACTION_DOWN事件中獲得的中點(diǎn)坐標(biāo),我們即可設(shè)置MOVE事件的縮放和旋轉(zhuǎn)。(TRANS)模式也是如此。最后通過(guò)獲取圖片變換的邊界值來(lái)判斷是否進(jìn)行繪圖渲染。
case MotionEvent.ACTION_MOVE:
// 縮放
if (mode == ZOOM) {
moveMatrix.set(downMatrix);
float deltaRotation = getSpaceRotation(event) - oldRotation;
float scale = getSpaceDistance(event) / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);
moveMatrix.postRotate(deltaRotation, midPoint.x, midPoint.y);
withinBorder = getMatrixBorderCheck(srcImage, event.getX(), event.getY());
if (withinBorder) {
matrix.set(moveMatrix);
invalidate();
}
}
// 平移
else if (mode == TRANS) {
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - downX, event.getY() - downY);
withinBorder = getMatrixBorderCheck(srcImage, event.getX(), event.getY());
if (withinBorder) {
matrix.set(moveMatrix);
invalidate();
}
}
break;
(4)ACTION_POINTER_UP和ACTION_UP - 在這兩個(gè)事件中,重新將觸屏的模式設(shè)置會(huì)NONE。
5. 邊界判斷
以下即為邊界判斷的邏輯是針對(duì)正方形的圖片來(lái)說(shuō)的。首先通過(guò)原圖片相對(duì)自己四個(gè)坐標(biāo)映射成為Matrix對(duì)應(yīng)屏幕的點(diǎn)坐標(biāo)。通過(guò)得到4個(gè)點(diǎn)的坐標(biāo),我們即可根據(jù)手指觸摸圖片時(shí)的坐標(biāo)與圖片的4個(gè)點(diǎn)坐標(biāo)進(jìn)行關(guān)聯(lián)。
邊界判斷的邏輯是手指觸摸圖片的點(diǎn)到4個(gè)頂點(diǎn)的距離之和如果小于(2+根號(hào)2倍)的斜邊長(zhǎng)度,即視為不超過(guò)邊界。
/**
* 將matrix的點(diǎn)映射成坐標(biāo)點(diǎn)
*
* @return
*/
protected float[] getBitmapPoints(Bitmap bitmap, Matrix matrix) {
float[] dst = new float[8];
float[] src = new float[]{
0, 0,
bitmap.getWidth(), 0,
0, bitmap.getHeight(),
bitmap.getWidth(), bitmap.getHeight()
};
matrix.mapPoints(dst, src);
return dst;
}
/**
* 檢查邊界
*
* @param x
* @param y
* @return true - 在邊界內(nèi) | false - 超出邊界
*/
private boolean getMatrixBorderCheck(Bitmap bitmap, float x, float y) {
if (bitmap == null) return false;
float[] points = getBitmapPoints(bitmap, moveMatrix);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
+ Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
+ Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
+ Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
return true;
}
return false;
}
總結(jié)
好了,本文的內(nèi)容到這就結(jié)束了,完成了以上的步驟,即可完成針對(duì)圖片在屏幕上的放大、平移和旋轉(zhuǎn)的操作。是不是還是很簡(jiǎn)單的。有興趣的可以自己動(dòng)手操作起來(lái),希望這篇文章對(duì)大家的學(xué)習(xí)和工作能有所幫助,如果有疑問(wèn)可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Kotlin Flow常見(jiàn)場(chǎng)景下的使用實(shí)例
這篇文章主要為大家介紹了Kotlin Flow常見(jiàn)場(chǎng)景下的使用實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Android開(kāi)發(fā)中GridView用法示例
這篇文章主要介紹了Android開(kāi)發(fā)中GridView用法,簡(jiǎn)單說(shuō)明了GridView控件的功能并結(jié)合實(shí)例形式給出了GridView組合圖片顯示的具體功能實(shí)現(xiàn)方法與布局操作技巧,需要的朋友可以參考下2017-10-10
Kotlin創(chuàng)建一個(gè)好用的協(xié)程作用域
這篇文章主要介紹了Kotlin創(chuàng)建一個(gè)好用的協(xié)程作用域,kotlin中使用協(xié)程,是一定要跟協(xié)程作用域一起配合使用的,否則可能協(xié)程的生命周期無(wú)法被準(zhǔn)確控制,造成內(nèi)存泄漏或其他問(wèn)題2022-07-07
Android APP與媒體存儲(chǔ)服務(wù)的交互
本文介紹如何在 Android 中,開(kāi)發(fā)者的 APP 如何使用媒體存儲(chǔ)服務(wù)(包含MediaScanner、MediaProvider以及媒體信息解析等部分),包括如何把 APP 新增或修改的文件更新到媒體數(shù)據(jù)庫(kù)、如何在多媒體應(yīng)用中隱藏 APP 產(chǎn)生的文件、如何監(jiān)聽(tīng)媒體數(shù)據(jù)庫(kù)的變化等等。2013-10-10
Android dip,px,pt,sp 的區(qū)別詳解
本篇文章是對(duì)Android中dip,px,pt,sp的區(qū)別進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
Android實(shí)現(xiàn)歌曲播放時(shí)歌詞同步顯示具體思路
歌曲播放時(shí)歌詞同步顯示,我們需要讀取以上歌詞文件的每一行轉(zhuǎn)換成成一個(gè)個(gè)歌詞實(shí)體,可根據(jù)當(dāng)前播放器的播放進(jìn)度與每句歌詞的開(kāi)始時(shí)間,得到當(dāng)前屏幕中央高亮顯示的那句歌詞2013-06-06
kotlin協(xié)程之coroutineScope函數(shù)使用詳解
這篇文章主要為大家介紹了kotlin協(xié)程之coroutineScope函數(shù)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09

