Android 高清加载长图或大图方案

不过值得一提的是:上面这个手势检测的写法,不是我想的,而是一个开源的项目https://github.com/rharter/android-gesture-detectors,里面包含很多的手势检测。对应的博文是:http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg那面上面两个类就是我偷学了的~ 哈

在实际的项目中,可能会有更多的需求,比如增加放大、缩小;增加快滑手势等等,那么大家可以去参考这个库:https://github.com/johnnylambada/WorldMap,该库基本实现了绝大多数的需求,

一、概述

对于加载图片,大家都不陌生,一般为了尽可能避免OOM都会按照如下做法:

  1. 对于图片显示:根据需要显示图片控件的大小对图片进行压缩显示。
  2. 如果图片数量非常多:则会使用LruCache等缓存机制,将所有图片占据的内容维持在一个范围内。

其实对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等。

那么对于这种需求,该如何做呢?

首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中,所以肯定是局部加载,那么就需要用到一个类:

  • BitmapRegionDecoder

其次,既然屏幕显示不完,那么最起码要添加一个上下左右拖动的手势,让用户可以拖动查看。

那么综上,本篇博文的目的就是去自定义一个显示巨图的View,支持用户去拖动查看,大概的效果图如下:

好吧,这清明上河图太长了,想要观看全图,文末下载,图片在assets目录。当然如果你的图,高度也很大,肯定也是可以上下拖动的。

二、初识BitmapRegionDecoder

BitmapRegionDecoder主要用于显示图片的某一块矩形区域,如果你需要显示某个图片的指定区域,那么这个类非常合适。

对于该类的用法,非常简单,既然是显示图片的某一块区域,那么至少只需要一个方法去设置图片;一个方法传入显示的区域即可;详见:

  • BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,支持传入文件路径,文件描述符,文件的inputstrem等。

    例如:

    [html]  view plain  copy
    1. BitmapRegionDecoder bitmapRegionDecoder =  
    2.  BitmapRegionDecoder.newInstance(inputStream, false);<code class="language-java hljs  has-numbering"></code>  

  • 上述解决了传入我们需要处理的图片,那么接下来就是显示指定的区域。

    [html]  view plain  copy
    1. bitmapRegionDecoder.decodeRegion(rect, options);  

    参数一很明显是一个rect,参数二是BitmapFactory.Options,你可以控制图片的inSampleSize,inPreferredConfig等。

那么下面看一个超级简单的例子:

[html]  view plain  copy
  1. package com.zhy.blogcodes.largeImage;  
  2.   
  3. import android.graphics.Bitmap;  
  4. import android.graphics.BitmapFactory;  
  5. import android.graphics.BitmapRegionDecoder;  
  6. import android.graphics.Rect;  
  7. import android.os.Bundle;  
  8. import android.support.v7.app.AppCompatActivity;  
  9. import android.widget.ImageView;  
  10.   
  11. import com.zhy.blogcodes.R;  
  12.   
  13. import java.io.IOException;  
  14. import java.io.InputStream;  
  15.   
  16. public class LargeImageViewActivity extends AppCompatActivity  
  17. {  
  18.     private ImageView mImageView;  
  19.   
  20.     @Override  
  21.     protected void onCreate(Bundle savedInstanceState)  
  22.     {  
  23.         super.onCreate(savedInstanceState);  
  24.         setContentView(R.layout.activity_large_image_view);  
  25.   
  26.         mImageView = (ImageView) findViewById(R.id.id_imageview);  
  27.         try  
  28.         {  
  29.             InputStream inputStream = getAssets().open("tangyan.jpg");  
  30.   
  31.             //获得图片的宽、高  
  32.             BitmapFactory.Options tmpOptions = new BitmapFactory.Options();  
  33.             tmpOptions.inJustDecodeBounds = true;  
  34.             BitmapFactory.decodeStream(inputStream, null, tmpOptions);  
  35.             int width = tmpOptions.outWidth;  
  36.             int height = tmpOptions.outHeight;  
  37.   
  38.             //设置显示图片的中心区域  
  39.             BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);  
  40.             BitmapFactory.Options options = new BitmapFactory.Options();  
  41.             options.inPreferredConfig = Bitmap.Config.RGB_565;  
  42.             Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);  
  43.             mImageView.setImageBitmap(bitmap);  
  44.   
  45.   
  46.         } catch (IOException e)  
  47.         {  
  48.             e.printStackTrace();  
  49.         }  
  50.   
  51.   
  52.     }  
  53.   
  54. }  

