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() < mmidscale){ //进行自动放大 post(new autoscalerunnable(mmidscale,x,y)); }else{ //当前图片的缩放值大于初试缩放值,则自动缩小 post(new autoscalerunnable(minitscale,x,y)); } return true; } }); } /** * 当view添加到window时调用,早于ongloballayout,因此可以在这里注册监听器 */ @override protected void onattachedtowindow() { super.onattachedtowindow(); getviewtreeobserver().addongloballayoutlistener(this); } /** * 当view从window上移除时调用,因此可以在这里移除监听器 */ @override protected void ondetachedfromwindow() { super.ondetachedfromwindow(); getviewtreeobserver().removeglobalonlayoutlistener(this); } /** * 当布局树发生变化时会调用此方法,我们可以在此方法中获得控件的宽和高 */ @override public void ongloballayout() { //只有当第一次加载图片的时候才会进行初始化,用一个变量mfirst控制 if (!mfirst){ mfirst = true; //得到控件的宽和高 int width = getwidth(); int height = getheight(); //得到当前imageview中加载的图片 drawable d = getdrawable(); if(d == null){//如果没有图片,则直接返回 return; } //得到当前图片的宽和高,图片的宽和高不一定等于控件的宽和高 //因此我们需要将图片的宽和高与控件宽和高进行判断 //将图片完整的显示在屏幕中 int dw = d.getintrinsicwidth(); int dh = d.getintrinsicheight(); //我们定义一个临时变量,根据图片与控件的宽高比例,来确定这个最终缩放值 float scale = 1.0f; //如果图片宽度大于控件宽度,图片高度小于控件高度 if (dw>width && dh<height){ //我们需要将图片宽度缩小,缩小至控件的宽度 //至于为什么要这样计算,我们可以这样想 //我们调用matrix.postscale(scale,scale)时,宽和高都要乘以scale的 //当前我们的图片宽度是dw,dw*scale=dw*(width/dw)=width,这样就等于控件宽度了 //我们的高度同时也乘以scale,这样能够保证图片的宽高比不改变,图片不变形 scale = width * 1.0f / dw; } //如果图片的宽度小于控件宽度,图片高度大于控件高度 if (dw<width && dh>height){ //我们就应该将图片的高度缩小,缩小至控件的高度,计算方法同上 scale = height * 1.0f / dh; } //如果图片的宽度小于控件宽度,高度小于控件高度时,我们应该将图片放大 //比如图片宽度是控件宽度的1/2 ,图片高度是控件高度的1/4 //如果我们将图片放大4倍,则图片的高度是和控件高度一样了,但是图片宽度就超出控件宽度了 //因此我们应该选择一个最小值,那就是将图片放大2倍,此时图片宽度等于控件宽度 //同理,如果图片宽度大于控件宽度,图片高度大于控件高度,我们应该将图片缩小 //缩小的倍数也应该为那个最小值 if ((dw < width && dh < height) || (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 <width){ //我们将图片向右边移动 deltax = width - rectf.right; } } //上面是调整宽度,这是调整高度 if (rectf.height() >= height){ //如果上面出现小白边,则向上移动 if (rectf.top > 0){ deltay = -rectf.top; } //如果下面出现小白边,则向下移动 if (rectf.bottom < height){ deltay = height - rectf.bottom; } } //如果图片的宽度小于控件的宽度,我们要对图片做一个水平的居中 if (rectf.width() < width){ deltax = width/2f - rectf.right + rectf.width()/2f; } //如果图片的高度小于控件的高度,我们要对图片做一个竖直方向的居中 if (rectf.height() < height){ deltay = height/2f - rectf.bottom + rectf.height()/2f; } //将平移的偏移量作用到矩阵上 mscalematrix.posttranslate(deltax, deltay); } /** * 平移时检查上下左右边界 */ private void checkborderwhentranslate() { //获得缩放后图片的相应矩形 rectf rectf = getmatrixrectf(); //初始化水平和竖直方向的偏移量 float deltax = 0.0f; float deltay = 0.0f; //得到控件的宽度 int width = getwidth(); //得到控件的高度 int height = getheight(); //如果是需要检查左和右边界 if (ischeckleftandright){ //如果左边出现的白边 if (rectf.left > 0){ //向左偏移 deltax = -rectf.left; } //如果右边出现的白边 if (rectf.right < width){ //向右偏移 deltax = width - rectf.right; } } //如果是需要检查上和下边界 if (ischecktopandbottom){ //如果上面出现白边 if (rectf.top > 0){ //向上偏移 deltay = -rectf.top; } //如果下面出现白边 if (rectf.bottom < height){ //向下偏移 deltay = height - rectf.bottom; } } mscalematrix.posttranslate(deltax,deltay); } /** * 自动放大缩小,自动缩放的原理是使用view.postdelay()方法,每隔16ms调用一次 * run方法,给人视觉上形成一种动画的效果 */ private class autoscalerunnable implements runnable{ //放大或者缩小的目标比例 private float mtargetscale; //可能是bigger,也可能是smaller private float tempscale; //放大缩小的中心点 private float x; private float y; //比1稍微大一点,用于放大 private final float bigger = 1.07f; //比1稍微小一点,用于缩小 private final float smaller = 0.93f; //构造方法,将目标比例,缩放中心点传入,并且判断是要放大还是缩小 public autoscalerunnable(float targetscale , float x , float y){ this.mtargetscale = targetscale; this.x = x; this.y = y; //如果当前缩放比例小于目标比例,说明要自动放大 if (getscale() < mtargetscale){ //设置为bigger tempscale = bigger; } //如果当前缩放比例大于目标比例,说明要自动缩小 if (getscale() > 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 ||(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 < mmaxscale) || 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 < minitscale + 0.01f){ //计算方法同上 scalefactor = minitscale / scale; } //前两个参数是缩放的比例,是一个稍微大于1或者稍微小于1的数,形成一个随着手指放大 //或者缩小的效果 //detector.getfocusx()和detector.getfocusy()得到的是多点触控的中点 //这样就能实现我们在图片的某一处局部放大的效果 mscalematrix.postscale(scalefactor, scalefactor, detector.getfocusx(), detector.getfocusy()); //因为图片的缩放点不是图片的中心点了,所以图片会出现偏移的现象,所以进行一次边界的检查和居中操作 checkborderandcenterwhenscale(); //将矩阵作用到图片上 setimagematrix(mscalematrix); } return true; } /** * 一定要返回true */ @override public boolean onscalebegin(scalegesturedetector detector) { return true; } @override public void onscaleend(scalegesturedetector detector) { } @override public boolean ontouch(view v, motionevent event) { //当双击操作时,不允许移动图片,直接返回true if (mgesturedetector.ontouchevent(event)){ return true; } //将事件传递给scalegesturedetector mscalegesturedetector.ontouchevent(event); //用于存储多点触控产生的坐标 float x = 0.0f; float y = 0.0f; //得到多点触控的个数 int pointercount = event.getpointercount(); //将所有触控点的坐标累加起来 for(int i=0 ; i<pointercount ; i++){ x += event.getx(i); y += event.gety(i); } //取平均值,得到的就是多点触控后产生的那个点的坐标 x /= pointercount; y /= pointercount; //如果触控点的数量变了,则置为不可滑动 if (mlastpointercount != pointercount){ iscandrag = false; mlastx = x; mlasty = y; } mlastpointercount = pointercount; rectf rectf = getmatrixrectf(); switch (event.getaction()){ case motionevent.action_down: iscandrag = false; //当图片处于放大状态时,禁止viewpager拦截事件,将事件传递给图片,进行拖动 if (rectf.width() > 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() < getwidth()){ //左右不可滑动 dx = 0; //左右不可滑动,也就不用检查左右的边界了 ischeckleftandright = false; } //如果图片的高度小于控件的高度 if (rectf.height() < getheight()){ //上下不可滑动 dy = 0; //上下不可滑动,也就不用检查上下边界了 ischecktopandbottom = false; } } mscalematrix.posttranslate(dx,dy); //当平移时,检查上下左右边界 checkborderwhentranslate(); setimagematrix(mscalematrix); } mlastx = x; mlasty = y; break; case motionevent.action_up: //当手指抬起时,将mlastpointercount置0,停止滑动 mlastpointercount = 0; break; case motionevent.action_cancel: break; } return true; } /** * 判断是否是移动的操作 */ private boolean ismoveaction(float dx , float dy){ //勾股定理,判断斜边是否大于可滑动的一个临界值 return math.sqrt(dx*dx + dy*dy) > 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 < mmaxscale) || scalefactor < 1.0f && scale * scalefactor > mminscale){ //边界控制,如果当前缩放比例乘以scalefactor之后小于了最小的缩放比例 //我们不允许再缩小 if (scale * scalefactor < mminscale + 0.01f){ scalefactor = mminscale / scale; } ... }
这样我们的图片最小就可以缩放到初始化比例的1/4大小了,然后我们还需要添加一个松手后回弹至初试化大小的动画效果,然后我们需要在ontouch的action_up中添加如下代码
@override public boolean ontouch(view v, motionevent event) { ... case motionevent.action_up: //当手指抬起时,将mlastpointercount置0,停止滑动 mlastpointercount = 0; //如果当前图片大小小于初始化大小 if (getscale() < minitscale){ //自动放大至初始化大小 post(new autoscalerunnable(minitscale,getwidth()/2,getheight()/2)); } break; ... }
现在我们看一下效果
实现图片放大后,松手回弹效果
这个功能实现起来和上面那个功能基本一致,大家可以先试着自己写一下。
同理,我们需要先定义一个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 < mmaxoverscale) || 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() < minitscale){ //自动放大至初始化大小 post(new autoscalerunnable(minitscale,getwidth()/2,getheight()/2)); } //如果当前图片大小大于最大值 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() < minitscale){ //自动放大至初始化大小 post(new autoscalerunnable(minitscale,getwidth()/2,getheight()/2)); } //如果当前图片大小大于最大值 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软件编程有所帮助。
上一篇: java无锁hashmap原理与实现详解
下一篇: java中的key接口解析