package org.witness.informacam.app.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.widget.ImageView;
/**
* class Pinch Zoom & Swipe Image View.
* @author huewu.yang
* @date 2012. 08. 23
*/
public class PZSImageView extends ImageView {
private static final String TAG = "GalleryImageView"; //debug tag.
//wrapped motion event code.
protected static final int PZS_ACTION_INIT = 100;
protected static final int PZS_ACTION_SCALE = 1001;
protected static final int PZS_ACTION_TRANSLATE = 1002;
protected static final int PZS_ACTION_SCALE_TO_TRANSLATE = 1003;
protected static final int PZS_ACTION_TRANSLATE_TO_SCALE = 1004;
protected static final int PZS_ACTION_FIT_CENTER = 1005;
protected static final int PZS_ACTION_CANCEL = -1;
//TODO below 2 values should be able to set from attributes.
private final static float MAX_SCALE_TO_SCREEN = 2.f;
private final static float MIN_SCALE_TO_SCREEN = .5f;
private static final long DOUBLE_TAP_MARGIN_TIME = 200;
private static final float MIN_SCALE_SPAN = 10.f;
//calculated min / max scale ratio based on image & screen size.
private float mMinScaleFactor = 0.5f;
private float mMaxScaleFactor = 2.f;
private boolean mIsFirstDraw = true; //check flag to calculate necessary init values.
private int mImageWidth; //current set image width
private int mImageHeight; //current set image height
private boolean mHandleTouch = true;
/**
* constructor
* @param context
*/
public PZSImageView(Context context) {
super(context);
init();
}
/**
* constructor
* @param context
*/
public PZSImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* constructor
* @param context
*/
public PZSImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
//should use matrix scale type.
setScaleType(ScaleType.MATRIX);
Matrix mat = getImageMatrix();
mat.reset();
setImageMatrix(mat);
}
//TODO how to handle bitmaps that set as different ways. (by res or src attr)
//TODO in that case this view should be worked just as a normal image view.
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
mIsFirstDraw = true;
mImageWidth = bm.getWidth();
mImageHeight = bm.getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
if( mIsFirstDraw == true ){
mIsFirstDraw = false;
fitCenter();
calculateScaleFactorLimit();
}
setImageMatrix(mCurrentMatrix);
super.onDraw(canvas);
}
private void calculateScaleFactorLimit() {
//set max / min scale factor.
mMaxScaleFactor = Math.max( getHeight() * MAX_SCALE_TO_SCREEN / mImageHeight,
getWidth() * MAX_SCALE_TO_SCREEN / mImageWidth);
mMinScaleFactor = Math.min( getHeight() * MIN_SCALE_TO_SCREEN / mImageHeight,
getWidth() * MIN_SCALE_TO_SCREEN / mImageWidth);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mHandleTouch)
{
int action = parseMotionEvent(event);
switch(action){
case PZS_ACTION_INIT:
initGestureAction(event.getX(), event.getY());
break;
case PZS_ACTION_SCALE:
handleScale(event);
break;
case PZS_ACTION_TRANSLATE:
handleTranslate(event);
break;
case PZS_ACTION_TRANSLATE_TO_SCALE:
initGestureAction(event.getX(), event.getY());
break;
case PZS_ACTION_SCALE_TO_TRANSLATE:
int activeIndex = (event.getActionIndex() == 0 ? 1 : 0);
initGestureAction(event.getX(activeIndex), event.getY(activeIndex));
break;
case PZS_ACTION_FIT_CENTER:
fitCenter();
initGestureAction(event.getX(), event.getY());
break;
case PZS_ACTION_CANCEL:
break;
}
//check current position of bitmap.
validateMatrix();
updateMatrix();
return true;
}
else
return false;
}
public void setHandleTouch (boolean handleTouch)
{
mHandleTouch = handleTouch;
}
private int parseMotionEvent(MotionEvent ev) {
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
if( isDoubleTap(ev) )
return PZS_ACTION_FIT_CENTER;
else
return PZS_ACTION_INIT;
case MotionEvent.ACTION_POINTER_DOWN:
//more than one pointer is pressed...
return PZS_ACTION_TRANSLATE_TO_SCALE;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if( ev.getPointerCount() == 2 ){
return PZS_ACTION_SCALE_TO_TRANSLATE;
}else{
return PZS_ACTION_INIT;
}
case MotionEvent.ACTION_MOVE:
if( ev.getPointerCount() == 1 )
return PZS_ACTION_TRANSLATE;
else if( ev.getPointerCount() == 2 )
return PZS_ACTION_SCALE;
return 0;
}
return 0;
}
/////////////////////////////////////////////////
// Related matrix calculation stuffs.
/////////////////////////////////////////////////
private Matrix mCurrentMatrix = new Matrix();
private Matrix mSavedMatrix = new Matrix();
// Remember some things for zooming
private PointF mStartPoint = new PointF();
private PointF mMidPoint = new PointF();
private float mInitScaleSpan = 1f;
protected void initGestureAction(float x, float y) {
mSavedMatrix.set(mCurrentMatrix);
mStartPoint.set(x, y);
mInitScaleSpan = 0.f;
}
/**
* check user double tapped this view.. or not.
* @param current motion event.
* @return true if user double tapped this view.
*/
private long mLastTocuhDownTime = 0;
protected boolean isDoubleTap(MotionEvent ev){
//if old pointer is tapped?
if( ev.getPointerCount() > 1){
//if there are more than one pointer... reset
mLastTocuhDownTime = 0;
return false;
}
long downTime = ev.getDownTime();
long diff = downTime - mLastTocuhDownTime;
mLastTocuhDownTime = downTime;
return diff < DOUBLE_TAP_MARGIN_TIME;
}
protected boolean handleScale(MotionEvent event){
float newSpan = spacing(event);
//if two finger is too close, pointer index is bumped.. so just ignore it.
if( newSpan < MIN_SCALE_SPAN )
return false;
if( mInitScaleSpan == 0.f ){
//init values. scale gesture action is just started.
mInitScaleSpan = newSpan;
midPoint(mMidPoint, event);
}else{
float scale = normalizeScaleFactor(mSavedMatrix, newSpan, mInitScaleSpan);
mCurrentMatrix.set(mSavedMatrix);
mCurrentMatrix.postScale(scale, scale, mMidPoint.x, mMidPoint.y);
}
return true;
}
private float normalizeScaleFactor(Matrix curMat, float newSpan, float stdSpan) {
float values[] = new float[9];
curMat.getValues(values);
float scale = values[Matrix.MSCALE_X];
if( stdSpan == newSpan ){
return scale;
} else {
float newScaleFactor = newSpan / stdSpan;
float candinateScale = scale * newScaleFactor;
if( candinateScale > mMaxScaleFactor ){
return mMaxScaleFactor / scale;
}else if( candinateScale < mMinScaleFactor ){
return mMinScaleFactor / scale;
}else{
return newScaleFactor;
}
}
}
protected boolean handleTranslate(MotionEvent event){
float xDiff = event.getX() - mStartPoint.x;
float yDiff = event.getY() - mStartPoint.y;
mCurrentMatrix.set(mSavedMatrix);
mCurrentMatrix.postTranslate(xDiff,yDiff);
return (xDiff > 0.1 || yDiff > 0.1);
}
private RectF mTraslateLimitRect = new RectF(); //reuse instance.
private void validateMatrix(){
float values[] = new float[9];
mCurrentMatrix.getValues(values);
//get current matrix values.
float scale = values[Matrix.MSCALE_X];
float tranX = values[Matrix.MTRANS_X];
float tranY = values[Matrix.MTRANS_Y];
int imageHeight = (int) (scale * mImageHeight);
int imageWidth = (int) (scale * mImageWidth);
mTraslateLimitRect.setEmpty();
//don't think about optimize code. first, just write code case by case.
//check TOP & BOTTOM
if( imageHeight > getHeight() ){
//image height is taller than view
mTraslateLimitRect.top = getHeight() - imageHeight - getPaddingTop() - getPaddingBottom();
mTraslateLimitRect.bottom = 0.f;
}else{
mTraslateLimitRect.top = mTraslateLimitRect.bottom =
(getHeight() - imageHeight - getPaddingTop() - getPaddingBottom() ) / 2.f;
}
//check LEFT & RIGHT
if( imageWidth > getWidth() ){
//image width is longer than view
mTraslateLimitRect.left = getWidth() - imageWidth - getPaddingRight() - getPaddingLeft();
mTraslateLimitRect.right = 0.f;
}else{
mTraslateLimitRect.left = mTraslateLimitRect.right =
(getWidth() - imageWidth - getPaddingLeft() - getPaddingRight()) / 2.f;
}
float newTranX = tranX;
newTranX = Math.max(newTranX, mTraslateLimitRect.left);
newTranX = Math.min(newTranX, mTraslateLimitRect.right);
float newTranY = tranY;
newTranY = Math.max(newTranY, mTraslateLimitRect.top);
newTranY = Math.min(newTranY, mTraslateLimitRect.bottom);
values[Matrix.MTRANS_X] = newTranX;
values[Matrix.MTRANS_Y] = newTranY;
mCurrentMatrix.setValues(values);
if( mTraslateLimitRect.contains(tranX, tranY) == false ){
//set new start point.
mStartPoint.offset(tranX - newTranX, tranY - newTranY);
}
}
protected void updateMatrix(){
setImageMatrix(mCurrentMatrix);
}
protected void fitCenter(){
//move image to center....
mCurrentMatrix.reset();
float scaleX = (getWidth() - getPaddingLeft() - getPaddingRight()) / (float)mImageWidth;
float scaleY = (getHeight() - getPaddingTop() - getPaddingBottom()) / (float)mImageHeight;
float scale = Math.min(scaleX, scaleY);
float dx = (getWidth() - getPaddingLeft() - getPaddingRight() - mImageWidth * scale) / 2.f;
float dy = (getHeight() - getPaddingTop() - getPaddingBottom() - mImageHeight * scale) / 2.f;
mCurrentMatrix.postScale(scale, scale);
mCurrentMatrix.postTranslate(dx, dy);
setImageMatrix(mCurrentMatrix);
}
/** Determine the space between the first two fingers */
private float spacing(MotionEvent event) {
// ...
double x = event.getX(0) - event.getX(1);
double y = event.getY(0) - event.getY(1);
return (float)Math.sqrt(x * x + y * y);
}
/** Calculate the mid point of the first two fingers */
private void midPoint(PointF point, MotionEvent event) {
// ...
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
}//end of class