不少安卓开发者都有图片加载的处理经验,比如通过压缩节省图片加载中对内存的消耗。
我们经常做的是把一张1280之类大小的图片以适应屏幕大小的尺寸展现出来,同时能够通过缩放来观察。
不过这是一般水平,通过压缩来处理的话通常会导致在最大尺寸放大后看不清细节,比如拿到一张清明上河图,或者一张世界地图,这个时候我们要保证在最大限度的放大后仍然能够看清楚每个人物每个城市,一般的压缩的方案就不合适了。

这里我们要讨论的是如何用局部解析(BitmapRegionDecoder)来做到在不占用过多内存的情况下实现超大图的缩放。

惯例贴源码:​ XPhotoViewDemo

XPhotoView继承ImageView实现了超大图加载,Demo中演示了如何在Pager加载静态图片和动图,同时也支持各种手势操作。
我在公司的产品上自定义了XPhotoView,在包括聊天列表,动图播放,还有高清大图查看的功能上已经验证了它的稳定和高效,平时的开发中可以直接使用。

超大图片加载和局部解析

对于普通的图片,我们加载的思路很简单就是压缩大小,用Options来获得大小然后和当前屏幕大小进行比较,然后以一定的值压缩。但是这样带来的问题是,压缩后的图片会丢失细节,如果是小泽...呸,如果是清明上河图压缩到屏幕大小,放大后怕是人都看不见。而整张图片加载肯定是行不通的,内存绝对立马爆。
解决方案很简单,我们只加载图片的局部区域,这部分区域适配屏幕大小,配合手势移动的时候更新显示对应的区域就可以了。
Android提供了BitmapRegionDecoder
来进行图片的局部解析,这是XPhotoView的主要思路。
剩下的就是如何高效的加载的问题了,如何设计代码逻辑,让它能够快速的响应手势动作呢。

局部解析的代码逻辑

代码结构

XPhotoView的代码概要如下所示,

--|--|--XPhotoView|  ||  |--PhotoViewAttacher|--GestureManager

大体可以分为两部分,XPhotoView和PhotoViewAttacher负责图片的加载和解析,GestureManager负责手势的判断和响应。整个库对外暴露的只是XPhotoView的几个public方法用来setImage和相关的Listener,还有是否是Gif的参数。
Attacher本身只负责图片的拆分解析和渲染过程,同时Bitmap也是保存在Attacher中。Attacher和XPhotoView之间通过Interface互相调用,以此隔离。

Attacher 的解析过程

我们暂时忽略Gif的部分,先描述一下Attacher的思路。
Attacher有一个内部子类BitmapUnit和BitmapGridStrategy,初始图片会被BitmapRegionDecoder切割为 N*M 的网格,然后存储在BitmapUnit[N][M]二维数组 mGrids 中。

image.png

以清明上河图为例,图中高亮的线条把图片分割为三部分,就是说我们用 BitmapUnit[1][3] 来存储这张图片。这么做的原因是,当我们放大图片来查看的时候,只需要解析单个格子以及它相邻格子里的图片。
当然在我们以适配屏幕的条件下查看全图时,是经过mSampleSize比例压缩的,也就是说在mGrids中的Bitmap是压缩过后的占小内存的位图,不用担心OOM的问题。

    /** 当前图片的的采样率 */private int mSampleSize = 0;/**** View Rect* View 坐标系*/private Rect mViewRect = new Rect();/** 原图 Rect*  Bitmap 坐标系 */private Rect mImageRect = new Rect();/*** 实际展示的 Bitmap 大小* Bitmap 坐标系 */private RectF mShowBitmapRect = new RectF();/*** view 相对 Show Bitmap 的坐标* Bitmap 坐标系 */private Rect mViewBitmapRect = new Rect();

以上是Attacher中的关键变量,整个解析和渲染的过程基于这四个Rect的坐标。

现在我们开始整个流程。

初始化

/*** @param bitmap 设置 bitmap* @param cache 是否cache*/
void setBitmap(Bitmap bitmap, boolean cache);/*** @param is 设置输入流* @param config config*/
void setInputStream(InputStream is, Bitmap.Config config);

