Android Open Source - zionhs Image Map






From Project

Back to project page zionhs.

License

The source code is released under:

GNU General Public License

If you think the Android project zionhs listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2011 Scott Lund/*from w w  w  .ja va2s  . c  o  m*/
 * Modified in 2013 by Oleksii Chyrkov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ctc.android.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.widget.ImageView;
import android.widget.Scroller;

import org.jetbrains.annotations.NotNull;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;

public class ImageMap extends ImageView
{
  // mFitImageToScreen
  // if true - initial image resized to fit the screen, aspect ratio may be broken
  // if false- initial image resized so that no empty screen is visible, aspect ratio maintained
  //           image size will likely be larger than screen

  // by default, this is true
  private boolean mFitImageToScreen=true;

  // For certain images, it is best to always resize using the original
  // image bits. This requires keeping the original image in memory along with the
  // current sized version and thus takes extra memory.
  // If you always want to resize using the original, set mScaleFromOriginal to true
  // If you want to use less memory, and the image scaling up and down repeatedly
  // does not blur or loose quality, set mScaleFromOriginal to false

  // by default, this is false
  private boolean mScaleFromOriginal=false;

  // mMaxSize controls the maximum zoom size as a multiplier of the initial size.
  // Allowing size to go too large may result in memory problems.
  //  set this to 1.0f to disable resizing
  // by default, this is 1.5f
  private static final float defaultMaxSize = 1.5f;
  private float mMaxSize = 1.5f;

  /* Touch event handling variables */
  private VelocityTracker mVelocityTracker;

  private int mTouchSlop;
  private int mMinimumVelocity;
  private int mMaximumVelocity;

  private Scroller mScroller;

  private boolean mIsBeingDragged = false;

  HashMap<Integer,TouchPoint> mTouchPoints = new HashMap<Integer,TouchPoint>();
  TouchPoint mMainTouch=null;
  TouchPoint mPinchTouch=null;

  /* Pinch zoom */
  float mInitialDistance;
  boolean mZoomEstablished=false;
  int mLastDistanceChange=0;
  boolean mZoomPending=false;

  /* Paint objects for drawing info bubbles */
  Paint textPaint;
  Paint textOutlinePaint;
  Paint bubblePaint;
  Paint bubbleShadowPaint;

  /*
   * Bitmap handling
   */
  Bitmap mImage;
  Bitmap mOriginal;

  // Info about the bitmap (sizes, scroll bounds)
  // initial size
  int mImageHeight;
  int mImageWidth;
  float mAspect;
  // scaled size
  int mExpandWidth;
  int mExpandHeight;
  // the right and bottom edges (for scroll restriction)
  int mRightBound;
  int mBottomBound;
  // the current zoom scaling (X and Y kept separate)
  protected float mResizeFactorX;
  protected float mResizeFactorY;
  // minimum height/width for the image
  int mMinWidth=-1;
  int mMinHeight=-1;
  // maximum height/width for the image
  int mMaxWidth=-1;
  int mMaxHeight=-1;

  // the position of the top left corner relative to the view
  int mScrollTop;
  int mScrollLeft;

  // view height and width
  int mViewHeight=-1;
  int mViewWidth=-1;

  /*
   * containers for the image map areas
   * using SparseArray<Area> instead of HashMap for the sake of performance
   */
  ArrayList<Area> mAreaList = new ArrayList<Area>();
  SparseArray<Area> mIdToArea = new SparseArray<Area>();

  // click handler list
  ArrayList<OnImageMapClickedHandler> mCallbackList;

  // list of open info bubbles
  SparseArray<Bubble> mBubbleMap = new SparseArray<Bubble>();

  // changed this from local variable to class field
  protected String mapName;

  // accounting for screen density
  protected float densityFactor;

  //possible to reduce memory consumption
  protected BitmapFactory.Options options;

  /*
   * Constructors
   */
  public ImageMap(Context context) {
    super(context);
    init();
  }

  public ImageMap(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
    loadAttributes(attrs);
  }

  public ImageMap(Context context, AttributeSet attrs, int defStyle)
  {
    super(context, attrs, defStyle);
    init();
    loadAttributes(attrs);
  }

  /**
   * get the map name from the attributes and load areas from xml
   * @param attrs
   */
  private void loadAttributes(AttributeSet attrs)
  {
    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ImageMap);

    this.mFitImageToScreen = a.getBoolean(R.styleable.ImageMap_fitImageToScreen, true);
    this.mScaleFromOriginal = a.getBoolean(R.styleable.ImageMap_scaleFromOriginal, false);
    this.mMaxSize = a.getFloat(R.styleable.ImageMap_maxSizeFactor, defaultMaxSize);

    this.mapName = a.getString(R.styleable.ImageMap_map);
    if (mapName != null)
    {
      loadMap(mapName);
    }
  }

  /**
   * parse the maps.xml resource and pull out the areas
   * @param map - the name of the map to load
   */
  private void loadMap(String map) {
    boolean loading = false;
    try {
      XmlResourceParser xpp = getResources().getXml(R.xml.maps);

      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 (tag.equalsIgnoreCase("map")) {
            String mapname = xpp.getAttributeValue(null, "name");
            if (mapname !=null) {
              if (mapname.equalsIgnoreCase(map)) {
                loading=true;
              }
            }
          }
          if (loading) {
            if (tag.equalsIgnoreCase("area")) {
              Area a=null;
              String shape = xpp.getAttributeValue(null, "shape");
              String coords = xpp.getAttributeValue(null, "coords");
              String id = xpp.getAttributeValue(null, "id");

              // 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) {
          String tag = xpp.getName();
          if (tag.equalsIgnoreCase("map")) {
            loading = false;
          }
        }
        eventType = xpp.next();
      }
    } catch (XmlPullParserException xppe) {
      // Having trouble loading? Log this exception
    } catch (IOException ioe) {
      // Having trouble loading? Log this exception
    }
  }

  /**
   * Create a new area and add to tracking
   * Changed this from private to protected!
   * @param shape
   * @param name
   * @param coords
   * @param id
   * @return
   */
  protected Area addShape( String shape, String name, String coords, String id)
  {
    Area a = null;
    String rid = id.replace("@+id/", "");
    int _id=0;

    try
    {
      Class<R.id> res = R.id.class;
      Field field = res.getField(rid);
      _id = field.getInt(null);
    }
    catch (Exception e)
    {
      _id = 0;
    }
    if (_id != 0)
    {
      if (shape.equalsIgnoreCase("rect"))
      {
        String[] v = coords.split(",");
        if (v.length == 4)
        {
          a = new RectArea(_id, name, Float.parseFloat(v[0]),
            Float.parseFloat(v[1]),
            Float.parseFloat(v[2]),
            Float.parseFloat(v[3]));
        }
      }
      if (shape.equalsIgnoreCase("circle"))
      {
        String[] v = coords.split(",");
        if (v.length == 3) {
          a = new CircleArea(_id,name, Float.parseFloat(v[0]),
            Float.parseFloat(v[1]),
            Float.parseFloat(v[2])
          );
        }
      }
      if (shape.equalsIgnoreCase("poly"))
      {
        a = new PolyArea(_id,name, coords);
      }
      if (a != null)
      {
        addArea(a);
      }
    }
    return a;
  }

  public void addArea( Area a )
  {
    mAreaList.add(a);
    mIdToArea.put(a.getId(), a);
  }

  public void addBubble(String text, int areaId )
  {
    if (mBubbleMap.get(areaId) == null)
    {
      Bubble b = new Bubble(text,areaId);
      mBubbleMap.put(areaId,b);
    }
  }

  public void showBubble(String text, int areaId)
  {
    mBubbleMap.clear();
    addBubble(text,areaId);
    invalidate();
  }

  public void showBubble(int areaId)
  {
    mBubbleMap.clear();
    Area a = mIdToArea.get(areaId);
    if (a != null)
    {
      addBubble(a.getName(),areaId);
    }
    invalidate();
  }

  public void centerArea( int areaId )
  {
    Area a = mIdToArea.get(areaId);
    if (a != null)
    {
      float x = a.getOriginX()*mResizeFactorX;
      float y = a.getOriginY()*mResizeFactorY;
      int left = (int)((mViewWidth/2)-x);
      int top  = (int)((mViewHeight/2)-y);
      moveTo(left,top);
    }
  }

  public void centerAndShowArea(String text, int areaId)
  {
    centerArea(areaId);
    showBubble(text,areaId);
  }

  public void centerAndShowArea(int areaId)
  {
    Area a = mIdToArea.get(areaId);
    if (a != null) {
      centerAndShowArea(a.getName(),areaId);
    }
  }

  public String getAreaAttribute(int areaId, String key)
  {
    String value = null;
    Area a = mIdToArea.get(areaId);
    if (a != null)
    {
      value = a.getValue(key);
    }
    return value;
  }

  /**
   * initialize the view
   */
  private void init()
  {
    // set up paint objects
    initDrawingTools();

    // create a scroller for flinging
    mScroller = new Scroller(getContext());

    // get some default values from the system for touch/drag/fling
    final ViewConfiguration configuration = ViewConfiguration
      .get(getContext());
    mTouchSlop = configuration.getScaledTouchSlop();
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

    //find out the screen density
    densityFactor = getResources().getDisplayMetrics().density;
  }

  /*
   * These methods will be called when images or drawables are set
   * in the XML for the view.  We handle either bitmaps or drawables
   * @see android.widget.ImageView#setImageBitmap(android.graphics.Bitmap)
   */
  @Override
  public void setImageBitmap(Bitmap bm)
  {
    if (mImage==mOriginal)
    {
      mOriginal=null;
    }
    else
    {
      mOriginal.recycle();
      mOriginal=null;
    }
    if (mImage != null)
    {
      mImage.recycle();
      mImage=null;
    }
    mImage = bm;
    mOriginal = bm;
    mImageHeight = mImage.getHeight();
    mImageWidth = mImage.getWidth();
    mAspect = (float)mImageWidth / mImageHeight;
    setInitialImageBounds();
  }

  @Override
  public void setImageResource(int resId)
  {
    final String imageKey = String.valueOf(resId);
    BitmapHelper bitmapHelper = BitmapHelper.getInstance();
    Bitmap bitmap = bitmapHelper.getBitmapFromMemCache(imageKey);

    // 1 is the default setting, powers of 2 used to decrease image quality (and memory consumption)
    // TODO: enable variable inSampleSize for low-memory devices
    options = new BitmapFactory.Options();
    options.inSampleSize = 1;

    if (bitmap == null)
    {
      bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
      bitmapHelper.addBitmapToMemoryCache(imageKey, bitmap);
    }
    setImageBitmap(bitmap);
  }

  /*
    setImageDrawable() is called by Android when the android:src attribute is set.
    To avoid this and use the more flexible setImageResource(),
    it is advised to omit the android:src attribute and call setImageResource() directly from code.
   */
  @Override
  public void setImageDrawable(Drawable drawable) {
    if (drawable instanceof BitmapDrawable) {
      BitmapDrawable bd = (BitmapDrawable) drawable;
      setImageBitmap(bd.getBitmap());
    }
  }

  /**
   * setup the paint objects for drawing bubbles
   */
  private void initDrawingTools() {
    textPaint = new Paint();
    textPaint.setColor(0xFF000000);
    textPaint.setTextSize(30);
    textPaint.setTypeface(Typeface.SERIF);
    textPaint.setTextAlign(Paint.Align.CENTER);
    textPaint.setAntiAlias(true);

    textOutlinePaint = new Paint();
    textOutlinePaint.setColor(0xFF000000);
    textOutlinePaint.setTextSize(18);
    textOutlinePaint.setTypeface(Typeface.SERIF);
    textOutlinePaint.setTextAlign(Paint.Align.CENTER);
    textOutlinePaint.setStyle(Paint.Style.STROKE);
    textOutlinePaint.setStrokeWidth(2);

    bubblePaint=new Paint();
    bubblePaint.setColor(0xFFFFFFFF);
    bubbleShadowPaint=new Paint();
    bubbleShadowPaint.setColor(0xFF000000);

  }

  /*
   * Called by the scroller when flinging
   * @see android.view.View#computeScroll()
   */
  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      int oldX = mScrollLeft;
      int oldY = mScrollTop;

      int x = mScroller.getCurrX();
      int y = mScroller.getCurrY();

      if (oldX != x) {
        moveX(x-oldX);
      }
      if (oldY != y) {
        moveY(y-oldY);
      }
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
  {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int chosenWidth = chooseDimension(widthMode, widthSize);
    int chosenHeight = chooseDimension(heightMode, heightSize);

    setMeasuredDimension(chosenWidth, chosenHeight);
  }

  private int chooseDimension(int mode, int size) {
    if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY)
    {
      return size;
    }
    else
    {
      // (mode == MeasureSpec.UNSPECIFIED)
      return getPreferredSize();
    }
  }


  /**
   * set the initial bounds of the image
   */
  void setInitialImageBounds()
  {
    if (mFitImageToScreen)
    {
      setInitialImageBoundsFitImage();
    }
    else
    {
      setInitialImageBoundsFillScreen();
    }
  }

  /**
   * setInitialImageBoundsFitImage sets the initial image size to match the
   * screen size.  aspect ratio may be broken
   */
  void setInitialImageBoundsFitImage()
  {
    if (mImage != null)
    {
      if (mViewWidth > 0)
      {
        mMinHeight = mViewHeight;
        mMinWidth = mViewWidth;
        mMaxWidth = (int)(mMinWidth * mMaxSize);
        mMaxHeight = (int)(mMinHeight * mMaxSize);

        mScrollTop = 0;
        mScrollLeft = 0;
        scaleBitmap(mMinWidth, mMinHeight);
      }
    }
  }

  /**
   * setInitialImageBoundsFillScreen sets the initial image size to so that there
   * is no uncovered area of the device
   */
  void setInitialImageBoundsFillScreen()
  {
    if (mImage != null)
    {
      if (mViewWidth > 0)
      {
        boolean resize=false;

        int newWidth=mImageWidth;
        int newHeight=mImageHeight;

        // The setting of these max sizes is very arbitrary
        // Need to find a better way to determine max size
        // to avoid attempts too big a bitmap and throw OOM
        if (mMinWidth==-1)
        {
          // set minimums so that the largest
          // direction we always filled (no empty view space)
          // this maintains initial aspect ratio
          if (mViewWidth > mViewHeight)
          {
            mMinWidth = mViewWidth;
            mMinHeight = (int)(mMinWidth/mAspect);
          } else {
            mMinHeight = mViewHeight;
            mMinWidth = (int)(mAspect*mViewHeight);
          }
          mMaxWidth = (int)(mMinWidth * 1.5f);
          mMaxHeight = (int)(mMinHeight * 1.5f);
        }

        if (newWidth < mMinWidth) {
          newWidth = mMinWidth;
          newHeight = (int) (((float) mMinWidth / mImageWidth) * mImageHeight);
          resize = true;
        }
        if (newHeight < mMinHeight) {
          newHeight = mMinHeight;
          newWidth = (int) (((float) mMinHeight / mImageHeight) * mImageWidth);
          resize = true;
        }

        mScrollTop = 0;
        mScrollLeft = 0;

        // scale the bitmap
        if (resize) {
          scaleBitmap(newWidth, newHeight);
        } else {
          mExpandWidth=newWidth;
          mExpandHeight=newHeight;

          mResizeFactorX = ((float) newWidth / mImageWidth);
          mResizeFactorY = ((float) newHeight / mImageHeight);
          mRightBound = 0 - (mExpandWidth - mViewWidth);
          mBottomBound = 0 - (mExpandHeight - mViewHeight);
        }
      }
    }
  }

  /**
   * Set the image to new width and height
   * create a new scaled bitmap and dispose of the previous one
   * recalculate scaling factor and right and bottom bounds
   * @param newWidth
   * @param newHeight
   */
  public void scaleBitmap(int newWidth, int newHeight) {
    // Technically since we always keep aspect ratio intact
    // we should only need to check one dimension.
    // Need to investigate and fix
    if ((newWidth > mMaxWidth) || (newHeight > mMaxHeight)) {
      newWidth = mMaxWidth;
      newHeight = mMaxHeight;
    }
    if ((newWidth < mMinWidth) || (newHeight < mMinHeight)) {
      newWidth = mMinWidth;
      newHeight = mMinHeight;
    }

    if ((newWidth != mExpandWidth) || (newHeight!=mExpandHeight)) {
      // NOTE: depending on the image being used, it may be
      //       better to keep the original image available and
      //       use those bits for resize.  Repeated grow/shrink
      //       can render some images visually non-appealing
      //       see comments at top of file for mScaleFromOriginal
      // try to create a new bitmap
      // If you get a recycled bitmap exception here, check to make sure
      // you are not setting the bitmap both from XML and in code
      Bitmap newbits = Bitmap.createScaledBitmap(mScaleFromOriginal ? mOriginal:mImage, newWidth,
        newHeight, true);
      // if successful, fix up all the tracking variables
      if (newbits != null) {
        if (mImage!=mOriginal) {
          mImage.recycle();
        }
        mImage = newbits;
        mExpandWidth=newWidth;
        mExpandHeight=newHeight;
        mResizeFactorX = ((float) newWidth / mImageWidth);
        mResizeFactorY = ((float) newHeight / mImageHeight);

        mRightBound = mExpandWidth>mViewWidth ? 0 - (mExpandWidth - mViewWidth) : 0;
        mBottomBound = mExpandHeight>mViewHeight ? 0 - (mExpandHeight - mViewHeight) : 0;
      }
    }
  }

  void resizeBitmap( int amount ) {
    int adjustWidth = amount;
    int adjustHeight = (int)(adjustWidth / mAspect);
    scaleBitmap( mExpandWidth+adjustWidth, mExpandHeight+adjustHeight);
  }

  /**
   * watch for screen size changes and reset the background image
   */
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    // save device height width, we use it a lot of places
    mViewHeight = h;
    mViewWidth = w;

    // fix up the image
    setInitialImageBounds();
  }

  private int getPreferredSize() {
    return 300;
  }

  /**
   * the onDraw routine when we are using a background image
   *
   * @param canvas
   */
  protected void drawMap(Canvas canvas)
  {
    canvas.save();
    if (mImage != null)
    {
      if (!mImage.isRecycled())
      {
        canvas.drawBitmap(mImage, mScrollLeft, mScrollTop, null);
      }
    }
    canvas.restore();
  }

  protected void drawBubbles(Canvas canvas)
  {
    for (int i = 0; i < mBubbleMap.size(); i++)
    {
      int key = mBubbleMap.keyAt(i);
      Bubble b = mBubbleMap.get(key);
      if (b != null)
      {
        b.onDraw(canvas);
      }
    }
  }

  protected void drawLocations(Canvas canvas)
  {
    for (Area a : mAreaList)
    {
      a.onDraw(canvas);
    }
  }

  /**
   * Paint the view
   *   image first, location decorations next, bubbles on top
   */
  @Override
  protected void onDraw(Canvas canvas)
  {
    drawMap(canvas);
    drawLocations(canvas);
    drawBubbles(canvas);
  }

  /*
   * Touch handler
   *   This handler manages an arbitrary number of points
   *   and detects taps, moves, flings, and zooms
   */
  public boolean onTouchEvent(@NotNull MotionEvent ev)
  {
    int id;

    if (mVelocityTracker == null)
    {
      mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    final int action = ev.getAction();

    int pointerCount = ev.getPointerCount();
    int index = 0;

    if (pointerCount > 1) {
      // If you are using new API (level 8+) use these constants
      // instead as they are much better names
      index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK);
      index = index >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

      // for api 7 and earlier we are stuck with these
      // constants which are poorly named
      // ID refers to INDEX, not the actual ID of the pointer
      // index = (action & MotionEvent.ACTION_POINTER_ID_MASK);
      // index = index >> MotionEvent.ACTION_POINTER_ID_SHIFT;
    }

    switch (action & MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
        // Clear all touch points
        // In the case where some view up chain is messing with our
        // touch events, it is possible to miss UP and POINTER_UP
        // events.  Whenever ACTION_DOWN happens, it is intended
        // to always be the first touch, so we will drop tracking
        // for any points that may have been orphaned
        for ( TouchPoint t: mTouchPoints.values() ) {
          onLostTouch(t.getTrackingPointer());
        }
        // fall through planned
      case MotionEvent.ACTION_POINTER_DOWN:
        id = ev.getPointerId(index);
        onTouchDown(id,ev.getX(index),ev.getY(index));
        break;

      case MotionEvent.ACTION_MOVE:
        for (int p=0;p<pointerCount;p++) {
          id = ev.getPointerId(p);
          TouchPoint t = mTouchPoints.get(id);
          if (t!=null) {
            onTouchMove(t,ev.getX(p),ev.getY(p));
          }
          // after all moves, check to see if we need
          // to process a zoom
          processZoom();
        }
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_POINTER_UP:
        id = ev.getPointerId(index);
        onTouchUp(id);
        break;
      case MotionEvent.ACTION_CANCEL:
        // Clear all touch points on ACTION_CANCEL
        // according to the google devs, CANCEL means cancel
        // tracking every touch.
        // cf: http://groups.google.com/group/android-developers/browse_thread/thread/8b14591ead5608a0/ad711bf24520e5c4?pli=1
        for ( TouchPoint t: mTouchPoints.values() ) {
          onLostTouch(t.getTrackingPointer());
        }
        // let go of the velocity tracker per API Docs
        if (mVelocityTracker != null) {
          mVelocityTracker.recycle();
          mVelocityTracker = null;
        }
        break;
    }
    return true;
  }


  void onTouchDown(int id, float x, float y) {
    // create a new touch point to track this ID
    TouchPoint t=null;
    synchronized (mTouchPoints) {
      // This test is a bit paranoid and research should
      // be done sot that it can be removed.  We should
      // not find a touch point for the id
      t = mTouchPoints.get(id);
      if (t == null) {
        t = new TouchPoint(id);
        mTouchPoints.put(id,t);
      }

      // for pinch zoom, we need to pick two touch points
      // they will be called Main and Pinch
      if (mMainTouch == null) {
        mMainTouch = t;
      } else {
        if (mPinchTouch == null) {
          mPinchTouch=t;
          // second point established, set up to
          // handle pinch zoom
          startZoom();
        }
      }
    }
    t.setPosition(x,y);
  }

  /*
   * Track pointer moves
   */
  void onTouchMove(TouchPoint t, float x, float y) {
    // mMainTouch will drag the view, be part of a
    // pinch zoom, or trigger a tap
    if (t == mMainTouch) {
      if (mPinchTouch == null) {
        // only on point down, this is a move
        final int deltaX = (int) (t.getX() - x);
        final int xDiff = (int) Math.abs(t.getX() - x);

        final int deltaY = (int) (t.getY() - y);
        final int yDiff = (int) Math.abs(t.getY() - y);

        if (!mIsBeingDragged) {
          if ((xDiff > mTouchSlop) || (yDiff > mTouchSlop)) {
            // start dragging about once the user has
            // moved the point far enough
            mIsBeingDragged = true;
          }
        } else {
          // being dragged, move the image
          if (xDiff > 0) {
            moveX(-deltaX);
          }
          if (yDiff > 0) {
            moveY(-deltaY);
          }
          t.setPosition(x, y);
        }
      } else {
        // two fingers down means zoom
        t.setPosition(x, y);
        onZoom();
      }
    } else {
      if (t == mPinchTouch) {
        // two fingers down means zoom
        t.setPosition(x, y);
        onZoom();
      }
    }
  }

  /*
   * touch point released
   */
  void onTouchUp(int id) {
    synchronized (mTouchPoints) {
      TouchPoint t = mTouchPoints.get(id);
      if (t != null) {
        if (t == mMainTouch) {
          if (mPinchTouch==null) {
            // This is either a fling or tap
            if (mIsBeingDragged) {
              // view was being dragged means this is a fling
              final VelocityTracker velocityTracker = mVelocityTracker;
              velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

              int xVelocity = (int) velocityTracker.getXVelocity();
              int yVelocity = (int) velocityTracker.getYVelocity();

              int xfling = Math.abs(xVelocity) > mMinimumVelocity ? xVelocity
                : 0;
              int yfling = Math.abs(yVelocity) > mMinimumVelocity ? yVelocity
                : 0;

              if ((xfling != 0) || (yfling != 0)) {
                fling(-xfling, -yfling);
              }

              mIsBeingDragged = false;

              // let go of the velocity tracker
              if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
              }
            } else {
              // no movement - this was a tap
              onScreenTapped((int)mMainTouch.getX(), (int)mMainTouch.getY());
            }
          }
          mMainTouch=null;
          mZoomEstablished=false;
        }
        if (t == mPinchTouch) {
          // lost the 2nd pointer
          mPinchTouch=null;
          mZoomEstablished=false;
        }
        mTouchPoints.remove(id);
        // shuffle remaining pointers so that we are still
        // tracking.  This is necessary for proper action
        // on devices that support > 2 touches
        regroupTouches();
      } else {
        // lost this ID somehow
        // This happens sometimes due to the way some
        // devices manage touch
      }
    }
  }

  /*
   * Touch handling varies from device to device, we may think we
   * are tracking an id which goes missing
   */
  void onLostTouch(int id) {
    synchronized (mTouchPoints) {
      TouchPoint t = mTouchPoints.get(id);
      if (t != null) {
        if (t == mMainTouch) {
          mMainTouch=null;
        }
        if (t == mPinchTouch) {
          mPinchTouch=null;
        }
        mTouchPoints.remove(id);
        regroupTouches();
      }
    }
  }

  /*
   * find a touch pointer that is not being used as main or pinch
   */
  TouchPoint getUnboundPoint() {
    TouchPoint ret=null;
    for (Integer i : mTouchPoints.keySet()) {
      TouchPoint p = mTouchPoints.get(i);
      if ((p!=mMainTouch)&&(p!=mPinchTouch)) {
        ret = p;
        break;
      }
    }
    return ret;
  }

  /*
   * go through remaining pointers and try to have
   * MainTouch and then PinchTouch if possible
   */
  void regroupTouches() {
    int s=mTouchPoints.size();
    if (s>0) {
      if (mMainTouch == null) {
        if (mPinchTouch != null) {
          mMainTouch=mPinchTouch;
          mPinchTouch=null;
        } else {
          mMainTouch=getUnboundPoint();
        }
      }
      if (s>1) {
        if (mPinchTouch == null) {
          mPinchTouch=getUnboundPoint();
          startZoom();
        }
      }
    }
  }

  /*
   * Called when the second pointer is down indicating that we
   * want to do a pinch-zoom action
   */
  void startZoom() {
    // this boolean tells the system that it needs to
    // initialize itself before trying to zoom
    // This is cleaner than duplicating code
    // see processZoom
    mZoomEstablished=false;
  }

  /*
   * one of the pointers for our pinch-zoom action has moved
   * Remember this until after all touch move actions are processed.
   */
  void onZoom() {
    mZoomPending=true;
  }

  /*
   * All touch move actions are done, do we need to zoom?
   */
  void processZoom() {
    if (mZoomPending) {
      // check pinch distance, set new scale factor
      float dx=mMainTouch.getX()-mPinchTouch.getX();
      float dy=mMainTouch.getY()-mPinchTouch.getY();
      float newDistance=(float)Math.sqrt((dx*dx)+(dy*dy));
      if (mZoomEstablished) {
        // baseline was set, check to see if there is enough
        // movement to resize
        int distanceChange=(int)(newDistance-mInitialDistance);
        int delta=distanceChange-mLastDistanceChange;
        if (Math.abs(delta)>mTouchSlop) {
          mLastDistanceChange=distanceChange;
          resizeBitmap(delta);
          invalidate();
        }
      } else {
        // first run through after touches established
        // just set baseline
        mLastDistanceChange=0;
        mInitialDistance=newDistance;
        mZoomEstablished=true;
      }
      mZoomPending=false;
    }
  }

  /*
   * Screen tapped x, y is screen coord from upper left and does not account
   * for scroll
   */
  void onScreenTapped(int x, int y)
  {
    boolean missed = true;
    boolean bubble = false;
    // adjust for scroll
    int testx = x-mScrollLeft;
    int testy = y-mScrollTop;

    /*
      Empirically, this works, but it's not guaranteed to be correct.
      Seems that we need to divide by densityFactor only if the picture is larger than the screen.
      When it is smaller than the screen, we don't need to do that.

      TODO: investigate this in detail.
     */
    if (mResizeFactorX > 1)
    {
      testx = (int)(((float)testx/mResizeFactorX));
    }
    else
    {
      testx = (int)(((float)testx/mResizeFactorX)/densityFactor);
    }
    if (mResizeFactorY > 1)
    {
      testy = (int)(((float)testy/mResizeFactorY));
    }
    else
    {
      testy = (int)(((float)testy/mResizeFactorY)/densityFactor);
    }

    // check if bubble tapped first
    // in case a bubble covers an area we want it to
    // have precedent
    for (int i = 0 ; i < mBubbleMap.size() ; i++)
    {
      int key = mBubbleMap.keyAt(i);
      Bubble b = mBubbleMap.get(key);
      //it can still be null if there are no bubbles at all
      if (b != null)
      {
        if (b.isInArea((float)x-mScrollLeft,(float)y-mScrollTop))
        {
          b.onTapped();
          bubble=true;
          missed=false;
          // only fire tapped for one bubble
          break;
        }
      }
    }

    if (!bubble)
    {
      // then check for area taps
      for (Area a : mAreaList)
      {
        if (a.isInArea((float)testx,(float)testy))
        {
          if (mCallbackList != null) {
            for (OnImageMapClickedHandler h : mCallbackList)
            {
              h.onImageMapClicked(a.getId(), this);
            }
          }
          missed=false;
          // only fire clicked for one area
          break;
        }
      }
    }

    if (missed)
    {
      // managed to miss everything, clear bubbles
      mBubbleMap.clear();
      invalidate();
    }
  }

  // process a fling by kicking off the scroller
  public void fling(int velocityX, int velocityY)
  {
    int startX = mScrollLeft;
    int startY = mScrollTop;

    mScroller.fling(startX, startY, -velocityX, -velocityY, mRightBound, 0,
      mBottomBound, 0);

    invalidate();
  }

  /*
   * move the view to this x, y
   */
  public void moveTo(int x, int y) {
    mScrollLeft = x;
    if (mScrollLeft > 0) {
      mScrollLeft = 0;
    }
    if (mScrollLeft < mRightBound) {
      mScrollLeft = mRightBound;
    }
    mScrollTop=y;
    if (mScrollTop > 0) {
      mScrollTop = 0;
    }
    if (mScrollTop < mBottomBound) {
      mScrollTop = mBottomBound;
    }
    invalidate();
  }

  /*
   * move the view by this delta in X direction
   */
  public void moveX(int deltaX) {
    mScrollLeft = mScrollLeft + deltaX;
    if (mScrollLeft > 0) {
      mScrollLeft = 0;
    }
    if (mScrollLeft < mRightBound) {
      mScrollLeft = mRightBound;
    }
    invalidate();
  }

  /*
   * move the view by this delta in Y direction
   */
  public void moveY(int deltaY) {
    mScrollTop = mScrollTop + deltaY;
    if (mScrollTop > 0) {
      mScrollTop = 0;
    }
    if (mScrollTop < mBottomBound) {
      mScrollTop = mBottomBound;
    }
    invalidate();
  }

  /*
   * A class to track touches
   */
  class TouchPoint {
    int _id;
    float _x;
    float _y;

    TouchPoint(int id) {
      _id=id;
      _x=0f;
      _y=0f;
    }
    int getTrackingPointer() {
      return _id;
    }
    void setPosition(float x, float y) {
      if ((_x != x) || (_y != y)) {
        _x=x;
        _y=y;
      }
    }
    float getX() {
      return _x;
    }
    float getY() {
      return _y;
    }
  }


  /*
   * on clicked handler add/remove support
   */
  public void addOnImageMapClickedHandler( OnImageMapClickedHandler h ) {
    if (h != null) {
      if (mCallbackList == null) {
        mCallbackList = new ArrayList<OnImageMapClickedHandler>();
      }
      mCallbackList.add(h);
    }
  }

  public void removeOnImageMapClickedHandler( OnImageMapClickedHandler h ) {
    if (mCallbackList != null) {
      if (h != null) {
        mCallbackList.remove(h);
      }
    }
  }

        /*
         * Begin map area support
         */
  /**
   *  Area is abstract Base for tappable map areas
   *   descendants provide hit test and focal point
   */
  abstract class Area {
    int _id;
    String _name;
    HashMap<String,String> _values;
    Bitmap _decoration=null;

    public Area(int id, String name) {
      _id = id;
      if (name != null) {
        _name = name;
      }
    }

    public int getId() {
      return _id;
    }

    public String getName() {
      return _name;
    }

    // all xml values for the area are passed to the object
    // the default impl just puts them into a hashmap for
    // retrieval later
    public void addValue(String key, String value) {
      if (_values == null) {
        _values = new HashMap<String,String>();
      }
      _values.put(key, value);
    }

    public String getValue(String key) {
      String value=null;
      if (_values!=null) {
        value=_values.get(key);
      }
      return value;
    }

    // a method for setting a simple decorator for the area
    public void setBitmap(Bitmap b) {
      _decoration = b;
    }

    // an onDraw is set up to provide an extensible way to
    // decorate an area.  When drawing remember to take the
    // scaling and translation into account
    public void onDraw(Canvas canvas)
    {
      if (_decoration != null)
      {
        float x = (getOriginX() * mResizeFactorX) + mScrollLeft - 17;
        float y = (getOriginY() * mResizeFactorY) + mScrollTop - 17;
        canvas.drawBitmap(_decoration, x, y, null);
      }
    }

    abstract boolean isInArea(float x, float y);
    abstract float getOriginX();
    abstract float getOriginY();
  }

  /**
   * Rectangle Area
   */
  class RectArea extends Area {
    float _left;
    float _top;
    float _right;
    float _bottom;


    RectArea(int id, String name, float left, float top, float right, float bottom) {
      super(id,name);
      _left = left;
      _top = top;
      _right = right;
      _bottom = bottom;
    }

    public boolean isInArea(float x, float y) {
      boolean ret = false;
      if ((x > _left) && (x < _right)) {
        if ((y > _top) && (y < _bottom)) {
          ret = true;
        }
      }
      return ret;
    }

    public float getOriginX() {
      return _left;
    }

    public float getOriginY() {
      return _top;
    }
  }

  /**
   * Polygon area
   */
  class PolyArea extends Area {
    ArrayList<Integer> xpoints = new ArrayList<Integer>();
    ArrayList<Integer> ypoints = new ArrayList<Integer>();

    // centroid point for this poly
    float _x;
    float _y;

    // number of points (don't rely on array size)
    int _points;

    // bounding box
    int top=-1;
    int bottom=-1;
    int left=-1;
    int right=-1;

    public PolyArea(int id, String name, String coords) {
      super(id,name);

      // split the list of coordinates into points of the
      // polygon and compute a bounding box
      String[] v = coords.split(",");

      int i=0;
      while ((i+1)<v.length) {
        int x = Integer.parseInt(v[i]);
        int y = Integer.parseInt(v[i+1]);
        xpoints.add(x);
        ypoints.add(y);
        top=(top==-1)?y:Math.min(top,y);
        bottom=(bottom==-1)?y:Math.max(bottom,y);
        left=(left==-1)?x:Math.min(left,x);
        right=(right==-1)?x:Math.max(right,x);
        i+=2;
      }
      _points=xpoints.size();

      // add point zero to the end to make
      // computing area and centroid easier
      xpoints.add(xpoints.get(0));
      ypoints.add(ypoints.get(0));

      computeCentroid();
    }

    /**
     * area() and computeCentroid() are adapted from the implementation
     * of polygon.java  published from a princeton case study
     * The study is here: http://introcs.cs.princeton.edu/java/35purple/
     * The polygon.java source is here: http://introcs.cs.princeton.edu/java/35purple/Polygon.java.html
     */

    // return area of polygon
    public double area() {
      double sum = 0.0;
      for (int i = 0; i < _points; i++) {
        sum = sum + (xpoints.get(i) * ypoints.get(i+1)) - (ypoints.get(i) * xpoints.get(i+1));
      }
      sum = 0.5 * sum;
      return Math.abs(sum);
    }

    // compute the centroid of the polygon
    public void computeCentroid() {
      double cx = 0.0, cy = 0.0;
      for (int i = 0; i < _points; i++) {
        cx = cx + (xpoints.get(i) + xpoints.get(i+1)) * (ypoints.get(i) * xpoints.get(i+1) - xpoints.get(i) * ypoints.get(i+1));
        cy = cy + (ypoints.get(i) + ypoints.get(i+1)) * (ypoints.get(i) * xpoints.get(i+1) - xpoints.get(i) * ypoints.get(i+1));
      }
      cx /= (6 * area());
      cy /= (6 * area());
      _x=Math.abs((int)cx);
      _y=Math.abs((int)cy);
    }


    @Override
    public float getOriginX() {
      return _x;
    }

    @Override
    public float getOriginY() {
      return _y;
    }

    /**
     * This is a java port of the
     * W. Randolph Franklin algorithm explained here
     * http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
     */
    @Override
    public boolean isInArea(float testx, float testy)
    {
      int i, j;
      boolean c = false;
      for (i = 0, j = _points-1; i < _points; j = i++) {
        if ( ((ypoints.get(i)>testy) != (ypoints.get(j)>testy)) &&
          (testx < (xpoints.get(j)-xpoints.get(i)) * (testy-ypoints.get(i)) / (ypoints.get(j)-ypoints.get(i)) + xpoints.get(i)) )
          c = !c;
      }
      return c;
    }

    // For debugging maps, it is occasionally helpful to see the
    // bounding box for the polygons
                /*
                @Override
                public void onDraw(Canvas canvas) {
                    // draw the bounding box
                        canvas.drawRect(left * mResizeFactorX + mScrollLeft,
                                                top * mResizeFactorY + mScrollTop,
                                                right * mResizeFactorX + mScrollLeft,
                                                bottom * mResizeFactorY + mScrollTop,
                                                textOutlinePaint);
                }
                */
  }

  /**
   * Circle Area
   */
  class CircleArea extends Area {
    float _x;
    float _y;
    float _radius;

    CircleArea(int id, String name, float x, float y, float radius) {
      super(id,name);
      _x = x;
      _y = y;
      _radius = radius;

    }

    public boolean isInArea(float x, float y) {
      boolean ret = false;

      float dx = _x-x;
      float dy = _y-y;

      // if tap is less than radius distance from the center
      float d = (float)Math.sqrt((dx*dx)+(dy*dy));
      if (d<_radius) {
        ret = true;
      }

      return ret;
    }

    public float getOriginX() {
      return _x;
    }

    public float getOriginY() {
      return _y;
    }
  }

  /**
   * information bubble class
   */
  class Bubble
  {
    Area _a;
    String _text;
    float _x;
    float _y;
    int _h;
    int _w;
    int _baseline;
    float _top;
    float _left;

    Bubble(String text, float x, float y)
    {
      init(text,x,y);
    }

    Bubble(String text, int areaId)
    {
      _a = mIdToArea.get(areaId);
      if (_a != null) {
        float x = _a.getOriginX();
        float y = _a.getOriginY();
        init(text,x,y);
      }
    }

    void init(String text, float x, float y)
    {
      _text = text;
      _x = x*mResizeFactorX;
      _y = y*mResizeFactorY;
      Rect bounds = new Rect();
      textPaint.setTextScaleX(1.0f);
      textPaint.getTextBounds(text, 0, _text.length(), bounds);
      _h = bounds.bottom-bounds.top+20;
      _w = bounds.right-bounds.left+20;

      if (_w>mViewWidth) {
        // too long for the display width...need to scale down
        float newscale=((float)mViewWidth/(float)_w);
        textPaint.setTextScaleX(newscale);
        textPaint.getTextBounds(text, 0, _text.length(), bounds);
        _h = bounds.bottom-bounds.top+20;
        _w = bounds.right-bounds.left+20;
      }

      _baseline = _h-bounds.bottom;
      _left = _x - (_w/2);
      _top = _y - _h - 30;

      // try to keep the bubble on screen
      if (_left < 0) {
        _left = 0;
      }
      if ((_left + _w) > mExpandWidth) {
        _left = mExpandWidth - _w;
      }
      if (_top < 0) {
        _top = _y + 20;
      }
    }

    public boolean isInArea(float x, float y) {
      boolean ret = false;

      if ((x>_left) && (x<(_left+_w))) {
        if ((y>_top)&&(y<(_top+_h))) {
          ret = true;
        }
      }

      return ret;
    }

    void onDraw(Canvas canvas)
    {
      if (_a != null) {
        // Draw a shadow of the bubble
        float l = _left + mScrollLeft + 4;
        float t = _top + mScrollTop + 4;
        canvas.drawRoundRect(new RectF(l,t,l+_w,t+_h), 20.0f, 20.0f, bubbleShadowPaint);
        Path path = new Path();
        float ox=_x+ mScrollLeft+ 1;
        float oy=_y+mScrollTop+ 1;
        int yoffset=-35;
        if (_top > _y) {
          yoffset=35;
        }
        // draw shadow of pointer to origin
        path.moveTo(ox,oy);
        path.lineTo(ox-5,oy+yoffset);
        path.lineTo(ox+5+4,oy+yoffset);
        path.lineTo(ox, oy);
        path.close();
        canvas.drawPath(path, bubbleShadowPaint);

        // draw the bubble
        l = _left + mScrollLeft;
        t = _top + mScrollTop;
        canvas.drawRoundRect(new RectF(l,t,l+_w,t+_h), 20.0f, 20.0f, bubblePaint);
        path = new Path();
        ox=_x+ mScrollLeft;
        oy=_y+mScrollTop;
        yoffset=-35;
        if (_top > _y)
        {
          yoffset=35;
        }
        // draw pointer to origin
        path.moveTo(ox,oy);
        path.lineTo(ox-5,oy+yoffset);
        path.lineTo(ox+5,oy+yoffset);
        path.lineTo(ox, oy);
        path.close();
        canvas.drawPath(path, bubblePaint);

        // draw the message
        canvas.drawText(_text,l+(_w/2),t+_baseline-10,textPaint);
      }
    }

    void onTapped() {
      // bubble was tapped, notify listeners
      if (mCallbackList != null) {
        for (OnImageMapClickedHandler h : mCallbackList) {
          h.onBubbleClicked(_a.getId());
        }
      }
    }
  }

  /**
   * Map tapped callback interface
   */
  public interface OnImageMapClickedHandler
  {
    /**
     * Area with 'id' has been tapped
     * @param id
     */
    void onImageMapClicked(int id, ImageMap imageMap);
    /**
     * Info bubble associated with area 'id' has been tapped
     * @param id
     */
    void onBubbleClicked(int id);
  }

  /*
  * Misc getters
  * TODO: setters for there?
  */

  public float getmMaxSize()
  {
    return mMaxSize;
  }

  public boolean ismScaleFromOriginal()
  {
    return mScaleFromOriginal;
  }

  public boolean ismFitImageToScreen()
  {
    return mFitImageToScreen;
  }
}




Java Source Code List

com.ctc.android.widget.BitmapHelper.java
com.ctc.android.widget.ImageMapTestActivity.java
com.ctc.android.widget.ImageMap.java
com.licubeclub.zionhs.Appinfo.java
com.licubeclub.zionhs.Doc_Contributors.java
com.licubeclub.zionhs.Doc_Copying.java
com.licubeclub.zionhs.Doc_Notices.java
com.licubeclub.zionhs.Doc_Readme.java
com.licubeclub.zionhs.DrawerListAdapter.java
com.licubeclub.zionhs.ListCalendarAdapter.java
com.licubeclub.zionhs.MainActivity.java
com.licubeclub.zionhs.MealActivity3.java
com.licubeclub.zionhs.MealLoadHelper.java
com.licubeclub.zionhs.Notices_Parents.java
com.licubeclub.zionhs.Notices.java
com.licubeclub.zionhs.PostListAdapter.java
com.licubeclub.zionhs.Schedule.java
com.licubeclub.zionhs.Schoolinfo.java
com.licubeclub.zionhs.Schoolintro.java
com.licubeclub.zionhs.WebViewActivity.java
com.licubeclub.zionhs.meal.FridayMeal.java
com.licubeclub.zionhs.meal.MondayMeal.java
com.licubeclub.zionhs.meal.ThursdayMeal.java
com.licubeclub.zionhs.meal.TuesdayMeal.java
com.licubeclub.zionhs.meal.WednsdayMeal.java