Android加载高清大图

一、概述

        在android开发的过程中,有时候会遇到这样的需求,需要显示很大的图片,并且还不允许压缩。比如显示:世界地图、微博长图等,那么如何完成这个需求呢?首先我们分析一下,图片非常大,考虑到内存的情况,我们不能一次将整个图片加载到内存中,因为这样会OOM,然后图片的宽或者高超出了手机屏幕的尺寸,要想显示整张没有压缩过的图片,我们只能每次加载图片的局部,然后监听控件的滑动事件,得到滑动的方向和距离,来重新加载图片的局部,这样我们就可以利用拖动的方式显示整张图片了。


二、BitmapRegionDecoder

       android加载图片的局部需要用到BitmapRegionDecoder类,BitmapRegionDecoder类主要用来加载图片的某一块矩形区域,如果你想显示图片的某一块矩形区域,那么这个类非常合适,下面来看一下它的用法:

BitmapRegionDecoder提供了一系列的newInstance方法来构造实例,支持传入文件路径,文件描述符,文件的数据流等,如下:

    public static BitmapRegionDecoder newInstance(InputStream is, boolean isShareable)
    public static BitmapRegionDecoder newInstance(String pathName, boolean isShareable)
    public static BitmapRegionDecoder newInstance(FileDescriptor fd, boolean isShareable)
    public static BitmapRegionDecoder newInstance(byte[] data, int offset, int length, boolean isShareable)
利用图片构造BitmapRegionDecoder对象后,我们可以调用它的decodeRegion方法来加载图片中的指定区域,如下:

    Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);
第一个参数指定加载区域,第二个参数是BitmapFactory.options,可以用来指定图片的解码格式,取样率等,可以参考: https://www.jianshu.com/p/0fbcadfd4213?winzoom=1


三、开发代码

       了解了以上内容后,我们就可以开发加载高清大图的控件了,下面是自定义的代码,注释很详细:

package com.liunian.androidbasic.addbigimage.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.io.IOException;
import java.io.InputStream;

/**
 * Created by dell on 2018/4/12.
 */

public class BigImageView extends View {
    private Context mContext;
    private BitmapRegionDecoder mBitmapRegionDecoder;
    private Rect mRect = new Rect();
    private int mImageWidth = 0;
    private int mImageHeight = 0;
    private BitmapFactory.Options mOptions;

    public BigImageView(Context context) {
        this(context, null);
    }

