本人有一個笑話類的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:)
如有錯誤,歡迎指正!