Android 长图大图加载

长图大图加载

需求:在项目开发中需要长图显示以及大图、巨图显示。

1.长图加载

长图加载显示

由于长图大小比较大,占用的内存比较多,所以需要优化加载。

理论:我们只加载需要显示的大小,其他部分不加载。

1.1画出图形

创建一个自定义控件继承View,它需要的参数:

//需要显示的区域
   private Rect mRect;
   //由于需要复用,所有需要option
   private BitmapFactory.Options mOption;
   //长图需要通过手势滑动来操作
   private GestureDetector mGestureDetector;
   //滑动帮助类
   private Scroller mScroller;
   //图片的宽度
   private int mImageWidth;
   //图片的高度
   private int mImageHeight;
   //控件的宽度
   private int mViewWidth;
   //控件的高度
   private int mViewHeight;
   //图片缩放因子
   private float mScale;
   //区域解码器
   private BitmapRegionDecoder mDecode;
   //需要展示的图片,是被复用的
   private Bitmap mBitmap;

构造函数中初始化需要显示的矩形区域、手势识别类、滑动帮助类等

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

        mRect = new Rect();
        mOption = new BitmapFactory.Options();
        mGestureDetector = new GestureDetector(context, this);
        setOnTouchListener(this);
        mScroller = new Scroller(context);
    }

为使用者提供一个输入图片的方法,参数使用输入流,方便使用。

/**
    * 由使用者输入一张图片
    *
    * @param is 图片输入流
    */
   public void setImage(InputStream is) {
       mOption.inJustDecodeBounds = true;
       BitmapFactory.decodeStream(is, null, mOption);

       mImageWidth = mOption.outWidth;
       mImageHeight = mOption.outHeight;
       //开启复用内存
       mOption.inMutable = true;
       //设置格式,减少内存
       mOption.inPreferredConfig = Bitmap.Config.RGB_565;
       mOption.inJustDecodeBounds = false;

       //创建一个区域解码器
       try {
           mDecode = BitmapRegionDecoder.newInstance(is, false);
       } catch (IOException e) {
           e.printStackTrace();
       }
       //刷新
       requestLayout();
   }

在自定义控件测量方法中,得到需要展示区域的大小,保存在Rect中。

/**
     * 在控件测量中把需要内存区域获取,保存在Rect中
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取测量的view的大小
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        mRect.top = 0;
        mRect.left = 0;
        mRect.right = mImageWidth;
        mScale = mViewWidth / (float) mImageWidth;
        mRect.bottom = (int) (mViewHeight / mScale);
    }

在onDraw中画出需要展示的内容

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //如果没有解码器 说明还没有图片,不需要绘制
    if (null == mDecode) {
        return;
    }
    mOption.inBitmap = mBitmap;
    //通过解码器把图解码出来,只加载矩形区域的内容
    mBitmap = mDecode.decodeRegion(mRect, mOption);
    //把得到的矩形局域大小的图片通过缩放因子,缩放成控件大小
    Matrix matrix = new Matrix();
    matrix.setScale(mScale, mScale);
    canvas.drawBitmap(mBitmap, matrix, null);
}

1.2滑动图形

将控件的onTouch事件交给手势识别来处理。

@Override
    public boolean onTouch(View v, MotionEvent event) {
        //将onTouch事件交给手势处理
        return mGestureDetector.onTouchEvent(event);
    }

如果控件由于惯性正在滑动,需要用户在点击的时候,立刻停止滑动,需要使用滑动帮助类强制停止滑动。

/**
    * 手指按下的回调
    */
   @Override
   public boolean onDown(MotionEvent e) {
       //如果移动还没有停止,强制停止
       if (!mScroller.isFinished()) {
           mScroller.forceFinished(true);
       }
       return true;
   }

在控件滑动过程中,需要根据用户滑动的距离来偏移需要显示的矩形区域,使得屏幕上显示图片根据用户的手势移动来移动。

在这个过程中需要处理矩形上下边界值,不能移出长图以外。

/**
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param distanceX x方向移动的距离
     * @param distanceY y方向移动的距离
     */
@Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //上下移动的时候,需要改变显示的区域 改mRect
        mRect.offset(0, (int) distanceY);
        //处理上下边界问题
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }
        if (mRect.bottom > mImageHeight) {
            mRect.top = mImageHeight - (int) (mViewHeight / mScale);
            mRect.bottom = mImageHeight;
        }
        invalidate();
        return false;
    }

1.3惯性滑动图形

使用滑动帮助类来处理滑动的惯性问题。

注意点:手势滑动方法onFling中的velocityY参数与Scroller.fling方法的参数velocityY方向是反的。

