編輯:關於Android編程
開源庫地址:https://github.com/chrisbanes/PhotoView
PhotoView是一個用來幫助開發者輕松實現ImageView縮放的庫。開發者可以輕易控制對圖片的縮放旋等等操作。
PhotoView的使用極其簡單,而且提供了兩種方案。可以使用普通的ImageView,也可以使用該庫中提供的ImageView(PhotoView)。
針對普通ImageView
PhotoViewAttacher mAttacher=new PhotoViewAttacher(mImageView);//用PhotoViewAttacher包裝
mAttacher.update();//當圖片改變時需調用update();
mAttacher.cleanup();//當ImageView不再使用時回收資源(可在onDestory中 調用)。PhotoView已經實現了這個功能不需要自己管理。
PhotoView真的很神奇,接下來我們去源碼裡一探究竟吧。順便多說一句,圖片的縮放大量運用到了Matrix相關知識,不了解的務必要先查閱相關資料哦。
這次源碼解讀我們從使用普通ImageView入手,普通的ImageView如果想縮放,必須依賴於PhotoViewAttacher,而PhotoViewAttacher又實現了IPhotoView接口。IPhotoView主要定義了一些常用的操作和默認值,由於方法實在太多了,就不一一列舉了,直接上圖。
IPhotoView定義的所有抽象方法如下。
IPhotoView的部分源碼如下。
public interface IPhotoView {
float DEFAULT_MAX_SCALE = 3.0f;//默認最大縮放倍數為3倍
float DEFAULT_MID_SCALE = 1.75f;//默認中間縮放倍數為1.75倍
float DEFAULT_MIN_SCALE = 1.0f;//默認最小縮放倍數為1倍
int DEFAULT_ZOOM_DURATION = 200;//默認的縮放間隔為200ms
boolean canZoom();//可以縮放
RectF getDisplayRect();//獲取顯示矩形
boolean setDisplayMatrix(Matrix finalMatrix);//設置顯示矩陣
Matrix getDisplayMatrix();//獲取顯示矩陣
//..
//省略了部分源碼
介紹完IPhotoView接口後,現在改來看看PhotoViewAttacher了,PhotoViewAttacher的屬性也比較多,如下:
private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();//插值器,用於縮放動畫
int ZOOM_DURATION = DEFAULT_ZOOM_DURATION;//默認的縮放間隔
static final int EDGE_NONE = -1;//圖片兩邊都不在邊緣內
static final int EDGE_LEFT = 0;//圖片左邊顯示在View的左邊緣內
static final int EDGE_RIGHT = 1;//圖片右邊顯示在View的右邊緣內
static final int EDGE_BOTH = 2;//圖片兩邊都在邊緣內
static int SINGLE_TOUCH = 1;//單指
private float mMinScale = DEFAULT_MIN_SCALE;//最小縮放倍數
private float mMidScale = DEFAULT_MID_SCALE;//中間縮放倍數
private float mMaxScale = DEFAULT_MAX_SCALE;//最大縮放倍數
private boolean mAllowParentInterceptOnEdge = true;//當在邊緣操作時,允許父布局攔截事件。
private boolean mBlockParentIntercept = false;//阻止父布局攔截事件
private WeakReference mImageView;//弱引用
//手勢探測器
private GestureDetector mGestureDetector;//單擊,長按,Fling
private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector;//縮放和拖拽
private final Matrix mBaseMatrix = new Matrix();//基礎矩陣,用來保存初始的顯示矩陣
private final Matrix mDrawMatrix = new Matrix();//繪畫矩陣,用來計算最後顯示區域的矩陣,是在mBaseMatrix和mSuppMatrix的基礎上計算出來的。
private final Matrix mSuppMatrix = new Matrix();//這個矩陣我也不知道怎麼稱呼,也不知道是不是Supply的意思,暫且叫作供應矩陣吧,用來保存旋轉平移和縮放的矩陣。
private final RectF mDisplayRect = new RectF();//顯示矩形
private final float[] mMatrixValues = new float[9];//用來保存矩陣的值。3*3
// 各類監聽
private OnMatrixChangedListener mMatrixChangeListener;
private OnPhotoTapListener mPhotoTapListener;
private OnViewTapListener mViewTapListener;
private OnLongClickListener mLongClickListener;
private OnScaleChangeListener mScaleChangeListener;
private OnSingleFlingListener mSingleFlingListener;
//保存ImageView的top,right,bottom,left
private int mIvTop, mIvRight, mIvBottom, mIvLeft;
//Fling時的Runable
private FlingRunnable mCurrentFlingRunnable;
private int mScrollEdge = EDGE_BOTH;//兩邊邊緣
private float mBaseRotation;//基礎旋轉角度
private boolean mZoomEnabled;//是否可以縮放
private ScaleType mScaleType = ScaleType.FIT_CENTER;//默認縮放類型
此外PhotoViewAttacher中還定義了以下幾個接口。
public interface OnMatrixChangedListener {
/**
* 當用來顯示Drawable的Matrix改變時回調
* @param rect - 顯示Drawable的新邊界
*/
void onMatrixChanged(RectF rect);
}
public interface OnScaleChangeListener {
/**
* 當ImageView改變縮放時回調
*
* @param scaleFactor 小於1表示縮小,大於1表示放大
* @param focusX 縮放焦點X
* @param focusY 縮放焦點Y
*/
void onScaleChange(float scaleFactor, float focusX, float focusY);
}
public interface OnPhotoTapListener {
/**
*
*當用戶敲擊在照片上時回調,如果在空白區域不會回調
* @param view - ImageView
* @param x -用戶敲擊的位置(在圖片中從左往右的位置)占圖片寬度的百分比
* @param y -用戶敲擊的位置(在圖片中從上往下的位置)占圖片高度的百分比
*/
void onPhotoTap(View view, float x, float y);
/**
* 在圖片外部的空白區域敲擊回調
* */
void onOutsidePhotoTap();
}
public interface OnViewTapListener {
/**
* 只要用戶敲擊ImageView就會回調,不管是不是在圖片上。
* @param view - View the user tapped.
* @param x -敲擊View的x坐標
* @param y -敲擊View的y坐標
*/
void onViewTap(View view, float x, float y);
}
public interface OnSingleFlingListener {
/**
* 用戶使用單指在ImageView上快速滑動時回調,不管是不是在圖片上。
* @param e1 - 第一次觸摸事件
* @param e2 - 第二次觸摸事件
* @param velocityX - 水平滑過的速度.
* @param velocityY - 豎直滑過的素組.
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
在看完PhotoViewAttacher的一些屬性和接口外,現在就來看PhotoViewAttacher的構造方法。即new PhotoViewAttacher(mImageView)
這一句。
public PhotoViewAttacher(ImageView imageView) {
this(imageView, true);
}
public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
mImageView = new WeakReference<>(imageView);//弱引用
imageView.setDrawingCacheEnabled(true);//開啟繪制緩存區,用於獲取可見區的bitmap
imageView.setOnTouchListener(this);//設置Touch監聽,用於添加手勢監聽
ViewTreeObserver observer = imageView.getViewTreeObserver();
if (null != observer)
observer.addOnGlobalLayoutListener(this);//用於監聽ImageView的大小
// 確保ImageView的ScaleType為Matrix
setImageViewScaleTypeMatrix(imageView);
if (imageView.isInEditMode()) {
return;
}
//初始化多指縮放/拖拽手勢探測器
mScaleDragDetector = VersionedGestureDetector.newInstance(
imageView.getContext(), this);
//初始化其它手勢監聽(長按,Fling)
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
//長按
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
//Fling
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (mSingleFlingListener != null) {
if (getScale() > DEFAULT_MIN_SCALE) {
return false;
}
if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
|| MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
return false;
}
return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
}
return false;
}
});
//設置默認的雙擊處理方案。
mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
//基礎旋轉角度
mBaseRotation = 0.0f;
//設置是否可縮放
setZoomable(zoomable);
}
構造方法主要做了一些初始化工作,比如添加了手勢監聽(雙指縮放,拖拽,雙擊,長按)等等。而且,如果希望圖片具備縮放功能,還得設置ImageView的scaleType為matrix,下面我們就一步步剖析。
為了理解起來更連貫一點,我們先看setZoomable
中的源碼。
@Override
public void setZoomable(boolean zoomable) {
mZoomEnabled = zoomable;
update();
}
public void update() {
ImageView imageView = getImageView();//獲取ImageView
if (null != imageView) {
if (mZoomEnabled) {
//再次確保ImageView的ScaleType為MATRIX
setImageViewScaleTypeMatrix(imageView);
//更新基礎矩陣mBaseMatrix
updateBaseMatrix(imageView.getDrawable());
} else {
//重置矩陣
resetMatrix();
}
}
}
可以看出,除了賦值mZoomEnabled外,還調用了update()
方法,前面我們說了,每次更換圖片時需調用update()
刷新。在update()
中,如果是可縮放的,就更新mBaseMatrix
,否則重置矩陣。
updateBaseMatrix
的源碼如下:
private void updateBaseMatrix(Drawable d) {
ImageView imageView = getImageView();//獲取ImageView
if (null == imageView || null == d) {
return;
}
//獲取ImageView的寬高
final float viewWidth = getImageViewWidth(imageView);
final float viewHeight = getImageViewHeight(imageView);
//獲取Drawable的固有的寬高
final int drawableWidth = d.getIntrinsicWidth();
final int drawableHeight = d.getIntrinsicHeight();
mBaseMatrix.reset();//重置mBaseMatrix矩陣
//獲取寬的縮放比,drawableWidth * widthScale = viewWidth
final float widthScale = viewWidth / drawableWidth;
//獲取高的縮放比,drawableHeight * heightScale = viewHeight
final float heightScale = viewHeight / drawableHeight;
//注意,這裡的ScaleType不是ImageView的ScaleType,因為ImageView的ScaleType已被強制設為Matrix。這裡的ScaleType是PhotoViewAttacher的ScaleType,因此可以通過設置PhotoViewAttacher的setScaleType來模擬原ImageView的效果,以滿足實際需求。
if (mScaleType == ScaleType.CENTER) {//如果縮放類型為ScaleType.CENTER
//基礎矩陣就平移兩者的寬度差一半,以保持居中
mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
(viewHeight - drawableHeight) / 2F);
} else if (mScaleType == ScaleType.CENTER_CROP) {//如果縮放類型為ScaleType.CENTER_CROP
float scale = Math.max(widthScale, heightScale);//取最大值
mBaseMatrix.postScale(scale, scale);//使最小的那一邊也縮放到View的尺寸
//平移到中間
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);
} else if (mScaleType == ScaleType.CENTER_INSIDE) {
//如果縮放類型為ScaleType.CENTER_INSIDE
//計算縮放值
float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
//當圖片寬高超出View寬高時調用,否則縮放還是1
mBaseMatrix.postScale(scale, scale);
//平移到中間
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);//平移
} else {
//如果是FIT_XX相關的縮放類型
RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
if ((int) mBaseRotation % 180 != 0) {
mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
}
//直接根據Matrix提供的setRectToRect來設置
switch (mScaleType) {
case FIT_CENTER:
mBaseMatrix
.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
break;
case FIT_START:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
break;
case FIT_END:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
break;
case FIT_XY:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
break;
default:
break;
}
}
//重置矩陣
resetMatrix();
}
可以看出updateBaseMatrix
,主要是在根據ScaleType來調整顯示位置和縮放級別,使其達到ImageView的ScaleType效果。為什麼需要這個功能?由於ImageView已被強制設置ScaleType為Matrix,但是如果我們仍然需要ScaleType的顯示效果怎麼辦?於是PhotoViewAttacher提供了setScaleType來模擬相關效果。從上面的源碼應該不難看出,mBaseMatrix
用來保存根據ScaleType調整過的的原始矩陣。默認的ScaleType為ScaleType.FIT_CENTER。
接下來,我們來看resetMatrix()
。
private void resetMatrix() {
mSuppMatrix.reset();//重置供應矩陣
setRotationBy(mBaseRotation);//設置初始的旋轉角度
setImageViewMatrix(getDrawMatrix());//把最mDrawMatrix設置給ImageView,以對圖片進行變化。
checkMatrixBounds();//檢查Matrix邊界
}
設置旋轉角度的源碼如下,mSuppMatrix後乘了旋轉角度。然後進行檢查邊界,最後進行顯示。
public void setRotationBy(float degrees) {
mSuppMatrix.postRotate(degrees % 360);//後乘旋轉角度
checkAndDisplayMatrix();//檢查Matrix邊界,然後顯示
}
//檢查Matrix邊界和顯示
private void checkAndDisplayMatrix() {
if (checkMatrixBounds()) {
//調整效果進行顯示
setImageViewMatrix(getDrawMatrix());
}
}
checkMatrixBounds()
用來檢查Matrix邊界。相關源碼如下。
private boolean checkMatrixBounds() {
final ImageView imageView = getImageView();
if (null == imageView) {
return false;
}
//獲取最終的顯示區域矩形
final RectF rect = getDisplayRect(getDrawMatrix());
if (null == rect) {
return false;
}
//獲取顯示矩形的寬高
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;//計算調整邊界時要平移的距離
//以下根據縮放類型來調整顯示區域
final int viewHeight = getImageViewHeight(imageView);//獲取View的高
if (height <= viewHeight) {//如果圖片的高小於等於View,說明圖片的垂直方向可以完全顯示在View裡面
//於是根據縮放類型進行邊界調整
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top;//向上移動到View的頂部
break;
case FIT_END:
deltaY = viewHeight - height - rect.top;//向下移動到View的底部
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top;//否則就居中顯示
break;
}
} else if (rect.top > 0) {
//如果圖片高度超出來View的高,但是rect.top > 0說明ImageView上邊還有空余的區域。
deltaY = -rect.top;//於是計算偏移距離
} else if (rect.bottom < viewHeight) {
//同理。底部也有空余
deltaY = viewHeight - rect.bottom;
}
//獲取ImageView的寬,同理進行邊界調整。
final int viewWidth = getImageViewWidth(imageView);
if (width <= viewWidth) {//如果寬度小於View的寬,進行相應調整
switch (mScaleType) {
case FIT_START:
deltaX = -rect.left;
break;
case FIT_END:
deltaX = viewWidth - width - rect.left;
break;
default:
deltaX = (viewWidth - width) / 2 - rect.left;
break;
}
mScrollEdge = EDGE_BOTH;//圖片寬度小於View的寬度,說明兩邊顯示在邊緣內
} else if (rect.left > 0) {
mScrollEdge = EDGE_LEFT;//rect.left > 0表示顯示在左邊邊緣內
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mScrollEdge = EDGE_RIGHT;//右邊在邊緣內
} else {
mScrollEdge = EDGE_NONE;//兩邊都不在邊緣內
}
//最後,將平移給mSuppMatrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}
為什麼要檢查邊界呢?那是因為當你進行旋轉或縮放變換後,由於縮放的錨點是以手指為中心的,有時候會發現顯示的區域不對,比如說,當圖片大於View的寬高時,但是矩陣的邊界與View之間居然還有空白區,顯然不太合理。這時需要進行平移對齊View的寬高。
在檢查顯示邊界時,我們需要獲取圖片的顯示矩形,那麼怎麼獲取Drawable的最終顯示矩形呢?
getDrawMatrix()
用來獲取mDrawMatrix最終矩陣,mDrawMatrix其實是在mBaseMatrix基礎矩陣上後乘mSuppMatrix供應矩陣產生的。
public Matrix getDrawMatrix() {
mDrawMatrix.set(mBaseMatrix);
mDrawMatrix.postConcat(mSuppMatrix);
return mDrawMatrix;
}
通過setImageViewMatrix
將最終的矩陣應用到ImageView中,這時我們就能看到顯示效果了。
private void setImageViewMatrix(Matrix matrix) {
ImageView imageView = getImageView();//獲取ImageView
if (null != imageView) {
checkImageViewScaleType();//檢查縮放類型,必須為Matrix,否則拋異常
imageView.setImageMatrix(matrix);//應用矩陣
//回調監聽
if (null != mMatrixChangeListener) {
RectF displayRect = getDisplayRect(matrix);//獲取顯示矩形
if (null != displayRect) {
mMatrixChangeListener.onMatrixChanged(displayRect);
}
}
}
}
此外,通過如下的源碼可以獲取顯示矩形,matrix.mapRect
用來映射最新的變換到原始的矩形。
private RectF getDisplayRect(Matrix matrix) {
ImageView imageView = getImageView();
if (null != imageView) {
Drawable d = imageView.getDrawable();
if (null != d) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
d.getIntrinsicHeight());//獲取Drawable尺寸,初始化原始矩形
matrix.mapRect(mDisplayRect);//將矩陣的變換映射給mDisplayRect,得到最終矩形
return mDisplayRect;
}
}
return null;
}
看完以上的源碼,相信流程已經非常清楚了,當設置圖片時,通過update()
我們可以初始化一個mBaseMatrix,然後如果想縮放、旋轉等,進行設置應用到mSuppMatrix,最終通過對mBaseMatrix和mSuppMatrix計算得到mDrawMatrix,然後應用到ImageView中,便完成了我們的使命了。
既然一切的變換都會應用到mSuppMatrix中。那麼接下來我們回到PhotoViewAttacher
的構造方法中繼續閱讀其他源碼,以了解這個過程到底是怎麼實現的。
Touch事件中,主要讓手勢探測器進行處理事件。核心源碼如下。
public boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
//可以縮放且有圖片時才能處理手勢監聽
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
if (null != parent) {
//不允許父布局攔截ACTION_DOWN事件
parent.requestDisallowInterceptTouchEvent(true);
} else {
LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
}
cancelFling(); //取消Fling事件
break;
case ACTION_CANCEL:
case ACTION_UP:
//當手指抬起時
if (getScale() < mMinScale) {//如果小於最小值
RectF rect = getDisplayRect();//獲取顯示矩陣
if (null != rect) {
//恢復到最小
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
//如果mScaleDragDetector(縮放、拖拽)不為空,讓它處理事件
if (null != mScaleDragDetector) {
//獲取狀態
boolean wasScaling = mScaleDragDetector.isScaling();
boolean wasDragging = mScaleDragDetector.isDragging();
handled = mScaleDragDetector.onTouchEvent(ev);
//mScaleDragDetector處理事件過後的狀態,如果前後都不在縮放和拖拽,就允許父布局攔截
boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
mBlockParentIntercept = didntScale && didntDrag;//阻止父類攔截的標識
}
// 如果mGestureDetector(雙擊,長按)不為空,交給它處理事件
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
我們來看一下雙擊縮放mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
這種實現方案。DefaultOnDoubleTapListener
實現了GestureDetector.OnDoubleTapListener
接口。
public interface OnDoubleTapListener {
/**
* 當單擊時回調,不同於OnGestureListener.onSingleTapUp(MotionEvent),這個回調方法只在確信用戶不會發生第二次敲擊時調用
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/
boolean onSingleTapConfirmed(MotionEvent e);
/**
* 當雙擊時調用.
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/
boolean onDoubleTap(MotionEvent e);
/**
*當兩次敲擊間回調,回調 MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP事件
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/
boolean onDoubleTapEvent(MotionEvent e);
}
既然知道DefaultOnDoubleTapListener
實現了GestureDetector.OnDoubleTapListener
接口,那麼直接去看DefaultOnDoubleTapListener
中是怎麼實現的。
//單擊事件
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (this.photoViewAttacher == null)
return false;
ImageView imageView = photoViewAttacher.getImageView();//獲取ImageView
//如果OnPhotoTapListener不為null時回調
if (null != photoViewAttacher.getOnPhotoTapListener()) {
final RectF displayRect = photoViewAttacher.getDisplayRect();//獲取當前的顯示矩形
if (null != displayRect) {
final float x = e.getX(), y = e.getY();//獲取第一次敲擊時的坐標
if (displayRect.contains(x, y)) {//判斷是不是敲擊在顯示矩陣內
//如果是的,就計算敲擊百分比
float xResult = (x - displayRect.left)
/ displayRect.width();
float yResult = (y - displayRect.top)
/ displayRect.height();
//敲擊圖片內回調
photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult);
return true;
}else{
//如果敲擊在圖片外回調
photoViewAttacher.getOnPhotoTapListener().onOutsidePhotoTap();
}
}
}
//如果OnViewTapListener不為null時回調,不管在不在圖片裡外
if (null != photoViewAttacher.getOnViewTapListener()) {
photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY());
}
return false;
}
//雙擊事件,在這裡實現縮放
@Override
public boolean onDoubleTap(MotionEvent ev) {
if (photoViewAttacher == null)
return false;
try {
float scale = photoViewAttacher.getScale();//獲取當前縮放比
float x = ev.getX();//獲取敲擊的坐標
float y = ev.getY();//獲取敲擊的坐標
if (scale < photoViewAttacher.getMediumScale()) {
//如果之前的縮放小於中等值,現在就縮放到中等值,縮放錨點就是當前的敲擊事件坐標,true表示需要動畫縮放。
photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true);
} else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) {
//如果之前的縮放大於中等值,現在就縮放到最大值,縮放錨點就是當前的敲擊事件坐標
photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true);
} else {
//否則縮放到最小值,縮放錨點就是當前的敲擊事件坐標
photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
//由於不需要處理兩次敲擊間的其他事件,故這裡不做處理
return false;
}
從這裡可以看出,在單擊時,會回調OnPhotoTapListener和OnViewTapListener,然後將坐標回調出去,如果是雙擊,則根據當前縮放比來判定現在的縮放比然後通過setScale
設置縮放比以及敲擊的坐標。單擊操作我們並不怎麼關心,我們更關心雙擊的縮放操作,於是,查看setScale
源碼。
@Override
public void setScale(float scale, float focalX, float focalY,
boolean animate) {
ImageView imageView = getImageView();//獲取ImageView
//..
//省略了部分源碼
//是否需要動畫
if (animate) {
imageView.post(new AnimatedZoomRunnable(getScale(), scale,
focalX, focalY));
} else {
//設置給mSuppMatrix矩陣
mSuppMatrix.setScale(scale, scale, focalX, focalY);
checkAndDisplayMatrix();
}
}
}
setScale
的源碼還是比較簡單的,如果不需要動畫,直接設置給mSuppMatrix,然後進行檢查顯示。如果需要動畫的話,就執行AnimatedZoomRunnable
。AnimatedZoomRunnable
實現了Runnable
接口,主要實現代碼如下。
private class AnimatedZoomRunnable implements Runnable {
private final float mFocalX, mFocalY;//焦點
private final long mStartTime;//開始時間
private final float mZoomStart, mZoomEnd;
public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
final float focalX, final float focalY) {
mFocalX = focalX;
mFocalY = focalY;
mStartTime = System.currentTimeMillis();
mZoomStart = currentZoom;
mZoomEnd = targetZoom;
}
@Override
public void run() {
ImageView imageView = getImageView();
if (imageView == null) {
return;
}
float t = interpolate();//獲取當前的時間插值
float scale = mZoomStart + t * (mZoomEnd - mZoomStart);//根據插值,獲取當前時間的縮放值
float deltaScale = scale / getScale();//獲取縮放比,大於1表示在放大,小於1在縮小。deltaScale * getScale() = scale
//回調出去,deltaScale表示相對上次要縮放的比例
onScale(deltaScale, mFocalX, mFocalY);
if (t < 1f) {//插值小於1表示沒有縮放完成,通過不停post進行執行動畫
Compat.postOnAnimation(imageView, this);//Compat根據版本做了兼容處理,小於4.2用了 view.postDelayed,大於等於4.2用了view.postOnAnimation。
}
}
}
//計算當前時間的插值
private float interpolate() {
float t = 1.0F * (float)(System.currentTimeMillis() - this.mStartTime) / (float)PhotoViewAttacher.this.ZOOM_DURATION;
t = Math.min(1.0F, t);
t = PhotoViewAttacher.sInterpolator.getInterpolation(t);
return t;
}
}
onScale
的相關源碼如下,可以看出,調用了mSuppMatrix.postScale
和checkAndDisplayMatrix()
來進行顯示縮放。
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
if (null != mScaleChangeListener) {
//監聽
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
//縮放
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkAndDisplayMatrix();
}
}
雙擊縮放中的動畫縮放的流程是這樣的,首先會記錄一個開始時間mStartTime
,然後根據當前時間來獲取插值interpolate()
以便了解當前應該處於的進度,根據插值求出當前的縮放值scale
,然後與上次相比求出縮放比差值deltaScale
,然後通過onScale
回調出去,最終通過Compat.postOnAnimation來執行這個Runable,如此反復直到插值為1,縮放到目標值為止。
雙擊縮放的相關源碼到此為止,接下來看看通過雙指縮放與拖拽的實現源碼。即VersionedGestureDetector.newInstance(imageView.getContext(), this);
這句。
VersionedGestureDetector看名字便知道又做了版本兼容處理。裡面只有一個靜態方法newInstance
,源碼如下。
//根據版本進行了控制
public final class VersionedGestureDetector {
public static GestureDetector newInstance(Context context,
OnGestureListener listener) {
final int sdkVersion = Build.VERSION.SDK_INT;
GestureDetector detector;
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
//小於Android 2.0
detector = new CupcakeGestureDetector(context);
} else if (sdkVersion < Build.VERSION_CODES.FROYO) {
//小於Android 2.2
detector = new EclairGestureDetector(context);
} else {
detector = new FroyoGestureDetector(context);
}
detector.setOnGestureListener(listener);
return detector;
}
}
newInstance
中傳入了OnGestureListener,這個OnGestureListener
是自定義的接口,源碼如下。
public interface OnGestureListener {
//拖拽時回調
void onDrag(float dx, float dy);
//Fling時回調
void onFling(float startX, float startY, float velocityX,
float velocityY);
//縮放時回調,`onScale`在雙擊動畫縮放時已經介紹過了,scaleFactor表示相對於上次的縮放比
void onScale(float scaleFactor, float focusX, float focusY);
}
可以看出,回調了縮放、Fling和拖拽三種情況。現在我們回到newInstance
相關源碼,可以看出有三種探測器CupcakeGestureDetector
、EclairGestureDetector
和FroyoGestureDetector
。且三者是相互繼承的關系,FroyoGestureDetector
繼承於EclairGestureDetector
,EclairGestureDetector
繼承於CupcakeGestureDetector
。
其中CupcakeGestureDetector和EclairGestureDetector不支持雙指縮放。由於Android2.0以下不支持多點觸控,於是CupcakeGestureDetector
核心源碼如下:
float getActiveX(MotionEvent ev) {
return ev.getX();
}
float getActiveY(MotionEvent ev) {
return ev.getY();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
//添加速度探測器
mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
} else {
LogManager.getLogger().i(LOG_TAG, "Velocity tracker is null");
}
//獲取坐標
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
mIsDragging = false;
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = getActiveX(ev);
final float y = getActiveY(ev);
final float dx = x - mLastTouchX, dy = y - mLastTouchY;
if (!mIsDragging) {
//如果手指移動的距離大於mTouchSlop,表示在拖拽
mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}
if (mIsDragging) {//如果在拖拽,就回調出去
mListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
case MotionEvent.ACTION_UP: {
//手指抬起時,如果之前在拖拽
if (mIsDragging) {
if (null != mVelocityTracker) {
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
//計算滑動速度
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
.getYVelocity();
//如果大於最小的Fling速度,就回調出去
if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
mListener.onFling(mLastTouchX,、mLastTouchY, -vX,-vY);
}
}
}
//回收速度探測器
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
return true;
}
從源碼可以看出CupcakeGestureDetector
實現了拖拽和Fling效果。EclairGestureDetector
用於Android 2.2以下,主要修正了多點觸控的問題,因為當雙指觸控時,我們需要獲取的是最後一個手指離開屏幕時的坐標,因此需要使getActiveX/getActiveY指向正確的點。源碼如下:
@Override
float getActiveX(MotionEvent ev) {
try {
return ev.getX(mActivePointerIndex);//mActivePointerIndex為手指的索引。根據當前手指的索引獲取坐標
} catch (Exception e) {
return ev.getX();
}
}
@Override
float getActiveY(MotionEvent ev) {
try {
return ev.getY(mActivePointerIndex);
} catch (Exception e) {
return ev.getY();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);//第一根手指的id
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mActivePointerId = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
//獲取某一根手指抬起時的索引
final int pointerIndex = Compat.getPointerIndex(ev.getAction());
//根據索引獲取id
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {//如果是抬起的是第一根手指
//那麼對應獲取第二點
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);//將id指向第二根手指
//獲取第二根手指的當前坐標
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
}
break;
}
//將索引指向後抬起的手指
mActivePointerIndex = ev
.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
: 0);
try {
return super.onTouchEvent(ev);//按照`CupcakeGestureDetector`的邏輯處理
} catch (IllegalArgumentException e) {
return true;
}
}
FroyoGestureDetector
用於Android 2.2以上,此時系統已經提供了一個縮放探索器,於是在拖拽和Fling的基礎上,添加了雙指縮放功能,核心源碼如下。
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();//獲取相比於當其縮放值的縮放比例
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
return false;
//回調出去。
mListener.onScale(scaleFactor,
detector.getFocusX(), detector.getFocusY());
return true;
}
//..
//省略了部分源碼
圖片的縮放與拖拽並沒有在探測器中實現,而是回調到了PhotoViewAttacher中,PhotoViewAttacher實現了OnGestureListener接口,相關處理如下。
//拖拽回調
@Override
public void onDrag(float dx, float dy) {
if (mScaleDragDetector.isScaling()) {
return; // 如果正在縮放,不許做其他操作
}
//根劇拖拽進行平移
ImageView imageView = getImageView();
mSuppMatrix.postTranslate(dx, dy);
checkAndDisplayMatrix();
//判斷父布局是不是可以攔截這一拖拽行為
ViewParent parent = imageView.getParent();
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
//如果沒有阻止父布局攔截且圖片已顯示在相關邊緣內,就允許攔截
if (mScrollEdge == EDGE_BOTH
|| (mScrollEdge == EDGE_LEFT && dx >= 1f)
|| (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
}
} else {
//否則不允許攔截
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
//Fling回調
@Override
public void onFling(float startX, float startY, float velocityX,
float velocityY) {
ImageView imageView = getImageView();
mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
//傳入fling的速度與View的寬高
mCurrentFlingRunnable.fling(getImageViewWidth(imageView),getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
imageView.post(mCurrentFlingRunnable);
}
其中FlingRunnable
的源碼如下。
private class FlingRunnable implements Runnable {
private final ScrollerProxy mScroller;//Scroller這裡做了版本兼容處理,API小於9時用了PreGingerScroller(內部用了Scroller),小於14用了GingerScroller(內部用了OverScroller),其他用了IcsScroller(內部用了OverScroller)。
private int mCurrentX, mCurrentY;//當前坐標
public FlingRunnable(Context context) {
mScroller = ScrollerProxy.getScroller(context);
}
public void cancelFling() {
mScroller.forceFinished(true);//停止
}
public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
final RectF rect = getDisplayRect();//獲取圖片的顯示區域
if (null == rect) {
return;
}
//水平方向上
final int startX = Math.round(-rect.left);//四捨五入,左邊的x坐標
final int minX, maxX, minY, maxY;//Fling的邊界值
if (viewWidth < rect.width()) {//如果圖片的寬度大於View寬時就計算X的邊界。
minX = 0;
maxX = Math.round(rect.width() - viewWidth);
} else {
minX = maxX = startX;//如果圖片寬小於View寬,就將三者設為一樣。
}
//豎直方向上
final int startY = Math.round(-rect.top);
if (viewHeight < rect.height()) {//如果顯示矩形高大於View的高。就計算邊界
minY = 0;
maxY = Math.round(rect.height() - viewHeight);
} else {
minY = maxY = startY;
}
mCurrentX = startX;
mCurrentY = startY;
//調用 mScroller.fling
if (startX != maxX || startY != maxY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX,
maxX, minY, maxY, 0, 0);
}
}
@Override
public void run() {
if (mScroller.isFinished()) {
return;
}
ImageView imageView = getImageView();
if (null != imageView && mScroller.computeScrollOffset()) {
//獲取當前的位置
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
//將平移差值應用到mSuppMatrix
mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
setImageViewMatrix(getDrawMatrix());//應用到矩陣
mCurrentX = newX;
mCurrentY = newY;
Compat.postOnAnimation(imageView, this);
}
}
}
同樣,利用了Compat.postOnAnimation
不停執行Runable來實現Fling慣性滾動效果。
關於PhotoViewAttacher的相關源碼已經解讀完畢,而該庫中的控件PhotoView的實現也是依賴於PhotoViewAttacher,在onDetachedFromWindow
中會自動回收資源,核心源碼如下,其他就不做詳細介紹了。
public PhotoView(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
super.setScaleType(ScaleType.MATRIX);
this.init();
}
//初始化
protected void init() {
if(null == this.mAttacher || null == this.mAttacher.getImageView()) {
this.mAttacher = new PhotoViewAttacher(this);
}
if(null != this.mPendingScaleType) {
this.setScaleType(this.mPendingScaleType);
this.mPendingScaleType = null;
}
}
//調用回收cleanup
protected void onDetachedFromWindow() {
this.mAttacher.cleanup();
super.onDetachedFromWindow();
}
//初始化
protected void onAttachedToWindow() {
this.init();
super.onAttachedToWindow();
}
感覺最近寫東西越來越啰嗦了,需要練習著把話講的簡練一點,下一期源碼解讀:Gson。
本期解讀到此結束,如有錯誤之處,歡迎指出。
XML初步今天我們來學習另一種非常重要的數據交換格式-XML。XML(Extensible Markup Language的縮寫,意為可擴展的標記語言),它是一種元標記
Activity類處於android.app包中,繼承體系如下:1.java.lang.Object2.android.content.Context3.android.
解釋一下,上面的sinX和cosX,表示旋轉角度的cos值和sin值,注意,旋轉角度是按順時針方向計算的。 translateX
原因分析用戶使用android 5.0以上的系統在安裝APP時,將消息通知的權限關閉掉了。實際上用戶本意只是想關閉Notification,但是Toast的show方法中