ScaleImageView图片缩放查看器

尊重劳动成果,转载请注明出处:https://blog.csdn.net/Emmanuel__/article/details/81264924

仓库地址:https://github.com/yongjianx/ScaleImageView
(使用方法详见github,源码附有详细注释,可供学习)

图片查看器支持单击、双击、长按、拖拽、多点触控缩放


效果:

这里写图片描述                                 这里写图片描述
多点触控缩放在模拟器上难以控制,多点触控效果大家可以到github仓库fork到本地编译看效果。


实现原理

ScaleImageView主要参考了PhotoView的实现,并在PhotoView的基础上做了改进,比如效果图1中允许图片偏离x方向边界,手指抬起时回弹。
ScaleImageView继承ImageView,并初始化缩放类型:

setScaleType(ScaleType.MATRIX);

关于Matrix类的使用,这篇文章写得挺好android matrix 最全方法详解与进阶(完整篇)

单击、双击、长按

使用GestureDetector类,首先创建一个GestureDetector对象并实现OnGestureListener接口

//长按、单击、双击
mGestureDetector = new GestureDetector(mImageView.getContext(), new GestureDetector.SimpleOnGestureListener(){
    @Override
    public void onLongPress(MotionEvent e) {
        Log.e(TAG, "onLongPress()...");
        if (mOnLongClickListener != null)
            mOnLongClickListener.onLongClick(mImageView);
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        Log.e(TAG, "onSingleTapConfirmed()...");
        if (mOnClickListener != null)
            mOnClickListener.onClick(mImageView);
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        Log.e(TAG, "onDoubleTap()...");
        //获取点击的当前坐标
        float x = e.getX();
        float y = e.getY();

        //获取当前缩放值
        float scale = getScale();
        /*
         * 由于计算机储存float类型时会采用四舍五入方法,导致最终的放大缩小倍数不是完全精确到指定的值,
         * 所以如果当前的放大缩小倍数与初始值的绝对值误差小于0.02时即认为本次的放大缩小倍数就是初始的倍数
         */
        if (Math.abs(scale - mInitScale) < 0.02)
            scale = mInitScale;

        //当前缩放值大于初使的缩放值,对其进行缩小操作
        if (scale > mInitScale){
            new Thread(new AutoScaleRunnable(x, y, getScale(), mInitScale)).start();
        }else {
            //当前的缩放值小于等于初始的缩放值,对其进行放大的操作
            new Thread(new AutoScaleRunnable(x, y, getScale(), mMaxScale)).start();
        }
        return true;
    }
});

接着,接管目标View的onTouchEvent()方法,在待监听View的onTouchEvent()方法中添加如下实现

//将事件传递给单击,双击,长按手势检测的onTouchEvent处理
if (mGestureDetector.onTouchEvent(event)){
    return true;
}


拖动、快速滑动、多点触控

首先,定义拖动、快速滑动、多点触控缩放监听器接口

/**
 * 拖动、快速滑动、多点触控缩放监听器
 */

public interface OnGestureListener {

    void onDrag(float dx, float dy);

    void onFling(float startX, float startY, float velocityX, float velocityY);

    void onScale(float scaleFactor, float focusX, float focusY);
}

接着,实现监听器接口

private OnGestureListener mOnGestureListner = new OnGestureListener() {
    @Override
    public void onDrag(float dx, float dy) {
        Log.e(TAG,"onDrag()...");

        boolean flag = true;//true,表示checkBorder(flag)只检查y方向的边界
        if (mIsViewPager){//表示当前父容器为ViewPager
            if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx>1f) ||
                    (mScrollEdge == EDGE_RIGHT && dx<-1f)){
                mImageView.getParent().requestDisallowInterceptTouchEvent(false);
                return;
            }
            else {
                mImageView.getParent().requestDisallowInterceptTouchEvent(true);
            }
            flag = false;//false,当前父容器为ViewPager,表示checkBorder(flag)同时检查x,y方向的边界
        }

        mMatrix.postTranslate(dx, dy);
        if (checkBorder(flag))//边界检查
            mImageView.setImageMatrix(mMatrix);
//            checkBorderAndCenter();
    }

    @Override
    public void onFling(float startX, float startY, float velocityX, float velocityY) {

        Log.e(TAG, "onFling()...");
        if (mAutoFlingRunnable == null)
            mAutoFlingRunnable = new AutoFlingRunnable(mImageView.getContext());

        mAutoFlingRunnable.fling(getImageViewWidth(), getImageViewHeight(), (int) velocityX, (int) velocityY);
        new Thread(mAutoFlingRunnable).start();
    }

    @Override
    public void onScale(float scaleFactor, float focusX, float focusY) {
        Log.e(TAG, "onScale()...");
        if (mImageView.getDrawable() == null)
            return;

        final float scale = getScale();
        //多点触控缩放时,条件一:还能继续放大;条件二:还能继续缩小
        if ((scale< mMaxOVerstep && scaleFactor>1.0f) || (scale>mMinOverstep && scaleFactor<1.0f)){
            mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
            checkBorderAndCenter();
        }
    }
};

实现监听

//拖动,多点触控缩放
mOnGeatureDetector = new OnGestureDetector(mImageView.getContext(), mOnGestureListner);

最后,接管目标View的onTouchEvent()方法,在待监听View的onTouchEvent()方法中添加如下实现

mOnGestureDetector.onTouchEvent(event);

