先来看两张效果图:
哈哈,就是这样了。效果差了一些,感兴趣的小伙伴们可以运行代码感受丝滑与弹性。前段时间在竞品小红书上看到了这样的效果:图片可以跟随手指移动,双指可以(无限)放大,缩小,还可以挤压,手指抬起后还有一个有趣的效果,图片回弹。。。一直想撸一个手势的控件,正好可以模仿小红书图片裁剪控件,话不多说,撸起袖子就是干。
本系列共有两篇,在第二篇会重点讲解与RecyclerView的联动效果,先放一张效果图,感兴趣的小伙伴们继续关注哦:
![在这里插入图片描述 640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/3ef9779cccb0fd18f7d0d7e8c9167db9.gif)
[]()初步分析
先来看看小红书的样子:
![在这里插入图片描述 640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/fd8d45df7631e610838e6a0f2649d249.gif)
![在这里插入图片描述 640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/a5c5b08dba7681b3712d12a765ee2af5.gif)
emmmm,从效果上来看呢,其实也只是基本的Translation和Scale组合而已,难点在于缩小态下的阻尼计算,左下角那个按钮用来控制留白,填充等状态的切换(好像小红书还有bug,状态切换会导致图片位置不正确,哈哈哈),接下来我们就一步步分析,从而打造出属于我们的自己的效果。
仔细观察,有没有发现:
单指滑动,图片跟随手指移动,当手指滑动到图片边缘继续沿同一方向滑动,会出现阻尼效果,滑动的距离越大,阻尼越大,手指抬起后,图片回弹到控件边缘;
双指触摸分两种情况,一种是双指向内挤压,图片缩小;另一种是双指向外扩散,图片放大;
当双指向外扩散达到一定的临界值,手指抬起后,图片缩小到临界值状态;
手指触摸且有一定的滑动值,会显示线条九宫格,且线条跟随图片的大小动态改变,始终分割图片为9等分,如果手指触摸停止,线条消失,再次滑动,线条则再次出现;
那么图片缩放时,需要一个缩放中心点,也就是PivotX和PivotY,这个点默认情况下在View的中心。但很明显,它这个就不是在中心了,至于在哪里,先看下这张图:
可以看到,图片始终是以双指的中点在缩放,那么缩放中心点就是双指连线的中点位置上了。又怎么获取到双指的中点坐标呢?这里涉及到了Android提供的两个帮助类:GestureDetector、ScaleGestureDetector。接下来让我们先来了解下这两个类,揭开它的神秘面纱。神秘?你个糟老头,坏得很,信你个鬼。。。
[]()手势帮助类
什么是手势帮助类?Android手机屏幕上,当我们触摸屏幕的时候,会产生许多手势事件,如down,up,scroll,filing等等。我们可以在onTouchEvent()方法里面完成各种手势识别。但是,我们自己去识别各种手势就比较麻烦了,而且有些情况可能考虑的不是那么的全面。所以,为了方便我们的使用Android就提供了GestureDetector帮助类,先来看看他的构造方法:
1 public GestureDetector(Context context, OnGestureListener listener, Handler handler,2 boolean unused) {3 }
context表示上下文,listener表示手势的监听回调,handler可以指定线程(UI线程、非UI线程),unused未被使用的参数。如果我们的手势不需要在子线程中处理,我们一般只关心前两个参数,context是上下文这个简单,重点看下listener参数:
GestureDetector给我们提供了三个接口类与一个外部类:
OnGestureListener:接口,用来监听手势事件(6种);
OnDoubleTapListener:接口,用来监听双击事件;
OnContextClickListener:接口,外接设备,比如外接鼠标产生的事件(本文中我们不考虑);
SimpleOnGestureListener:外部类,SimpleOnGestureListener其实是上面三个接口中所有函数的集成,它包含了这三个接口里所有必须要实现的函数而且都已经重写,但所有方法体都是空的。需要自己根据情况去重写;
OnGestureListener接口方法:
1public interface OnGestureListener { 2 /** 3 * 按下。返回值表示事件是否处理 4 */ 5 boolean onDown(MotionEvent e); 6 7 /** 8 * 短按(手指尚未松开也没有达到scroll条件) 9 */10 void onShowPress(MotionEvent e);1112 /**13 * 轻触(手指松开)14 */15 boolean onSingleTapUp(MotionEvent e);1617 /**18 * 滑动(一次完整的事件可能会多次触发该函数)。返回值表示事件是否处理19 */20 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);2122 /**23 * 长按(手指尚未松开也没有达到scroll条件)24 */25 void onLongPress(MotionEvent e);2627 /**28 * 滑屏(用户按下触摸屏、快速滑动后松开,返回值表示事件是否处理)29 */30 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);31 }
OnDoubleTapListener接口方法:
1 public interface OnDoubleTapListener { 2 /** 3 * 单击事件(onSingleTapConfirmed,onDoubleTap是两个互斥的函数) 4 */ 5 boolean onSingleTapConfirmed(MotionEvent e); 6 7 /** 8 * 双击事件 9 */10 boolean onDoubleTap(MotionEvent e);1112 /**13 * 双击事件产生之后手指还没有抬起的时候的后续事件14 */15 boolean onDoubleTapEvent(MotionEvent e);16 }
GestureDetector的使用:
定义GestureDetector类;
将touch事件交给GestureDetector(onTouchEvent函数里面调用GestureDetector的onTouchEvent函数);
处理SimpleOnGestureListener或者OnGestureListener、OnDoubleTapListener、OnContextClickListener三者之一的回调;
GestureDetector使用流程如下(有关例子会在后文中讲到):
1 public GestureView(Context context, @Nullable AttributeSet attrs) { 2 this(context, attrs, 0); 3 } 4 5 public GestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 6 super(context, attrs, defStyleAttr); 7 // 第一步 8 mGestureDetector = new GestureDetector(context, mOnGestureListener); 9 }1011 @Override12 public boolean onTouchEvent(MotionEvent event) {13 // 第三步14 return mGestureDetector.onTouchEvent(event);15 }16 // 第二步17 GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.OnGestureListener() {18 @Override19 public boolean onDown(MotionEvent e) {20 return false;21 }
这里就不再深入GestureDetector源码讲解,有感兴趣的小伙伴可以自行查阅资料,接着了解ScaleGestureDetector缩放手势类,用法与GestureDetector类似,都是通过onTouchEvent()关联相应的MotionEvent事件。
ScaleGestureDetector类给提供了OnScaleGestureListener接口,来告诉我们缩放的过程中的一些回调:
1 public interface OnScaleGestureListener { 2 /** 3 * 缩放进行中,返回值表示是否下次缩放需要重置,如果返回ture,那么detector就会重置缩放事件,如果返回false,detector会在之前的缩放上继续进行计算 4 */ 5 public boolean onScale(ScaleGestureDetector detector); 6 7 /** 8 * 缩放开始,返回值表示是否受理后续的缩放事件 9 */10 public boolean onScaleBegin(ScaleGestureDetector detector);1112 /**13 * 缩放结束14 */15 public void onScaleEnd(ScaleGestureDetector detector);16 }
ScaleGestureDetector类常用函数介绍,因为在缩放的过程中,要通过ScaleGestureDetector来获取一些缩放信息:
1 /** 2 * 缩放是否正处在进行中 3 */ 4 public boolean isInProgress(); 5 6 /** 7 * 返回组成缩放手势(两个手指)中点x的位置 8 */ 9 public float getFocusX();1011 /**12 * 返回组成缩放手势(两个手指)中点y的位置13 */14 public float getFocusY();1516 /**17 * 组成缩放手势的两个触点的跨度(两个触点间的距离)18 */19 public float getCurrentSpan();2021 /**22 * 同上,x的距离23 */24 public float getCurrentSpanX();2526 /**27 * 同上,y的距离28 */29 public float getCurrentSpanY();3031 /**32 * 组成缩放手势的两个触点的前一次缩放的跨度(两个触点间的距离)33 */34 public float getPreviousSpan();3536 /**37 * 同上,x的距离38 */39 public float getPreviousSpanX();4041 /**42 * 同上,y的距离43 */44 public float getPreviousSpanY();4546 /**47 * 获取本次缩放事件的缩放因子,缩放事件以onScale()返回值为基准,一旦该方法返回true,代表本次事件结束,重新开启下次缩放事件。48 */49 public float getScaleFactor();5051 /**52 * 返回上次缩放事件结束时到当前的时间间隔53 */54 public long getTimeDelta();5556 /**57 * 获取当前motion事件的时间58 */59 public long getEventTime();
ScaleGestureDetector使用方式与GestureDetector类似,这里就不再重复讲解,了解了相关手势类,接下来开始代码构思。
[]()构思代码
想一想,图片有任意尺寸,怎样才能让图片铺满控件,那么就需要对图片进行缩放,平移。还有一点是必须考虑的,在加载高分辨率的图片非常消耗内存,在低内存的手机上很容易造成OOM,那么针对高分辨率的图片就必须压缩。还有一种情况是来回切换相同的两张图片,如果每次都加载本地图片,既消耗内存速度还很慢,这时候缓存就很有必要了,第一次加载本地图片,再次切回到该图片加载缓存图片。
显示图片,一般有两种方式,一种是Android提供了ImageView控件来显示图片;另一种直接在onDraw()方法里调用canvas.drawBitmap()方法,通过调研小红书显示方案,发现他采用了第二种:
![在这里插入图片描述 640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/5caad6fa44eb3dae16db77af682a9e5f.jpeg)
(^__^) 嘻嘻……那我们就用第一种显示图片的方式,继承ImageView来显示图片。
通过观察小红书,我们会发现:
图片显示区域为宽高相等的矩形,那么在测量onMeasure的时候需要保证宽高一致,左下角小按钮的状态切换先不考虑,后面会重点讲解。
图片默认会充满整个控件并居中对齐,那么怎么保证图片充满控件,最常规的做法就是:取控件的宽高与图片的宽高比的最大值缩放
Math.max(控件宽度/图片宽度,控件高度/图片高度)
;同理,取控件宽高与图片宽高的偏移量的一半来平移图片保证居中对齐。在2的基础上,非宽高相等的图片有一部分会显示在控件区域之外,可以通过手指滑动来显示,相信大家都用过PhotoView,效果一致。 移动图片与移动控件的原理一样,都是改变setTranslation的值,不过这里用到了图片矩阵,通过改变Matrix.postTranslate(dx, dy)的值来移动图片。
移动图片,那就不得不考虑越界问题,请观察下图,这里以上边界为例(左,右,下边界同理)。注意:这里的越界指的不是数组越界,而是图片滑动到边缘继续沿相同方向滑动,图片未铺满控件区域。 在下图中你会发现:图片跟随手指继续滑动,手指滑动的距离越大阻尼越大,手指抬起后图片会回弹到控件顶部。
在这里插入图片描述 双指挤压图片缩小,扩散图片放大,缩放中心点是双指中点坐标,那么缩放比例怎么计算呢?最开始取的
缩放因子ScaleGestureDetector.getScaleFactor()
,出来的效果真的天马行空(轻微挤压扩散图片无限放大缩小 ),接着给缩放因子加一个比例,效果依旧不行,哦豁。没办法,打印缩放数据,观察数据,寻找规律。几经尝试最后取了缩放因子的偏移量。为了写好控件,没什么捷径,只能多观察,多尝试。 在缩小至越界的状态下,手指抬起,图片放大到充满控件;在放大到一定的阈值后放手后,图片回弹到一定的缩放比例。前文提到了在缩小至越界状态下单指滑动图片,根据四周滑动的距离,会出现阻尼效果,在后文会讲解阻尼算法。图片在滑动或缩放态下,会出现九宫格白色线条,线条始终平分控件内的图片为九等分,滑动或缩放停止线条消失,再次滑动或缩放线条出现,手指抬起后线条消失。
嗯,整个过程的大致行为就是这样了。
开工写代码咯~
[]()起名字
在开始写代码之前,要先给这个自定义控件起一个名字,又哦豁。。。不会起名字,
就叫:裁剪图片控件(MCropImageView) 吧。不要问我M字母是啥含义,我不会告诉你的。
[]()编写代码
[]()宽高相等矩阵测量
测量比较简单,具体请看相关代码:
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 4 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 5 if (widthSize > heightSize) { 6 // 取高 7 super.onMeasure(heightMeasureSpec, heightMeasureSpec); 8 } else { 9 // 取宽10 super.onMeasure(widthMeasureSpec, widthMeasureSpec);11 }12 }
[]()铺满居中
铺满的原理上文已经讲到了,对应的公式如下:
1控件宽度/图片宽度 = a2控件高度/高度高度 = b 3mBaseScale = Math.max(a,b)4Matrix.postScale(mBaseScale, mBaseScale, 控件宽度/ 2, 控件高度/ 2)
居中的原理上面也提到过了,来看看代码怎么写:
1 @Override 2 public void onGlobalLayout() { 3 mMatrix.reset(); 4 // 获取控件的宽度和高度 5 int viewWidth = getWidth(); 6 int viewHeight = getHeight(); 7 8 // 图片的固定宽度 高度 9 // 获取图片的宽度和高度10 Drawable drawable = getDrawable();11 if (null == drawable) {12 return;13 }14 int drawableWidth = drawable.getIntrinsicWidth();15 int drawableHeight = drawable.getIntrinsicHeight();1617 // 将图片移动到屏幕的中点位置18 float dx = (viewWidth - drawableWidth) / 2;19 float dy = (viewHeight - drawableHeight) / 2;20 // 取最大值21 mBaseScale = Math.max((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);22 // 平移居中23 mMatrix.postTranslate(dx, dy);24 // 缩放25 mMatrix.postScale(mBaseScale, mBaseScale, viewWidth / 2, viewHeight / 2);26 setImageMatrix(mMatrix);27 }
有关Matrix的set 、 pre、post方法调用顺序,这里简单说一下(个人理解,有错还望指出 ),可以把Matrix的操作看成队列,post方法添加到队列的尾部,pre添加到队列的头部,而set方法则重置队列。
看看铺满居中的效果:
![在这里插入图片描述 640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/a94e1257f723e0752857239e4f89a2e3.jpeg)
[]()单指滑动
单指滑动,在上文已经讲到GestureDetector.SimpleOnGestureListener内部接口用来处理手势滑动,重写以下接口方法:
1 // 处理手指滑动 2 private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { 3 4 @Override 5 public boolean onDown(MotionEvent e) { 6 // 消费事件 7 return true; 8 } 910 @Override11 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {12 // 限定单指13 if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {14 // distanceX 左正右负 所以这里取相反数15 mMatrix.postTranslate(-distanceX, -distanceY);16 setImageMatrix(mMatrix);17 return true;18 }19 return super.onScroll(e1, e2, distanceX, distanceY);20 }21 };
获取到手指滑动的距离,对图片矩阵进行平移Matrix.postTranslate(),但在x轴方向获取到的滑动距离右负左正,y轴方向获取到的滑动距离上正下负,跟实际平移的值相反,那么平移值Matrix.postTranslate(-distanceX, -distanceY)取滑动距离的负数。
单指滑动还有一个效果,越界下的阻尼效果,看看效果图:
![在这里插入图片描述 640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/2648ad09f6d1484e66f6faa8ec407929.gif)
很明显图片跟随手指滑动,距离控件边缘越近,阻尼越大。那么很明显需要获取图片边缘距离控件的距离,然后根据滑动偏移量进行计算。为了获取图片边缘距离控件的距离,就需要获取图片的位置信息。那么怎样才能获取图片位置信息呢?
在ViewGroup的transformPointToViewLocal方法中有这样一段代码:
1 if (!child.hasIdentityMatrix()) {2 child.getInverseMatrix().mapPoints(point);3 }
如果child所对应的矩阵发生过旋转、缩放等变化的话(补间动画不算,因为是临时的),会通过矩阵的mapPoints方法来将触摸点转换到矩阵变换后的坐标。
没错,我们也可以用矩阵的mapRect方法来将图片的坐标及尺寸转换一下,就像这样:
![在这里插入图片描述 640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/d34318254d60f388b028e6da589c93e7.gif)
这样就可以获取到图片的矩形区域,相关方法如下:
1 // 获取图片矩阵区域 2 private RectF getMatrixRectF() { 3 RectF rectF = new RectF(); 4 Drawable drawable = getDrawable(); 5 if (drawable != null) { 6 // 注意set 7 rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 8 mMatrix.mapRect(rectF); 9 }10 return rectF;11 }
获取到了图片矩阵,那么图片越界就很容易判定了,先看下面两张越界图:
![在这里插入图片描述 640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/ee4ab38d22be84e25af50c311f727e98.jpeg)
![在这里插入图片描述 640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/af1fe2fd6099ea38747ffe46dda99871.jpeg)
图片上边缘距离控件顶部变量为topEdgeDistanceTop,左边缘距离控件左边变量为leftEdgeDistanceLeft,右边缘距离控件右边变量为rightEdgeDistanceRight,下边缘距离控件底部变量为bottomEdgeDistanceBottom,分别对应的代码如下:
1 // 获取图片矩阵2 RectF rectF = getMatrixRectF();3 float leftEdgeDistanceLeft = rectF.left;4 float topEdgeDistanceTop = rectF.top;5 //位移 rectF.right - rectF.left 图片宽度 6 float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth();7 // rectF.bottom - rectF.top 图片高度8 float bottomEdgeDistanceBottom = topEdgeDistanceTop + rectF.bottom - rectF.top - getHeight();
好了,这样就可以准确判定图片是否越界。接下来我们看看越界状态下的阻尼算法是怎么计算的,有什么规律:
先来观察图片左右越界的情况(上下越界同理),左右越界又分为三种情况,左越界&右不越界(简称左越界),右越界&左不越界(简称右越界),左越界&右越界(简称左右越界) 左越界的情况与右越界类似,那么就只有两种情况:
左越界
在这里插入图片描述
可以看到在向左滑动的情况下,图片左侧距离控件左侧距离越大,阻力越大。通俗一点,手指滑动的距离越大,图片跟随手指滑动的距离就越小,那么可以根据以下公式获取阻尼系数:
1 最大阻尼数 / 最大偏移量 * leftEdgeDistanceLeft
最大阻尼数默认取值为9,最大偏移量为控件宽度的三分之一,对应的代码如下:
1 // 获取图片矩阵 2 RectF rectF = getMatrixRectF(); 3 float leftEdgeDistanceLeft = rectF.left; 4 float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth(); 5 6 // MAX_SCROLL_FACTOR = 3 7 int maxOffsetWidth = getWidth() / MAX_SCROLL_FACTOR; 8 int maxOffsetHeight = getHeight() / MAX_SCROLL_FACTOR; 9 // 图片左侧越界并且图片右侧未越界10 if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight > 0) {11 // distanceX < 0 表示继续向右滑动12 if (distanceX < 0) {13 if (leftEdgeDistanceLeft < maxOffsetWidth) {14 // DAMP_FACTOR = 9 系数越大阻尼越大 +1防止ratio为015 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * leftEdgeDistanceLeft) + 1;16 distanceX /= ratio;17 } else {18 // 图片向右滑动超过了最大偏移量 图片则不平移19 distanceX = 0;20 }21 }22 // 向左滑动不做处理 默认取值distanceX23 }
左右越界
在这里插入图片描述
左右越界的情况与左越界的情况正好相反,距离控件边缘越近,图片阻力越大。那么怎么判定图片距离控件边缘越近,这里分两种情况,图片中点在控件中点左侧以及图片中点在控件中点右侧。第一种情况图片中点在控件中点左侧,向左滑动阻力越大,向右滑动阻力为0;第二种情况图片中点在控件中点的右侧,向右滑动阻力越大,向左滑动阻力为0。
来看看代码怎么写:
1 // 图片左侧越界并且图片右侧越界 2 if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight < 0) { 3 // 控件宽度的一半 4 int halfWidth = getWidth() / 2; 5 // 获取图片中点x坐标 6 float centerX = (rectF.right - rectF.left) / 2 + rectF.left; 7 // 图片中点x坐标是否右侧偏移 8 boolean rightOffsetCenterX = centerX >= halfWidth; 9 // 右侧偏移并且向右滑动10 if (distanceX < 0 && rightOffsetCenterX) {11 // centerX - halfWidth 图片右侧偏移量12 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (centerX - halfWidth)) + 1;13 distanceX /= ratio;14 }15 // 左侧偏移并且向左滑动16 else if (distanceX > 0 && !rightOffsetCenterX) {17 // halfWidth - centerX 左侧的偏移量18 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (halfWidth - centerX)) + 1;19 distanceX /= ratio;20 }21 }
好了,左右越界就讲到这里,上下越界同理,越界的整体代码如下:
1 @Override 2 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 3 if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) { 4 // 获取图片矩阵 5 RectF rectF = getMatrixRectF(); 6 7 float leftEdgeDistanceLeft = rectF.left; 8 float topEdgeDistanceTop = rectF.top; 9 10 float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth(); 11 float bottomEdgeDistanceBottom = topEdgeDistanceTop + rectF.bottom - rectF.top - getHeight(); 12 13 // MAX_SCROLL_FACTOR = 3 14 int maxOffsetWidth = getWidth() / MAX_SCROLL_FACTOR; 15 int maxOffsetHeight = getHeight() / MAX_SCROLL_FACTOR; 16 17 // 图片左侧越界并且图片右侧未越界 18 if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight > 0) { 19 // distanceX < 0 表示继续向右滑动 20 if (distanceX < 0) { 21 if (leftEdgeDistanceLeft < maxOffsetWidth) { 22 // DAMP_FACTOR = 9 系数越大阻尼越大 +1防止ratio为0 23 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * leftEdgeDistanceLeft) + 1; 24 distanceX /= ratio; 25 } else { 26 // 图片向右滑动超过了最大偏移量 图片则不平移 27 distanceX = 0; 28 } 29 } 30 // 向左滑动不做处理 默认取值distanceX 31 } 32 // 图片右侧越界并且图片左侧未越界 (同上处理) 33 else if (rightEdgeDistanceRight < 0 && leftEdgeDistanceLeft < 0) { 34 // distanceX > 0 表示继续向左滑动 35 if (distanceX > 0) { 36 if (rightEdgeDistanceRight > -maxOffsetWidth) { 37 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * -rightEdgeDistanceRight) + 1; 38 distanceX /= ratio; 39 } else { 40 // 图片右侧距离控件右侧超过最大偏移量 图片则不平移 41 distanceX = 0; 42 } 43 } 44 } 45 // 图片左侧越界并且图片右侧越界 46 else if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight < 0) { 47 // 控件宽度的一半 48 int halfWidth = getWidth() / 2; 49 // 获取图片中点x坐标 50 float centerX = (rectF.right - rectF.left) / 2 + rectF.left; 51 // 图片中点x坐标是否右侧偏移 52 boolean rightOffsetCenterX = centerX >= halfWidth; 53 // 右侧偏移并且向右滑动 54 if (distanceX < 0 && rightOffsetCenterX) { 55 // centerX - halfWidth 图片右侧偏移量 56 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (centerX - halfWidth)) + 1; 57 distanceX /= ratio; 58 } 59 // 左侧偏移并且向左滑动 60 else if (distanceX > 0 && !rightOffsetCenterX) { 61 // halfWidth - centerX 左侧的偏移量 62 int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (halfWidth - centerX)) + 1; 63 distanceX /= ratio; 64 } 65 } 66 67 // 上下越界 处理方式同左右处理方式一样 本可以提成一个方法但为了方便理解先这样了 68 // 图片上侧越界并且图片下侧未越界 69 if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom > 0) { 70 // distanceY < 0 表示图片继续向下滑动 71 if (distanceY < 0) { 72 if (topEdgeDistanceTop < maxOffsetHeight) { 73 // 获取阻尼比例 74 int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * topEdgeDistanceTop) + 1; 75 distanceY /= ratio; 76 } else { 77 // 向下滑动超过了最大偏移量 则图片不滑动 78 distanceY = 0; 79 } 80 } 81 } 82 // 图片下侧越界并且图片上侧未越界 83 else if (bottomEdgeDistanceBottom < 0 && topEdgeDistanceTop < 0) { 84 if (distanceY > 0) { 85 if (bottomEdgeDistanceBottom > -maxOffsetHeight) { 86 int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * -bottomEdgeDistanceBottom) + 1; 87 distanceY /= ratio; 88 } else { 89 // 向上滑动超过了最大偏移量 则图片不滑动 90 distanceY = 0; 91 } 92 } 93 } else if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom < 0) { 94 int halfHeight = getHeight() / 2; 95 // 获取图片中点y坐标 96 float centerY = (rectF.bottom - rectF.top) / 2 + rectF.top; 97 // 图片中点y坐标是否向下偏移 98 boolean bottomOffsetCenterY = centerY >= halfHeight; 99 // 向下偏移并且向下移动100 if (distanceY < 0 && bottomOffsetCenterY) {101 // centerY - halfHeight 图片偏移量102 int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (centerY - halfHeight)) + 1;103 distanceY /= ratio;104 } else if (distanceY > 0 && !bottomOffsetCenterY) { // 向上偏移并且向上移动105 int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (halfHeight - centerY)) + 1;106 distanceY /= ratio;107 }108 }109110 mMatrix.postTranslate(-distanceX, -distanceY);111 setImageMatrix(mMatrix);112 return true;113 }114 return super.onScroll(e1, e2, distanceX, distanceY);115 }
[]()双指缩放
双指缩放的原理在上文已经提及过了,重写ScaleGestureDetector.OnScaleGestureListener缩放手势类接口方法:
1 // 处理双指的缩放 2 private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() { 3 @Override 4 public boolean onScale(ScaleGestureDetector detector) { 5 if (null == getDrawable() || mMatrix == null) { 6 // 如果返回true那么detector就会重置缩放事件 7 return true; 8 } 9 // 缩放因子,缩小小于1,放大大于110 float scaleFactor = mScaleGestureDetector.getScaleFactor();1112 // 缩放因子偏移量13 float deltaFactor = scaleFactor - mPreScaleFactor;1415 if (scaleFactor != 1.0F && deltaFactor != 0F) {16 mMatrix.postScale(deltaFactor + 1F, deltaFactor + 1F, mScaleGestureDetector.getFocusX(),17 mScaleGestureDetector.getFocusY());18 setImageMatrix(mMatrix);19 }20 mPreScaleFactor = scaleFactor;21 return false;22 }2324 @Override25 public boolean onScaleBegin(ScaleGestureDetector detector) {26 // 注意返回true27 return true;28 }2930 @Override31 public void onScaleEnd(ScaleGestureDetector detector) {32 }33 };