package com.android.facelock; import java.util.Random; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.MotionEvent; import android.widget.ImageView; public class PicturePasswordView extends ImageView { public interface OnFingerUpListener { void onFingerUp( PicturePasswordView picturePassword, boolean shouldUnlock ); } // The seed for the random number generator private int mSeed; private final boolean DEBUG = false; private static final int DEFAULT_GRID_SIZE = 10; private static final int FONT_SIZE = 20; private static final int COLOR_UNLOCK_CIRCLE_OFF = Color.rgb( 150, 150, 150 ); private static final int COLOR_UNLOCK_CIRCLE_ON = Color.rgb( 132, 212, 39 ); // How far we have scrolled from (0, 0) private float mScrollX = 0; private float mScrollY = 0; // The current position of the finger, used for scrolling calculations private float mFingerX; private float mFingerY; // Size of the number 8. private Rect mTextBounds; // Paint objects private Paint mPaint; // Paint for text private Paint mCirclePaint; // Paint for circles private Paint mUnlockPaint; // Paint for unlock circles // Grid size and grid size without randomization private int mGridSize; private int mActualSize; // Whether we should show numbers or not private boolean mShowNumbers; // Scalar for fade animations private float mScale; private ObjectAnimator mAnimator; // PRNG object private Random mRandom; // Whether we should highlight any number, and if so, which number private boolean mHighlight = false; private int mHighlightX; private int mHighlightY; // The position in the image of the highlighted number (0..1) private float mHighlightImageX; private float mHighlightImageY; // Listener we should notify when the user lifts their finger private OnFingerUpListener mListener; // Number + position combo required to unlock device private int mUnlockNumber = -1; private float mUnlockNumberX = -1; private float mUnlockNumberY = -1; // Whether we should unlock next time the user lifts their finger private boolean mShouldUnlock = false; // Whether we should highlight our unlock number private boolean mHighlightUnlockNumber = false; // Whether we're resetting. Used in setScale to reset parameters when scale is 0 private boolean mResetting = false; // Whether the user has enabled grid size randomization private boolean mRandomGridSize = false; // Number of unlock circles to show at the bottom private int mUnlockCircles = 0; // Number of filled unlock circles // The decimal part is used to fade in/out the rightmost circle private float mUnlockProgress = 0; // Size/spacing/padding of unlock circles private int mCircleSize; private int mCircleSpacing; private int mCirclePadding; // Animator for circle progress private ObjectAnimator mCircleAnimator; private int getNumberForXY( int x, int y ) { // TODO: still sucks return Math.abs( mSeed ^ ( x * 2138105 + 1 ) * ( y + 1 * 23490 ) ) % 10; } public PicturePasswordView( Context context, AttributeSet attrs ) { super( context, attrs ); setScaleType( ScaleType.CENTER_CROP ); mRandom = new Random(); mSeed = mRandom.nextInt(); mGridSize = DEFAULT_GRID_SIZE; /////////////////////// // Initialize Paints // /////////////////////// final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); final float shadowOff = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, displayMetrics ); mPaint = new Paint( Paint.LINEAR_TEXT_FLAG ); mPaint.setColor( Color.WHITE ); mPaint.setShadowLayer( 10, shadowOff, shadowOff, Color.BLACK ); mPaint.setTextSize( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, FONT_SIZE, displayMetrics ) ); mPaint.setAntiAlias( true ); mCirclePaint = new Paint( Paint.ANTI_ALIAS_FLAG ); mCirclePaint.setColor( Color.argb( 255, 0x33, 0xb5, 0xe5 ) ); mCirclePaint.setStyle( Paint.Style.STROKE ); mCirclePaint.setStrokeWidth( 5 ); mUnlockPaint = new Paint( Paint.ANTI_ALIAS_FLAG ); mTextBounds = new Rect(); mPaint.getTextBounds( "8", 0, 1, mTextBounds ); /////////////////////////// // Initialize animations // /////////////////////////// mScale = 1.0f; mAnimator = new ObjectAnimator(); mAnimator.setTarget( this ); mAnimator.setFloatValues( 0, 1 ); mAnimator.setPropertyName( "scale" ); mAnimator.setDuration( 200 ); mCircleAnimator = new ObjectAnimator(); mCircleAnimator.setTarget( this ); mCircleAnimator.setPropertyName( "internalUnlockProgress" ); // ugh! mCircleAnimator.setDuration( 300 ); /////////////////////// // Hide/show numbers // /////////////////////// mShowNumbers = true; TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PicturePasswordView, 0, 0 ); try { mShowNumbers = a.getBoolean( R.styleable.PicturePasswordView_showNumbers, true ); } finally { a.recycle(); } ////////////////////// // Initialize sizes // ////////////////////// mCircleSize = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 6, displayMetrics ); mCircleSpacing = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, displayMetrics ); mCirclePadding = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 10, displayMetrics ); } public boolean isShowNumbers() { return mShowNumbers; } public void reset() { mResetting = true; mAnimator.setDuration( 400 ); // Repeat 0 to ensure setScale( 0 ) is called at least once mAnimator.setFloatValues( 1, 0, 0, 1 ); mAnimator.start(); setEnabled( false ); } public void setShowNumbers( boolean show ) { setShowNumbers( show, false ); invalidate(); } public void setScale( float scale ) { if ( mScale != scale ) { mScale = scale; invalidate(); if ( mResetting && ( scale == 0 || scale == 1 ) ) { if ( scale == 0 ) { mSeed = mRandom.nextInt(); mScrollX = 0; mScrollY = 0; if ( mRandomGridSize ) { setGridSize( mActualSize ); } } else { mAnimator.setFloatValues( 0, 1 ); mAnimator.setDuration( 200 ); mResetting = false; setEnabled( true ); } } } } public float getScale() { return mScale; } public void setShowNumbers( boolean show, boolean animate ) { if ( animate ) { mShowNumbers = true; if ( show ) mAnimator.start(); else mAnimator.reverse(); } else { mShowNumbers = show; mScale = show ? 1.0f : 0.0f; } invalidate(); } public Point findNumberInGrid( int number ) { if ( number < 0 || number > 9 ) return null; for ( int x = 0; x < mGridSize; x++ ) { for ( int y = 0; y < mGridSize; y++ ) { if ( getNumberForXY( x, y ) == number ) { return new Point( x, y ); } } } return null; } public void enforceNumber( int number ) { if ( number < 0 || number > 9 ) return; while ( findNumberInGrid( number ) == null ) { mSeed = mRandom.nextInt(); } } public void setFocusNumber( int number ) { if ( number >= 0 && number <= 9 ) { mHighlight = true; mScrollX = mScrollY = 0; enforceNumber( number ); Point position = findNumberInGrid( number ); mHighlightX = position.x; mHighlightY = position.y; } else { mHighlight = false; } } public void setUnlockNumber( int number, float x, float y ) { mUnlockNumber = number; mUnlockNumberX = x; mUnlockNumberY = y; } public void setOnFingerUpListener( OnFingerUpListener l ) { mListener = l; } public void setGridSize( int size ) { if ( size > 3 && size <= 8 ) { mGridSize = mActualSize = size; if ( mRandomGridSize ) { mGridSize = mActualSize + mRandom.nextInt( 3 ) - 1; } invalidate(); } } public void setRandomize( boolean randomize ) { mRandomGridSize = randomize; setGridSize( mActualSize ); } public int getGridSize() { return mGridSize; } public PointF getHighlightPosition() { if ( mHighlight == false ) return null; return new PointF( mHighlightImageX, mHighlightImageY ); } public void setHighlightUnlockNumber( boolean highlight ) { mHighlightUnlockNumber = highlight; } public void setUnlockCircles( int circles ) { mUnlockCircles = circles; } public void setInternalUnlockProgress( float progress ) { mUnlockProgress = progress; } public float getInternalUnlockProgress() { return mUnlockProgress; } public void setUnlockProgress( int progress ) { mCircleAnimator.setFloatValues( mUnlockProgress, progress ); mCircleAnimator.start(); } private static int lerp( int a, int b, float t ) { return ( int ) ( t * ( b - a ) + a ); } @Override protected void onDraw( Canvas canvas ) { super.onDraw( canvas ); if ( !mShowNumbers ) return; mPaint.setAlpha( ( int ) ( mScale * ( float ) ( ( mHighlight || mHighlightUnlockNumber ) ? 64 : 255 ) ) ); final float cellSize = ( canvas.getWidth() / ( float ) mGridSize ) * ( mScale * 0.4f + 0.6f ); final float xOffset = ( 1.0f - ( mScale * 0.4f + 0.6f ) ) * canvas.getWidth() / 2; final float yOffset = ( 1.0f - ( mScale * 0.4f + 0.6f ) ) * canvas.getWidth() / 2; float drawX = -cellSize / 1.5F + xOffset; mShouldUnlock = false; for ( int x = -1; x < mGridSize + 1; x++ ) { float drawY = -mTextBounds.bottom + cellSize / 1.5F - cellSize + yOffset; for ( int y = -1; y < mGridSize + 1; y++ ) { if ( DEBUG ) { if ( x == -1 || y == -1 || x == mGridSize || y == mGridSize ) { mPaint.setColor( Color.RED ); } else { mPaint.setColor( Color.WHITE ); } } int cellX = ( int ) ( x - Math.floor( mScrollX / cellSize ) ); int cellY = ( int ) ( y - Math.floor( mScrollY / cellSize ) ); if ( mScrollX / cellSize <= 0 && cellX != 0 && mScrollX != 0 ) cellX--; if ( mScrollY / cellSize <= 0 && cellY != 0 && mScrollY != 0 ) cellY--; float numX = drawX + mScrollX % cellSize; float numY = drawY + mScrollY % cellSize; Integer number = getNumberForXY( cellX, cellY ); boolean shouldHighlight = false; if ( number == mUnlockNumber ) { float unlockX = mUnlockNumberX * getWidth(); float unlockY = mUnlockNumberY * getWidth(); float dist = PointF.length( unlockX - numX, unlockY - numY ); if ( dist < mTextBounds.right * 1.3f ) { mShouldUnlock = true; if ( mHighlightUnlockNumber ) shouldHighlight = true; } } if ( ( mHighlight && mHighlightX == cellX && mHighlightY == cellY ) || shouldHighlight ) { mPaint.setAlpha( ( int ) ( mScale * 255 ) ); canvas.drawCircle( numX + ( mTextBounds.right - mTextBounds.left ) / 2, numY + mTextBounds.top / 2, mPaint.getTextSize() / 1.5f, mCirclePaint ); } canvas.drawText( number.toString(), numX, numY, mPaint ); if ( ( mHighlight && mHighlightX == cellX && mHighlightY == cellY ) || shouldHighlight ) { mHighlightImageX = numX / getWidth(); mHighlightImageY = numY / getHeight(); mPaint.setAlpha( ( int ) ( mScale * 64 ) ); } drawY += cellSize; } drawX += cellSize; } int circlesWidth = mCircleSize * mUnlockCircles + mCircleSpacing * ( mUnlockCircles - 1 ); int x = canvas.getWidth() / 2 - circlesWidth / 2; int y = canvas.getHeight() - mCirclePadding - mCircleSize / 2; int fullCircles = ( int ) Math.floor( mUnlockProgress ); float partCircles = mUnlockProgress - fullCircles; for ( int i = 1; i < mUnlockCircles + 1; i++ ) { if ( i <= fullCircles ) { mUnlockPaint.setColor( COLOR_UNLOCK_CIRCLE_ON ); } else if ( i == fullCircles + 1 ) { int r = lerp( Color.red( COLOR_UNLOCK_CIRCLE_OFF ), Color.red( COLOR_UNLOCK_CIRCLE_ON ), partCircles ); int g = lerp( Color.green( COLOR_UNLOCK_CIRCLE_OFF ), Color.green( COLOR_UNLOCK_CIRCLE_ON ), partCircles ); int b = lerp( Color.blue( COLOR_UNLOCK_CIRCLE_OFF ), Color.blue( COLOR_UNLOCK_CIRCLE_ON ), partCircles ); mUnlockPaint.setColor( Color.rgb( r, g, b ) ); } else { mUnlockPaint.setColor( COLOR_UNLOCK_CIRCLE_OFF ); } mUnlockPaint.setAlpha( 150 ); canvas.drawCircle( x + mCircleSize / 2, y, mCircleSize, mUnlockPaint ); x += mCircleSize * 2 + mCircleSpacing; } if ( DEBUG ) { canvas.drawText( mScrollX / cellSize + "," + mScrollY / cellSize, 0, mTextBounds.bottom * 26.5f, mPaint ); } } @Override public boolean onTouchEvent( MotionEvent event ) { if ( !isEnabled() ) return true; float x = event.getX(); float y = event.getY(); switch( event.getAction() ) { case MotionEvent.ACTION_DOWN: mFingerX = x; mFingerY = y; break; case MotionEvent.ACTION_MOVE: float diffx = x - mFingerX; float diffy = y - mFingerY; mScrollX += diffx; mScrollY += diffy; mFingerX = x; mFingerY = y; invalidate(); break; case MotionEvent.ACTION_UP: if ( mListener != null ) { mListener.onFingerUp( this, mShouldUnlock ); } } return true; // super.onTouchEvent( event ); } }