    public BigImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BigImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    private void init() {
        // 指定图片的解码格式为RGB_565
        mOptions = new BitmapFactory.Options();
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    // 传入需要加载的图片的inputStream
    public void setInputStream(InputStream inputStream) {
        try {
            // 根据图片对应的BitmapRegionDecoder对象
            mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
            // 获得图片的宽高
            BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
            tmpOptions.inJustDecodeBounds = true; // 将inJuestDecodeBounds设为true,这样BitmapFactory只会解析图片的原始宽/高信息,并不会真正的去加载图片
            BitmapFactory.decodeStream(inputStream, null, tmpOptions); // 解析图片获得图片的宽高
            mImageWidth = tmpOptions.outWidth; // 保存图片的宽
            mImageHeight = tmpOptions.outHeight; // 保存图片的搞

            requestLayout(); // 这里调用requestLayout方法,请求重新布局,触发调用控件的onMeasure,初始化加载区域
            invalidate(); // 调用invalidate方法,请求重绘
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 保存上一次事件的发生位置
    float lastEventX = 0;
    float lastEventY = 0;
    // 保存当前事件的发生位置
    float eventX = 0;
    float eventY = 0;

    // 重载控件的onTouchEvent方法,监控控件的事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里只处理了控件的Move事件,如果有更复杂需求,比如说根据手势缩放图片,可以将事件交由给GestureDetector处理
        if (event != null) {
            // 得到当前事件的发生位置
            eventX = event.getX();
            eventY = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE: // move事件
                    // 根据当前事件的发生位置和上一次事件的发生位置计算出用户的滑动距离,并调整图片的加载区域
                    move((int)(lastEventX - eventX), (int)(lastEventY - eventY));
                    break;
            }
            // 保存上一次事件的发生位置
            lastEventX = event.getX();
            lastEventY = event.getY();
        }
        return true;
    }

    // 根据滑动距离调整图片的加载区域
    private void move(int moveX, int moveY) {
        // 只有当图片的高大于控件的高,控件纵向显示不下图片时,才需要调整加载区域的上下位置
        if (mImageHeight > getHeight()) {
            mRect.top = mRect.top + moveY;
            mRect.bottom = mRect.top + getHeight();
        }
        // 只有当图片的宽大于控件的宽,控件横向显示不下图片时,才需要调整加载区域的左右位置
        if (mImageWidth > getWidth()) {
            mRect.left = mRect.left + moveX;
            mRect.right = mRect.left + getWidth();
        }

        invalidate();
    }

    // 重写onDraw方法,绘制图片的局部
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mBitmapRegionDecoder != null) {
            // 检查绘制区域的宽高,以免绘制到图片以为的区域
            checkHeight();
            checkWidth();
            // 加载图片的指定区域
            Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);
            // 绘制图片的局部到控件上
            if (bitmap != null) {
                canvas.drawBitmap(bitmap, 0, 0, null);
            }
        }
    }

    /**
     * 检查加载区域是否超出图片范围
     */
    private void checkWidth() {
        // 只有当图片的宽大于控件的宽,控件横向显示不下图片时,才需要调整加载区域的左右位置
        if (mImageWidth > getWidth()) {
            if (mRect.right > mImageWidth) {
                mRect.right = mImageWidth;
                mRect.left = mRect.right - getWidth();
            }

            if (mRect.left < 0) {
                mRect.left = 0;
                mRect.right = getWidth();
            }
        } else {
            mRect.left = (mImageWidth - getWidth()) / 2;
            mRect.right = mRect.left + getWidth();
        }
    }

    /**
     * 检查加载区域是否超出图片范围
     */
    private void checkHeight() {
        // 只有当图片的高大于控件的高,控件纵向显示不下图片时,才需要调整加载区域的上下位置
        if (mImageHeight > getHeight()) {
            if (mRect.bottom > mImageHeight) {
                mRect.bottom = mImageHeight;
                mRect.top = mRect.bottom - getHeight();
            }
            if (mRect.top < 0) {
                mRect.top = 0;
                mRect.bottom = getHeight();
            }
        } else {
            mRect.top = (mImageHeight - getHeight()) / 2;
            mRect.bottom = mRect.top + getHeight();
        }
    }

    /**
     * 重写测量控件大小的方法,初始化图片的加载区域
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 得到控件的宽高
        int width = getMeasuredWidth();
        int height = getHeight();

        // 初始化图片的加载区域为图片的中心,可以自行根据需求调整
        mRect.left = (mImageWidth - width) / 2;
        mRect.right = mRect.left + width;
        mRect.top = (mImageHeight - height) / 2;
        mRect.bottom = mRect.top + height;
    }
}

使用方法

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.liunian.androidbasic.MainActivity">
    <com.liunian.androidbasic.addbigimage.view.BigImageView
        android:id="@+id/big_image_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>
        mBigImageView = (BigImageView) findViewById(R.id.big_image_view);
        try {
            mBigImageView.setInputStream(getAssets().open("ditu.jpg"));
        } catch (IOException e) {
            e.printStackTrace();
        }

使用效果如下:

不知道csdn怎样上传视频,就上传了一张图片,滑动控件可以加载地图的其他区域。


四、总结

1、加载高清大图需要用到BitmapRegionDecoder类,这个类可以加载图片的指定区域;

2、由于图片超出了控件的大小,我们需要监听控件的滑动事件,实现可以通过拖动的方式显示整张图片;

3、加载图片的指定区域,获得对应的Bitmap,然后通过重写onDraw方法将Bitmap绘制到控件上。


附上源码:https://github.com/2449983723/AndroidComponents







猜你喜欢

转载自blog.csdn.net/xiao_nian/article/details/79923549