Android實現(xiàn)圖片查看

一、效果圖

image

二嵌施、支持功能

  • 本地與網(wǎng)絡(luò)圖片
  • 可用于各大圖片加載框架(Fresco腿箩,Glide,Picasso)
  • 圖片縮放
  • 放大后的圖片慣性滑動
  • 下拉縮小退出

三雇逞、核心實現(xiàn)方法

3.1 縮放 Matrix.postScale(float sx, float sy, float px, float py)

參數(shù)解析:

  • sx: 目標(biāo)寬度 / 現(xiàn)有寬度
  • sy: 目標(biāo)高度 / 現(xiàn)有高度
  • (px,py): 縮放焦點坐標(biāo)

使用示例:

/**
 * 縮放手勢監(jiān)聽
 */
private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactor = detector.getScaleFactor();
        float wantScale = mScale * scaleFactor;
        if (wantScale >= MIN_SCALE) {
            mScale = wantScale;
            focusX = detector.getFocusX();
            focusY = detector.getFocusY();
            mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
            invalidate();
        }
        return true;
    }
};

3.2 移動 Matrix.postTranslate(float dx, float dy)

參數(shù)解析:

  • dx: 目標(biāo)位置X坐標(biāo) - 當(dāng)前位置X坐標(biāo)
  • sy: 目標(biāo)位置Y坐標(biāo) - 當(dāng)前位置Y坐標(biāo)

使用示例:

/**
 * 簡單手勢監(jiān)聽
 */
private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {

    ...
    
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (!isAlwaysSingleTouch) {
            return true;
        }
        mMatrix.postTranslate(-distanceX, -distanceY);
        invalidate();
        return false;
    }

    ...
};

3.3 將Matrix的操作關(guān)聯(lián)到ImageView上

View提供onDraw的方法荤懂,可以操作到Canvas,Canvas提供concat的方法來關(guān)聯(lián)Matrix塘砸。每次針對Matrix有操作之后調(diào)用invalidate()刷新一下onDraw()即可节仿。這就是個操作配置,而且是View早就提供好了的配置掉蔬。
調(diào)用示例:

@Override
protected void onDraw(Canvas canvas) {
    int saveCount = canvas.save();
    canvas.concat(mMatrix);
    super.onDraw(canvas);
    canvas.restoreToCount(saveCount);
}

3.4 慣性滑動 OverScroller.fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)

參數(shù)解析:

  • (startX, startY): 初始位置坐標(biāo)
  • (velocityX, velocityY): XY方向的初始速度
  • (minX, maxX, minY, maxY): 限定了移動后的位置邊界

使用示例:

/**
 * 慣性滑動工具類
 * 使用fling方法開始滑動
 * 使用stop方法停止滑動
 */
private class FlingUtil implements Runnable {
    private int mLastFlingX = 0;
    private int mLastFlingY = 0;
    private OverScroller mScroller;
    private boolean mEatRunOnAnimationRequest = false;
    private boolean mReSchedulePostAnimationCallback = false;

    /**
     * RecyclerView使用的慣性滑動插值器
     * f(x) = (x-1)^5 + 1
     */
    private final Interpolator sQuinticInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    public FlingUtil() {
        mScroller = new OverScroller(getContext(), sQuinticInterpolator);
    }

    @Override
    public void run() {
        disableRunOnAnimationRequests();
        final OverScroller scroller = mScroller;
        if (scroller.computeScrollOffset()) {
            final int y = scroller.getCurrY();
            int dy = y - mLastFlingY;
            final int x = scroller.getCurrX();
            int dx = x - mLastFlingX;
            mLastFlingY = y;
            mLastFlingX = x;
            constrainScrollBy(dx, dy);
            postOnAnimation();
        }
        enableRunOnAnimationRequests();
    }

    public void fling(int velocityX, int velocityY) {
        mLastFlingX = 0;
        mLastFlingY = 0;
        mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        postOnAnimation();
    }

    public void stop() {
        removeCallbacks(this);
        mScroller.abortAnimation();
    }

    private void disableRunOnAnimationRequests() {
        mReSchedulePostAnimationCallback = false;
        mEatRunOnAnimationRequest = true;
    }

    private void enableRunOnAnimationRequests() {
        mEatRunOnAnimationRequest = false;
        if (mReSchedulePostAnimationCallback) {
            postOnAnimation();
        }
    }

