package com.aviary.android.feather.widget;
import it.sephiroth.android.library.imagezoom.ImageViewTouch;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
import com.aviary.android.feather.library.graphics.drawable.IBitmapDrawable;
import com.aviary.android.feather.library.utils.UIConfiguration;
// TODO: Auto-generated Javadoc
/**
* The Class CropImageView.
*/
public class CropImageView extends ImageViewTouch {
/**
* The listener interface for receiving onLayout events. The class that is interested in processing a onLayout event implements
* this interface, and the object created with that class is registered with a component using the component's
* <code>addOnLayoutListener<code> method. When
* the onLayout event occurs, that object's appropriate
* method is invoked.
*
* @see OnLayoutEvent
*/
public interface OnLayoutListener {
/**
* On layout changed.
*
* @param changed
* the changed
* @param left
* the left
* @param top
* the top
* @param right
* the right
* @param bottom
* the bottom
*/
void onLayoutChanged( boolean changed, int left, int top, int right, int bottom );
}
/**
* The listener interface for receiving onHighlightSingleTapUpConfirmed events. The class that is interested in processing a
* onHighlightSingleTapUpConfirmed event implements this interface, and the object created with that class is registered with a
* component using the component's <code>addOnHighlightSingleTapUpConfirmedListener<code> method. When
* the onHighlightSingleTapUpConfirmed event occurs, that object's appropriate
* method is invoked.
*
* @see OnHighlightSingleTapUpConfirmedEvent
*/
public interface OnHighlightSingleTapUpConfirmedListener {
/**
* On single tap up confirmed.
*/
void onSingleTapUpConfirmed();
}
/** The Constant GROW. */
public static final int GROW = 0;
/** The Constant SHRINK. */
public static final int SHRINK = 1;
/** The m motion edge. */
private int mMotionEdge = HighlightView.GROW_NONE;
/** The m highlight view. */
private HighlightView mHighlightView;
/** The m layout listener. */
private OnLayoutListener mLayoutListener;
/** The m highlight single tap up listener. */
private OnHighlightSingleTapUpConfirmedListener mHighlightSingleTapUpListener;
/** The m motion highlight view. */
private HighlightView mMotionHighlightView;
/** The m crop min size. */
private int mCropMinSize = 10;
protected Handler mHandler = new Handler();
/**
* Instantiates a new crop image view.
*
* @param context
* the context
* @param attrs
* the attrs
*/
public CropImageView( Context context, AttributeSet attrs ) {
super( context, attrs );
}
/**
* Sets the on highlight single tap up confirmed listener.
*
* @param listener
* the new on highlight single tap up confirmed listener
*/
public void setOnHighlightSingleTapUpConfirmedListener( OnHighlightSingleTapUpConfirmedListener listener ) {
mHighlightSingleTapUpListener = listener;
}
/**
* Sets the min crop size.
*
* @param value
* the new min crop size
*/
public void setMinCropSize( int value ) {
mCropMinSize = value;
if ( mHighlightView != null ) {
mHighlightView.setMinSize( value );
}
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouch#init()
*/
@Override
protected void init() {
super.init();
mGestureDetector = null;
mScaleDetector = null;
mGestureListener = null;
mScaleListener = null;
mScaleDetector = new ScaleGestureDetector( getContext(), new CropScaleListener() );
mGestureDetector = new GestureDetector( getContext(), new CropGestureListener(), null, true );
mGestureDetector.setIsLongpressEnabled( false );
// mTouchSlop = 20 * 20;
}
/**
* Sets the on layout listener.
*
* @param listener
* the new on layout listener
*/
public void setOnLayoutListener( OnLayoutListener listener ) {
mLayoutListener = listener;
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouchBase#setImageBitmap(android.graphics.Bitmap, boolean)
*/
@Override
public void setImageBitmap( Bitmap bitmap, boolean reset ) {
mMotionHighlightView = null;
super.setImageBitmap( bitmap, reset );
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouchBase#onLayout(boolean, int, int, int, int)
*/
@Override
protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
super.onLayout( changed, left, top, right, bottom );
if ( mLayoutListener != null ) mLayoutListener.onLayoutChanged( changed, left, top, right, bottom );
mHandler.post( onLayoutRunnable );
}
Runnable onLayoutRunnable = new Runnable() {
@Override
public void run() {
final Drawable drawable = getDrawable();
if ( drawable != null && ( (IBitmapDrawable) drawable ).getBitmap() != null ) {
if ( mHighlightView != null ) {
if ( mHighlightView.isRunning() ) {
mHandler.post( this );
} else {
// Log.d( LOG_TAG, "onLayoutRunnable.. running" );
mHighlightView.getMatrix().set( getImageMatrix() );
mHighlightView.invalidate();
}
}
}
}
};
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouchBase#postTranslate(float, float)
*/
@Override
protected void postTranslate( float deltaX, float deltaY ) {
super.postTranslate( deltaX, deltaY );
if ( mHighlightView != null ) {
if ( mHighlightView.isRunning() ) {
return;
}
if ( getScale() != 1 ) {
float[] mvalues = new float[9];
getImageMatrix().getValues( mvalues );
final float scale = mvalues[Matrix.MSCALE_X];
mHighlightView.getCropRectF().offset( -deltaX / scale, -deltaY / scale );
}
mHighlightView.getMatrix().set( getImageMatrix() );
mHighlightView.invalidate();
}
}
private Rect mRect1 = new Rect();
private Rect mRect2 = new Rect();
@Override
protected void postScale( float scale, float centerX, float centerY ) {
if ( mHighlightView != null ) {
if ( mHighlightView.isRunning() ) return;
RectF cropRect = mHighlightView.getCropRectF();
mHighlightView.getDisplayRect( getImageViewMatrix(), mHighlightView.getCropRectF(), mRect1 );
super.postScale( scale, centerX, centerY );
mHighlightView.getDisplayRect( getImageViewMatrix(), mHighlightView.getCropRectF(), mRect2 );
float[] mvalues = new float[9];
getImageViewMatrix().getValues( mvalues );
final float currentScale = mvalues[Matrix.MSCALE_X];
cropRect.offset( ( mRect1.left - mRect2.left ) / currentScale, ( mRect1.top - mRect2.top ) / currentScale );
cropRect.right += -( mRect2.width() - mRect1.width() ) / currentScale;
cropRect.bottom += -( mRect2.height() - mRect1.height() ) / currentScale;
mHighlightView.getMatrix().set( getImageMatrix() );
mHighlightView.getCropRectF().set( cropRect );
mHighlightView.invalidate();
} else {
super.postScale( scale, centerX, centerY );
}
}
/**
* Ensure visible.
*
* @param hv
* the hv
*/
private void ensureVisible( HighlightView hv ) {
Rect r = hv.getDrawRect();
int panDeltaX1 = Math.max( 0, getLeft() - r.left );
int panDeltaX2 = Math.min( 0, getRight() - r.right );
int panDeltaY1 = Math.max( 0, getTop() - r.top );
int panDeltaY2 = Math.min( 0, getBottom() - r.bottom );
int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
if ( panDeltaX != 0 || panDeltaY != 0 ) {
panBy( panDeltaX, panDeltaY );
}
}
/*
* (non-Javadoc)
*
* @see android.widget.ImageView#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw( Canvas canvas ) {
super.onDraw( canvas );
if ( mHighlightView != null ) mHighlightView.draw( canvas );
}
/**
* Sets the highlight view.
*
* @param hv
* the new highlight view
*/
public void setHighlightView( HighlightView hv ) {
if ( mHighlightView != null ) {
mHighlightView.dispose();
}
mMotionHighlightView = null;
mHighlightView = hv;
invalidate();
}
/**
* Gets the highlight view.
*
* @return the highlight view
*/
public HighlightView getHighlightView() {
return mHighlightView;
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouch#onTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onTouchEvent( MotionEvent event ) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
mScaleDetector.onTouchEvent( event );
if ( !mScaleDetector.isInProgress() ) mGestureDetector.onTouchEvent( event );
switch ( action ) {
case MotionEvent.ACTION_UP:
if ( mHighlightView != null ) {
mHighlightView.setMode( HighlightView.Mode.None );
}
mMotionHighlightView = null;
mMotionEdge = HighlightView.GROW_NONE;
if ( getScale() < 1f ) {
zoomTo( 1f, 50 );
}
break;
}
return true;
}
/**
* Distance.
*
* @param x2
* the x2
* @param y2
* the y2
* @param x1
* the x1
* @param y1
* the y1
* @return the float
*/
@SuppressLint("FloatMath")
static float distance( float x2, float y2, float x1, float y1 ) {
return (float) Math.sqrt( Math.pow( x2 - x1, 2 ) + Math.pow( y2 - y1, 2 ) );
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouch#onDoubleTapPost(float, float)
*/
@Override
protected float onDoubleTapPost( float scale, float maxZoom ) {
return super.onDoubleTapPost( scale, maxZoom );
}
/**
* The listener interface for receiving cropGesture events. The class that is interested in processing a cropGesture event
* implements this interface, and the object created with that class is registered with a component using the component's
* <code>addCropGestureListener<code> method. When
* the cropGesture event occurs, that object's appropriate
* method is invoked.
*
* @see CropGestureEvent
*/
class CropGestureListener extends GestureDetector.SimpleOnGestureListener {
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onDown(android.view.MotionEvent)
*/
@Override
public boolean onDown( MotionEvent e ) {
mMotionHighlightView = null;
HighlightView hv = mHighlightView;
if ( hv != null ) {
int edge = hv.getHit( e.getX(), e.getY() );
if ( edge != HighlightView.GROW_NONE ) {
mMotionEdge = edge;
mMotionHighlightView = hv;
mMotionHighlightView.setMode( ( edge == HighlightView.MOVE ) ? HighlightView.Mode.Move : HighlightView.Mode.Grow );
}
}
return super.onDown( e );
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(android.view.MotionEvent)
*/
@Override
public boolean onSingleTapConfirmed( MotionEvent e ) {
mMotionHighlightView = null;
return super.onSingleTapConfirmed( e );
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapUp(android.view.MotionEvent)
*/
@Override
public boolean onSingleTapUp( MotionEvent e ) {
mMotionHighlightView = null;
if ( mHighlightView != null && mMotionEdge == HighlightView.MOVE ) {
if ( mHighlightSingleTapUpListener != null ) {
mHighlightSingleTapUpListener.onSingleTapUpConfirmed();
}
}
return super.onSingleTapUp( e );
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onDoubleTap(android.view.MotionEvent)
*/
@Override
public boolean onDoubleTap( MotionEvent e ) {
if ( mDoubleTapEnabled ) {
mMotionHighlightView = null;
float scale = getScale();
float targetScale = scale;
targetScale = CropImageView.this.onDoubleTapPost( scale, getMaxZoom() );
targetScale = Math.min( getMaxZoom(), Math.max( targetScale, 1 ) );
mCurrentScaleFactor = targetScale;
zoomTo( targetScale, e.getX(), e.getY(), 200 );
// zoomTo( targetScale, e.getX(), e.getY() );
invalidate();
}
return super.onDoubleTap( e );
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent,
* float, float)
*/
@Override
public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) {
if ( e1 == null || e2 == null ) return false;
if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
if ( mScaleDetector.isInProgress() ) return false;
if ( mMotionHighlightView != null && mMotionEdge != HighlightView.GROW_NONE ) {
mMotionHighlightView.handleMotion( mMotionEdge, -distanceX, -distanceY );
ensureVisible( mMotionHighlightView );
return true;
} else {
scrollBy( -distanceX, -distanceY );
invalidate();
return true;
}
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent,
* float, float)
*/
@Override
public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY ) {
if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
if ( mScaleDetector.isInProgress() ) return false;
if ( mMotionHighlightView != null ) return false;
float diffX = e2.getX() - e1.getX();
float diffY = e2.getY() - e1.getY();
if ( Math.abs( velocityX ) > 800 || Math.abs( velocityY ) > 800 ) {
scrollBy( diffX / 2, diffY / 2, 300 );
invalidate();
}
return super.onFling( e1, e2, velocityX, velocityY );
}
}
/**
* The listener interface for receiving cropScale events. The class that is interested in processing a cropScale event implements
* this interface, and the object created with that class is registered with a component using the component's
* <code>addCropScaleListener<code> method. When
* the cropScale event occurs, that object's appropriate
* method is invoked.
*
* @see CropScaleEvent
*/
class CropScaleListener extends SimpleOnScaleGestureListener {
/*
* (non-Javadoc)
*
* @see
* it.sephiroth.android.library.imagezoom.ScaleGestureDetector.SimpleOnScaleGestureListener#onScaleBegin(it.sephiroth.android
* .library.imagezoom.ScaleGestureDetector)
*/
@Override
public boolean onScaleBegin( ScaleGestureDetector detector ) {
return super.onScaleBegin( detector );
}
/*
* (non-Javadoc)
*
* @see
* it.sephiroth.android.library.imagezoom.ScaleGestureDetector.SimpleOnScaleGestureListener#onScaleEnd(it.sephiroth.android
* .library.imagezoom.ScaleGestureDetector)
*/
@Override
public void onScaleEnd( ScaleGestureDetector detector ) {
super.onScaleEnd( detector );
}
/*
* (non-Javadoc)
*
* @see
* it.sephiroth.android.library.imagezoom.ScaleGestureDetector.SimpleOnScaleGestureListener#onScale(it.sephiroth.android.library
* .imagezoom.ScaleGestureDetector)
*/
@Override
public boolean onScale( ScaleGestureDetector detector ) {
float targetScale = mCurrentScaleFactor * detector.getScaleFactor();
if ( true ) {
targetScale = Math.min( getMaxZoom(), Math.max( targetScale, 1 ) );
zoomTo( targetScale, detector.getFocusX(), detector.getFocusY() );
mCurrentScaleFactor = Math.min( getMaxZoom(), Math.max( targetScale, 1 ) );
mDoubleTapDirection = 1;
invalidate();
}
return true;
}
}
/** The m aspect ratio. */
protected double mAspectRatio = 0;
/** The m aspect ratio fixed. */
private boolean mAspectRatioFixed;
/**
* Set the new image display and crop view. If both aspect
*
* @param bitmap
* Bitmap to display
* @param aspectRatio
* aspect ratio for the crop view. If 0 is passed, then the crop rectangle can be free transformed by the user,
* otherwise the width/height are fixed according to the aspect ratio passed.
*/
public void setImageBitmap( Bitmap bitmap, double aspectRatio, boolean isFixed ) {
mAspectRatio = aspectRatio;
mAspectRatioFixed = isFixed;
setImageBitmap( bitmap, true, null, UIConfiguration.IMAGE_VIEW_MAX_ZOOM );
}
/**
* Sets the aspect ratio.
*
* @param value
* the value
* @param isFixed
* the is fixed
*/
public void setAspectRatio( double value, boolean isFixed ) {
// Log.d( LOG_TAG, "setAspectRatio" );
if ( getDrawable() != null ) {
mAspectRatio = value;
mAspectRatioFixed = isFixed;
updateCropView( false );
}
}
/*
* (non-Javadoc)
*
* @see it.sephiroth.android.library.imagezoom.ImageViewTouch#onBitmapChanged(android.graphics.drawable.Drawable)
*/
@Override
protected void onBitmapChanged( Drawable drawable ) {
super.onBitmapChanged( drawable );
if ( null != getHandler() ) {
getHandler().post( new Runnable() {
@Override
public void run() {
updateCropView( true );
}
} );
}
}
/**
* Update crop view.
*/
public void updateCropView( boolean bitmapChanged ) {
if ( bitmapChanged ) {
setHighlightView( null );
}
if ( getDrawable() == null ) {
setHighlightView( null );
invalidate();
return;
}
if ( getHighlightView() != null ) {
updateAspectRatio( mAspectRatio, getHighlightView(), true );
} else {
HighlightView hv = new HighlightView( this );
hv.setMinSize( mCropMinSize );
updateAspectRatio( mAspectRatio, hv, false );
setHighlightView( hv );
}
invalidate();
}
/**
* Update aspect ratio.
*
* @param aspectRatio
* the aspect ratio
* @param hv
* the hv
*/
private void updateAspectRatio( double aspectRatio, HighlightView hv, boolean animate ) {
// Log.d( LOG_TAG, "updateAspectRatio" );
float width = getDrawable().getIntrinsicWidth();
float height = getDrawable().getIntrinsicHeight();
Rect imageRect = new Rect( 0, 0, (int) width, (int) height );
Matrix mImageMatrix = getImageMatrix();
RectF cropRect = computeFinalCropRect( aspectRatio );
if ( animate ) {
hv.animateTo( mImageMatrix, imageRect, cropRect, mAspectRatioFixed );
} else {
hv.setup( mImageMatrix, imageRect, cropRect, mAspectRatioFixed );
}
}
public void onConfigurationChanged( Configuration config ) {
// Log.d( LOG_TAG, "onConfigurationChanged" );
if ( null != getHandler() ) {
getHandler().postDelayed( new Runnable() {
@Override
public void run() {
setAspectRatio( mAspectRatio, getAspectRatioIsFixed() );
}
}, 500 );
}
postInvalidate();
}
private RectF computeFinalCropRect( double aspectRatio ) {
float scale = getScale();
float width = getDrawable().getIntrinsicWidth();
float height = getDrawable().getIntrinsicHeight();
final int viewWidth = getWidth();
final int viewHeight = getHeight();
float cropWidth = Math.min( Math.min( width / scale, viewWidth ), Math.min( height / scale, viewHeight ) ) * 0.8f;
float cropHeight = cropWidth;
if ( aspectRatio != 0 ) {
if ( aspectRatio > 1 ) {
cropHeight = cropHeight / (float) aspectRatio;
} else {
cropWidth = cropWidth * (float) aspectRatio;
}
}
Matrix mImageMatrix = getImageMatrix();
Matrix tmpMatrix = new Matrix();
if( !mImageMatrix.invert( tmpMatrix ) ){
Log.e( LOG_TAG, "cannot invert matrix" );
}
RectF r = new RectF( 0, 0, viewWidth, viewHeight );
tmpMatrix.mapRect( r );
float x = r.centerX() - cropWidth / 2;
float y = r.centerY() - cropHeight / 2;
RectF cropRect = new RectF( x, y, x + cropWidth, y + cropHeight );
return cropRect;
}
/**
* Gets the aspect ratio.
*
* @return the aspect ratio
*/
public double getAspectRatio() {
return mAspectRatio;
}
public boolean getAspectRatioIsFixed() {
return mAspectRatioFixed;
}
}