项目开发中,你们APP开发通常都会用到上传图片,好比是上传了本身的生活照,而后在某个界面处查看上传的图片,这时候通常在这个查看详情的界面,会有手势放大缩小功能,手势进行旋转功能,双击放大图片等等。html
不巧,我之前也有须要这个需求的时候,并且特别指出了要用手势进行图片的选择功能。android
因而我查看了BiliBili的开源库:git
使用了这个Demo后发现里面有手势控制图片大小,手势控制图片旋转等功能,看了代码后我发现BiliBili这个demo中也是用了第三方的库:程序员
咱们能够看到介绍:在PhotoView的基础上添加了经过二个手指来旋转图片的功能,因此这个库又是用了其余的第三方库:github
咱们能够看到这个PhotoView的库有一万多个star了。说明仍是很不错的。api
因此经过此次。我就来看PhotoView如何进行实现那么多功能。bash
你们在看正文以前若是对于Matrix不是很了解的,能够先看看:
android matrix 最全方法详解与进阶(完整篇)
Android Matrix
Float中的那些常量 Infinity、NaNide
原本是想直接拿着PhotoView 的源码,贴上源码分析一个个具体的功能,可是由于源码是考虑到不少功能,因此有不少代码量,并且太多看着很乱,因此个人方案是直接本身写个demo,而后根据咱们要讲解的功能,仿照PhotoView的源码,在本身一个个具体的功能demo分别实现。因此本文我先来实现实现根据手势来实现图片的缩放功能:工具
PhotoView是继承了ImageView,而后直接在layout中使用PhotoView,为了更方便的讲解,我就直接仍是使用ImageView,而后让你们看到是如何对ImageView作处理实现相应的功能。源码分析
先添加咱们要的demo布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.dialog.photoviewdemo.MainActivity">
<ImageView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
/>
</LinearLayout>复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//对咱们的ImageView设置相应的一张图片
ivPhoto = (ImageView) findViewById(R.id.photo_view);
drawable = ContextCompat.getDrawable(this, R.mipmap.ic_launcher);
ivPhoto.setImageDrawable(drawable);
//对咱们的ImageView设置触摸事件监听,而且把监听交给了GestureDetector.
ivPhoto.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return scaleGestureDetector.onTouchEvent(event);
}
});
//GestureDetector的实例生成
scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
return false;
}
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
if(checkMatrixBounds()) {
ivPhoto.setImageMatrix(getDrawMatrix());
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
});
}复制代码
根据上面的代码咱们同样样来分析:
当用户触摸屏幕的时候,会产生许多手势,例如down,up,scroll,filing等等。
通常状况下,咱们知道View类有个View.OnTouchListener内部接口,经过重写他的onTouch(View v, MotionEvent event)方法,咱们能够处理一些touch事件,可是这个方法太过简单,若是须要处理一些复杂的手势,用这个接口就会很麻烦(由于咱们要本身根据用户触摸的轨迹去判断是什么手势)。
Android sdk给咱们提供了GestureDetector(Gesture:手势Detector:识别)类,经过这个类咱们能够识别不少的手势,主要是经过他的onTouchEvent(event)方法完成了不一样手势的识别。虽然他能识别手势,可是不一样的手势要怎么处理,应该是提供给程序员实现的。
具体具体能够看这篇文章,写的很详细:用户手势检测-GestureDetector使用详解
而此处咱们由于作的功能是经过手势来缩放图片,因此咱们就要监听二个手指头缩放动做,因此咱们使用的是ScaleGestureDetector。
ScaleGestureDetector介绍:
用于处理缩放的工具类,用法与GestureDetector相似,都是经过onTouchEvent()关联相应的MotionEvent的。使用该类时,用户须要传入一个完整的接二连三地motion事件(包含ACTION_DOWN,ACTION_MOVE和ACTION_UP事件)。
咱们看上面的代码就会发现ScaleGestureDetector有三个方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {}复制代码
onScaleBegin
:缩放开始会执行的方法,可是咱们发现这个方法须要返回一个Boolean
值,这个值决定是否处理后继的缩放事件,返回false
时,不会执行onScale()
。
onScaleEnd
:缩放结束执行
onScale
:缩放时候执行的方法,用来作具体的逻辑处理。
咱们具体来看看onScale
方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
return true;
}复制代码
咱们能够看到这里是返回Boolean值,那这里返回true和false有什么区别呢。
float scaleFactor = detector.getScaleFactor();复制代码
咱们能够经过这个方法获取到缩放因子,缩放因子会根据你的手势的变大会愈来愈大,若是你返回了true,那就说明此次的缩放行为就已经结束了,若是你返回了false,那就说明没有结束,而后缩放因子愈来愈大。
public boolean onScale(ScaleGestureDetector detector) {
if(detector.getScaleFactor()< 2){
return false;
}
return true;
}复制代码
(PS:若是二个手指作缩小的手势,那么这个缩放因子就会小于1,若是返回false,那么就会从1开始愈来愈小。)
假设咱们如今的ImageView设置的是全屏,咱们有个小图片,ImageView设置了图片后是这样的:
咱们发现默认是在左上角,并且由于咱们的ImageView设置的是全屏,而图片又特别小,这样的初步呈现方式很不友好。
因此咱们要作以下操做:
<1>把图片居中显示。
<2>图片和ImageView相适应(咱们这里是把图片适当的放大,来适应这么大的ImageView.)
因此也就是咱们上面提到过的代码:
drawableWidth = drawable.getIntrinsicWidth();
drawableHeight = drawable.getIntrinsicHeight();
viewWidth = ivPhoto.getWidth() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
RectF mTempScr = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
mBaseMatrix.setRectToRect(mTempScr, mTempDst, Matrix.ScaleToFit.CENTER);
mDrawableMatrix.set(mBaseMatrix);
ivPhoto.setImageMatrix(mDrawableMatrix);复制代码
获取图片的真实宽高和ImageView用来显示图片的宽高我就很少说了。重点是setRectToRect
方法:
public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf)复制代码
将rect变换成rect,经过stf参数来控制。
ScaleToFit 有以下四个值:
FILL: 可能会变换矩形的长宽比,保证变换和目标矩阵长宽一致。
START:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。左上对齐。
CENTER: 保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。
END:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。右下对齐。
这里使用谷歌的api demo的图片做为例子:
咱们很明显发现,那个蓝色的小球的变化不就是咱们想要的变化么,而且咱们是要居中,因此用的是Matrix.ScaleToFit.CENTER
。
咱们看下咱们最终的效果:
咱们前面已经知道了。手势变化的时候会触发onScale
方法,因此咱们只要把图片的具体的放大缩小的逻辑放在onScale
里面便可。
@Override
public boolean onScale(ScaleGestureDetector detector) {
//缩放因子
float scaleFactor = detector.getScaleFactor();
//返回组成该手势的两个触点的中点在组件上的x和y轴坐标,单位为像素。
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
//若是为nan或者无强大,则无效
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
return false;
}
//进行缩放,传入x轴缩放比例,y轴缩放比例,缩放中心点的x和y值
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
if(checkMatrixBounds()) {
ivPhoto.setImageMatrix(getDrawMatrix());
}
return true;
}复制代码
你们应该看到了我这边有个checkMatrixBounds
方法,原本其实单纯的缩放就是先postScale
而后在直接setImageMatrix
就能够了。
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
ivPhoto.setImageMatrix(getDrawMatrix());复制代码
可是这样有什么很差的地方呢。我来具体跟你们说下:
好比我是二个红点分别是个人手指,而后不停的缩小图片动做,图片不只变小,并且会随着那个方向作平移。放大则相反。这不是咱们想要的,咱们想要的是一样是作缩放,同时,图片还在中间。
既然咱们知道了图片在作缩小放大的同时还在平移,那咱们就作相应的反方向的平移处理不就行了
咱们分为二种状况:
若是图片再缩放过程当中没超过ImageView的大小。咱们只须要让图片一直居中现实便可。因此比较简单:
只要算出咱们在前面第二个大步里面的初始化后的图片的初始状态后(即和ImageView相适应而且居中),相应的图片的矩阵的宽和高是否是超过ImageView。若是没有超过,咱们能够看到咱们但愿的图片放大和缩小都是但愿在正中间的位置,可是如今变成了绿色的地方,咱们只须要把绿色的地方移动到咖啡色的地方就行。
以Y轴为例(X轴一样处理):
实际图片的TOP值(先获取相应的实际图片的矩阵Rect,在获取top属性):
private RectF getDisplayRect(Matrix matrix) {
Drawable d = drawable;
if (d != null) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
matrix.mapRect(mDisplayRect);
return mDisplayRect;
}
return null;
}复制代码
ImageView的高度:
viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();复制代码
实际变化后的图片的高度(rect为上面获取的实际图片的Rect):final float height = rect.height(), width = rect.width();
因此咱们这里只须要:
private boolean checkMatrixBounds() {
RectF rect = getDisplayRect(getDrawMatrix());
if (rect == null) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
if (height <= viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
}
if (width <= viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
}
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}复制代码
这个时候咱们就不行简单的在中心位置就能够了。由于这时候不能反而不让他在中心位置,为何????咱们如今的图片是一个安卓机器人,好比我如今要放大它的图片查看它的右眼,咱们在右上角用手机不挺放大。变成这样:
这时候就说了。那我什么都不处理,放大这边就是这个效果啊。说的没错的确这样,可是好比如今已经放大成这个样子了。我缩小它,可是我不是从右上角来进行缩小,而是在左边进行缩小,你们知道咱们不作处理,这时候缩小的时候是按咱们手势的位置进行,因此头像在缩小时候先是往左边方向,而后当小于ImageView的高度时候,又忽然居中,效果很很差。
因此咱们这个例子里面处理方式是:若是宽度都大于ImageView而且图片的右边界还没出如今ImageView中的时候,先按照本身原来的方式缩小,当图片的右边界出如今了ImageView的范围内了,让它慢慢往右边移动(也就是ImageView的宽度 - Rect.right的距离),这时候就会很和谐。最后宽度小于ImageView的时候居于中间。
PS:还有一种正好反过来。咱们放大的图片是左眼!!(这时候移动的距离是 -rect.left)
因此最终变成这样:
private boolean checkMatrixBounds() {
RectF rect = getDisplayRect(getDrawMatrix());
if (rect == null) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
if (height <= viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = viewHeight - rect.bottom;
}
if (width <= viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}复制代码
仍是老样子,但愿你们不要吐槽。有问题留言哈哈。。O(∩_∩)O哈哈~
附上Demo地址:ScaleImageVewDemo