巨图加载之BitmapRegionDecoder,防止OOM

先看下效果图

实现原理

思路就是利用BitmapRegionDecoder加载巨图的部分,不全部加载整张巨图,然后对拖动,缩放,双击等进行处理,更改BitmapRegionDecoder所需要的Rect的大小,就搞定了。

代码实现

在构造函数里进行初始化,创建手势识别器等

    private void init() {
        mOptions = new BitmapFactory.Options();
        mScroller = new Scroller(getContext());
        mMatrix = new Matrix();//用于缩放的矩阵
        mRect = new Rect();

        //手势识别
        mGestureDetector = new GestureDetector(getContext(), this);
        mScaleGestureDetector = new ScaleGestureDetector(getContext(), this);
    }

加载大图,我们这里以传进来的大图数据为InputStream为例,首先利用inJustDecodeBounds = true加载大图的宽高信息,获取完大图的宽之后,我们将inJustDecodeBounds设置为false,这样下次加载的时候就会真正加载大图了,因为是大图,我们将BitmapFactory.Options的图片加载格式设置为RGB_565,RGB_565相比ARGB_8888内存节省了一半,降低OOM的风险,如果图片有透明度的话,那还是的使用ARGB_8888。最后我们创建一个BitmapRegionDecoder的实例。

    //设置大图
    public void setImage(InputStream inputStream) {
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, mOptions);
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;//RGB_565比ARGB_8888节省一半内存开销
        mOptions.inJustDecodeBounds = false;
        try {
            mRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        requestLayout();
    }

不光需要大图的宽高,我们还需要拿到显示区域的宽高,即控件的宽高,并将BitmapRegionDecoder所需的Rect设置成控件的大小,默认显示大图的左上角,然后计算出大图与控件宽高的缩放比,同时将当前的缩放比设置成原始缩放比。

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        mRect.set(0, 0, mViewWidth, mViewHeight);
        mScale = mImageWidth * 1.0f / mViewWidth;
        mCurrentScale = mScale;
    }

这样该计算的都计算完成,绘制大图的时候我们要配置mOptions.inBitmap = mBitmap,这个配置可以复用内存,保证内存的使用一直只是矩形的这块区域,然后按照缩放比例将Bitmap绘制出来即可

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mRegionDecoder == null) return;

        //复用内存
        mOptions.inBitmap = mBitmap;
        mBitmap = mRegionDecoder.decodeRegion(mRect, mOptions);
        mMatrix.setScale(mCurrentScale, mCurrentScale);
        canvas.drawBitmap(mBitmap, mMatrix, null);
    }

到这里大图已经能加载出来了,只是一系列的事件还没有处理,重写onTouchEvent,将event传递给GestureDetector和ScaleGestureDetector

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        mScaleGestureDetector.onTouchEvent(event);
        return true;
    }

接着处理GestureDetector中的事件

    @Override
    public boolean onDown(MotionEvent e) {
        //当手指按下的时候,如果图片正在飞速滑动,那么停止
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     * onScroll中处理滑,根据手指移动的参数,来移动矩形绘制区域,这里需要处理各个边界点,
     * 比如左边最小就为0,右边最大为图片的宽度,不能超出边界否则就报错了。
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //滑动的时候,改变mRect显示区域的位置
        mRect.offset((int) distanceX, (int) distanceY);
        //处理上下左右的边界
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = (int) (mViewWidth / mCurrentScale);
        }
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = (int) (mImageWidth - mViewWidth / mCurrentScale);
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mCurrentScale);
        }
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = (int) mImageHeight;
            mRect.top = (int) (mImageHeight - mViewHeight / mCurrentScale);
        }
        invalidate();
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    /*
    在onFling方法中调用滑动器Scroller的fling方法来处理手指离开之后惯性滑动。
    惯性移动的距离在View的computeScroll()方法中计算,也需要注意边界问题,不要滑出边界。
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(mRect.left, mRect.top, -(int) velocityX, -(int) velocityY, 0,
                mImageWidth, 0, (int) mImageHeight);
        return false;
    }


    @Override
    public void computeScroll() {
        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
            if (mRect.top + mViewHeight / mCurrentScale < mImageHeight) {
                mRect.top = mScroller.getCurrY();
                mRect.bottom = (int) (mRect.top + mViewHeight / mCurrentScale);
            }
            if (mRect.bottom > mImageHeight) {
                mRect.top = (int) (mImageHeight - mViewHeight / mCurrentScale);
                mRect.bottom = (int) mImageHeight;
            }
            invalidate();
        }
    }

接着处理缩放事件

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //处理手指缩放事件
        //获取与上次事件相比,得到的比例因子
        float scaleFactor = detector.getScaleFactor();
        mCurrentScale += scaleFactor - 1;
//        mCurrentScale *= scaleFactor;
        if (mCurrentScale > mScale * mMultiple) {
            mCurrentScale = mScale * mMultiple;
        } else if (mCurrentScale <= mScale) {
            mCurrentScale = mScale;
        }
        mRect.right = mRect.left + (int) (mViewWidth / mCurrentScale);
        mRect.bottom = mRect.top + (int) (mViewHeight / mCurrentScale);
        invalidate();
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        //当 >= 2 个手指碰触屏幕时调用,若返回 false 则忽略改事件调用
        return true;
    }

接着处理双击事件

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        //处理双击事件
        if (mCurrentScale > mScale) {
            mDoubleScaleAnimator = ValueAnimator.ofFloat(mCurrentScale, mScale);
        } else {
            mDoubleScaleAnimator = ValueAnimator.ofFloat(mCurrentScale, mScale * mMultiple);
        }
        mDoubleScaleAnimator.setDuration(300).setInterpolator(new LinearInterpolator());
        mDoubleScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentScale = (float) animation.getAnimatedValue();
                mRect.right = mRect.left + (int) (mViewWidth / mCurrentScale);
                mRect.bottom = mRect.top + (int) (mViewHeight / mCurrentScale);
                //处理上下左右的边界
                handleBorder();
                invalidate();
            }
        });
        mDoubleScaleAnimator.start();

        return true;
    }

    private void handleBorder() {
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = (int) (mViewWidth / mCurrentScale);
        }
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = (int) (mImageWidth - mViewWidth / mCurrentScale);
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mCurrentScale);
        }
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = (int) mImageHeight;
            mRect.top = (int) (mImageHeight - mViewHeight / mCurrentScale);
        }
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

 

Demo地址:https://github.com/987570437/BitmapDemo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值