这两个是Attacher定义的对外接口,它只允许两种方式来设置图片,不管是哪个方式,都会转换为InputStream对象mDecodeInputStream,来作为BitmapRegionDecoder的来源。
若以setBitmap()方法初始化的话,会多设置一个mSrcBitmap,当进行局部解析时就不会通过BitmapRegionDecoder来解析,而是会直接从mSrcBitmap中createBitmap对应的区域出来。这种方式的前提是默认不会出现OOM,毕竟已经可以整个Bitmap作为参数传进来了,但是不能保证在后面createBitmap时不会OOM,所以不提倡用这个方法来初始化。

在调用这两个方法任何一个之后,都会调用initialize()来初始化需要的线程和Handler,

/** 初始化所需参数和线程*/
private synchronized void initialize(Bitmap.Config config)

然后我们来到setBitmapDecoder(final InputStream is)方法,此时我们开始真正的拆图和解析。这个方法是所有的起点,而且只会也只应该走一次。
它会把mInstanceDecoderRunnable丢给handler然后开始运行,在解析完成后通过回调告知上层解析完毕,可以进行关闭进度条之类的操作。

获取图片初始显示参数

此时我们会调用这个方法

private void initiateViewRect(int viewWidth, int viewHeight)

但是第一次调用的时候是在onDraw之前,在setImage之后,此时我们并不知道具体的Canvas的大小,因此没法确定缩放比例,还有其他的Rect所需要的初始化的具体值。因此此时mViewRect的值都还是0,作为参数传进来后经过校验是无效值,则会退出此次的方法。
这时我们来看看draw()方法,

