前言
在上次的文章:圖片操作系列 —(1)手勢(shì)縮放圖片功能中,我們已經(jīng)學(xué)會(huì)了如何用手勢(shì)來對(duì)圖片進(jìn)行縮放乙墙。這次我們繼續(xù)來看第二個(gè)操作宁舰,那就是如何用手勢(shì)來旋轉(zhuǎn)圖片。
所以我們本文我們一共要實(shí)現(xiàn)二個(gè)功能:
- 根據(jù)二個(gè)手指頭的旋轉(zhuǎn)來使圖片跟著旋轉(zhuǎn)
- 當(dāng)二個(gè)手指頭放開后额衙,圖片會(huì)自動(dòng)回歸到合適的位置。
我說明下第二個(gè)功能點(diǎn)的意思:什么叫回歸到合適的位置怕吴,比如如圖一窍侧,我們只轉(zhuǎn)動(dòng)了一點(diǎn)點(diǎn),沒有超過45度转绷,然后放在手指伟件,然后就會(huì)回到圖二的樣子。但是如果超過了45度议经,然后放開手指斧账,就回變成圖三的樣子。
前面基本的東西說明我都不說了煞肾。比如Matrix等知識(shí)咧织。大家可以直接參考圖片操作系列 —(1)手勢(shì)縮放圖片功能。
ps:我這邊可以再貼出相關(guān)基礎(chǔ)的鏈接:
android matrix 最全方法詳解與進(jìn)階(完整篇)
Android Matrix
根據(jù)二個(gè)手指頭的旋轉(zhuǎn)來使圖片跟著旋轉(zhuǎn):
我們知道使圖片進(jìn)行旋轉(zhuǎn)特定的角度很簡(jiǎn)單:
使用Matrix.postRotate(float degrees, float px, float py)
方法即可籍救。繞著(px,py)
點(diǎn)進(jìn)行旋轉(zhuǎn)degrees
角度习绢。
所以我們的問題就變成了如果獲取二個(gè)手指頭在做旋轉(zhuǎn)手勢(shì)的時(shí)候,相應(yīng)的角度的變化蝙昙,從而通過Matrix.postRotate方法來讓圖片也跟著變化闪萄。
1.獲取二個(gè)手指頭的手勢(shì)監(jiān)聽
在圖片操作系列 —(1)手勢(shì)縮放圖片功能文中我們知道,控制圖片的縮放是專門有個(gè)ScaleGestureDetector
;在OnTouch
事件中把相應(yīng)的事件傳遞給ScaleGestureDetector
奇颠。然后監(jiān)聽處理败去。我們也可以模仿著寫一個(gè)RotateGestureDetector
來進(jìn)行圖片旋轉(zhuǎn)的監(jiān)聽和處理。
public interface IRotateDetector {
/**
* handle rotation in onTouchEvent
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
boolean onTouchEvent(MotionEvent event);
/**
* is the Gesture Rotate
*
* @return true:rotating;false,otherwise
*/
boolean isRotating();
}
public class RotateGestureDetector implements IRotateDetector{
private int mLastAngle = 0;//最后一次的角度值
private IRotateListener mListener;//用來旋轉(zhuǎn)的回調(diào)Listener
private boolean mIsRotate;//是否處于旋轉(zhuǎn)
//用來設(shè)置回調(diào)Listener的方法
public void setRotateListener(IRotateListener listener) {
this.mListener = listener;
}
//用來接收觸摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
return doRotate(event);
}
//真正的計(jì)算手勢(shì)操作所得到的角度值的方法大刊,及回調(diào)調(diào)用为迈。
private boolean doRotate(MotionEvent ev) {
if (ev.getPointerCount() != 2) {
return false;
}
//Calculate the angle between the two fingers
int pivotX = (int) (ev.getX(0) + ev.getX(1)) / 2;
int pivotY = (int) (ev.getY(0) + ev.getY(1)) / 2;
float deltaX = ev.getX(0) - ev.getX(1);
float deltaY = ev.getY(0) - ev.getY(1);
double radians = Math.atan(deltaY / deltaX);
int degrees = (int) Math.round(Math.toDegrees(Math.atan2(deltaY,deltaX)));
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastAngle = degrees;
mIsRotate = false;
break;
case MotionEvent.ACTION_UP:
mIsRotate = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mLastAngle = degrees;
mIsRotate = false;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_POINTER_UP:
mIsRotate = false;
upRotate(pivotX, pivotY);
mLastAngle = degrees;
break;
case MotionEvent.ACTION_MOVE:
mIsRotate = true;
int degreesValue = degrees - mLastAngle;
if (degreesValue > 45) {
//Going CCW across the boundary
rotate(-5, pivotX, pivotY);
} else if (degreesValue < -45) {
//Going CW across the boundary
rotate(5, pivotX, pivotY);
} else {
//Normal rotation, rotate the difference
rotate(degreesValue, pivotX, pivotY);
}
//Save the current angle
mLastAngle = degrees;
break;
}
return true;
}
//回調(diào)的方法之一:控制圖片根據(jù)手勢(shì)的變化實(shí)時(shí)進(jìn)行旋轉(zhuǎn)
private void rotate(int degree, int pivotX, int pivotY) {
if (mListener != null) {
mListener.rotate(degree, pivotX, pivotY);
}
}
//回調(diào)的方法之一:最后某個(gè)手指放開后,控制圖片自動(dòng)回歸到合適的位置缺菌。
private void upRotate(int pivotX, int pivotY) {
if (mListener != null) {
mListener.upRotate(pivotX, pivotY);
}
}
}
2.獲取二個(gè)手指頭的角度變化
所以我們只需要來分析一下具體OnTouch
事件中的doRotate
方法即可:
//真正的計(jì)算手勢(shì)操作所得到的角度值的方法,及回調(diào)調(diào)用搜锰。
private boolean doRotate(MotionEvent ev) {
//如果觸摸的手指頭不是2個(gè)伴郁,直接返回。
if (ev.getPointerCount() != 2) {
return false;
}
//獲取二個(gè)手指頭的中心點(diǎn)的X與Y值蛋叼,等會(huì)選擇二個(gè)手指頭的中心點(diǎn)作為旋轉(zhuǎn)的中心
int pivotX = (int) (ev.getX(0) + ev.getX(1)) / 2;
int pivotY = (int) (ev.getY(0) + ev.getY(1)) / 2;
//獲取二個(gè)手指頭之間的X和Y的差值
float deltaX = ev.getX(0) - ev.getX(1);
float deltaY = ev.getY(0) - ev.getY(1);
//獲取角度
int degrees = (int) Math.round(Math.toDegrees(Math.atan2(deltaY,deltaX)));
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastAngle = degrees;
mIsRotate = false;
break;
case MotionEvent.ACTION_UP:
mIsRotate = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mLastAngle = degrees;
mIsRotate = false;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_POINTER_UP:
mIsRotate = false;
upRotate(pivotX, pivotY);
mLastAngle = degrees;
break;
case MotionEvent.ACTION_MOVE:
mIsRotate = true;
/*
每次把上一次的角度賦值給mLastAngle焊傅,然后獲取當(dāng)前新獲取的角度degrees,
二者相減獲取到二個(gè)手指頭在移動(dòng)的時(shí)候相應(yīng)的角度變化鸭栖。
*/
int degreesValue = degrees - mLastAngle;
/*
這里主要出現(xiàn)這么個(gè)情況握巢,二個(gè)手指頭如果相隔有一段距離晕鹊,那么在移動(dòng)的過程中,角度不會(huì)一下子變化很大.
但是比如我們這里故意二個(gè)手指頭是碰在一起的,然后二個(gè)手指頭稍微動(dòng)一下溅话,你就會(huì)發(fā)現(xiàn)角度變化會(huì)很大歌焦。
這樣圖片就會(huì)瞬間也旋轉(zhuǎn)了很大的角度飞几,讓人體驗(yàn)感覺很怪,所以我們這里瞬間順時(shí)針或者逆時(shí)針超過45度独撇,都只移動(dòng)5度值。
*/
if (degreesValue > 45) {
rotate(-5, pivotX, pivotY);
} else if (degreesValue < -45) {
rotate(5, pivotX, pivotY);
} else {
rotate(degreesValue, pivotX, pivotY);
}
//Save the current angle
mLastAngle = degrees;
break;
}
return true;
}
而doRotate方法
中最主要的就是根據(jù)二個(gè)手指頭觸摸獲取到的X,Y的差值绪钥,根據(jù)Math.atan2來獲取到角度关炼。我們具體來看下為什么這樣可以來獲取角度:
先附上一個(gè)基礎(chǔ)概念:Math.atan與Math.atan2
假設(shè)我們先點(diǎn)擊了(50,50),再點(diǎn)擊(10,10),這時(shí)候我們的deltaX = 40寸潦,deltaY = 40;也就是說
我們的弧度就是Math.atan2(40,40)
见转,而角度就是再用Math.toDegrees
對(duì)弧度進(jìn)行轉(zhuǎn)換即可。最終獲得額角度是45度乘客。
我們可以通過圖形來查看為什么Math.atan2(40,40)對(duì)應(yīng)的角度是45度淀歇。
如果我們的第二個(gè)手指頭從(10,10)移動(dòng)到了(50,10)浪默,也就是說最后變成了Math.atan2(40,0)
,根據(jù)圖形來看我們就知道是:
所以一共旋轉(zhuǎn)了45度缀匕,所以我們的圖片也跟著順時(shí)針旋轉(zhuǎn)45度即可乡小。
那假如我們的二個(gè)手指頭的放入順序反過來饵史,變成:
那這時(shí)候就變成了Math.atan2(-40,-40)
,我們根據(jù)圖形就知道了角度:
這時(shí)候還是跟剛才一樣的操作满钟,把(10,10)這個(gè)點(diǎn)移動(dòng)到了(50,10)零远,那這時(shí)候就是Math.atan2(-40,0)
;
所以最終得到的旋轉(zhuǎn)的角度是(-135)-(-90) = 45度厌蔽,所以最終也是順時(shí)針旋轉(zhuǎn)45度奴饮。所以我們不管是哪個(gè)手指頭先放下都不影響結(jié)果。
也許有人就會(huì)問了,你這邊按照二個(gè)手指的中點(diǎn)作為旋轉(zhuǎn)中心去旋轉(zhuǎn)逾条,豈不是會(huì)旋轉(zhuǎn)超出原來的圖片的邊界投剥。如果你還記得我們上一篇文章:圖片操作系列 —(1)手勢(shì)縮放圖片功能江锨,這篇文章最后的內(nèi)容講的就是當(dāng)圖片超過邊界,如果能隨著手勢(shì)慢慢回到邊界里面:
checkMatrixBounds()
酌心。
3.在Activity中設(shè)置Listener來進(jìn)行圖片的旋轉(zhuǎn)
然后我們只需要在相應(yīng)的Activity處對(duì)回調(diào)回來的(degreesValue, pivotX, pivotY)
三個(gè)值做相應(yīng)的旋轉(zhuǎn)即可挑豌。
rotateGestureDetector.setRotateListener(new IRotateListener() {
@Override
public void rotate(int degree, int pivotX, int pivotY) {
//圖片跟著手勢(shì)進(jìn)行旋轉(zhuǎn)
mSuppMatrix.postRotate(degree, pivotX, pivotY);
//Post the rotation to the image
checkAndDisplayMatrix();
}
@Override
public void upRotate(int pivotX, int pivotY) {
//當(dāng)手指頭松開的時(shí)候侯勉,讓圖片自動(dòng)更新到合適的位置债蓝。
float[] v = new float[9];
mSuppMatrix.getValues(v);
// calculate the degree of rotation
int angle = (int) Math.round(Math.toDegrees(Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])));
mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
photoView.post(mRightAngleRunnable);
}
});
手指頭松開手圖片自動(dòng)旋轉(zhuǎn)到合適位置:
我們知道饰迹,前面圖片跟著旋轉(zhuǎn),是獲取到了(int degree, int pivotX, int pivotY)
這三個(gè)值锹淌,然后讓mSuppMatrix.postRotate(degree, pivotX, pivotY)
;那我們就當(dāng)手指頭松開的時(shí)候赂摆,獲取到最終這個(gè)圖片比原來變化了多少角度即可钟些。然后根據(jù)這個(gè)當(dāng)前最終圖片的變化角度來進(jìn)行適當(dāng)?shù)男D(zhuǎn),讓其旋轉(zhuǎn)到合適位置汪拥。
我們來具體看怎么實(shí)現(xiàn)的:
@Override
public void upRotate(int pivotX, int pivotY) {
//當(dāng)手指頭松開的時(shí)候篙耗,讓圖片自動(dòng)更新到合適的位置宗弯。
float[] v = new float[9];
mSuppMatrix.getValues(v);
// calculate the degree of rotation
int angle = (int) Math.round(Math.toDegrees(Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])));
mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
photoView.post(mRightAngleRunnable);
}
Matrix,中文里叫矩陣辕棚,高等數(shù)學(xué)里有介紹邑狸,在圖像處理方面单雾,主要是用于平面的縮放屿储、平移、旋轉(zhuǎn)等操作茄菊。在Android里面竖哩,Matrix由9個(gè)float值構(gòu)成相叁,是一個(gè)3*3的矩陣。最好記住虑润。如下圖:
image
imageimage
我們發(fā)現(xiàn)mSuppMatrix.getValues(v)方法返回的9個(gè)float值中,第一個(gè)為cosX,第四個(gè)為sinX,所以我們就取下標(biāo)為0和3的值渗柿,也就是MSCALE_X和MSKEW_Y。我們用Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])來獲取弧度终惑。再用Math.toDegrees來獲取相應(yīng)的最終圖片的旋轉(zhuǎn)的度數(shù)。
public class Matrix {
public static final int MSCALE_X = 0; //!< use with getValues/setValues
public static final int MSKEW_X = 1; //!< use with getValues/setValues
public static final int MTRANS_X = 2; //!< use with getValues/setValues
public static final int MSKEW_Y = 3; //!< use with getValues/setValues
public static final int MSCALE_Y = 4; //!< use with getValues/setValues
public static final int MTRANS_Y = 5; //!< use with getValues/setValues
public static final int MPERSP_0 = 6; //!< use with getValues/setValues
public static final int MPERSP_1 = 7; //!< use with getValues/setValues
public static final int MPERSP_2 = 8; //!< use with getValues/setValues
......
......
......
}
然后我們?cè)侔勋@取到的角度和中心點(diǎn),通過一個(gè)Runnable來進(jìn)行圖片最后的矯正:
mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
photoView.post(mRightAngleRunnable);
我們知道最后是RightAngleRunnable來進(jìn)行圖片的矯正质帅,所以我們具體來分析下這個(gè)Runnable:
class RightAngleRunnable implements Runnable {
private static final int RECOVER_SPEED = 4;
private int mOldDegree;
private int mNeedToRotate;
private int mRoPivotX;
private int mRoPivotY;
RightAngleRunnable(int degree, int pivotX, int pivotY) {
Log.v("dyp4", "oldDegree:" + degree + "," + "calDegree:" + calDegree(degree));
this.mOldDegree = degree;
this.mNeedToRotate = calDegree(degree);
this.mRoPivotX = pivotX;
this.mRoPivotY = pivotY;
}
//最終計(jì)算需要矯正的角度值
/*
例如:
比如最終是60度嫉嘀,這時(shí)候其實(shí)是超過了45度汤善,應(yīng)該矯正成90度,
所以最終要多給它30度在旱。順時(shí)針多選擇30度。這里計(jì)算會(huì)得到30噪服。
比如如果是-60度,這時(shí)候應(yīng)該是變成-90讀雹顺,所以我們逆時(shí)針多旋轉(zhuǎn)30度。
這時(shí)候計(jì)算會(huì)得到-30没酣。
如果是20度四康,這時(shí)候沒有超過45度,所以應(yīng)該矯正成0度哎垦,
所以最終要逆時(shí)針轉(zhuǎn)回20度,所以這里計(jì)算會(huì)得到-20鸳碧。
如果是-120度瞻离,這時(shí)候要變成-90度,所以要順時(shí)針轉(zhuǎn)回30度肉迫,
所以計(jì)算會(huì)得到30抛猖。
*/
private int calDegree(int oldDegree) {
int N = Math.abs(oldDegree) / 45;
if ((0 <= N && N < 1) || 2 <= N && N < 3) {
return -oldDegree % 45;
} else {
if (oldDegree < 0) {
return -(45 + oldDegree % 45);
} else {
return (45 - oldDegree % 45);
}
}
}
/*
我們上面的calDegree方法可以獲得我們需要矯正的角度财著,但是我們不是一下子就讓圖片選擇N度,而是慢慢的轉(zhuǎn)過來伟姐。
比如我們用RECOVER_SPEED = 4,4度的慢慢來旋轉(zhuǎn)過來懦鼠,不會(huì)給用戶很突兀的感覺肛冶。
*/
@Override
public void run() {
if (mNeedToRotate == 0) {
return;
}
if (photoView == null) {
return;
}
if (mNeedToRotate > 0) {
//Clockwise rotation
if (mNeedToRotate >= RECOVER_SPEED) {
mSuppMatrix.postRotate(RECOVER_SPEED, mRoPivotX, mRoPivotY);
mNeedToRotate -= RECOVER_SPEED;
} else {
mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY);
mNeedToRotate = 0;
}
} else if (mNeedToRotate < 0) {
//Counterclockwise rotation
if (mNeedToRotate <= -RECOVER_SPEED) {
mSuppMatrix.postRotate(-RECOVER_SPEED, mRoPivotX, mRoPivotY);
mNeedToRotate += RECOVER_SPEED;
} else {
mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY);
mNeedToRotate = 0;
}
}
checkAndDisplayMatrix();
Compat.postOnAnimation(photoView, this);
}
}
結(jié)尾
還是老樣子,希望大家不要吐槽伦乔。有問題留言哈哈。。O(∩_∩)O哈哈~
PS:有好的畫圖軟件介紹嗎。高帖。求介紹o(╥﹏╥)o
附上Demo地址:ScaleImageVewDemo(已經(jīng)把圖片旋轉(zhuǎn)的Activity demo 加入里面)