    void postOnAnimation() {
        if (mEatRunOnAnimationRequest) {
            mReSchedulePostAnimationCallback = true;
        } else {
            removeCallbacks(this);
            ViewCompat.postOnAnimation(ZoomImageView.this, this);
        }
    }
}

Scroller只提供在基于已有位置和已有速度下的位置計算廊宪,需要主動調(diào)用scroller.getCurrY()scroller.getCurrX()方法去獲取位置信息。
這里使用的是RecyclerView中的慣性滑動的實現(xiàn)方式女轿。

四箭启、三個必要的細(xì)節(jié)處理

在有了上面的4個方法,基本上一個可縮放的ImageView所需要的功能都可以實現(xiàn)了蛉迹。但是傅寡,一些細(xì)節(jié)方面的問題也不可忽視,比如說:

  • 移動不能超過圖片的邊緣
  • 在ImageView的ScaleType為FIT_CENTER時北救,獲取真實的圖片內(nèi)容的寬高荐操,畢竟需要縮放的是圖片的內(nèi)容
  • 圖片是否移動到最左側(cè)或最右側(cè),用于ViewPager中的滑動判斷

4.1 邊緣處理

在移動前珍策,校驗此次的移動是否會造成圖片內(nèi)容是否會移動超出邊界淀零。Canvas關(guān)聯(lián)的Matrix是針對整個ImageView的,我們需要知道ImageView中圖片部分在ImageView中的初始位置信息,如圖:

image

在得到圖片內(nèi)容在初始狀態(tài)下的展示區(qū)域Rect(a,b,c,d)后膛壹,使用Matrix提供的Matrix.mapRect(Rect)方法驾中,可以得到經(jīng)歷縮放后的展示區(qū)域。得到內(nèi)容縮放后的展示區(qū)域后模聋,與ImageView的展示區(qū)域Rect(0,0,W,H)作比較便可得出是否超出邊界肩民。

示例方法:

/**
 * 獲得縮放移動后的圖片內(nèi)容的位置區(qū)域
 *
 * @param matrix
 * @return RectF
 */
private RectF getScaledRect(Matrix matrix) {
    RectF rectF = new RectF(mImageRectF);
    //轉(zhuǎn)化為縮放后的相對位置
    matrix.mapRect(rectF);
    return rectF;
}

/**
 * 針對邊緣問題,約束移動
 *
 * @param dx
 * @param dy
 */
private void constrainScrollBy(float dx, float dy) {
    RectF rectF = getScaledRect(mMatrix);
    float scaleImageWidth = rectF.width();
    float scaleImageHeight = rectF.height();

    if (scaleImageWidth > mWidth) {
        //right
        if (rectF.right + dx < mWidth) {
            dx = -rectF.right + mWidth;
        }
        //left
        if (rectF.left + dx > 0) {
            dx = -rectF.left;
        }
    } else {
        //center
        dx = -rectF.left + ((float) mWidth - scaleImageWidth) / 2;
    }

    if (scaleImageHeight > mHeight) {
        //bottom
        if (rectF.bottom + dy < mHeight) {
            dy = -rectF.bottom + mHeight;
        }
        //top
        if (rectF.top + dy > 0) {
            dy = -rectF.top;
        }
    } else {
        //center
        dy = -rectF.top + ((float) mHeight - scaleImageHeight) / 2;
    }

    mMatrix.postTranslate(dx, dy);
    invalidate();
    checkBorder();
}

4.2 獲取ImageView中內(nèi)容的寬高

針對不同的網(wǎng)絡(luò)加載框架有不同的操作方式链方,這里一Fresco位示例:
PipelineDraweeControllerBuilder提供setControllerListener方法持痰,可以設(shè)置一個圖片加載的監(jiān)聽。
示例代碼:

private ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
    @Override
    public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable anim) {
        if (imageInfo == null) {
            return;
        }
        int preWidth = imageInfo.getWidth();
        int preHeight = imageInfo.getHeight();
        if (preWidth != mWidth || preHeight != mHeight) {
            //獲取到最新的圖片內(nèi)容的寬高
            mWidth = preWidth;
            mHeight = preHeight;
        }
    }

    @Override
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
        Log.d("zhufeng", "Intermediate image received");
    }

    @Override
    public void onFailure(String id, Throwable throwable) {
        throwable.printStackTrace();
    }
};