@Override
public boolean draw(@NonNull Canvas canvas, int width, int height) {if (isNotAvailable()) {return false;}if (mSrcBitmap != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {int mw = canvas.getMaximumBitmapWidth();int mh = canvas.getMaximumBitmapHeight();/*** 如果图片太大,直接使用bitmap 会占用很大内存,所以建议缓存为文件再显示*/if (mSrcBitmap.getHeight() > mh || mSrcBitmap.getWidth() > mw) {//TODO}}/*** 更新视图或者画出图片*/return !checkOrUpdateViewRect(width, height) && mBitmapGrid.drawVisibleGrid(canvas);
}

在XPhotoView调用draw()方法后,最后会进行有效性检查,就是checkOrUpdateViewRect(),这时会把真正的视图的大小作为参数传给 initiateViewRect(),然后再真正的进行参数的初始化。
是的,这里我们用延迟的方式来获取到真正的视图大小,虽然代码不容易理解,但是稳定性提高了。

接下来我们要初始化几个参数,

mShowBitmapRect, mViewBitmapRect, mSampleSize

图片的初始化显示方式有几种,按Android的定义有FIT_CENTER,CENTER_CROP等,这里我们默认用 CENTER_CROP 的方式,而且显示图片的起始部分,横图从左边开始,竖图从最上面开始。
这里需要关注的关键代码是,

    private void initiateViewRect(int viewWidth, int viewHeight) {····/** 以 view 宽/长比例和 image 宽/长比例做比较*  iW/iH < vW/vH : 左右留空,取高比值*  iW/iH > vW/vH : 上下留空,取宽比值 */float ratio = (imgWidth/imgHeight < viewWidth/viewHeight) ? (imgHeight * 1.0f / viewHeight) : (imgWidth * 1.0f / viewWidth);mShowBitmapRect.set(0, 0, (int) (imgWidth / ratio), (int) (imgHeight / ratio));/** 保存初始大小 */mShowBitmapRect.round(mInitiatedShowBitmapRect);/** 取缩小到适配view时的bitmap的起始位置 */int left = (int) ((mShowBitmapRect.width() - mViewRect.width()) / 2);int top = (int) ((mShowBitmapRect.height() - mViewRect.height()) / 2);left = mShowBitmapRect.width() < mViewRect.width() ? left : 0;int right = left + mViewRect.width();top = mShowBitmapRect.height() < mViewRect.height() ? top : 0;int bottom = top + mViewRect.height();mViewBitmapRect.set(left, top, right, bottom);····}

我们通过比较View和Image的高/宽比例,确定图片是横图还是竖图,
以横图为例子,此时我们需要将它上下撑大到刚好铺满屏幕,那么就得以View的高度和Image的高度来算出压缩值 ratio,用它来算出压缩后的图片的实际大小,保存在 mShowBitmapRect中。
完成这一步后,接下来计算mViewBitmapRect。还记得我们的设定是显示图片的起始部位么,

/** 取缩小到适配view时的bitmap的起始位置 */
int left = (int) ((mShowBitmapRect.width() - mViewRect.width()) / 2);
int top = (int) ((mShowBitmapRect.height() - mViewRect.height()) / 2);left = mShowBitmapRect.width() < mViewRect.width() ? left : 0;
int right = left + mViewRect.width();
top = mShowBitmapRect.height() < mViewRect.height() ? top : 0;
int bottom = top + mViewRect.height();mViewBitmapRect.set(left, top, right, bottom);

这部分代码,既保证了超大图在缩放后从起始位置开始,也保证了普通图片缩放后不满屏的情况下居中显示,大家可以琢磨琢磨。

要留意initiateViewRect这个方法,它不仅在初始化的时候调用,后面每次的缩放,都需要调用它来更新mShowBitmapRect,因为每次的缩放都会让实际显示的图片大小发生改变。

绘制流程

关注 draw()方法,这里是绘制流程的关键部分,
每次绘制前都会先更新当前的各个Rect对象,以获得对应的显示中的Grid,我们只绘制显示出来的部分Grid单元,
下面的图大致描述在绘制时的情形,

* +--+--+--+--+--+
* |XX|XX|11|11|XX|
* +--+--+--+--+--+
* |XX|XX|11|11|XX|
* +--+--+--+--+--+
* |XX|XX|XX|XX|XX|
* +--+--+--+--+--+

标记为11的四个格子,表示目前可见的区域,xx的表示不可见区域。
对于可见区域,会结合当前的缩放值,从mBitmapGrid中取出,然后通过XPhotoView传进来的Canvas对象绘制,
对于不可见区域,会回收掉对应的bitmap对象,以节省内存。

手势响应

关于手势响应,是比较简单的一个部分,
我们定义了GestureManager,把XPhotoView的事件交给GestureManager的onTouchEvent()处理,
这部分代码相对简单,不做过多解释。

兼容动图

动图的显示方式有两种方案,

  • 用Movie类来显示
  • 托管给Glide的GifDrawable去渲染

Movie的方式

这种方式相对简单,
在我们不知道对应的文件或者图片是否是动图的情况下,以正常逻辑设置即可,
设置之后会用Movie类来判断是否是一个有效的GIF图,
之后在draw时,在Gif的情况下会用Movie类来进行渲染

Glide的方式

很多项目会用Glide来做图片的下载和显示,
Glide本身会判断图片是否为Gif,当是Gif时会构造一个 GifDrawable 对象,
我们直接把这个GifDrawable对象用 setImageDrawable 的方式设置到XPhotoView,
GifDrawable会接管动图的绘制流程。
注意如果这种情况下动图不动的话,需要在 setImageDrawable 之后调用 GifDrawable 的 start()方法,

if(glideDrawable instanceof GifDrawable) {holder.photoView.setGif(true);holder.photoView.setImageDrawable(glideDrawable);((GifDrawable) glideDrawable).start();
}

超大图片的显示:BitmapRegionDecoder 1相关推荐

  1. 超大超长图片居中显示且放大缩小无影响

    1. 超大图片居中显示 1.1 放大缩小都居中显示 <!DOCTYPE html> <html lang="en"> <head> <me ...

  2. js大屏导出图片_超大图片全屏动态展示js插件

    intense-images是一款非常实用的超大图片全屏动态展示js插件.该图片查看插件可以全屏显示超大图片,可以使用鼠标来和图片进行交互,上下左右移动鼠标会相应的移动图片,对于超大图片的展示是非常好 ...

  3. 兼容超大图片的处理_N年都没弄明白!PS保存的图片格式都啥意思?

    点击蓝字 关注南哥   这个表里的格式,你都说得清么! 01 PSD 特点:支持PS全部功能,一般不用于成品图保存 后缀:.psd PSD是Photoshop的官方格式,可以自动保留下PS里的全部功能 ...

  4. 前端加载超大图片(100M以上)实现秒开解决方案

    前言 前端加载超大图片时,一般可以采取以下措施实现加速: 图片压缩:将图片进行压缩可以大幅减小图片的大小,从而缩短加载时间.压缩图片时需要注意保持图片质量,以免影响图片显示效果. 图片分割:将超大图片 ...

  5. 图片的略小图图片不显示的处理方法

    刚才写博客不小心勾选了其中的一项图片的略小图不显示图片 只显示背景,例如如下 让人感觉很不好, 处理方法: 也是开发文件夹,计算机,或我的电脑找到工具点击 ---在点击下面的文件夹选项会弹出一个界面如 ...

  6. bmp文件头_「正点原子FPGA连载」第十九章SD卡读BMP图片LCD显示

    1)摘自[正点原子]领航者 ZYNQ 之嵌入式开发指南 2)实验平台:正点原子领航者ZYNQ开发板 3)平台购买地址:https://item.taobao.com/item.htm?&id= ...

  7. ios中图片不显示的问题

    从昨天为止,发现在button上面拖动的图片一直显示不出来,也不知道什么原因.后来发现图片命名上有问题. 刚开始我将图片命名为"1.png" 然后作为button的image.在m ...

  8. img 服务器上的图片不显示图片,img显示服务器图片不显示

    img显示服务器图片不显示 内容精选 换一换 您可根据需求自定义下发消息内容,例如新增场景:终端用户发送"应用魔方"时,聊天机器人自动回复该产品介绍给用户.在"ChatB ...

  9. 向窗体中拖放图片并显示

    Frm_Main.cs View Code 1 using System; 2 using System.Collections.Generic; 3 using System.ComponentMo ...

