Android 自定义控件:加载长图View(局部加载)

当我们在需要加载大图,长图的时候。如果,我们把整张图片都加载进去的话,很可能会OOM。

因为,我们的手机屏幕是有限的。所以,我们可以只加载显示的部分就可以了,这样就需要用到局部加载。


需求分析:

  • Rect。我们既然想要局部加载,肯定是需要一个Rect的绘制矩形的。
  • GestureDetector。既然是局部加载,我们肯定是需要手势判定,然后滑动的。
  • Scroller。 局部加载,肯定是可以让滑动的。
  • BitmapRegionDecoder。 用于显示图片的某一块矩形区域,这也是局部加载的核心类。
  • BitmapFactory.Options。 既然是局部加载。那么,我们肯定需要复用Bitmap的。
  • Bitmap 。 既然需要Bitmamp复用,当然是需要这个啦。

其实除了上面的东西,我们也需要图片的宽高,view的宽高等等属性。

下面,我们就开始编写:

  • 定义属性
  • 初始化
  • 设置图片输入流
  • 设置尺寸onMeasure
  • 绘制onDraw
  • 处理滑动事件
  • 异常处理
  • 控件调用
  • 全类

定义属性

我们先来定义这个类的属性

	//要加载的局部图片的矩形
    private Rect mRect;
    //设置内存复用。
    private BitmapFactory.Options mOptions;
    //手势识别
    private GestureDetector mGestureDetector;
	//处理图片的滑动
    private Scroller mScroller;
	//图片的实际宽高
    private int mImageWidth;
    private int mImageHeight;
	//图片的局部加载类
    private BitmapRegionDecoder mDecoder;
	//View的宽高
    private int mViewWidth;
    private int mViewHeight;
	//要复用的Bitmap
    private Bitmap mBitmap;
	//图片的缩放大小
    private float mScale;

这里的属性,主要就是开始分析时,用到的那些属性。

初始化

下面,我们就要初始化这些属性了

public class LongImageView extends View implements GestureDetector.OnGestureListener,View.OnTouchListener {

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

    public LongImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LongImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
		//需要绘制的图片矩形
        mRect = new Rect();
		//手势识别
        mGestureDetector = new GestureDetector(context, this);
		
        mScroller = new Scroller(context);

		mOptions = new BitmapFactory.Options();

        setOnTouchListener(this);
    }

这里,我们主要是初始化要绘制的图片矩形,及图片滑动时候的手势(GestureDetector)及滑动处理(Scroller)。

设置图片输入流

这个方法,主要是设置要查看的图片的流

 public void setImage(InputStream inputStream) {
        

        //只解码,不返回bitmap。可以获取图片的宽高等属性
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, mOptions);

        //拿到图片的真实宽高
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;

        //设置图片内存复用
        mOptions.inMutable = true;
		//图片格式
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;

        mOptions.inJustDecodeBounds = false;


        try {
            //初始化BitmapRegionDecoder。用于显示某一区域的图片
            mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        requestLayout();
    }

这里,主要是通过流获取图片的真实宽高,设置图片可复用,及初始化BitmapRegionDecoder。

设置尺寸onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		//获取View的大小
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        //确定加载图片的区域
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mImageWidth;

        //设置缩放的比例
        mScale = mViewWidth / (float) mImageWidth;
        mRect.bottom = (int) (mViewHeight / mScale);
    }

这里,我们主要是

  • 获取这个自定义view的宽高。
  • 确定下需要绘制图片的矩形的位置,这里的矩形是根据图片来的。
  • 需要设置的缩放比例。

绘制onDraw

这里,就是要绘制的图片的局部区域。

 private Matrix matrix=new Matrix();
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDecoder == null) {
            return;
        }
		//设置要复用的bitmap
        mOptions.inBitmap = mBitmap;
        //指定显示绘制的局部区域
        mBitmap = mDecoder.decodeRegion(mRect, mOptions);

		//把矩形的大小,通过缩放的大小,设置成控件的大小
        matrix.reset();
        matrix.setScale(mScale, mScale);

		//绘制bitmap
        canvas.drawBitmap(mBitmap, matrix, null);
    }

这里,主要是:

  • 设置要复用的Bitmap。
  • 设置要绘制的局部区域
  • 通过Matrix矩阵,把要绘制的图片的区域,转换成控件的大小。
  • 绘制指定的局部区域的Bitmap。

处理滑动事件

这里,主要是图片在滑动过程中的处理。

	@Override
    public boolean onTouch(View v, MotionEvent event) {
		//设置GestureDetector手势监听
        return mGestureDetector.onTouchEvent(event);
    }
    @Override
    public boolean onDown(MotionEvent e) {
        //只有接受down事件,才能接受后续事件

        //如果,图片在滚动。强制停止滚动事件
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }


    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //更改绘制矩形的位置
        mRect.offset(0, (int) distanceY);
        //处理临界位置

        // 到最下面的话
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = (int) (mImageHeight - (mViewHeight / mScale));
        }
		//到最上面的话
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }
        invalidate();

        return false;
    }

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

这里,主要就是设置图片,在滑动过程中的位置,及绘制区域的计算。