public void loadImage(int resizeX, int resizeY, Uri uri) {
    ImageRequest request = ImageRequestBuilder
            .newBuilderWithSource(uri)
            .setResizeOptions(new ResizeOptions(resizeX, resizeY))
            .build();
    PipelineDraweeController controller = (PipelineDraweeController) Fresco.newDraweeControllerBuilder().setControllerListener(controllerListener).setOldController(getController()).setImageRequest(request).build();
    setController(controller);
}

4.3 處理與ViewPager的滑動沖突

需要明確:

  • 左滑時祟蚀,當(dāng)圖片內(nèi)容到達(dá)右側(cè)邊界工窍,進行圖片切換的處理(事件交由ViewPager處理)
  • 右滑時割卖,當(dāng)圖片內(nèi)容到達(dá)左側(cè)邊界,進行圖片切換的處理(事件交由ViewPager處理)
  • 剩下的ImageView自己處理

ImageView中的處理:
在約束移動的時候標(biāo)記圖片是否已經(jīng)觸及左右邊界患雏。并提供方法:

/**
 * 用于ViewPager滑動攔截
 *
 * @param direction
 * @return
 */
public boolean canScroll(int direction) {
    return !((direction < 0 && isRightSide()) || (direction > 0 && isLeftSide()));
}

ViewPager中的處理:
在canScroll方法中進行狀態(tài)判斷鹏溯。重寫ViewPager:

/**
 * 相冊ViewPager
 *
 * @author zhufeng on 2017/10/22
 */
public class AlbumViewPager extends ViewPager {

    ...
    
    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ZoomImageView) {
            return ((ZoomImageView) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y);
        }
        return super.canScroll(v, checkV, dx, x, y);
    }
    
    ...
    
}

五、源碼下載

https://github.com/zhufeng1222/Gallery

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淹仑,一起剝皮案震驚了整個濱河市丙挽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匀借,老刑警劉巖颜阐,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吓肋,居然都是意外死亡凳怨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門是鬼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猿棉,“玉大人,你說我怎么就攤上這事屑咳∪蓿” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵兆龙,是天一觀的道長杖爽。 經(jīng)常有香客問我,道長紫皇,這世上最難降的妖魔是什么慰安? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮聪铺,結(jié)果婚禮上化焕,老公的妹妹穿的比我還像新娘。我一直安慰自己铃剔,他們只是感情好撒桨,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著键兜,像睡著了一般凤类。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上普气,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天谜疤,我揣著相機與錄音,去河邊找鬼。 笑死夷磕,一個胖子當(dāng)著我的面吹牛履肃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坐桩,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼尺棋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了撕攒?” 一聲冷哼從身側(cè)響起陡鹃,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烘浦,失蹤者是張志新(化名)和其女友劉穎抖坪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闷叉,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡擦俐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了握侧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚯瞧。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖品擎,靈堂內(nèi)的尸體忽然破棺而出埋合,到底是詐尸還是另有隱情,我是刑警寧澤萄传,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布甚颂,位于F島的核電站,受9級特大地震影響秀菱,放射性物質(zhì)發(fā)生泄漏振诬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一衍菱、第九天 我趴在偏房一處隱蔽的房頂上張望赶么。 院中可真熱鬧,春花似錦脊串、人聲如沸辫呻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽印屁。三九已至,卻和暖如春斩例,著一層夾襖步出監(jiān)牢的瞬間雄人,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留础钠,地道東北人恰力。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像旗吁,于是被迫代替她去往敵國和親踩萎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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

  • 效果圖: Github鏈接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL閱讀 2,218評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,185評論 25 707
  • 手勢圖片控件 PinchImageView 點擊圖片框架 photoView packagecom.example...
    Ztufu閱讀 728評論 0 1
  • CSDN博客 img cquwentao android matrix 最全方法詳解與進階(完整篇) 發(fā)表于201...
    北風(fēng)知我意閱讀 4,820評論 0 0
  • 又到了5月20日很钓,這樣的日子于我來說香府,早已不是大家理解或者期待中的樣子,14年的今天码倦,我和現(xiàn)在的老公結(jié)束了長達(dá)八年...
    新晴_love閱讀 194評論 0 0