55

Andorid加载大图,双击放大,手势缩放

 4 years ago
source link: https://chsmy.github.io/2019/12/20/technology/Andorid加载大图,双击放大,手势缩放/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Andorid加载大图,双击放大,手势缩放

Android开发中,有时候会有加载巨图的需求,如何加载一个大图而不产生OOM呢,使用系统提供的BitmapRegionDecoder这个类可以很轻松的完成。

效果图:

IZjM7jv.gif

BitmapRegionDecoder:区域解码器,可以用来解码一个矩形区域的图像,有了这个我们就可以自定义一块矩形的区域,然后根据手势来移动矩形区域的位置就能慢慢看到整张图片了。

OK 核心原理就是这么简单,不过做起来还是有一些细节处理,下面就一步一步的完成一个加载大图,支持拖动查看,双击放大,手势缩放的的自定义View。

第一步初始化变量

private void init(){
     mOptions = new BitmapFactory.Options();
     //滑动器
     mScroller = new Scroller(getContext());
     //所放器
     mMatrix = new Matrix();
     //手势识别
     mGestureDetector = new GestureDetector(getContext(),this);
     mScaleGestureDetector = new ScaleGestureDetector(getContext(),this);
 }

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

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

第二步设置需要加载的图片

public void setImage(InputStream is){
    mOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(is,null,mOptions);
    mImageWidth = mOptions.outWidth;
    mImageHeight = mOptions.outHeight;
    mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
    mOptions.inJustDecodeBounds = false;
    try {
        //区域解码器
        mRegionDecoder = BitmapRegionDecoder.newInstance(is,false);
    } catch (IOException e) {
        e.printStackTrace();
    }
    requestLayout();
}

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

inPreferredConfig这个参数默认是Bitmap.Config.ARGB_8888,这里将它改成Bitmap.Config.RGB_565,去掉透明通道,可以减少一半的内存使用。最后初始化区域解码器BitmapRegionDecoder。

ARGB_8888就是由4个8位组成即32位,

RGB_565就是R为5位,G为6位,B为5位共16位

第三步获取View的宽高,计算缩放值

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mViewWidth = w;
    mViewHeight = h;
    mRect.top = 0;
    mRect.left = 0;
    mRect.right = (int) mViewWidth;
    mRect.bottom = (int) mViewHeight;
    mScale = mViewWidth/mImageWidth;
    mCurrentScale = mScale;
}

onSizeChanged方法在布局期间,当此视图的大小发生更改时,将调用此方法,第一次在onMeasure之后调用,可以方便的拿到View的宽高。

然后给我们自定义的矩形mRect的上下左右的边界赋值。一般情况下我们使用这个自定义的View显示大图,都是占满这个View,所以这里矩形初始大小就让它跟View一样大。

mScale用来记录原始的所方比,mCurrentScale用来记录当前的所方比,因为有双击放大和手势缩放,mCurrentScale随着手势变化。

第四步绘制

@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);
}

绘制也很简单,通过区域解码器解码一个矩形的区域,返回一个Bitmap对象,然后通过canvas绘制Bitmap。需要注意 mOptions.inBitmap = mBitmap; 这个配置可以服用内存,保证内存的使用一直只是矩形的这块区域。

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

第五步分发事件

@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);

    mScaleGestureDetector.onTouchEvent(event);
    return true;
}

onTouchEvent中很简单,事件都交给两个手势检测器自己去处理。

第六步处理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.left<0){
        mRect.left = 0;
        mRect.right = (int) (mViewWidth/mCurrentScale);
    }
    if(mRect.right>mImageWidth){
        mRect.right = (int) 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;
}

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

 @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    mScroller.fling(mRect.left,mRect.top,-(int)velocityX,-(int)velocityY,0,(int)mImageWidth
            ,0,(int)mImageHeight);
    return false;
}

@Override
public void computeScroll() {
    super.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();
    }
}

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

第七步处理双击事件

@Override
public boolean onDoubleTap(MotionEvent e) {
    //处理双击事件
    if(mCurrentScale>mScale){
        mCurrentScale = mScale;
    }else {
        mCurrentScale = mScale*mMultiple;
    }
    mRect.right = mRect.left+(int)(mViewWidth/mCurrentScale);
    mRect.bottom = mRect.top+(int)(mViewHeight/mCurrentScale);
    //处理边界
     if(mRect.left<0){
        mRect.left = 0;
        mRect.right = (int) (mViewWidth/mCurrentScale);
    }
    if(mRect.right>mImageWidth){
        mRect.right = (int) 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 true;
}

mMultiple为双击之后放大几倍,这里设置3倍。第一次双击放大3倍,第二次双击返回原状。缩放完成之后,需要根据当前的缩放比重新设置绘制区域的边界。最后也需要重新定位一下边界,因为如果使用两个手指放大之后,这时候双击返回原状,如果不处理边界,位置会出错。处理边界的代码可以抽取出来。

第八步处理手指缩放事件

   @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;
    }

onScaleBegin方法需要返回true,否则无法检测到手势缩放。onScale方法中获取缩放因子,这个缩放因子是跟上次事件相比的出来的。所以这里使用 *= ,完成之后也需要重新设置绘制区域mRect的边界。

到这里各种功能就完成啦, 点击获取源码


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK