package com.aviary.android.feather.widget; import it.sephiroth.android.library.imagezoom.ImageViewTouch; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.BlurMaskFilter.Blur; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; import com.aviary.android.feather.library.services.DragControllerService.DragSource; import com.aviary.android.feather.library.services.drag.DragView; import com.aviary.android.feather.library.services.drag.DropTarget; import com.aviary.android.feather.widget.DrawableHighlightView.Mode; public class ImageViewDrawableOverlay extends ImageViewTouch implements DropTarget { /** * 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 onDrawableEvent events. The class that is interested in processing a onDrawableEvent * event implements this interface, and the object created with that class is registered with a component using the component's * <code>addOnDrawableEventListener<code> method. When * the onDrawableEvent event occurs, that object's appropriate * method is invoked. * * @see OnDrawableEventEvent */ public static interface OnDrawableEventListener { /** * On focus change. * * @param newFocus * the new focus * @param oldFocus * the old focus */ void onFocusChange( DrawableHighlightView newFocus, DrawableHighlightView oldFocus ); /** * On down. * * @param view * the view */ void onDown( DrawableHighlightView view ); /** * On move. * * @param view * the view */ void onMove( DrawableHighlightView view ); /** * On click. * * @param view * the view */ void onClick( DrawableHighlightView view ); }; /** The m motion edge. */ private int mMotionEdge = DrawableHighlightView.GROW_NONE; /** The m overlay views. */ private List<DrawableHighlightView> mOverlayViews = new ArrayList<DrawableHighlightView>(); /** The m overlay view. */ private DrawableHighlightView mOverlayView; /** The m layout listener. */ private OnLayoutListener mLayoutListener; /** The m drawable listener. */ private OnDrawableEventListener mDrawableListener; /** The m force single selection. */ private boolean mForceSingleSelection = true; private DropTargetListener mDropTargetListener; private Paint mDropPaint; private Rect mTempRect = new Rect(); private boolean mScaleWithContent = false; /** * Instantiates a new image view drawable overlay. * * @param context * the context * @param attrs * the attrs */ public ImageViewDrawableOverlay( Context context, AttributeSet attrs ) { super( context, attrs ); } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouch#init() */ @Override protected void init() { super.init(); mTouchSlop = 20 * 20; mGestureDetector.setIsLongpressEnabled( false ); } /** * How overlay content will be scaled/moved when zomming/panning the base image * * @param value * - if true then the content will be scale according to the image */ public void setScaleWithContent( boolean value ) { mScaleWithContent = value; } public boolean getScaleWithContent() { return mScaleWithContent; } /** * If true, when the user tap outside the drawable overlay and there is only one active overlay selection is not changed. * * @param value * the new force single selection */ public void setForceSingleSelection( boolean value ) { mForceSingleSelection = value; } public void setDropTargetListener( DropTargetListener listener ) { mDropTargetListener = listener; } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouch#getGestureListener() */ @Override protected OnGestureListener getGestureListener() { return new CropGestureListener(); } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouch#getScaleListener() */ @Override protected OnScaleGestureListener getScaleListener() { return new CropScaleListener(); } /** * Sets the on layout listener. * * @param listener * the new on layout listener */ public void setOnLayoutListener( OnLayoutListener listener ) { mLayoutListener = listener; } /** * Sets the on drawable event listener. * * @param listener * the new on drawable event listener */ public void setOnDrawableEventListener( OnDrawableEventListener listener ) { mDrawableListener = listener; } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouchBase#setImageBitmap(android.graphics.Bitmap, boolean, * android.graphics.Matrix) */ @Override public void setImageBitmap( final Bitmap bitmap, final boolean reset, Matrix matrix ) { clearOverlays(); super.setImageBitmap( bitmap, reset, matrix ); } /* * (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 ); if ( getDrawable() != null && changed ) { Iterator<DrawableHighlightView> iterator = mOverlayViews.iterator(); while ( iterator.hasNext() ) { DrawableHighlightView view = iterator.next(); view.getMatrix().set( getImageMatrix() ); view.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 ); Iterator<DrawableHighlightView> iterator = mOverlayViews.iterator(); while ( iterator.hasNext() ) { DrawableHighlightView view = iterator.next(); if ( getScale() != 1 ) { float[] mvalues = new float[9]; getImageMatrix().getValues( mvalues ); final float scale = mvalues[Matrix.MSCALE_X]; if ( !mScaleWithContent ) view.getCropRectF().offset( -deltaX / scale, -deltaY / scale ); } view.getMatrix().set( getImageMatrix() ); view.invalidate(); } } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouchBase#postScale(float, float, float) */ @Override protected void postScale( float scale, float centerX, float centerY ) { if ( mOverlayViews.size() > 0 ) { Iterator<DrawableHighlightView> iterator = mOverlayViews.iterator(); Matrix oldMatrix = new Matrix( getImageViewMatrix() ); super.postScale( scale, centerX, centerY ); while ( iterator.hasNext() ) { DrawableHighlightView view = iterator.next(); if ( !mScaleWithContent ) { RectF cropRect = view.getCropRectF(); RectF rect1 = view.getDisplayRect( oldMatrix, view.getCropRectF() ); RectF rect2 = view.getDisplayRect( getImageViewMatrix(), view.getCropRectF() ); float[] mvalues = new float[9]; getImageViewMatrix().getValues( mvalues ); final float currentScale = mvalues[Matrix.MSCALE_X]; cropRect.offset( ( rect1.left - rect2.left ) / currentScale, ( rect1.top - rect2.top ) / currentScale ); cropRect.right += -( rect2.width() - rect1.width() ) / currentScale; cropRect.bottom += -( rect2.height() - rect1.height() ) / currentScale; view.getMatrix().set( getImageMatrix() ); view.getCropRectF().set( cropRect ); } else { view.getMatrix().set( getImageMatrix() ); } view.invalidate(); } } else { super.postScale( scale, centerX, centerY ); } } /** * Ensure visible. * * @param hv * the hv */ private void ensureVisible( DrawableHighlightView hv, float deltaX, float deltaY ) { RectF r = hv.getDrawRect(); int panDeltaX1 = 0, panDeltaX2 = 0; int panDeltaY1 = 0, panDeltaY2 = 0; if ( deltaX > 0 ) panDeltaX1 = (int) Math.max( 0, getLeft() - r.left ); if ( deltaX < 0 ) panDeltaX2 = (int) Math.min( 0, getRight() - r.right ); if ( deltaY > 0 ) panDeltaY1 = (int) Math.max( 0, getTop() - r.top ); if ( deltaY < 0 ) panDeltaY2 = (int) 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 public void onDraw( Canvas canvas ) { super.onDraw( canvas ); for ( int i = 0; i < mOverlayViews.size(); i++ ) { canvas.save( Canvas.MATRIX_SAVE_FLAG ); mOverlayViews.get( i ).draw( canvas ); canvas.restore(); } if ( null != mDropPaint ) { getDrawingRect( mTempRect ); canvas.drawRect( mTempRect, mDropPaint ); } } /** * Clear overlays. */ public void clearOverlays() { setSelectedHighlightView( null ); while ( mOverlayViews.size() > 0 ) { DrawableHighlightView hv = mOverlayViews.remove( 0 ); hv.dispose(); } mOverlayView = null; mMotionEdge = DrawableHighlightView.GROW_NONE; } /** * Adds the highlight view. * * @param hv * the hv * @return true, if successful */ public boolean addHighlightView( DrawableHighlightView hv ) { for ( int i = 0; i < mOverlayViews.size(); i++ ) { if ( mOverlayViews.get( i ).equals( hv ) ) return false; } mOverlayViews.add( hv ); postInvalidate(); if ( mOverlayViews.size() == 1 ) { setSelectedHighlightView( hv ); } return true; } /** * Gets the highlight count. * * @return the highlight count */ public int getHighlightCount() { return mOverlayViews.size(); } /** * Gets the highlight view at. * * @param index * the index * @return the highlight view at */ public DrawableHighlightView getHighlightViewAt( int index ) { return mOverlayViews.get( index ); } /** * Removes the hightlight view. * * @param view * the view * @return true, if successful */ public boolean removeHightlightView( DrawableHighlightView view ) { for ( int i = 0; i < mOverlayViews.size(); i++ ) { if ( mOverlayViews.get( i ).equals( view ) ) { DrawableHighlightView hv = mOverlayViews.remove( i ); if ( hv.equals( mOverlayView ) ) { setSelectedHighlightView( null ); } hv.dispose(); return true; } } return false; } @Override protected void onZoomAnimationCompleted( float scale ) { Log.i( LOG_TAG, "onZoomAnimationCompleted: " + scale ); super.onZoomAnimationCompleted( scale ); if ( mOverlayView != null ) { mOverlayView.setMode( Mode.Move ); mMotionEdge = DrawableHighlightView.MOVE; } } /** * Return the current selected highlight view. * * @return the selected highlight view */ public DrawableHighlightView getSelectedHighlightView() { return mOverlayView; } /* * (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 ( mOverlayView != null ) { mOverlayView.setMode( DrawableHighlightView.Mode.None ); } mMotionEdge = DrawableHighlightView.GROW_NONE; if ( getScale() < 1f ) { zoomTo( 1f, 50 ); } break; } return true; } /* * (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 ); } private boolean onDoubleTap( MotionEvent e ) { float scale = getScale(); float targetScale = scale; targetScale = ImageViewDrawableOverlay.this.onDoubleTapPost( scale, getMaxZoom() ); targetScale = Math.min( getMaxZoom(), Math.max( targetScale, 1 ) ); mCurrentScaleFactor = targetScale; zoomTo( targetScale, e.getX(), e.getY(), DEFAULT_ANIMATION_DURATION ); invalidate(); return true; } /** * Check selection. * * @param e * the e * @return the drawable highlight view */ private DrawableHighlightView checkSelection( MotionEvent e ) { Iterator<DrawableHighlightView> iterator = mOverlayViews.iterator(); DrawableHighlightView selection = null; while ( iterator.hasNext() ) { DrawableHighlightView view = iterator.next(); int edge = view.getHit( e.getX(), e.getY() ); if ( edge != DrawableHighlightView.GROW_NONE ) { selection = view; } } return selection; } /** * Check up selection. * * @param e * the e * @return the drawable highlight view */ private DrawableHighlightView checkUpSelection( MotionEvent e ) { Iterator<DrawableHighlightView> iterator = mOverlayViews.iterator(); DrawableHighlightView selection = null; while ( iterator.hasNext() ) { DrawableHighlightView view = iterator.next(); if ( view.getSelected() ) { view.onSingleTapConfirmed( e.getX(), e.getY() ); } } return selection; } /** * Sets the selected highlight view. * * @param newView * the new selected highlight view */ public void setSelectedHighlightView( DrawableHighlightView newView ) { final DrawableHighlightView oldView = mOverlayView; if ( mOverlayView != null && !mOverlayView.equals( newView ) ) { mOverlayView.setSelected( false ); } if ( newView != null ) { newView.setSelected( true ); } mOverlayView = newView; if ( mDrawableListener != null ) { mDrawableListener.onFocusChange( newView, oldView ); } } /** * 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 { boolean mScrollStarted; float mLastMotionX, mLastMotionY; @Override public boolean onDown( MotionEvent e ) { mScrollStarted = false; mLastMotionX = e.getX(); mLastMotionY = e.getY(); DrawableHighlightView newSelection = checkSelection( e ); DrawableHighlightView realNewSelection = newSelection; if ( newSelection == null && mOverlayViews.size() == 1 && mForceSingleSelection ) { newSelection = mOverlayViews.get( 0 ); } setSelectedHighlightView( newSelection ); if ( realNewSelection != null && mScaleWithContent ) { RectF displayRect = realNewSelection.getDisplayRect( realNewSelection.getMatrix(), realNewSelection.getCropRectF() ); boolean invalidSize = realNewSelection.getContent().validateSize( displayRect ); if ( !invalidSize ) { float minW = realNewSelection.getContent().getMinWidth(); float minH = realNewSelection.getContent().getMinHeight(); float minSize = Math.min( minW, minH ) * 1.1f; float minRectSize = Math.min( displayRect.width(), displayRect.height() ); float diff = minSize / minRectSize; Log.d( LOG_TAG, "drawable too small!!!" ); Log.d( LOG_TAG, "min.size: " + minW + "x" + minH ); Log.d( LOG_TAG, "cur.size: " + displayRect.width() + "x" + displayRect.height() ); zoomTo( getScale() * diff, displayRect.centerX(), displayRect.centerY(), DEFAULT_ANIMATION_DURATION * 1.5f ); return true; } } if ( mOverlayView != null ) { int edge = mOverlayView.getHit( e.getX(), e.getY() ); if ( edge != DrawableHighlightView.GROW_NONE ) { mMotionEdge = edge; mOverlayView .setMode( ( edge == DrawableHighlightView.MOVE ) ? DrawableHighlightView.Mode.Move : ( edge == DrawableHighlightView.ROTATE ? DrawableHighlightView.Mode.Rotate : DrawableHighlightView.Mode.Grow ) ); if ( mDrawableListener != null ) { mDrawableListener.onDown( mOverlayView ); } } } return super.onDown( e ); } /* * (non-Javadoc) * * @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(android.view.MotionEvent) */ @Override public boolean onSingleTapConfirmed( MotionEvent e ) { checkUpSelection( e ); return super.onSingleTapConfirmed( e ); } /* * (non-Javadoc) * * @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapUp(android.view.MotionEvent) */ @Override public boolean onSingleTapUp( MotionEvent e ) { if ( mOverlayView != null ) { int edge = mOverlayView.getHit( e.getX(), e.getY() ); if ( ( edge & DrawableHighlightView.MOVE ) == DrawableHighlightView.MOVE ) { if ( mDrawableListener != null ) mDrawableListener.onClick( mOverlayView ); return true; } mOverlayView.setMode( Mode.None ); if ( mOverlayViews.size() != 1 ) setSelectedHighlightView( null ); } return super.onSingleTapUp( e ); } /* * (non-Javadoc) * * @see android.view.GestureDetector.SimpleOnGestureListener#onDoubleTap(android.view.MotionEvent) */ @Override public boolean onDoubleTap( MotionEvent e ) { if ( !mDoubleTapEnabled ) return false; return ImageViewDrawableOverlay.this.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 ( !mScrollEnabled ) return false; if ( e1 == null || e2 == null ) return false; if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false; if ( mScaleDetector.isInProgress() ) return false; // remove the touch slop lag ( see bug @1084 ) float x = e2.getX(); float y = e2.getY(); if( !mScrollStarted ){ distanceX = 0; distanceY = 0; mScrollStarted = true; } else { distanceX = mLastMotionX - x; distanceY = mLastMotionY - y; } mLastMotionX = x; mLastMotionY = y; if ( mOverlayView != null && mMotionEdge != DrawableHighlightView.GROW_NONE ) { mOverlayView.onMouseMove( mMotionEdge, e2, -distanceX, -distanceY ); if ( mDrawableListener != null ) { mDrawableListener.onMove( mOverlayView ); } if ( mMotionEdge == DrawableHighlightView.MOVE ) { if ( !mScaleWithContent ) { ensureVisible( mOverlayView, distanceX, distanceY ); } } 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 ( !mScrollEnabled ) return false; if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false; if ( mScaleDetector.isInProgress() ) return false; if ( mOverlayView != null && mOverlayView.getMode() != Mode.None ) 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 ScaleListener { /* * (non-Javadoc) * * @see * it.sephiroth.android.library.imagezoom.ScaleGestureDetector.SimpleOnScaleGestureListener#onScaleBegin(it.sephiroth.android * .library.imagezoom.ScaleGestureDetector) */ @Override public boolean onScaleBegin( ScaleGestureDetector detector ) { if ( !mScaleEnabled ) return false; 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 ) { if ( !mScaleEnabled ) return; super.onScaleEnd( detector ); } /* * (non-Javadoc) * * @see it.sephiroth.android.library.imagezoom.ImageViewTouch.ScaleListener#onScale(it.sephiroth.android.library.imagezoom. * ScaleGestureDetector) */ @Override public boolean onScale( ScaleGestureDetector detector ) { if ( !mScaleEnabled ) return false; return super.onScale( detector ); } } @Override public void onDrop( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo ) { if ( mDropTargetListener != null ) { mDropTargetListener.onDrop( source, x, y, xOffset, yOffset, dragView, dragInfo ); } } @Override public void onDragEnter( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo ) { mDropPaint = new Paint(); mDropPaint.setColor( 0xff33b5e5 ); mDropPaint.setStrokeWidth( 2 ); mDropPaint.setMaskFilter( new BlurMaskFilter( 4.0f, Blur.NORMAL ) ); mDropPaint.setStyle( Paint.Style.STROKE ); invalidate(); } @Override public void onDragOver( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo ) {} @Override public void onDragExit( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo ) { mDropPaint = null; invalidate(); } @Override public boolean acceptDrop( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo ) { if ( mDropTargetListener != null ) { return mDropTargetListener.acceptDrop( source, x, y, xOffset, yOffset, dragView, dragInfo ); } return false; } @Override public Rect estimateDropLocation( DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo, Rect recycle ) { return null; } }