先上效果圖

一、需求分析
單點(diǎn)拖動(dòng)圖片對(duì)圖片進(jìn)行平移操作。雙手縮放圖片大小和旋轉(zhuǎn)圖片到一定的角度。圖片縮放的時(shí)候 不能大于最大的縮放因子和小于最小的縮放因子。大于最大縮放因子或者小于最小縮放因子需要對(duì)圖像進(jìn)行回彈。圖片旋轉(zhuǎn)的角度只能為90度的倍數(shù),不滿足90度要進(jìn)行回彈。圖片回彈要一個(gè)漸變的效果。
二、 思路分析
大體思路:首先,Android中提供了Matrix類可以對(duì)圖像進(jìn)行處理。其次,要顯示一張圖片最容易想到的就是ImageView?;貜椧鬂u變的過程,可以通過屬性動(dòng)畫進(jìn)行設(shè)置。所以大體的思路是:繼承ImageView,重寫onTouchEvent()方法,判斷事件類型,在對(duì)應(yīng)的事件使用Matrix對(duì)圖像進(jìn)行變換。
Matrix是一個(gè)已經(jīng)封裝好的矩陣,最重要的作用就是對(duì)坐標(biāo)點(diǎn)進(jìn)行變換。
舉個(gè)栗子:
1.某個(gè)點(diǎn)(x0,y0,1)通過單位矩陣E映射得到的點(diǎn)還是(x0,y0,1)。

2.調(diào)用單位矩陣E的postTranslate(dx,dy)。單位矩陣轉(zhuǎn)換成矩陣T

3.點(diǎn)(x0,y0,1)通過矩陣T映射得到的點(diǎn)就會(huì)做如下的變換

