Skip to content

Instantly share code, notes, and snippets.

@eliantor
Created June 8, 2012 11:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliantor/2894999 to your computer and use it in GitHub Desktop.
Save eliantor/2894999 to your computer and use it in GitHub Desktop.
public class ImageMapView extends ImageView{
private final static int INVALID_POINTER_ID = -1;
private final ArrayList<Area> mAreaList = new ArrayList<ImageMapView.Area>();
private final SparseArray<Area> mIdToArea = new SparseArray<ImageMapView.Area>();
private OnClickAreaListener mListener;
private int mTouchId = INVALID_POINTER_ID;
private float mTouchX;
private float mTouchY;
private final float mSquaredTouchSlop;
private Area mTouchedArea;
private float mOriginalWidth;
private float mOriginalHeight;
private float mScaledWidth;
private float mScaledHeight;
private float mDipScaling;
/// Drawing
private final static boolean DEBUG=false;
private final Paint protoPaint;
private final static int[] CANDYCANE={ 0x55FF0000,0x5500FF00,0x550000FF};
public ImageMapView(Context context) {
this(context,null,0);
}
public ImageMapView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ImageMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ViewConfiguration config = ViewConfiguration.get(context);
float slop=config.getScaledTouchSlop();
mSquaredTouchSlop=slop*slop;
mTouchedArea=null;
protoPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
protoPaint.setStyle(Style.FILL);
mDipScaling = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.0f, getResources().getDisplayMetrics());
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.ImageMapView);
String mapName=a.getString(R.styleable.ImageMapView_map);
if(mapName!=null){
loadMap(mapName);
}
}
private XmlResourceParser openParser(){
Resources res = getContext().getResources();
return res.getXml(R.xml.maps);
}
private void loadMap(String mapName){
XmlResourceParser xpp = null;
boolean loading = false;
try{
xpp=openParser();
int event=xpp.getEventType();
while(event!=XmlPullParser.END_DOCUMENT){
if(event == XmlPullParser.START_TAG){
String tag = xpp.getName();
if(tag.equals("map")){
loading = mapName.equals(xpp.getAttributeValue(null,"id"));
}
if(loading){
if(tag.equals("area")){
String shape=xpp.getAttributeValue(null,"shape");
String coords=xpp.getAttributeValue(null,"coords");
String id=xpp.getAttributeValue(null,"id");
if((shape!=null)&&(coords!=null)&&(id!=null)){
addShape(shape,coords,id);
}
}
}
}else if(event == XmlPullParser.END_TAG){
String tag = xpp.getName();
if(tag.equals("map")) loading = false;
}
event=xpp.next();
}
}catch(XmlPullParserException ex){
}catch(IOException ex){
}finally{
if(xpp!=null){
xpp.close();
}
}
}
private void addShape(String shape,String coords,String id){
Area a = null;
String rid=id.replace("@+id/", "");
int cid=0;
try{
Class<R.id> res = R.id.class;
Field field = res.getField(rid);
cid = field.getInt(null);
}catch(Exception e){
cid=0;
}
if(cid!=0){
if(shape.equals("rect")){
String[] v = coords.split(",");
if(v.length==4){
a = new RectArea(cid, Float.parseFloat(v[0]),
Float.parseFloat(v[1]),
Float.parseFloat(v[2]),
Float.parseFloat(v[3]));
}else{
throw new RuntimeException("Wrong number of coordinates for rectangular area");
}
}else if(shape.equals("circle")){
String[] v = coords.split(",");
if(v.length == 3){
a = new CircleArea(cid, Float.parseFloat(v[0]),
Float.parseFloat(v[1]),
Float.parseFloat(v[2]));
}else{
throw new RuntimeException("Wrong number of coordinates for circular area");
}
}else if(shape.equals("poly")){
a = new PolyArea(cid, coords);
}else{
throw new RuntimeException("Unsupported imagemap shape type");
}
if(a!=null){
mAreaList.add(a);
mIdToArea.put(a.id, a);
}
}
}
public void setOnClickAreaListener(OnClickAreaListener listener){
this.mListener=listener;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Drawable dr=getDrawable();
Rect bounds = dr.getBounds();
mScaledWidth = w;
mScaledHeight = h;
mOriginalWidth= dr.getIntrinsicWidth();
mOriginalHeight= dr.getIntrinsicHeight();
Log.d("IMGMAP", "w: "+mOriginalWidth+", scaled:"+mScaledWidth+" h:"+mOriginalHeight+", scaled: "+mScaledHeight);
Log.d("IMGMAP", "x: "+bounds.left+ " y:"+bounds.top);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(DEBUG){
canvas.save();
float[] values=new float[9];
getImageMatrix().getValues(values);
float xScale = values[Matrix.MSCALE_X];
float yScale = values[Matrix.MSCALE_Y];
canvas.scale(xScale*mDipScaling, yScale*mDipScaling, 0, 0);
int i=0;
for(Area a:mAreaList){
protoPaint.setColor(CANDYCANE[(i++)%CANDYCANE.length]);
a.draw(canvas, protoPaint);
}
canvas.restore();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
boolean handled= false;
switch(action & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_DOWN:
handled= startHandling(event);
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_CANCEL:
mTouchId=INVALID_POINTER_ID;
mTouchedArea=null;
handled=false;
break;
case MotionEvent.ACTION_MOVE:
if(mTouchId!=INVALID_POINTER_ID){
final int index = event.findPointerIndex(mTouchId);
final int count = event.getPointerCount();
if(index>=0&& index<count){
final float xPos=event.getX(index);
final float yPos=event.getY(index);
final float dx=xPos-mTouchX;
final float dy=yPos-mTouchY;
handled= (dx*dx+dy*dy)<mSquaredTouchSlop;
if(!handled){
mTouchId=INVALID_POINTER_ID;
mTouchedArea=null;
}
}else{
mTouchId=INVALID_POINTER_ID;
mTouchedArea=null;
handled=false;
}
}else{
handled=false;
}
break;
case MotionEvent.ACTION_UP:
handled= maybeClick(event);
break;
}
return handled||super.onTouchEvent(event);
}
private boolean maybeClick(MotionEvent event) {
boolean handled=false;
if(mTouchId != INVALID_POINTER_ID){
performClickArea(this.mTouchedArea.id);
handled=true;
}
mTouchId=INVALID_POINTER_ID;
mTouchedArea=null;
return handled;
}
public void performClickArea(int id){
if(mListener!=null){
playSoundEffect(SoundEffectConstants.CLICK);
mListener.onClickArea(id);
}
}
private boolean startHandling(MotionEvent event) {
final float xPos=event.getX();
final float yPos=event.getY();
float[] values=new float[9];
getImageMatrix().getValues(values);
float xScale = values[Matrix.MSCALE_X]*mDipScaling; //TODO Why this factor?
float yScale = values[Matrix.MSCALE_Y]*mDipScaling; //TODO May depend on dp configuration
for(Area a:mAreaList){
if(a.isInside(xPos/xScale, yPos/yScale)){
mTouchedArea=a;
break;
}
}
if(mTouchedArea!=null){
mTouchX=xPos;
mTouchY=yPos;
mTouchId=event.getPointerId(0);
return true;
}
return false;
}
public static interface OnClickAreaListener{
public void onClickArea(int id);
}
private abstract static class Area{
final int id;
final Path areaPath;
Area(int id){
this.id=id;
areaPath=new Path();
}
abstract boolean isInside(final float x,final float y);
void draw(Canvas c,Paint p){
c.drawPath(areaPath, p);
}
}
private static class RectArea extends Area{
private final RectF mBounding=new RectF();
RectArea(int id,final float l,final float t,final float r,final float b){
super(id);
mBounding.set(l, t, r, b);
}
@Override
boolean isInside(float x, float y) {
return mBounding.contains(x, y);
}
}
private static class CircleArea extends Area{
private final float centerX;
private final float centerY;
private final float radius;
CircleArea(int id,float cx,float cy,float r){
super(id);
this.centerX=cx;
this.centerY=cy;
this.radius=r;
}
@Override
boolean isInside(float x, float y) {
final float dx=x-centerX;
final float dy=y-centerY;
return FloatMath.sqrt(dx*dx+dy*dy)<radius;
}
}
private static class PolyArea extends Area{
private final Region mRegion;
public PolyArea(int id,String coordString) {
super(id);
String[] v = coordString.split(",");
if(v.length%2 != 0)throw new RuntimeException("Error parsing poly area");
int sides=v.length/2;
if(sides>0){
areaPath.moveTo(Float.parseFloat(v[0]), Float.parseFloat(v[1]));
int j;
for(int i=1;i<sides;i++){
j=i*2;
float xC = Float.parseFloat(v[j]);
float yC = Float.parseFloat(v[j+1]);
areaPath.lineTo(xC, yC);
}
areaPath.close();
}
mRegion=new Region();
RectF bounds = new RectF();
areaPath.computeBounds(bounds, true);
mRegion.setPath(areaPath, new Region((int)bounds.left, (int)bounds.top, (int)bounds.right, (int)bounds.bottom));
}
@Override
boolean isInside(float x, float y) {
return mRegion.contains((int)x, (int)y);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment