本人有一个笑话类的app,里面可以看笑话段子、搞笑图片和搞笑视频。内容大多来自于互联网,最近发现,抓到不少超长的图片,在打开大图预览时,上面的部分显示正常,滑动到下面的时候,全是空白;效果如下:
百度了一下,答案是硬件加速导致的openglRender大小限制引起的,具体原理这里不赘述,百度有很多,有兴趣了解的可以搜下。这里只关注解决方法,一是关闭硬件加速就可以,但会导致界面有卡顿现象,作为一个有追求的码农,果断放弃这个;第二个方案就是在拿到图片后,根据当前显示位置和当前缩放比例从图片中剪切合适的部分显示到屏幕中。
今天主要讲下第二种方案的实现逻辑,文末会有源码提供,已封装为一个扩展LargeImageView组件,可直接替换系统ImageView。
还是一贯的原则,主要讲解思想和逻辑,尽量用简单易懂的语言讲清楚,希望读者能够看完之后自己实现一套出来,如果不太关心这些,也可以直接跳到文末下载代码。
一、背景
超长图片显示不全,底部显示空白,无法正确浏览图片。
二、目的
继承实现一个新的LargeImageView组件,支持显示超长图片,支持触摸上下左右滑动,支持双指缩放,支持双击放大/还原。
三、实现
现在我们来一步一步实现LargeImageView组件。
1. 首先新建一个类,继承自ImageView,并且设置缩放类型为ScaleType.FIT_XY
1 public class LargeImageView extends ImageView { 2 public LargeImageView(Context context) 3 { 4 this(context, null); 5 } 6 7 public LargeImageView(Context context, AttributeSet attrs) 8 { 9 super(context, attrs); 10 this.setScaleType(ScaleType.FIT_XY); 11 } 12 }
2. 重载所有设置图片源的方法,将得到图片信息以Bitmap保存,后面备用
1 public class LargeImageView extends ImageView { 2 private Bitmap mBitmap; 3 4 public LargeImageView(Context context) 5 { 6 this(context, null); 7 } 8 9 public LargeImageView(Context context, AttributeSet attrs) 10 { 11 super(context, attrs); 12 this.setScaleType(ScaleType.FIT_XY); 13 } 14 15 @Override 16 public void setImageBitmap(Bitmap bmp) { 17 mBitmap = bmp; 18 super.setImageBitmap(bmp); 19 } 20 21 @Override 22 public void setImageDrawable(Drawable drawable) { 23 mBitmap = getBitmapFromDrawable(drawable); 24 super.setImageDrawable(drawable); 25 } 26 27 @Override 28 public void setImageResource(int resId) { 29 super.setImageResource(resId); 30 mBitmap = getBitmapFromDrawable(getDrawable()); 31 } 32 33 @Override 34 public void setImageURI(Uri uri) { 35 super.setImageURI(uri); 36 mBitmap = getBitmapFromDrawable(getDrawable()); 37 } 38 }
3. 重载onDraw方法,根据缩放比例和滚动位置进行切图绘制
1 @Override 2 protected void onDraw(Canvas canvas) { 3 if (!mIsDrawing) { 4 mIsDrawing = true; 5 6 //组件的宽度和高度,即图片显示区域的大小 7 int width = getWidth(); 8 int height = getHeight(); 9 10 if (mBitmap == null || width <= 0 || height <= 0) { 11 return; 12 } 13 14 //创建一个和显示区域大小相同的位图 15 Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 16 Canvas bmpCanvas = new Canvas(bmp); 17 18 try { 19 //计算切图的宽度、高度 20 int srcWidth = getRealSrcWidth(); 21 int srcHeight = getRealSrcHeight(); 22 23 //计算切图的X、Y坐标 24 int srcOffsetX = mOffsetX; 25 int srcOffsetY = mOffsetY; 26 if (srcOffsetY + srcHeight > mBitmap.getHeight()) { 27 srcOffsetY = mBitmap.getHeight() - srcHeight; 28 } 29 30 //计算目标显示区域绘制的宽度、高度 31 int desWidth = getRealDesWidth(); 32 int desHeight = getRealDesHeight(); 33 34 //计算目标显示区域绘制的X、Y坐标 35 int desOffsetX = Math.max(0, (width - desWidth) / 2); 36 int desOffsetY = 0; 37 if (desHeight < height) { 38 desOffsetY = Math.max(0, (height - desHeight) / 2); 39 } 40 41 //绘制切图图片背景,切图大小小于显示区域时需要 42 if (mDrawScale < 1 || desHeight < height) { 43 Paint fillPaint = new Paint(); 44 fillPaint.setColor(Color.BLACK); 45 bmpCanvas.drawRect(0, 0, width, height, fillPaint); 46 } 47 48 //绘制切图图片 49 bmpCanvas.drawBitmap(mBitmap, 50 new Rect(srcOffsetX, srcOffsetY, srcOffsetX + srcWidth, srcOffsetY + srcHeight), 51 new Rect(desOffsetX, desOffsetY, desOffsetX + desWidth, desOffsetY + desHeight), 52 null); 53 54 //将切图图片绘制到画布 55 canvas.drawBitmap(bmp, 56 new Rect(0, 0, bmp.getWidth(), bmp.getHeight()), 57 new Rect(0, 0, width, height), 58 null); 59 60 //销毁切图图片,重要 61 bmp.recycle(); 62 } 63 catch (Exception exp) { 64 } 65 finally { 66 mIsDrawing = false; 67 } 68 } 69 }
4. 增加手势检测GestureDetector,处理滚动(onScroll)和双击处理(onDoubleTap)
1 GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { 2 @Override 3 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 4 if (!mIsScaling) { 5 int width = getWidth(); 6 int height = getHeight(); 7 8 if (mBitmap != null && width > 0 && height > 0) { 9 boolean needInvalidate = false; 10 11 //计算原图的宽、高,需要考虑缩放比例 12 int srcWidth = getRealSrcWidth(); 13 int srcHeight = getRealSrcHeight(); 14 15 //计算滚动的X坐标 16 if (srcWidth < mBitmap.getWidth()) { 17 int oldOffsetX = mOffsetX; 18 oldOffsetX += distanceX; 19 if (oldOffsetX < 0) { 20 oldOffsetX = 0; 21 } 22 if (oldOffsetX + srcWidth > mBitmap.getWidth()) { 23 oldOffsetX = mBitmap.getWidth() - srcWidth; 24 } 25 if (mOffsetX != oldOffsetX) { 26 mOffsetX = oldOffsetX; 27 needInvalidate = true; 28 } 29 } 30 31 //计算滚动的Y坐标 32 if (srcHeight < mBitmap.getHeight()) { 33 int oldOffsetY = mOffsetY; 34 oldOffsetY += distanceY; 35 if (oldOffsetY < 0) { 36 oldOffsetY = 0; 37 } 38 if (oldOffsetY + srcHeight > mBitmap.getHeight()) { 39 oldOffsetY = mBitmap.getHeight() - srcHeight; 40 } 41 if (mOffsetY != oldOffsetY) { 42 mOffsetY = oldOffsetY; 43 needInvalidate = true; 44 } 45 } 46 47 //重新绘制 48 if (needInvalidate) { 49 invalidate(); 50 } 51 } 52 } 53 54 return super.onScroll(e1, e2, distanceX, distanceY); 55 } 56 57 @Override 58 public boolean onDoubleTap(MotionEvent e) { 59 //处理双击事件,放大或还原 60 if (mDrawScale != 1) { 61 scale(1); 62 mScale = 1; 63 } else { 64 scale(2); 65 mScale = 2; 66 } 67 68 return super.onDoubleTap(e); 69 } 70 };
5. 增加缩放手势检测ScaleGestureDetector,处理缩放事件
1 ScaleGestureDetector.SimpleOnScaleGestureListener simpleOnScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 2 @Override 3 public boolean onScale(ScaleGestureDetector detector) { 4 if (mIsScaling) { 5 //计算缩放比例 6 float currSpan = detector.getCurrentSpan(); 7 float prevSpan = detector.getPreviousSpan(); 8 mScaleFactor = currSpan / prevSpan; 9 10 float currScale = mScale * mScaleFactor; 11 12 //按新的缩放比例进行缩放 13 scale(currScale); 14 } 15 16 return super.onScale(detector); 17 } 18 19 @Override 20 public void onScaleEnd(ScaleGestureDetector detector) { 21 super.onScaleEnd(detector); 22 23 //重新计算缩放比例,别做结束缩放前的最后一次缩放 24 mScale = mScale * mScaleFactor; 25 if (mScale < 1) { 26 mScale = 1; 27 } 28 scale(mScale); 29 30 //恢复缩放标机 31 mIsScaling = false; 32 } 33 34 @Override 35 public boolean onScaleBegin(ScaleGestureDetector detector) { 36 //开始缩放,打标记 37 mIsScaling = true; 38 return super.onScaleBegin(detector); 39 } 40 };
至此,我们的目的功能都已实现,有些细节实现,有兴趣的朋友可以下载代码了解。
四、用法
LargeImageView组件继承自ImageView,可以完全替代ImageView,使用方法相同。
1 <com.puerlink.imagepreview.LargeImageView 2 android:id="@+id/image_scale_view" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 </com.puerlink.imagepreview.LargeImageView>
五、最终效果
六、未实现功能
1. 滚动onFling事件未处理
2. 细节优化和绘制性能
七、源代码
Git源码地址
开源不易,谢谢Star:)
如有错误,欢迎指正!