Android大图加载(显示)原理

2020-09-17  本文已影响0人  巫师Android

首先看一下大图加载的应用场景:
现在需要将下面这张图片显示到手机上,它的像素为 7504039。如果不做任何处理,这张图片被加载到内存后,将会占用 7504039*4 = 12117000个字节,共12M。如果用ScrollView + ImageView,直接来显示的话,就是占用12117000个字节。这个内存占用已经相当大了。很容易出现OOM。
因此,我们的解决思路是,自己定义一个控件,专门用来优化并显示大长图。同时学习一下大图加载的原理是什么。

big_image2.jpg

一、自定义大图长图加载控件

本篇就直接上代码了,一边写代码一边复习大图加载的原理。所谓原理,简单的理解,就是运用现有的一些Api对图片加载方式进行配置。
这篇文章的主要目的就是熟练的掌握并灵活的运用这些Api,同时在巩固一下自定义控件的知识。

第0步:分析问题,提出解决方案

前面我们说了,希望通过自定义一个控件来加载大长图,那么现在来分析以下这个控件的细节,希望它可以完成什么功能:
1、显示图片
2、可以上下滑动
3、滑动具有惯性
4、滑动中可以停止
5、性能要好,不能占用太多内存

具体实现

直接上代码了,注释写的很详细了,里面有我对Bitmap高效加载的理解。

/**
 * 这个控件被专门用来加载并显示大长图。
 * 它具有以下功能和特性:
 * 1、显示图片
 * 2、支持上下滑动
 * 3、滑动具有惯性
 * 4、触摸后,滑动可以停止
 * 5、性能较好,内存占用少
 *
 * @author Li Zongwei
 * @date 2020/9/17
 **/
public class MyBigView2 extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {

    /**
     * 用于分块加载,需要跟 BitmapRegionDecoder 配合使用
     */
    private final Rect mRect;

    /**
     * 区域解码器,解码那些区域,由Rect决定
     */
    private BitmapRegionDecoder mBitmapRegionDecoder;

    /**
     * 用于对Bitmap的加载方式进行配置,这是Bitmap实现高效(低内存)加载需要用的一个核心类
     */
    private final BitmapFactory.Options mOptions;

    /**
     * 用于手势支持,要配合滑动使用
     */
    private final GestureDetector mGestureDetector;

    /**
     * 实现滑动
     */
    private final Scroller mScroller;

    /**
     * 图片的原始宽度
     */
    private int mOriginalImageWidth;

    /**
     * 图片的原始高度
     */
    private int mOriginalImageHeight;

    /**
     * 当前控件的宽度
     */
    private int mViewWidth;

    /**
     * 当前控件的高度
     */
    private int mViewHeight;

    /**
     * 缩放因子,就是原始图片相对控件大小的缩放
     */
    private float mScale;

    /**
     * 复用Bitmap
     */
    private Bitmap mBitmap;


    /**
     * @param context
     */
    public MyBigView2(Context context) {
        this(context, null);
    }

    public MyBigView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyBigView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        /**
         * 第一步:初始化需要用的对象
         */
        // 1、内存复用
        mOptions = new BitmapFactory.Options();

        // 2、分块加载
        mRect = new Rect();

        // 3、分块加载,需要配合


        // 4、手势支持,因为要滑动嘛,这里的this是需要传入一个OnGestureListener接口实现,自然是当前的MyBigView2了。
        mGestureDetector = new GestureDetector(context, this);

        // 5、滑动
        mScroller = new Scroller(context);

        // 6、监听滑动事件,此View需要把滑动事件交给 mGestureDetector 去处理  TODO 这里现在还没有思考清除啊。。。标记一下
        setOnTouchListener(this);

