高清巨圖

概述

對于加載圖片愉豺,大家都不陌生拧烦,一般為了盡可能避免OOM都會按照如下做法:

  • 對于圖片顯示:根據(jù)需要顯示圖片控件的大小對圖片進行壓縮顯示锣披。
  • 如果圖片數(shù)量非常多:則會使用LruCache等緩存機制精续,將所有圖片占據(jù)的內(nèi)容維持在一個范圍內(nèi)。

其實對于圖片加載還有種情況婚脱,就是單個圖片非常巨大今魔,并且還不允許壓縮勺像。比如顯示:世界地圖、清明上河圖错森、微博長圖等吟宦。

那么對于這種需求,該如何做呢涩维?

首先不壓縮殃姓,按照原圖尺寸加載,那么屏幕肯定是不夠大的激挪,并且考慮到內(nèi)存的情況辰狡,不可能一次性整圖加載到內(nèi)存中锋叨,所以肯定是局部加載垄分,那么就需要用到一個類:

  • BitmapRegionDecoder

其次,既然屏幕顯示不完娃磺,那么最起碼要添加一個上下左右拖動的手勢薄湿,讓用戶可以拖動查看。

那么綜上偷卧,本篇博文的目的就是去自定義一個顯示巨圖的View豺瘤,支持用戶去拖動查看,大概的效果圖如下:

好吧听诸,這清明上河圖太長了坐求,想要觀看全圖,文末下載晌梨,圖片在assets目錄桥嗤。當然如果你的圖,高度也很大仔蝌,肯定也是可以上下拖動的泛领。

清明上河圖

好吧,這清明上河圖太長了敛惊,想要觀看全圖渊鞋,文末下載,圖片在assets目錄瞧挤。當然如果你的圖锡宋,高度也很大,肯定也是可以上下拖動的特恬。

BitmapRegionDecoder

BitmapRegionDecoder主要用于顯示圖片的某一塊矩形區(qū)域执俩,如果你需要顯示某個圖片的指定區(qū)域,那么這個類非常合適鸵鸥。

對于該類的用法奠滑,非常簡單丹皱,既然是顯示圖片的某一塊區(qū)域,那么至少只需要一個方法去設置圖片宋税;一個方法傳入顯示的區(qū)域即可摊崭;詳見:

  • BitmapRegionDecoder提供了一系列的newInstance方法來構(gòu)造對象,支持傳入文件路徑杰赛,文件描述符呢簸,文件的inputstrem等。

    例如:

BitmapRegionDecoder bitmapRegionDecoder =
BitmapRegionDecoder.newInstance(inputStream, false);

  • 上述解決了傳入我們需要處理的圖片乏屯,那么接下來就是顯示指定的區(qū)域根时。

bitmapRegionDecoder.decodeRegion(rect, options);

參數(shù)一很明顯是一個rect,參數(shù)二是BitmapFactory.Options辰晕,你可以控制圖片的inSampleSize蛤迎,inPreferredConfig等。

那么下面看一個超級簡單的例子:

package com.zhy.blogcodes.largeImage;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;

import com.zhy.blogcodes.R;

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

public class LargeImageViewActivity extends AppCompatActivity
{
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_large_image_view);

        mImageView = (ImageView) findViewById(R.id.id_imageview);
        try
        {
            InputStream inputStream = getAssets().open("tangyan.jpg");

            //獲得圖片的寬含友、高
            BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
            tmpOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, tmpOptions);
            int width = tmpOptions.outWidth;
            int height = tmpOptions.outHeight;

            //設置顯示圖片的中心區(qū)域
            BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
            mImageView.setImageBitmap(bitmap);
        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

上述代碼替裆,就是使用BitmapRegionDecoder去加載assets中的圖片,調(diào)用bitmapRegionDecoder.decodeRegion解析圖片的中間矩形區(qū)域窘问,返回bitmap辆童,最終顯示在ImageView上。

效果圖:

妹子

上面的小圖顯示的即為下面的大圖的中間區(qū)域惠赫。

ok把鉴,那么目前我們已經(jīng)了解了BitmapRegionDecoder的基本用戶,那么往外擴散儿咱,我們需要自定義一個控件去顯示巨圖就很簡單了庭砍,首先Rect的范圍就是我們View的大小,然后根據(jù)用戶的移動手勢概疆,不斷去更新我們的Rect的參數(shù)即可逗威。

自定義顯示大圖控件

