地图
要做的功能如上面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(newSubsamplingScaleImageView.OnImageEventListener(){@Override
publicvoidonReady(){}@Override
publicvoidonImageLoaded(){}@Override
publicvoidonPreviewLoadError(Exceptione){}@Override
publicvoidonImageLoadError(Exceptione){}@Override
publicvoidonTileLoadError(Exceptione){}@Override
publicvoidonPreviewReleased(){}
});
加载导航图
大图加载完后调用showNavigation设置导航图,并加载一张缩略的Bitmap。导航图使用自定义的NavigateImageView,继承ImageView。privatevoidshowNavigation(){intnavigationWidth=(int)(lav_bitmap.getWidth()*NAVIGATION_SCREEN_WIDTH_SCALE);
mScale=(float)navigationWidth/lav_bitmap.getSWidth();intnavigationHeight=(int)(lav_bitmap.getSHeight()*mScale);//控件大小
ViewUtil.setWidth(iv_navigate,navigationWidth);
ViewUtil.setHeight(iv_navigate,navigationHeight);//生成缩略图
Bitmapthumbnail=BitmapUtils.decodeSampledBitmapFromResource(getResources(),R.mipmap.large_world_map,navigationWidth,navigationHeight);
iv_navigate.setImageBitmap(thumbnail);
}
重点要算出导航图和原图的比例mScale,然后通过目标宽高获取缩略图。BitmapUtils网上资料很多,就不多介绍。
导航图红框
大图缩放时,导航图要用红框展示当前局部位置。lav_bitmap.setOnStateChangedListener(newSubsamplingScaleImageView.OnStateChangedListener(){@Override
publicvoidonScaleChanged(floatscale,intorientation){
}@Override
publicvoidonCenterChanged(PointFpointF,intorientation){
}
});
大图的变化使用OnStateChangedListener监听,可以获取缩放比、图片当前中点位置和图片方向的变化。这里只需要知道图片当前中点位置变化就行,在onCenterChanged里调用drawFrame。privatevoiddrawFrame(PointFpointF){//中点在view位置
PointFcenterInViewPointF=lav_bitmap.sourceToViewCoord(pointF);//view的四个点
floatviewLeft=lav_bitmap.getWidth()/2-centerInViewPointF.x;floatviewTop=lav_bitmap.getHeight()/2-centerInViewPointF.y;floatviewRight=viewLeft+lav_bitmap.getWidth();floatviewBottom=viewTop+lav_bitmap.getHeight();//view对应大图的位置
PointFpoint1=lav_bitmap.viewToSourceCoord(viewLeft,viewTop);
PointFpoint2=lav_bitmap.viewToSourceCoord(viewRight,viewTop);
PointFpoint3=lav_bitmap.viewToSourceCoord(viewLeft,viewBottom);//PointFpoint4
//比例
floatleft=point1.x*mScale;floattop=point1.y*mScale;floatright=point2.x*mScale;floatbottom=point3.y*mScale;
iv_navigate.refreshFrame(left,top,right,bottom);
}
这里要分清楚图片坐标和屏幕坐标,SubsamplingScaleImageView提供sourceToViewCoord和viewToSourceCoord对两种坐标进行转换。
入参pointF是大图当前中点坐标,目标是得到当前图片局部的四个角坐标,所以要获取控件在屏幕四个角的坐标,然后viewToSourceCoord获取对应在大图上的坐标。最后,结果乘以mScale,就是红框在导航图上的坐标。
在导航图上画一个红色矩形就比较简单了,View的measure、layout、draw工作流程理应人人熟悉。privatePaintmPolygonSidePaint=newPaint();privateRectFmFrameRectF=newRectF();
在NavigateImageView里增加两个变量,mPolygonSidePaint是画笔,mFrameRectF记录矩形的四个坐标。publicvoidrefreshFrame(floatleft,floattop,floatright,floatbottom){if(left
left=0;
}if(top
top=0;
}if(right>getWidth()){
right=getWidth();
}if(bottom>getHeight()){
bottom=getHeight();
}
mFrameRectF.set(left,top,right,bottom);
invalidate();
}@OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas);
drawFrame(canvas);
}privatevoiddrawFrame(Canvascanvas){if(mFrameRectF!=null){
Pathpath=newPath();
path.addRect(mFrameRectF,Path.Direction.CW);
canvas.drawPath(path,mPolygonSidePaint);
}
}
refreshFrame设置了mFrameRectF,然后重写onDraw方法,增加drawFrame方法,用drawPath画出矩形。
滑动导航图
导航图需要支持滑动,对应切换大图的焦点,实现快速移动到大图某个局部。iv_navigate.setOnTouchListener(newView.OnTouchListener(){@Override
publicbooleanonTouch(Viewv,MotionEventevent){floattargetX=event.getX()/mScale;floattargetY=event.getY()/mScale;
lav_bitmap.animateCenter(newPointF(targetX,targetY))
.withDuration(1)
.start();returntrue;
}
});
在NavigateImageView的onTouch里增加处理方法,覆盖DOWN、MOVE、UP三种MotionEvent。将导航图点击坐标乘以mScale,得到对应大图中点坐标,然后调用animateCenter移动到指定局部。
增加图钉
需要在大图上展示图钉,为了避免图钉非常密集的情况下遮挡图片,所以初始时图钉有一定透明度,随着图片放大,减少透明度。publicvoidrefreshPinAlpha(){intalpha=(int)(MAX_ALPHA*getScale()/getMaxScale()+INITIAL_ALPHA);if(alpha>MAX_ALPHA){
alpha=MAX_ALPHA;
}this.mPinAlpha=alpha;
}privatevoiddrawPin(Canvascanvas){
mBitmapPaint.setAlpha(mPinAlpha);
mTextPaint.setAlpha(mPinAlpha);for(Pinpin:mPinList){
PointFvPointF=sourceToViewCoord(pin.getPointF().x,pin.getPointF().y);floatvCenterX=vPointF.x-mPinBitmap.getWidth()/2;floatvCenterY=vPointF.y-mPinBitmap.getHeight();
canvas.drawBitmap(mPinBitmap,vCenterX,vCenterY,mBitmapPaint);if(!TextUtils.isEmpty(pin.getName())){//获取字体高度
Paint.FontMetricsfm=newPaint.FontMetrics();
mTextPaint.getFontMetrics(fm);floatfontHeight=fm.top+fm.bottom;//显示名称
floattextX=vPointF.x+mPinBitmap.getWidth()/2;floattextY=vCenterY-fontHeight;
canvas.drawText(pin.getName(),textX,textY,mTextPaint);
}
}
}
图钉的绘画,和导航图红框的原理是一样的,在onDraw里增加drawPin方法。没有什么特别要说,唯一一点是要认真通过计算,让图钉尖画在坐标上。
后记
磨刀不误砍柴,做出了demo,再移到项目中,不过是个优化过程。后续继续了解图片局部加载的原理,和回顾View的工作原理。
作者:展翅而飞
链接:/p/3796d69b24b0