Android:为超大图增加一个导航图

地图

要做的功能如上面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的工作原理。

如果对你有帮助,请给我点赞。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 去年有一段时间,很明显感觉自己整个人都很焦虑。 孩子的学习上: 平常和小朋友的妈妈们有交流一些各自的育儿情况。尤其...
    弥小木阅读 261评论 0 0
  • Merci 南宁的四季并不十分明显,分明是秋天却仍旧带有夏天的炎热,我和几个朋友一同坐在这家店里,消磨一整个午...
    杳Cecilia阅读 362评论 0 1
  • 十二点之前睡觉 每个月读完一本书✔ 减称十斤后恢复低碳水健康饮食 健身房好了坚持运动 拒绝甜食 每周犒劳自己✔ 控...
    看城市的人阅读 109评论 0 0