package com.aviary.android.feather.effects;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import android.R.attr;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.AsyncTask;
import android.util.FloatMath;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import com.aviary.android.feather.R;
import com.aviary.android.feather.graphics.CropCheckboxDrawable;
import com.aviary.android.feather.graphics.DefaultGalleryCheckboxDrawable;
import com.aviary.android.feather.graphics.GalleryCircleDrawable;
import com.aviary.android.feather.graphics.PreviewCircleDrawable;
import com.aviary.android.feather.library.filters.FilterLoaderFactory;
import com.aviary.android.feather.library.filters.FilterLoaderFactory.Filters;
import com.aviary.android.feather.library.filters.IFilter;
import com.aviary.android.feather.library.filters.SpotBrushFilter;
import com.aviary.android.feather.library.graphics.FlattenPath;
import com.aviary.android.feather.library.moa.Moa;
import com.aviary.android.feather.library.moa.MoaAction;
import com.aviary.android.feather.library.moa.MoaActionFactory;
import com.aviary.android.feather.library.moa.MoaActionList;
import com.aviary.android.feather.library.services.ConfigService;
import com.aviary.android.feather.library.services.EffectContext;
import com.aviary.android.feather.library.utils.BitmapUtils;
import com.aviary.android.feather.library.utils.SystemUtils;
import com.aviary.android.feather.library.utils.UIConfiguration;
import com.aviary.android.feather.utils.UIUtils;
import com.aviary.android.feather.widget.AdapterView;
import com.aviary.android.feather.widget.Gallery;
import com.aviary.android.feather.widget.Gallery.OnItemsScrollListener;
import com.aviary.android.feather.widget.IToast;
import com.aviary.android.feather.widget.ImageViewSpotDraw;
import com.aviary.android.feather.widget.ImageViewSpotDraw.OnDrawListener;
import com.aviary.android.feather.widget.ImageViewSpotDraw.TouchMode;
/**
* The Class SpotDrawPanel.
*/
public class DelayedSpotDrawPanel extends AbstractContentPanel implements OnDrawListener {
/** The default option. */
protected int defaultOption = 0;
/** The brush size. */
protected int mBrushSize;
/** The filter type. */
protected Filters mFilterType;
protected Gallery mGallery;
/** The brush sizes. */
protected int[] mBrushSizes;
/** The current selected view. */
protected View mSelected;
/** The current selected position. */
protected int mSelectedPosition = -1;
protected ImageButton mLensButton;
/** The background draw thread. */
private MyHandlerThread mBackgroundDrawThread;
IToast mToast;
PreviewCircleDrawable mCircleDrawablePreview;
MoaActionList mActions = MoaActionFactory.actionList();
/** if true the drawing area will be limited */
private boolean mLimitDrawArea;
/**
* Show Brush size preview.
*
* @param size
* the brush preview size
*/
private void showSizePreview( int size ) {
if ( !isActive() ) return;
mToast.show();
updateSizePreview( size );
}
/**
* Hide the Brush size preview.
*/
private void hideSizePreview() {
if ( !isActive() ) return;
mToast.hide();
}
/**
* Update the Brush size preview
*
* @param size
* the brush preview size
*/
private void updateSizePreview( int size ) {
if ( !isActive() ) return;
mCircleDrawablePreview.setRadius( size / 2 );
View v = mToast.getView();
v.findViewById( R.id.size_preview_image );
v.invalidate();
}
/**
* Instantiates a new spot draw panel.
*
* @param context
* the context
* @param filter_type
* the filter_type
*/
public DelayedSpotDrawPanel( EffectContext context, Filters filter_type, boolean limit_area ) {
super( context );
mFilterType = filter_type;
mLimitDrawArea = limit_area;
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onCreate(android.graphics.Bitmap)
*/
@Override
public void onCreate( Bitmap bitmap ) {
super.onCreate( bitmap );
ConfigService config = getContext().getService( ConfigService.class );
mBrushSizes = config.getSizeArray( R.array.feather_spot_brush_sizes );
mBrushSize = mBrushSizes[0];
defaultOption = Math.min( mBrushSizes.length - 1,
Math.max( 0, config.getInteger( R.integer.feather_spot_brush_selected_size_index ) ) );
mLensButton = (ImageButton) getContentView().findViewById( R.id.lens_button );
mImageView = (ImageViewSpotDraw) getContentView().findViewById( R.id.image );
( (ImageViewSpotDraw) mImageView ).setBrushSize( (float) mBrushSize );
( (ImageViewSpotDraw) mImageView ).setDrawLimit( mLimitDrawArea ? 1000.0 : 0.0 );
mPreview = BitmapUtils.copy( mBitmap, Config.ARGB_8888 );
mImageView.setImageBitmap( mPreview, true, getContext().getCurrentImageViewMatrix(), UIConfiguration.IMAGE_VIEW_MAX_ZOOM );
mGallery = (Gallery) getOptionView().findViewById( R.id.gallery );
mGallery.setCallbackDuringFling( false );
mGallery.setSpacing( 0 );
mGallery.setOnItemsScrollListener( new OnItemsScrollListener() {
@Override
public void onScrollFinished( AdapterView<?> parent, View view, int position, long id ) {
mBrushSize = mBrushSizes[position];
( (ImageViewSpotDraw) mImageView ).setBrushSize( (float) mBrushSize );
setSelectedTool( TouchMode.DRAW );
updateSelection( view, position );
hideSizePreview();
}
@Override
public void onScrollStarted( AdapterView<?> parent, View view, int position, long id ) {
showSizePreview( mBrushSizes[position] );
setSelectedTool( TouchMode.DRAW );
}
@Override
public void onScroll( AdapterView<?> parent, View view, int position, long id ) {
updateSizePreview( mBrushSizes[position] );
}
} );
mBackgroundDrawThread = new MyHandlerThread( "filter-thread", Thread.MIN_PRIORITY );
initAdapter();
}
/**
* Inits the adapter.
*/
private void initAdapter() {
int height = mGallery.getHeight();
if ( height < 1 ) {
mGallery.getHandler().post( new Runnable() {
@Override
public void run() {
initAdapter();
}
} );
return;
}
mGallery.setAdapter( new GalleryAdapter( getContext().getBaseContext(), mBrushSizes ) );
mGallery.setSelection( defaultOption, false, true );
mSelectedPosition = defaultOption;
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onActivate()
*/
@Override
public void onActivate() {
super.onActivate();
disableHapticIsNecessary( mGallery );
( (ImageViewSpotDraw) mImageView ).setOnDrawStartListener( this );
mBackgroundDrawThread.start();
mBackgroundDrawThread.setRadius( (float) Math.max( 1, mBrushSizes[0] ), mPreview.getWidth() );
mToast = IToast.make( getContext().getBaseContext(), -1 );
mCircleDrawablePreview = new PreviewCircleDrawable( 0 );
ImageView image = (ImageView) mToast.getView().findViewById( R.id.size_preview_image );
image.setImageDrawable( mCircleDrawablePreview );
mLensButton.setOnClickListener( new OnClickListener() {
@Override
public void onClick( View arg0 ) {
// boolean selected = arg0.isSelected();
setSelectedTool( ( (ImageViewSpotDraw) mImageView ).getDrawMode() == TouchMode.DRAW ? TouchMode.IMAGE : TouchMode.DRAW );
}
} );
mLensButton.setVisibility( View.VISIBLE );
// TODO: check if selection is correct when panel opens
// updateSelection( (View) mGallery.getSelectedView(), mGallery.getSelectedItemPosition() );
contentReady();
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractContentPanel#onDispose()
*/
@Override
protected void onDispose() {
mContentReadyListener = null;
super.onDispose();
}
/**
* Update selection.
*
* @param newSelection
* the new selection
* @param position
* the position
*/
protected void updateSelection( View newSelection, int position ) {
if ( mSelected != null ) {
mSelected.setSelected( false );
}
mSelected = newSelection;
mSelectedPosition = position;
if ( mSelected != null ) {
mSelected = newSelection;
mSelected.setSelected( true );
}
mGallery.invalidateViews();
}
/**
* Sets the selected tool.
*
* @param which
* the new selected tool
*/
private void setSelectedTool( TouchMode which ) {
( (ImageViewSpotDraw) mImageView ).setDrawMode( which );
mLensButton.setSelected( which == TouchMode.IMAGE );
setPanelEnabled( which != TouchMode.IMAGE );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onDeactivate()
*/
@Override
public void onDeactivate() {
( (ImageViewSpotDraw) mImageView ).setOnDrawStartListener( null );
if ( mBackgroundDrawThread != null ) {
mBackgroundDrawThread.mQueue.clear();
if ( mBackgroundDrawThread.started ) {
mBackgroundDrawThread.pause();
}
if ( mBackgroundDrawThread.isAlive() ) {
mBackgroundDrawThread.quit();
while ( mBackgroundDrawThread.isAlive() ) {
// wait...
}
}
}
onProgressEnd();
super.onDeactivate();
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onDestroy()
*/
@Override
public void onDestroy() {
super.onDestroy();
mBackgroundDrawThread = null;
mImageView.clear();
mToast.hide();
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onCancelled()
*/
@Override
public void onCancelled() {
super.onCancelled();
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.widget.ImageViewSpotDraw.OnDrawListener#onDrawStart(float[], int)
*/
@Override
public void onDrawStart( float[] points, int radius ) {
radius = Math.max( 1, radius );
mBackgroundDrawThread.pathStart( (float) radius, mPreview.getWidth() );
mBackgroundDrawThread.moveTo( points );
mBackgroundDrawThread.lineTo( points );
setIsChanged( true );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.widget.ImageViewSpotDraw.OnDrawListener#onDrawing(float[], int)
*/
@Override
public void onDrawing( float[] points, int radius ) {
mBackgroundDrawThread.quadTo( points );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.widget.ImageViewSpotDraw.OnDrawListener#onDrawEnd()
*/
@Override
public void onDrawEnd() {
// TODO: empty
mBackgroundDrawThread.pathEnd();
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractEffectPanel#onGenerateResult()
*/
@Override
protected void onGenerateResult() {
mLogger.info( "onGenerateResult: " + mBackgroundDrawThread.isCompleted() + ", " + mBackgroundDrawThread.isAlive() );
if ( !mBackgroundDrawThread.isCompleted() && mBackgroundDrawThread.isAlive() ) {
GenerateResultTask task = new GenerateResultTask();
task.execute();
} else {
onComplete( mPreview, mActions );
}
}
/**
* Sets the panel enabled.
*
* @param value
* the new panel enabled
*/
public void setPanelEnabled( boolean value ) {
if ( mOptionView != null ) {
if ( value != mOptionView.isEnabled() ) {
mOptionView.setEnabled( value );
if ( value ) {
getContext().restoreToolbarTitle();
} else {
getContext().setToolbarTitle( R.string.zoom_mode );
}
mOptionView.findViewById( R.id.disable_status ).setVisibility( value ? View.INVISIBLE : View.VISIBLE );
}
}
}
/**
* Prints the rect.
*
* @param rect
* the rect
* @return the string
*/
@SuppressWarnings("unused")
private String printRect( Rect rect ) {
return "( left=" + rect.left + ", top=" + rect.top + ", width=" + rect.width() + ", height=" + rect.height() + ")";
}
/**
* Creates the filter.
*
* @return the i filter
*/
protected IFilter createFilter() {
return FilterLoaderFactory.get( mFilterType );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractContentPanel#generateContentView(android.view.LayoutInflater)
*/
@Override
protected View generateContentView( LayoutInflater inflater ) {
return inflater.inflate( R.layout.feather_spotdraw_content, null );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractOptionPanel#generateOptionView(android.view.LayoutInflater,
* android.view.ViewGroup)
*/
@Override
protected ViewGroup generateOptionView( LayoutInflater inflater, ViewGroup parent ) {
return (ViewGroup) inflater.inflate( R.layout.feather_pixelbrush_panel, parent, false );
}
/*
* (non-Javadoc)
*
* @see com.aviary.android.feather.effects.AbstractContentPanel#getContentDisplayMatrix()
*/
@Override
public Matrix getContentDisplayMatrix() {
return mImageView.getDisplayMatrix();
}
/**
* background draw thread
*/
class MyHandlerThread extends Thread {
/** The started. */
boolean started;
/** The running. */
volatile boolean running;
/** The paused. */
boolean paused;
/** the filters queue */
Queue<SpotBrushFilter> mQueue = new LinkedBlockingQueue<SpotBrushFilter>();
SpotBrushFilter mCurrentFilter = null;
/**
* Instantiates a new my handler thread.
*
* @param name
* the name
* @param priority
* the priority
*/
public MyHandlerThread( String name, int priority ) {
super( name );
setPriority( priority );
init();
}
/**
* Inits the.
*/
void init() {}
/*
* (non-Javadoc)
*
* @see java.lang.Thread#start()
*/
@Override
synchronized public void start() {
started = true;
running = true;
super.start();
}
/**
* Quit.
*/
synchronized public void quit() {
running = false;
pause();
interrupt();
};
/**
* Initialize a new path draw operation
*/
synchronized public void pathStart( float radius, int bitmapWidth ) {
SpotBrushFilter filter = (SpotBrushFilter) createFilter();
filter.setRadius( radius, bitmapWidth );
RectF rect = ( (ImageViewSpotDraw) mImageView ).getImageRect();
if ( null != rect ) {
( (ImageViewSpotDraw) mImageView ).getImageViewMatrix().mapRect( rect );
double ratio = rect.width() / mImageView.getWidth();
filter.getActions().get( 0 ).setValue( "image2displayratio", ratio );
}
setRadius( radius, bitmapWidth );
mCurrentFilter = filter;
}
/**
* Completes the path drawing operation and apply the filter
*/
synchronized public void pathEnd() {
if ( mCurrentFilter != null ) {
mQueue.add( mCurrentFilter );
}
mCurrentFilter = null;
}
/**
* Pause.
*/
public void pause() {
if ( !started ) throw new IllegalAccessError( "thread not started" );
paused = true;
}
/**
* Unpause.
*/
public void unpause() {
if ( !started ) throw new IllegalAccessError( "thread not started" );
paused = false;
}
/** the current brush radius size */
float mRadius = 10;
public void setRadius( float radius, int bitmapWidth ) {
mRadius = radius;
}
public void moveTo( float values[] ) {
mCurrentFilter.moveTo( values );
}
public void lineTo( float values[] ) {
mCurrentFilter.lineTo( values );
}
public void quadTo( float values[] ) {
mCurrentFilter.quadTo( values );
}
public boolean isCompleted() {
return mQueue.size() == 0;
}
public int queueSize() {
return mQueue.size();
}
public PointF getLerp( PointF pt1, PointF pt2, float t ) {
return new PointF( pt1.x + ( pt2.x - pt1.x ) * t, pt1.y + ( pt2.y - pt1.y ) * t );
}
@Override
public void run() {
while ( !started ) {}
boolean s = false;
mLogger.log( "thread.start!" );
while ( running ) {
if ( paused ) {
continue;
}
int currentSize;
currentSize = queueSize();
if ( currentSize > 0 && !isInterrupted() ) {
if ( !s ) {
s = true;
onProgressStart();
}
PointF firstPoint, lastPoint;
SpotBrushFilter filter = mQueue.peek();
FlattenPath path = filter.getFlattenPath();
firstPoint = path.remove();
while ( firstPoint == null && path.size() > 0 ) {
firstPoint = path.remove();
}
final int w = mPreview.getWidth();
final int h = mPreview.getHeight();
while ( path.size() > 0 ) {
lastPoint = path.remove();
float x = Math.abs( firstPoint.x - lastPoint.x );
float y = Math.abs( firstPoint.y - lastPoint.y );
float length = FloatMath.sqrt( x * x + y * y );
float currentPosition = 0;
float lerp;
if ( length == 0 ) {
filter.addPoint( firstPoint.x / w, firstPoint.y / h );
} else {
while ( currentPosition < length ) {
lerp = currentPosition / length;
PointF point = getLerp( lastPoint, firstPoint, lerp );
currentPosition += filter.getRealRadius();
filter.addPoint( point.x / w, point.y / h );
}
}
firstPoint = lastPoint;
}
filter.draw( mPreview );
if ( paused ) continue;
if ( SystemUtils.isHoneyComb() ) {
// There's a bug in Honeycomb which prevent the bitmap to be updated on a glcanvas
// so we need to force it
Moa.notifyPixelsChanged( mPreview );
}
try {
mActions.add( (MoaAction) filter.getActions().get( 0 ).clone() );
} catch ( CloneNotSupportedException e ) {}
mQueue.remove();
mImageView.postInvalidate();
} else {
if ( s ) {
onProgressEnd();
s = false;
}
}
}
onProgressEnd();
mLogger.log( "thread.end" );
};
};
/**
* Bottom Gallery adapter.
*
* @author alessandro
*/
class GalleryAdapter extends BaseAdapter {
private final int VALID_POSITION = 0;
private final int INVALID_POSITION = 1;
private int[] sizes;
LayoutInflater mLayoutInflater;
Drawable checkbox_unselected, checkbox_selected;
Resources mRes;
/**
* Instantiates a new gallery adapter.
*
* @param context
* the context
* @param values
* the values
*/
public GalleryAdapter( Context context, int[] values ) {
mLayoutInflater = UIUtils.getLayoutInflater();
sizes = values;
mRes = getContext().getBaseContext().getResources();
checkbox_selected = mRes.getDrawable( R.drawable.feather_crop_checkbox_selected );
checkbox_unselected = mRes.getDrawable( R.drawable.feather_crop_checkbox_unselected );
}
/*
* (non-Javadoc)
*
* @see android.widget.Adapter#getCount()
*/
@Override
public int getCount() {
return sizes.length;
}
/*
* (non-Javadoc)
*
* @see android.widget.Adapter#getItem(int)
*/
@Override
public Object getItem( int position ) {
return sizes[position];
}
/*
* (non-Javadoc)
*
* @see android.widget.Adapter#getItemId(int)
*/
@Override
public long getItemId( int position ) {
return 0;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType( int position ) {
final boolean valid = position >= 0 && position < getCount();
return valid ? VALID_POSITION : INVALID_POSITION;
}
/*
* (non-Javadoc)
*
* @see android.widget.Adapter#getView(int, android.view.View, android.view.ViewGroup)
*/
@SuppressWarnings("deprecation")
@Override
public View getView( int position, View convertView, ViewGroup parent ) {
final int type = getItemViewType( position );
GalleryCircleDrawable mCircleDrawable = null;
int biggest = sizes[sizes.length - 1];
int size = 1;
View view;
if ( convertView == null ) {
if ( type == VALID_POSITION ) {
mCircleDrawable = new GalleryCircleDrawable( 1, 0 );
view = mLayoutInflater.inflate( R.layout.feather_checkbox_button, mGallery, false );
StateListDrawable st = new StateListDrawable();
Drawable d1 = new CropCheckboxDrawable( mRes, false, mCircleDrawable, 0.67088f, 0.4f, 0.0f );
Drawable d2 = new CropCheckboxDrawable( mRes, true, mCircleDrawable, 0.67088f, 0.4f, 0.0f );
st.addState( new int[] { -attr.state_selected }, d1 );
st.addState( new int[] { attr.state_selected }, d2 );
view.setBackgroundDrawable( st );
view.setTag( mCircleDrawable );
} else {
// use the blank view
view = mLayoutInflater.inflate( R.layout.feather_checkbox_button, mGallery, false );
Drawable unselectedBackground = new DefaultGalleryCheckboxDrawable( mRes, false );
view.setBackgroundDrawable( unselectedBackground );
}
} else {
view = convertView;
if ( type == VALID_POSITION ) {
mCircleDrawable = (GalleryCircleDrawable) view.getTag();
}
}
if ( mCircleDrawable != null && type == VALID_POSITION ) {
size = sizes[position];
float value = (float) size / biggest;
mCircleDrawable.update( value, 0 );
}
view.setSelected( mSelectedPosition == position );
return view;
}
}
/**
* GenerateResultTask is used when the background draw operation is still running. Just wait until the draw operation completed.
*/
class GenerateResultTask extends AsyncTask<Void, Void, Void> {
/** The m progress. */
ProgressDialog mProgress = new ProgressDialog( getContext().getBaseContext() );
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#onPreExecute()
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
mProgress.setTitle( getContext().getBaseContext().getString( R.string.feather_loading_title ) );
mProgress.setMessage( getContext().getBaseContext().getString( R.string.effect_loading_message ) );
mProgress.setIndeterminate( true );
mProgress.setCancelable( false );
mProgress.show();
}
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#doInBackground(Params[])
*/
@Override
protected Void doInBackground( Void... params ) {
if ( mBackgroundDrawThread != null ) {
while ( mBackgroundDrawThread != null && !mBackgroundDrawThread.isCompleted() ) {
mLogger.log( "waiting.... " + mBackgroundDrawThread.queueSize() );
try {
Thread.sleep( 100 );
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#onPostExecute(java.lang.Object)
*/
@Override
protected void onPostExecute( Void result ) {
super.onPostExecute( result );
if ( getContext().getBaseActivity().isFinishing() ) return;
if ( mProgress.isShowing() ) {
try {
mProgress.dismiss();
} catch ( IllegalArgumentException e ) {}
}
onComplete( mPreview, mActions );
}
}
}