項目開發中,你們APP開發通常都會用到上傳圖片,好比是上傳了本身的生活照,而後在某個界面處查看上傳的圖片,這時候通常在這個查看詳情的界面,會有手勢放大縮小功能,手勢進行旋轉功能,雙擊放大圖片等等。html
不巧,我之前也有須要這個需求的時候,並且特別指出了要用手勢進行圖片的選擇功能。android
因而我查看了BiliBili的開源庫:git
使用了這個Demo後發現裏面有手勢控制圖片大小,手勢控制圖片旋轉等功能,看了代碼後我發現BiliBili這個demo中也是用了第三方的庫:程序員
咱們能夠看到介紹:在PhotoView的基礎上添加了經過二個手指來旋轉圖片的功能,因此這個庫又是用了其餘的第三方庫:github
咱們能夠看到這個PhotoView的庫有一萬多個star了。說明仍是很不錯的。api
因此經過此次。我就來看PhotoView如何進行實現那麼多功能。bash
你們在看正文以前若是對於Matrix不是很瞭解的,能夠先看看:
android matrix 最全方法詳解與進階(完整篇)
Android Matrix
Float中的那些常量 Infinity、NaNide
原本是想直接拿着PhotoView 的源碼,貼上源碼分析一個個具體的功能,可是由於源碼是考慮到不少功能,因此有不少代碼量,並且太多看着很亂,因此個人方案是直接本身寫個demo,而後根據咱們要講解的功能,仿照PhotoView的源碼,在本身一個個具體的功能demo分別實現。因此本文我先來實現實現根據手勢來實現圖片的縮放功能:工具
PhotoView是繼承了ImageView,而後直接在layout中使用PhotoView,爲了更方便的講解,我就直接仍是使用ImageView,而後讓你們看到是如何對ImageView作處理實現相應的功能。源碼分析
先添加咱們要的demo佈局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.dialog.photoviewdemo.MainActivity">
<ImageView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
/>
</LinearLayout>複製代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//對咱們的ImageView設置相應的一張圖片
ivPhoto = (ImageView) findViewById(R.id.photo_view);
drawable = ContextCompat.getDrawable(this, R.mipmap.ic_launcher);
ivPhoto.setImageDrawable(drawable);
//對咱們的ImageView設置觸摸事件監聽,而且把監聽交給了GestureDetector.
ivPhoto.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return scaleGestureDetector.onTouchEvent(event);
}
});
//GestureDetector的實例生成
scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
return false;
}
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
if(checkMatrixBounds()) {
ivPhoto.setImageMatrix(getDrawMatrix());
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
});
}複製代碼
根據上面的代碼咱們同樣樣來分析:
當用戶觸摸屏幕的時候,會產生許多手勢,例如down,up,scroll,filing等等。
通常狀況下,咱們知道View類有個View.OnTouchListener內部接口,經過重寫他的onTouch(View v, MotionEvent event)方法,咱們能夠處理一些touch事件,可是這個方法太過簡單,若是須要處理一些複雜的手勢,用這個接口就會很麻煩(由於咱們要本身根據用戶觸摸的軌跡去判斷是什麼手勢)。
Android sdk給咱們提供了GestureDetector(Gesture:手勢Detector:識別)類,經過這個類咱們能夠識別不少的手勢,主要是經過他的onTouchEvent(event)方法完成了不一樣手勢的識別。雖然他能識別手勢,可是不一樣的手勢要怎麼處理,應該是提供給程序員實現的。
具體具體能夠看這篇文章,寫的很詳細:用戶手勢檢測-GestureDetector使用詳解
而此處咱們由於作的功能是經過手勢來縮放圖片,因此咱們就要監聽二個手指頭縮放動做,因此咱們使用的是ScaleGestureDetector。
ScaleGestureDetector介紹:
用於處理縮放的工具類,用法與GestureDetector相似,都是經過onTouchEvent()關聯相應的MotionEvent的。使用該類時,用戶須要傳入一個完整的接二連三地motion事件(包含ACTION_DOWN,ACTION_MOVE和ACTION_UP事件)。
咱們看上面的代碼就會發現ScaleGestureDetector有三個方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {}複製代碼
onScaleBegin
:縮放開始會執行的方法,可是咱們發現這個方法須要返回一個Boolean
值,這個值決定是否處理後繼的縮放事件,返回false
時,不會執行onScale()
。
onScaleEnd
:縮放結束執行
onScale
:縮放時候執行的方法,用來作具體的邏輯處理。
咱們具體來看看onScale
方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
return true;
}複製代碼
咱們能夠看到這裏是返回Boolean值,那這裏返回true和false有什麼區別呢。
float scaleFactor = detector.getScaleFactor();複製代碼
咱們能夠經過這個方法獲取到縮放因子,縮放因子會根據你的手勢的變大會愈來愈大,若是你返回了true,那就說明此次的縮放行爲就已經結束了,若是你返回了false,那就說明沒有結束,而後縮放因子愈來愈大。
public boolean onScale(ScaleGestureDetector detector) {
if(detector.getScaleFactor()< 2){
return false;
}
return true;
}複製代碼
(PS:若是二個手指作縮小的手勢,那麼這個縮放因子就會小於1,若是返回false,那麼就會從1開始愈來愈小。)
假設咱們如今的ImageView設置的是全屏,咱們有個小圖片,ImageView設置了圖片後是這樣的:
咱們發現默認是在左上角,並且由於咱們的ImageView設置的是全屏,而圖片又特別小,這樣的初步呈現方式很不友好。
因此咱們要作以下操做:
<1>把圖片居中顯示。
<2>圖片和ImageView相適應(咱們這裏是把圖片適當的放大,來適應這麼大的ImageView.)
因此也就是咱們上面提到過的代碼:
drawableWidth = drawable.getIntrinsicWidth();
drawableHeight = drawable.getIntrinsicHeight();
viewWidth = ivPhoto.getWidth() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
RectF mTempScr = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
mBaseMatrix.setRectToRect(mTempScr, mTempDst, Matrix.ScaleToFit.CENTER);
mDrawableMatrix.set(mBaseMatrix);
ivPhoto.setImageMatrix(mDrawableMatrix);複製代碼
獲取圖片的真實寬高和ImageView用來顯示圖片的寬高我就很少說了。重點是setRectToRect
方法:
public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf)複製代碼
將rect變換成rect,經過stf參數來控制。
ScaleToFit 有以下四個值:
FILL: 可能會變換矩形的長寬比,保證變換和目標矩陣長寬一致。
START:保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。左上對齊。
CENTER: 保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。
END:保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。右下對齊。
這裏使用谷歌的api demo的圖片做爲例子:
咱們很明顯發現,那個藍色的小球的變化不就是咱們想要的變化麼,而且咱們是要居中,因此用的是Matrix.ScaleToFit.CENTER
。
咱們看下咱們最終的效果:
咱們前面已經知道了。手勢變化的時候會觸發onScale
方法,因此咱們只要把圖片的具體的放大縮小的邏輯放在onScale
裏面便可。
@Override
public boolean onScale(ScaleGestureDetector detector) {
//縮放因子
float scaleFactor = detector.getScaleFactor();
//返回組成該手勢的兩個觸點的中點在組件上的x和y軸座標,單位爲像素。
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
//若是爲nan或者無強大,則無效
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
return false;
}
//進行縮放,傳入x軸縮放比例,y軸縮放比例,縮放中心點的x和y值
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
if(checkMatrixBounds()) {
ivPhoto.setImageMatrix(getDrawMatrix());
}
return true;
}複製代碼
你們應該看到了我這邊有個checkMatrixBounds
方法,原本其實單純的縮放就是先postScale
而後在直接setImageMatrix
就能夠了。
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
ivPhoto.setImageMatrix(getDrawMatrix());複製代碼
可是這樣有什麼很差的地方呢。我來具體跟你們說下:
好比我是二個紅點分別是個人手指,而後不停的縮小圖片動做,圖片不只變小,並且會隨着那個方向作平移。放大則相反。這不是咱們想要的,咱們想要的是一樣是作縮放,同時,圖片還在中間。
既然咱們知道了圖片在作縮小放大的同時還在平移,那咱們就作相應的反方向的平移處理不就行了
咱們分爲二種狀況:
若是圖片再縮放過程當中沒超過ImageView的大小。咱們只須要讓圖片一直居中現實便可。因此比較簡單:
只要算出咱們在前面第二個大步裏面的初始化後的圖片的初始狀態後(即和ImageView相適應而且居中),相應的圖片的矩陣的寬和高是否是超過ImageView。若是沒有超過,咱們能夠看到咱們但願的圖片放大和縮小都是但願在正中間的位置,可是如今變成了綠色的地方,咱們只須要把綠色的地方移動到咖啡色的地方就行。
以Y軸爲例(X軸一樣處理):
實際圖片的TOP值(先獲取相應的實際圖片的矩陣Rect,在獲取top屬性):
private RectF getDisplayRect(Matrix matrix) {
Drawable d = drawable;
if (d != null) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
matrix.mapRect(mDisplayRect);
return mDisplayRect;
}
return null;
}複製代碼
ImageView的高度:
viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();複製代碼
實際變化後的圖片的高度(rect爲上面獲取的實際圖片的Rect):final float height = rect.height(), width = rect.width();
因此咱們這裏只須要:
private boolean checkMatrixBounds() {
RectF rect = getDisplayRect(getDrawMatrix());
if (rect == null) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
if (height <= viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
}
if (width <= viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
}
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}複製代碼
這個時候咱們就不行簡單的在中心位置就能夠了。由於這時候不能反而不讓他在中心位置,爲何????咱們如今的圖片是一個安卓機器人,好比我如今要放大它的圖片查看它的右眼,咱們在右上角用手機不挺放大。變成這樣:
這時候就說了。那我什麼都不處理,放大這邊就是這個效果啊。說的沒錯的確這樣,可是好比如今已經放大成這個樣子了。我縮小它,可是我不是從右上角來進行縮小,而是在左邊進行縮小,你們知道咱們不作處理,這時候縮小的時候是按咱們手勢的位置進行,因此頭像在縮小時候先是往左邊方向,而後當小於ImageView的高度時候,又忽然居中,效果很很差。
因此咱們這個例子裏面處理方式是:若是寬度都大於ImageView而且圖片的右邊界還沒出如今ImageView中的時候,先按照本身原來的方式縮小,當圖片的右邊界出如今了ImageView的範圍內了,讓它慢慢往右邊移動(也就是ImageView的寬度 - Rect.right的距離),這時候就會很和諧。最後寬度小於ImageView的時候居於中間。
PS:還有一種正好反過來。咱們放大的圖片是左眼!!(這時候移動的距離是 -rect.left)
因此最終變成這樣:
private boolean checkMatrixBounds() {
RectF rect = getDisplayRect(getDrawMatrix());
if (rect == null) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
if (height <= viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = viewHeight - rect.bottom;
}
if (width <= viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}複製代碼
仍是老樣子,但願你們不要吐槽。有問題留言哈哈。。O(∩_∩)O哈哈~
附上Demo地址:ScaleImageVewDemo