最新文章

  1. 时间序列预测---差分自回归移动平均模型(ARIMA模型)
  2. C语言库函数大全及应用实例二
  3. BigData之Hbase:Hbase数据管理的简介、下载、案例应用之详细攻略
  4. 如何选择开源许可证?(转)
  5. pc网站和移动网站在同一服务器吗,机动都市阿尔法PC服和移动服互通吗
  6. TZOJ 5471: 数据结构实验--图的最小代价生成树
  7. 计算机音乐植物大战僵尸泳池,植物大战僵尸(泳池)
  8. linux鼠标滚轮不能上下滚动页面,解决办法:如果鼠标滚轮无法上下滚动怎么办...
  9. java类加载器ClassLoader浅析
  10. 使用spring data Querydsl 实现动态查询功能
  11. java5 64_java8 64位(官方免费版下载2020)
  12. Android 开发自己的网络收音机4——读取XML文件的电台数据
  13. 61-70作业关系符运算
  14. HTML吸引人眼球的网页,优秀网页设计:35个吸引眼球的精美作品集网站
  15. kafka集群如何内外网均可访问
  16. 电赛校赛总结----一维板球系统【代码开源】
  17. Qt编写的开源帖子集合(懒人专用)
  18. 微信小程序Swiper组件轮播图片尺寸填坑
  19. idea设置类注释和方法注释模板
  20. fast角点检测 java_米联客 ZYNQ/SOC 精品教程 S04-CH11 快速角点检测之硬件实现

热门文章

  1. C4D 从入门到放弃
  2. mysql索引升序降序失效原因_关于联合索引的升序降序和order by关系以及失效问题...
  3. 冒泡排序 以及利用函数升序 降序
  4. DxLib做弹幕射击游戏(二)——画一台敌机
  5. 电源管理芯片的架构与功率
  6. PC微信3.5.0.46几个函数位置
  7. java求π的近似值
  8. 如何修改Word文档里面的显示比例
  9. 办公娱乐一站式服务 威联通TS-251 NAS评测
  10. 【管理】管理者三种思维