點(diǎn)擊小圖片轉(zhuǎn)到圖片查看的頁面在Android開發(fā)中很常用到,抱著學(xué)習(xí)和分享的心態(tài),在這里寫下自己自定義的一個(gè)ImageView,可以實(shí)現(xiàn)類似微信朋友圈中查看圖片的功能和效果。

主要功能需求:
1.自由縮放,有最大和最小的縮放限制
2.若圖片沒充滿整個(gè)ImageView,則縮放過程將圖片居中
3.根據(jù)當(dāng)前縮放的狀態(tài),雙擊放大兩倍或縮小到原來
4.當(dāng)圖片尺寸超過ImageView所能展示,圖片可以移動
5.移動以及縮放過程中,圖片不可脫離ImageView邊緣
6.縮放到離開ImageView邊緣,自動回彈
7.沒有縮放狀態(tài)下,下滑圖片,背景色逐漸透明,達(dá)到一段距離可以退出
8.若設(shè)置了退出動畫,那么將圖片縮回到原來打開的位置。
實(shí)現(xiàn)方式:
我們在這可以通過繼承ImageView,自己寫一個(gè)TouchImageView來實(shí)現(xiàn),功能核心的實(shí)現(xiàn)涉及到Touch事件,因此需要重寫它的onTouchEvent方法,進(jìn)行相應(yīng)處理。宏觀上說,就是點(diǎn)擊小圖片,然后進(jìn)到新的Activiy,該Activity創(chuàng)建這個(gè)TouchImageView,布局代碼和Activity代碼不再給出,可以根據(jù)需要自由使用。
對圖片的移動、縮放、位置等的控制,我們用Matrix這個(gè)類來實(shí)現(xiàn),這是一個(gè)功能強(qiáng)大的類,這里簡單的介紹一下:
在這個(gè)功能里,用的比較多的是它的這幾個(gè)方法,網(wǎng)上有很多解釋,這里簡單解釋一下,新手更容易理解。
首先介紹一下Matrix內(nèi)部的一個(gè)3x3矩陣,它用一個(gè)float[9]的數(shù)組來表示,這里我們需要常用Matrix.getValues(float[] values)的方法來獲取最新的數(shù)組數(shù)據(jù),從而知道現(xiàn)在圖片的放大倍數(shù)和坐標(biāo)位置等。如(values[MTRANS_X],values[MTRANS_Y])表示圖片左上角的位移情況,可以當(dāng)做是當(dāng)前這個(gè)點(diǎn)的坐標(biāo),(0,0)時(shí)沒經(jīng)過位移,在左上角。

