热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android多点触控实现对图片放大缩小平移,惯性滑动等功能

这篇文章主要介绍了Android多点触控实现对图片放大缩小平移,惯性滑动等功能的相关资料,需要的朋友可以参考下

文章将在原有基础之上做了一些扩展功能:

1.图片的惯性滑动
2.图片缩放小于正常比例时,松手会自动回弹成正常比例
3.图片缩放大于最大比例时,松手会自动回弹成最大比例

实现图片的缩放,平移,双击缩放等基本功能的代码如下,每一行代码我都做了详细的注释

public class ZoomImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
  View.OnTouchListener , ViewTreeObserver.OnGlobalLayoutListener{
 /**
  * 缩放手势的监测
  */
 private ScaleGestureDetector mScaleGestureDetector;
 /**
  * 监听手势
  */
 private GestureDetector mGestureDetector;
 /**
  * 对图片进行缩放平移的Matrix
  */
 private Matrix mScaleMatrix;
 /**
  * 第一次加载图片时调整图片缩放比例,使图片的宽或者高充满屏幕
  */
 private boolean mFirst;
 /**
  * 图片的初始化比例
  */
 private float mInitScale;
 /**
  * 图片的最大比例
  */
 private float mMaxScale;
 /**
  * 双击图片放大的比例
  */
 private float mMidScale;

 /**
  * 是否正在自动放大或者缩小
  */
 private boolean isAutoScale;

 //-----------------------------------------------
 /**
  * 上一次触控点的数量
  */
 private int mLastPointerCount;
 /**
  * 是否可以拖动
  */
 private boolean isCanDrag;
 /**
  * 上一次滑动的x和y坐标
  */
 private float mLastX;
 private float mLastY;
 /**
  * 可滑动的临界值
  */
 private int mTouchSlop;
 /**
  * 是否用检查左右边界
  */
 private boolean isCheckLeftAndRight;
 /**
  * 是否用检查上下边界
  */
 private boolean isCheckTopAndBottom;


 public ZoomImageView(Context context) {
  this(context, null, 0);
 }

 public ZoomImageView(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
 }

 public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  //一定要将图片的ScaleType设置成Matrix类型的
  setScaleType(ScaleType.MATRIX);
  //初始化缩放手势监听器
  mScaleGestureDetector = new ScaleGestureDetector(context,this);
  //初始化矩阵
  mScaleMatrix = new Matrix();
  setOnTouchListener(this);
  mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
  //初始化手势检测器,监听双击事件
  mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
   @Override
   public boolean onDoubleTap(MotionEvent e) {
    //如果是正在自动缩放,则直接返回,不进行处理
    if (isAutoScale) return true;
    //得到点击的坐标
    float x = e.getX();
    float y = e.getY();
    //如果当前图片的缩放值小于指定的双击缩放值
    if (getScale() width && dhheight){
    //我们就应该将图片的高度缩小,缩小至控件的高度,计算方法同上
    scale = height * 1.0f / dh;
   }
   //如果图片的宽度小于控件宽度,高度小于控件高度时,我们应该将图片放大
   //比如图片宽度是控件宽度的1/2 ,图片高度是控件高度的1/4
   //如果我们将图片放大4倍,则图片的高度是和控件高度一样了,但是图片宽度就超出控件宽度了
   //因此我们应该选择一个最小值,那就是将图片放大2倍,此时图片宽度等于控件宽度
   //同理,如果图片宽度大于控件宽度,图片高度大于控件高度,我们应该将图片缩小
   //缩小的倍数也应该为那个最小值
   if ((dw  width && dh > height)){
    scale = Math.min(width * 1.0f / dw , height * 1.0f / dh);
   }

   //我们还应该对图片进行平移操作,将图片移动到屏幕的居中位置
   //控件宽度的一半减去图片宽度的一半即为图片需要水平移动的距离
   //高度同理,大家可以画个图看一看
   int dx = width/2 - dw/2;
   int dy = height/2 - dh/2;
   //对图片进行平移,dx和dy分别表示水平和竖直移动的距离
   mScaleMatrix.postTranslate(dx, dy);
   //对图片进行缩放,scale为缩放的比例,后两个参数为缩放的中心点
   mScaleMatrix.postScale(scale, scale, width / 2, height / 2);
   //将矩阵作用于我们的图片上,图片真正得到了平移和缩放
   setImageMatrix(mScaleMatrix);

   //初始化一下我们的几个缩放的边界值
   mInitScale = scale;
   //最大比例为初始比例的4倍
   mMaxScale = mInitScale * 4;
   //双击放大比例为初始化比例的2倍
   mMidScale = mInitScale * 2;
  }
 }

 /**
  * 获得图片当前的缩放比例值
  */
 private float getScale(){
  //Matrix为一个3*3的矩阵,一共9个值
  float[] values = new float[9];
  //将Matrix的9个值映射到values数组中
  mScaleMatrix.getValues(values);
  //拿到Matrix中的MSCALE_X的值,这个值为图片宽度的缩放比例,因为图片高度
  //的缩放比例和宽度的缩放比例一致,我们取一个就可以了
  //我们还可以 return values[Matrix.MSCALE_Y];
  return values[Matrix.MSCALE_X];
 }

 /**
  * 获得缩放后图片的上下左右坐标以及宽高
  */
 private RectF getMatrixRectF(){
  //获得当钱图片的矩阵
  Matrix matrix = mScaleMatrix;
  //创建一个浮点类型的矩形
  RectF rectF = new RectF();
  //得到当前的图片
  Drawable d = getDrawable();
  if (d != null){
   //使这个矩形的宽和高同当前图片一致
   rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
   //将矩阵映射到矩形上面,之后我们可以通过获取到矩阵的上下左右坐标以及宽高
   //来得到缩放后图片的上下左右坐标和宽高
   matrix.mapRect(rectF);
  }
  return rectF;
 }

 /**
  * 当缩放时检查边界并且使图片居中
  */
 private void checkBorderAndCenterWhenScale(){
  if (getDrawable() == null){
   return;
  }
  //初始化水平和竖直方向的偏移量
  float deltaX = 0.0f;
  float deltaY = 0.0f;
  //得到控件的宽和高
  int width = getWidth();
  int height = getHeight();
  //拿到当前图片对应的矩阵
  RectF rectF = getMatrixRectF();
  //如果当前图片的宽度大于控件宽度,当前图片处于放大状态
  if (rectF.width() >= width){
   //如果图片左边坐标是大于0的,说明图片左边离控件左边有一定距离,
   //左边会出现一个小白边
   if (rectF.left > 0){
    //我们将图片向左边移动
    deltaX = -rectF.left;
   }
   //如果图片右边坐标小于控件宽度,说明图片右边离控件右边有一定距离,
   //右边会出现一个小白边
   if (rectF.right = height){
   //如果上面出现小白边,则向上移动
   if (rectF.top > 0){
    deltaY = -rectF.top;
   }
   //如果下面出现小白边,则向下移动
   if (rectF.bottom  0){
    //向左偏移
    deltaX = -rectF.left;
   }
   //如果右边出现的白边
   if (rectF.right  0){
    //向上偏移
    deltaY = -rectF.top;
   }
   //如果下面出现白边
   if (rectF.bottom  mTargetScale){
    //设置为Smaller
    tempScale = SMALLER;
   }
  }
  @Override
  public void run() {
   //这里缩放的比例非常小,只是稍微比1大一点或者比1小一点的倍数
   //但是当每16ms都放大或者缩小一点点的时候,动画效果就出来了
   mScaleMatrix.postScale(tempScale, tempScale, x, y);
   //每次将矩阵作用到图片之前,都检查一下边界
   checkBorderAndCenterWhenScale();
   //将矩阵作用到图片上
   setImageMatrix(mScaleMatrix);
   //得到当前图片的缩放值
   float currentScale = getScale();
   //如果当前想要放大,并且当前缩放值小于目标缩放值
   //或者 当前想要缩小,并且当前缩放值大于目标缩放值
   if ((tempScale > 1.0f) && currentScale  mTargetScale){
    //每隔16ms就调用一次run方法
    postDelayed(this,16);
   }else {
    //current*scale=current*(mTargetScale/currentScale)=mTargetScale
    //保证图片最终的缩放值和目标缩放值一致
    float scale = mTargetScale / currentScale;
    mScaleMatrix.postScale(scale, scale, x, y);
    checkBorderAndCenterWhenScale();
    setImageMatrix(mScaleMatrix);
    //自动缩放结束,置为false
    isAutoScale = false;
   }
  }
 }

 /**
  * 这个是OnScaleGestureListener中的方法,在这个方法中我们可以对图片进行放大缩小
  */
 @Override
 public boolean onScale(ScaleGestureDetector detector) {
  //当我们两个手指进行分开操作时,说明我们想要放大,这个scaleFactor是一个稍微大于1的数值
  //当我们两个手指进行闭合操作时,说明我们想要缩小,这个scaleFactor是一个稍微小于1的数值
  float scaleFactor = detector.getScaleFactor();
  //获得我们图片当前的缩放值
  float scale = getScale();
  //如果当前没有图片,则直接返回
  if (getDrawable() == null){
   return true;
  }
  //如果scaleFactor大于1,说明想放大,当前的缩放比例乘以scaleFactor之后小于
  //最大的缩放比例时,允许放大
  //如果scaleFactor小于1,说明想缩小,当前的缩放比例乘以scaleFactor之后大于
  //最小的缩放比例时,允许缩小
  if ((scaleFactor > 1.0f && scale * scaleFactor  mInitScale){
   //边界控制,如果当前缩放比例乘以scaleFactor之后大于了最大的缩放比例
   if (scale * scaleFactor > mMaxScale + 0.01f){
    //则将scaleFactor设置成mMaxScale/scale
    //当再进行matrix.postScale时
    //scale*scaleFactor=scale*(mMaxScale/scale)=mMaxScale
    //最后图片就会放大至mMaxScale缩放比例的大小
    scaleFactor = mMaxScale / scale;
   }
   //边界控制,如果当前缩放比例乘以scaleFactor之后小于了最小的缩放比例
   //我们不允许再缩小
   if (scale * scaleFactor  getWidth() + 0.01f || rectF.height() > getHeight() + 0.01f){
     if (getParent() instanceof ViewPager){
      getParent().requestDisallowInterceptTouchEvent(true);
     }
    }
    break;
   case MotionEvent.ACTION_MOVE:
    //当图片处于放大状态时,禁止ViewPager拦截事件,将事件传递给图片,进行拖动
    if (rectF.width() > getWidth() + 0.01f || rectF.height() > getHeight() + 0.01f){
     if (getParent() instanceof ViewPager){
      getParent().requestDisallowInterceptTouchEvent(true);
     }
    }
    //得到水平和竖直方向的偏移量
    float dx = x - mLastX;
    float dy = y - mLastY;
    //如果当前是不可滑动的状态,判断一下是否是滑动的操作
    if (!isCanDrag){
     isCanDrag = isMoveAction(dx,dy);
    }
    //如果可滑动
    if (isCanDrag){
     if (getDrawable() != null){
      isCheckLeftAndRight = true;
      isCheckTopAndBottom = true;
      //如果图片宽度小于控件宽度
      if (rectF.width()  mTouchSlop;
 }
}

实现图片缩小后,松手回弹的效果

实现这个功能很简单,我们先添加一个mMinScale作为可缩小到的最小值,我们指定为初试比例的1/4

 /**
  * 最小缩放比例
  */
 private float mMinScale;
//在onGlobalLayout中进行初始化

 @Override
 public void onGlobalLayout() {
 ...
 //最小缩放比例为初试比例的1/4倍
 mMinScale = mInitScale / 4;
 ...
 }
//在onScale中,修改如下代码

 @Override
 public boolean onScale(ScaleGestureDetector detector) {
 ...
  if ((scaleFactor > 1.0f && scale * scaleFactor  mMinScale){

   //边界控制,如果当前缩放比例乘以scaleFactor之后小于了最小的缩放比例
   //我们不允许再缩小
   if (scale * scaleFactor 

这样我们的图片最小就可以缩放到初始化比例的1/4大小了,然后我们还需要添加一个松手后回弹至初试化大小的动画效果,然后我们需要在onTouch的ACTION_UP中添加如下代码

 @Override
 public boolean onTouch(View v, MotionEvent event) {
 ...
  case MotionEvent.ACTION_UP:
    //当手指抬起时,将mLastPointerCount置0,停止滑动
    mLastPointerCount = 0;
    //如果当前图片大小小于初始化大小
    if (getScale() 

现在我们看一下效果

实现图片放大后,松手回弹效果

这个功能实现起来和上面那个功能基本一致,大家可以先试着自己写一下。
同理,我们需要先定义一个mMaxOverScale作为放大到最大值后,还能继续放大到的值。

/**
  * 最大溢出值
  */
 private float mMaxOverScale;
//在onGlobalLayout中进行初始化

 @Override
 public void onGlobalLayout() {
 ...
 //最大溢出值为最大值的5倍,可以随意调
 mMaxOverScale = mMaxScale * 5;
 ...
 }
//在onScale中,修改如下代码

 @Override
 public boolean onScale(ScaleGestureDetector detector) {
 ...
  if ((scaleFactor > 1.0f && scale * scaleFactor  mMinScale){

   if (scale * scaleFactor > mMaxOverScale + 0.01f){

    scaleFactor = mMaxOverScale / scale;
   }
 ...
 }

这样当我们图片放大至最大比例后还可以继续放大,然后我们同样需要在onTouch中的ACTION_UP中添加自动缩小的功能

 case MotionEvent.ACTION_UP:
    //当手指抬起时,将mLastPointerCount置0,停止滑动
    mLastPointerCount = 0;
    //如果当前图片大小小于初始化大小
    if (getScale()  mMaxScale){
     //自动缩小至最大值
     post(new AutoScaleRunnable(mMaxScale,getWidth()/2,getHeight()/2));
    }
    break;

然后我们看一下效果

实现图片的惯性滑动

要实现图片的惯性滑动,我们需要借助VelocityTracker来帮我们检测当我们手指离开图片时的一个速度,然后根据这个速度以及图片的位置来调用Scroller的fling方法来计算惯性滑动过程中的x和y的坐标

 @Override
 public boolean onTouch(View v, MotionEvent event) {
 ...
  switch (event.getAction()){
   case MotionEvent.ACTION_DOWN:
    //初始化速度检测器
    mVelocityTracker = VelocityTracker.obtain();
    if (mVelocityTracker != null){
     //将当前的事件添加到检测器中
     mVelocityTracker.addMovement(event);
    }
    //当手指再次点击到图片时,停止图片的惯性滑动
    if (mFlingRunnable != null){
     mFlingRunnable.cancelFling();
     mFlingRunnable = null;
    }
    ...
  }
  ...
  case MotionEvent.ACTION_MOVE:
  ...
  //如果可滑动
    if (isCanDrag){
     if (getDrawable() != null){

      if (mVelocityTracker != null){
       //将当前事件添加到检测器中
       mVelocityTracker.addMovement(event);
      }
      ...
    }
    ...
  case MotionEvent.ACTION_UP:
    //当手指抬起时,将mLastPointerCount置0,停止滑动
    mLastPointerCount = 0;
    //如果当前图片大小小于初始化大小
    if (getScale()  mMaxScale){
     //自动缩小至最大值
     post(new AutoScaleRunnable(mMaxScale,getWidth()/2,getHeight()/2));
    }
    if (isCanDrag){//如果当前可以滑动
     if (mVelocityTracker != null){
      //将当前事件添加到检测器中
      mVelocityTracker.addMovement(event);
      //计算当前的速度
      mVelocityTracker.computeCurrentVelocity(1000);
      //得到当前x方向速度
      final float vX = mVelocityTracker.getXVelocity();
      //得到当前y方向的速度
      final float vY = mVelocityTracker.getYVelocity();
      mFlingRunnable = new FlingRunnable(getContext());
      //调用fling方法,传入控件宽高和当前x和y轴方向的速度
      //这里得到的vX和vY和scroller需要的velocityX和velocityY的负号正好相反
      //所以传入一个负值
      mFlingRunnable.fling(getWidth(),getHeight(),(int)-vX,(int)-vY);
      //执行run方法
      post(mFlingRunnable);
     }
    }
    break;
 case MotionEvent.ACTION_CANCEL:
    //释放速度检测器
    if (mVelocityTracker != null){
     mVelocityTracker.recycle();
     mVelocityTracker = null;
    }
    break;



/**
  * 惯性滑动
  */
 private class FlingRunnable implements Runnable{
  private Scroller mScroller;
  private int mCurrentX , mCurrentY;

  public FlingRunnable(Context context){
   mScroller = new Scroller(context);
  }

  public void cancelFling(){
   mScroller.forceFinished(true);
  }

  /**
   * 这个方法主要是从onTouch中或得到当前滑动的水平和竖直方向的速度
   * 调用scroller.fling方法,这个方法内部能够自动计算惯性滑动
   * 的x和y的变化率,根据这个变化率我们就可以对图片进行平移了
   */
  public void fling(int viewWidth , int viewHeight , int velocityX ,
       int velocityY){
   RectF rectF = getMatrixRectF();
   if (rectF == null){
    return;
   }
   //startX为当前图片左边界的x坐标
   final int startX = Math.round(-rectF.left);
   final int minX , maxX , minY , maxY;
   //如果图片宽度大于控件宽度
   if (rectF.width() > viewWidth){
    //这是一个滑动范围[minX,maxX],详情见下图
    minX = 0;
    maxX = Math.round(rectF.width() - viewWidth);
   }else{
    //如果图片宽度小于控件宽度,则不允许滑动
    minX = maxX = startX;
   }
   //如果图片高度大于控件高度,同理
   final int startY = Math.round(-rectF.top);
   if (rectF.height() > viewHeight){
    minY = 0;
    maxY = Math.round(rectF.height() - viewHeight);
   }else{
    minY = maxY = startY;
   }
   mCurrentX = startX;
   mCurrentY = startY;

   if (startX != maxX || startY != maxY){
    //调用fling方法,然后我们可以通过调用getCurX和getCurY来获得当前的x和y坐标
    //这个坐标的计算是模拟一个惯性滑动来计算出来的,我们根据这个x和y的变化可以模拟
    //出图片的惯性滑动
    mScroller.fling(startX,startY,velocityX,velocityY,minX,maxX,minY,maxY);
   }

  }

关于startX,minX,maxX做一个解释


我们从图中可以看出,当前图片可滑动的一个区间就是左边多出来的那块区间,所以minX和maxX代表的是区间的最小值和最大值,startX就是屏幕左边界的坐标值,我们可以想象成是startX在区间[minX,maxX]的移动。Y轴方向同理。

现在我们看一下效果

以上就是本文的全部内容,希望对大家学习Android软件编程有所帮助。


推荐阅读
author-avatar
_Terr1鄭x宜_F
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有