1.演示,代码
下载示例apk 下载项目 : https://gitee.com/xi/LImage.git
2.遇到的问题
- 想省内存,不太可能
- 只支持拖拽手势,不支持缩放相对简单,解码view对应的区域就可以。
- 不支持缩放好像说不过去,同时支持缩放+拖拽后变复杂,如转屏后的位置,指定锚点缩放,缩放后又移动,移动后又缩放。
- 用系统图库打开图片?直接打开超大图片不能正常显示.用图库打开图片所在目录?没有找到相关代码.
3.缩放,移动的思路
3.1 缩放思路
- 用一张解码区域是整张图片,但是inSampleSize大小适中的缩略图缩放.虽然图片是不清晰的,但是省内存,省解码时间.
- 缩小过程不需要高清的.
- 放大到100%的时候就重新解码当前view对应区域的位图,inSampleSize = 1 .
3.2 移动思路
- 当缩放比例小于100%时,就移动缩略图,
- 当缩放比例等于100%时,按当前view区域大小平移,重新解码对应的位图.
4.核心代码
4.1 解码
- BitmapRegionDecoder 可以指定一个区域(大图范围内,超出会抛异常)对一张大位图解码,解出一张小位图.然后在view上显示这个小位图.
- BitmapFactory.Options 解码选项,下面是常用的几个重要选项:
inSampleSize | 缩略大小,值为2的n次幂(n为自然数).其它值无意义 |
inPreferredConfig |
色彩模式,决定一个像素占用的字节数 |
inBitmap |
指定复用的位图..不用在申请内存. |
- 代码
private BitmapRegionDecoder mDecoder; private BitmapFactory.Options mOptions; private Rect mDecodeRegion; void initRegionDecoder(){ try { InputStream fs = getResources().getAssets().open("world_map.jpg"); mDecoder = BitmapRegionDecoder.newInstance(fs,false); mBitmapWidth = mDecoder.getWidth(); mBitmapHeight = mDecoder.getHeight(); mOptions = new BitmapFactory.Options(); mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; mOptions.inSampleSize = ; } catch (IOException e) { e.printStackTrace(); } } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //... mDecodeRegion = new Rect(left,top,left + viewWidth ,top + viewHeight ); // mDecodeRegion = new Rect(0,0,mBitmapWidth ,mBitmapHeight); mOptions.inSampleSize = calculateInSampleSize(mDecodeRegion.width(),mDecodeRegion.height(),viewWidth,viewHeight); // mOptions.inSampleSize = 2; long begin,end; begin = SystemClock.elapsedRealtime(); mBitmap = mDecoder.decodeRegion(mDecodeRegion,mOptions); end = SystemClock.elapsedRealtime(); //.. }
4.2 手势
- ScaleGestureDetector 缩放手势识别器
- GestureDetectorCompat 移动手势识别器
这两个比较简单,先构造,然后在 public boolean onTouchEvent(MotionEvent event) 里接收触摸事件,最后在相应的手势回调方法处理手势就可以.
4.3 变形
缩放: 用变形矩阵.小于100%时移动也用变形矩阵,等于100%时自己计算.
@Override protected void onDraw(Canvas canvas) { if (mPercentage < 100.0f && mThumbnailBitmap != null){ canvas.drawBitmap(mThumbnailBitmap,mMatrix,mPaint); }else if(mPercentage == 100.0f && mBitmap != null){ canvas.drawBitmap(mBitmap,mBitmapLeft,mBitmapTop,mPaint); } //... }
4.4 LargeImageView.java
package com.example.ff.limage; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.os.AsyncTask; import android.os.Environment; import android.os.Parcelable; import android.os.SystemClock; import android.support.annotation.Nullable; import android.support.v4.view.GestureDetectorCompat; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import static android.os.Environment.isExternalStorageRemovable; public class LargeImageView extends View { private String TAG = "LargeImageView"; private Paint mPaint; private BitmapRegionDecoder mDecoder; private BitmapFactory.Options mOptions; private Rect mDecodeRegion; private static Bitmap mThumbnailBitmap; private Bitmap mBitmap; private Matrix mMatrix; private float thumbnailWidth,thumbnailHeight; private float mPercentage; private float mThumbnailLeft,mThumbnailTop,mBitmapLeft,mBitmapTop; private int mBitmapWidth,mBitmapHeight; private Point mFocus; private boolean mScaling = false; private DiskLruCache mDiskLruCache; public void destroyBitmaps(){ Log.e(TAG, "destroyBitmaps: release bitmaps" ); if (!mDecoder.isRecycled()) { mDecoder.recycle(); } if (!mBitmap.isRecycled()){ mBitmap.recycle(); mBitmap = null; } } public void destroyThumbnailBitmap(){ if (!mThumbnailBitmap.isRecycled()){ mThumbnailBitmap.recycle(); mThumbnailBitmap = null; } } public void createThumbnailBitmap(){ if (null == mThumbnailBitmap){ DecodeTask task = new DecodeTask(); task.execute(mDecodeRegion); } } public void createBitmaps(){ } private class DecodeTask extends AsyncTask<Rect,Void,Void>{ @Override protected Void doInBackground(Rect... rects) { Log.e(TAG, "doInBackground: " ); long begin,end; mDecodeRegion = ,,mBitmapWidth,mBitmapHeight); mOptions.inSampleSize = ; begin = SystemClock.elapsedRealtime(); mThumbnailBitmap = mDecoder.decodeRegion(mDecodeRegion,mOptions); end = SystemClock.elapsedRealtime(); Log.e(TAG, "doInBackground: decode mThumbnailBitmap need " + (end - begin) + " ms. " ); return null; } } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } // 打印Matrix内数据 void showMatrix(Matrix matrix,String tag){ // 下面的代码是为了查看matrix中的元素 ]; matrix.getValues(matrixValues); String temp ; ; i < ; ++i) { temp = ""; ; j < ; ++j) { temp += matrixValues[ * i + j] + "\t"; } Log.e(tag, temp); } } private void printThumbnailRect(){ float left = mThumbnailLeft; float top = mThumbnailTop; float right = mThumbnailLeft + thumbnailWidth; float bottom = mThumbnailTop + thumbnailHeight; Log.e(TAG, "printThumbnailRect: left = " + left + " top = " + top + " right = " + right + " bottom = " + bottom + " width = " + thumbnailWidth + " height = " + thumbnailHeight); } private ScaleGestureDetector scaleDetector; private ScaleGestureDetector.SimpleOnScaleGestureListener scaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { float factor = detector.getScaleFactor(); ) return true; && factor > ){ return true; } if (thumbnailWidth * factor < mBitmapWidth){ if (thumbnailWidth * factor >= mBitmapWidth * 0.01f){ thumbnailWidth *= factor; thumbnailHeight *= factor; mMatrix.postScale(factor,factor,detector.getFocusX(),detector.getFocusY()); } }else{ factor = mBitmapWidth / thumbnailWidth; mMatrix.postScale(factor,factor,detector.getFocusX(),detector.getFocusY()); thumbnailWidth = mBitmapWidth; thumbnailHeight = mBitmapHeight; mPercentage = 100.0f; ]; mMatrix.getValues(matrix); mThumbnailLeft = matrix[]; mThumbnailTop = matrix[]; float left = mThumbnailLeft; float top = mThumbnailTop; float right = mThumbnailLeft + thumbnailWidth; float bottom = mThumbnailTop + thumbnailHeight; int viewWith = getWidth(); int viewHeight = getHeight(); mBitmapLeft = ; mBitmapTop = ; ){ mBitmapLeft = left; mDecodeRegion.left = ; mDecodeRegion.right = (int) (viewWith - left); ){ mBitmapTop = top; mDecodeRegion.top = ; mDecodeRegion.bottom = (int) (viewHeight - top); }else if(bottom < viewHeight){ mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else { mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } }){ mBitmapTop = top; mDecodeRegion.top = ; mDecodeRegion.bottom = (int) (viewHeight - top); if (right < viewWith){ mDecodeRegion.right = mBitmapWidth; mDecodeRegion.left = (int) (mBitmapWidth - right); }else{ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; } }else if(right < viewWith ){ mDecodeRegion.right = mBitmapWidth; mDecodeRegion.left = (int) (mBitmapWidth - right); if(bottom < viewHeight){ mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else{ mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } }else if(bottom < viewHeight){ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else{ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } mOptions.inSampleSize = ; && mDecodeRegion.height() > ){ mBitmap = mDecoder.decodeRegion(mDecodeRegion,mOptions); }else{ if (mBitmap != null && !mBitmap.isRecycled()){ mBitmap.recycle(); mBitmap = null; } } } BigDecimal bd = ).setScale(,BigDecimal.ROUND_HALF_UP); mPercentage = bd.floatValue(); Log.i(TAG, "onScale: bitmap.w = " + mThumbnailBitmap.getWidth() + " bitmap.h = " + mThumbnailBitmap.getHeight() + " thumbnailWidth = " + thumbnailWidth + " thumbnailHeight = " + thumbnailHeight); invalidate(); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { mScaling = true; float focusX = detector.getFocusX(); float focusY = detector.getFocusY(); mFocus = new Point((int)focusX,(int)focusY); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { mScaling = false; mFocus = null; super.onScaleEnd(detector); } }; private GestureDetectorCompat scrollDetector; private GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { && Math.abs(distanceY) < ) return true; ]; mMatrix.getValues(matrix); mThumbnailLeft = matrix[]; mThumbnailTop = matrix[]; int viewWith = getWidth(); int viewHeight = getHeight(); if (mThumbnailLeft <= viewWith && mThumbnailLeft >= -thumbnailWidth && mThumbnailTop <= viewHeight && mThumbnailTop >= -thumbnailHeight){ && viewWith - mThumbnailLeft < -distanceX){ distanceX = -(viewWith - mThumbnailLeft); } && mThumbnailLeft + thumbnailWidth < distanceX){ distanceX = mThumbnailLeft + thumbnailWidth; } && viewHeight - mThumbnailTop < -distanceY){ distanceY = -(viewHeight - mThumbnailTop); } && mThumbnailTop + thumbnailHeight < distanceY){ distanceY = mThumbnailTop + thumbnailHeight; } mMatrix.postTranslate(-distanceX,-distanceY); if (mPercentage == 100.0f){ mMatrix.getValues(matrix); mThumbnailLeft = matrix[]; mThumbnailTop = matrix[]; float left = mThumbnailLeft; float top = mThumbnailTop; float right = mThumbnailLeft + thumbnailWidth; float bottom = mThumbnailTop + thumbnailHeight; mBitmapLeft = ; mBitmapTop = ; ){ mBitmapLeft = left; mDecodeRegion.left = ; mDecodeRegion.right = (int) (viewWith - left); ){ mBitmapTop = top; mDecodeRegion.top = ; mDecodeRegion.bottom = (int) (viewHeight - top); }else if(bottom < viewHeight){ mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else { mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } }){ mBitmapTop = top; mDecodeRegion.top = ; mDecodeRegion.bottom = (int) (viewHeight - top); if (right < viewWith){ mDecodeRegion.right = mBitmapWidth; mDecodeRegion.left = (int) (mBitmapWidth - right); }else{ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; } }else if(right < viewWith ){ mDecodeRegion.right = mBitmapWidth; mDecodeRegion.left = (int) (mBitmapWidth - right); if(bottom < viewHeight){ mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else{ mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } }else if(bottom < viewHeight){ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; mDecodeRegion.bottom = mBitmapHeight; mDecodeRegion.top = (int) (mBitmapHeight - bottom); }else{ mDecodeRegion.left = (int) -left; mDecodeRegion.right = mDecodeRegion.left + viewWith; mDecodeRegion.top = (int) -top; mDecodeRegion.bottom = mDecodeRegion.top + viewHeight; } mOptions.inSampleSize = ; Log.e(TAG, "onScroll: mDecodeRegion = " + mDecodeRegion ); && mDecodeRegion.height() > ){ mBitmap = mDecoder.decodeRegion(mDecodeRegion,mOptions); }else{ mBitmap = null; } } invalidate(); } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return true; } }; private void initGestureDetector() { scrollDetector = new GestureDetectorCompat(getContext(), simpleOnGestureListener); scaleDetector = new ScaleGestureDetector(getContext(), scaleGestureListener); } @Override public boolean onTouchEvent(MotionEvent event) { boolean ret = scaleDetector.onTouchEvent(event); ret |= scrollDetector.onTouchEvent(event); return ret || true; } * * ; // 256MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; void initCache(){ try { Context context = getContext(); File cacheDir = context.getCacheDir(); cacheDir = context.getExternalCacheDir(); final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||!isExternalStorageRemovable() ? context.getExternalCacheDir().getPath() : context.getCacheDir().getPath(); cacheDir = new File(cachePath + File.separator + DISK_CACHE_SUBDIR); PackageManager pm = context.getPackageManager(); PackageInfo pi = pm.getPackageInfo(context.getPackageName(), ); int appVersion = pi.versionCode; ; mDiskLruCache = DiskLruCache.open(cacheDir, appVersion,valueCount,DISK_CACHE_SIZE); } catch (IOException e) { e.printStackTrace(); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } } void init(){ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL); mPaint.setTextSize(); setLayerType(View.LAYER_TYPE_SOFTWARE,mPaint); // LinearGradient backGradient = new LinearGradient(0, 0, 0, mPaint.getTextSize(), new int[]{Color.BLACK, Color.GRAY ,Color.YELLOW}, null, Shader.TileMode.CLAMP); // mPaint.setShader(backGradient); mMatrix = new Matrix(); // initCache(); initGestureDetector(); initRegionDecoder(); } void initRegionDecoder(){ try { InputStream fs = getResources().getAssets().open("world_map.jpg"); mDecoder = BitmapRegionDecoder.newInstance(fs,false); mBitmapWidth = mDecoder.getWidth(); mBitmapHeight = mDecoder.getHeight(); mOptions = new BitmapFactory.Options(); mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; mOptions.inSampleSize = ; } catch (IOException e) { e.printStackTrace(); } } private void getAttrs(Context context, AttributeSet attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LargeImageViewAttrs); String file = ta.getString(R.styleable.LargeImageViewAttrs_src); ta.recycle(); } public LargeImageView(Context context) { super(context); init(); } public LargeImageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); getAttrs(context,attrs); init(); } public LargeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); getAttrs(context,attrs); init(); } @SuppressLint("NewApi") public LargeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); getAttrs(context,attrs); init(); } @Override protected void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(state); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } public static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) { // Raw height and width of image ; if (height > reqHeight || width > reqWidth) { final ; final ; || halfWidth <= ) return inSampleSize; if (width > height) { // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= ; } } else { while ((halfHeight / inSampleSize) >= reqHeight) { inSampleSize *= ; } } } return inSampleSize; } @Override protected void onDraw(Canvas canvas) { if (mPercentage < 100.0f && mThumbnailBitmap != null){ canvas.drawBitmap(mThumbnailBitmap,mMatrix,mPaint); }else if(mPercentage == 100.0f && mBitmap != null){ canvas.drawBitmap(mBitmap,mBitmapLeft,mBitmapTop,mPaint); } if (mFocus != null){ mPaint.setColor(Color.RED); mPaint.setMaskFilter(, BlurMaskFilter.Blur.SOLID)); mPaint.setColor(Color.parseColor("#ff0000")); canvas.drawCircle(mFocus.x,mFocus.y,,mPaint); } //draw percentage String percentage = mPercentage + "%"; float textSize = mPaint.getTextSize(); float percentWidth = mPaint.measureText("100.0%"); float circleX = getWidth() - percentWidth ; float circleY = percentWidth ; mPaint.setColor(Color.parseColor("#7f7A378B")); mPaint.setMaskFilter(, BlurMaskFilter.Blur.NORMAL)); canvas.drawCircle(circleX ,circleY,percentWidth * 0.66f,mPaint); mPaint.setColor(Color.WHITE); mPaint.setMaskFilter(null); percentWidth = mPaint.measureText(percentage); circleY += textSize / ; circleX -= percentWidth / ; canvas.drawText(percentage,circleX ,circleY,mPaint); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.e(TAG, "onMeasure: " ); int viewWidth = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); ) viewWidth = ; viewWidth = resolveSizeAndState(viewWidth, widthMeasureSpec, ); int viewHeight = getSuggestedMinimumHeight() + getPaddingBottom() + getPaddingTop(); ) viewHeight = ; //计算最佳值,在其中解析了 heightMeasureSpec viewHeight = resolveSizeAndState(viewHeight, heightMeasureSpec, ); //将量算的结果保存到View的成员变量mMeasuredWidth 和mMeasuredHeight中。 setMeasuredDimension(viewWidth, viewHeight); // 量算完成之后,View的父控件就可以通过调用 // getMeasuredWidth、getMeasuredState、getMeasuredWidthAndState // 这三个方法获取View的量算结果。 mBitmapLeft = ; mBitmapTop = ; - viewWidth / ; - viewHeight / ; mDecodeRegion = new Rect(left,top,left + viewWidth ,top + viewHeight ); // mDecodeRegion = new Rect(0,0,mBitmapWidth ,mBitmapHeight); mOptions.inSampleSize = calculateInSampleSize(mDecodeRegion.width(),mDecodeRegion.height(),viewWidth,viewHeight); // mOptions.inSampleSize = 2; long begin,end; begin = SystemClock.elapsedRealtime(); mBitmap = mDecoder.decodeRegion(mDecodeRegion,mOptions); end = SystemClock.elapsedRealtime(); thumbnailWidth = mBitmapWidth; thumbnailHeight = mBitmapHeight; BigDecimal bd = ).setScale(,BigDecimal.ROUND_HALF_UP); mPercentage = bd.floatValue(); Log.e(TAG, "init region = " + mDecodeRegion + " ratio = " + (float) mDecodeRegion.width() / mDecodeRegion.height() + " bitmap.w = " + mBitmap.getWidth() + " bitmap.h = " + mBitmap.getHeight() + " ratio = " + (float) mBitmap.getWidth() / mBitmap.getHeight() + " need " + (end - begin) + " ms"); if (mThumbnailBitmap != null){ Log.e(TAG, "onMeasure : mThumbnailBitmap is ok."); float width = mThumbnailBitmap.getWidth(); float height = mThumbnailBitmap.getHeight(); - width / ; - height / ; mMatrix.setTranslate(dx,dy); float factor = mBitmapWidth / width; mMatrix.postScale(factor,factor,viewWidth/ ,viewHeight /); } } @Override protected void onFinishInflate() { super.onFinishInflate(); Log.e(TAG, "onFinishInflate: " ); createThumbnailBitmap(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Log.e(TAG, "onAttachedToWindow: " ); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Log.e(TAG, "onDetachedFromWindow: " ); } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); Log.e(TAG, "onWindowVisibilityChanged : " + visibility); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); Log.e(TAG, "onWindowFocusChanged: hasWindowFocus = " + hasWindowFocus ); } Bitmap loadBitmapFromCache(String key){ Bitmap bitmap = null; try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { InputStream ); bitmap = BitmapFactory.decodeStream(is); } } catch (IOException e) { e.printStackTrace(); } return bitmap; } void pushToCache(String key, Bitmap value){ try { DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null){ OutputStream ); value.compress(Bitmap.CompressFormat.JPEG, , out); out.flush(); editor.commit(); mDiskLruCache.flush(); } } catch (IOException e) { e.printStackTrace(); } } }