可以看到點(diǎn)(x0,y0,1)經(jīng)過T矩陣在x軸方向上平移了dx,在y軸方向上平移了dy。
通過以上的變換可以得到具體的思路:我們維護(hù)一個(gè)圖像對(duì)應(yīng)的矩陣mCurrentMatrix,該矩陣主要是對(duì)ImageView中的圖像的各個(gè)點(diǎn)進(jìn)行映射。ImageView在容器位置擺放完成之后,置mCurrentMatrix矩陣為單位矩陣。當(dāng)onTouchEvent()方法中觸發(fā)單點(diǎn)觸控并且手指進(jìn)行平移的時(shí)候,調(diào)用矩陣mCurrentMatrix的postTranslate(dx,dy),對(duì)mCurrentMatrix進(jìn)行變換。當(dāng)手指抬起,利用變換結(jié)束后的矩陣對(duì)圖像的各個(gè)點(diǎn)進(jìn)行映射,從而得到平移變換后的圖像。同理可得,在兩只手指進(jìn)行縮放旋轉(zhuǎn)的時(shí)候,我們對(duì)矩陣mCurrentMatrix進(jìn)行各種變換,當(dāng)縮放旋轉(zhuǎn)的事件結(jié)束再利用變換完的矩陣去映射圖像的各個(gè)點(diǎn),從而得到縮放、旋轉(zhuǎn)后的圖像。
三、Matrix
安卓自定義View進(jìn)階 - Matrix原理
安卓自定義View進(jìn)階 - Matrix詳解
對(duì)于Matrix的使用以上兩篇博客中有詳細(xì)的描述Matrix的使用方法以及原理。例如矩陣的第一行第三列的浮點(diǎn)數(shù)以及第二行第三列的浮點(diǎn)數(shù)是用來映射點(diǎn)的平移。
注意調(diào)用Matrix的setXxx方法,會(huì)將Matrix之前的變換都清除。
四、主要方法
首先理清事件的邏輯:
單只手指按下的時(shí)候是可以進(jìn)行平移操作的,多點(diǎn)觸控的時(shí)候不能進(jìn)行平移操作。所以維護(hù)一個(gè)boolean變量mCanTranslate,在ACTION_DOWN的時(shí)候置位true,ACTION_POINTER_DOWN置為false。而平移操作的位移量需要手指滑動(dòng)時(shí)與上一次點(diǎn)的坐標(biāo)進(jìn)行相比較,所以維護(hù)一個(gè)mLastSinglePoint記錄手指按下以及手指滑動(dòng)時(shí)上一次點(diǎn)的坐標(biāo)。
當(dāng)兩只手指按下的時(shí)候是可以進(jìn)行旋轉(zhuǎn)和縮放操作的,維護(hù)兩個(gè)boolean變量:mCanRotate、mCanScale在ACTION_POINTER_DOWN置為true,ACTION_POINTER_UP置為false。
圖像的縮放比例因子采取的是前后兩個(gè)手指間距離的比值。所以維護(hù)mLastDist,在ACTION_POINTER_DOWN的時(shí)候算出兩只手指間的歐氏距離。在ACTION_MOVE的時(shí)候,算出新的手指間的距離,并和上一次手指間距離的運(yùn)算求出比例,更新mLastDist為本次手指間的距離。
圖像的旋轉(zhuǎn)角度采取的是兩只手構(gòu)成的向量轉(zhuǎn)過的角度。維護(hù)一個(gè)mLastVector。在ACTION_POINTER_DOWN的時(shí)候記錄兩只手指構(gòu)成的向量。在ACTION_MOVE的時(shí)候,記錄新的兩只手指構(gòu)成的向量,求出轉(zhuǎn)過的角度,最后更新mLastVector為本次兩手指構(gòu)成的向量。
初始化圖像大小和位置
縮放圖像大小和控件大小自適應(yīng),平移圖像中心和控件中心重合
private void init() {
mCurrentMatrix.reset();
upDateBoundRectF();
float scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
mInitialScaleFactor = scaleFactor;
mTotalScaleFactor *= scaleFactor;
//以圖片的中心點(diǎn)進(jìn)行縮放,縮放圖片大小和控件大小適應(yīng)
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
//將圖片中心點(diǎn)平移到和控件中心點(diǎn)重合
mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
//對(duì)圖片進(jìn)行變換,并更新圖片的邊界矩形
transform();
}
onTouchEvent()函數(shù)
/**
* 當(dāng)單點(diǎn)觸控的時(shí)候可以進(jìn)行平移操作
* 當(dāng)多點(diǎn)觸控的時(shí)候:可以進(jìn)行圖片的縮放、旋轉(zhuǎn)
* ACTION_DOWN:標(biāo)記能平移、不能旋轉(zhuǎn)、不能縮放
* ACTION_POINTER_DOWN:如果手指?jìng)€(gè)數(shù)為2,標(biāo)記不能平移、能旋轉(zhuǎn)、能縮放
* 記錄平移開始時(shí)兩手指的中點(diǎn)、兩只手指形成的向量、兩只手指間的距離
* ACTION_MOVE:進(jìn)行平移、旋轉(zhuǎn)、縮放的操作。
* ACTION_POINTER_UP:有一只手指抬起的時(shí)候,設(shè)置圖片不能旋轉(zhuǎn)、不能縮放,可以平移
*
* @param event 點(diǎn)擊事件
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
//單點(diǎn)觸控,設(shè)置圖片可以平移、不能旋轉(zhuǎn)和縮放
case MotionEvent.ACTION_DOWN:
mCanTranslate = true;
mCanRotate = false;
mCanScale = false;
//記錄單點(diǎn)觸控的上一個(gè)單點(diǎn)的坐標(biāo)
mLastSinglePoint.set(event.getX(), event.getY());
break;
case MotionEvent.ACTION_POINTER_DOWN:
animator.cancel();
//多點(diǎn)觸控,設(shè)置圖片不能平移
mCanTranslate = false;
//當(dāng)手指?jìng)€(gè)數(shù)為兩個(gè)的時(shí)候,設(shè)置圖片能夠旋轉(zhuǎn)和縮放
if (event.getPointerCount() == 2) {
mCanRotate = true;
mCanScale = true;
//記錄兩手指的中點(diǎn)
PointF pointF = midPoint(event);
//記錄開始滑動(dòng)前兩手指中點(diǎn)的坐標(biāo)
mLastMidPoint.set(pointF.x, pointF.y);
//記錄開始滑動(dòng)前兩個(gè)手指之間的距離
mLastDist = distance(event);
//設(shè)置向量,以便于計(jì)算角度
mLastVector.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
}
break;
case MotionEvent.ACTION_MOVE:
//判斷能否平移操作
if (mCanTranslate) {
float dx = event.getX() - mLastSinglePoint.x;
float dy = event.getY() - mLastSinglePoint.y;
//平移操作
translation(dx, dy);
//重置上一個(gè)單點(diǎn)的坐標(biāo)
mLastSinglePoint.set(event.getX(), event.getY());
}
//判斷能否縮放操作
if (mCanScale) {
float scaleFactor = distance(event) / mLastDist;
scale(scaleFactor);
//重置mLastDist,讓下次縮放在此基礎(chǔ)上進(jìn)行
mLastDist = distance(event);
}
//判斷能否旋轉(zhuǎn)操作
if (mCanRotate) {
//當(dāng)前兩只手指構(gòu)成的向量
PointF vector = new PointF(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
//計(jì)算本次向量和上一次向量之間的夾角
float degree = calculateDeltaDegree(mLastVector, vector);
rotation(degree);
//更新mLastVector,以便下次旋轉(zhuǎn)計(jì)算旋轉(zhuǎn)過的角度
mLastVector.set(vector.x, vector.y);
}
//圖像變換
transform();
break;
case MotionEvent.ACTION_POINTER_UP:
//當(dāng)兩只手指有一只抬起的時(shí)候,設(shè)置圖片不能縮放和選擇,能夠進(jìn)行平移
if (event.getPointerCount() == 2) {
mCanScale = false;
mCanRotate = false;
mCanTranslate = true;
//重置旋轉(zhuǎn)和縮放使用到的中點(diǎn)坐標(biāo)
mLastMidPoint.set(0f, 0f);
//重置兩只手指的距離
mLastDist = 0f;
//重置兩只手指形成的向量
mLastVector.set(0f, 0f);
}
//獲得開始動(dòng)畫之前的矩陣
mCurrentMatrix.getValues(mBeginMatrixValues);
//縮放回彈
backScale();
upDateBoundRectF();
//旋轉(zhuǎn)回彈
backRotation();
upDateBoundRectF();
//獲得動(dòng)畫結(jié)束之后的矩陣
mCurrentMatrix.getValues(mEndMatrixValues);
animator.start();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
backTranslation();
upDateBoundRectF();
mLastSinglePoint.set(0f, 0f);
mCanTranslate = false;
mCanScale = false;
mCanRotate = false;
break;
}
return true;
}
平移操作
將圖像對(duì)應(yīng)的矩陣進(jìn)行變換。
protected void translation(float dx, float dy) {
//檢查圖片邊界的平移是否超過控件的邊界
if (mBoundRectF.left + dx > getWidth() - 20 || mBoundRectF.right + dx < 20
|| mBoundRectF.top + dy > getHeight() - 20 || mBoundRectF.bottom + dy < 20) {
return;
}
mCurrentMatrix.postTranslate(dx, dy);
}
縮放操作
mBoundRectF為記錄圖像邊界的矩形??s放的時(shí)候選取圖像的中心進(jìn)行縮放。
private void scale(float scaleFactor) {
//累乘得到總的的縮放因子
mTotalScaleFactor *= scaleFactor;
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
}
旋轉(zhuǎn)操作
旋轉(zhuǎn)的時(shí)候旋轉(zhuǎn)的旋轉(zhuǎn)中心也是圖像的中心
private void rotation(float degree) {
//旋轉(zhuǎn)變換
mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());
}
圖像中各個(gè)點(diǎn)的映射
調(diào)用ImageView的setImageMatrix(Matrix matrix)會(huì)讓ImageView根據(jù)設(shè)置的matrix去重新繪制圖像。
private void transform() {
setImageMatrix(mCurrentMatrix);
upDateBoundRectF();
}
更新圖像的矩形邊界
獲得圖像的矩形,并根據(jù)矩陣映射矩形各個(gè)點(diǎn)的坐標(biāo)。
private void upDateBoundRectF() {
if (getDrawable() != null) {
mBoundRectF.set(getDrawable().getBounds());
mCurrentMatrix.mapRect(mBoundRectF);
}
}
縮放回彈
private void backScale() {
float scaleFactor = 1.0f;
//如果總的縮放比例因子比初始化的縮放因子還小,進(jìn)行回彈
if (mTotalScaleFactor / mInitialScaleFactor < mMinScaleFactor) {
//1除以總的縮放因子再乘初始化的縮放因子,求得回彈的縮放因子
scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMinScaleFactor;
//更新總的縮放因子,以便下次在此縮放比例的基礎(chǔ)上進(jìn)行縮放
mTotalScaleFactor = mInitialScaleFactor * mMinScaleFactor;
}
//如果總的縮放比例因子大于最大值,讓圖片放大到最大倍數(shù)
else if (mTotalScaleFactor / mInitialScaleFactor > mMaxScaleFactor) {
//求放大到最大倍數(shù),需要的比例因子
scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMaxScaleFactor;
//更新總的縮放因子,以便下次在此縮放比例的基礎(chǔ)上進(jìn)行縮放
mTotalScaleFactor = mInitialScaleFactor * mMaxScaleFactor;
}
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
}
旋轉(zhuǎn)回彈
private void backRotation() {
//x軸方向的單位向量,在極坐標(biāo)中,角度為0
float[] x_vector = new float[]{1.0f, 0.0f};
//映射向量
mCurrentMatrix.mapVectors(x_vector);
//計(jì)算x軸方向的單位向量轉(zhuǎn)過的角度
float totalDegree = (float) Math.toDegrees((float) Math.atan2(x_vector[1], x_vector[0]));
float degree = totalDegree;
degree = Math.abs(degree);
//如果旋轉(zhuǎn)角度的絕對(duì)值在45-135度之間,讓其旋轉(zhuǎn)角度為90度
if (degree > 45 && degree <= 135) {
degree = 90;
} //如果旋轉(zhuǎn)角度的絕對(duì)值在135-225之間,讓其旋轉(zhuǎn)角度為180度
else if (degree > 135 && degree <= 225) {
degree = 180;
} //如果旋轉(zhuǎn)角度的絕對(duì)值在225-315之間,讓其旋轉(zhuǎn)角度為270度
else if (degree > 225 && degree <= 315) {
degree = 270;
}//如果旋轉(zhuǎn)角度的絕對(duì)值在315-360之間,讓其旋轉(zhuǎn)角度為0度
else {
degree = 0;
}
degree = totalDegree < 0 ? -degree : degree;
//degree-totalDegree計(jì)算達(dá)到90的倍數(shù)角,所需的差值
mCurrentMatrix.postRotate(degree - totalDegree, mBoundRectF.centerX(), mBoundRectF.centerY());
}
一些計(jì)算方法
/**
* 計(jì)算兩個(gè)手指頭之間的中心點(diǎn)的位置
* x = (x1+x2)/2;
* y = (y1+y2)/2;
*
* @param event 觸摸事件
* @return 返回中心點(diǎn)的坐標(biāo)
*/
private PointF midPoint(MotionEvent event) {
float x = (event.getX(0) + event.getX(1)) / 2;
float y = (event.getY(0) + event.getY(1)) / 2;
return new PointF(x, y);
}
/**
* 計(jì)算兩個(gè)手指間的距離
*
* @param event 觸摸事件
* @return 放回兩個(gè)手指之間的距離
*/
private float distance(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);//兩點(diǎn)間距離公式
}
/**
* 計(jì)算兩個(gè)向量之間的夾角
*
* @param lastVector 上一次兩只手指形成的向量
* @param vector 本次兩只手指形成的向量
* @return 返回手指旋轉(zhuǎn)過的角度
*/
private float calculateDeltaDegree(PointF lastVector, PointF vector) {
float lastDegree = (float) Math.atan2(lastVector.y, lastVector.x);
float degree = (float) Math.atan2(vector.y, vector.x);
float deltaDegree = degree - lastDegree;
return (float) Math.toDegrees(deltaDegree);
}
五、動(dòng)畫
要求圖像的變換是一個(gè)漸變的過程,很容易想到的就是屬性動(dòng)畫。因?yàn)閷傩詣?dòng)畫本身就是對(duì)值進(jìn)行不斷set的過程。而我們維護(hù)的矩陣也是一個(gè)值,所以很自然可以想到,如果得到回彈之前的矩陣的值以及回彈之后矩陣的值,就可以根據(jù)動(dòng)畫監(jiān)聽器中動(dòng)畫當(dāng)前的系數(shù)值去改變矩陣的值。
/**
* 動(dòng)畫監(jiān)聽器
*/
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//獲得動(dòng)畫過程當(dāng)前的系數(shù)值
float animatedValue = (float) animation.getAnimatedValue();
for (int i = 0; i < 9; i++) {
//使用漸變過程中的系數(shù)值去變換矩陣
mTransformMatrixValues[i] = mBeginMatrixValues[i] + (mEndMatrixValues[i] - mBeginMatrixValues[i]) * animatedValue;
}
//動(dòng)態(tài)更新矩陣中的值
mCurrentMatrix.setValues(mTransformMatrixValues);
//圖像變化
transform();
}
};
/**
* 動(dòng)畫監(jiān)聽器
*/
private Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentMatrix.setValues(mEndMatrixValues);
transform();
}
};
對(duì)animator對(duì)象設(shè)置完監(jiān)聽器之后,就可以在手指抬起的時(shí)候調(diào)用屬性動(dòng)畫的start()方法開啟動(dòng)畫。
六、總結(jié)
自定義可平移、縮放、旋轉(zhuǎn)的控件主要點(diǎn)有兩個(gè)方面:一是onTouchEvent()中判斷平移、旋轉(zhuǎn)、縮放的觸發(fā)條件,平移位移量、縮放比例因子、旋轉(zhuǎn)角度的計(jì)算。二是Matrix矩陣的應(yīng)用。