我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状、不同的大小,但在大多数情况下,这些图片都会大于我们程序所需要的大小。比如微博长图,海报等等。所以我们就要对图片进行局部显示。
大图加载基本需求和原理分析
基本需求:当我们有一张绿色大小的大图,我们需要让其展示成蓝色部分的大小,一般在我们滑动的过程中我们就只能看到蓝色部分的图片,蓝色部分的下面部分通过向下滑动才能看到。
原理分析:这里涉及到区域加载,由于我们人眼就只能看到占满手机屏幕大小的图片,蓝色部分下面部分是看不到的,这就意味着我们每次加载图片只需要加载到我们能看到的区域即可,看不到的区域就不加载。
假设我们的图片高度是手机的5倍,那我们首次加载其实就是图片的1/5,而我们不管继续往下滑,每次都加载图片的1/5,那么我们就能节省4/5的内存。
那么问题来了?我们如何做到区域加载和内存复用?
比如我们讲图片分成了5份,我们每次都加载这1/5的内存,为了确保每次都加载1/5的内存,假设我们滑到了第二块区域,依然也是用我们加载第一块区域时的内存,不然的话就相当于我们把5份的内存都加载进去了,可能会造成OOM。
大图加载基础api解析
//设置一个矩形区域(可以理解为矩形区域框定)
Rect mRect = new Rect();
//用于内存复用(Google提供的对内存复用设置一些参数,比如设置编码格式)
BitmapFactory.Options mOptions = new BitmapFactory.Options();
//手势支持
GestureDetector mGestureDetector = new GestureDetector(context, this);
//滚动类
Scroller mScroller = new Scroller(context);
//触摸时触发事件,比如触碰就停止屏幕滚动
setOnTouchListener(this);
我们要将绿色大小的原图转换成手机屏幕大小的蓝图就需要对图片进行缩放,就需要获取图片大小等相关信息。但问题有来了,我们在获取图片宽高信息的时候不能把整个图片加载进来,不然我们内存复用就没意义,这个时候就用到了mOptions。
//inJustDecodeBounds方法,只加载边缘区域来获取图片宽高
mOptions.inJustDecodeBounds=true;
//将is传进去解码就能获取到图片的宽和高
BitmapFactory.decodeStream(is,null,mOptions);
//拿到宽和高
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//开启内存复用
mOptions.inMutable=true;
//设置图片格式:rgb565
mOptions.inPreferredConfig= Bitmap.Config.RGB_565;
//用完需要关闭
mOptions.inJustDecodeBounds=false;
通过这种方式就能获取到图片的宽和高,并且没有将整张图片加载进内存
图片编码格式与占用内存之间关系
比如Glide使用的是RGB_565,Picasso使用的是ARGB_8888
当我们对一张图片进行无限的放大,你会发现它是由n个像素点组成,每个像素点都有自己的颜色,比如下面的图片就是有黑色,黄色和浅黄色
而当缩回去的时候就会发现是一张正常的图片,可以发现图片由像素点组成
像素点由RGB组成,三元色(红绿蓝)
而ARGB_8888表示图片中的像素有A,R,G,B四种颜色通道,每个通道占用内存为8位,共32位,相当于4个字节,也就是说每个像素点占用4个字节。
RGB_565相比于ARGB_8888少了A透明通道,表示的是R通道占5位,G通道占6位,B通道占5位,共16位,相当于2个字节,也就是说每个像素点占用2个字节。这样的话内存就相比于上面减少可一半。
大图加载之图片初始化展示实现
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BigView bigView= findViewById(R.id.bigView);
InputStream is=null;
try {
is= getResources().getAssets().open("test.jpg");
bigView.setImage(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class BigView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {
private Rect mRect;
private BitmapFactory.Options mOptions;
private GestureDetector mGestureDetector;
private Scroller mScroller;
private int mImageWidth;
private int mImageHeight;
private BitmapRegionDecoder mDecoder;
private int mViewWidth;
private int mViewHeight;
private Bitmap mBitmap;
private float mScaleX;
private float mScaleY;
public BigView(Context context) {
this(context,null);
}
public BigView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//第一步 设置BigView需要的成员变量
//设置一个矩形区域(矩形区域框定)
mRect = new Rect();
//用于内存复用(设置编码格式)
mOptions = new BitmapFactory.Options();
//手势支持
mGestureDetector = new GestureDetector(context, this);
//滚动类
mScroller = new Scroller(context);
//触摸时触发事件
setOnTouchListener(this);
}
//第二步设置图片
public void setImage(InputStream is){
//获取图片的宽和高
//此时不能将整张图片加载进来,这样内存复用无意义,需要使用inJustDecodeBounds方法,只加载部分区域来获取图片宽高
mOptions.inJustDecodeBounds=true;
//将is传进去解码就能获取到图片的宽和高
BitmapFactory.decodeStream(is,null,mOptions);
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//开启内存复用
mOptions.inMutable=true;
//设置图片格式:rgb565
mOptions.inPreferredConfig= Bitmap.Config.RGB_565;
//用完需要关闭
mOptions.inJustDecodeBounds=false;
//区域解码器
try {
mDecoder = BitmapRegionDecoder.newInstance(is, false);
} catch (IOException e) {
e.printStackTrace();
}
//去调用onMeasure方法
requestLayout();
}
//第三步 加载图片
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
//绑定图片加载区域
//上边界为0
mRect.top=0;
//左边界为0
mRect.left=0;
//右边界为图片的宽度
mRect.right=mImageWidth;
//下边界为view的高度,在这里相当于手机的高度
mRect.bottom=mViewHeight;
}
//第四步 画图
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mDecoder==null){
return;
}
//内存复用
//复用inBitmap这块的内存(每次滚动重新绘制都会复用这块内存,达到内存复用)
mOptions.inBitmap=mBitmap;
mBitmap=mDecoder.decodeRegion(mRect,mOptions);
//计算缩放因子
mScaleX = mViewWidth / (float) mImageWidth;
mScaleY = mViewHeight / (float) mImageHeight;
//得到矩阵缩放
Matrix matrix = new Matrix();
matrix.setScale(mScaleX, mScaleX);//如果matrix.setScale(mScaleX, mScaleY)则图片会在充满在当前的view的x和y轴
canvas.drawBitmap(mBitmap,matrix,null);
}
//第五步 处理点击事件
@Override
public boolean onTouch(View v, MotionEvent event) {
//将Touch事件传递给手势
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
}
大图加载之图片滚动功能实现
//第五步 处理点击事件
@Override
public boolean onTouch(View v, MotionEvent event) {
//将Touch事件传递给手势
return mGestureDetector.onTouchEvent(event);
}
//第六步 处理手势按下事件
@Override
public boolean onDown(MotionEvent e) {
//如果滑动没有停止就 强制停止
if(!mScroller.isFinished()){
mScroller.forceFinished(true);
}
//将事件进行传递,接收后续事件
//因为在GestureDetector中,onDown方法是用于监听手指按下事件的,如果不返回true消费该事件,
// GestureDetector就不会将后续的事件传递给其他的方法进行处理,
// 包括滑动事件。因此,如果要实现按下手指后进行滑动图片的效果,需要在onDown方法中返回true进行消费。
return true;
}
//第七步 处理滑动事件(手势)指手势的拖动
//e1 开始事件
//e2 即时事件也就是滑动时
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//上下滑动时,直接改变Rect的显示区域
mRect.offset(0,(int) distanceY);//上下滑动只需要改变Y轴
//判断到顶和到底的情况
if(mRect.bottom>mImageHeight){//滑到最底
mRect.bottom=mImageHeight;
mRect.top=mImageHeight-mViewHeight;
}
if(mRect.top<0){//滑到最顶
mRect.top=0;
mRect.bottom=mViewHeight;
}
invalidate();
return true;
}
大图加载之图片惯性滚动功能实现
//第八步 处理惯性问题(手势)指手势的滑动
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//velocityY表示Y轴的惯性值,startX和startY为滑动的开始位置,minY和maxY为滑动距离的最小值和最大值
mScroller.fling(0,mRect.top,0,(int) -velocityY,0,0,0,mImageHeight-mViewHeight);
return false;
}
//该方法可以获取当前的滚动值
@Override
public void computeScroll() {
super.computeScroll();
//如果没有滚动,直接返回即可
if(mScroller.isFinished()){
return;
}
//如果已经滚动到新位置返回true
if(mScroller.computeScrollOffset()){
mRect.top=mScroller.getCurrY();
mRect.bottom=mRect.top+mViewHeight;//底部边框等于更新的top位置加上
}
invalidate();
}
项目地址
github点击查看