OnGestureDetector是自定义的拖动、快速滑动、多点触控缩放手势检测类,内部处理onTouchEvent()事件逻辑如下:

private boolean processTouchEvent(MotionEvent event){
    //getAction获得的int值是由pointer的index值和事件类型值组合而成的
    //getActionWithMasked则只返回事件的类型值
    //getAction() & ACTION_POINTER_INDEX_MASK就获得了pointer的id,等同于getActionIndex函数;
    //getAction()& ACTION_MASK就获得了pointer的事件类型,等同于getActionMasked函数
    switch (event.getActionMasked()){
        case MotionEvent.ACTION_DOWN:
            //获取第一根手指的id
            mActivePointerId = event.getPointerId(0);

            //手势速度追踪
            if (mVelocityTracker == null)
                mVelocityTracker = VelocityTracker.obtain();
            //单击情况
            if (mVelocityTracker != null)
                mVelocityTracker.addMovement(event);

            mLastTouchX = getActiveX(event);
            mLastTouchY = getActiveY(event);
            mIsDragging = false;
            break;

        case MotionEvent.ACTION_MOVE://move处理是否拖动
            final float x = getActiveX(event);
            final float y = getActiveY(event);
            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;
            //判断手势滑动距离是否足以触发ACTION_MOVE事件
            if (!mIsDragging)
                mIsDragging = isMoveAction(dx, dy);

            if (mIsDragging){
                if (event.getPointerCount() == 1)//手指数为1时才能拖动
                    mOnGestureListener.onDrag(dx, dy);
                mLastTouchX = x;
                mLastTouchY = y;

                if (mVelocityTracker != null)
                    mVelocityTracker.addMovement(event);
            }
            break;

        case MotionEvent.ACTION_UP://手指抬起时处理是否快速滑动
            //当前活动的手指设置为无效状态
            mActivePointerId = INVALID_POINTER_ID;
            if (mIsDragging){
                if (mVelocityTracker != null){
                    mLastTouchX = getActiveX(event);
                    mLastTouchY = getActiveY(event);

                    //设置时间单位为1s
                    mVelocityTracker.computeCurrentVelocity(1000);
                    //x,y轴的速度,单位:像素/s
                    final float vX = mVelocityTracker.getXVelocity();
                    final float vY = mVelocityTracker.getYVelocity();

                    //判断是否触发快速滑动事件
                    if (isFlingAction(vX, vY))
                        mOnGestureListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);
                }
            }
            //回收mVelocityTracker
            if (mVelocityTracker != null){
                mVelocityTracker.clear();
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            break;

        case MotionEvent.ACTION_CANCEL:
            mActivePointerId = INVALID_POINTER_ID;
            //回收mVelocityTracker
            if (mVelocityTracker != null){
                mVelocityTracker.clear();
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            break;

        case MotionEvent.ACTION_POINTER_UP://多点触控中,手指抬起处理手指id的切换问题
            //获取某一根手指抬起时的索引
            int pointerIndex = event.getActionIndex();
            //根据索引获取id
            int pointerId = event.getPointerId(pointerIndex);
            //如果是抬起的是第一根手指,即正在滑动的手指
            if (pointerId == mActivePointerId){
                //那么对应获取第二点
                final int newPointerIndex = pointerId == 0? 1 : 0;
                //将id指向第二根手指
                mActivePointerId = event.getPointerId(newPointerIndex);
                //获取第二根手指的当前坐标
                mLastTouchX = event.getX(newPointerIndex);
                mLastTouchY = event.getY(newPointerIndex);
            }
            break;
    }
    //根据id将索引指向后抬起的手指
    mActivePointerIndex = event.
            findPointerIndex(mActivePointerId != INVALID_POINTER_ID? mActivePointerId : 0);
//        Log.e(TAG, "mActivePointerIndex= "+mActivePointerIndex);

    return true;
}

关于图片加载

当使用像imageView.setBackgroundResource(), imageView.setImageResource(), 或者 BitmapFactory.decodeResource() 这样的方法来设置一张大图片的时候,这些函数在完成decode后,最终都是通过java层的createBitmap()来完成的,需要消耗更多内存,图片滑动时会出现卡顿现象,甚至可能OOM。
所以为防止OOM, 改用先通过BitmapFactory.decodeStream()方法,创建出一个bitmap,再将其设为ImageView的 source。这是因为,decodeStream()直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap(),从而节省了java层的空间。如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛out of Memory异常。
值得注意的是,decodeStream()是直接读取图片资料的字节码了, 不会根据机器的各种分辨率来自动适应,使用了decodeStream()之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。

/**
 * 大图片处理机制
 * 利用Bitmap 转存 R图片
 * @param mContext
 * @param imgId
 * @param mImageView
 * @throws IOException
 */
public static void getBitmapForImgResourse(Context mContext, int imgId, ImageView mImageView) throws IOException {
    InputStream is = mContext.getResources().openRawResource(imgId);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = false;
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inSampleSize = 1;
    final Bitmap btp = BitmapFactory.decodeStream(is, null, options);
    mImageView.setImageBitmap(btp);
    is.close();
}


另外,也可以使用图片加载框架Glide(Github地址:https://github.com/bumptech/glide/releases), 关于Glide的用法网上资源很多,这里不再详述。

ps : 限于篇幅,详细代码参考github仓库:https://github.com/yongjianx/ScaleImageView,不足之处,欢迎交流学习!

猜你喜欢

转载自blog.csdn.net/Emmanuel__/article/details/81264924