上述代码,就是使用BitmapRegionDecoder去加载assets中的图片,调用bitmapRegionDecoder.decodeRegion解析图片的中间矩形区域,返回bitmap,最终显示在ImageView上。

效果图:

上面的小图显示的即为下面的大图的中间区域。

ok,那么目前我们已经了解了BitmapRegionDecoder的基本用户,那么往外扩散,我们需要自定义一个控件去显示巨图就很简单了,首先Rect的范围就是我们View的大小,然后根据用户的移动手势,不断去更新我们的Rect的参数即可。

三、自定义显示大图控件

根据上面的分析呢,我们这个自定义控件思路就非常清晰了:

  • 提供一个设置图片的入口
  • 重写onTouchEvent,在里面根据用户移动的手势,去更新显示区域的参数
  • 每次更新区域参数后,调用invalidate,onDraw里面去regionDecoder.decodeRegion拿到bitmap,去draw

理清了,发现so easy,下面上代码:

[html]  view plain  copy
  1. package com.zhy.blogcodes.largeImage.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Bitmap;  
  5. import android.graphics.BitmapFactory;  
  6. import android.graphics.BitmapRegionDecoder;  
  7. import android.graphics.Canvas;  
  8. import android.graphics.Rect;  
  9. import android.util.AttributeSet;  
  10. import android.view.MotionEvent;  
  11. import android.view.View;  
  12.   
  13. import java.io.IOException;  
  14. import java.io.InputStream;  
  15.   
  16. /**  
  17.  * Created by zhy on 15/5/16.  
  18.  */  
  19. public class LargeImageView extends View  
  20. {  
  21.     private BitmapRegionDecoder mDecoder;  
  22.     /**  
  23.      * 图片的宽度和高度  
  24.      */  
  25.     private int mImageWidth, mImageHeight;  
  26.     /**  
  27.      * 绘制的区域  
  28.      */  
  29.     private volatile Rect mRect = new Rect();  
  30.   
  31.     private MoveGestureDetector mDetector;  
  32.   
  33.   
  34.     private static final BitmapFactory.Options options = new BitmapFactory.Options();  
  35.   
  36.     static  
  37.     {  
  38.         options.inPreferredConfig = Bitmap.Config.RGB_565;  
  39.     }  
  40.   
  41.     public void setInputStream(InputStream is)  
  42.     {  
  43.         try  
  44.         {  
  45.             mDecoder = BitmapRegionDecoder.newInstance(is, false);  
  46.             BitmapFactory.Options tmpOptions = new BitmapFactory.Options();  
  47.             // Grab the bounds for the scene dimensions  
  48.             tmpOptions.inJustDecodeBounds = true;  
  49.             BitmapFactory.decodeStream(is, null, tmpOptions);  
  50.             mImageWidth = tmpOptions.outWidth;  
  51.             mImageHeight = tmpOptions.outHeight;  
  52.   
  53.             requestLayout();  
  54.             invalidate();  
  55.         } catch (IOException e)  
  56.         {  
  57.             e.printStackTrace();  
  58.         } finally  
  59.         {  
  60.   
  61.             try  
  62.             {  
  63.                 if (is != null) is.close();  
  64.             } catch (Exception e)  
  65.             {  
  66.             }  
  67.         }  
  68.     }  
  69.   
  70.   
  71.     public void init()  
  72.     {  
  73.         mDetector = new MoveGestureDetector(getContext(), new MoveGestureDetector.SimpleMoveGestureDetector()  
  74.         {  
  75.             @Override  
  76.             public boolean onMove(MoveGestureDetector detector)  
  77.             {  
  78.                 int moveX = (int) detector.getMoveX();  
  79.                 int moveY = (int) detector.getMoveY();  
  80.   
  81.                 if (mImageWidth > getWidth())  
  82.                 {  
  83.                     mRect.offset(-moveX, 0);  
  84.                     checkWidth();  
  85.                     invalidate();  
  86.                 }  
  87.                 if (mImageHeight > getHeight())  
  88.                 {  
  89.                     mRect.offset(0, -moveY);  
  90.                     checkHeight();  
  91.                     invalidate();  
  92.                 }  
  93.   
  94.                 return true;  
  95.             }  
  96.         });  
  97.     }  
  98.   
  99.   
  100.     private void checkWidth()  
  101.     {  
  102.   
  103.   
  104.         Rect rect = mRect;  
  105.         int imageWidth = mImageWidth;  
  106.         int imageHeight = mImageHeight;  
  107.   
  108.         if (rect.right > imageWidth)  
  109.         {  
  110.             rect.right = imageWidth;  
  111.             rect.left = imageWidth - getWidth();  
  112.         }  
  113.   
  114.         if (rect.left < 0)  
  115.         {  
  116.             rect.left = 0;  
  117.             rect.right = getWidth();  
  118.         }  
  119.     }  
  120.   
  121.   
  122.     private void checkHeight()  
  123.     {  
  124.   
  125.         Rect rect = mRect;  
  126.         int imageWidth = mImageWidth;  
  127.         int imageHeight = mImageHeight;  
  128.   
  129.         if (rect.bottom > imageHeight)  
  130.         {  
  131.             rect.bottom = imageHeight;  
  132.             rect.top = imageHeight - getHeight();  
  133.         }  
  134.   
  135.         if (rect.top < 0)  
  136.         {  
  137.             rect.top = 0;  
  138.             rect.bottom = getHeight();  
  139.         }  
  140.     }  
  141.   
  142.   
  143.     public LargeImageView(Context context, AttributeSet attrs)  
  144.     {  
  145.         super(context, attrs);  
  146.         init();  
  147.     }  
  148.   
  149.     @Override  
  150.     public boolean onTouchEvent(MotionEvent event)  
  151.     {  
  152.         mDetector.onToucEvent(event);  
  153.         return true;  
  154.     }  
  155.   
  156.     @Override  
  157.     protected void onDraw(Canvas canvas)  
  158.     {  
  159.         Bitmap bm = mDecoder.decodeRegion(mRect, options);  
  160.         canvas.drawBitmap(bm, 0, 0, null);  
  161.     }  
  162.   
  163.     @Override  
  164.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
  165.     {  
  166.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  167.   
  168.         int width = getMeasuredWidth();  
  169.         int height = getMeasuredHeight();  
  170.   
  171.         int imageWidth = mImageWidth;  
  172.         int imageHeight = mImageHeight;  
  173.   
  174.          //默认直接显示图片的中心区域,可以自己去调节  
  175.         mRect.left = imageWidth / 2 - width / 2;  
  176.         mRect.top = imageHeight / 2 - height / 2;  
  177.         mRect.right = mRect.left + width;  
  178.         mRect.bottom = mRect.top + height;  
  179.   
  180.     }  
  181.   
  182.   
  183. }  

