先看下效果图
思路就是利用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