/**
* Filename: ZoomAndScrollImageView.java (in org.repin.android.ui.mapview)
* This file is part of the Redpin project.
*
* Redpin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Redpin is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Redpin. If not, see <http://www.gnu.org/licenses/>.
*
* (c) Copyright ETH Zurich, Pascal Brogle, Philipp Bolliger, 2010, ALL RIGHTS RESERVED.
*
* www.redpin.org
*/
package org.redpin.android.ui.mapview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Picture;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.Scroller;
import android.widget.ZoomButtonsController;
import android.widget.ZoomButtonsController.OnZoomListener;
/**
* ImageView that is capable of zooming and scrolling an image.
*
* @author Pascal Brogle (broglep@student.ethz.ch)
*
*/
public class ZoomAndScrollImageView extends View implements OnZoomListener,
OnDoubleTapListener, OnGestureListener, OnScaleGestureListener {
private static final String TAG = ZoomAndScrollImageView.class
.getSimpleName();
private static final float ZOOM_STEP = 0.5f;
private static final float DPAD_MOVEMENT_STEP = 20;
static float MAX_ZOOM = 4.0f;
static float MIN_ZOOM = 1f;
float scale = 1.0f;
private Scroller scroller;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleGestureDetector;
private ZoomButtonsController zoomController;
private Matrix matrix;
private float currentX;
private float currentY;
private float contentWidth;
private float contentHeight;
private ZoomAndScrollViewListener listener;
private static float[] ORIGIN = new float[] { 0, 0 };
private float[] destination;
private Picture picture;
/**
* Construct a new ZoomAndScrollImageView with layout parameters and a
* default style.
*
* @param context
* A Context object used to access application assets.
* @param attrs
* An AttributeSet passed to our parent.
* @param defStyle
* The default style resource ID.
*/
public ZoomAndScrollImageView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init(context);
}
/**
* Construct a new ZoomAndScrollImageView with layout parameters.
*
* @param context
* A Context object used to access application assets.
* @param attrs
* An AttributeSet passed to our parent.
*/
public ZoomAndScrollImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* Construct a new ZoomAndScrollImageView with a Context object.
*
* @param context
* A Context object used to access application assets.
*/
public ZoomAndScrollImageView(Context context) {
super(context);
init(context);
}
/**
* Initializes the view
*
* @param context
* {@link Context}
*/
private void init(Context context) {
setFocusable(true);
scroller = new Scroller(context);
gestureDetector = new GestureDetector(context, this);
scaleGestureDetector = new ScaleGestureDetector(context, this);
zoomController = new ZoomButtonsController(this);
zoomController.setOnZoomListener(this);
matrix = new Matrix();
destination = new float[2];
// setVerticalScrollBarEnabled(true);
// setHorizontalScrollBarEnabled(true);
}
/**
* Displays a bitmap
*
* @param bitmap
* {@link Bitmap}
*/
public void setImageBitmap(Bitmap bitmap) {
setZoom(1.0f, false);
setContentSize(bitmap.getWidth(), bitmap.getHeight());
picture = new Picture();
Canvas c = picture
.beginRecording(bitmap.getWidth(), bitmap.getHeight());
c.drawBitmap(bitmap, 0, 0, null);
picture.endRecording();
}
/**
* Displays a drawable
*
* @param bDrawable
* {@link BitmapDrawable}
*/
public void setImageDrawable(BitmapDrawable bDrawable) {
setImageBitmap(bDrawable.getBitmap());
}
/**
* Adjusts minimal zoom level depending on the image size
*/
public void adjustMinZoom() {
int w = getWidth();
int h = getHeight();
if (w == 0 || h == 0) {
return;
}
MIN_ZOOM = Math.max(w / contentWidth, h / contentHeight);
}
/**
* Sets the image content size
*
* @param width
* Image width
* @param height
* Image height
*/
public void setContentSize(int width, int height) {
contentWidth = width;
contentHeight = height;
adjustMinZoom();
}
/**
*
* @return Current zoom scale
*/
public float getScale() {
return scale;
}
/**
*
* @param scale
* Desired zoom scale
*/
public void setScale(float scale) {
this.scale = scale;
}
/**
* Notifies the listener about the changed matrix
*
* @param m
* Changed matrix
*/
private void notifyMatrix(Matrix m) {
if (listener != null) {
listener.onMatrixChange(m, this);
}
}
/**
* {@inheritDoc}
*/
@Override
public void scrollBy(int x, int y) {
}
/**
* {@inheritDoc}
*/
@Override
public void scrollTo(int x, int y) {
currentX = -x;
currentY = -y;
currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));
currentY = Math.max(getHeight() - contentHeight, Math.min(0, currentY));
invalidate();
}
/**
*
* @return current x and y coordinate of the
*/
public float[] getCurrentXY() {
return new float[] { currentX, currentY };
}
/*
* {@link OnDoubleTapListener}
*/
private boolean zoomedIn = false;
@Override
public boolean onDoubleTap(MotionEvent e) {
float oldX, oldY;
oldX = currentX;
oldY = currentY;
currentX -= (e.getX() - getWidth() / 2) / scale;
currentY -= (e.getY() - getHeight() / 2) / scale;
currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));
zoomedIn = !zoomedIn;
changeZoom(zoomedIn ? 1 : -1, oldX, currentX, oldY, currentY);
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (listener != null) {
listener.onSingleTab(e);
}
return true;
}
/*
* {@link OnGestureListener}
*/
@Override
public boolean onDown(MotionEvent e) {
zoomController.setVisible(false);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
final float velocityFactor = 1.5f;
int minX = (int) (getWidth() - contentWidth);
int minY = (int) (getHeight() - contentHeight);
scroller.fling((int) currentX, (int) currentY,
(int) (velocityX / velocityFactor),
(int) (velocityY / velocityFactor), minX, 0, minY, 0);
return true;
}
@Override
public void onLongPress(MotionEvent e) {
zoomController.setVisible(true);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
currentX -= distanceX / scale;
currentY -= distanceY / scale;
currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));
currentY = Math.max(getHeight() - contentHeight, Math.min(0, currentY));
// notifyOnScroll();
invalidate();
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/*
* {@link OnZoomListener}
*/
@Override
public void onVisibilityChanged(boolean visible) {
}
@Override
public void onZoom(boolean zoomIn) {
float toX = currentX;
float toY = currentY;
changeZoom(zoomIn ? ZOOM_STEP : -ZOOM_STEP, currentX, toX, currentY,
toY);
}
public void changeZoom(float amount, float fromX, float toX, float fromY,
float toY) {
myTranslation.start(amount, fromX, toX, fromY, toY);
}
public void setZoom(float zoom, boolean adjust) {
Log.d(TAG, "Before: " + zoom + "(max: " + MAX_ZOOM + ",min: "
+ MIN_ZOOM + ")");
scale = zoom;
if (adjust) {
scale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale));
}
Log.d(TAG, "After: " + scale);
zoomController.setZoomInEnabled(scale != MAX_ZOOM);
zoomController.setZoomOutEnabled(scale != MIN_ZOOM);
invalidate();
}
/*
* {@link View}
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
zoomController.setVisible(false);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean h1 = scaleGestureDetector.onTouchEvent(ev);
boolean h2 = gestureDetector.onTouchEvent(ev);
return h1 || h2;
}
@Override
protected void onDraw(Canvas canvas) {
if (myTranslation != null && myTranslation.hasStarted()
&& !myTranslation.hasEnded()) {
myTranslation.getTransformation(AnimationUtils
.currentAnimationTimeMillis(), null);
}
int saveCount = canvas.save();
if (scroller.computeScrollOffset()) {
currentX = scroller.getCurrX();
currentY = scroller.getCurrY();
invalidate();
}
int width = getWidth();
int height = getHeight();
matrix.reset();
float scaledWidth = contentWidth * scale;
float scaledHeigth = contentHeight * scale;
float transX = scaledWidth > width ? currentX * scale
: (width - scaledWidth) / 2;
float transY = scaledHeigth > height ? currentY * scale
: (height - scaledHeigth) / 2;
matrix.preTranslate(transX, transY);
float pivotX = 0;
if (scaledWidth > width) {
pivotX = Math.max(Math.min(-currentX, width / 2), 2 * width
- contentWidth - currentX);
}
float pivotY = 0;
if (scaledHeigth > height) {
pivotY = Math.max(Math.min(-currentY, height / 2), 2 * height
- contentHeight - currentY);
}
matrix.preScale(scale, scale, pivotX, pivotY);
notifyMatrix(matrix);
canvas.concat(matrix);
if (picture != null) {
picture.draw(canvas);
}
canvas.restoreToCount(saveCount);
}
@Override
protected int computeHorizontalScrollExtent() {
return Math.round(computeHorizontalScrollRange() * getWidth()
/ (contentWidth * scale));
}
@Override
protected int computeHorizontalScrollOffset() {
matrix.mapPoints(destination, ORIGIN);
float x = -destination[0] / scale;
return Math.round(computeHorizontalScrollRange() * x / contentWidth);
}
@Override
protected int computeVerticalScrollExtent() {
return Math.round(computeVerticalScrollRange() * getHeight()
/ (contentHeight * scale));
}
@Override
protected int computeVerticalScrollOffset() {
matrix.mapPoints(destination, ORIGIN);
float y = -destination[1] / scale;
return Math.round(computeVerticalScrollRange() * y / contentHeight);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean handeled = false;
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
currentX += DPAD_MOVEMENT_STEP / scale;
currentX = Math.max(getWidth() - contentWidth, Math
.min(0, currentX));
invalidate();
handeled = true;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
currentX -= DPAD_MOVEMENT_STEP / scale;
currentX = Math.max(getWidth() - contentWidth, Math
.min(0, currentX));
invalidate();
handeled = true;
break;
case KeyEvent.KEYCODE_DPAD_UP:
currentY += DPAD_MOVEMENT_STEP / scale;
currentY = Math.max(getHeight() - contentHeight, Math.min(0,
currentY));
invalidate();
handeled = true;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
currentY -= DPAD_MOVEMENT_STEP / scale;
currentY = Math.max(getHeight() - contentHeight, Math.min(0,
currentY));
invalidate();
handeled = true;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
zoomedIn = !zoomedIn;
changeZoom(zoomedIn ? 1 : -1, currentX, currentX, currentX,
currentY);
handeled = true;
break;
default:
break;
}
return handeled;
}
@Override
public boolean onTrackballEvent(MotionEvent e) {
boolean handeled = false;
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
zoomedIn = !zoomedIn;
changeZoom(zoomedIn ? 1 : -1, currentX, currentX, currentX,
currentY);
handeled = true;
break;
case MotionEvent.ACTION_MOVE:
currentX -= e.getX() * DPAD_MOVEMENT_STEP / scale;
currentY -= e.getY() * DPAD_MOVEMENT_STEP / scale;
currentX = Math.max(getWidth() - contentWidth, Math
.min(0, currentX));
currentY = Math.max(getHeight() - contentHeight, Math.min(0,
currentY));
handeled = true;
invalidate();
break;
default:
break;
}
return handeled;
}
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
adjustMinZoom();
}
ZoomAndTranslate myTranslation = new ZoomAndTranslate();
/**
* Animation that zoom and translates to a given position
*
* @author Pascal Brogle (broglep@student.ethz.ch)
*
*/
class ZoomAndTranslate extends Animation {
private static final int DURATION = 1000;
private float mFrom;
private float mTo;
private Interpolator translateInterpolator;
private float fromX;
private float toX;
private float fromY;
private float toY;
private Interpolator zoomInterpolator;
public ZoomAndTranslate() {
setDuration(DURATION);
setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
notifyScaleBegin();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
notifyScaleEnd();
}
});
}
/**
* Starts the animation
*
* @param amount
* Zoom amount
* @param fromX
* From x coordinate
* @param toX
* To x coordinate
* @param fromY
* From y coordinate
* @param toY
* To y coordinate
*/
public void start(float amount, float fromX, float toX, float fromY,
float toY) {
this.fromX = fromX;
this.toX = toX;
this.fromY = fromY;
this.toY = toY;
translateInterpolator = new DecelerateInterpolator(); // new
// LinearInterpolator();//
// new
// DecelerateInterpolator();
zoomInterpolator = new AccelerateDecelerateInterpolator(); // new
// LinearInterpolator();//
// AccelerateDecelerateInterpolator();
mFrom = scale;
mTo = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale + amount));
start();
long t = AnimationUtils.currentAnimationTimeMillis();
getTransformation(t, null);
}
/**
* {@inheritDoc}
*/
@Override
protected void applyTransformation(float interpolatedTime,
Transformation t) {
float time = interpolatedTime;
float tInterpolatedTime = translateInterpolator
.getInterpolation(time);
float zInterpolatedTime = zoomInterpolator.getInterpolation(time);
currentX = fromX + (toX - fromX) * tInterpolatedTime;
currentY = fromY + (toY - fromY) * tInterpolatedTime;
setZoom(mFrom + (mTo - mFrom) * zInterpolatedTime, false);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
// Log.i(TAG, "onScale, factor:" + detector.getScaleFactor());
float factor = detector.getScaleFactor();
scale *= factor;
setZoom(scale, true);
Log.i(TAG, "onScale, " + scale + "(factor: +" + factor + ")");
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
notifyScaleBegin();
Log.i(TAG, "onScale Begin");
return true;
}
/**
* Notifies the beginning of the scaling to the listener
*/
private void notifyScaleBegin() {
if (listener != null) {
listener.onScaleBegin(this);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
notifyScaleEnd();
Log.i(TAG, "onScale End");
System.out.println("End scale: " + scale);
}
/**
* Notifies the ending of the scaling to the listener
*/
private void notifyScaleEnd() {
if (listener != null) {
listener.onScaleEnd(this);
}
}
public void setListener(ZoomAndScrollViewListener l) {
listener = l;
}
/**
* Listener-Interface for {@link ZoomAndScrollImageView}
*
* @author Pascal Brogle (broglep@student.ethz.ch)
*
*/
public interface ZoomAndScrollViewListener {
/**
* Called when a change in the drawing matrix occours
*
* @param m
* New matrix
* @param view
* View that calls the method
*/
public void onMatrixChange(Matrix m, ZoomAndScrollImageView view);
/**
* Called when the user begins to scale
*
* @param view
* View that calls the method
*/
public void onScaleBegin(ZoomAndScrollImageView view);
/**
* Called when the user ends scaling
*
* @param view
* View that calls the method
*/
public void onScaleEnd(ZoomAndScrollImageView view);
/**
* Called when the user tabs the view
*
* @param e
* MotionEvent
*/
public void onSingleTab(MotionEvent e);
}
}