根据上述源码:

  1. setInputStream里面去获得图片的真实的宽度和高度,以及初始化我们的mDecoder
  2. onMeasure里面为我们的显示区域的rect赋值,大小为view的尺寸
  3. onTouchEvent里面我们监听move的手势,在监听的回调里面去改变rect的参数,以及做边界检查,最后invalidate
  4. 在onDraw里面就是根据rect拿到bitmap,然后draw了

ok,上面并不复杂,不过大家有没有注意到,这个监听用户move手势的代码写的有点奇怪,恩,这里模仿了系统的ScaleGestureDetector,编写了MoveGestureDetector,代码如下:

  • MoveGestureDetector

    [html]  view plain  copy
    1. package com.zhy.blogcodes.largeImage.view;  
    2.   
    3. import android.content.Context;  
    4. import android.graphics.PointF;  
    5. import android.view.MotionEvent;  
    6.   
    7. public class MoveGestureDetector extends BaseGestureDetector  
    8. {  
    9.   
    10.     private PointF mCurrentPointer;  
    11.     private PointF mPrePointer;  
    12.     //仅仅为了减少创建内存  
    13.     private PointF mDeltaPointer = new PointF();  
    14.   
    15.     //用于记录最终结果,并返回  
    16.     private PointF mExtenalPointer = new PointF();  
    17.   
    18.     private OnMoveGestureListener mListenter;  
    19.   
    20.   
    21.     public MoveGestureDetector(Context context, OnMoveGestureListener listener)  
    22.     {  
    23.         super(context);  
    24.         mListenter = listener;  
    25.     }  
    26.   
    27.     @Override  
    28.     protected void handleInProgressEvent(MotionEvent event)  
    29.     {  
    30.         int actionCode = event.getAction() & MotionEvent.ACTION_MASK;  
    31.         switch (actionCode)  
    32.         {  
    33.             case MotionEvent.ACTION_CANCEL:  
    34.             case MotionEvent.ACTION_UP:  
    35.                 mListenter.onMoveEnd(this);  
    36.                 resetState();  
    37.                 break;  
    38.             case MotionEvent.ACTION_MOVE:  
    39.                 updateStateByEvent(event);  
    40.                 boolean update = mListenter.onMove(this);  
    41.                 if (update)  
    42.                 {  
    43.                     mPreMotionEvent.recycle();  
    44.                     mPreMotionEvent = MotionEvent.obtain(event);  
    45.                 }  
    46.                 break;  
    47.   
    48.         }  
    49.     }  
    50.   
    51.     @Override  
    52.     protected void handleStartProgressEvent(MotionEvent event)  
    53.     {  
    54.         int actionCode = event.getAction() & MotionEvent.ACTION_MASK;  
    55.         switch (actionCode)  
    56.         {  
    57.             case MotionEvent.ACTION_DOWN:  
    58.                 resetState();//防止没有接收到CANCEL or UP ,保险起见  
    59.                 mPreMotionEvent = MotionEvent.obtain(event);  
    60.                 updateStateByEvent(event);  
    61.                 break;  
    62.             case MotionEvent.ACTION_MOVE:  
    63.                 mGestureInProgress = mListenter.onMoveBegin(this);  
    64.                 break;  
    65.         }  
    66.   
    67.     }  
    68.   
    69.     protected void updateStateByEvent(MotionEvent event)  
    70.     {  
    71.         final MotionEvent prev = mPreMotionEvent;  
    72.   
    73.         mPrePointer = caculateFocalPointer(prev);  
    74.         mCurrentPointer = caculateFocalPointer(event);  
    75.   
    76.         //Log.e("TAG", mPrePointer.toString() + " ,  " + mCurrentPointer);  
    77.   
    78.         boolean mSkipThisMoveEvent = prev.getPointerCount() != event.getPointerCount();  
    79.   
    80.         //Log.e("TAG", "mSkipThisMoveEvent = " + mSkipThisMoveEvent);  
    81.         mExtenalPointer.x = mSkipThisMoveEvent ? 0 : mCurrentPointer.x - mPrePointer.x;  
    82.         mExtenalPointer.y = mSkipThisMoveEvent ? 0 : mCurrentPointer.y - mPrePointer.y;  
    83.   
    84.     }  
    85.   
    86.     /**  
    87.      * 根据event计算多指中心点  
    88.      *  
    89.      * @param event  
    90.      * @return  
    91.      */  
    92.     private PointF caculateFocalPointer(MotionEvent event)  
    93.     {  
    94.         final int count = event.getPointerCount();  
    95.         float x = 0y = 0;  
    96.         for (int i = 0; i < count; i++)  
    97.         {  
    98.             x += event.getX(i);  
    99.             y += event.getY(i);  
    100.         }  
    101.   
    102.         x /= count;  
    103.         y /= count;  
    104.   
    105.         return new PointF(x, y);  
    106.     }  
    107.   
    108.   
    109.     public float getMoveX()  
    110.     {  
    111.         return mExtenalPointer.x;  
    112.   
    113.     }  
    114.   
    115.     public float getMoveY()  
    116.     {  
    117.         return mExtenalPointer.y;  
    118.     }  
    119.   
    120.   
    121.     public interface OnMoveGestureListener  
    122.     {  
    123.         public boolean onMoveBegin(MoveGestureDetector detector);  
    124.   
    125.         public boolean onMove(MoveGestureDetector detector);  
    126.   
    127.         public void onMoveEnd(MoveGestureDetector detector);  
    128.     }  
    129.   
    130.     public static class SimpleMoveGestureDetector implements OnMoveGestureListener  
    131.     {  
    132.   
    133.         @Override  
    134.         public boolean onMoveBegin(MoveGestureDetector detector)  
    135.         {  
    136.             return true;  
    137.         }  
    138.   
    139.         @Override  
    140.         public boolean onMove(MoveGestureDetector detector)  
    141.         {  
    142.             return false;  
    143.         }  
    144.   
    145.         @Override  
    146.         public void onMoveEnd(MoveGestureDetector detector)  
    147.         {  
    148.         }  
    149.     }  
    150.   
    151. }  

  • BaseGestureDetector

    [html]  view plain  copy
    1. package com.zhy.blogcodes.largeImage.view;  
    2.   
    3. import android.content.Context;  
    4. import android.view.MotionEvent;  
    5.   
    6.   
    7. public abstract class BaseGestureDetector  
    8. {  
    9.   
    10.     protected boolean mGestureInProgress;  
    11.   
    12.     protected MotionEvent mPreMotionEvent;  
    13.     protected MotionEvent mCurrentMotionEvent;  
    14.   
    15.     protected Context mContext;  
    16.   
    17.     public BaseGestureDetector(Context context)  
    18.     {  
    19.         mContext = context;  
    20.     }  
    21.   
    22.   
    23.     public boolean onToucEvent(MotionEvent event)  
    24.     {  
    25.   
    26.         if (!mGestureInProgress)  
    27.         {  
    28.             handleStartProgressEvent(event);  
    29.         } else  
    30.         {  
    31.             handleInProgressEvent(event);  
    32.         }  
    33.   
    34.         return true;  
    35.   
    36.     }  
    37.   
    38.     protected abstract void handleInProgressEvent(MotionEvent event);  
    39.   
    40.     protected abstract void handleStartProgressEvent(MotionEvent event);  
    41.   
    42.     protected abstract void updateStateByEvent(MotionEvent event);  
    43.   
    44.     protected void resetState()  
    45.     {  
    46.         if (mPreMotionEvent != null)  
    47.         {  
    48.             mPreMotionEvent.recycle();  
    49.             mPreMotionEvent = null;  
    50.         }  
    51.         if (mCurrentMotionEvent != null)  
    52.         {  
    53.             mCurrentMotionEvent.recycle();  
    54.             mCurrentMotionEvent = null;  
    55.         }  
    56.         mGestureInProgress = false;  
    57.     }  
    58.   
    59.   
    60. }  

    你可能会说,一个move手势搞这么多代码,太麻烦了。的确是的,move手势的检测非常简单,那么之所以这么写呢,主要是为了可以复用,比如现在有一堆的XXXGestureDetector,当我们需要监听什么手势,就直接拿个detector来检测多方便。我相信大家肯定也郁闷过Google,为什么只有ScaleGestureDetector而没有RotateGestureDetector呢。

