package com.aviary.android.feather.widget; import it.sephiroth.android.library.imagezoom.easing.Easing; import it.sephiroth.android.library.imagezoom.easing.Sine; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.DrawFilter; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable.Orientation; import android.os.Handler; import android.os.Message; import android.os.Vibrator; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import com.aviary.android.feather.R; import com.aviary.android.feather.graphics.LinearGradientDrawable; import com.aviary.android.feather.library.utils.ReflectionUtils; import com.aviary.android.feather.library.utils.ReflectionUtils.ReflectionException; import com.aviary.android.feather.widget.IFlingRunnable.FlingRunnableView; // TODO: Auto-generated Javadoc /** * The Class Wheel. */ public class Wheel extends View implements OnGestureListener, FlingRunnableView, VibrationWidget { /** The Constant LOG_TAG. */ static final String LOG_TAG = "wheel"; /** * The listener interface for receiving onScroll events. The class that is interested in processing a onScroll event implements * this interface, and the object created with that class is registered with a component using the component's * <code>addOnScrollListener<code> method. When * the onScroll event occurs, that object's appropriate * method is invoked. * * @see OnScrollEvent */ public interface OnScrollListener { /** * On scroll started. * * @param view * the view * @param value * the value * @param roundValue * the round value */ void onScrollStarted( Wheel view, float value, int roundValue ); /** * On scroll. * * @param view * the view * @param value * the value * @param roundValue * the round value */ void onScroll( Wheel view, float value, int roundValue ); /** * On scroll finished. * * @param view * the view * @param value * the value * @param roundValue * the round value */ void onScrollFinished( Wheel view, float value, int roundValue ); } public interface OnLayoutListener { void onLayout( View view ); } /** The Constant MSG_VIBRATE. */ static final int MSG_VIBRATE = 1; /** The m padding left. */ int mPaddingLeft = 0; /** The m padding right. */ int mPaddingRight = 0; /** The m padding top. */ int mPaddingTop = 0; /** The m padding bottom. */ int mPaddingBottom = 0; /** The m height. */ int mWidth, mHeight; /** The m in layout. */ boolean mInLayout = false; /** The m min x. */ int mMaxX, mMinX; /** The m scroll listener. */ OnScrollListener mScrollListener; OnLayoutListener mLayoutListener; Paint mPaint; /** The m shader3. */ Shader mShader3; /** The m tick bitmap. */ Bitmap mTickBitmap; /** The m indicator. */ Bitmap mIndicator; /** The m df. */ DrawFilter mFast, mDF; /** The m gesture detector. */ GestureDetector mGestureDetector; /** The m is first scroll. */ boolean mIsFirstScroll; /** The m fling runnable. */ IFlingRunnable mFlingRunnable; /** The m animation duration. */ int mAnimationDuration = 200; /** The m to left. */ boolean mToLeft; /** The m touch slop. */ int mTouchSlop; /** The m indicator x. */ float mIndicatorX = 0; /** The m original x. */ int mOriginalX = 0; /** The m original delta x. */ int mOriginalDeltaX = 0; /** The m tick space. */ float mTickSpace = 30; /** The m wheel size factor. */ int mWheelSizeFactor = 2; /** The m ticks count. */ int mTicksCount = 18; /** The m ticks size. */ float mTicksSize = 7.0f; /** The m vibrator. */ Vibrator mVibrator; /** The m vibration handler. */ static Handler mVibrationHandler; /** The m ticks easing. */ Easing mTicksEasing = new Sine(); /** The m draw matrix. */ Matrix mDrawMatrix = new Matrix(); /** The m force layout. */ boolean mForceLayout; private int[] mBgColors = { 0xffa1a1a1, 0xffa1a1a1, 0xffffffff, 0xffa1a1a1, 0xffa1a1a1 }; private float[] mBgPositions = { 0, 0.2f, 0.5f, 0.8f, 1f }; /** * Instantiates a new wheel. * * @param context * the context * @param attrs * the attrs * @param defStyle * the def style */ public Wheel( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); init( context, attrs, defStyle ); } /** * Instantiates a new wheel. * * @param context * the context * @param attrs * the attrs */ public Wheel( Context context, AttributeSet attrs ) { this( context, attrs, 0 ); } /** * Instantiates a new wheel. * * @param context * the context */ public Wheel( Context context ) { this( context, null ); } /** * Sets the on scroll listener. * * @param listener * the new on scroll listener */ public void setOnScrollListener( OnScrollListener listener ) { mScrollListener = listener; } public void setOnLayoutListener( OnLayoutListener listener ) { mLayoutListener = listener; } /** * change the current wheel position and value * @param value - the new value. it should be between -1.0f and 1.0f * @param fireScrollEvent - if true this will call the scrollCompletion listener */ public void setValue( float value, boolean fireScrollEvent ) { if( value >= -1 && value <= 1 ) { int w = getRealWidth(); mFlingRunnable.stop( false ); mOriginalDeltaX = (int) ( value * ( w * mWheelSizeFactor ) ); invalidate(); if( fireScrollEvent ) { scrollCompleted(); } } } /** * Inits the. * * @param context * the context * @param attrs * the attrs * @param defStyle * the def style */ private void init( Context context, AttributeSet attrs, int defStyle ) { if ( android.os.Build.VERSION.SDK_INT > 8 ) { try { mFlingRunnable = (IFlingRunnable) ReflectionUtils.newInstance( "com.aviary.android.feather.widget.Fling9Runnable", new Class<?>[] { FlingRunnableView.class, int.class }, this, mAnimationDuration ); } catch ( ReflectionException e ) { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } } else { mFlingRunnable = new Fling8Runnable( this, mAnimationDuration ); } TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.Wheel, defStyle, 0 ); mTicksCount = a.getInteger( R.styleable.Wheel_ticks, 18 ); mWheelSizeFactor = a.getInteger( R.styleable.Wheel_numRotations, 2 ); a.recycle(); mFast = new PaintFlagsDrawFilter( Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG, 0 ); mPaint = new Paint( Paint.FILTER_BITMAP_FLAG ); mGestureDetector = new GestureDetector( context, this ); mGestureDetector.setIsLongpressEnabled( false ); setFocusable( true ); setFocusableInTouchMode( true ); mTouchSlop = ViewConfiguration.get( context ).getScaledTouchSlop(); DisplayMetrics metrics = context.getResources().getDisplayMetrics(); TypedValue.complexToDimensionPixelSize( 25, metrics ); try { mVibrator = (Vibrator) context.getSystemService( Context.VIBRATOR_SERVICE ); } catch ( Exception e ) { Log.e( LOG_TAG, e.toString() ); } if ( mVibrator != null ) { setVibrationEnabled( true ); } setBackgroundDrawable( new LinearGradientDrawable( Orientation.LEFT_RIGHT, mBgColors, mBgPositions ) ); } @Override public synchronized void setVibrationEnabled( boolean value ) { if ( !value ) { mVibrationHandler = null; } else { if ( null == mVibrationHandler ) { mVibrationHandler = new Handler() { @Override public void handleMessage( Message msg ) { super.handleMessage( msg ); switch ( msg.what ) { case MSG_VIBRATE: try { mVibrator.vibrate( 10 ); } catch ( SecurityException e ) { // missing VIBRATE permission } } } }; } } } @Override public synchronized boolean getVibrationEnabled() { return mVibrationHandler != null; } /** * Make bitmap3. * * @param width * the width * @param height * the height * @return the bitmap */ private static Bitmap makeBitmap3( int width, int height ) { Bitmap bm = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ); Canvas c = new Canvas( bm ); Paint p = new Paint( Paint.ANTI_ALIAS_FLAG ); int colors[] = { 0xdd000000, 0x00000000, 0x00000000, 0xdd000000 }; float positions[] = { 0f, 0.2f, 0.8f, 1f }; LinearGradient gradient = new LinearGradient( 0, 0, width, 0, colors, positions, TileMode.REPEAT ); p.setShader( gradient ); p.setDither( true ); c.drawRect( 0, 0, width, height, p ); return bm; } /** * Make ticker bitmap. * * @param width * the width * @param height * the height * @return the bitmap */ private static Bitmap makeTickerBitmap( int width, int height ) { float ellipse = width / 2; Bitmap bm = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ); Canvas c = new Canvas( bm ); Paint p = new Paint( Paint.ANTI_ALIAS_FLAG ); p.setDither( true ); p.setColor( 0xFF888888 ); float h = (float)height; float y = (h+10.0f)/10.0f; float y2 = y*2.5f; RectF rect = new RectF( 0, y, width, height - y2 ); c.drawRoundRect( rect, ellipse, ellipse, p ); p.setColor( 0xFFFFFFFF ); rect = new RectF( 0, y2, width, height - y ); c.drawRoundRect( rect, ellipse, ellipse, p ); p.setColor( 0xFFCCCCCC ); rect = new RectF( 0, y+2, width, height - (y+2) ); c.drawRoundRect( rect, ellipse, ellipse, p ); return bm; } /** * Make bitmap indicator. * * @param width * the width * @param height * the height * @return the bitmap */ private static Bitmap makeBitmapIndicator( int width, int height ) { float ellipse = width / 2; float h = (float)height; float y = (h+10.0f)/10.0f; float y2 = y*2.5f; Bitmap bm = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ); Canvas c = new Canvas( bm ); Paint p = new Paint( Paint.ANTI_ALIAS_FLAG ); p.setDither( true ); p.setColor( 0xFF666666 ); RectF rect = new RectF( 0, y, width, height - y2 ); c.drawRoundRect( rect, ellipse, ellipse, p ); p.setColor( 0xFFFFFFFF ); rect = new RectF( 0, y2, width, height - y ); c.drawRoundRect( rect, ellipse, ellipse, p ); rect = new RectF( 0, y+2, width, height - (y+2) ); int colors[] = { 0xFF0076E7, 0xFF00BBFF, 0xFF0076E7 }; float positions[] = { 0f, 0.5f, 1f }; LinearGradient gradient = new LinearGradient( 0, 0, width, 0, colors, positions, TileMode.REPEAT ); p.setShader( gradient ); c.drawRoundRect( rect, ellipse, ellipse, p ); return bm; } /* * (non-Javadoc) * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw( Canvas canvas ) { super.onDraw( canvas ); if ( mShader3 != null ) { canvas.setDrawFilter( mDF ); final int w = mWidth; int total = mTicksCount; float x2; float scale, scale2; mPaint.setShader( null ); for ( int i = 0; i < total; i++ ) { float x = ( mOriginalDeltaX + ( ( (float) i / total ) * w ) ); if ( x < 0 ) { x = w - ( -x % w ); } else { x = x % w; } scale = (float) mTicksEasing.easeInOut( x, 0, 1.0, mWidth ); scale2 = (float) ( Math.sin( Math.PI * ( x / mWidth ) ) ); mDrawMatrix.reset(); mDrawMatrix.setScale( scale2, 1 ); mDrawMatrix.postTranslate( (int) ( scale * mWidth ) - ( mTicksSize / 2 ), 0 ); canvas.drawBitmap( mTickBitmap, mDrawMatrix, mPaint ); } float indicatorx = ( mIndicatorX + mOriginalDeltaX ); if ( indicatorx < 0 ) { indicatorx = ( mWidth * 2 ) - ( -indicatorx % ( mWidth * 2 ) ); } else { indicatorx = indicatorx % ( mWidth * 2 ); } if ( indicatorx > 0 && indicatorx < mWidth ) { x2 = (float) mTicksEasing.easeInOut( indicatorx, 0, mWidth, w ); scale2 = (float) ( Math.sin( Math.PI * ( indicatorx / mWidth ) ) ); mDrawMatrix.reset(); mDrawMatrix.setScale( scale2, 1 ); mDrawMatrix.postTranslate( x2 - ( mTicksSize / 2 ), 0 ); canvas.drawBitmap( mIndicator, mDrawMatrix, mPaint ); } mPaint.setShader( mShader3 ); canvas.drawPaint( mPaint ); } } /** * Change the background gradient colors. the size of the colors array must be the same as the size of the positions array. * * @param colors * @param positions */ public void setBackgroundColors( int[] colors, float[] positions ) { if ( colors != null && positions != null && colors.length == positions.length ) { mBgColors = colors; mBgPositions = positions; setBackgroundDrawable( new LinearGradientDrawable( Orientation.LEFT_RIGHT, mBgColors, mBgPositions ) ); } } /** * Sets the wheel scale factor. * * @param value * the new wheel scale factor */ public void setWheelScaleFactor( int value ) { mWheelSizeFactor = value; mForceLayout = true; requestLayout(); postInvalidate(); } /** * Gets the wheel scale factor. * * @return the wheel scale factor */ public int getWheelScaleFactor() { return mWheelSizeFactor; } /** * Gets the tick space. * * @return the tick space */ public float getTickSpace() { return mTickSpace; } /* * (non-Javadoc) * * @see android.view.View#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 ); mInLayout = true; if ( changed || mForceLayout ) { mWidth = right - left; mHeight = bottom - top; mTickSpace = (float) mWidth / mTicksCount; mTicksSize = mWidth / mTicksCount / 4.0f; mTicksSize = Math.min( Math.max( mTicksSize, 3.5f ), 6.0f ); mIndicatorX = (float) mWidth / 2.0f; mOriginalX = (int) mIndicatorX; mMaxX = mWidth * mWheelSizeFactor; mIndicator = makeBitmapIndicator( (int) Math.ceil( mTicksSize ), bottom - top ); mTickBitmap = makeTickerBitmap( (int) Math.ceil( mTicksSize ), bottom - top ); mShader3 = new BitmapShader( makeBitmap3( right - left, bottom - top ), Shader.TileMode.CLAMP, Shader.TileMode.REPEAT ); mMinX = -mMaxX; if ( null != mLayoutListener ) { mLayoutListener.onLayout( this ); } } mInLayout = false; mForceLayout = false; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onDown(android.view.MotionEvent) */ @Override public boolean onDown( MotionEvent event ) { mDF = mFast; mFlingRunnable.stop( false ); mIsFirstScroll = true; return true; } /** * On up. */ void onUp() { if ( mFlingRunnable.isFinished() ) { scrollIntoSlots(); } } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float) */ @Override public boolean onFling( MotionEvent event0, MotionEvent event1, float velocityX, float velocityY ) { boolean toleft = velocityX < 0; if ( !toleft ) { if ( mOriginalDeltaX > mMaxX ) { mFlingRunnable.startUsingDistance( mOriginalDeltaX, mMaxX - mOriginalDeltaX ); return true; } } else { if ( mOriginalDeltaX < mMinX ) { mFlingRunnable.startUsingDistance( mOriginalDeltaX, mMinX - mOriginalDeltaX ); return true; } } mFlingRunnable.startUsingVelocity( mOriginalDeltaX, (int) velocityX / 2 ); return true; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onLongPress(android.view.MotionEvent) */ @Override public void onLongPress( MotionEvent arg0 ) {} /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float) */ @Override public boolean onScroll( MotionEvent event0, MotionEvent event1, float distanceX, float distanceY ) { getParent().requestDisallowInterceptTouchEvent( true ); if ( mIsFirstScroll ) { if ( distanceX > 0 ) distanceX -= mTouchSlop; else distanceX += mTouchSlop; scrollStarted(); } mIsFirstScroll = false; float delta = -1 * distanceX; mToLeft = delta < 0; if ( !mToLeft ) { if ( mOriginalDeltaX + delta > mMaxX ) { delta /= ( ( (float) mOriginalDeltaX + delta ) - mMaxX ) / 10; } } else { if ( mOriginalDeltaX + delta < mMinX ) { delta /= -( ( (float) mOriginalDeltaX + delta ) - mMinX ) / 10; } } trackMotionScroll( (int) ( mOriginalDeltaX + delta ) ); return true; } /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onShowPress(android.view.MotionEvent) */ @Override public void onShowPress( MotionEvent arg0 ) {} /* * (non-Javadoc) * * @see android.view.GestureDetector.OnGestureListener#onSingleTapUp(android.view.MotionEvent) */ @Override public boolean onSingleTapUp( MotionEvent arg0 ) { return false; } /* * (non-Javadoc) * * @see android.view.View#onTouchEvent(android.view.MotionEvent) */ @Override public boolean onTouchEvent( MotionEvent event ) { boolean retValue = mGestureDetector.onTouchEvent( event ); int action = event.getAction(); if ( action == MotionEvent.ACTION_UP ) { mDF = null; onUp(); } else if ( action == MotionEvent.ACTION_CANCEL ) {} return retValue; } /** The m last motion value. */ float mLastMotionValue; @Override public void trackMotionScroll( int newX ) { mOriginalDeltaX = newX; scrollRunning(); invalidate(); } /** * Gets the limited motion scroll amount. * * @param motionToLeft * the motion to left * @param deltaX * the delta x * @return the limited motion scroll amount */ int getLimitedMotionScrollAmount( boolean motionToLeft, int deltaX ) { if ( motionToLeft ) {} else { if ( mMaxX >= mOriginalDeltaX ) { // The extreme child is past his boundary point! return deltaX; } } int centerDifference = mOriginalDeltaX - mMaxX; return motionToLeft ? Math.max( centerDifference, deltaX ) : Math.min( centerDifference, deltaX ); } @Override public void scrollIntoSlots() { if ( !mFlingRunnable.isFinished() ) { return; } if ( mOriginalDeltaX > mMaxX ) { mFlingRunnable.startUsingDistance( mOriginalDeltaX, mMaxX - mOriginalDeltaX ); return; } else if ( mOriginalDeltaX < mMinX ) { mFlingRunnable.startUsingDistance( mOriginalDeltaX, mMinX - mOriginalDeltaX ); return; } int diff = Math.round( mOriginalDeltaX % mTickSpace ); int diff2 = (int) ( mTickSpace - diff ); int diff3 = (int) ( mTickSpace + diff ); if ( diff != 0 && diff2 != 0 && diff3 != 0 ) { if ( Math.abs( diff ) < ( mTickSpace / 2 ) ) { mFlingRunnable.startUsingDistance( mOriginalDeltaX, -diff ); } else { mFlingRunnable.startUsingDistance( mOriginalDeltaX, (int) ( mToLeft ? -diff3 : diff2 ) ); } } else { onFinishedMovement(); } } /** * On finished movement. */ private void onFinishedMovement() { scrollCompleted(); } /** * Gets the real width. * * @return the real width */ private int getRealWidth() { return ( getWidth() - mPaddingLeft - mPaddingRight ); } /** The m scroll selection notifier. */ ScrollSelectionNotifier mScrollSelectionNotifier; /** * The Class ScrollSelectionNotifier. */ private class ScrollSelectionNotifier implements Runnable { /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { if ( mShader3 == null ) { post( this ); } else { fireOnScrollCompleted(); } } } /** * Scroll completed. */ void scrollCompleted() { if ( mScrollListener != null ) { if ( mInLayout ) { if ( mScrollSelectionNotifier == null ) { mScrollSelectionNotifier = new ScrollSelectionNotifier(); } post( mScrollSelectionNotifier ); } else { fireOnScrollCompleted(); } } } /** * Scroll started. */ void scrollStarted() { if ( mScrollListener != null ) { if ( mInLayout ) { if ( mScrollSelectionNotifier == null ) { mScrollSelectionNotifier = new ScrollSelectionNotifier(); } post( mScrollSelectionNotifier ); } else { fireOnScrollStarted(); } } } /** * Scroll running. */ void scrollRunning() { if ( mScrollListener != null ) { if ( mInLayout ) { if ( mScrollSelectionNotifier == null ) { mScrollSelectionNotifier = new ScrollSelectionNotifier(); } post( mScrollSelectionNotifier ); } else { fireOnScrollRunning(); } } } /** * Gets the value. * * @return the value */ public float getValue() { int w = getRealWidth(); int position = mOriginalDeltaX; float value = (float) position / ( w * mWheelSizeFactor ); return value; } /** * Gets the tick value. * * @return the tick value */ int getTickValue() { return (int) ( ( getCurrentPage() * mTicksCount ) + ( mOriginalDeltaX % mWidth ) / mTickSpace ); } /** * Return the total number of ticks available for scrolling. * * @return the ticks count */ public int getTicksCount() { try { return (int) ( ( ( mMaxX / mWidth ) * mTicksCount ) + ( mOriginalDeltaX % mWidth ) / mTickSpace ) * 2; } catch ( ArithmeticException e ) { return 0; } } /** * Gets the ticks. * * @return the ticks */ public int getTicks() { return mTicksCount; } /** * Gets the current page. * * @return the current page */ int getCurrentPage() { return ( mOriginalDeltaX / mWidth ); } /** * Fire on scroll completed. */ private void fireOnScrollCompleted() { mScrollListener.onScrollFinished( this, getValue(), getTickValue() ); } /** * Fire on scroll started. */ private void fireOnScrollStarted() { mScrollListener.onScrollStarted( this, getValue(), getTickValue() ); } /** * Fire on scroll running. */ private void fireOnScrollRunning() { int value = getTickValue(); if ( value != mLastMotionValue ) { if ( mVibrationHandler != null ) { mVibrationHandler.sendEmptyMessage( MSG_VIBRATE ); } } mScrollListener.onScroll( this, getValue(), value ); mLastMotionValue = value; } @Override public int getMinX() { return mMinX; } @Override public int getMaxX() { return mMaxX; } }