在开发中,我们有时候需要再点击图片不同区域的时候进行不同的操作,比如在展示一个地图图片,点击不同的地区,当前地区高亮或者播放该地区的介绍视频等;又或者在医疗健康的App中,需要点击人体图片的某个部位展示不同的医学建议等等。
我目前的项目需求是第二种情况。实现类似下图(图片来源网上)的效果:
我们可以要求UI提供多张大小完全一致的图片,对应每个需要高亮的部位,点击对应区域的时候展示对应的图片然后执行对应的操作即可。
所以咱们最终目标是实现点击图片不同的地方触发不同的操作。这里有两种方案:
首先因为是展示图片,我们自定义的控件自然是继承ImageView或者AppCompatImageView。其次这个和触摸、点击相关,还需要获取对应的触摸事件的坐标,我们很容易想到在onTouchEvent中去处理,但是,如果我们重写onTouchEvent来处理点击事件,那需要做很多的判断,搞不好还容易出现滑动冲突之类的问题。所以这里我打算在performClick中处理,但是perforClick中没有传递对应的坐标,不过好在performClick是在onTouchEvent方法中调用的,所以在onTouchEvent方法中把对应的坐标记录下来即可。
public class ImageMapView extends AppCompatImageView {
private float currentX;
private float currentY;
...
@Override
public boolean onTouchEvent(MotionEvent event) {
this.currentX = event.getX();
this.currentY = event.getY();
return super.onTouchEvent(event);
}
@Override
public boolean performClick() {
OnAreaClickListener listener = this.onAreaClickListener;
if(listener != null) {
Point point = getImagePoint(currentX, currentY);
Log.v(TAG, "Map coor: " + point);
synchronized (areas) {
for (Area area : areas) {
if(area.isInArea(point.x, point.y)) {
Log.d(TAG, "Area " + area.getName() + "[" + area.getId() + "] was clicked");
listener.onAreaClicked(this, area);
return true;
}
}
}
}
return super.performClick();
}
...
}
那么还有一个问题,就是需要把当前触摸事件的坐标转化成图片原始的坐标
private Point getImagePoint(float x, float y) {
Matrix matrix = getImageMatrix();
Matrix copy = new Matrix();
matrix.invert(copy);
RectF rectF = new RectF();
Drawable drawable = getDrawable();
if (drawable != null) {
rectF.set(0, 0, x, y);
copy.mapRect(rectF);
float scaleX = mapWidth * 1.0f / drawable.getIntrinsicWidth();
float scaleY = mapHeight * 1.0f / drawable.getIntrinsicHeight();
rectF.right = rectF.right * scaleX;
rectF.bottom = rectF.bottom * scaleY;
}
return new Point(Math.round(rectF.right), Math.round(rectF.bottom));
}
剩下的就是区域的定义、判断落点是否在区域内了,这些都不是核心问题了。
这里展示一下XML定义点击区域:
<map xmlns:android="http://schemas.android.com/apk/res/android"
width="500"
height="500">
<area name="Rect" shape="rect" coords="62,49,193,123" id="@+id/shape_rect"/>
<area name="Circle" shape="circle" coords="211,262,50" id="@+id/shape_circle"/>
<area name="Poly" shape="poly" coords="300,332,360,288,421,332,399,404,322,404" id="@+id/shape_poly"/>
map>
增加一个自定义属性:
<resources>
<declare-styleable name="ImageMapView">
<attr name="imageMap" format="reference"/>
declare-styleable>
resources>
在代码里面设置自定义属性
<com.xinyanruanjian.imagemapview.ImageMapView
android:id="@+id/imv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/shapes"
android:scaleType="fitCenter"
app:imageMap="@xml/map"/>
在自定义控件中加载这些区域定义
public class ImageMapView extends AppCompatImageView {
public ImageMapView(@NonNull Context context) {
this(context, null);
}
public ImageMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ImageMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if(attrs != null) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.ImageMapView);
int resourceId = ta.getResourceId(R.styleable.ImageMapView_imageMap, 0);
if(resourceId != 0) {
loadMapInfo(resourceId);
}
ta.recycle();
}
if(onClickListener == null) {
setOnClickListener(v -> {});
}
}
private void loadMapInfo(int xmlId) {
try {
XmlResourceParser xpp = getResources().getXml(xmlId);
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_DOCUMENT) {
// Start document
// This is a useful branch for a debug log if
// parsing is not working
} else if (eventType == XmlPullParser.START_TAG) {
String tag = xpp.getName();
if("map".equalsIgnoreCase(tag)) {
mapHeight = xpp.getAttributeIntValue(null, "height", 0);
mapWidth = xpp.getAttributeIntValue(null, "width", 0);
} else if ("area".equalsIgnoreCase(tag)) {
Area a = null;
String shape = xpp.getAttributeValue(null, "shape");
String coords = xpp.getAttributeValue(null, "coords");
int id = xpp.getIdAttributeResourceValue(0);
// as a name for this area, try to find any of these
// attributes
// name attribute is custom to this impl (not standard in html area tag)
String name = xpp.getAttributeValue(null, "name");
if (name == null) {
name = xpp.getAttributeValue(null, "title");
}
if (name == null) {
name = xpp.getAttributeValue(null, "alt");
}
if ((shape != null) && (coords != null)) {
a = addShape(shape, name, coords, id);
if (a != null) {
// add all of the area tag attributes
// so that they are available to the
// implementation if needed (see getAreaAttribute)
for (int i = 0; i < xpp.getAttributeCount(); i++) {
String attrName = xpp.getAttributeName(i);
String attrVal = xpp.getAttributeValue(null, attrName);
a.addValue(attrName, attrVal);
}
}
}
}
} else if (eventType == XmlPullParser.END_TAG) {
}
eventType = xpp.next();
}
} catch (Exception e) {
e.printStackTrace();
}
if(mapWidth == 0 || mapHeight == 0) {
throw new IllegalStateException("width and height must not be 0, width: " + mapWidth + ", height: " + mapHeight);
}
Log.v(TAG, "Area size: " + areas.size());
}
}
代码链接:Github
才疏学浅,如有不对,欢迎指正