不过值得一提的是:上面这个手势检测的写法,不是我想的,而是一个开源的项目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,该库基本实现了绝大多数的需求,大家根据本文这个思路再去看这个库,也会简单很多,定制起来也容易。我这个地图的图就是该库里面提供的。

更多相关文章

  1. Bitmap保存图片到指定路径
  2. Android(安卓)Studio自带的底部导航栏 实现从其他Activity跳转到
  3. Android自带的下载功能,不需要断点续传、大文件下载、通知栏显示
  4. android 图片下载方法总结
  5. Android(安卓)的WebView长按保存图片
  6. Android(安卓)使用SeekBar时动态显示进度且随SeekBar一起移动
  7. 解决Viewpager满屏不能自适应填充内容的三种办法
  8. Android(安卓)Dialog种类大全,让Activity显示在另外一个Activity
  9. AndroidStudio快捷键

随机推荐

  1. 三分钟掌握爬虫技术加具体实例
  2. 模拟 HDOJ 5099 Comparison of Android(
  3. 在android中如何判断当前的apk是否处于运
  4. Android自动化工具Monkeyrunner使用(七) —
  5. Android下拉刷新,上拉加载——Ultra-Pull-
  6. Android模拟器使用SD卡(2)
  7. Android(安卓)EditText禁止输入中文字符
  8. android振动器
  9. [置顶] Android学习之Alarm driver
  10. 关于TabLayout+ViewPager组合实现多页面