/**
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param velocityX 每秒移动的x像素点
     * @param velocityY 每秒移动的y像素点
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(0, mRect.top,
                0, (int) -velocityY,
                0, 0,
                0, mImageHeight - (int) (mViewHeight / mScale));
        return false;
    }

最后在computeScroll方法中通过滑动帮助类来告诉控件惯性滑动的距离,让控件重新绘制。

ps:computeScroll不是来让ViewGroup滑动的,真正让ViewGroup滑动的是scrollTo,scrollBy。computeScroll的作用是计算ViewGroup如何滑动。而computeScroll是通过draw来调用的。

@Override
public void computeScroll() {
    super.computeScroll();
    if (mScroller.isFinished()) {
        return;
    }
    //true 当前滑动还没有结束
    if (mScroller.computeScrollOffset()) {
        mRect.top = mScroller.getCurrY();
        mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
        invalidate();
    }
}

1.4 长图加载所有代码,以及展示

Android 长图大图加载

调用方式,可以根据需要调整Inpustream,演示是从assets中获取。

BigView mView = findViewById(R.id.big_view);
        InputStream is = null;
        try {
            is = getAssets().open("big.png");
            mView.setImage(is);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

长图加载的类

/**
 * Author:JR
 * Time: 2019/5/17
 * Description: 长图加载
 */
public class BigView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {

    //需要显示的区域
    private Rect mRect;
    //由于需要复用,所有需要option
    private BitmapFactory.Options mOption;
    //长图需要通过手势滑动来操作
    private GestureDetector mGestureDetector;
    //滑动帮助类
    private Scroller mScroller;
    //图片的宽度
    private int mImageWidth;
    //图片的高度
    private int mImageHeight;
    //控件的宽度
    private int mViewWidth;
    //控件的高度
    private int mViewHeight;
    //图片缩放因子
    private float mScale;
    //区域解码器
    private BitmapRegionDecoder mDecode;
    //需要展示的图片,是被复用的
    private Bitmap mBitmap;

    public BigView(Context context) {
        this(context, null);
    }

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

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

        mRect = new Rect();
        mOption = new BitmapFactory.Options();
        mGestureDetector = new GestureDetector(context, this);
        setOnTouchListener(this);
        mScroller = new Scroller(context);
    }

    /**
     * 由使用者输入一张图片
     *
     * @param is 图片输入流
     */
    public void setImage(InputStream is) {
        mOption.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, mOption);

        mImageWidth = mOption.outWidth;
        mImageHeight = mOption.outHeight;
        //开启复用内存
        mOption.inMutable = true;
        //设置格式,减少内存
        mOption.inPreferredConfig = Bitmap.Config.RGB_565;
        mOption.inJustDecodeBounds = false;

        //创建一个区域解码器
        try {
            mDecode = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //刷新
        requestLayout();
    }

    /**
     * 在控件测量中把需要内存区域获取,保存在Rect中
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取测量的view的大小
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        mRect.top = 0;
        mRect.left = 0;
        mRect.right = mImageWidth;
        mScale = mViewWidth / (float) mImageWidth;
        mRect.bottom = (int) (mViewHeight / mScale);
    }

    /**
     * 画出内容
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //如果没有解码器 说明还没有图片,不需要绘制
        if (null == mDecode) {
            return;
        }
        mOption.inBitmap = mBitmap;
        //通过解码器把图解码出来,只加载矩形区域的内容
        mBitmap = mDecode.decodeRegion(mRect, mOption);
        //把得到的矩形局域大小的图片通过缩放因子,缩放成控件大小
        Matrix matrix = new Matrix();
        matrix.setScale(mScale, mScale);
        canvas.drawBitmap(mBitmap, matrix, null);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //将onTouch事件交给手势处理
        return mGestureDetector.onTouchEvent(event);
    }

    /**
     * 手指按下的回调
     */
    @Override
    public boolean onDown(MotionEvent e) {
        //如果移动还没有停止,强制停止
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }

    /**
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param distanceX x方向移动的距离
     * @param distanceY y方向移动的距离
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //上下移动的时候,需要改变显示的区域 改mRect
        mRect.offset(0, (int) distanceY);
        //处理上下边界问题
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }
        if (mRect.bottom > mImageHeight) {
            mRect.top = mImageHeight - (int) (mViewHeight / mScale);
            mRect.bottom = mImageHeight;
        }
        invalidate();
        return false;
    }

    /**
     * 处理滑动的惯性的问题
     *
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param velocityX 每秒移动的x像素点
     * @param velocityY 每秒移动的y像素点
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(0, mRect.top,
                0, (int) -velocityY,
                0, 0,
                0, mImageHeight - (int) (mViewHeight / mScale));
        return false;
    }

    /**
     * 利用onFling的计算结果 在惯性滑动过程中重新计算滑动过程中Rect的top以及bottom的值
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.isFinished()) {
            return;
        }
        //true 当前滑动还没有结束
        if (mScroller.computeScrollOffset()) {
            mRect.top = mScroller.getCurrY();
            mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
            invalidate();
        }
    }
    @Override
    public void onShowPress(MotionEvent e) {
    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
    }
}

2.大图加载

在长图加载的基础上,把缩放因子去掉,长图加载只是操作了Y轴上的变化,大图加载需要操作X和Y轴上的变化。

代码如下:


/**
 * Author:JR
 * Time: 2019/5/17
 * Description: 大图加载
 */
