</pre>现在基本上每个项目中都有大量的图片,用户习惯了双击放大,然后拖动平移它,如果还是简单的只能看下图那用户不得骂死,最近的项目中就有类似朋友圈的功能模块,查看用户发的图片必须要求能放大。</p><p>也许你会说这还不简单,github就有现成的,对photoView就是,而且很有名,但是项目中都遇到bug,我也亲手继承了一下,确实有,如果自己改源码的话,文件还是挺多 ,而且项目比较紧也没太多时间,所以我就有了自己写一个的想法,思路倒不复杂,但是细节需要处理好。</p><p>我们先来看别人做好的轮子,我很多也是参考了这篇文章<a target=_blank href="http://blog.youkuaiyun.com/lmj623565791/article/details/39474553" target="_blank">http://blog.youkuaiyun.com/lmj623565791/article/details/39474553</a>,博主很强大。</p><p>好了,我们进正题,如果我们直接继承View,自己写的东西就太多了,我们可以直接继承ImageView</p><p>自定义控件无非就是这几个步骤:</p><p>1,定义属性</p><p>2,布局中使用控件,设置属性</p><p>3,控件中得到属性,并作出逻辑处理,合理调用onMeasure,onDraw方法等</p><p>可是我们这个图片查看器没有必须做太多的指定属性,这里需要控制放大的最小/大比例,我们可以在代码中定义常量,如果需要修改的话,可以直接修改值,就省去了好多步骤</p><p>直接看核心类的代码</p><p><span style="font-family:Courier New;background-color: rgb(240, 240, 240);"></span><pre class="java" name="code">package com.example.test.view;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageView;
/**
* http://my.youkuaiyun.com/xingliuhua
*
* @author xingliuhua
*
*/
public class XlhZoomImageView extends ImageView implements OnTouchListener,
OnGlobalLayoutListener, OnScaleGestureListener {
private GestureDetector mDetector;// 双击手势
private ScaleGestureDetector mScaleGestureDetector;// 缩放手势
private Context mContext;
private float MIN_SCALE = 0.5f;// 最小的缩放比例
private float MAX_SCALE = 2.5f;// 最大的缩放比例
private boolean isFirst = true;// 是否是第一次(第一次呈现出来)
private Matrix mMatrix;// 缩放的矩阵
private float lastX, lastY;// 手指最后一次触碰的X,y坐标
private float initScale;// 刚开始的时候要把宽或高适应屏幕的宽高,此时的缩放比例
public XlhZoomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
public XlhZoomImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
init();
}
public XlhZoomImageView(Context context) {
super(context);
mContext = context;
init();
}
/**
* 初始化控件
*/
private void init() {
// 一定要设置成按矩阵方式缩放图片,不然肯定无效的,防止在xml中遗漏改属性,索性在这里统统设置一下
this.setScaleType(ScaleType.MATRIX);
mMatrix = new Matrix();
this.setOnTouchListener(this);
mDetector = new GestureDetector(mContext,
new SimpleOnGestureListener() {
/**
* 图片进行滑翔状态
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
//暂不处理
return super.onFling(e1, e2, velocityX, velocityY);
}
/**
* 对图片进行了双击
*/
@Override
public boolean onDoubleTap(MotionEvent e) {
boolean isScaled;// 已经是放大2倍
if (getScale() > initScale * 1.5f) {
// 如果已经放大了超过1.5倍就认为是放大了
isScaled = true;
} else {
isScaled = false;
}
if (isScaled) {
// 如果已经放大,双击当然就是要缩小了
mMatrix.postScale(initScale / getScale(), initScale
/ getScale(), e.getX(), e.getY());
// 采用mMatrix.setScale(sx, sy, px,
// py);方式发现缩放中心不是很准确,不知道什么原因,请大家一起探讨
} else {
mMatrix.postScale(2 * initScale / getScale(), 2
* initScale / getScale(), e.getX(),
e.getY());
}
updateView();
checkBorderAndCenter();
return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(mContext, this);
// 监听布局变化,我们只需要在刚开始的时候进行监听一下,把图片进行缩放和放到中心
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
/**
* 得到图片此时的缩放比例
*
* @return 图片此时的缩放比例
*/
private float getScale() {
float[] matrixValues = new float[9];
mMatrix.getValues(matrixValues);
return matrixValues[Matrix.MSCALE_X];
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// 先让它处理双击滑翔事件,如果是双击直接返回
if (mDetector.onTouchEvent(event)) {
return true;
}
// 放大事件好像每次都能响应,所以不能像上面的双一样进行返回,因为还要处理按下,滑动事件
mScaleGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
mMatrix.postTranslate(event.getX() - lastX, event.getY() - lastY);
break;
case MotionEvent.ACTION_UP:
break;
}
lastX = event.getX();
lastY = event.getY();
updateView();
checkBorderAndCenter();
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
// 取出此次缩放手势的缩放比例
float scaleFactor = detector.getScaleFactor();
// 如果此时的缩放比例已经是最大或最小,肯定就不用再缩放啦
if (getScale() < MIN_SCALE * initScale
|| getScale() > MAX_SCALE * initScale) {
return true;
}
// detector.getFocusY()是得到缩放时感兴趣的焦点Y坐标,x坐标同理。我们缩放图片时经常是想看图片的某一部分,比如MM的大眼睛or樱桃口~~
mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(),
detector.getFocusY());
updateView();
checkBorderAndCenter();
return true;
}
private void updateView() {
this.setImageMatrix(mMatrix);
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
// 缩放完成后要进行缩放比例的校验,不能大于最大缩放比或小于最小缩放比
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (getScale() >= MAX_SCALE * initScale) {
mMatrix.postScale(MAX_SCALE * initScale / getScale(), MAX_SCALE
* initScale / getScale(), detector.getFocusX(),
detector.getFocusY());
} else if (getScale() <= initScale) {
mMatrix.postScale(initScale / getScale(), initScale / getScale(),
detector.getFocusX(), detector.getFocusY());
}
updateView();
}
/**
* 根据矩阵得到图片所在的矩形,相当于给出了图片的边界
*
* @return
*/
private RectF getMatrixRectF() {
Matrix matrix = mMatrix;
RectF rect = new RectF();
Drawable d = getDrawable();
if (null != d) {
rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
// 把这个矩阵应用到矩形,并把它写回转换后的矩形。这是通过改变矩形的4个角来完成的,然后将其设置为点的界限
// 还有这个方法,也挺有用的matrix.mapRadius(radius)
matrix.mapRect(rect);
}
return rect;
}
/**
* 图片放大要检查边界是否出界,缩小时要判断是否在中心
*/
private void checkBorderAndCenter() {
RectF rect = getMatrixRectF();
float deltaX = 0;
float deltaY = 0;
int width = getWidth();
int height = getHeight();
// 如果宽或高大于屏幕,则控制范围
if (rect.width() >= width) {
if (rect.left > 0) {
deltaX = -rect.left;
}
if (rect.right < width) {
deltaX = width - rect.right;
}
}
if (rect.height() >= height) {
if (rect.top > 0) {
deltaY = -rect.top;
}
if (rect.bottom < height) {
deltaY = height - rect.bottom;
}
}
// 如果宽或高小于屏幕,则让其居中
if (rect.width() < width) {
deltaX = width * 0.5f - rect.right + 0.5f * rect.width();
}
if (rect.height() < height) {
deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();
}
mMatrix.postTranslate(deltaX, deltaY);
updateView();
if (rect.right <= getWidth() || rect.left >= 0) {
// 滑动viewpager,让viewpager来处理
getParent().requestDisallowInterceptTouchEvent(false);
} else {
// 滑动图片,自己来处理
getParent().requestDisallowInterceptTouchEvent(true);
}
}
/**
* 布局方式变化的时候调用,在这里我们只需要使用一次,就是刚进入的时候,图片是在左上角的, 我们要进行适度的缩放及平移,让他在中心
*/
@Override
public void onGlobalLayout() {
if (isFirst) {
fixImage();
isFirst = false;
}
}
private void fixImage() {
Drawable d = getDrawable();
if (d == null)
return;
int width = getWidth();
int height = getHeight();
// 拿到图片的宽和高
int dw = d.getIntrinsicWidth();
int dh = d.getIntrinsicHeight();
float scale = 1.0f;
// 如果图片的宽或者高大于屏幕,则缩放至屏幕的宽或者高
if (dw > width && dh <= height) {
scale = width * 1.0f / dw;
}
if (dh > height && dw <= width) {
scale = height * 1.0f / dh;
}
// 如果宽和高都大于屏幕,则让其按按比例适应屏幕大小
if (dw > width && dh > height) {
scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
}
// 如果宽和高都小于于屏幕,则让其按按比例适应屏幕大小
if (dw < width && dh < height) {
scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
}
initScale = scale;
mMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);
mMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
// 图片移动至屏幕中心
updateView();
getParent().requestDisallowInterceptTouchEvent(false);
}
}
代码并不是很多,我们一一来看
放大图片需要双击放大,或者缩小,只有放大状态下才能拖动,拖动到边缘就进入到下个图片(这里根据用户的需求,有的需求拖到边缘不能进入下个图片),那刚进入页面的时候,要把图片适应的宽和高,图片比屏幕小了显得有边缘,大了不能看全部,这当然都是不行的。
监听双击我们可以使用GestureDetector,里面传入SimpleOnGestureListener手势监听器,它里面有滑动,双击等事件的回调,不用我们再判断滑动的距离,或双击的时间差等,方便多了好多。
缩放手势我们可以使用ScaleGestureDetector来处理,它也为我们方便的提供了缩放的回调,我们可以方便的得到此次缩放的比例。
我们进入页面的时候需要getViewTreeObserver().addOnGlobalLayoutListener(this);来摆放图片的比例及位置,那必须是放到最中间了,这里有个回调方法
public void onGlobalLayout(),我们在这里判断如果是第一次进入,就得到屏幕和图片的实际宽高,计算出需要缩放的比例即可
这里有个非常重要方法,就是我们必须知道该图片的范围
/**
* 根据矩阵得到图片所在的矩形,相当于给出了图片的边界
*
* @return
*/
private RectF getMatrixRectF() {
Matrix matrix = mMatrix;
RectF rect = new RectF();
Drawable d = getDrawable();
if (null != d) {
rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
// 把这个矩阵应用到矩形,并把它写回转换后的矩形。这是通过改变矩形的4个角来完成的,然后将其设置为点的界限
// 还有这个方法,也挺有用的matrix.mapRadius(radius)
matrix.mapRect(rect);
}
return rect;
}
最后,我们的iamgeView大多数情况是放到viewpager中的,那事件的分发,拦截及处理我们必须深入的理解才能处理好冲突
我们在图片放大的状态且没超过屏幕范围的情况下,让iamgeview来处理,当没有放大或超过图片范围的情况下就交给viewpager来处理
getParent().requestDisallowInterceptTouchEvent(false);就可以控制父控件是不是拦截事件
试用下吧,oh,bug来了,拖动图片的时候偶尔会出异常,查了查资料终于找到了解决办法,我们不能直接用v4包中的viewpager,我们需要重写了
其实也简单,重写viewPager两个方法即可:
@Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
try {
return super.onInterceptTouchEvent(arg0);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent arg0) {
try {
return super.onTouchEvent(arg0);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
好了,就写到这吧,欢迎大家提出宝贵的修改意见,一起进步