Matrix.postScale(float sx, float sy)
Matrix.postScale(float sx, float sy, float px, float py)
這兩個(gè)方法是用來進(jìn)行縮放,第二個(gè)方法的后兩個(gè)參數(shù)是放大的中心點(diǎn)。每調(diào)用一次postScale,都是與之前的倍數(shù)相乘得到最終的放大倍數(shù),如sx*Values[Matrix.MSCALE_X] ,所以在移動過程每次進(jìn)入ACTION_MOVE體,先初始化一下matrix,確保是上一次操作后的prematrix,這樣得到的放大倍數(shù)才是正確的。這些地方讀代碼會理解更完整一點(diǎn)。另外,需要提一下的是,postScale會產(chǎn)生一定的位移,如果同時(shí)你要用postTranslate進(jìn)行一些特定移動的話,可能需要消除postScale位移的影響,否則最終可能移動的位置不是你想要的,我在代碼中也有體現(xiàn)這點(diǎn)。
Matrix.postTranslate(float dx, float dy)
這個(gè)方法進(jìn)行圖片的位移,dx和dy是相對現(xiàn)在位置的位移距離,根據(jù)Android中的坐標(biāo)表示,dx正值表示向右移,dy正值表示向下移。
另外給出一張簡單流程圖,希望有用。
程序代碼:
下面貼出完整代碼,有點(diǎn)長
public class TouchImageView extends ImageView{
//圖片的尺寸
private float imgHeight = 0;
private float imgWidth = 0;
private Context context;
//View的尺寸
private int viewHeight = 0;
private int viewWidth = 0;
//圖片的縮放最大值
private float maxHeight;
private float maxWidth;
private float minHeight;
private float minWidth;
//移動前兩指直接的距離
private double beginDistance;
private boolean isOnePointer = true;
//下滑退出的控制變量
private boolean canQuit = false;
private boolean tryQuit = false;
//圖片縮放時(shí)居中過程產(chǎn)生的位移量
private float tempdy = 0;
private float tempdx = 0;
//兩指的中點(diǎn)坐標(biāo),用于設(shè)置縮放的中心點(diǎn)
private float xMid,yMid;
//第一根手指按下的初始坐標(biāo)
private float xDown,yDown;
//目前操作的Matrix對象
private Matrix matrix = new Matrix();
//上一次操作的Matrix對象
private Matrix preMatrix = new Matrix();
private boolean isMovePic = false;
private boolean isZoomPic = false;
//退出時(shí)需要縮放到的位置
private int preWidth = 0;
private int preHeight = 0;
private int xLocation = -1;
private int yLocation = -1;
public TouchImageView(Context context) {
super(context);
}
public TouchImageView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
setBackgroundColor(Color.BLACK);
setScaleType(ScaleType.FIT_CENTER); //在沒有獲得View尺寸來進(jìn)行initBitmap前,先通過這個(gè)進(jìn)行居中顯示
}
GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {//單擊事件
return super.onSingleTapConfirmed(e);
}
@Override
public boolean onDoubleTap(MotionEvent e) {//雙擊事件
doubleClickZoom();
return super.onDoubleTap(e);
}
});
/**
* 設(shè)置TouchImageView的尺寸,在onCreate階段展示圖片,需要手動設(shè)置
* @param h
* @param w
*/
public void setViewSize(int h, int w){
System.out.println("setViewSize");
viewHeight = h;
viewWidth = w;
//獲得View尺寸后初始化圖片
initBitmap();
}
//初始化圖片
public void initBitmap(){
System.out.println("initBitmap");
preMatrix.reset(); //必須有,否則會受上一張圖片的影響
//縮放到寬與控件平齊
float scaleX = (float) viewWidth / imgWidth;
float scaleY = (float) viewHeight / imgHeight;
float defaultScale = scaleX < scaleY ? scaleX : scaleY;
preMatrix.postScale(defaultScale, defaultScale);
//平移到居中
float tranY = (viewHeight - imgHeight*defaultScale)/2;
preMatrix.postTranslate(0, tranY);
//獲取最大最小縮放尺寸
maxHeight = imgHeight * defaultScale * 3;
maxWidth = imgWidth * defaultScale * 3;
minHeight = imgHeight* defaultScale / 2;
minWidth = imgWidth * defaultScale / 2;
setScaleType(ScaleType.MATRIX);
setImageMatrix(preMatrix);
}
@Override
public void setImageBitmap(Bitmap bm){ //先執(zhí)行這個(gè)方法,再執(zhí)行initBitmap
super.setImageBitmap(bm);
imgWidth = bm.getWidth();
imgHeight = bm.getHeight();
System.out.println("setImageBitmap: imgWidth="+imgWidth);
System.out.println("setImageBitmap: TimgHeightH="+imgHeight);
if(viewHeight!=0 && viewWidth!=0) {
initBitmap();
} else{
viewHeight = getHeight();
viewWidth = getWidth();
if(viewHeight!=0 && viewWidth!=0){
initBitmap();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event){
gestureDetector.onTouchEvent(event);
switch(event.getAction() & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_DOWN:
matrix.set(preMatrix); //時(shí)序問題,doubleClickZoom方法中設(shè)置該項(xiàng)時(shí),前面會通過ACTION_UP將preMatrix設(shè)置成matrix,故提前設(shè)置該項(xiàng)
xDown = event.getX();
yDown = event.getY();
break;
case MotionEvent.ACTION_POINTER_DOWN:
isOnePointer = false;
if(!isMovePic){ // 第二根手指放下時(shí)可能已經(jīng)進(jìn)入了移動模式,已經(jīng)進(jìn)入移動模式則不激發(fā)縮放模式
isZoomPic = true;
}
if(event.getPointerCount() == 2) { // 兩根手指才進(jìn)行測量與計(jì)算距離,避免縮放過程中第三根手指放下激發(fā)重新測量
float downX1 = event.getX(0);
float downX2 = event.getX(1);
float downY1 = event.getY(0);
float downY2 = event.getY(1);
float xDistance = Math.abs(downX1-downX2);
float yDistance = Math.abs(downY1-downY2);
xMid = (downX1+downX2)/2;
yMid = (downY1+downY2)/2;
beginDistance = Math.sqrt(xDistance * xDistance + yDistance * yDistance);
}
break;
case MotionEvent.ACTION_MOVE:
if(event.getPointerCount() == 1) {
if(!isZoomPic) {
float moveX = event.getX();
float moveY = event.getY();
if(Math.abs(moveX - xDown)>10 || Math.abs(moveY - yDown)>10){
isZoomPic = false;
isMovePic = true;
}
}
isOnePointer = true;
}
else if(event.getPointerCount() == 2){
if(!isMovePic){
isZoomPic = true;
isMovePic = false;
}
isOnePointer = false;
}
else {
isZoomPic = false;
isMovePic = false;
isOnePointer = false;
}
if(isZoomPic && !isOnePointer) { //雙指縮放圖片
matrix.set(preMatrix);
float moveX1 = event.getX(0);
float moveX2 = event.getX(1);
float moveY1 = event.getY(0);
float moveY2 = event.getY(1);
float xDistance2 = Math.abs(moveX1-moveX2);
float yDistance2 = Math.abs(moveY1-moveY2);
//移動后兩指間的距離
double moveDistance = Math.sqrt(xDistance2 * xDistance2 + yDistance2 * yDistance2);
float zoomScale = (float) ((float)moveDistance / beginDistance);
matrix.postScale(zoomScale, zoomScale,xMid,yMid);
float[] values = new float[9];
//縮放最大最小的限制
limitZoomSize(values);
//放大或縮小時(shí),若高或?qū)挍]有填充完控件則過程需要居中
placeXCenter(values);
placeYCenter(values);
//檢查圖片邊緣有沒離開邊界,返回需要移動的位移量
float dx = checkXBorder(values, 0);
float dy = checkYBorder(values, 0);
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);
}
if(isMovePic && isOnePointer){ //單手指移動圖片
matrix.set(preMatrix);
float moveX = event.getX();
float moveY = event.getY();
//計(jì)算位移量
float dy = moveY - yDown;
float dx = moveX - xDown;
float[] values = new float[9];
quitViewPicture(values,dx,dy);
//檢查圖片邊緣有沒離開邊界,返回需要移動的位移量
dx = checkXBorder(values, dx);
dy = checkYBorder(values, dy);
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);
}
break;
case MotionEvent.ACTION_UP:
float[] values = new float[9];
if(tryQuit) {
checkQuit(values);
} else {
//縮小離開屏幕邊緣時(shí),松手自動放大到屏幕邊緣
autoMatchParent(values);
}
preMatrix.set(matrix);
isOnePointer = true;
isMovePic = false;
isZoomPic = false;
//將前面居中過程產(chǎn)生的位移量置0
tempdy = 0;
tempdx = 0;
tryQuit = false;
break;
}
return true;
}
//下滑退出操作,松手時(shí)是否達(dá)到可以退出的條件(位移量)
public void checkQuit(float[] values) {
matrix.getValues(values);
if(canQuit) {
if(preWidth != 0 && preHeight != 0 && xLocation != -1 && yLocation != -1) {
setBackgroundColor(Color.argb(0, 0, 0, 0));
float toScale;
if(preWidth > preHeight) {
toScale = preWidth / (values[Matrix.MSCALE_X]*imgWidth);
} else {
toScale = preHeight / (values[Matrix.MSCALE_Y]*imgHeight);
}
matrix.getValues(values);
float dx = xLocation - values[Matrix.MTRANS_X];
float dy = yLocation - values[Matrix.MTRANS_Y];
setMyAnimation(dx,dy,toScale,toScale);
}
else {
AcManager.getAcManager().popActivity((Activity)context);
}
}
else {
float scale = getWidth()/(values[Matrix.MSCALE_X] * imgWidth);
matrix.postScale(scale,scale);
placeXCenter(values);
placeYCenter(values);
setImageMatrix(matrix);
}
}
//當(dāng)圖片未縮放狀態(tài)時(shí),向下滑可以退出圖片瀏覽
public void quitViewPicture(float[] values, float dx, float dy){
matrix.getValues(values);
float beforeZoom = values[Matrix.MTRANS_Y];
if((imgWidth * values[Matrix.MSCALE_X] <= getWidth()) && (imgHeight * values[Matrix.MSCALE_Y] <= getHeight()) ) {
if(dy > 0) {
if(dy>5) { //防止雙擊放大時(shí)被認(rèn)為是想退出
tryQuit = true;
}
// 設(shè)置背景色透明程度
int alpha = 255- (int) ((255 * dy)/getHeight());
setBackgroundColor(Color.argb(alpha, 0, 0, 0));
//float scale = values[Matrix.MSCALE_X] - dy * values[Matrix.MSCALE_X] / getHeight(); //目標(biāo)縮放尺寸
float scale = 1 - dy / getHeight(); //需要Post的縮放尺寸
matrix.postScale(scale, scale);
matrix.getValues(values);
float dyZoom = beforeZoom - values[Matrix.MTRANS_Y]; //縮小過程會產(chǎn)生一定回縮的位移
float dxZoom = getWidth()/2 -imgWidth * values[Matrix.MSCALE_X]/2 ;
matrix.postTranslate(dx + dxZoom, dy + dyZoom);
//placeXCenter(values);
if(dy > 200) {
canQuit = true;
} else {
canQuit = false;
}
}
}
}
//雙擊放大一倍
public void doubleClickZoom(){
matrix.set(preMatrix);
float[] values = new float[9];
matrix.getValues(values);
float currentWidth = values[Matrix.MSCALE_X] * imgWidth;
if(currentWidth > getWidth()+ 5) {
float scale = getWidth()/currentWidth;
matrix.postScale(scale,scale);
placeXCenter(values);
placeYCenter(values);
setImageMatrix(matrix);
preMatrix.set(matrix);
} else {
matrix.postScale(2.0f,2.0f);
matrix.getValues(values);
float dx = getWidth()/2 - (imgWidth*values[Matrix.MSCALE_X]/2 + values[Matrix.MTRANS_X]);
float dy = getHeight()/2 - (imgHeight*values[Matrix.MSCALE_Y]/2 + values[Matrix.MTRANS_Y]);
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);
preMatrix.set(matrix);
}
}
//縮小離開屏幕邊緣時(shí),松手自動放大到屏幕邊緣
public void autoMatchParent(float[] values) {
matrix.getValues(values);
float currentHeight = values[Matrix.MSCALE_Y] * imgHeight;
float currentWidth = values[Matrix.MSCALE_X] * imgWidth;
if(currentHeight < (getHeight()-5) && currentWidth < (getWidth()-5)) {
float scale = getWidth()/currentWidth;
matrix.postScale(scale,scale);
placeXCenter(values);
placeYCenter(values);
setImageMatrix(matrix);
}
}
//縮放最大最小的限制
public void limitZoomSize(float[] values) {
matrix.getValues(values);
if(values[Matrix.MSCALE_Y] * imgHeight > maxHeight || values[Matrix.MSCALE_X] * imgWidth > maxWidth) {
float scaleX = maxWidth / (imgWidth*values[Matrix.MSCALE_X]);
float scaleY = maxHeight / (imgHeight*values[Matrix.MSCALE_Y]);
matrix.postScale(scaleX, scaleY,xMid,yMid);
} else if(values[Matrix.MSCALE_Y] * imgHeight < minHeight || values[Matrix.MSCALE_X] * imgWidth < minWidth) {
float scaleX = minWidth / (imgWidth*values[Matrix.MSCALE_X]);
float scaleY = minHeight / (imgHeight*values[Matrix.MSCALE_Y]);
matrix.postScale(scaleX, scaleY,xMid,yMid);
}
}
//X方向上縮放過程中若未充滿控件寬度,那么居中縮放
public void placeXCenter(float[] values){
//獲得最新的values
matrix.getValues(values);
//圖片橫向能完全顯示時(shí),需要居中
if(imgWidth*values[Matrix.MSCALE_X] < getWidth() + 1) {
System.out.println("placeXCenter:橫向正在居中");
float dx = getWidth()/2 - (imgWidth*values[Matrix.MSCALE_X]/2 + values[Matrix.MTRANS_X]);
tempdx = dx;
matrix.postTranslate(dx, 0);
}
else {
matrix.postTranslate(tempdx, 0); //圖像的橫向向從未充滿屏幕到充滿屏幕過程中,由于居中會產(chǎn)生的一定位移tempdx,需要補(bǔ)上,否則會跳變
}
}
//Y方向上縮放過程中若未充滿控件寬度,那么居中縮放
public void placeYCenter(float[] values){
//獲得最新的values
matrix.getValues(values);
//圖片縱向能完全顯示時(shí),需要居中
if(imgHeight*values[Matrix.MSCALE_Y] < getHeight() + 1) {
System.out.println("placeYCenter:縱向正在居中");
float dy = getHeight()/2 - (imgHeight*values[Matrix.MSCALE_Y]/2 + values[Matrix.MTRANS_Y]);
tempdy = dy;
matrix.postTranslate(0, dy);
} else {
matrix.postTranslate(0, tempdy); //圖像的縱向從未充滿屏幕到充滿屏幕過程中,由于居中會產(chǎn)生的一定位移tempdy,需要補(bǔ)上,否則會跳變
}
}
//X方向上的邊緣檢測
public float checkXBorder(float[] values, float dx) {
//獲得最新的values
matrix.getValues(values);
if(imgWidth*values[Matrix.MSCALE_X] < getWidth()){ //圖片寬度小于控件寬度時(shí),不移動
dx = 0;
}
else if(values[Matrix.MTRANS_X]+dx>0) { //圖片右移后若離開控件左邊緣,那么將圖片移動對齊到左邊緣
dx = -values[Matrix.MTRANS_X];
}
else if(imgWidth*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X]+ dx< getWidth()){ //圖片左移后若離開控件右邊緣,那么將圖片移動對齊到右邊緣
dx = - imgWidth*values[Matrix.MSCALE_X] + getWidth() - values[Matrix.MTRANS_X];
}
return dx;
}
//Y方向上的邊緣檢測
public float checkYBorder(float[] values, float dy) {
//獲得最新的values
matrix.getValues(values);
if(imgHeight*values[Matrix.MSCALE_Y] < getHeight()){ //圖片高度小于控件寬度時(shí),不移動
dy = 0;
}
else if(values[Matrix.MTRANS_Y]+dy>0) { //圖片下移后若離開控件上邊緣,那么將圖片移動對齊到上邊緣
dy = -values[Matrix.MTRANS_Y];
}
else if(imgHeight * values[Matrix.MSCALE_Y] + values[Matrix.MTRANS_Y] + dy < getHeight()){ //圖片上移后若離開控件下邊緣,那么將圖片移動對齊到下邊緣
dy = - imgHeight*values[Matrix.MSCALE_Y] + getHeight() - values[Matrix.MTRANS_Y];
}
return dy;
}
/**
* 設(shè)置退出動畫需要縮放到的位置 , 需要設(shè)置才有這個(gè)效果
* @param x 相對屏幕的絕對位置坐標(biāo)x
* @param y 相對屏幕的絕對位置坐標(biāo)y
* @param height
* @param width
*/
public void setQuitAnimation(int x, int y, int height, int width) {
preWidth = width;
preHeight = height;
xLocation = x;
yLocation = y;
}
public void setMyAnimation (float toTranslateX, float toTranslateY, float toScaleX, float toScaleY) {
PropertyValuesHolder translateX = PropertyValuesHolder.ofFloat("translateX", 0.0f,toTranslateX);
PropertyValuesHolder translateY = PropertyValuesHolder.ofFloat("translateY", 0.0f,toTranslateY);
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f,toScaleX);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f,toScaleY);
ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(translateX,translateY,scaleX,scaleY);
animator.addUpdateListener ( new MyAnimatorListener ( getImageMatrix() ) );
animator.setDuration ( 150 );
animator.setInterpolator ( new LinearInterpolator () );
animator.setStartDelay ( 0 );
animator.start ();
animator.addListener(new AnimatorListener(){
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
AcManager.getAcManager().popActivity((Activity)context);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
class MyAnimatorListener implements AnimatorUpdateListener {
private Matrix mMatrix;
public MyAnimatorListener(Matrix matrix) {
mMatrix = new Matrix(matrix);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float dx = (float) animation.getAnimatedValue("translateX");
float dy = (float) animation.getAnimatedValue("translateY");
float scaleX = (float) animation.getAnimatedValue("scaleX");
float scaleY = (float) animation.getAnimatedValue("scaleY");
Matrix matrix = new Matrix(mMatrix);
float[] values = new float[9];
matrix.getValues(values);
float beforeX = values[Matrix.MTRANS_X];
float beforeY = values[Matrix.MTRANS_Y];
matrix.postScale(scaleX,scaleY);
matrix.getValues(values);
matrix.postTranslate(beforeX - values[Matrix.MTRANS_X] , beforeY - values[Matrix.MTRANS_Y]);
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);
}
}
/**
* 獲取當(dāng)前圖片的高
* @return
*/
public float getCurrentImageHeight(){
float[] values = new float[9];
matrix.getValues(values);
return imgHeight*values[Matrix.MSCALE_Y];
}
/**
* 獲取當(dāng)前圖片的寬
* @return
*/
public float getCurrentImageWidth(){
float[] values = new float[9];
matrix.getValues(values);
return imgWidth*values[Matrix.MSCALE_X];
}
}
圖片退出動畫
然后如果要在onCreate里設(shè)置圖片的話,由于此時(shí)是獲得的ImageView的尺寸是0,所以要在Activity的onCreate中用ViewTreeObserver來獲取尺寸:
ViewTreeObserver viewTreeObserver = pic.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
pic.setViewSize(pic.getHeight(),pic.getWidth());
}
});
如果要設(shè)置下滑退出,縮回到原來小圖片位置的動畫,需要在Activity中設(shè)置此項(xiàng):
//xLocation和yLocation是原來的小圖片相對于屏幕的坐標(biāo)
//preHeight和preWidth是小圖片的高和寬
pic.setQuitAnimation(xLocation, yLocation, preHeight, preWidth);
由于傳入的是相對屏幕的坐標(biāo),所以TouchImageView的大小也要是整個(gè)屏幕,這樣動畫才會跑到正確的位置。在TouchImageView對應(yīng)的Activity中設(shè)置:
getWindow().setFlags(WindowManager.LayoutParams. FLAG_FULLSCREEN , WindowManager.LayoutParams. FLAG_FULLSCREEN);
另外,需要把TouchImageView對應(yīng)的Activity的背景色設(shè)置為透明,在AndroidManifest.xml文件中對該Activity設(shè)置:
android:theme="@android:style/Theme.Translucent.NoTitleBar"
最后最后,其實(shí)代碼中已經(jīng)有比較多的注釋了,大家可以看看。希望通過寫文章,能加深自己的理解以及發(fā)現(xiàn)自己的錯誤,在學(xué)習(xí)的道路上不斷前行。