根据上述,大家应该理解了为什么要这么做,当时不强制,每个人都有个性。

不过值得一提的是:上面这个手势检测的写法,不是我想的,而是一个开源的项目https://github.com/rharter/android-gesture-detectors,里面包含很多的手势检测。对应的博文是:http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg那面上面两个类就是我偷学了的~ 哈

四、测试

测试其实没撒好说的了,就是把我们的LargeImageView放入布局文件,然后Activity里面去设置inputstream了。

[html]  view plain  copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.                 xmlns:tools="http://schemas.android.com/tools"  
  3.                 android:layout_width="match_parent"  
  4.                 android:layout_height="match_parent">  
  5.   
  6.   
  7.     <com.zhy.blogcodes.largeImage.view.LargeImageView  
  8.         android:id="@+id/id_largetImageview"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="match_parent"/>  
  11.   
  12. </RelativeLayout>  

然后在Activity里面去设置图片:

[html]  view plain  copy
  1. package com.zhy.blogcodes.largeImage;  
  2.   
  3. import android.os.Bundle;  
  4. import android.support.v7.app.AppCompatActivity;  
  5.   
  6. import com.zhy.blogcodes.R;  
  7. import com.zhy.blogcodes.largeImage.view.LargeImageView;  
  8.   
  9. import java.io.IOException;  
  10. import java.io.InputStream;  
  11.   
  12. public class LargeImageViewActivity extends AppCompatActivity  
  13. {  
  14.     private LargeImageView mLargeImageView;  
  15.   
  16.     @Override  
  17.     protected void onCreate(Bundle savedInstanceState)  
  18.     {  
  19.         super.onCreate(savedInstanceState);  
  20.         setContentView(R.layout.activity_large_image_view);  
  21.   
  22.         mLargeImageView = (LargeImageView) findViewById(R.id.id_largetImageview);  
  23.         try  
  24.         {  
  25.             InputStream inputStream = getAssets().open("world.jpg");  
  26.             mLargeImageView.setInputStream(inputStream);  
  27.   
  28.         } catch (IOException e)  
  29.         {  
  30.             e.printStackTrace();  
  31.         }  
  32.   
  33.   
  34.     }  
  35.   
  36. }  

效果图:

ok,那么到此,显示巨图的方案以及详细的代码就描述完成了,总体还是非常简单的。 
但是,在实际的项目中,可能会有更多的需求,比如增加放大、缩小;增加快滑手势等等,那么大家可以去参考这个库:https://github.com/johnnylambada/WorldMap,该库基本实现了绝大多数的需求,大家根据本文这个思路再去看这个库,也会简单很多,定制起来也容易。我这个地图的图就是该库里面提供的。

猜你喜欢

转载自blog.csdn.net/lin20044140410/article/details/79485920