Bitmap之巨图加载

前言

在Android开发中,有时候会有加载巨图(长图)的需求,可以上下左右滑动,双击放大或缩小,手指缩放。如何加载一个大图而不产生OOM呢?使用体统提供的BitmapRegionDecoder,区域解码器,可以用来解码一个矩形区域(Rect)的图像,有了这个类我们就可以自定义一块矩形的区域,然后根据手势来移动矩形区域的位置就能慢慢看到整张图片了。

效果演示:
在这里插入图片描述

1.初始化变量

public BigImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //第1步:设置bigImageView需要的成员变量
        //加载显示的区域
        mRect = new Rect();
        mOptions = new BitmapFactory.Options();
        //手勢识别
        mGestureDetector = new GestureDetector(context, this);
        //滚动类
        mScroller = new Scroller(context);
        //缩放手势识别
        mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGesture() );
        //设置触摸监听
        setOnTouchListener(this);
    }

BitmapFactory.Options我们很熟悉,用来配置Bitmap相关的参数,比如获取Bitmap的宽高,内存复用等参数。

GestureDetector用来识别双击事件,ScaleGestureDetector用来监听手指的缩放事件,都是系统提供的类,比较方便使用。

2.设置需要加载的图片

public void setImage(InputStream is) {
        //获取图片的信息,不会将整张图片加载到内存
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, mOptions);
        //获取图片的宽高信息
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
        //开启内存复用
        mOptions.inMutable = true;
        //设置像素格式
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        //真正意义加载图片
        mOptions.inJustDecodeBounds = false;
        try {
            //创建一个区域解码器
            mDecoder = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //刷新页面,与invalidate方法相反,只会触发onMeasure和onLayout方法,不会触发onDraw
        requestLayout();
    }

设置需要要加载的图片,无论图片放到哪里都可以拿到图片的一个输入流,所以参数使用输入流,通过BitmapFactory.Options拿到图片的真实宽高。

接下来是设置inMutable,开启内存复用。

关于像素格式设置,inPreferredConfig这个参数默认是Bitmap.Config.ARGB_8888,这里将它改成Bitmap.Config.RGB_565,去掉透明通道,可以减少一半的内存使用。

最后初始化区域解码器BitmapRegionDecoder。需要注意的是newInstance方法中参数false表示不共享输入流,可以理解为BitmapRegionDecoder拷贝了一份输入流,即获得的图片输入流close了,仍然可以进行解码操作。

requestLayout()刷新页面,需要注意与invalidate()的区别,后者请求重绘View树,即onDraw方法,不会触发onMeasure和onLayout方法,刚好相反。

3.测量控件View的宽高,计算缩放因子/比例

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量控件额宽高
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        //确定加载图片的区域
        /*mRect.left = 0;
        mRect.top = 0;
        mRect.right = mImageWidth;
        //得到图片的宽度,就能根据view的宽度计算缩放因子/比例
        mScale = mViewWidth/(float)mImageWidth;
        mRect.bottom = (int)(mViewHeight/mScale);*/

        //加了缩放手势之后的逻辑
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = Math.min(mImageWidth, mViewWidth);
        mRect.bottom = Math.min(mImageHeight, mViewHeight);

        //再定义一个缩放因子
        originalScale = mViewWidth / (float)mImageWidth;
        mScale = originalScale;

    }

mImageWidth为通过前面的mOptions解析的图片的宽度。

mImageHeight为通过前面的mOptions解析的图片的高度。

mViewWidth为要显示图片的控件的测量宽度

mViewHeight为要显示图片的控件的测量高度

mRect设置left、top、right、bottom等参数来决定要显示的区域。缩放比例就是mViewWidth/mImageWidth, 等比缩放,根据比例竖直方向显示对应的高度。

4.绘制/画出具体内容

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDecoder == null) {
            return;
        }
        
        mBitmap = mDecoder.decodeRegion(mRect, mOptions);
        //复用内存
        mOptions.inBitmap = mBitmap;
        //没有双击放大缩小的时候
        //mMatrix.setScale(mScale,mScale);
        //需要双击放大或缩小的时候
        mMatrix.setScale(mViewWidth/(float)mRect.width(),mViewWidth/(float)mRect.width());
        canvas.drawBitmap(mBitmap, mMatrix, null);
    }

绘制也很简单,通过区域解码器解码一个矩形的区域,返回一个Bitmap对象,然后通过canvas绘制Bitmap。需要注意mOptions.inBitmap = mBitmap;这个配置可以复用内存,保证内存的使用一直只是矩形的这块区域。3.0~4.4版本是占用内存相等才能复用,4.4之后只要加载图片内存小于可复用内存就可以复用。此处就不过多介绍了。

到这里运行就能绘制出一部分图片了,想要看全部的图片,需要手指拖动来看,这就需要处理各种事件了。

5.处理点击事件

@Override
    public boolean onTouch(View v, MotionEvent event) {
        //直接将事件传递给手势事件
        mGestureDetector.onTouchEvent(event);
        //传递给双击事件
        mScaleGestureDetector.onTouchEvent(event);
        return true;
    }

6.处理GestureDetector中的事件

