在上一篇文章中,初步实现了自定义ImageView的多点触控。但是从最终效果来看,却发现自由缩放时,图片与屏幕出现了空白间隙,这当然是让人十分厌恶的。在这篇文章中,就来解决这个问题。如果你还没读过上一篇文章,可以点击下面的链接:
http://www.cnblogs.com/fuly550871915/p/4939629.html
先贴出上一篇文章所实现的最终效果图吧,如下:
从上图中,可以看到,图片缩放后,与屏幕之间形成了很大的空白间隙。现在来解决这个问题,主要是两个方面:
(1)图片自由缩放后,它的中心点不应该移动,即依旧是屏幕的中心点。
(2)图片自由缩放后,不应该与屏幕有空白间隙,主要是指宽度上的间隙。
一、分析
下面就从这两个方面着手进行分析。那么缩放后,与屏幕的宽高相比较,有几种情况呢?很简单,只有四种,缩放后的宽比屏幕的宽要大,缩放的宽比屏幕的宽要小,缩放后的高度比屏幕的高要大,缩放后的高比屏幕的高要下。其实仔细想想,因为我们前面写代码时,最下缩放比例都要求宽度为屏幕的宽,所以缩放后的宽度是不可能比屏幕宽的,不过为了完整,我们写上也无妨。以缩放后的宽比屏幕的宽要大为例,仔细分析,看下面一张图片:
如果缩放后,图片为图示A的状态,此时其宽度大于屏幕宽度,与屏幕右侧出现了间隙,那么它应该往右移动,而移动的距离就是:屏幕宽度与右坐标的差值。同理,如果缩放后为B的状态,则图片应该左移,移动的距离应该是:0与图片左坐标的差值。同样的道理,如果图片缩放后比屏幕高,你应该也会分析了。就是将宽度改为了高度而已。
再看一张图片:
缩放后,A,B,C,D四种情况已经在上面讨论过,这样子只要缩放后出现上面四种情况,我们对其进行相应移动,图片的中心点就会依旧与原来的图片中心点重合,即与屏幕中心点重合。那么如果是E那种情况呢?针对情况E,再画一张图。如下:
从图上可以看出,显然此时需要移动的距离是,x方向就是黄线的长度,可以这样子计算:屏幕宽度/2 - 图片右坐标+图片缩放后的宽度/2。在y方向上,同理,只是将宽度改为高度即可。
好了,通过上面的分析,所有情况下应该移动的算法,我们都清楚了。下面就可以将它们写进代码中了。移动嘛,利用的依旧是Matrix.postTranslate方法了。
二、代码
还剩下一个问题,就是怎么获得图片根据矩阵变换后的新的坐标呢?这个问题,代码中有答案。修改ZoomImageView中的代码,如下:
1 package com.example.view; 2 3 import android.annotation.SuppressLint; 4 import android.content.Context; 5 import android.graphics.Matrix; 6 import android.graphics.RectF; 7 import android.graphics.drawable.Drawable; 8 import android.util.AttributeSet; 9 import android.util.Log; 10 import android.view.MotionEvent; 11 import android.view.ScaleGestureDetector; 12 import android.view.ScaleGestureDetector.OnScaleGestureListener; 13 import android.view.View; 14 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 15 import android.view.View.OnTouchListener; 16 import android.widget.ImageView; 17 18 public class ZoomImageView extends ImageView implements OnGlobalLayoutListener, 19 OnScaleGestureListener, OnTouchListener 20 { 21 private boolean mOnce = false;//是否执行了一次 22 23 /** 24 * 初始缩放的比例 25 */ 26 private float initScale; 27 /** 28 * 缩放比例 29 */ 30 private float midScale; 31 /** 32 * 可放大的最大比例 33 */ 34 private float maxScale; 35 /** 36 * 缩放矩阵 37 */ 38 private Matrix scaleMatrix; 39 40 /** 41 * 缩放的手势监控类 42 */ 43 private ScaleGestureDetector mScaleGestureDetector; 44 45 46 public ZoomImageView(Context context) 47 { 48 this(context,null); 49 } 50 public ZoomImageView(Context context, AttributeSet attrs) 51 { 52 this(context, attrs,0); 53 54 } 55 public ZoomImageView(Context context, AttributeSet attrs, int defStyle) 56 { 57 super(context, attrs, defStyle); 58 59 scaleMatrix = new Matrix(); 60 61 setScaleType(ScaleType.MATRIX); 62 63 mScaleGestureDetector = new ScaleGestureDetector(context, this); 64 //触摸回调 65 setOnTouchListener(this); 66 67 } 68 69 /** 70 * 该方法在view与window绑定时被调用,且只会被调用一次,其在view的onDraw方法之前调用 71 */ 72 protected void onAttachedToWindow() 73 { 74 super.onAttachedToWindow(); 75 //注册监听器 76 getViewTreeObserver().addOnGlobalLayoutListener(this); 77 } 78 79 /** 80 * 该方法在view被销毁时被调用 81 */ 82 @SuppressLint("NewApi") protected void onDetachedFromWindow() 83 { 84 super.onDetachedFromWindow(); 85 //取消监听器 86 getViewTreeObserver().removeOnGlobalLayoutListener(this); 87 } 88 89 /** 90 * 当一个view的布局加载完成或者布局发生改变时,OnGlobalLayoutListener会监听到,调用该方法 91 * 因此该方法可能会被多次调用,需要在合适的地方注册和取消监听器 92 */ 93 public void onGlobalLayout() 94 { 95 if(!mOnce) 96 { 97 //获得当前view的Drawable 98 Drawable d = getDrawable(); 99 100 if(d == null) 101 { 102 return; 103 } 104 105 //获得Drawable的宽和高 106 int dw = d.getIntrinsicWidth(); 107 int dh = d.getIntrinsicHeight(); 108 109 //获取当前view的宽和高 110 int width = getWidth(); 111 int height = getHeight(); 112 113 //缩放的比例,scale可能是缩小的比例也可能是放大的比例,看它的值是大于1还是小于1 114 float scale = 1.0f; 115 116 //如果仅仅是图片宽度比view宽度大,则应该将图片按宽度缩小 117 if(dw>width&&dh<height) 118 { 119 scale = width*1.0f/dw; 120 } 121 //如果图片和高度都比view的大,则应该按最小的比例缩小图片 122 if(dw>width&&dh>height) 123 { 124 scale = Math.min(width*1.0f/dw, height*1.0f/dh); 125 } 126 //如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片 127 if(dw<width&&dh<height) 128 { 129 scale = Math.min(width*1.0f/dw, height*1.0f/dh); 130 } 131 //如果仅仅是高度比view的大,则按照高度缩小图片即可 132 if(dw<width&&dh>height) 133 { 134 scale = height*1.0f/dh; 135 } 136 137 //初始化缩放的比例 138 initScale = scale; 139 midScale = initScale*2; 140 maxScale = initScale*4; 141 142 //移动图片到达view的中心 143 int dx = width/2 - dw/2; 144 int dy = height/2 - dh/2; 145 scaleMatrix.postTranslate(dx, dy); 146 147 //缩放图片 148 scaleMatrix.postScale(initScale, initScale, width/2, height/2); 149 150 setImageMatrix(scaleMatrix); 151 mOnce = true; 152 } 153 154 } 155 /** 156 * 获取当前已经缩放的比例 157 * @return 因为x方向和y方向比例相同,所以只返回x方向的缩放比例即可 158 */ 159 private float getDrawableScale() 160 { 161 162 float[] values = new float[9]; 163 scaleMatrix.getValues(values); 164 165 return values[Matrix.MSCALE_X]; 166 167 } 168 169 /** 170 * 缩放手势进行时调用该方法 171 * 172 * 缩放范围:initScale~maxScale 173 */ 174 public boolean onScale(ScaleGestureDetector detector) 175 { 176 177 if(getDrawable() == null) 178 { 179 return true;//如果没有图片,下面的代码没有必要运行 180 } 181 182 float scale = getDrawableScale(); 183 //获取当前缩放因子 184 float scaleFactor = detector.getScaleFactor(); 185 186 if((scale<maxScale&&scaleFactor>1.0f)||(scale>initScale&&scaleFactor<1.0f)) 187 { 188 //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子 189 if(scale*scaleFactor<initScale&&scaleFactor<1.0f) 190 { 191 scaleFactor = initScale/scale; 192 } 193 //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子 194 if(scale*scaleFactor>maxScale&&scaleFactor>1.0f) 195 { 196 scaleFactor = maxScale/scale; 197 } 198 199 // scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth()/2, getHeight()/2); 200 scaleMatrix.postScale(scaleFactor, scaleFactor,detector.getFocusX(), 201 detector.getFocusY()); 202 203 checkBoderAndCenter();//处理缩放后图片边界与屏幕有间隙或者不居中的问题 204 205 206 setImageMatrix(scaleMatrix);//千万不要忘记设置这个,我总是忘记 207 } 208 return true; 209 } 210 /** 211 * 处理缩放后图片边界与屏幕有间隙或者不居中的问题 212 */ 213 private void checkBoderAndCenter() 214 { 215 RectF rectf = getDrawableRectF(); 216 217 int width = getWidth(); 218 int height = getHeight(); 219 220 float delaX =0; 221 float delaY = 0; 222 223 if(rectf.width()>=width) 224 { 225 if(rectf.left>0) 226 { 227 delaX = - rectf.left; 228 } 229 230 if(rectf.right<width) 231 { 232 delaX = width - rectf.right; 233 } 234 } 235 236 if(rectf.height()>=height) 237 { 238 if(rectf.top>0) 239 { 240 delaY = -rectf.top; 241 } 242 if(rectf.bottom<height) 243 { 244 delaY = height - rectf.bottom; 245 } 246 } 247 248 if(rectf.width()<width) 249 { 250 delaX = width/2 - rectf.right+ rectf.width()/2; 251 } 252 253 if(rectf.height()<height) 254 { 255 delaY = height/2 - rectf.bottom+ rectf.height()/2; 256 } 257 258 scaleMatrix.postTranslate(delaX, delaY); 259 } 260 /** 261 * 获取图片根据矩阵变换后的四个角的坐标,即left,top,right,bottom 262 * @return 263 */ 264 private RectF getDrawableRectF() 265 { 266 Matrix matrix = scaleMatrix; 267 RectF rectf = new RectF(); 268 Drawable d = getDrawable(); 269 if(d != null) 270 { 271 272 rectf.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 273 } 274 275 matrix.mapRect(rectf); 276 return rectf; 277 } 278 /** 279 * 缩放手势开始时调用该方法 280 */ 281 public boolean onScaleBegin(ScaleGestureDetector detector) 282 { 283 //返回为true,则缩放手势事件往下进行,否则到此为止,即不会执行onScale和onScaleEnd方法 284 return true; 285 } 286 /** 287 * 缩放手势完成后调用该方法 288 */ 289 public void onScaleEnd(ScaleGestureDetector detector) 290 { 291 292 293 } 294 295 /** 296 * 监听触摸事件 297 */ 298 public boolean onTouch(View v, MotionEvent event) 299 { 300 301 if(mScaleGestureDetector != null) 302 { 303 //将触摸事件传递给手势缩放这个类 304 mScaleGestureDetector.onTouchEvent(event); 305 } 306 return true; 307 } 308 309 310 311 312 }
红色部分是我们增加的核心代码。第203行,我们增加了checkBoderAndCenter方法用来处理缩放后出现间隙空白的问题。而在getDrawableRectF方法中,通过一个Rectf拿到图片缩放后的新的坐标,这个方法需要牢牢掌握。其他的代码,都是按照上面我们分析的逻辑来编写的,相信你一定能看的懂了。注意第248行到第251行的代码可要可不要,因为我们最小的缩放比例就要求宽度必须等于屏幕宽度,因此不可能比屏幕宽度小,做这个判断没意义。好了,要修改的就这么多了。现在运行下程序,看看是否真的解决问题了呢?效果如下:
依旧是在真机上录制的gif,效果还是不错的,完美解决了这个出现空隙的问题。
三、总结
获得图片缩放后的坐标的方法,必须要掌握。如下:
/** * 获取图片根据矩阵变换后的四个角的坐标,即left,top,right,bottom * @return */ private RectF getDrawableRectF() { Matrix matrix = scaleMatrix; RectF rectf = new RectF(); Drawable d = getDrawable(); if(d != null) { rectf.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); } matrix.mapRect(rectf); return rectf; }
自由缩放算是告一段落了,下面我们要实现的是图片的自由移动。保存好代码,快进入下一节吧:《(三)多点触控之自由移动缩放后的图片》