要做的功能如上面demo所示,在圖片縮放時(shí)呵扛,增加一個(gè)展示當(dāng)前局部位置的導(dǎo)航圖每庆。要求是:
- 支持超大圖;
- 導(dǎo)航圖紅框定位當(dāng)前局部位置今穿;
- 導(dǎo)航圖支持滑動(dòng)缤灵,快速定位大圖位置;
- 支持圖釘顯示蓝晒,隨圖片放大更新透明度腮出。
簡(jiǎn)書(shū)記錄下開(kāi)發(fā)過(guò)程,demo可以在github找到
加載超大圖
測(cè)試用的是一張21m大的圖片(上傳代碼里換了張小的)芝薇,無(wú)論如何不能一次載入內(nèi)存顯示胚嘲。github里找到一個(gè)顯示超大圖的控件subsampling-scale-image-view
,原理是使用Android的BitmapRegionDecoder局部加載圖片洛二。
創(chuàng)建一個(gè)自定義LargeBitmapView馋劈,繼承SubsamplingScaleImageView。加載圖片使用setImage方法晾嘶,圖片來(lái)源可以多種妓雾,通過(guò)ImageSource獲取。
lav_bitmap.setImage(ImageSource.resource(R.mipmap.large_world_map));
監(jiān)聽(tīng)圖片加載過(guò)程使用OnImageEventListener垒迂,回調(diào)豐富械姻。
lav_bitmap.setOnImageEventListener(new SubsamplingScaleImageView.OnImageEventListener() {
@Override
public void onReady() {}
@Override
public void onImageLoaded() {}
@Override
public void onPreviewLoadError(Exception e) {}
@Override
public void onImageLoadError(Exception e) {}
@Override
public void onTileLoadError(Exception e) {}
@Override
public void onPreviewReleased() {}
});
加載導(dǎo)航圖
大圖加載完后調(diào)用showNavigation設(shè)置導(dǎo)航圖,并加載一張縮略的Bitmap机断。導(dǎo)航圖使用自定義的NavigateImageView楷拳,繼承ImageView材部。
private void showNavigation() {
int navigationWidth = (int) (lav_bitmap.getWidth() * NAVIGATION_SCREEN_WIDTH_SCALE);
mScale = (float) navigationWidth / lav_bitmap.getSWidth();
int navigationHeight = (int) (lav_bitmap.getSHeight() * mScale);
//控件大小
ViewUtil.setWidth(iv_navigate, navigationWidth);
ViewUtil.setHeight(iv_navigate, navigationHeight);
//生成縮略圖
Bitmap thumbnail = BitmapUtils.decodeSampledBitmapFromResource(getResources(), R.mipmap.large_world_map, navigationWidth, navigationHeight);
iv_navigate.setImageBitmap(thumbnail);
}
重點(diǎn)要算出導(dǎo)航圖和原圖的比例mScale,然后通過(guò)目標(biāo)寬高獲取縮略圖唯竹。BitmapUtils網(wǎng)上資料很多,就不多介紹苦丁。
導(dǎo)航圖紅框
大圖縮放時(shí)浸颓,導(dǎo)航圖要用紅框展示當(dāng)前局部位置。
lav_bitmap.setOnStateChangedListener(new SubsamplingScaleImageView.OnStateChangedListener() {
@Override
public void onScaleChanged(float scale, int orientation) {
}
@Override
public void onCenterChanged(PointF pointF, int orientation) {
}
});
大圖的變化使用OnStateChangedListener監(jiān)聽(tīng)旺拉,可以獲取縮放比产上、圖片當(dāng)前中點(diǎn)位置和圖片方向的變化。這里只需要知道圖片當(dāng)前中點(diǎn)位置變化就行蛾狗,在onCenterChanged里調(diào)用drawFrame晋涣。
private void drawFrame(PointF pointF) {
//中點(diǎn)在view位置
PointF centerInViewPointF = lav_bitmap.sourceToViewCoord(pointF);
//view的四個(gè)點(diǎn)
float viewLeft = lav_bitmap.getWidth() / 2 - centerInViewPointF.x;
float viewTop = lav_bitmap.getHeight() / 2 - centerInViewPointF.y;
float viewRight = viewLeft + lav_bitmap.getWidth();
float viewBottom = viewTop + lav_bitmap.getHeight();
//view對(duì)應(yīng)大圖的位置
PointF point1 = lav_bitmap.viewToSourceCoord(viewLeft, viewTop);
PointF point2 = lav_bitmap.viewToSourceCoord(viewRight, viewTop);
PointF point3 = lav_bitmap.viewToSourceCoord(viewLeft, viewBottom);
//PointF point4
//比例
float left = point1.x * mScale;
float top = point1.y * mScale;
float right = point2.x * mScale;
float bottom = point3.y * mScale;
iv_navigate.refreshFrame(left, top, right, bottom);
}
這里要分清楚圖片坐標(biāo)和屏幕坐標(biāo),SubsamplingScaleImageView提供sourceToViewCoord和viewToSourceCoord對(duì)兩種坐標(biāo)進(jìn)行轉(zhuǎn)換沉桌。
入?yún)ointF是大圖當(dāng)前中點(diǎn)坐標(biāo)谢鹊,目標(biāo)是得到當(dāng)前圖片局部的四個(gè)角坐標(biāo),所以要獲取控件在屏幕四個(gè)角的坐標(biāo)留凭,然后viewToSourceCoord獲取對(duì)應(yīng)在大圖上的坐標(biāo)佃扼。最后,結(jié)果乘以mScale蔼夜,就是紅框在導(dǎo)航圖上的坐標(biāo)兼耀。
在導(dǎo)航圖上畫(huà)一個(gè)紅色矩形就比較簡(jiǎn)單了,View的measure求冷、layout瘤运、draw工作流程理應(yīng)人人熟悉。
private Paint mPolygonSidePaint = new Paint();
private RectF mFrameRectF = new RectF();
在NavigateImageView里增加兩個(gè)變量匠题,mPolygonSidePaint是畫(huà)筆拯坟,mFrameRectF記錄矩形的四個(gè)坐標(biāo)。
public void refreshFrame(float left, float top, float right, float bottom) {
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > getWidth()) {
right = getWidth();
}
if (bottom > getHeight()) {
bottom = getHeight();
}
mFrameRectF.set(left, top, right, bottom);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFrame(canvas);
}
private void drawFrame(Canvas canvas) {
if (mFrameRectF != null) {
Path path = new Path();
path.addRect(mFrameRectF, Path.Direction.CW);
canvas.drawPath(path, mPolygonSidePaint);
}
}
refreshFrame設(shè)置了mFrameRectF梧躺,然后重寫onDraw方法似谁,增加drawFrame方法,用drawPath畫(huà)出矩形掠哥。
滑動(dòng)導(dǎo)航圖
導(dǎo)航圖需要支持滑動(dòng)巩踏,對(duì)應(yīng)切換大圖的焦點(diǎn),實(shí)現(xiàn)快速移動(dòng)到大圖某個(gè)局部续搀。
iv_navigate.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
float targetX = event.getX() / mScale;
float targetY = event.getY() / mScale;
lav_bitmap.animateCenter(new PointF(targetX, targetY))
.withDuration(1)
.start();
return true;
}
});
在NavigateImageView的onTouch里增加處理方法塞琼,覆蓋DOWN、MOVE禁舷、UP三種MotionEvent彪杉。將導(dǎo)航圖點(diǎn)擊坐標(biāo)乘以mScale毅往,得到對(duì)應(yīng)大圖中點(diǎn)坐標(biāo),然后調(diào)用animateCenter移動(dòng)到指定局部派近。
增加圖釘
需要在大圖上展示圖釘攀唯,為了避免圖釘非常密集的情況下遮擋圖片,所以初始時(shí)圖釘有一定透明度渴丸,隨著圖片放大侯嘀,減少透明度。
public void refreshPinAlpha() {
int alpha = (int) (MAX_ALPHA * getScale() / getMaxScale() + INITIAL_ALPHA);
if (alpha > MAX_ALPHA) {
alpha = MAX_ALPHA;
}
this.mPinAlpha = alpha;
}
private void drawPin(Canvas canvas) {
mBitmapPaint.setAlpha(mPinAlpha);
mTextPaint.setAlpha(mPinAlpha);
for (Pin pin : mPinList) {
PointF vPointF = sourceToViewCoord(pin.getPointF().x, pin.getPointF().y);
float vCenterX = vPointF.x - mPinBitmap.getWidth() / 2;
float vCenterY = vPointF.y - mPinBitmap.getHeight();
canvas.drawBitmap(mPinBitmap, vCenterX, vCenterY, mBitmapPaint);
if (!TextUtils.isEmpty(pin.getName())) {
//獲取字體高度
Paint.FontMetrics fm = new Paint.FontMetrics();
mTextPaint.getFontMetrics(fm);
float fontHeight = fm.top + fm.bottom;
//顯示名稱
float textX = vPointF.x + mPinBitmap.getWidth() / 2;
float textY = vCenterY - fontHeight;
canvas.drawText(pin.getName(), textX, textY, mTextPaint);
}
}
}
圖釘?shù)睦L畫(huà)谱轨,和導(dǎo)航圖紅框的原理是一樣的戒幔,在onDraw里增加drawPin方法。沒(méi)有什么特別要說(shuō)土童,唯一一點(diǎn)是要認(rèn)真通過(guò)計(jì)算诗茎,讓圖釘尖畫(huà)在坐標(biāo)上。
后記
磨刀不誤砍柴献汗,做出了demo敢订,再移到項(xiàng)目中,不過(guò)是個(gè)優(yōu)化過(guò)程雀瓢。后續(xù)繼續(xù)了解圖片局部加載的原理枢析,和回顧View的工作原理。
如果對(duì)你有幫助刃麸,請(qǐng)給我點(diǎn)贊醒叁。