根據(jù)上面的分析呢,我們這個自定義控件思路就非常清晰了:

  • 提供一個設置圖片的入口

  • 重寫onTouchEvent岔冀,在里面根據(jù)用戶移動的手勢凯旭,去更新顯示區(qū)域的參數(shù)

  • 每次更新區(qū)域參數(shù)后,調(diào)用invalidate使套,onDraw里面去regionDecoder.decodeRegion拿到bitmap罐呼,去draw

理清了,發(fā)現(xiàn)so easy侦高,下面上代碼:

package com.zhy.blogcodes.largeImage.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.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

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

/**
 * Created by zhy on 15/5/16.
 */
public class LargeImageView extends View {
    private BitmapRegionDecoder mDecoder;
    /**
     * 圖片的寬度和高度
     */
    private int mImageWidth, mImageHeight;
    /**
     * 繪制的區(qū)域
     */
    private volatile Rect mRect = new Rect();

    private MoveGestureDetector mDetector;


    private static final BitmapFactory.Options options = new BitmapFactory.Options();

    static {
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    public void setInputStream(InputStream is) {
        try {
            mDecoder = BitmapRegionDecoder.newInstance(is, false);
            BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
            // Grab the bounds for the scene dimensions
            tmpOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, tmpOptions);
            mImageWidth = tmpOptions.outWidth;
            mImageHeight = tmpOptions.outHeight;

            requestLayout();
            invalidate();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            try {
                if (is != null) is.close();
            } catch (Exception e) {
            }
        }
    }


    public void init() {
        mDetector = new MoveGestureDetector(getContext(), new MoveGestureDetector.SimpleMoveGestureDetector() {
            @Override
            public boolean onMove(MoveGestureDetector detector) {
                int moveX = (int) detector.getMoveX();
                int moveY = (int) detector.getMoveY();

                if (mImageWidth > getWidth()) {
                    mRect.offset(-moveX, 0);
                    checkWidth();
                    invalidate();
                }
                if (mImageHeight > getHeight()) {
                    mRect.offset(0, -moveY);
                    checkHeight();
                    invalidate();
                }

                return true;
            }
        });
    }


    private void checkWidth() {


        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.right > imageWidth) {
            rect.right = imageWidth;
            rect.left = imageWidth - getWidth();
        }

        if (rect.left < 0) {
            rect.left = 0;
            rect.right = getWidth();
        }
    }


    private void checkHeight() {

        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.bottom > imageHeight) {
            rect.bottom = imageHeight;
            rect.top = imageHeight - getHeight();
        }

        if (rect.top < 0) {
            rect.top = 0;
            rect.bottom = getHeight();
        }
    }


    public LargeImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDetector.onToucEvent(event);
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Bitmap bm = mDecoder.decodeRegion(mRect, options);
        canvas.drawBitmap(bm, 0, 0, null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        //默認直接顯示圖片的中心區(qū)域嫉柴,可以自己去調(diào)節(jié)
        mRect.left = imageWidth / 2 - width / 2;
        mRect.top = imageHeight / 2 - height / 2;
        mRect.right = mRect.left + width;
        mRect.bottom = mRect.top + height;

    }

}

根據(jù)上述源碼:

  • setInputStream里面去獲得圖片的真實的寬度和高度,以及初始化我們的mDecoder

  • onMeasure里面為我們的顯示區(qū)域的rect賦值奉呛,大小為view的尺寸

  • onTouchEvent里面我們監(jiān)聽move的手勢计螺,在監(jiān)聽的回調(diào)里面去改變rect的參數(shù)夯尽,以及做邊界檢查,最后invalidate

  • 在onDraw里面就是根據(jù)rect拿到bitmap登馒,然后draw了

ok匙握,上面并不復雜,不過大家有沒有注意到陈轿,這個監(jiān)聽用戶move手勢的代碼寫的有點奇怪圈纺,恩,這里模仿了系統(tǒng)的ScaleGestureDetector麦射,編寫了MoveGestureDetector蛾娶,代碼如下:

  • MoveGestureDetector
package com.zhy.blogcodes.largeImage.view;

import android.content.Context;
import android.graphics.PointF;
import android.view.MotionEvent;
public class MoveGestureDetector extends BaseGestureDetector {

    private PointF mCurrentPointer;
    private PointF mPrePointer;
    //僅僅為了減少創(chuàng)建內(nèi)存
    private PointF mDeltaPointer = new PointF();

    //用于記錄最終結(jié)果,并返回
    private PointF mExtenalPointer = new PointF();

    private OnMoveGestureListener mListenter;


