图片预览功能几乎是每个APP里面都要用到的,看似简单,做完这个小demo你一定可以熟悉View的各种手势相关的操作。 需要实现的功能有手势缩放,双击图片缩放,放大后滑动查看,惯性滑动等。 通过对功能的分析可知我们需要的工具类有:Matrix(实现图片的移动等一些列变换),GestureDetector(实现对滑动、单击、双击、fling等手势处理),ScaleGestureDetector(实现对缩放动作的处理) 准备工作:首先想要让着两个手势处理的类起到作用,需要复写OnTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean retVal = mScaleGestureDetector.onTouchEvent(event);
retVal = mGestureDetector.onTouchEvent(event) || retVal;
return retVal || super.onTouchEvent(event);
}
而这两个手势处理类在初始化的时候分别需要传递各自监听手势的监听器, ScaleGestureDetector.OnScaleGestureListener和 GestureDetector.OnGestureListener 它们当中包含了各种手势的回调方法,如果我们不想监听每种手势可以使用其对应的OnSimple****GestureListener来选择性的复写某个方法。具体不熟悉的地方可以查看http://hukai.me/android-training-course-in-chinese/input/gestures/detector.html手势检测的Android官方培训课
第一个功能、实现图片经过缩放手势的放大缩小功能 首先我们要复写ScaleGestureDetector.OnScaleGestureListene的onScale方法
@Override
public boolean onScale(ScaleGestureDetector detector) {
//得到缩放中心
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
float targetScale = getScale(mScaleMatrix) * detector.getScaleFactor();
//不要让缩放比例太小或者太大
targetScale = Math.max(getMinScale(), Math.min(targetScale, getMaxScale()));
zoomTo(targetScale,focusX,focusY);
invalidate();
return true;
}
其中的getScale方法是通过matrix获得x方向上长度
public float getScale(Matrix matrix) {
//获取各个方向上的值
matrix.getValues(matrixValues);
return this.matrixValues[0];
}
关键变换是在zoomTo方法:
private void zoomTo(float scale, float centerX, float centerY) {
if(scale > this.getMaxScale()) {
scale = this.getMaxScale();
}
float oldScale = this.getScale(mScaleMatrix);
float deltaScale = scale / oldScale;
this.postScale(deltaScale, centerX, centerY);
this.center(true, true);
}
其中调用了postScale方法:
protected void postScale(float scale, float centerX, float centerY) {
this.mScaleMatrix.postScale(scale, scale, centerX, centerY);
this.setImageMatrix(this.mScaleMatrix);
}
这个方法就是将缩放变化的因数和缩放中心传递给了matrix实例,然后再将这个matrix设置给当前的ImageView。 细心点可能会发现,你怎么先在onScale方法里用缩放因子乘以当前的大小,又在zoomTo方法里除以它了,不是说最终传递给matrix的是一个缩放因子吗?确实通过detector.getScaleFactor()获得的缩放因子(放大时其值大于1,缩小时其值小于1)就是要传递给matrix的值。但是由于我在调用zoomTo方法之前限制了缩放的范围,我们的图片肯定不需要无限制的放大和缩小,所以我们要根据图片的实际大小得到一个最大和最小的缩放比例,来对图片的缩放进行限制。 我们来看下getMaxScale方法:
/**
* 获取最大缩放比例
* @return
*/
private float getMaxScale(){
if(mMaxScale == -1.0F){
this.mMaxScale = computeMaxZoom();
}
return this.mMaxScale;
}
如果还没有初始化最大缩放比例时就调用computeMaxZoom来计算最大缩放比例:
private float computeMaxZoom() {
Drawable drawable = getDrawable();
if(drawable == null){
return 1.0F;
}else {
float fw = this.mBitmapRect.width() / this.mContentRect.width();
float fh = this.mBitmapRect.height() / this.mContentRect.height();
float scale = Math.max(fw,fh) * 4.0F;
return scale;
}
}
其中的mBitmapRect表示的是图片实际大小的RectF矩形,mContentRect表示的是当前可视区域矩形的大小。 其计算的值并不是缩放因子,而是能够放大的最大比例。我们也可以通过这里的限制实现微信那样宽度只能缩放到屏幕大小。只需要让最大缩放值为this.mContentRect.width() / this.mBitmapRect.width()即可。 这时我们的手势缩放功能实现了,但是呢,你会发现缩放之后其位置不是居中的,这样会造成,一半超出了屏幕而另外一半还没有图片在,这当然是不合理的。所以在zoomTo方法中我们调用了center方法。
/**
* 变换之后将图片整体以屏幕中心为对称中心
*
*/
protected void center(boolean horizontal, boolean vertical) {
if (drawable != null) {
RectF rect = this.getCenter(mScaleMatrix,horizontal, vertical);
if (rect.left != 0.0F || rect.top != 0.0F) {
postTranslate(rect.left, rect.top);
}
}
}
可以看到我们是调用了getCenter方法得到了一个保存着移动到中心所需要移动的范围:
/**
* 获得图片缩放后要以屏幕中央为中心应该平移的范围
*
* @param horizontal
* @param vertical
* @return
*/
protected RectF getCenter(Matrix matrix, boolean horizontal, boolean vertical) {
Drawable drawable = this.getDrawable();
if (drawable == null) {
return new RectF(0.0F, 0.0F, 0.0F, 0.0F);
} else {
this.mCenterRect.set(0.0F, 0.0F, 0.0F, 0.0F);
//根据当前图片的Matrix获得当前图片的范围
RectF rect = this.getBitmapRect(matrix);
float height = rect.height();
float width = rect.width();
float deltaX = 0.0F;
float deltaY = 0.0F;
if (vertical) {
if (height < this.mContentRect.height()) {
//(this.mContentRect.height() - height) / 2.0F 居中显示时图片顶部距离屏幕顶部的距离
//rect.top - this.mContentRect.top 当前图片顶部与屏幕顶部的距离
deltaY = (this.mContentRect.height() - height) / 2.0F -
(rect.top - this.mContentRect.top);
} else if (rect.top > this.mContentRect.top) {//图片的下边超过屏幕下边界
deltaY = -(rect.top - this.mContentRect.top);
} else if (rect.bottom < this.mContentRect.bottom) {//图片的上边超过屏幕上边界
deltaY = this.mContentRect.bottom - rect.bottom;
}
}
/* ... 省略水平方向需要移动距离的获取,方法与竖直方向类似*/
this.mCenterRect.set(deltaX, deltaY, 0.0F, 0.0F);
return this.mCenterRect;
}
}
这样我们的手势缩放功能就完成了。
第二个功能 实现缩放后的图片滑动功能 这时我们要复写GestureDetector.OnGestureListener的onScroll方法
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//只有单点滑动和不是缩放操作才会产生滑动事件
return !(e1.getPointerCount() > 1 ||
e2.getPointerCount() > 1 ||
mScaleGestureDetector.isInProgress()) &&
ZoomImageView.this.onScroll( distanceX, distanceY);
}
我们实际在回调方法中调用了自己写的onScroll方法:
public boolean onScroll( float distanceX, float distanceY){
if (!this.canScroll()) {
return false;
} else {
this.scrollBy(-distanceX, -distanceY);
this.invalidate();
return true;
}
}
其中的scrollBy方法知识调用了matrix.postTranslate方法来进行移动,我们在滑动之前需要进行能否滑动的判断:
/**
* 只有当图片大小超过屏幕的宽高时才能滑动
* @return
*/
public boolean canScroll() {
if (this.getScale(mScaleMatrix) > 1.0F) {
return true;
} else {
RectF bitmapRect = this.getBitmapRect(mScaleMatrix);
return !this.mContentRect.contains(bitmapRect);
}
}
这样我们的ZoomImageView就可以进行滑动了。
第三个功能:双击放大达到最大后再缩小 这时我们要复写GestureDetector.OnGestureListener的onDoubleTap方法
@Override
public boolean onDoubleTap(MotionEvent e) {
float scale = getScale(mScaleMatrix);
float targetScale = onDoubleTapPost(scale,getMaxScale(),getMinScale());
targetScale = Math.max(getMinScale(), Math.min(targetScale, getMaxScale()));
ZoomImageView.this.zoomTo(targetScale,e.getX(),e.getY(),ZoomImageView.this.mDefaultAnimationDuration);
return true;
}
其中的onDoublePost方法是缩放的逻辑,如果放大后的结果大于了最大缩放比例则将其变为图片的初始状态 每次双击的缩放大小为this.mScaleFactor = (this.getMaxScale() - this.getMinScale()) / 2.0F + 0.5F;
private float onDoubleTapPost(float scale, float maxScale, float minScale) {
return (scale + this.mScaleFactor) <= maxScale?scale + this.mScaleFactor:minScale;
}
由于双击放大的倍数是比较大的,所以我们放大时要使用动画来淡化这种突然的变化:
/**
* 双击缩放时加上动画
* @param targetScale 缩放比例
* @param x 点击点的水平位置
* @param y 点击点的竖直位置
* @param durationMs 动画执行时间
*/
private void zoomTo(float targetScale, float x, float y, int durationMs) {
if(targetScale == getMaxScale()){
targetScale = 1.0F;
}
if(targetScale > getMaxScale()){
targetScale = getMaxScale();
}
float oldScale = getScale(mScaleMatrix);
Matrix m = new Matrix(mScaleMatrix);
m.postScale(targetScale,targetScale,x,y);
RectF rect = getCenter(m,true,true);
//直接把缩放的中心变成缩放之后刚图片中央刚好为屏幕中心
final float destX = x + rect.left * targetScale;
final float destY = y + rect.top * targetScale;
//动画的范围是在旧的缩放比例和新的缩放比例之间变化,通过将value值传递给zoomTo方法实现了有过渡的缩放。
ValueAnimator animation = ValueAnimator.ofFloat(oldScale, targetScale);
animation.setDuration(durationMs);
animation.setInterpolator(new DecelerateInterpolator(1.0F));
animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
ZoomImageView.this.zoomTo(value, destX, destY);
ZoomImageView.this.postInvalidateOnAnimation();
}
});
animation.start();
}
这样我们双击放大的功能就实现了。
剩下还有对onFling回调的处理,还有单击退出的处理就不在详细叙述了。一般图片预览都是放在一个Activity中的所以单击退出应该写一个回调来实现。
参考的文章:
http://blog.csdn.net/lmj623565791/article/details/39474553