这是一个图片查看库,实现图片浏览功能,支持pinch(捏合)手势或者点击放大缩小。支持在ViewPager中翻页浏览图片。
PhotoView 是一款扩展自Android ImageView ,支持通过单点/多点触摸来进行图片缩放的智能控件。功能实用和强大。
- 可以用于查看图片,并对图片进行拖动缩放,拖动过程中不会出现边缘空白;
- 双击缩小放大,Fling移动,并支持上述过程的渐变;
- 在放大情况下也支持viewpager等的拖动切换;
- 支持多击事件检测,单机,双击事件;
- 支持各种回调给调用者;
- PhotoView 是 ImageView 的子类,自然的支持所有 ImageView 的源生行为。
- 任意项目可以非常方便的从 ImageView 升级到 PhotoView,不用做任何额外的修改。
- 可以非常方便的与 ImageLoader/Picasso 之类的异步网络图片读取库集成使用。
- 事件分发做了很好的处理,可以方便的与 ViewPager 等同样支持滑动手势的控件集成。
PhotoView 的主要实现 结合 GestureDetector手势 Scroller滚动 来实现拖动滚动的效果。
当用户触摸屏幕的时候,会产生许多手势,例如down,up,scroll,Fling等等。Android给我们提供了GestureDetector(Gesture:手势Detector:识别)类,通过这个类我们可以识别很多的手势,虽然它能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。
参考文章:
http://blog.csdn.net/harvic880925/article/details/39520901
来学GestureDetector。
GestureDetector接口方法使用介绍
public class GestureListener extends GestureDetector.SimpleOnGestureListener {
/*--------------- OnGestureListener ---------------*/
/**
* 手指按下就会触发(调用onTouch()方法的ACTION_DOWN事件时触发)
*/
@Override
public boolean onDown(MotionEvent e) {
Log.i("aa", "onDown ");
return false;
}
/**
* down事件发生而move或则up还没发生前触发该事件 (100毫秒内)
* Touch了还没有滑动时触发(与onDown,onLongPress)比较onDown只要Touch down一定立刻触发。
* 而Touchdown后过一会没有滑动先触发onShowPress再是onLongPress。所以Touchdown后一直不滑动onLongPress之前触发
*/
@Override
public void onShowPress(MotionEvent e) {
Log.i("aa", " onShowPress ");
}
/**
* 一次点击up事件;在touch down后又没有滑动(onScroll),也没有长按(onLongPress),然后Touch up时触发
*/
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.i("aa", " onSingleTapUp ");
return false;
}
/**
* 滚动,触摸屏按下后移动(执行onTouch()方法的ACTION_MOVE事件时会触发)
*
* @param e1 按下的点
* @param e2 滑动后结束的点
* @param distanceX x轴方向的速度,单位:像素/秒
* @param distanceY y轴方向的速度
* @return 事件是否自己消费,拦截。
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.i("aa", "onScroll distanceX = " + distanceX + " distanceX = " + distanceX);
return false;
}
/**
* 长按,触摸屏按下后既不抬起也不移动,过一段时间后触发
*/
@Override
public void onLongPress(MotionEvent e) {
Log.i("aa", " onLongPress ");
}
/**
* 滑动,触摸屏按下后快速移动并抬起,会先触发滚动手势,跟着触发一个滑动手势
*
* @param e1 按下的点
* @param e2 滑动后结束的点
* @param velocityX x轴方向的速度,单位:像素/秒
* @param velocityY y轴方向的速度
* @return 事件是否自己消费,拦截。
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float e2X = e2.getX();
float e1X = e1.getX();
if(e2X - e1X > 50){
Log.i("aa" , " 从左往右滑");
}else if(e2X - e1X < - 50){
Log.i("aa" , " 从右往左滑");
}
Log.i("aa", " onFling velocityX = " + velocityX + " velocityY = " + velocityY);
return false;
}
/*--------------- OnDoubleTapListener ---------------*/
/**
* 1. 单击确认,用来判定该次点击是SingleTap而不是DoubleTap,如果连续点击两次就是DoubleTap手势,
* 如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为SingleTap而不是DoubleTap,
* 然后触发SingleTapConfirmed事件。
*
* onSingleTapConfirmed和onSingleTapUp的一点区别
* onSingleTapUp,只要手抬起就会执行,而对于onSingleTapConfirmed来说,如果双击的话,则onSingleTapConfirmed不会执行。
*/
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("aa", " onSingleTapConfirmed ");
return super.onSingleTapConfirmed(e);
}
/**
* 在双击的第二下,Touch down时触发 。
*/
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("aa", " onDoubleTap ");
return super.onDoubleTap(e);
}
/**
* 双击的第二下Touch down和up都会触发,可用e.getAction()区分。 双击后 改方法调用二次
*/
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.i("aa", " onDoubleTapEvent " + e.getAction());
return super.onDoubleTapEvent(e);
}
/**
* 调用顺序
* 单击
* onDown -> onSingleTapUp -> onSingleTapConfirmed
* 长按
* onDown -> onShowPress -> onLongPress
* 双击 调用该onDoubleTap 方法 说明已经发生了双击
* onDown -> onSingleTapUp -> onDoubleTap -> onDoubleTapEvent 0 -> onDown -> onDoubleTapEvent 1
* 拖动滑动 onFling触发 滑动速度大于 MINIMUM_FLING_VELOCITY = 50 否则不调用
* onDown -> onScroll -> onScroll ... -> onFling
*/
/**
* 经典例子 左右滑动的判断
* 注意点:
* 每次有效的滑动都会调用onScroll()方法,也就是说在一个完成的滑动事件中,onScroll()会进行频繁的调用,
* 这会带来一些不必要的问题。
* 而onFling()方法的优点是,在一个完整的滑动周期中,它只会调用一次,因为它代表的是一个完整的快速滑动的行为。
*
* 在onFling 中判断进行左右滑动的判断
*/
}
ScaleGestureDetector这个类是专门用于处理缩放的工具类,用法与GestureDetector类似,都是通过onTouchEvent()关联相应的MotionEvent的。
ScaleGestureDetector接口方法使用介绍
public class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
/**
* 缩放时
* 返回值代表本次缩放事件是否已被处理。
* 如果已被处理,那么detector就会重置缩放事件;如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,
* 直到被处理为止。因此,它常用在判断只有缩放值达到一定数值时才进行缩放
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
//获取比率
float scaleFactor = detector.getScaleFactor();
Log.i("aa", " onScale scaleFactor = " + scaleFactor);
if (scaleFactor < 2) {
return false;
}
Log.i("aa" , "onScale -- " + "可以做些该做的事情了 ");
//进行缩放处理
return true;
}
/**
* 缩放开始。该detector是否处理后继的缩放事件。返回false时,不会执行onScale()。
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
/**
* 缩放结束时。
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
使用参考文章 http://blog.csdn.net/u010410408/article/details/39577399
ScaleGestureDetector的一些公共方法介绍
onTouchEvent()
//关联MotionEvent。返回true代表该detector想继续接收后继的motion事件;否则反之。默认时该方法返回true。
getCurrentSpan ()
//返回手势过程中,组成该手势的两个触点的当前距离,以像素为单位的触点距离。
getEventTime ()
//返回事件被捕捉时的时间。
getFocusX ()
//返回当前手势焦点的 X 坐标。
getFocusY ()
//返回当前手势焦点的 Y 坐标。
getPreviousSpan ()
//返回手势过程中,组成该手势的两个触点的前一次距离。
getScaleFactor ()
//返回从前一个伸缩事件至当前伸缩事件的伸缩比率。该值定义为 (getCurrentSpan() / getPreviousSpan())。
getTimeDelta ()
//返回前一次接收到的伸缩事件距当前伸缩事件的时间差,以毫秒为单位。
isInProgress ()
//如果手势处于进行过程中,返回 true.
方法介绍参考文章 http://www.mamicode.com/info-detail-276128.html
Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用scroller的场景并不多,但是很多大家所熟知的控件的内部都是使用scroller来实现的,如ViewPager、ListView等.如果能够把scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似viewpager这样的功能。
参考文章
http://blog.csdn.net/guolin_blog/article/details/48719871来学习Scroller。
Scroller 的相关方法介绍
getCurrX()
//获取Scroller当前水平滚动的位置
getCurrY()
//获取Scroller当前竖直滚动的位置
getFinalX()
//获取Scroller最终停止的水平位置
getFinalY()
//获取Scroller最终停止的竖直位置
setFinalX(int newX)
//设置Scroller最终停留的水平位置,没有动画效果,直接跳到目标位置
setFinalY(int newY)
//设置Scroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
startScroll(int startX, int startY, int dx, int dy)
//使用默认完成时间250ms
startScroll(int startX, int startY, int dx, int dy, int duration)
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)
//基于快速滑动触发的滚动, 滚动的距离 由初始的fling速度决定
//startX 开始的X坐标
//startY 开始的Y坐标
//velocityX 初始速度X
//velocityY 初始速度Y (可以使用系统仍为的速度值)
//minX 最小的X坐标,
//maxX 最大的X坐标
//minY 最小的Y坐标
//maxY 最大的Y坐标 scroller不能滚动出超过这个坐标边界
//overX fling滚动超过有效值的范围
//overY fling滚动超过有效值的范围 水平 滚动越过两个方向都有可能
computeScrollOffset()
//返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。
isFinished()
//用来判断当前滚动是否结束
OverScroller 的相关方法介绍
PhotoView 使用到了 OverScroller
OverScroller 和 Scroller 有什么区别呢 ?
这两个类80%的API是一致的。OverScroller 是 Scroller 的加强版,增加了滚出视图范围之后的回弹效果。
OverScroller新增方法介绍
isOverScrolled()
//是否超出滚动边界
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
//当你想回滚的时候,回滚的范围在有效的坐标范围内
notifyHorizontalEdgeReached(int startX, int finalX, int overX)
notifyVerticalEdgeReached(int startY, int finalY, int overY)
//通知滚动是否到达边界,通常 这个信息来处理 知道什么时候已经开始滚动
给出一个参考文章方法介绍
http://blog.csdn.net/u013598111/article/details/50198101?locationNum=3&fps=1
Scroller的使用完成简单的切换效果
本例子使用的
http://blog.csdn.net/guolin_blog/article/details/48719871中的例子,增加了一些自己理解的注释
public class ScrollerLayout extends ViewGroup {
/**
* 用于完成滚动操作的实例
*/
private Scroller mScroller;
/**
* 判定为拖动的最小移动像素数
*/
private int mTouchSlop;
/**
* 手机按下时的屏幕坐标
*/
private float mXDown;
/**
* 手机当时所处的屏幕坐标
*/
private float mXMove;
/**
* 上次触发ACTION_MOVE事件时的屏幕坐标
*/
private float mXLastMove;
/**
* 界面可滚动的左边界
*/
private int leftBorder;
/**
* 界面可滚动的右边界
*/
private int rightBorder;
public ScrollerLayout(Context context) {
this(context, null);
}
public ScrollerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
// 获取TouchSlop值
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为ScrollerLayout中的每一个子控件测量大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为ScrollerLayout中的每一个子控件在水平方向上进行布局
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
//初始化左右边界值
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(childCount - 1).getRight();
}
}
/*
* 如果是 拖拽 那么拦截掉事件传递 自己处理事件 走 onTouchEvent
* */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下时的屏幕坐标
mXDown = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//移动时的坐标
mXMove = ev.getRawX();
//移动的绝对值 差值
float diff = Math.abs(mXMove - mXDown);
mXLastMove = mXMove;
// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
if (diff > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
//上次记录的 坐标 减去本次移动的左边 得到 本地移动的距离
int scrolledX = (int) (mXLastMove - mXMove);
//如果移动的距离超出边界值 则 设置到边界
//getScrollX() 获取移动的距离 每次移动的距离 + 已经移动的距离 如果超过边界值,则设置到最大位置处
if (getScrollX() + scrolledX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
//右边界需要加上整个view的宽度
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
//否则 自身不断移动
scrollBy(scrolledX, 0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
//当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
//第几个位置宽度 - 移动的距离 = 还需滚动的距离
int dx = targetIndex * getWidth() - getScrollX();
/*--------------- scroller使用完成滚动 ---------------*/
//通过scroller完成后续的滚动动作
//设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,
// 再重写View的computeScroll(),完成实际的滚动。
//开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为(startX+dx , startY+dy)处。
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
/**
* computeScroll()是View的一个空方法,在父容器重画自己的孩子时,它会调用孩子的computeScroll方法。
*
*/
@Override
public void computeScroll() {
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
//获取mScroller当前水平滚动的位置
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// invalidate();
postInvalidate();
}
}
}
OverScroller的使用
使用了OverScroller的springBack()方法
public class JellyTextView extends TextView {
private OverScroller mScroller;
private float lastX;//记录view停止滑动的X位置
private float lastY;//记录view停止滑动的Y位置
private float startX;//记录最初始位置X坐标
private float startY;//记录最初始位置Y坐标
public JellyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
//实例化对象OverScroller
mScroller = new OverScroller(context, new BounceInterpolator());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//第一次获取静止的坐标
lastX = event.getRawX();
lastY = event.getRawY();
return true; //注意,这里是让事件不要被拦截,继续触发ACTION_MOVE,ACTION_UP
case MotionEvent.ACTION_MOVE:
//移动的偏移量
float disX = event.getRawX() - lastX;
float disY = event.getRawY() - lastY;
//完成控件的位置移动
offsetLeftAndRight((int) disX);
offsetTopAndBottom((int) disY);
//获取移动后的坐标
lastX = event.getRawX();
lastY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
//移动view到原位置,同时触发computeScroll();
// mScroller.startScroll((int)getX(), (int)getY(), -(int) (getX() - startX),
// -(int) (getY() - startY));
spingBack();
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) { //判断当前的滑动动作是否完成的
setX(mScroller.getCurrX());
setY(mScroller.getCurrY());
invalidate();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//记录View 的初始位置
startX = getX();
startY = getY();
Log.d("aa", "onSizeChanged startX = " + startX + " startY = " + startY);
}
/**
* 参数,前两个是开始位置,是绝对坐标,minX和maxX是用来设定滚动范围的,
* 也是绝对坐标范围,如果startX不在这个范围里面,
* 比如大于maxX,就会触发computeScroll(),我们可以移动距离,
* 最终回弹到maxX所在的位置,并返回true,从而完成后续的滚动效果,比minX小的话,
* 就会回弹到minX,一样的道理。所以我们可以像上面代码里面一样,判断是否在范围内,在的话,
* 就invalidate()一下,触发滚动动画
*/
public void spingBack() {
//回滚到初始位置
if (mScroller.springBack((int) getX(), (int) getY(), 0, (int) startX, 0,
(int) startY)) {
Log.d("aa", "getX()=" + getX() + "__getY()=" + getY());
invalidate();
}
}
}
自定义的textView 放入到布局文件中使用,即可看到效果。。
PhotoViewAttacher是对ImageView的核心处理类
public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
/**
* 这个是防止内存泄漏的一个技巧,注意外面activity的imageView,现在这里的imageView也是指向外层的imageView的那个对象,
* 那么就相当于有2个引用指向同一块地址,当外层的那个imageView对象成为了null时,这里面还是还一个强引用所指向,因此并不会
* 被销毁,在这里使用了弱引用的好处,当外层的imageView被销毁时,里面imgview会被销毁掉。
* 合理的释放了imageView这个对象,避免造成不必要的内存泄漏。
* 参考文章http://blog.csdn.net/matrix_xu/article/details/8424038
* 主要作用,避免造成内存泄漏。
*/
mImageView = new WeakReference<>(imageView);
/*
在调用getDrawingCache()方法从ImageView对象获取图像之前,一定要调用setDrawingCacheEnabled(true)方法:
*/
imageView.setDrawingCacheEnabled(true);
/*设置imgview的touch事件*/
imageView.setOnTouchListener(this);
/**
* view的变化监听器
* --------------- OnGlobalLayoutListener ---------------
* @Override
* public void onGlobalLayout() {
* //View 发生变化的时候调用事件
* }
*
* interface ViewTreeObserver.OnDrawListener
* 挡在一个视图树绘制时,所要调用的回调函数的接口类(level 16)
*
* interface ViewTreeObserver.OnGlobalFocusChangeListener
* 当在一个视图树中的焦点状态发生改变时,所要调用的回调函数的接口类
*
* interface ViewTreeObserver.OnGlobalLayoutListener
* 当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类
*
* interface ViewTreeObserver.OnPreDrawListener
* 当一个视图树将要绘制时,所要调用的回调函数的接口类
*
* interface ViewTreeObserver.OnScrollChangedListener
* 当一个视图树中的一些组件发生滚动时,所要调用的回调函数的接口类
*
* interface ViewTreeObserver.OnTouchModeChangeListener
* 当一个视图树的触摸模式发生改变时,所要调用的回调函数的接口类
*
* 参考文章 http://blog.csdn.net/pipisky2006/article/details/8480106
*/
ViewTreeObserver observer = imageView.getViewTreeObserver();
if (null != observer)
//View 发生变化的时候调用事件
observer.addOnGlobalLayoutListener(this);
// Make sure we using MATRIX Scale Type
/**
* 设置imageView scaleType matrix ,可以随着matrix矩阵进行变换
* 在PhotoView中imageView 始终都是将scaleType设置成这个属性的
*/
setImageViewScaleTypeMatrix(imageView);
/**
* 这个是让你在可视化界面能看到预览效果的
*/
if (imageView.isInEditMode()) {
return;
}
// Create Gesture Detectors...
mScaleDragDetector = VersionedGestureDetector.newInstance(
imageView.getContext(), this);
//设置系统GestureDetector 手势识别
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
/**
* 长按,触摸屏按下后既不抬起也不移动,过一段时间后触发
*/
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
/**
* 滑动,触摸屏按下后快速移动并抬起,会先触发滚动手势,跟着触发一个滑动手势
*
* @param e1 按下的点
* @param e2 滑动后结束的点
* @param velocityX x轴方向的速度,单位:像素/秒
* @param velocityY y轴方向的速度
* @return 事件是否自己消费,拦截。
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (mSingleFlingListener != null) {
if (getScale() > DEFAULT_MIN_SCALE) {
Log.i("aa", "getScale " + getScale() + " velocityX " + velocityX + " velocityY " + velocityY);
return false;
}
if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
|| MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
Log.i("aa", "MotionEventCompat.getPointerCount(e1) " + MotionEventCompat.getPointerCount(e1) + " MotionEventCompat.getPointerCount(e2) " + MotionEventCompat.getPointerCount(e2) + " velocityX " + velocityX + " velocityY " + velocityY);
return false;
}
return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
}
return false;
}
});
//设置了setOnDoubleTapListener 该 DefaultOnDoubleTapListener 实现了 单击确认 双击
mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
//设置默认的旋转角度
mBaseRotation = 0.0f;
// Finally, update the UI so that we're zoomable
//最后,更新UI,这样我们可缩放
setZoomable(zoomable);
}
在构造方法中,对ImageView做了设置,使用了WeakReference,设置ImageView的OnTouchListener接口,所要实现方法:onTouch(), 设置了View的变化监听addOnGlobalLayoutListener(),它所要实现的方法是:onGlobalLayout(),分别创建了 mScaleDragDetector 和 mGestureDetector,mScaleDragDetector就是实现pinch手势的后面会进行讲解,mGestureDetector就是上面说到的手势识别,系统帮我们定义好的,这里分别是现了onLongPress() 和 onFling() 。设置了setOnDoubleTapListener 该 DefaultOnDoubleTapListener 看了上面手势识别的介绍,相信这里的几个方法的作用应该都不难理解。
那么就继续看 setZoomable 之后是怎么操作的,如何初始化设置图片。
@Override
public void setZoomable(boolean zoomable) {
mZoomEnabled = zoomable;
update();
}
可以看到 mZoomEnabled = zoomable; 在这里赋值了,是否需要进行手势缩放处理。继续upDate();
public void update() {
//getImgageView() 从WeakReference中获取imageView
ImageView imageView = getImageView();
if (null != imageView) {
if (mZoomEnabled) {
// Make sure we using MATRIX Scale Type
//再次检查 ImageView的ScaleType为MATRIX
setImageViewScaleTypeMatrix(imageView);
// Update the base matrix using the current drawable
//更新基础矩阵mBaseMatrix
updateBaseMatrix(imageView.getDrawable());
} else {
// Reset the Matrix...
//重置矩阵
resetMatrix();
}
}
}
在构造的时候其实已经有调用过了 setImageViewScaleTypeMatrix() 这里再次检查了一次,设置ImageView的ScaleType为matrix。来看一下这个方法吧。
/**
* Sets the ImageView's ScaleType to Matrix.
*/
private static void setImageViewScaleTypeMatrix(ImageView imageView) {
/**
* PhotoView sets its own ScaleType to Matrix, then diverts all calls
* setScaleType to this.setScaleType automatically.
*/
if (null != imageView && !(imageView instanceof IPhotoView)) {
if (!ScaleType.MATRIX.equals(imageView.getScaleType())) {
imageView.setScaleType(ScaleType.MATRIX);
}
}
}
将ImageView的ScaleType设置为matrix,通过矩阵对图片进行变换。 updateBaseMatrix(imageView.getDrawable());
这个就是更新基础矩阵mBaseMatrix,先来看一下实现。
private void updateBaseMatrix(Drawable d) {
ImageView imageView = getImageView();
if (null == imageView || null == d) {
return;
}
//获取ImageView的宽高
final float viewWidth = getImageViewWidth(imageView);
final float viewHeight = getImageViewHeight(imageView);
//获取原始图片的大小
final int drawableWidth = d.getIntrinsicWidth();
final int drawableHeight = d.getIntrinsicHeight();
//重置baseMatrix
mBaseMatrix.reset();
//获取宽高缩放比
final float widthScale = viewWidth / drawableWidth;
final float heightScale = viewHeight / drawableHeight;
Log.i("aa", "viewWidth = " + viewWidth + " viewHeight = " + viewHeight + " drawableWidth = " + drawableWidth + " drawableHeight = " + drawableHeight + " widthScale = " + widthScale + " heightScale = " + heightScale);
/**
* 这个方法就是 根据 mScaleType 调整显示位置和缩放级别,使其达到ImageView的ScaleType效果。
* mBaseMatrix存储ScaleType变换的矩阵 初始化后基本不变了,重新设置ScaleType后更改
*/
if (mScaleType == ScaleType.CENTER) {
mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
(viewHeight - drawableHeight) / 2F);
} else if (mScaleType == ScaleType.CENTER_CROP) {
float scale = Math.max(widthScale, heightScale);
mBaseMatrix.postScale(scale, scale);
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);
} else if (mScaleType == ScaleType.CENTER_INSIDE) {
float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
mBaseMatrix.postScale(scale, scale);
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);
} else {
//FIT_XX相关的缩放类型
RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
if ((int) mBaseRotation % 180 != 0) {
mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
}
switch (mScaleType) {
case FIT_CENTER:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
break;
case FIT_START:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
break;
case FIT_END:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
break;
case FIT_XY:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
break;
default:
break;
}
}
//更新矩阵设置到imageView上
resetMatrix();
}
ScaleType 效果参考 http://blog.csdn.net/larryl2003/article/details/6919513通过矩阵对图片进行变换 mBaseMatrix用来保存根据ScaleType调整过的的原始矩阵。scaleType:可以看出,ImageView总是设置为Matrix的,在xml中配置为其他了,到了这里也会被修改,没有任何效果;但是代码中却支持其他类型,原因就是Imageview依然是Matrix的,但PhotoViewAttacher中内部会维护一个scaleType类型,在进行base矩阵初始化updateBaseMatrix,会根据内部的类型去动态改变显示的位置等。但是scaleType需要设定在PhotoViewAttacher中才有效果,即调用setScaleType(ScaleType scaleType)才行,否则内部默认是FitCenter,而Imageview一直为matrix。不过注意PhotoViewAttacher.setScaleType(ScaleType scaleType)不要传入matrix,否则会异常。可以去看下ImageView源码中的configureBounds()方法也是这样子的(源码比较高深- -!)
mBaseMatrix的作用: 用来保存根据ScaleType调整过的的原始矩阵
关于ScaleToFit作用:
Matrix.ScaleToFit定义了四种选项:
* CENTER: 保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。
* START:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。START提供左上对齐
* END:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。END提供右下对齐
* FILL: 可能会变换矩形的长宽比,保证变换和目标矩阵长宽一致。
参考文章 http://blog.csdn.net/briup_acmer/article/details/47020079
继续向下resetMatrix()更新矩阵了。
/**
* Resets the Matrix back to FIT_CENTER, and then displays it.s
*/
private void resetMatrix() {
mSuppMatrix.reset(); //重置mSuppMatrix矩阵
setRotationBy(mBaseRotation); //设置旋转角度 默认就是 0
setImageViewMatrix(getDrawMatrix()); //设置矩阵到imageView上
checkMatrixBounds();//检查Matrix边界
}
关于Matrix矩阵的 set pre post :
set — set是直接设置matrix的值,每次set一次, 整个matrix的值都会变掉。所以如果进行组合变换的话,只能在第一次使用set,以后要使用post和pre,也可以只是用post。
pre — 可以看到pre操作是拿参数给出的矩阵右乘当前矩阵M,即pre执行的是一个右乘的操作。而在图形操作中,越靠近右边的矩阵就越先执行。所以,pre操作是在当前矩阵的最前面执行的。
post — post是拿参数给出的矩阵左乘当前矩阵M,即post执行的是一个左乘的操作。所以,post操作是在当前矩阵的最后面执行的。所以完全可以只使用post操作来完成所需的变换。
参考文章 感兴趣的可以深入学习
http://www.07net01.com/2015/08/902338.html
http://www.2cto.com/kf/201605/510416.html
重置了 mSuppMatrix 矩阵,setRotationBy(mBaseRotation);设置旋转的角度。
@Override
public void setRotationBy(float degrees) {
mSuppMatrix.postRotate(degrees % 360);
checkAndDisplayMatrix();
}
先注意 这个角度是 给 mSuppMatrix矩阵的,并不是 mBaseMatrix,mBaseMatrix记录ScaleType变换,基本就不会在变动了,除非是重新设置了 ScaleType,才会重新设置mBaseMatrix,而mSuppMatrix是记录存储需要变换的值的,PhotoView中总共使用到了三个Matrix,还有一个稍后会讲到。接着看 checkAndDisplayMatrix();
/**
* Helper method that simply checks the Matrix, and then displays the result
* 检查Matrix边界并进行显示
*/
private void checkAndDisplayMatrix() {
if (checkMatrixBounds()) {
setImageViewMatrix(getDrawMatrix());
}
}
检查Matrix的边界并调用setImageViewMatrix(getDrawMatrix())设置到ImageView上,就可以看到最终的效果了。先看下checkMatrixBounds()检查边界的方法。
/**
* 检查矩阵是否在边界上,设置postTranslate 对位置进行调转
*/
private boolean checkMatrixBounds() {
//看是有检查边界的和没有检查边界的区别,这里直接return 掉
// if(true){
// return true;
// }
final ImageView imageView = getImageView();
if (null == imageView) {
return false;
}
//获取显示在界面上图片的区域矩形
final RectF rect = getDisplayRect(getDrawMatrix());
if (null == rect) {
return false;
}
//获取矩形的宽高
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0; //计算调整边界时要平移的距离
//下面就是根据缩放的 类型进行边界的调整
//计算 高度 deltaY 的距离
final int viewHeight = getImageViewHeight(imageView);
//如果图片的高度小于等于View的高度,说明图片的垂直方向是可以完全显示在view中的
if (height <= viewHeight) {
//根据缩放类型进行边界的调整
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top; //fit_start 向上移动到View的顶部
break;
case FIT_END:
deltaY = viewHeight - height - rect.top; //fit_end 向下移动到View底部
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top; //否则就居中显示
break;
}
} else if (rect.top > 0) {
//如果图片的高度超出来View的高度,但是 图片的高度是 大于0 的 说明 距离边界时有有一段 空的区域的,因此需要设置偏移距离
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
//同理 底部 有空的区域
deltaY = viewHeight - rect.bottom;
}
//计算 宽度 deltaX 的距离
final int viewWidth = getImageViewWidth(imageView);
if (width <= viewWidth) {
//如果图片的宽度小于View的宽度,也是同样说明图片的横向是可以完全显示在view中的
//根据不同的类型进行调整
switch (mScaleType) {
case FIT_START:
deltaX = -rect.left;
break;
case FIT_END:
deltaX = viewWidth - width - rect.left;
break;
default:
deltaX = (viewWidth - width) / 2 - rect.left;
break;
}
mScrollEdge = EDGE_BOTH; //图片的两边都在边界内
} else if (rect.left > 0) {
//如果图片的左边 大于了 0 说明图片 移动到了 边界内部,应该偏移图片的 左边位置
mScrollEdge = EDGE_LEFT; // 左边在边界内
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mScrollEdge = EDGE_RIGHT; //图片在右边边界内 偏移右边
} else {
mScrollEdge = EDGE_NONE; //两边 都不在 边界内 图片放大 显示中间部分的情况
}
// Finally actually translate the matrix
//最后讲计算出来的偏移值 需要进行的位置调整交给了 mSuppMatrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}
当你进行旋转或缩放变换后,由于缩放的锚点是以手指为中心的,有时候会发现显示的区域不对,比如说,当图片大于View的宽高时,但是矩阵的边界与View之间居然还有空白区,显然不太合理。这时需要进行平移对齐View的宽高。
看是有检查边界的和没有检查边界的区别,最方便的做法就是 在checkMatrixBounds()方法直接return true 然后运行Demo看效果。。。
接着setImageViewMatrix(getDrawMatrix()); 设置Matrix到ImageView的方法,需要一个矩阵,getDrawMatrix(),这方法,这里要就使用到第三个矩阵了。
/**
* getDrawMatrix()用来获取mDrawMatrix最终矩阵,
* mDrawMatrix其实是在mBaseMatrix基础矩阵上后乘mSuppMatrix供应矩阵产生的。
*/
private Matrix getDrawMatrix() {
mDrawMatrix.set(mBaseMatrix);
mDrawMatrix.postConcat(mSuppMatrix);
return mDrawMatrix;
}
给ImageView设置的都是 mDrawMatrix 这个矩阵,mDrawMatrix的作用就知道了,是需要被设置的 最终变换使用的,由上下两个合成而来。
/**
* 通过setImageViewMatrix将最终的矩阵应用到ImageView中,这时就能看到显示效果了。
*/
private void setImageViewMatrix(Matrix matrix) {
ImageView imageView = getImageView();
if (null != imageView) {
//再次确认Imageview 的ScaleType
checkImageViewScaleType();
//通过matrix 矩阵 设置imageView
imageView.setImageMatrix(matrix);
// Call MatrixChangedListener if needed
if (null != mMatrixChangeListener) {
RectF displayRect = getDisplayRect(matrix);
if (null != displayRect) {
mMatrixChangeListener.onMatrixChanged(displayRect);
}
}
}
}
在这里可以在把getDisplayRect(matrix);说一下,看代码,
/**
* Helper method that maps the supplied Matrix to the current Drawable
*
* @param matrix - Matrix to map Drawable against
* @return RectF - Displayed Rectangle
*
* 获取imageView 图片矩形的方法
*/
private RectF getDisplayRect(Matrix matrix) {
ImageView imageView = getImageView();
if (null != imageView) {
Drawable d = imageView.getDrawable();
if (null != d) {
//获取drawable 的尺寸
mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
d.getIntrinsicHeight());
//将矩阵的变换映射给 mDisplayRect 得到最终的矩形
matrix.mapRect(mDisplayRect);
return mDisplayRect;
}
}
return null;
}
一些listener的作用,大家可以自己理解它的作用,就不分析了。看到了这里,或许大家还忘了一个东西,那就是ViewTreeObserver.OnGlobalLayoutListener()的监听,当View发生变化的时候会进行调用的方法,当启动Activity,要走一系列的绘制流程,这个过程是耗时的,虽然平时打开页面,都是立马看到界面的效果,但是这个过程是需要一定的时间,ImageView从没有到绘制出来,那么ImageView就是发生变化了,会调用OnGlobalLayoutListener的回调方法,onGlobalLayout();
@Override
public void onGlobalLayout() {
ImageView imageView = getImageView();
if (null != imageView) {
if (mZoomEnabled) {
/**
* 获取imgview的四个坐标值 只要这view的大小不发生变化 ,四个坐标值就不会发生变化的
* 当imgview的大小发生变化,onGlobalLayout 该方法会重新进行调用,设置最新的四个边界值
*/
final int top = imageView.getTop();
final int right = imageView.getRight();
final int bottom = imageView.getBottom();
final int left = imageView.getLeft();
/**
* We need to check whether the ImageView's bounds have changed.
* This would be easier if we targeted API 11+ as we could just use
* View.OnLayoutChangeListener. Instead we have to replicate the
* work, keeping track of the ImageView's bounds and then checking
* if the values change.
*/
if (top != mIvTop || bottom != mIvBottom || left != mIvLeft
|| right != mIvRight) {
// Update our base matrix, as the bounds have changed
//更新矩阵
updateBaseMatrix(imageView.getDrawable());
// Update values as something has changed
//记录 四个坐标值
mIvTop = top;
mIvRight = right;
mIvBottom = bottom;
mIvLeft = left;
}
} else {
updateBaseMatrix(imageView.getDrawable());
}
}
}
获取ImageView的四个坐标值,mIvTop,mIvBottom,mIvLeft,mIvRight,并没有在其它地方进行赋值,第一次初始化肯定是为 0的,那么必定会进入到if语句中,并且更新矩阵调用 updateBaseMatrix(imageView.getDrawable());之后给mIvTop,mIvBottom,mIvLeft,mIvRight,记录住View的四个坐标值。
onGlobalLayout()中的代码其实和update()的方法是很相似的,大家也可以在demo中这两个地方打印Log看下初始化的过程中是哪个方法先调用。。。
其实在 setZoomable()方法中注释掉update()方法也是可以的。当ImageView绘制完毕,onGlobalLayout必定会进行一次调用。(为什么在setZoomable()中也调用一次update,估计也是以防万一吧(mZoomEnabled还没赋值,onGlobalLayout()方法就已经执行了)。代码的严谨性很重要,呃。。我是这样理解的。)
看完以上的源码,相信流程已经非常清楚了,当设置图片时,通过update()我们可以初始化一个mBaseMatrix,然后如果想缩放、旋转等,进行设置应用到mSuppMatrix,最终通过对mBaseMatrix和mSuppMatrix计算得到mDrawMatrix,然后应用到ImageView中,整个过程也就结束了。
那么接下来就是用户触摸的各种处理了,onTouch事件处理。
@Override
public boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
//可以缩放并且imgageView上有图片时 才响应事件
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
// First, disable the Parent from intercepting the touch
// event
//申请父控件不要拦截我的DOWN事件
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(true);
} else {
LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
}
// If we're flinging, and the user presses down, cancel
// fling
//按下的时候,取消fling事件
cancelFling();
break;
case ACTION_CANCEL:
case ACTION_UP:
// If the user has zoomed less than min scale, zoom back
// to min scale
//原始的图片默认是1.0 如果小于 将恢复到最小状态
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();//获取图片矩形
if (null != rect) {
//关于 View.post的理解 http://www.jianshu.com/p/b1d5e31e2011
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
// Try the Scale/Drag detector
/*--------------- 通过手势处理当前的事件 ---------------*/
//如果mScaleDragDetector(缩放、拖拽)不为空,让它处理事件
if (null != mScaleDragDetector) {
//是否在缩放
boolean wasScaling = mScaleDragDetector.isScaling();
//是否在拖拽
boolean wasDragging = mScaleDragDetector.isDragging();
handled = mScaleDragDetector.onTouchEvent(ev);
//mScaleDragDetector处理事件过后的状态,如果前后都不在缩放和拖拽
boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
//都不在缩放和拖拽 就允许父布局拦截的标识
mBlockParentIntercept = didntScale && didntDrag;
}
// Check to see if the user double tapped
// 如果mGestureDetector(双击,长按)不为空,交给它处理事件
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
只有当可以缩放,并且ImageView有图片显示才能处理手势监听,当按下DOWN的时候,有父控件,申请父控件不要拦截ImageView的事件 通过parent.requestDisallowInterceptTouchEvent(true);
调用cancelFling();
private void cancelFling() {
if (null != mCurrentFlingRunnable) {
mCurrentFlingRunnable.cancelFling();
mCurrentFlingRunnable = null;
}
}
取消fling事件,也就是Scroller的滚动,当然你第一次按下的时候,肯定不会调用这个东西,还并没有实例化过。mCurrentFlingRunnable在滑动的时候会讲到。
抬起UP的时候,获取图片的当前Scale如果小于默认的1.0 会将图片恢复到1.0的状态。这个AnimatedZoomRunnable就是实现图片放大缩小过程中的动画了。
同时mScaleDragDetector mGestureDetector当前的手势事件。
在构造中mScaleDragDetector = VersionedGestureDetector.newInstance( imageView.getContext(), this);
VersionedGestureDetector这里面只有一个静态方法,
public final class VersionedGestureDetector {
public static GestureDetector newInstance(Context context, OnGestureListener listener) {
final int sdkVersion = Build.VERSION.SDK_INT;
GestureDetector detector;
if (sdkVersion < Build.VERSION_CODES.ECLAIR) { //2009: Android 2.0
Log.i("aa", " CupcakeGestureDetector sdkversion" + sdkVersion);
detector = new CupcakeGestureDetector(context);
} else if (sdkVersion < Build.VERSION_CODES.FROYO) { // 2010: Android 2.2
detector = new EclairGestureDetector(context);
Log.i("aa", " EclairGestureDetector sdkversion" + sdkVersion);
} else {
detector = new FroyoGestureDetector(context); //4.0以下的手机都已经基本没有了,,默认就是创建FroyoGestureDetector
Log.i("aa", " FroyoGestureDetector sdkversion" + sdkVersion);
}
//设置OnGestureListener
detector.setOnGestureListener(listener);
return detector;
}
}
根据android不同的版本创建相应的GestureDetector。newInstance中需要传入OnGestureListener。设置给GestureDetector。在看下这个接口中都定义了什么方法吧。
public interface OnGestureListener {
//拖拽
void onDrag(float dx, float dy);
// onFling
void onFling(float startX, float startY, float velocityX, float velocityY);
// 缩放
void onScale(float scaleFactor, float focusX, float focusY);
}
源码中有三个GestureDetector,分别是FroyoGestureDetector,EclairGestureDetector,CupcakeGestureDetector,它们是相互继承的关系,FroyoGestureDetector继承EclairGestureDetector,EclairGestureDetector继承CupcakeGestureDetector。虽然做了版本控,创建相应GestureDetector,但是现在4.0以下的手机基本都已经没有了,可以说默认都是创建的FroyoGestureDetector。先来瞧瞧FroyoGestureDetector,我们需要去找到三个方法都在哪里被调用了。
@TargetApi(8)
public class FroyoGestureDetector extends EclairGestureDetector {
protected final ScaleGestureDetector mDetector;
public FroyoGestureDetector(Context context) {
super(context);
//处理缩放
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
return false;
//缩放时,调用 OnGestureListener onScale 传递出去
mListener.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY());
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// NO-OP
}
};
mDetector = new ScaleGestureDetector(context, mScaleListener);
}
//是否在缩放
@Override
public boolean isScaling() {
return mDetector.isInProgress();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
try {
mDetector.onTouchEvent(ev);
//还会去走父类EclairGestureDetector的 onTouchEvent
return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) {
// Fix for support lib bug, happening when onDestroy is
return true;
}
}
}
FroyoGestureDetector所干的事情,就是对图片进行缩放处理,构造中创建ScaleGestureDetector,ScaleGestureDetector这个就是我们上面讲到的,专门用于处理缩放。缩放时调用mListener.onScale将值回调出去。
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
Log.i("aa", "onScale scaleFactor " + scaleFactor + " focusX " + focusX + " focusY " + focusY);
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f",
scaleFactor, focusX, focusY));
}
//监听
if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
if (null != mScaleChangeListener) {
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
//将变化的值交给 mSuppMatrix
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
//检查边界设置图片
checkAndDisplayMatrix();
}
}
onScale的实现很简单 通过mSuppMatrix.postScale和checkAndDisplayMatrix()来进行显示缩放。
@TargetApi(5)
public class EclairGestureDetector extends CupcakeGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private int mActivePointerIndex = 0;
public EclairGestureDetector(Context context) {
super(context);
}
//根据当前手指的索引获取坐标
@Override
float getActiveX(MotionEvent ev) {
try {
return ev.getX(mActivePointerIndex);
} catch (Exception e) {
return ev.getX();
}
}
@Override
float getActiveY(MotionEvent ev) {
try {
return ev.getY(mActivePointerIndex);
} catch (Exception e) {
return ev.getY();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//获取第一根手指id,当有多个手指按下时,只会相应第一个按下的手指,其余不响应
mActivePointerId = ev.getPointerId(0);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//最后一个手指抬起时会调用
mActivePointerId = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
// Ignore deprecation, ACTION_POINTER_ID_MASK and
// ACTION_POINTER_ID_SHIFT has same value and are deprecated
// You can have either deprecation or lint target api warning
//根据action找到当前手指的index
final int pointerIndex = Compat.getPointerIndex(ev.getAction());
//根据index获取手指id
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {//于按下的手指进行比较,一样的表示是有效的那根手指
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
//pointerIndex的值肯定是从0开始的,因此如果当前是0,那么就以 1 下一个点作为新的点,否则 0 为新的点,
//因为这个点有可能是第一个手指抬起后,最新按下的那个手指,ID是重复使用的,index是根据id排出来的。
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
////将id指向第二根手指
mActivePointerId = ev.getPointerId(newPointerIndex);
//获取第二根手指的当前坐标
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
}
break;
}
//将索引指向后抬起的手指
mActivePointerIndex = ev
.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
: 0);
try {
return super.onTouchEvent(ev); //调用更高一层CupcakeGestureDetector的onTouchEvent方法
} catch (IllegalArgumentException e) {
// Fix for support lib bug, happening when onDestroy is
return true;
}
}
}
EclairGestureDetector主要是修正了多点触控的问题,因为当双指触控时,我们需要获取的是最后一个手指离开屏幕时的坐标,因此需要使getActiveX/getActiveY指向正确的点。
关于手指触发规则:
a手指按下触发down,之后b手指按下没有down,然后b手指抬起,触发ACTION_POINTER_UP,接着a手指抬起触发ACTION_UP;如果a手指先抬起,b后抬起,触发顺序一样,最后还是ACTION_UP,当存在三个以上手指时,效果还是一样的,即多次ACTION_POINTER_UP,最后是ACTION_UP。
pointerId的规则:当多个手指依次按下时,他们的顺序编号为0,1,2等等此时如果0抬起了,之后有按下一个手指,那么最新按的那个手指编号是0,即编号是重复利用的,并且每次从0开始检测,如果没有手指使用,那么就给当前手指了,否则就查找下一个编号。ID是按下时确定的,之后便不变了。
pointerIndex的规则:根据当前按下的手指ID大小排序,计算出pointerIndex的大小,比如按下了三个手指,之后中间那个1抬起了,那么第三个手指的ID还是2,但是它的index变成了1. INDEX是手指个数发生变化时,根据ID排序重新计算出来的。比如如果第一个手指抬起了,那么之后的所有手指的index都会减1.
public class CupcakeGestureDetector implements GestureDetector {
protected OnGestureListener mListener;
private static final String LOG_TAG = "CupcakeGestureDetector";
float mLastTouchX;
float mLastTouchY;
final float mTouchSlop;
final float mMinimumVelocity;
@Override
public void setOnGestureListener(OnGestureListener listener) {
this.mListener = listener;
}
public CupcakeGestureDetector(Context context) {
/**
* ViewConfiguration类
* 获得一些关于timeouts(时间)、sizes(大小)、distances(距离)的标准常量值 。
*/
final ViewConfiguration configuration = ViewConfiguration
.get(context);
// 获取fling的最小速度,单位是每秒多少像素 默认50
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//获得一个触摸移动的最小像素值。也就是说,只有超过了这个值,才代表我们该滑屏处理了。
mTouchSlop = configuration.getScaledTouchSlop();
}
private VelocityTracker mVelocityTracker;
private boolean mIsDragging;
float getActiveX(MotionEvent ev) {
return ev.getX();
}
float getActiveY(MotionEvent ev) {
return ev.getY();
}
@Override
public boolean isScaling() {
return false;
}
@Override
public boolean isDragging() {
return mIsDragging;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
/**
* VelocityTracker 是一个帮助类,看注释意思是 辅助跟踪触摸事件的速度 根据触摸位置计算每像素的移动速率。
* mVelocityTracker.addMovement(ev);来添加触摸轨迹,用于计算触摸速率。
* mVelocityTracker.computeCurrentVelocity(1000); 这个方法的意思就是根据最近的1秒的时间来计算出当前手势的速度,
* mVelocityTracker.getXVelocity();获得X轴方向的移动速率。
*/
case MotionEvent.ACTION_DOWN: {
mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
//添加触摸轨迹
mVelocityTracker.addMovement(ev);
} else {
LogManager.getLogger().i(LOG_TAG, "Velocity tracker is null");
}
//它的子类复写实现了该方法,会走 EclairGestureDetector 方法获取 根据当前手指的索引获取坐标
//按下的点
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
mIsDragging = false;
break;
}
case MotionEvent.ACTION_MOVE: {
//移动的点
final float x = getActiveX(ev);
final float y = getActiveY(ev);
//移动的距离
final float dx = x - mLastTouchX, dy = y - mLastTouchY;
if (!mIsDragging) {
// Use Pythagoras to see if drag length is larger than
// touch slop
//移动的距离是否超过系统仍为的拖拽值 ,超过则视为拖拽
mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}
if (mIsDragging) {
//拖拽的时候可以通过监听器来传递拖拽的距离,回传出去
mListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (null != mVelocityTracker) {
//添加触摸轨迹
mVelocityTracker.addMovement(ev);
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
case MotionEvent.ACTION_UP: {
if (mIsDragging) {
if (null != mVelocityTracker) {
//抬起的点
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
// Compute velocity within the last 1000ms
// 最近的1秒的时间来计算出当前手势的速度
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
.getYVelocity();
// If the velocity is greater than minVelocity, call
// listener 这里计算是否是大于系统认为fling的最小速度的这个值
if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
//回传fling
mListener.onFling(mLastTouchX, mLastTouchY, -vX,
-vY);
}
}
}
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
return true;
}
}
CupcakeGestureDetector实现了拖拽和fling效果,move的过程中,如果超过系统仍为的拖拽值,则调用OnGestureListener
接口中的onDrag方法回调出去。
@Override
public void onDrag(float dx, float dy) {
//如果当前在缩放,return
if (mScaleDragDetector.isScaling()) {
return; // Do not drag if we are already scaling
}
if (DEBUG) {
LogManager.getLogger().d(LOG_TAG,
String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy));
}
ImageView imageView = getImageView();
//移将变化的值交给 mSuppMatrix
mSuppMatrix.postTranslate(dx, dy);
//检查边界设置图片
checkAndDisplayMatrix();
/**
* Here we decide whether to let the ImageView's parent to start taking
* over the touch event.
*
* First we check whether this function is enabled. We never want the
* parent to take over if we're scaling. We then check the edge we're
* on, and the direction of the scroll (i.e. if we're pulling against
* the edge, aka 'overscrolling', let the parent take over).
*/
//判断父布局是否可以拦截这个行为 到了边缘拖拽不动就是拦截了该事件
ViewParent parent = imageView.getParent();
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
//如果拖拽的过程中,如果是在相关边缘内,就拦截
if (mScrollEdge == EDGE_BOTH
|| (mScrollEdge == EDGE_LEFT && dx >= 1f)
|| (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
}
} else {
//否则不拦截事件
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
同样的 onDrag的实现也很简单,通过mSuppMatrix.postTranslate和checkAndDisplayMatrix()来完成拖拽的动作。
继续看抬起的时候,计算fling速度的值,如果大于系统认为的最小速度,则触发Fling事件,调用OnGestureListener接口中的onFling()回调出去。
@Override
public void onFling(float startX, float startY, float velocityX,
float velocityY) {
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
"onFling. sX: " + startX + " sY: " + startY + " Vx: "
+ velocityX + " Vy: " + velocityY);
}
ImageView imageView = getImageView();
mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
//传入了fling的速度是ImageView的宽高
mCurrentFlingRunnable.fling(getImageViewWidth(imageView),
getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
imageView.post(mCurrentFlingRunnable);
}
通过 FlingRunnable 来完成 fling 后续的惯性滚动。Scroller要登场了。。
public abstract class ScrollerProxy {
/**
* 根据系统的版本提供对应的scrolle对象
*/
public static ScrollerProxy getScroller(Context context) {
if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
return new PreGingerScroller(context);
} else if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) {
return new GingerScroller(context);
} else {
return new IcsScroller(context);
}
}
public abstract boolean computeScrollOffset();
public abstract void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY,
int maxY, int overX, int overY);
public abstract void forceFinished(boolean finished);
public abstract boolean isFinished();
public abstract int getCurrX();
public abstract int getCurrY();
}
抽象类,里面根据系统版本会提供对应的Scroller对象,与VersionedGestureDetector类似的作用。接着来看FlingRunnable的实现。
private class FlingRunnable implements Runnable {
private final ScrollerProxy mScroller;
private int mCurrentX, mCurrentY;
public FlingRunnable(Context context) {
//构造中获取Scroller
mScroller = ScrollerProxy.getScroller(context);
}
public void cancelFling() {
if (DEBUG) {
LogManager.getLogger().d(LOG_TAG, "Cancel Fling");
}
//停止fling
mScroller.forceFinished(true);
}
public void fling(int viewWidth, int viewHeight, int velocityX,
int velocityY) {
//获取图片的矩形
final RectF rect = getDisplayRect();
if (null == rect) {
return;
}
// X方向 左边左边
final int startX = Math.round(-rect.left);
//fling的边界最大最小值
final int minX, maxX, minY, maxY;
//如果图片的宽度大于了View的宽度
if (viewWidth < rect.width()) {
//计算最小 最大的边界值
minX = 0;
maxX = Math.round(rect.width() - viewWidth);
} else {
// 否则就是不能动的情况
minX = maxX = startX; //如果图片宽小于View宽,就将三者设为一样。不能动
}
//Y方向 同理
final int startY = Math.round(-rect.top);
if (viewHeight < rect.height()) {
minY = 0;
maxY = Math.round(rect.height() - viewHeight);
} else {
minY = maxY = startY;
}
mCurrentX = startX;
mCurrentY = startY;
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
"fling. StartX:" + startX + " StartY:" + startY
+ " MaxX:" + maxX + " MaxY:" + maxY);
}
// If we actually can move, fling the scroller
//调用 mScroller.fling 不等的情况下调用
if (startX != maxX || startY != maxY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX,
maxX, minY, maxY, 0, 0);
}
}
@Override
public void run() {
if (mScroller.isFinished()) {
return; // remaining post that should not be handled
}
ImageView imageView = getImageView();
if (null != imageView && mScroller.computeScrollOffset()) {
//不断的获取当前的位置
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
"fling run(). CurrentX:" + mCurrentX + " CurrentY:"
+ mCurrentY + " NewX:" + newX + " NewY:"
+ newY);
}
//将平移差值应用到mSuppMatrix
mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
//设置
setImageViewMatrix(getDrawMatrix());
mCurrentX = newX;
mCurrentY = newY;
// Post On animation
//利用了Compat.postOnAnimation不停执行Runable来实现Fling惯性滚动效果。
Compat.postOnAnimation(imageView, this);
}
}
}
通过Scroller来完成后续的滚动动作,当然前面也讲到过设置Scroller滚动的位置时,并不会导致直接滚动,Scroller只是起到了记录位置的作用,最后可以通过重写View的computeScroll()方法来完成实际的滚动,当然,这里是实现是通过Compat.postOnAnimation不停执行Runable来实现。到这里就完成了一个双指缩放及拖拽的部分。当然这并还没有讲完。。。
PhotoViewAttacher里面一共使用到了 两个Runnable,FlingRunnable完成fling后续惯性的滚动,还有一个是,AnimatedZoomRunnable。
private class AnimatedZoomRunnable implements Runnable {
private final float mFocalX, mFocalY; //焦点坐标
private final long mStartTime; //开始的时间
private final float mZoomStart, mZoomEnd;
public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
final float focalX, final float focalY) {
mFocalX = focalX;
mFocalY = focalY;
mStartTime = System.currentTimeMillis();
mZoomStart = currentZoom;
mZoomEnd = targetZoom;
}
@Override
public void run() {
ImageView imageView = getImageView();
if (imageView == null) {
return;
}
//获取当前的时间的插值
float t = interpolate();
//根据插值,获取当前时间的缩放值
float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
//获取缩放比 deltaScale 表示相对于上次的缩放比
float deltaScale = scale / getScale();
//调用onScale去设置图片
onScale(deltaScale, mFocalX, mFocalY);
// We haven't hit our target scale yet, so post ourselves again
//插值小于1表示没有缩放完成,通过不停post进行执行动画
if (t < 1f) {
Compat.postOnAnimation(imageView, this);
}
}
//计算插值
private float interpolate() {
float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION;
t = Math.min(1f, t);
t = mInterpolator.getInterpolation(t);
return t;
}
}
它的作用是执行一个缩放的动画,同样是通过Compat.postOnAnimation不停执行Runable来实现Scale的效果,流程是这样的,首先会记录一个开始时间mStartTime,然后根据当前时间来获取插值interpolate()以便了解当前应该处于的进度,根据插值求出当前的缩放值scale,然后与上次相比求出缩放比差值deltaScale,然后通过onScale去设置,最终通过Compat.postOnAnimation来执行这个Runable,如此反复直到插值为1,缩放到目标值为止。这里就可以解释onTouch中的抬起Up的时候所做的事情。
//原始的图片默认是1.0 如果小于 通过AnimatedZoomRunnable将恢复到最小状态
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();//获取图片矩形
if (null != rect) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
还有一个就是 双击放大图片,这个缩放的过程也是通过AnimatedZoomRunnable来实现的。在构造中,mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
DefaultOnDoubleTapListener中实现了GestureDetector.OnDoubleTapListener接口。实现onSingleTapConfirmed,onDoubleTap,onDoubleTapEvent,具体介绍可以回到文章上面进行查看。
先来看下单击的方法 onSingleTapConfirmed();
/**
* 单击
*/
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (this.photoViewAttacher == null)
return false;
//获取ImageView
ImageView imageView = photoViewAttacher.getImageView();
//如果这个接口不为null
if (null != photoViewAttacher.getOnPhotoTapListener()) {
//获取矩形
final RectF displayRect = photoViewAttacher.getDisplayRect();
if (null != displayRect) {
//获取用户按下的 x ,y坐标
final float x = e.getX(), y = e.getY();
// Check to see if the user tapped on the photo
//矩形是否包含 该坐标 包含 则点击在图片范围内
if (displayRect.contains(x, y)) {
//计算点击百分百
float xResult = (x - displayRect.left)
/ displayRect.width();
float yResult = (y - displayRect.top)
/ displayRect.height();
//点击图片内的回调
photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult);
return true;
}else{//否则点击在图片范围外 图片外的回调
photoViewAttacher.getOnPhotoTapListener().onOutsidePhotoTap();
}
}
}
//这里还回调了 OnViewTapListener 不管点击在哪里的回调
if (null != photoViewAttacher.getOnViewTapListener()) {
photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY());
}
return false;
}
单击确认的方法中,主要是对用户点击的位置做回调处理,OnPhotoTapListener,OnViewTapListener接口的回调。
双击的方法 onDoubleTap();
/**
* 双击
*/
@Override
public boolean onDoubleTap(MotionEvent ev) {
if (photoViewAttacher == null)
return false;
try {
//获取缩放比
float scale = photoViewAttacher.getScale();
//点击的 x y 坐标
float x = ev.getX();
float y = ev.getY();
//如果缩放的比例值 是 小于 1.75倍 1.0 --> 1.75
if (scale < photoViewAttacher.getMediumScale()) {
photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true);
//如果缩放的比例值是 大于 1.75倍 && 是小于 3.0倍 3.0 --> 1.75
} else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) {
photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true);
} else {
// 1.75 -- > 1.0 通过 setScale 设置图片
photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
双击 放大支持 三种 默认的1.0 , 然后 1.75 , 3.0 ,在这个之间进行放大缩小。
private float mMinScale = DEFAULT_MIN_SCALE;
private float mMidScale = DEFAULT_MID_SCALE;
private float mMaxScale = DEFAULT_MAX_SCALE;
float DEFAULT_MAX_SCALE = 3.0f;
float DEFAULT_MID_SCALE = 1.75f;
float DEFAULT_MIN_SCALE = 1.0f;
通过setScale设置
@Override
public void setScale(float scale, float focalX, float focalY,
boolean animate) {
ImageView imageView = getImageView();
if (null != imageView) {
// Check to see if the scale is within bounds
if (scale < mMinScale || scale > mMaxScale) {
LogManager
.getLogger()
.i(LOG_TAG,
"Scale must be within the range of minScale and maxScale");
return;
}
// animate 是否需要动画
if (animate) {
imageView.post(new AnimatedZoomRunnable(getScale(), scale,
focalX, focalY));
} else {
//否则直接设置
mSuppMatrix.setScale(scale, scale, focalX, focalY);
checkAndDisplayMatrix();
}
}
}
看到了这里,setScale根据animate是否需要一个放大缩小的动画,如果不需要动画,直接设置给mSuppMatrix,然后进行检查显示。如果需要动画的话,就执行AnimatedZoomRunnable。
cleanup();回收调用 在Activity的onDestroy中调用 清空一系列属性
@SuppressWarnings("deprecation")
public void cleanup() {
if (null == mImageView) {
return; // cleanup already done
}
final ImageView imageView = mImageView.get();
if (null != imageView) {
// Remove this as a global layout listener
ViewTreeObserver observer = imageView.getViewTreeObserver();
if (null != observer && observer.isAlive()) {
observer.removeGlobalOnLayoutListener(this);
}
// Remove the ImageView's reference to this
imageView.setOnTouchListener(null);
// make sure a pending fling runnable won't be run
cancelFling();
}
if (null != mGestureDetector) {
mGestureDetector.setOnDoubleTapListener(null);
}
// Clear listeners too
mMatrixChangeListener = null;
mPhotoTapListener = null;
mViewTapListener = null;
// Finally, clear ImageView
mImageView = null;
}
到这里,PhotoView的核心部分就基本上都讲完了,缩放,拖拽滚动,双击放大。全部已经讲到,参考了许多人写的源码分析,写下了这么一篇文章。