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();
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读