Android 长图加载
2020-03-17 本文已影响0人
RookieRun
1.思考
我们拿到的长图,不可能一下就设置到对应的ImageView中,原因有二:
1.图片显示不全
2.一次性加载长图会占用较多的内存,App中多处这样操作的话,有OOM的风险
2.思路
自定义View,每次只绘制当前可见范围内的区域或者只绘制控件范围内的区域,然后监听控件的触摸事件,在控件的滑动过程中,更新绘制区域
3.代码实现
package com.hua.stick_test.long_img;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Scroller;
import java.io.IOException;
import java.io.InputStream;
/**
* 加载长图的ImageView
* 1.支持滑动的长图加载
* 2.支持fling
* 3.#支持单指拖动,双击放大,手势放大,拖动旋转等
* 思路
* 1.只加载与屏幕内的图像信息并显示,所以,当图片的宽高均超过屏幕的宽高的时候,就会涉及到图片的缩放
* 2.将当前需要显示的图片的位置信息存储与Rect中,然后在onDraw方法中,将图片信息画至对应的区域
* 3.监听手势及触摸事件处理滑动及fling事件
*/
public class LongImageView extends View implements View.OnTouchListener, GestureDetector.OnGestureListener {
private final Context mContext;
private int mImageWidth;
private int mImageHeight;
private Scroller mScroller;
private GestureDetector mGestureDetector;
private BitmapRegionDecoder mBitDecoder;
private BitmapFactory.Options mOptions;
private Rect mRect;
private int mViewWidth;
private int mViewHeight;
private float mScale;
private Bitmap mBitmap;
public LongImageView(Context context) {
this(context, null);
}
public LongImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
/**
* 初始化
*/
private void init() {
mScroller = new Scroller(mContext);
//手势监听
mGestureDetector = new GestureDetector(mContext, this);
mOptions = new BitmapFactory.Options();
//保存需要绘制的区域
mRect = new Rect();
setOnTouchListener(this);
}
/**
* 第二步,先打到与图片处理相关的配置信息
*/
public void setImg(InputStream inputStream) {
if (inputStream == null) {
return;
}
//1.先拿到图片对应的宽高
//只获取图片的尺寸信息
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, mOptions);
//拿到图片的宽高
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//开启复用
mOptions.inMutable = true;
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
//区域解码器
try {
mBitDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
} catch (IOException e) {
Log.e("test", "decoder-exception-->" + e.getLocalizedMessage());
}
mOptions.inJustDecodeBounds = false;
}
/**
* 第三步,拿到当前view的尺寸,并计算缩放系数
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
mRect.top = mRect.left = 0;
mRect.right = mViewWidth;
mScale = mImageWidth / (float) mViewWidth;
mRect.bottom = (int) (mImageHeight / mScale);
//注意缩放的特殊情况,细节1
if (mScale <= 1) {
mRect.bottom = mViewHeight;
}
}
/**
* 第四步,在内容区域画出具体的内容
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitDecoder == null) {
throw new RuntimeException("请设置图片");
}
mOptions.inBitmap = mBitmap;
mBitmap = mBitDecoder.decodeRegion(mRect, mOptions);
Matrix matrix = new Matrix();
matrix.setScale(mScale, mScale);
canvas.drawBitmap(mBitmap, matrix, null);
}
/**
* 第五步,处理触摸事件,统一交给GestureDector处理
*
* @return
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
//直接交给dector处理
return mGestureDetector.onTouchEvent(event);
}
/**
* 第六步,down事件需要返回true,代表消费此事件
*/
@Override
public boolean onDown(MotionEvent e) {
//触摸时,如果还在滚动,就强制的停止滚动
if (mScroller != null && !mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
/**
* 第七步,处理滚动,滚动时,需要动态更新mRect并重绘来保证图片对应的区域能绘制到view中
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mRect.offset(0, ((int) distanceY));
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
if (mScale == 1) {
mRect.bottom = mViewHeight;
}
mRect.top = mImageHeight - ((int) (mViewHeight / mScale));
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = ((int) (mViewHeight / mScale));
}
invalidate();
return false;
}
/**
* 第八步,处理惯性事件
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(0, mRect.top, ((int) velocityX), -((int) velocityY), 0, 0, 0, mImageHeight - ((int) (mViewHeight / mScale)));
return false;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller == null) {
return;
}
if (mScroller.computeScrollOffset()) {
mRect.top = mScroller.getCurrY();
mRect.bottom = mScroller.getCurrY() + ((int) (mViewHeight / mScale));
invalidate();
}
}
@Override
public void onShowPress(MotionEvent e) {
Log.e("test", "onShowPress");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.e("test", "onSingleTapUp");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.e("test", "onLongPress");
}
}
4.细节
1.mScale,缩放因子,需要注意,如果图片宽度小于等于view宽度,而长度较长时,需要注意mRect.bottom的初始值,因为mScale=1时,高度也不会缩放,所以高度会是图片高度,这时应该只显示图片高度就行了
2.scroll或者fling时,注意边界情况并且,更新mRect区域后,使用invalidate()通知控件重绘
5.待完善
该控件暂不支持双击放大与缩小,滑动查看其他区域
6.控件的使用
布局中引用,或java直接使用,然后调用setImage()方法传入对应图片的InputStream
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hua.stick_test.long_img.LongImageView
android:layout_width="match_parent"
android:id="@+id/long_img_iv"
android:layout_height="match_parent" />
</LinearLayout>
public class LongImgActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_long_img);
LongImageView longImageView = (LongImageView) findViewById(R.id.long_img_iv);
try {
longImageView.setImg(getAssets().open("long_img.jpeg"));
} catch (IOException e) {
e.printStackTrace();
}
}
}