    public MoveGestureDetector(Context context, OnMoveGestureListener listener) {
        super(context);
        mListenter = listener;
    }

    @Override
    protected void handleInProgressEvent(MotionEvent event) {
        int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
        switch (actionCode) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mListenter.onMoveEnd(this);
                resetState();
                break;
            case MotionEvent.ACTION_MOVE:
                updateStateByEvent(event);
                boolean update = mListenter.onMove(this);
                if (update) {
                    mPreMotionEvent.recycle();
                    mPreMotionEvent = MotionEvent.obtain(event);
                }
                break;

        }
    }

    @Override
    protected void handleStartProgressEvent(MotionEvent event) {
        int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
        switch (actionCode) {
            case MotionEvent.ACTION_DOWN:
                resetState();//防止沒有接收到CANCEL or UP ,保險起見
                mPreMotionEvent = MotionEvent.obtain(event);
                updateStateByEvent(event);
                break;
            case MotionEvent.ACTION_MOVE:
                mGestureInProgress = mListenter.onMoveBegin(this);
                break;
        }

    }

    protected void updateStateByEvent(MotionEvent event) {
        final MotionEvent prev = mPreMotionEvent;

        mPrePointer = caculateFocalPointer(prev);
        mCurrentPointer = caculateFocalPointer(event);

        //Log.e("TAG", mPrePointer.toString() + " ,  " + mCurrentPointer);

        boolean mSkipThisMoveEvent = prev.getPointerCount() != event.getPointerCount();

        //Log.e("TAG", "mSkipThisMoveEvent = " + mSkipThisMoveEvent);
        mExtenalPointer.x = mSkipThisMoveEvent ? 0 : mCurrentPointer.x - mPrePointer.x;
        mExtenalPointer.y = mSkipThisMoveEvent ? 0 : mCurrentPointer.y - mPrePointer.y;

    }

    /**
     * 根據(jù)event計算多指中心點
     *
     * @param event
     * @return
     */
    private PointF caculateFocalPointer(MotionEvent event) {
        final int count = event.getPointerCount();
        float x = 0, y = 0;
        for (int i = 0; i < count; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }

        x /= count;
        y /= count;

        return new PointF(x, y);
    }


    public float getMoveX() {
        return mExtenalPointer.x;

    }

    public float getMoveY() {
        return mExtenalPointer.y;
    }


    public interface OnMoveGestureListener {
        public boolean onMoveBegin(MoveGestureDetector detector);

        public boolean onMove(MoveGestureDetector detector);

        public void onMoveEnd(MoveGestureDetector detector);
    }

    public static class SimpleMoveGestureDetector implements OnMoveGestureListener {

        @Override
        public boolean onMoveBegin(MoveGestureDetector detector) {
            return true;
        }

        @Override
        public boolean onMove(MoveGestureDetector detector) {
            return false;
        }

        @Override
        public void onMoveEnd(MoveGestureDetector detector) {
        }
    }

}
  • BaseGestureDetector
package com.zhy.blogcodes.largeImage.view;

import android.content.Context;
import android.view.MotionEvent;

public abstract class BaseGestureDetector {

    protected boolean mGestureInProgress;

    protected MotionEvent mPreMotionEvent;
    protected MotionEvent mCurrentMotionEvent;

    protected Context mContext;

    public BaseGestureDetector(Context context) {
        mContext = context;
    }


    public boolean onToucEvent(MotionEvent event) {

        if (!mGestureInProgress) {
            handleStartProgressEvent(event);
        } else {
            handleInProgressEvent(event);
        }

        return true;

    }

    protected abstract void handleInProgressEvent(MotionEvent event);

    protected abstract void handleStartProgressEvent(MotionEvent event);

    protected abstract void updateStateByEvent(MotionEvent event);

    protected void resetState() {
        if (mPreMotionEvent != null) {
            mPreMotionEvent.recycle();
            mPreMotionEvent = null;
        }
        if (mCurrentMotionEvent != null) {
            mCurrentMotionEvent.recycle();
            mCurrentMotionEvent = null;
        }
        mGestureInProgress = false;
    }


}

你可能會說潜秋,一個move手勢搞這么多代碼蛔琅,太麻煩了。的確是的半等,move手勢的檢測非常簡單揍愁,那么之所以這么寫呢,主要是為了可以復用杀饵,比如現(xiàn)在有一堆的XXXGestureDetector,當我們需要監(jiān)聽什么手勢谬擦,就直接拿個detector來檢測多方便切距。我相信大家肯定也郁悶過Google,為什么只有ScaleGestureDetector而沒有RotateGestureDetector呢惨远。

根據(jù)上述谜悟,大家應該理解了為什么要這么做,當時不強制北秽,每個人都有個性葡幸。

對應的我們的xml:

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent">


    <com.zhy.blogcodes.largeImage.view.LargeImageView
        android:id="@+id/id_largetImageview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

然后在Activity里面去設置圖片:

package com.zhy.blogcodes.largeImage;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import com.zhy.blogcodes.R;
import com.zhy.blogcodes.largeImage.view.LargeImageView;

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

public class LargeImageViewActivity extends AppCompatActivity {
    private LargeImageView mLargeImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_large_image_view);

        mLargeImageView = (LargeImageView) findViewById(R.id.id_largetImageview);
        try {
            InputStream inputStream = getAssets().open("world.jpg");
            mLargeImageView.setInputStream(inputStream);

        } catch (IOException e) {
            e.printStackTrace();
        }


    }

}

效果圖:

世界地圖

ok,那么到此贺氓,顯示巨圖的方案以及詳細的代碼就描述完成了蔚叨,總體還是非常簡單的。
但是辙培,在實際的項目中蔑水,可能會有更多的需求,比如增加放大扬蕊、縮胁蟊稹;增加快滑手勢等等尾抑,那么大家可以去參考這個庫:https://github.com/johnnylambada/WorldMap歇父,該庫基本實現(xiàn)了絕大多數(shù)的需求蒂培,大家根據(jù)本文這個思路再去看這個庫,也會簡單很多榜苫,定制起來也容易毁渗。我這個地圖的圖就是該庫里面提供的。

哈单刁,掌握了這個灸异,以后面試過程中也可以悄悄的裝一把了,當你優(yōu)雅的答完android加載圖片的方案以后羔飞,然后接一句肺樟,其實還有一種情況,就是高清顯示巨圖逻淌,那么我們應該…相信面試官對你的印象會好很多~ have a nice day ~

源碼點擊下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末么伯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子卡儒,更是在濱河造成了極大的恐慌田柔,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骨望,死亡現(xiàn)場離奇詭異硬爆,居然都是意外死亡,警方通過查閱死者的電腦和手機擎鸠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門缀磕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人劣光,你說我怎么就攤上這事袜蚕。” “怎么了绢涡?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵牲剃,是天一觀的道長。 經(jīng)常有香客問我雄可,道長凿傅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任滞项,我火速辦了婚禮狭归,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘文判。我一直安慰自己过椎,他們只是感情好,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布戏仓。 她就那樣靜靜地躺著疚宇,像睡著了一般亡鼠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上敷待,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天间涵,我揣著相機與錄音,去河邊找鬼榜揖。 笑死勾哩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的举哟。 我是一名探鬼主播思劳,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妨猩!你這毒婦竟也來了潜叛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤壶硅,失蹤者是張志新(化名)和其女友劉穎威兜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庐椒,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡椒舵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扼睬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逮栅。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窗宇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情特纤,我是刑警寧澤军俊,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站捧存,受9級特大地震影響粪躬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昔穴,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一镰官、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吗货,春花似錦泳唠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拓哺。三九已至,卻和暖如春脖母,著一層夾襖步出監(jiān)牢的瞬間士鸥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工谆级, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烤礁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓肥照,卻偏偏與公主長得像脚仔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子建峭,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1玻侥、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,981評論 3 119
  • 興慶公園南門,對面就是交大的北門亿蒸,中間隔了一條馬路凑兰。。因為與興慶宮園相鄰边锁,有時到公園游玩姑食,順便就到校園里走走,拍拍...
    霜葉shuangye閱讀 449評論 0 0
  • 接下里做打印功能 需求:打印的時候不要側(cè)邊欄寫個CSS茅坛,然后在引用里加媒體查詢 接下來做換膚功能解決:直接切換對應...
    tsl1127閱讀 322評論 0 0
  • 孩子們一上學音半,就會出現(xiàn)各種各樣的問題,那么贡蓖,父母要做好面臨孩子的一切問題的準備曹鸠。 其中幾個典型的,又多方都會遇到的...
    周z周老師閱讀 377評論 2 4
  • 一個成功節(jié)目的從前 昨天和今天,看了《奇葩說》第一季的前四期海選晾蜘,心里突然有一種很奇妙的感覺邻眷。原來他們的最開始是這...
    默默喜歡你閱讀 731評論 9 10