要做的功能如上面demo所示,在图片缩放时,增加一个展示当前局部位置的导航图。要求是:
- 支持超大图;
- 导航图红框定位当前局部位置;
- 导航图支持滑动,快速定位大图位置;
- 支持图钉显示,随图片放大更新透明度。
简书记录下开发过程,demo可以在github找到
加载超大图
测试用的是一张21m大的图片(上传代码里换了张小的),无论如何不能一次载入内存显示。github里找到一个显示超大图的控件subsampling-scale-image-view
,原理是使用Android的BitmapRegionDecoder局部加载图片。
创建一个自定义LargeBitmapView,继承SubsamplingScaleImageView。加载图片使用setImage方法,图片来源可以多种,通过ImageSource获取。
lav_bitmap.setImage(ImageSource.resource(R.mipmap.large_world_map));
监听图片加载过程使用OnImageEventListener,回调丰富。
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() {}
});
加载导航图
大图加载完后调用showNavigation设置导航图,并加载一张缩略的Bitmap。导航图使用自定义的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);
}
重点要算出导航图和原图的比例mScale,然后通过目标宽高获取缩略图。BitmapUtils网上资料很多,就不多介绍。
导航图红框
大图缩放时,导航图要用红框展示当前局部位置。
lav_bitmap.setOnStateChangedListener(new SubsamplingScaleImageView.OnStateChangedListener() {
@Override
public void onScaleChanged(float scale, int orientation) {
}
@Override
public void onCenterChanged(PointF pointF, int orientation) {
}
});
大图的变化使用OnStateChangedListener监听,可以获取缩放比、图片当前中点位置和图片方向的变化。这里只需要知道图片当前中点位置变化就行,在onCenterChanged里调用drawFrame。
private void drawFrame(PointF pointF) {
//中点在view位置
PointF centerInViewPointF = lav_bitmap.sourceToViewCoord(pointF);
//view的四个点
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对应大图的位置
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);
}
这里要分清楚图片坐标和屏幕坐标,SubsamplingScaleImageView提供sourceToViewCoord和viewToSourceCoord对两种坐标进行转换。
入参pointF是大图当前中点坐标,目标是得到当前图片局部的四个角坐标,所以要获取控件在屏幕四个角的坐标,然后viewToSourceCoord获取对应在大图上的坐标。最后,结果乘以mScale,就是红框在导航图上的坐标。
在导航图上画一个红色矩形就比较简单了,View的measure、layout、draw工作流程理应人人熟悉。
private Paint mPolygonSidePaint = new Paint();
private RectF mFrameRectF = new RectF();
在NavigateImageView里增加两个变量,mPolygonSidePaint是画笔,mFrameRectF记录矩形的四个坐标。
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设置了mFrameRectF,然后重写onDraw方法,增加drawFrame方法,用drawPath画出矩形。
滑动导航图
导航图需要支持滑动,对应切换大图的焦点,实现快速移动到大图某个局部。
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。将导航图点击坐标乘以mScale,得到对应大图中点坐标,然后调用animateCenter移动到指定局部。
增加图钉
需要在大图上展示图钉,为了避免图钉非常密集的情况下遮挡图片,所以初始时图钉有一定透明度,随着图片放大,减少透明度。
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);
}
}
}
图钉的绘画,和导航图红框的原理是一样的,在onDraw里增加drawPin方法。没有什么特别要说,唯一一点是要认真通过计算,让图钉尖画在坐标上。
后记
磨刀不误砍柴,做出了demo,再移到项目中,不过是个优化过程。后续继续了解图片局部加载的原理,和回顾View的工作原理。
如果对你有帮助,请给我点赞。