前言
我们在对图片查看的时候,经常会选择开源项目PhotoView去实现,现在进行对PhotoView的一个简单实现。
自定义控件准备
自己实现图片查看,并且可以随手势放大缩小,用的的核心类如下
Matrix
进行图片处理,经常会使用到Matrix,首先稍微介绍下这个Matrix:3维矩阵,内部存储:new Float[9]
{
MSCALE_X, MSKEW_X, MTRANS_X,
MSKEW_Y, MSCALE_Y, MTRANS_Y,
MPERSP_0, MPERSP_1, MPERSP_2
};
可以实现:
Translate 平移变换
Rotate 旋转变换
Scale 缩放变换
Skew 错切变换
简单操作:
你想要设置matrix的偏移量为200,100
Matrix transMatrix = new Matrix();
float[] values = new float[] { 1.0, 0, 200, 0, 1.0, 100, 0, 0, 1.0 };
transMatrix.setValues(values);
也可以这样:
Matrix transMatrix = new Matrix();
transMatrix.postTranslate(200, 100);
获取缩放级别:
public final float getScale() {
scaleMatrix.getValues(matrixValues);
return matrixValues[Matrix.MSCALE_X];
}
想详细了解,可以观看这里:Matrix详解
GestureDetector
识别手势解析类,我们需要在自定义控件的onTouch()或者onTouchEvent()方法中,调用GestureDetector.onTouchEvent(),并把MotionEvent传递进去即可。对于各种手势的回调,可以通过GestureDetector中的接口OnGestureListener来完成。
@Override
public boolean onTouch(View v, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
}
本例使用到的接口回调方法:onDoubleTap,双击时,最后一次点击调用,详细在demo查看
ScaleGestureDetector
处理缩放的工具类,用法与GestureDetector类似,需要在自定义控件的onTouch()或者onTouchEvent()方法中,调用ScaleGestureDetector .onTouchEvent(),并把MotionEvent传递进去即可。在ScaleGestureDetector 中的接口OnScaleGestureListener进行缩放的操作。
@Override
public boolean onTouch(View v, MotionEvent event) {
mScaleGestureDetector.onTouchEvent(event);
}
在OnScaleGestureListener的回调方法中,主要有三个方法:
onScale
缩放时。返回值代表本次缩放事件是否已被处理。如果已被处理,那么detector就会重置缩放事件;如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,直到被处理为止。因此,它常用在判断只有缩放值达到一定数值时才进行缩放
onScaleBegin
缩放开始。该detector是否处理后继的缩放事件。返回false时,不会执行onScale()。
onScaleEnd
缩放结束时。
开发之路
先上效果图:
既然是做对图片查看功能的自定义控件,这里我们选择继承已有控件ImageView
手势缩放实现
主要要实现的功能:当图片加载时,将图片在屏幕中居中;图片宽或高大于屏幕的,缩小至屏幕大小;自由对图片进行方法或缩小。下面是代码,注释相当详细,相信你可以看懂的
public class ZoomImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
View.OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener {
// 检测两个手指在屏幕上做缩放的手势工具类
private ScaleGestureDetector mScaleGestureDetector;
private float scale;
// 图片缩放工具操作类Matrix
private Matrix mScaleMatrix;
private float[] matrixValues;
// 图片放大的最大值
public static final float SCALE_MAX = 10.0f;
//初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于0
private float initScale = 1.0f;
// 是否是初次加载
private boolean once = true;
private RectF matrixRectF;
//记录上次触摸点个数
int lastPointerCount;
private boolean isCanDrag;
private float mLastX;
private float mLastY;
private boolean isCheckLeftAndRight;
private boolean isCheckTopAndBottom;
private double mTouchSlop;
private GestureDetector mGestureDetector;
boolean isAutoScale;
//自动缩放中的节点
private float SCALE_MID = 2f;
public ZoomImageView(Context context) {
this(context, null);
}
public ZoomImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mScaleMatrix = new Matrix();
matrixValues = new float[9];
mScaleGestureDetector = new ScaleGestureDetector(context, this);
//获得的是触发移动事件的最短距离,如果小于这个距离就不触发移动控件
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
super.setScaleType(ScaleType.MATRIX);
this.setOnTouchListener(this);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = getScale();
// 前一个伸缩事件至当前伸缩事件的伸缩比率
float scaleFactor = detector.getScaleFactor();
if (getDrawable() == null) {
return true;
}
//缩放的范围控制
if ((scale < SCALE_MAX && scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){
//最大值最小值判断
if (scaleFactor * scale <initScale){
scaleFactor = initScale / scale;
}
if (scaleFactor * scale > SCALE_MAX){
scaleFactor = SCALE_MAX / scale;
}
mScaleMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
setImageMatrix(mScaleMatrix);
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector){
//消费事件
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) { }
@Override
public boolean onTouch(View v, MotionEvent event) {
return mScaleGestureDetector.onTouchEvent(event);
}
/**
* 当View加载完成时可能通过OnGlobalLayoutListener监听,在布局加载完成后获得一个view的宽高。
*/
@Override
public void onGlobalLayout() {
if (once){
Drawable d = getDrawable();
if (d == null){
return;
}
// 获取控件的宽度和高度
int width = getWidth();
int height = getHeight();
// 获取到ImageView对应图片的宽度和高度
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(dw * 1.0f /width,dh*1.0f/height);
}
initScale = scale;
// 将图片移动到手机屏幕的中间位置
mScaleMatrix.postTranslate((width - dw) / 2,(height - dh) /2);
mScaleMatrix.postScale(scale,scale,getWidth()/2,getHeight()/2);
setImageMatrix(mScaleMatrix);
once = false;
}
}
/**
* 当view被附着到一个窗口时触发
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
/**
* 当view离开附着的窗口时触发
*/
@SuppressWarnings("deprecation")
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
/**
* 获取当前的缩放比例
* @return
*/
public float getScale() {
mScaleMatrix.getValues(matrixValues);
// 变化的倍数
return matrixValues[Matrix.MSCALE_X];
}
}
我们在代码中设置了中心,但是这样会导致图片的位置的变化,最终导致,图片宽高大于屏幕时,图片与屏幕间出现白边;图片小于屏幕,但是不居中。
@Override
public boolean onScale(ScaleGestureDetector detector) {
......
if ((scale < SCALE_MAX && scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){
......
mScaleMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
setImageMatrix(mScaleMatrix);
}
return true;
}
所以,我们在缩放的时候需要手动控制下范围,
/**
* 在缩放时,进行图片显示范围的控制
*/
private void checkBorderAndCenterWhenScale() {
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();
}
mScaleMatrix.postTranslate(deltaX,deltaY);
}
/**
* 根据当前图片的Matrix获取图片的范围
* @return
*/
public RectF getMatrixRectF() {
Matrix matrix = mScaleMatrix;
RectF rect = new RectF();
Drawable d = getDrawable();
if (d != null){
rect.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
matrix.mapRect(rect);
}
return rect;
}
在onScale里面记得调用
@Override
public boolean onScale(ScaleGestureDetector detector) {
...
//缩放的范围控制
if ((scale < SCALE_MAX && scaleFactor > 1.0f)||(scale > initScale && scaleFactor <1.0f) ){
checkBorderAndCenterWhenScale();
}
return true;
}
自由的进行移动
在图片长或宽大于屏幕,我们设置图片可以移动。
首先我们需要拿到触摸点的个数,
int pointerCount = event.getPointerCount();
然后求出多个触摸点的平均值,设置给我们的mLastX , mLastY ,具体代码如下
@Override
public boolean onTouch(View v, MotionEvent event) {
RectF rectF = getMatrixRectF();
mScaleGestureDetector.onTouchEvent(event);
mGestureDetector.onTouchEvent(event);
float x = 0,y = 0;
// 拿到触摸点的个数
int pointerCount = event.getPointerCount();
// 得到多个触摸点的x与y均值
for (int i = 0; i < pointerCount; i++) {
x += event.getX();
y += event.getY();
}
x = x / pointerCount;
y = y / pointerCount;
//每当触摸点发生变化时,重置mLasX , mLastY
if (pointerCount != lastPointerCount){
isCanDrag = false;
mLastX = x;
mLastY = y;
}
lastPointerCount = pointerCount;
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mLastX;
float dy = y - mLastY;
if (!isCanDrag){
isCanDrag = isCanDrag(dx,dy);
}
if (isCanDrag){
if (getDrawable() != null){
isCheckLeftAndRight = isCheckTopAndBottom = true;
// 如果宽度小于屏幕宽度,则禁止左右移动
if (rectF.width() < getWidth()){
dx = 0;
isCheckLeftAndRight = false;
}
// 如果高度小于屏幕高度,则禁止上下移动
if (rectF.height() < getHeight()){
dy = 0;
isCheckTopAndBottom = false;
}
if (rectF.left == 0 && dx > 0) {
getParent().requestDisallowInterceptTouchEvent(false);
}
if (rectF.right == getWidth() && dx < 0) {
getParent().requestDisallowInterceptTouchEvent(false);
}
mScaleMatrix.postTranslate(dx,dy);
checkMatrixBounds();
setImageMatrix(mScaleMatrix);
}
}
mLastX = x;
mLastY = y;
if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_DOWN:
rectF = getMatrixRectF();
if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
lastPointerCount = 0;
}
return true;
}
/**
* 移动时,进行边界判断,主要判断宽或高大于屏幕的
*/
private void checkMatrixBounds() {
RectF rect = getMatrixRectF();
float deltaX = 0,deltaY = 0;
float viewWidth = getWidth();
float viewHeight = getHeight();
// 判断移动或缩放后,图片显示是否超出屏幕边界
if (rect.top >0 && isCheckTopAndBottom){
deltaY = -rect.top;
}
if (rect.bottom < viewHeight && isCheckTopAndBottom){
deltaY = viewHeight - rect.bottom;
}
if (rect.left > 0 && isCheckLeftAndRight){
deltaX = -rect.left;
}
if (rect.right < viewWidth && isCheckLeftAndRight){
deltaX = viewWidth - rect.right;
}
mScaleMatrix.postTranslate(deltaX,deltaY);
}
/**
* 是否是推动行为
* @param dx
* @param dy
* @return
*/
private boolean isCanDrag(float dx, float dy) {
return Math.sqrt((dx * dx) + (dy *dy)) >= mTouchSlop;
}
双击放大与缩小
这个需要为GestureDetector设置监听器OnGestureListener,由于OnGestureListener默认要实现的方法很多,我们只需要onDoubleTap一个方法实现双击后的操作,所以我们实现一个SimpleOnGestureListener,重写onDoubleTap方法
public ZoomImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mScaleMatrix = new Matrix();
matrixValues = new float[9];
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
// private boolean isAutoScale;
@Override
public boolean onDoubleTap(MotionEvent e) {
// 只缩放一次
// if (isAutoScale == true){
// return true;
// }
float x = e.getX();
float y = e.getY();
//如果是小于2的,我们双击直接到变为原图的2倍
if (getScale() < SCALE_MID){
ZoomImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MID, x, y),16);
// isAutoScale = true;
}else if (getScale() >= SCALE_MID && getScale() <SCALE_MAX){
ZoomImageView.this.postDelayed(
new AutoScaleRunnable(SCALE_MAX, x, y), 16);
// isAutoScale = true;
}else{
//还原
ZoomImageView.this.postDelayed(
new AutoScaleRunnable(initScale, x, y), 16);
// isAutoScale = true;
}
return true;
}
});
//获得的是触发移动事件的最短距离,如果小于这个距离就不触发移动控件
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
super.setScaleType(ScaleType.MATRIX);
this.setOnTouchListener(this);
}
这里有个自动缩放的任务
/**
* 自动缩放的任务
*/
private class AutoScaleRunnable implements Runnable{
static final float BIGGER = 1.07f;
static final float SMALLER = 0.93f;
private float mTargetScale;
private float tmpScale;
/**
* 缩放的中心
*/
private float x;
private float y;
/**
* 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小
*
* @param targetScale
*/
public AutoScaleRunnable(float targetScale, float x, float y) {
this.mTargetScale = targetScale;
this.x = x;
this.y = y;
if (getScale() < mTargetScale){
tmpScale = BIGGER;
}else{
tmpScale = SMALLER;
}
}
@Override
public void run() {
//进行缩放
mScaleMatrix.postScale(tmpScale,tmpScale,x,y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
float currentScale = getScale();
//如果值在合法范围内,继续缩放
if (((tmpScale > 1f) && (currentScale < mTargetScale))|| ((tmpScale <1f) && (mTargetScale < currentScale))){
ZoomImageView.this.postDelayed(this,16);
}else{//设置为目标的缩放比例
float deltaScale = mTargetScale / currentScale;
mScaleMatrix.postScale(deltaScale , deltaScale , x , y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
isAutoScale = false;
}
}
}
代码中注释比较详细,如有需要请下载demo查阅