@Override
    public boolean onDown(MotionEvent e) {
        //如果移动没有停止,就强行停止
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }

此处处理的是手按下事件,如果图片滑动没有停止,就强行停止

@Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //上下移动的时候,mRect需要改变现实区域
        mRect.offset((int)distanceX, (int)distanceY);
        //向上滑动边界处理
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - (int)(mViewHeight/mScale);
        }
        //向下滑动边界处理
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int)(mViewHeight/mScale);
        }
        //向左滑动边界处理
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = mImageWidth - (int)(mViewWidth/mScale);
        }

        //向右滑动边界处理
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = (int)(mViewWidth/mScale);
        }

        //请求重绘View树,即onDraw方法,不会触发onMeasure和onLayout方法
        invalidate();
        return false;
    }

onScroll中处理滑动,根据手指移动的参数,来移动矩形绘制区域,这里需要处理各个边界点,比如左边最小就为0,右边最大为图片的宽度,上边最小为0,下边最大为图片的高度,不能超出边界否则就报错了。

	/**
     * 处理惯性问题
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(mRect.left, mRect.top, (int)-velocityX, (int)-velocityY, 0, mImageWidth- (int)(mViewWidth/mScale),
                0, mImageHeight - (int)(mViewHeight/mScale));
        return false;
    }

    //处理结果
    @Override
    public void computeScroll() {
        if (mScroller.isFinished()) {
            return;
        }
        if (mScroller.computeScrollOffset()) {
            mRect.top = mScroller.getCurrY();
            mRect.bottom = mRect.top + (int)(mViewHeight/mScale);
            invalidate();
        }
    }

在onFling方法中调用滑动器Scroller的fling方法来处理手指离开之后惯性滑动。参数就是起始位置,滑动的惯性移动的距离在View的computeScroll()方法中计算,也需要注意边界问题,不要滑出边界。

7.双击事件处理

@Override
    public boolean onDoubleTap(MotionEvent e) {
        //双击事件
        //双击是放大还是缩小,自己决定,此处定义为图片放大倍数小于1.5倍的时候,双击放大
        if (mCurrentScale < mOriginalScale * 1.5) {
            mCurrentScale = mOriginalScale * 2;
        } else {
            mCurrentScale = mOriginalScale;
        }
        mRect.right = mRect.left + (int)(mViewWidth/mCurrentScale);
        mRect.bottom = mRect.top + (int)(mViewHeight/mCurrentScale);
        
        //放大过程中,超出边界处理
        //上边界处理
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - (int)(mViewHeight/mCurrentScale);
        }
        //下边界处理
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mCurrentScale);
        }
        //左滑界处理
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = mImageWidth - (int) (mViewWidth / mCurrentScale);
        }

        //右边界处理
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = (int) (mViewWidth / mCurrentScale);
        }
        //重绘
        invalidate();
        return false;
    }

双击是放大还是缩小,自己决定,此处定义为图片放大倍数小于1.5倍的时候,双击放大,否则缩小即还原。倍数缩放倍数自己根据需求设定。在放大的过程中,也可能出现超出边界情形,和滑动的边界处理逻辑一样,可以将相同代码抽取为一个方法handleBorder();

8.手指缩放处理

@Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scale = mCurrentScale;
            scale += detector.getScaleFactor() - 1;
            if (scale <= mOriginalScale) {
                scale = mOriginalScale;
            } else if (scale > mOriginalScale * 2) {//设置最大的放大倍数,自行设定
                scale = mOriginalScale * 2;
            }
            mRect.right = mRect.left + (int)(mViewWidth/scale);
            mRect.bottom = mRect.top + (int)(mViewHeight/scale);
            mCurrentScale = scale;
            invalidate();
            return true;
        }

9.小结

到这里,巨图(长图)的加载就基本实现了,可以进行上下左右滑动,双击放大或缩小,手指缩放等操作。满足了巨图、长图加载的基本需求了。但是还有两个方向可以优化:

  • 内存优化
    尽管是截取图片的一部分进行显示,以及内存复用,在一定程度上减轻了内存压力,如果巨图占用的内存比较大,还有进行优化的空间,那就是通过设置 mOptions.inJustDecodeBounds = true; 计算采样率inSampleSize, 对加载的bitmap进行尺寸压缩,达到减小内存占用的目的,但是在缩放的时候,mMatrix.setScale(),参数在原来的基础上要乘以inSampleSize才能达到和原来缩放的视觉效果,减轻了内存压力,代价就是图片有一定程度的失真。这个可以根据加载的bitmap的内存占用情况,图片清晰程度进行合理的配置和权衡,是可以达到相对平衡的。
  • 双击缩放问题
    虽然实现了双击缩放,但是不够完美,在显示的Rect区域,无论双击哪个部分,缩放效果是一样的,就是说没有以双击的点为焦点进行缩放,这个部分暂时还没有解决,有待优化,如有知道如何处理的,还望不吝指教。

代码链接:
https://github.com/mitufengyun/LoadBigBitmap

原创文章 23 获赞 30 访问量 9564

猜你喜欢

转载自blog.csdn.net/my_csdnboke/article/details/104672792
今日推荐