        // 好了,到此,准备工作就做完了,下面可以拿图片进来处理了
    }

    /**
     * 第二步:获取图片
     * 这个不用多说了,这个View就是用来显示图片的嘛
     *
     * @param is 传入一个输入流,至于这里为什么用输入流,其实传个Bitmap、url也都是可以的,
     *           但这不是本例的重点哈,重点是拿到图片后的处理
     */
    public void setImage(InputStream is) {
        //哇,这个属性看过Bitmap优化的应该都不陌生,就是解析的时候只是拿到属性,并没有真正的加载到内存
        //这个属性设置完成后,我们就可以解析图片了,主要是为了拿到 图片的:宽、高
        //在真正获取图片前,记得设置为false
        mOptions.inJustDecodeBounds = true;
        // 获取图片,在这之前要对 mOptions 做一下设置
        BitmapFactory.decodeStream(is, null, mOptions);

        mOriginalImageWidth = mOptions.outWidth;
        mOriginalImageHeight = mOptions.outHeight;

        // 设置bitmap的解析格式
        // 这里涉及到一个知识点:
        // RGB_565:表示只有每个像素只有 RGB,没有A(透明通道),565表示 RGB 分别占 5、6、5个bit,也就是共16位,2字节(2byte)
        // ARGB_8888:即 ARGB 分别占 8、8、8、8个bit,也就是共32位,4个字节(4byte)
        // 自然 ARGB_8888 的颜色要比 RGB_565 丰富
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;

        // 设为true,表示内存复用,这个属性还没有做细致的了解
        mOptions.inMutable = true;

        mOptions.inJustDecodeBounds = false;

        // mOptions的设置到此就结束了,下面可以真正的加载图片了

        // 我们前面说了,要进行低内存加载,这需要用到两个对象:BitmapRegionDecoder和Rect,Rect是要作为

        try {
            //初始化区域解码器,解码器有了Bitmap的流,待会儿就可以从解码器中,获取特定区域的图片了,这个特定区域便是由Rect指定的。
            mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //到这里,图片也有了,解码器也有了,mOptions也设置好了,万事俱备,只欠绘制

        // 当然,绘制前还得测量,然后进行绘制
        requestLayout();
    }

    /**
     * 第三步:测量
     * 用于获取控件宽高、缩放比,然后计算出取图片的区域Rect
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //测量,首先,自然是获取此控件的宽高了
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        //缩放比例,这里得注意 把 mOriginalImageWidth 转为float类型,缩放小数位是需要的,不然不准
        mScale = mViewWidth / (float) mOriginalImageWidth;

        // 有了控件的宽高,之前也得到了图片的原始宽高,就可以确定 Rect 的范围了,这个范围将会被解码器用来解析图片
        mRect.top = 0;
        mRect.left = 0;
        // 这里需要思考下,我们说了 mRect 是来规定解码器解析图片范围的,宽度自然要图片宽度 mOriginalImageWidth
        mRect.right = mOriginalImageWidth;
        // mRect.bottom 的话,我们需要根据 控件宽度和图片宽度的大小 缩放比例,进行一下计算
        // 这个计算得说明一下,当时还是蒙了好一会儿
        // 首先,bottom指定了要从解码器中得到的图片底部,我们从0开始取,底部自然应该是控件底部:ViewHeight
        // 那为什么还需要用 mViewHeight / mScale 呢?
        //
        // 举个例子来说明吧:
        // 现在有一张图片宽高为:500*3000   控件宽高为:1000*1000  此时:mScale = 1000 / 500  = 2
        // 我们现在要把这张图片完全显示在控件上,需要
        // 1、把宽度 放大两倍,因此为了保证图片宽高比一致,高度也需要放大 2
        // (图片宽高的缩放是在onDraw里面进行的),所以我们需要先在这里把高度缩小2倍
        // 2、我们现在只要知道,后面高度会放大两倍就好,因此,我们要在  高1000的控件上面显示出这张图片,
        //    实际上只要取图片高度的500就可以了,正好是 控件高度的一半
        // 总而言之:图片待会会进行一次  mScale 的缩放,为保证图片正好可以显示在控件上,高度需要先缩放一次
        //
        // 这么说吧,要在1000*1000的控件上显示500*3000的图片,我们每次只需要每次取 500*500 就好了,onDraw中一放大,图片正好是1000*1000,然后通过上下滑动显示,完美
        mRect.bottom = (int) (mViewHeight / mScale);

        // ok,有了 Rect 的取图区域,我们就可以去onDraw中绘制了
    }

    /**
     * 矩阵
     */
    private Matrix mMatrix;

    /**
     * 第四步:绘制Bitmap
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 这里还是判断下,解码器还在不,如果解码器没了,图片都没了,更别说显示了
        if (mBitmapRegionDecoder == null) {
            return;
        }

        // 高性能,低内存的主旨不能忘了,通过下面这句话来设置内存复用
        mOptions.inBitmap = mBitmap;

        // 然后就是获取要显示的图片了
        mBitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);

        // 在显示前,我们要进行一次缩放
        // 看,在这里设置了 对宽高进行一次缩放,因此啊,前面 mRect.bottom = (int) (mViewHeight / mScale);也就懂了。
        mMatrix = new Matrix();
        mMatrix.setScale(mScale, mScale);

        // 绘制图片
        canvas.drawBitmap(mBitmap, mMatrix, null);

        // 到此,可以运行看一下第一屏了
    }

    /**
     * 第五步:处理touch事件
     *
     * @param v
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //交给 mGestureDetector 进行处理
        return mGestureDetector.onTouchEvent(event);
    }

    /**
     * 第六步:处理点击事件
     *
     * @param e
     * @return
     */
    @Override
    public boolean onDown(MotionEvent e) {
        //如果正在滑动,则停止滑动
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        //接收后续事件
        return true;
    }

    /**
     * 第七步:处理滑动事件
     * 滑动的本质是改变Rect的区域,然后重新绘制,也就达到了滑动的效果
     * 此View是要改变 mRect.top 和 mRect.bottom
     *
     * @param e1        开始事件
     * @param e2        即时事件
     * @param distanceX
     * @param distanceY
     * @return
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //上下移动时,改变Rect显示区域
        mRect.offset(0, (int) distanceY);

        //判断是否到底
        if (mRect.bottom > mOriginalImageHeight) {
            mRect.bottom = mOriginalImageHeight;
            mRect.top = (int) (mOriginalImageHeight - mViewHeight / mScale);
        }

        //判断是否到顶
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }

        // 冲冲冲,开始绘制
        invalidate();

        // 到这里我们的View已经可以完整的显示图片了,但是呢,还没有滑动的惯性,我们接着完善


        return false;
    }

    /**
     * 第八步:处理惯性问题。呃,我怎么发现这个方法就没执行嘛。。。先放着吧,有空再分析下
     *
     * @param e1
     * @param e2
     * @param velocityX
     * @param velocityY
     * @return
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(0, mRect.top, 0, (int) -velocityY, 0, 0,
                0, mOriginalImageHeight - (int) (mViewHeight / mScale));
        return false;
    }

    /**
     * 第九步:处理计算结果
     */
    @Override
    public void computeScroll() {
        super.computeScroll();

        if (mScroller.isFinished()){
            return;
        }

        if (mScroller.computeScrollOffset()){
            mRect.top = mScroller.getCurrY();
            mRect.bottom = (int) (mRect.top + mViewHeight/mScale);
            invalidate();
        }
        
        //到此,这个自定义View也就结束了。bye~
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

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

    @Override
    public void onLongPress(MotionEvent e) {

    }

}

二、总结

Bitmap的高效加载主要还是依赖于对:BitmapFactory.Options的属性设置,然后,就是配合解码器BitmapRegionDecoder可以分块加载图片,分块的区域是由Rect决定的。

然后就是实际运用了滑动:GestureDetector ,Scroller ,当然滑动的一些方法,现在的理解还不是很深入,后面学习自定义View滑动的部分,再深入的看下涉及到的几个Api。

三、拓展

后续,准备写一下横向的大长图展示View。

写于:2020/09/17

上一篇下一篇

猜你喜欢

热点阅读