到这里,我们的自定义控件,就处理完了。

异常处理

通过上面的代码,我们的自定义控件就写完了。

但是,当我们第二次,通过setImage()设置图片的时候,就报错啦

 java.lang.IllegalArgumentException: Problem decoding into existing bitmap
        at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:626)
        at com.liu.testproject.img.widget.LongImageView.setImage(LongImageView.java:84)
        at com.liu.testproject.img.LongImageTestActivity$2.onClick(LongImageTestActivity.java:54)
        at android.view.View.performClick(View.java:5637)
        at android.view.View$PerformClick.run(View.java:22429)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6169)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:891)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:781)

我们查看下BitmapFactory#decodeStream()方法,具体报错的原因

public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
            @Nullable Options opts) {
        	
			......

            if (bm == null && opts != null && opts.inBitmap != null) {
                throw new IllegalArgumentException("Problem decoding into existing bitmap");
            }

            ......

        return bm;
    }

通过源码,我们知道,当我们第二次设置图片的时候,我们并没有清空BitmapFactory.Options这个类。所以,才引起的异常。

那么,我们就再写个reset()的方法,在每次设置图片的时候,来释放Option资源。

private void reset() {
		//释放复用的Bitmap
        if (mBitmap != null) {
            mBitmap.recycle();
            mBitmap = null;
        }
		//释放Decoder
        if (mDecoder != null) {
            mDecoder.recycle();
            mDecoder = null;
        }
		//释放Options的资源
        if (mOptions != null) {
            if (mOptions.inBitmap != null) {
                mOptions.inBitmap.recycle();
                mOptions.inBitmap = null;
            }
            mOptions = null;
        }
		if (mScroller != null && !mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
    }

把这个方法,添加在设置图片的方法上。

public void setImage(InputStream inputStream) {
        reset();
		mOptions = new BitmapFactory.Options();

        //只解码,不返回bitmap。可以获取图片的宽高等属性
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, mOptions);

        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;

        //内存复用
        mOptions.inMutable = true;
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;

        mOptions.inJustDecodeBounds = false;


        try {
            //这里放到上面的话,就拿不到图片宽高了
            mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //刷新
        requestLayout();
    }

到这里,我们的自定义加载长图的控件就写完啦,下面通过调用看看下过吧

控件调用

控件调用

		InputStream is;
        try {
            is = getAssets().open("and_long_one.jpg");
            mLongImageView.setImage(is);
        } catch (IOException e) {
            e.printStackTrace();
        }

查看效果

全类


public class LongImageView extends View implements GestureDetector.OnGestureListener,View.OnTouchListener {

    //加载的矩形
    private Rect mRect;
    //设置内存复用
    private BitmapFactory.Options mOptions;
    //手势识别
    private GestureDetector mGestureDetector;

    private Scroller mScroller;

    private int mImageWidth;

    private int mImageHeight;

    private BitmapRegionDecoder mDecoder;


    private int mViewWidth;
    private int mViewHeight;

    private Bitmap mBitmap;

    private float mScale;

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

    public LongImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LongImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mRect = new Rect();

        mGestureDetector = new GestureDetector(context, this);

        mScroller = new Scroller(context);


        setOnTouchListener(this);
    }


    public void setImage(InputStream inputStream) {
        reset();
        mOptions = new BitmapFactory.Options();

        //只解码,不返回bitmap。可以获取图片的宽高等属性
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, mOptions);

        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;

        //内存复用
        mOptions.inMutable = true;
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;

        mOptions.inJustDecodeBounds = false;


        try {
            
            mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //刷新
        requestLayout();
    }

    private void reset() {
        if (mBitmap != null) {
            mBitmap.recycle();
            mBitmap = null;
        }
        if (mDecoder != null) {
            mDecoder.recycle();
            mDecoder = null;
        }
        if (mOptions != null) {
            if (mOptions.inBitmap != null) {
                mOptions.inBitmap.recycle();
                mOptions.inBitmap = null;
            }
            mOptions = null;
        }
        if (mScroller != null && !mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
    }

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

        //设置缩放的比例
        mScale = mViewWidth / (float) mImageWidth;
        mRect.bottom = (int) (mViewHeight / mScale);
    }
    private Matrix matrix=new Matrix();
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDecoder == null) {
            return;
        }
        mOptions.inBitmap = mBitmap;
        //指定解码区域
        mBitmap = mDecoder.decodeRegion(mRect, mOptions);


        matrix.reset();
        matrix.setScale(mScale, mScale);

        canvas.drawBitmap(mBitmap, matrix, null);
    }


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
    @Override
    public boolean onDown(MotionEvent e) {
        //只有接受down事件,才能接受后续事件

        //强制停止滚动事件
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        return true;
    }


    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //更改绘制矩形的位置
        mRect.offset(0, (int) distanceY);
        //处理临界位置
        // 到最下面的话
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = (int) (mImageHeight - (mViewHeight / mScale));
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }
        invalidate();

        return false;
    }

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

    @Override
    public void onShowPress(MotionEvent e) {

    }

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

    @Override
    public void onLongPress(MotionEvent e) {

    }

}


你可能感兴趣的:(Android,自定义控件)