年轻人的第一个开源框架学习——ImageViewer(二)

本文是接上一篇文章:年轻人的第一个开源框架学习——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的设置,大家可以自己去看一下。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值