public class BigView2 extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {

    //需要显示的区域
    private Rect mRect;
    //由于需要复用,所有需要option
    private BitmapFactory.Options mOption;
    //长图需要通过手势滑动来操作
    private GestureDetector mGestureDetector;
    //滑动帮助类
    private Scroller mScroller;
    //图片的宽度
    private int mImageWidth;
    //图片的高度
    private int mImageHeight;
    //控件的宽度
    private int mViewWidth;
    //控件的高度
    private int mViewHeight;
    //区域解码器
    private BitmapRegionDecoder mDecode;
    //需要展示的图片,是被复用的
    private Bitmap mBitmap;

    public BigView2(Context context) {
        this(context, null);
    }

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

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

        mRect = new Rect();
        mOption = new BitmapFactory.Options();
        mGestureDetector = new GestureDetector(context, this);
        setOnTouchListener(this);
        mScroller = new Scroller(context);
    }

    /**
     * 由使用者输入一张图片
     *
     * @param is 图片输入流
     */
    public void setImage(InputStream is) {
        mOption.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, mOption);

        mImageWidth = mOption.outWidth;
        mImageHeight = mOption.outHeight;
        //开启复用内存
        mOption.inMutable = true;
        //设置格式,减少内存
        mOption.inPreferredConfig = Bitmap.Config.RGB_565;
        mOption.inJustDecodeBounds = false;

        //创建一个区域解码器
        try {
            mDecode = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //刷新
        requestLayout();
    }

    /**
     * 在控件测量中把需要内存区域获取,保存在Rect中
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取测量的view的大小
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        mRect.top = 0;
        mRect.left = 0;
        mRect.right = mViewWidth;
        mRect.bottom = mViewHeight;
    }

    /**
     * 画出内容
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //如果没有解码器 说明还没有图片,不需要绘制
        if (null == mDecode) {
            return;
        }
        mOption.inBitmap = mBitmap;
        //通过解码器把图解码出来,只加载矩形区域的内容
        mBitmap = mDecode.decodeRegion(mRect, mOption);
        //把得到的矩形局域大小的图片通过缩放因子,缩放成控件大小
        Matrix matrix = new Matrix();
        canvas.drawBitmap(mBitmap, matrix, null);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //将onTouch事件交给手势处理
        return mGestureDetector.onTouchEvent(event);
    }

    /**
     * 手指按下的回调
     */
    @Override
    public boolean onDown(MotionEvent e) {
        //如果移动还没有停止,强制停止
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }

    /**
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param distanceX x方向移动的距离
     * @param distanceY y方向移动的距离
     */
    @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 = mViewWidth;
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = mViewHeight;
        }
        if (mRect.right > mImageWidth) {
            mRect.left = mImageWidth - mViewWidth;
            mRect.right = mImageWidth;
        }
        if (mRect.bottom > mImageHeight) {
            mRect.top = mImageHeight - mViewHeight;
            mRect.bottom = mImageHeight;
        }
        invalidate();
        return false;
    }

    /**
     * 处理滑动的惯性的问题
     *
     * @param e1        手指按下去的事件 开始的坐标
     * @param e2        当前手势事件
     * @param velocityX 每秒移动的x像素点
     * @param velocityY 每秒移动的y像素点
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(mRect.left, mRect.top,
                (int)-velocityX, (int) -velocityY,
                0, mImageWidth - mViewWidth,
                0, mImageHeight - mViewHeight);
        return false;
    }

    /**
     * 利用onFling的计算结果 在惯性滑动过程中重新计算滑动过程中Rect的top以及bottom的值
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.isFinished()) {
            return;
        }
        //true 当前滑动还没有结束
        if (mScroller.computeScrollOffset()) {
            mRect.left = mScroller.getCurrX();
            mRect.top = mScroller.getCurrY();
            mRect.right = mRect.left + mViewHeight;
            mRect.bottom = mRect.top + mViewHeight;
            invalidate();
        }
    }
    @Override
    public void onShowPress(MotionEvent e) {
    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
    }
}

3.大图缩放

不想写了 下次再写吧。