本文是接上一篇文章:年轻人的第一个开源框架学习——ImageViewer(一)
还记得上篇文章提到的那些类吗,它们主要是ViewData,ScaleImageView,WxImageDragger,今天就来逐一讲讲这三个类的实现细节,当然,还有显得并不那么重要的ImageViewerUtil和GlideUtil和ImageDraggerType,也会在最后简单的提一下。准备好了吗,请系好安全带,并不老的司机又要开车了。
凡事都讲究由浅入深,那就先从最简单的ViewData开始吧!
public class ViewData {
// 目标 view 的 x 轴坐标
private float targetX;
// 目标 view 的 y 轴坐标
private float targetY;
// 目标 view 的宽度
private float targetWidth;
// 目标 view 的高度
private float targetHeight;
// 图片的原始宽度
private float imageWidth;
// 图片的原始高度
private float imageHeight;
......
此处省略构造方法以及各个声明的变量的get和set方法
......
}
如你所见,ViewData就是一个很普通的Bean类,结合ImagePagerAty来看,这里的目标view就是那个缩略图,ViewData中不仅保存着缩略图的坐标信息和宽高信息,还有所加载的网络图片的原始宽高。
简单的ViewData就当作是热身了,接下来水就慢慢开始变深了。。。先来看WxImageDragger,之前说了,这是图片下拉拖拽时的辅助类,ScaleImageView中也用到了它。
public class WxImageDragger extends ImageDragger {
......
}
WxImageDragger继承自ImageDragger,既然如此,那就把目光先转移到ImageDragger这个基类:
public class ImageDragger {
// 默认的背景透明度
protected final int DEF_BACKGROUND_ALPHA = 255;
// 在不退出浏览的情况下, Y 轴上的最大可移动距离
protected float mMaxDisOnY;
// 图片被拖拽时的背景透明度基数
protected float mAlphaBase;
// 预览背景
protected Drawable mBackground;
// 背景透明度
protected float mBackgroundAlpha;
// 预览界面的宽高
protected float mPreviewWidth, mPreviewHeight;
// 图片拖拽状态监听
protected ImageDraggerStateListener mStateListener;
protected ScaleImageView scaleImageView;
protected ImageViewerAttacher mAttacher;
......
}
关于变量的解释,注释已经说的足够清楚。需要补充的是,ImageViewerAttacher是涉及到和viewpager的相关操作,那关于这方面我们先不关心,由浅入深不是吗,咱先把单图的给研究清楚,再来谈配合viewpager的那些事儿。
public void bindScaleImageView(ScaleImageView scaleImageView) {
this.scaleImageView = scaleImageView;
}
由于ImageDragger及其子类(WxImageDragger)是用来进行ScaleImageView拖拽时处理的辅助类,自然需要绑定一个ScaleImageView。
public void bindImageViewerAttacher(ImageViewerAttacher attacher) {
this.mAttacher = attacher;
}
前面说了,先不谈这个mAttacher,跳过~
public void setBackground(Drawable drawable) {
if (drawable != null) {
mBackground = drawable.mutate();
} else {
mBackground = null;
}
}
设置背景Drawable我懂(默认是黑色背景,在ImagePagerAty已经设置: mDefDragger.setBackground(...)),但这个drawable.mutate()又是什么鬼,官方文档对它的解释是:使这个drawable可变,此操作无法撤消,并且一个可变的drawable保证不与任何其他drawable共享其状态。当你需要修改从资源加载的drawable的属性时尤其有用,默认情况下,从同一资源加载的所有drawables实例共享一个公共状态,
如果修改一个实例的状态,则所有其他实例将收到相同的修改。
好吧我还是比较想听人话:其实就是如果多个控件使用同一个Drawable,如果其中一个控件的Drawable发生改变,其他所有的Drawable都会发生改变。如果使用Drawable.mutate(),就可以从Drawable里新建一个不可变的实例,那么当这个Drawable发生改变时,不会导致其他的Drawable发生改变。
/**
* 计算相关值
*
* @param height
*/
public void calculateValue(float height) {
mMaxDisOnY = height / 5f;
mAlphaBase = mMaxDisOnY * 2;
}
/**
* 准备拖拽
*
* @param width
* @param height
*/
public void onReady(float width, float height) {
mBackgroundAlpha = DEF_BACKGROUND_ALPHA;
mPreviewWidth = width;
mPreviewHeight = height;
calculateValue(mPreviewHeight);
if (checkAttacherNotNull()) mAttacher.setViewPagerScrollable(false);//先忽略这行
setImageDraggerState(ImageDraggerState.DRAG_STATE_READY);
}
这两个方法得连着看,因为calculateValue在onReady中被调用了。
由于DEF_BACKGROUND_ALPHA=255,所以在准备开始拖拽时,背景还是纯黑的(透明度为0的意思吧),这个onReady在ScaleImageView中被调用时传入的width和height是ScaleImageView本身的宽高,如果为0则默认是屏幕宽高。然后将它们设为预览界面的宽高:mPreviewWidth和mPreviewHeight,并将高传给calculateValue,计算出mMaxDisOnY和mAlphaBase的值,关于这些变量的解释前面已经有了。
最后设置拖拽的状态为DRAG_STATE_READY,即准备拖拽。
/**
* 拖拽图片中
*/
public void onDragging(final float x1, final float y1, final float x2, final float y2) {
setImageDraggerState(ImageDraggerState.DRAG_STATE_DRAGGING);
setPreviewStatus(ImageViewerState.STATE_DRAGGING, scaleImageView);
}
这是拖拽图片中,同样是设置拖拽的状态,由于setPreviewStatus又是和Viewpager相关,故然后同样先跳过。剩下的几个方法大家自己瞅瞅就行了,很简单,这里就不再赘述了哈。
对了,ImageDraggerStateListener这个接口用于监听图片被拖拽时的状态,它里面就一个方法:
public interface ImageDraggerStateListener {
void onImageDraggerState(int state);
}
很显然,参数state就是那些拖拽状态值,它们被封装在ImageDraggerState中:
public final class ImageDraggerState {
/**
* 准备拖拽 imageView
*/
public static final int DRAG_STATE_READY = 1;
/**
* imageView 正在被拖拽中
*/
public static final int DRAG_STATE_DRAGGING = 2;
/**
* imageView 开始复位
*/
public static final int DRAG_STATE_BEGIN_REBACK = 3;
/**
* imageView 正在复位中
*/
public static final int DRAG_STATE_REBACKING = 4;
/**
* imageView 复位完毕
*/
public static final int DRAG_STATE_END_REBACK = 5;
/**
* imageView 开始退出
*/
public static final int DRAG_STATE_BEGIN_EXIT = 6;
/**
* imageView 正在退出中
*/
public static final int DRAG_STATE_EXITTING = 7;
/**
* imageView 退出完毕
*/
public static final int DRAG_STATE_END_EXIT = 8;
}
感觉已经偏离轨道了,赶紧拉回来,说完这个ImageDragger,我们终于可以谈谈它的子类WxImageDragger了!
public class WxImageDragger extends ImageDragger {
// 恢复原样的动画时间
protected final int BACK_ANIM_DURATION = 200;
// 退出预览的动画时间
private final int EXIT_ANIM_DURATION = 280;
// 默认的最小缩放比例
private final float MIN_SCALE_WEIGHT = 0.25f;
// imageView 的当前缩放比例
private float mCurScale;
// 图片在预览界面中的当前坐标
private float mCurImgX, mCurImgY;
//调整后的缩放比例
private float mAdjustScale;
//调整后的图片的宽高
private float mAdjustImgWidth, mAdjustImgHeight;
// 图片的原始宽高
private float mOriImg_width = 0, mOriImg_height = 0;
private FrameLayout.LayoutParams mImageParams;
......
}
声明的变量就在上面,下面来看看主要的几个方法。因为WxImageDragger继承自ImageDragger,所以它主要做的就是具体的实现:
@Override
public void bindScaleImageView(ScaleImageView scaleImageView) {
super.bindScaleImageView(scaleImageView);
final ViewData viewData = scaleImageView.getViewData();
final ImageView imageView = scaleImageView.getImageView();
mImageParams = (FrameLayout.LayoutParams) imageView.getLayoutParams();
Drawable drawable = imageView.getDrawable();
if (drawable != null) {
mOriImg_width = drawable.getIntrinsicWidth();
mOriImg_height = drawable.getIntrinsicHeight();
} else if (viewData.getImageWidth() != 0 && viewData.getImageHeight() != 0) {
mOriImg_width = viewData.getImageWidth();
mOriImg_height = viewData.getImageHeight();
}
}
在bindScaleImageView中:
1.从scaleImageView拿到缩略图的信息viewData和装载网络图片的imageView(其实这里是PhotoView)
2.获得imageView的布局参数,后面通过改变imageView的布局参数而控制缩放的大小
3.获得网络图片的原始宽高
@Override
public void onReady(float width, float height) {
super.onReady(width, height);
mAdjustScale = Math.min(mPreviewWidth / mOriImg_width, mPreviewHeight / mOriImg_height);
}
预览界面的宽度 / 图片的原始宽度,预览界面的高度 / 图片的原始高度,哪个小,就用哪个。
@Override
public void onDragging(float x1, float y1, float x2, float y2) {
super.onDragging(x1, y1, x2, y2);
View imageView = scaleImageView.getImageView();
// 计算 view 的坐标
final float diffX = x2 - x1;
final float diffY = y2 - y1;
final float viewX = imageView.getX() + diffX;
final float viewY = imageView.getY() + diffY;
// 计算背景透明度
if (viewY <= 0) {
mBackgroundAlpha = DEF_BACKGROUND_ALPHA;
mCurScale = 1f;
if (imageView.getY() > 0) {
mImageParams.width = (int) (mPreviewWidth * mCurScale);
mImageParams.height = (int) (mPreviewHeight * mCurScale);
imageView.setLayoutParams(mImageParams);
setBackgroundAlpha((int) mBackgroundAlpha);
}
} else {
final float value = Math.abs(viewY) / mAlphaBase;
mBackgroundAlpha = (value <= 0.8f ? 1 - value : 0.2f) * DEF_BACKGROUND_ALPHA;
// 计算缩放比例
mCurScale = Math.min(Math.max(viewY < 0 ? 1f : (1f - Math.abs(viewY) / mPreviewHeight), MIN_SCALE_WEIGHT), 1);
mImageParams.width = (int) (mPreviewWidth * mCurScale);
mImageParams.height = (int) (mPreviewHeight * mCurScale);
imageView.setLayoutParams(mImageParams);
setBackgroundAlpha((int) mBackgroundAlpha);
}
imageView.setX(viewX);
imageView.setY(viewY);
}
这块就是图片拖拽时的处理逻辑了:
1.先说方法中的四个参数:x1,y1:手指按下时的触摸点的坐标,x2,y2:手指移动时不断更新的新坐标
2.计算x和y方向上的变化量,diffX和diffY,随后更新ImageView的坐标
3.如果viewY<=0,也就是先向下拖拽再往上拉并达到这个条件后,背景就是黑色,图片保持预览时的大小
4.如果viewY>0,就是将预览图下拉拖拽时,根据viewY的值来计算value,从而不断改变背景透明度。与此同时,缩放比例mCurScale也将随着viewY变化而变化
5.设置新的坐标值和布局参数,还有背景透明度
@Override
public void onRelease() {
super.onRelease();
if (!scaleImageView.isImageAnimRunning()) {
View imageView = scaleImageView.getImageView();
final float viewX = imageView.getX();
final float viewY = imageView.getY();
mAdjustImgWidth = mOriImg_width * mAdjustScale;
mAdjustImgHeight = mOriImg_height * mAdjustScale;
// 图片在预览界面中的当前坐标
mCurImgX = viewX + (mPreviewWidth - mAdjustImgWidth) / 2 * mCurScale;
mCurImgY = viewY + (mPreviewHeight - mAdjustImgHeight) / 2 * mCurScale;
if (viewY <= mMaxDisOnY) {
reback();
} else {
exit();
}
}
}
onRelease,顾名思义,就是拖拽完松开手时的一些逻辑:
1.如果scaleImageView此时没有在执行动画,计算图片在预览界面中的当前坐标
2.如果viewY<=mMaxDisOnY,也就是还未达到缩小归位的条件,则reback恢复到拖拽前的大图预览状态
3.如果viewY>mMaxDisOnY,也就是达到条件了,就执行exit()方法,缩小归位到缩略图
reback()和exit()中就是分别完成恢复和退出的动画,大家自己应该看的明白。不过,在exit()中有一个判断值得注意:
private void exit() {
......
// 是否需要改变 imageView 的尺寸
final boolean needChangeImageSize;
if ((mCurImgX + mAdjustImgWidth * mCurScale) <= 0 || mCurImgX >= mPreviewWidth || mCurImgY >= mPreviewHeight) {
needChangeImageSize = false;
} else {
needChangeImageSize = true;
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
}
......
}
这里将设置center_crop的原因,我猜测是为了让动画更加流畅
好,终于要讲ScaleImageView了,好激动啊!
public class ScaleImageView extends FrameLayout {
// 图片的位置
private int mPosition;
// view 的相关数据
private ViewData mViewData;
// 动画执行时间
private int mDuration;
// 默认的预览界面的宽高
private float mDefWidth, mDefHeight;
// 图片拖拽处理类
private ImageDragger mImageDragger;
// 图片拖拽模式
private int mDragType;
// 是否执行背景透明度渐变
private boolean doBackgroundAlpha;
// 加载进度 view
private View progressView;
// 可缩放的 imageView
private PhotoView imageView;
private FrameLayout.LayoutParams mImageParams;
// 过渡背景
private Drawable mBackground;
// 手指按下时的坐标
private float mDownX, mDownY;
// imageView 是否正在执行动画
private boolean isImageAnimRunning;
// imageView 是否正在正在被拖拽
private boolean isImageDragging;
// 是否定义了图片尺寸
private boolean hasImageSize;
// 图片拖拽状态监听
private ImageDraggerStateListener mStateListener;
}
声明的变量如上所示,从中可以看出,ScaleImageView继承自FrameLayout,其中真正完成图片展示的其实是PhotoView
private void initView(Context context) {
mDuration = ImageViewerAttacher.DEF_DURATION;
isImageAnimRunning = false;
isImageDragging = false;
hasImageSize = false;
doBackgroundAlpha = true;
imageView = new PhotoView(context);
imageView.setX(0);
imageView.setY(0);
mImageParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
imageView.setLayoutParams(mImageParams);
addView(imageView);
initDragStateMonitor();
}
在initView中,主要是一些初始化的工作,讲imageView添加到ScaleImageView(毕竟它是个ViewGroup啊)中,最后调用initDragStateMonitor()方法:
private void initDragStateMonitor() {
mStateListener = new ImageDraggerStateListener() {
@Override
public void onImageDraggerState(int state) {
switch (state) {
case ImageDraggerState.DRAG_STATE_READY:
isImageDragging = true;
break;
case ImageDraggerState.DRAG_STATE_DRAGGING:
isImageDragging = true;
break;
case ImageDraggerState.DRAG_STATE_BEGIN_REBACK:
isImageAnimRunning = true;
isImageDragging = false;
break;
case ImageDraggerState.DRAG_STATE_REBACKING:
break;
case ImageDraggerState.DRAG_STATE_END_REBACK:
isImageAnimRunning = false;
break;
case ImageDraggerState.DRAG_STATE_BEGIN_EXIT:
isImageAnimRunning = true;
isImageDragging = false;
break;
case ImageDraggerState.DRAG_STATE_EXITTING:
break;
case ImageDraggerState.DRAG_STATE_END_EXIT:
isImageAnimRunning = false;
setVisibility(View.GONE);
break;
}
if (isImageDragging || isImageAnimRunning) {
setScaleable(false);
} else {
setScaleable(true);
}
}
};
}
initDragStateMonitor()方法用于图片拖拽状态监测,还记得之前在WxImageDragger中多次见到的setImageDraggerState(...)语句吗,就是为这个方法服务的,最终通过这些状态来确定图片是否正在被拖拽,是否正在执行动画。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = super.onInterceptTouchEvent(ev);
switch (ev.getAction() & ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
/**
* 拖拽触发条件:
* 1、仅有一个触摸点
* 2、图片的缩放等级为 1f
* 3、拖拽处理类不为空
*/
if (ev.getPointerCount() == 1 && getScale() <= 1f && mImageDragger != null) {
float diffX = ev.getX() - mDownX;
float diffY = ev.getY() - mDownY;
// 上下滑动手势
if (Math.abs(diffX) < Math.abs(diffY)) {
if ((mDragType == ImageDraggerType.DRAG_TYPE_DEFAULT) || (mDragType == ImageDraggerType.DRAG_TYPE_WX && diffY > 0)) {
mImageDragger.bindScaleImageView(this);
mImageDragger.onReady(getWidth() != 0 ? getWidth() : mDefWidth, getHeight() != 0 ? getHeight() : mDefHeight);
isIntercept = true;
}
}
}
break;
}
return isIntercept;
}
重写onInterceptTouchEvent方法,判断是否要拦截触摸事件:
若拦截,则 mImageDragger来处理触摸事件,先后要开始调用bindScaleImageView和onReady方法了,熟悉吗,这个拖拽事件的辅助类在这个时候开始起作用了;若不拦截,则 imageView自己处理触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
onActionDown(event);
break;
case MotionEvent.ACTION_MOVE:
onActionMove(event);
break;
case MotionEvent.ACTION_UP:
onActionUp(event);
break;
}
return super.onTouchEvent(event);
}
学过事件分发机制之后我们知道,当ViewGroup决定拦截事件,也就是onInterceptTouchEvent返回true时,就会来到onTouchEvent,执行自己拦截后要做的事情:
private void onActionDown(MotionEvent event) {
mDownX = event.getX();
mDownY = event.getY();
}
ACTION_DOWN:记下手指按下的坐标
private void onActionMove(MotionEvent event) {
// 拖拽图片,只有一个触摸点时触发
if (event.getPointerCount() == 1 && getScale() <= 1f && isImageDragging && mImageDragger != null) {
mImageDragger.onDragging(mDownX, mDownY, event.getX(), event.getY());
}
mDownX = event.getX();
mDownY = event.getY();
}
当只有一个触摸点触发,图片的缩放等级<=1f,拖拽处理类不为空,状态为正在拖拽时,开始执行mImageDragger的onDragging中的逻辑,具体在之前已经分析过了。
private void onActionUp(MotionEvent event) {
// 释放图片
if (getScale() <= 1f && isImageDragging && mImageDragger != null) {
mImageDragger.onRelease();
}
mDownX = 0;
mDownY = 0;
}
ACTION_UP:手指抬起之后,执行 mImageDragger.onRelease(),并将mDownX和mDownY置零
public void setImageDraggerType(@ImageDraggerType int type) {
setImageDraggerType(type, null, getBackground());
}
public void setImageDraggerType(@ImageDraggerType int type, ImageViewerAttacher attacher, Drawable background) {
mDragType = type;
if (mDragType == ImageDraggerType.DRAG_TYPE_DEFAULT) {
mImageDragger = new DefaultImageDragger();
} else if (mDragType == ImageDraggerType.DRAG_TYPE_WX) {
mImageDragger = new WxImageDragger();
}
if (mImageDragger != null) {
mImageDragger.setBackground(background);
if (attacher != null) mImageDragger.bindImageViewerAttacher(attacher);
mImageDragger.setImageDraggerStateListener(mStateListener);
}
}
之前说过,作者是预设了两种大图退出的效果的,这里就是做了个判断,这里我只关心当mImageDragger是WxImageDragger的情况。还有设置背景,设置拖拽监听。
/**
* 是否正在拖拽图片
*/
public boolean isImageDragging() {
return isImageDragging;
}
/**
* imageView 是否正在执行动画
*/
public boolean isImageAnimRunning() {
return isImageAnimRunning;
}
这两个方法也很简单,就是单纯的返回而已。接下来需要重点关注的方法已经没几个了,进场动画和退场动画就不谈了,大家可以自己去看,套路都是一样的:拿到大图和小图的各种坐标和大小信息,然后做动画,当然,这中间作者有一些细节处理,例如说setScaleType的设置,大家可以自己去看一下。