Java tutorial
/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.authme.sdk.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Build; import android.os.Debug; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.support.v4.view.VelocityTrackerCompat; import android.util.AttributeSet; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import io.authme.sdk.R; import io.authme.sdk.sensors.Accelerometer; import io.authme.sdk.sensors.Gyroscope; import io.authme.sdk.sensors.Magnetic; import io.authme.sdk.sensors.Orientation; import io.authme.sdk.sensors.RawXY; import io.authme.sdk.sensors.Velocity; import io.authme.sdk.server.FormData; import io.authme.sdk.util.FloatAnimator; import io.authme.sdk.util.ResourceUtils; import io.authme.sdk.widget.LockPatternUtils; /** * Displays and detects the user's unlock attempt, which is a drag of a finger across 9 regions of the screen. * <p/> * Is also capable of displaying a static pattern in "in progress", "wrong" or "correct" states. */ public class LockPatternView extends View implements SensorEventListener { private static boolean collectSensor = false; private SensorManager senSensorManager; private Sensor senAccelerometer, senMagnetometer, sensGyro; private ArrayList<Accelerometer> accelList; private ArrayList<Magnetic> magnetics; private ArrayList<Gyroscope> gyrolist; private ArrayList<RawXY> rawXYList; private ArrayList<Velocity> velocityList; private ArrayList<Orientation> orientationArrayList; private VelocityTracker mVelocityTracker = null; private float lastupdatedAccel = 0, lastupdatedGyro = 0; float[] mGravity; float[] mGeomagnetic; private static final float ALPHA = 0.45f; @Override public void onSensorChanged(SensorEvent event) { if (collectSensor) { Sensor sensor = event.sensor; if (sensor.getType() == Sensor.TYPE_ACCELEROMETER) { mGravity = event.values; Accelerometer sensordata = new Accelerometer(mGravity[0], mGravity[1], mGravity[2], event.timestamp); accelList.add(sensordata); } if (sensor.getType() == Sensor.TYPE_GRAVITY) { mGravity = event.values; Accelerometer sensordata = new Accelerometer(mGravity[0], mGravity[1], mGravity[2], event.timestamp); accelList.add(sensordata); } if (sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { mGeomagnetic = event.values; Magnetic magnetic = new Magnetic(mGeomagnetic[0], mGeomagnetic[1], mGeomagnetic[2], event.timestamp); magnetics.add(magnetic); } if (sensor.getType() == Sensor.TYPE_GYROSCOPE) { float x = event.values[0]; float y = event.values[1]; float z = event.values[2]; Gyroscope gyro = new Gyroscope(x, y, z, event.timestamp); gyrolist.add(gyro); } if (mGravity != null && mGeomagnetic != null) { float R[] = new float[9]; float I[] = new float[9]; boolean success = SensorManager.getRotationMatrix(R, I, mGravity, mGeomagnetic); if (success) { float orientation[] = new float[3]; SensorManager.getOrientation(R, orientation); Orientation orientationObj = new Orientation(orientation[0], orientation[1], orientation[2], event.timestamp); orientationArrayList.add(orientationObj); } } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } /** * Represents a cell in the MATRIX_WIDTH x MATRIX_WIDTH matrix of the unlock pattern view. */ public static class Cell implements Parcelable { /** * Row. */ public final int row; /** * Column. */ public final int column; /* * keep # objects limited to MATRIX_SIZE */ static Cell[][] sCells = new Cell[MATRIX_WIDTH][MATRIX_WIDTH]; static { for (int i = 0; i < MATRIX_WIDTH; i++) { for (int j = 0; j < MATRIX_WIDTH; j++) { sCells[i][j] = new Cell(i, j); } } } /** * @param row The row of the cell. * @param column The column of the cell. */ private Cell(int row, int column) { checkRange(row, column); this.row = row; this.column = column; } /** * Gets the ID.It is counted from left to right, top to bottom of the matrix, starting by zero. * * @return the ID. */ public int getId() { return row * MATRIX_WIDTH + column; }// getId() /** * @param row The row of the cell. * @param column The column of the cell. */ public static synchronized Cell of(int row, int column) { checkRange(row, column); return sCells[row][column]; } /** * Gets a cell from its ID. * * @param id the cell ID. * @return the cell. * @author Shardul * @since v2.7 beta */ public static synchronized Cell of(int id) { return of(id / MATRIX_WIDTH, id % MATRIX_WIDTH); }// of() private static void checkRange(int row, int column) { if (row < 0 || row > MATRIX_WIDTH - 1) { throw new IllegalArgumentException("row must be in range 0-" + (MATRIX_WIDTH - 1)); } if (column < 0 || column > MATRIX_WIDTH - 1) { throw new IllegalArgumentException("column must be in range 0-" + (MATRIX_WIDTH - 1)); } } @Override public String toString() { return "(ROW=" + row + ",COL=" + column + ")"; }// toString() @Override public boolean equals(Object object) { if (object instanceof Cell) return column == ((Cell) object).column && row == ((Cell) object).row; return super.equals(object); }// equals() /* * PARCELABLE */ @Override public int describeContents() { return 0; }// describeContents() @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(column); dest.writeInt(row); }// writeToParcel() public static final Parcelable.Creator<Cell> CREATOR = new Parcelable.Creator<Cell>() { public Cell createFromParcel(Parcel in) { return new Cell(in); }// createFromParcel() public Cell[] newArray(int size) { return new Cell[size]; }// newArray() };// CREATOR private Cell(Parcel in) { column = in.readInt(); row = in.readInt(); }// Cell() }// Cell /** * How to display the current pattern. */ public enum DisplayMode { /** * The pattern drawn is correct (i.e draw it in a friendly color) */ Correct, /** * Animate the pattern (for demo, and help). */ Animate, /** * The pattern is wrong (i.e draw a foreboding color) */ Wrong } /** * The call back interface for detecting patterns entered by the user. */ public static interface OnPatternListener { /** * A new pattern has begun. */ void onPatternStart(); /** * The pattern was cleared. */ void onPatternCleared(); /** * The user extended the pattern currently being drawn by one cell. * * @param pattern The pattern with newly added cell. */ void onPatternCellAdded(List<Cell> pattern); /** * A pattern was detected from the user. * * @param pattern The pattern. * @param recordJson */ void onPatternDetected(List<Cell> pattern, JSONObject recordJson); } // Aspect to use when rendering this view private static final int ASPECT_SQUARE = 0; // View will be the minimum of // width/height private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will // be minimum of (w,h) private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will // be minimum of (w,h) /** * This is the width of the matrix (the number of dots per row and column). Change this value to change the * dimension of the pattern's matrix. * * @author Thomas Breitbach * @since v2.7 beta */ public static final int MATRIX_WIDTH = 4; /** * The size of the pattern's matrix. */ public static final int MATRIX_SIZE = MATRIX_WIDTH * MATRIX_WIDTH; private static final boolean PROFILE_DRAWING = false; private final CellState[][] mCellStates; private final int mDotSize; private final int mDotSizeActivated; private final int mPathWidth; private boolean mDrawingProfilingStarted = false; private Paint mPaint = new Paint(); private Paint mPathPaint = new Paint(); /** * How many milliseconds we spend animating each circle of a lock pattern if the animating mode is set. The entire * animation should take this constant * the length of the pattern to complete. */ private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; /** * This can be used to avoid updating the display for very small motions or noisy panels. It didn't seem to have * much impact on the devices tested, so currently set to 0. */ private static final float DRAG_THRESHHOLD = 0.0f; private OnPatternListener mOnPatternListener; private ArrayList<Cell> mPattern = new ArrayList<Cell>(MATRIX_SIZE); /** * Lookup table for the circles of the pattern we are currently drawing. This will be the cells of the complete * pattern unless we are animating, in which case we use this to hold the cells we are drawing for the in progress * animation. */ private boolean[][] mPatternDrawLookup = new boolean[MATRIX_WIDTH][MATRIX_WIDTH]; /** * the in progress point: - during interaction: where the user's finger is - during animation: the current tip of * the animating line */ private float mInProgressX = -1; private float mInProgressY = -1; private long mAnimatingPeriodStart; private DisplayMode mPatternDisplayMode = DisplayMode.Correct; private boolean mInputEnabled = true; private boolean mInStealthMode = false; private boolean mEnableHapticFeedback = true; private boolean mPatternInProgress = false; private float mHitFactor = 0.6f; private float mSquareWidth; private float mSquareHeight; private final Path mCurrentPath = new Path(); private final Rect mInvalidate = new Rect(); private final Rect mTmpInvalidateRect = new Rect(); private int mAspect; private int mRegularColor; private int mErrorColor; private int mSuccessColor; private Interpolator mFastOutSlowInInterpolator; private Interpolator mLinearOutSlowInInterpolator; public static class CellState { public float scale = 1.0f; public float translateY = 0.0f; public float alpha = 1.0f; public float size; public float lineEndX = Float.MIN_VALUE; public float lineEndY = Float.MIN_VALUE; public ValueAnimator lineAnimator; } public LockPatternView(Context context) { this(context, null); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public LockPatternView(Context context, AttributeSet attrs) { super(context, attrs); senSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); senAccelerometer = senSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); if (senAccelerometer == null) { senAccelerometer = senSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); } senSensorManager.registerListener(this, senAccelerometer, SensorManager.SENSOR_DELAY_GAME); sensGyro = senSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); if (sensGyro != null) { senSensorManager.registerListener(this, sensGyro, SensorManager.SENSOR_DELAY_GAME); } senMagnetometer = senSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); if (senMagnetometer != null) { senSensorManager.registerListener(this, senMagnetometer, SensorManager.SENSOR_DELAY_GAME); } accelList = new ArrayList<>(); magnetics = new ArrayList<>(); gyrolist = new ArrayList<>(); rawXYList = new ArrayList<>(); velocityList = new ArrayList<>(); orientationArrayList = new ArrayList<>(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Alp_42447968_LockPatternView); final String aspect = a.getString(R.styleable.Alp_42447968_LockPatternView_alp_42447968_aspect); if ("square".equals(aspect)) { mAspect = ASPECT_SQUARE; } else if ("lock_width".equals(aspect)) { mAspect = ASPECT_LOCK_WIDTH; } else if ("lock_height".equals(aspect)) { mAspect = ASPECT_LOCK_HEIGHT; } else { mAspect = ASPECT_SQUARE; } setClickable(true); mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); mRegularColor = getResources().getColor( ResourceUtils.resolveAttribute(getContext(), R.attr.alp_42447968_color_lock_pattern_view_regular)); mErrorColor = getResources().getColor( ResourceUtils.resolveAttribute(getContext(), R.attr.alp_42447968_color_lock_pattern_view_error)); mSuccessColor = getResources().getColor( ResourceUtils.resolveAttribute(getContext(), R.attr.alp_42447968_color_lock_pattern_view_success)); mRegularColor = a.getColor(R.styleable.Alp_42447968_LockPatternView_alp_42447968_regularColor, mRegularColor); mErrorColor = a.getColor(R.styleable.Alp_42447968_LockPatternView_alp_42447968_errorColor, mErrorColor); mSuccessColor = a.getColor(R.styleable.Alp_42447968_LockPatternView_alp_42447968_successColor, mSuccessColor); int pathColor = a.getColor(R.styleable.Alp_42447968_LockPatternView_alp_42447968_pathColor, mRegularColor); mPathPaint.setColor(pathColor); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); mPathWidth = getResources().getDimensionPixelSize(R.dimen.alp_42447968_lock_pattern_dot_line_width); mPathPaint.setStrokeWidth(mPathWidth); mDotSize = getResources().getDimensionPixelSize(R.dimen.alp_42447968_lock_pattern_dot_size); mDotSizeActivated = getResources() .getDimensionPixelSize(R.dimen.alp_42447968_lock_pattern_dot_size_activated); mPaint.setAntiAlias(true); mPaint.setDither(true); mCellStates = new CellState[MATRIX_WIDTH][MATRIX_WIDTH]; for (int i = 0; i < MATRIX_WIDTH; i++) { for (int j = 0; j < MATRIX_WIDTH; j++) { mCellStates[i][j] = new CellState(); mCellStates[i][j].size = mDotSize; } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isInEditMode()) { mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); } // if }// LockPatternView() public CellState[][] getCellStates() { return mCellStates; } /** * @return Whether the view is in stealth mode. */ public boolean isInStealthMode() { return mInStealthMode; } /** * @return Whether the view has tactile feedback enabled. */ public boolean isTactileFeedbackEnabled() { return mEnableHapticFeedback; } /** * Set whether the view is in stealth mode. If {@code true}, there will be no visible feedback as the user enters * the pattern. * * @param inStealthMode Whether in stealth mode. */ public void setInStealthMode(boolean inStealthMode) { mInStealthMode = inStealthMode; } /** * Set whether the view will use tactile feedback. If {@code true}, there will be tactile feedback as the user * enters the pattern. * * @param tactileFeedbackEnabled Whether tactile feedback is enabled */ public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { mEnableHapticFeedback = tactileFeedbackEnabled; } /** * Set the call back for pattern detection. * * @param onPatternListener The call back. */ public void setOnPatternListener(OnPatternListener onPatternListener) { mOnPatternListener = onPatternListener; } /** * Retrieves current pattern. * * @return current displaying pattern. <b>Note:</b> This is an independent list with the view's pattern itself. */ @SuppressWarnings("unchecked") public List<Cell> getPattern() { return (List<Cell>) mPattern.clone(); }// getPattern() /** * Set the pattern explicitely (rather than waiting for the user to input a pattern). * * @param displayMode How to display the pattern. * @param pattern The pattern. */ public void setPattern(DisplayMode displayMode, List<Cell> pattern) { mPattern.clear(); mPattern.addAll(pattern); clearPatternDrawLookup(); for (Cell cell : pattern) { mPatternDrawLookup[cell.row][cell.column] = true; } setDisplayMode(displayMode); } /** * Gets display mode. * * @return display mode. */ public DisplayMode getDisplayMode() { return mPatternDisplayMode; }// getDisplayMode() /** * Set the display mode of the current pattern. This can be useful, for instance, after detecting a pattern to tell * this view whether change the in progress result to correct or wrong. * * @param displayMode The display mode. */ public void setDisplayMode(DisplayMode displayMode) { mPatternDisplayMode = displayMode; if (displayMode == DisplayMode.Animate) { if (mPattern.size() == 0) { throw new IllegalStateException( "you must have a pattern to " + "animate if you want to set the display mode to animate"); } mAnimatingPeriodStart = SystemClock.elapsedRealtime(); final Cell first = mPattern.get(0); mInProgressX = getCenterXForColumn(first.column); mInProgressY = getCenterYForRow(first.row); clearPatternDrawLookup(); } invalidate(); } private void notifyCellAdded() { sendAccessEvent(R.string.alp_42447968_lockscreen_access_pattern_cell_added); if (mOnPatternListener != null) { mOnPatternListener.onPatternCellAdded(mPattern); } } private void notifyPatternStarted() { sendAccessEvent(R.string.alp_42447968_lockscreen_access_pattern_start); collcectSensorIps(); if (mOnPatternListener != null) { mOnPatternListener.onPatternStart(); } } private void notifyPatternDetected() { sendAccessEvent(R.string.alp_42447968_lockscreen_access_pattern_detected); JSONObject recordJson = stopSensorIps(); if (mOnPatternListener != null) { mOnPatternListener.onPatternDetected(mPattern, recordJson); } } private void notifyPatternCleared() { sendAccessEvent(R.string.alp_42447968_lockscreen_access_pattern_cleared); if (mOnPatternListener != null) { mOnPatternListener.onPatternCleared(); } } private void collcectSensorIps() { collectSensor = true; } private JSONObject stopSensorIps() { collectSensor = false; FormData sensordata = new FormData(accelList, magnetics, gyrolist, rawXYList, velocityList, orientationArrayList); //Log.d("OrientationArrayList", orientationArrayList.) JSONObject biggerJson = sensordata.formJson(); // storeJson(biggerJson); clearArrayList(); return biggerJson; } private void storeJson(JSONObject jsonObject) { SharedPreferences settings = this.getContext().getSharedPreferences(this.getContext().getPackageName(), Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); editor.putString("pattern_json", jsonObject.toString()); editor.commit(); } private void clearArrayList() { accelList.clear(); magnetics.clear(); gyrolist.clear(); rawXYList.clear(); velocityList.clear(); } private void setDeviceId(long number, Context context) { SharedPreferences deviceData = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); SharedPreferences.Editor editor = deviceData.edit(); editor.putString("IEMI", String.valueOf(number)); editor.commit(); editor.clear(); } protected float[] lowPass(float[] input, float[] output) { if (output == null) return input; for (int i = 0; i < input.length; i++) { output[i] = output[i] + ALPHA * (input[i] - output[i]); } return output; } /** * Clear the pattern. */ public void clearPattern() { resetPattern(); } /** * Reset all pattern state. */ private void resetPattern() { mPattern.clear(); clearPatternDrawLookup(); mPatternDisplayMode = DisplayMode.Correct; invalidate(); } /** * yy * Clear the pattern lookup table. */ private void clearPatternDrawLookup() { for (int i = 0; i < MATRIX_WIDTH; i++) { for (int j = 0; j < MATRIX_WIDTH; j++) { mPatternDrawLookup[i][j] = false; } } } /** * Disable input (for instance when displaying a message that will timeout so user doesn't get view into messy * state). */ public void disableInput() { mInputEnabled = false; } /** * Enable input. */ public void enableInput() { mInputEnabled = true; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { final int width = w - getPaddingLeft() - getPaddingRight(); mSquareWidth = width / (float) MATRIX_WIDTH; final int height = h - getPaddingTop() - getPaddingBottom(); mSquareHeight = height / (float) MATRIX_WIDTH; } private int resolveMeasured(int measureSpec, int desired) { int result = 0; int specSize = MeasureSpec.getSize(measureSpec); switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.UNSPECIFIED: result = desired; break; case MeasureSpec.AT_MOST: result = Math.max(specSize, desired); break; case MeasureSpec.EXACTLY: default: result = specSize; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int minimumWidth = getSuggestedMinimumWidth(); final int minimumHeight = getSuggestedMinimumHeight(); int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); switch (mAspect) { case ASPECT_SQUARE: viewWidth = viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_WIDTH: viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_HEIGHT: viewWidth = Math.min(viewWidth, viewHeight); break; } // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + // viewHeight); setMeasuredDimension(viewWidth, viewHeight); } /** * Determines whether the point x, y will add a new point to the current pattern (in addition to finding the cell, * also makes heuristic choices such as filling in gaps based on current pattern). * * @param x The x coordinate. * @param y The y coordinate. */ @TargetApi(Build.VERSION_CODES.ECLAIR) private Cell detectAndAddHit(float x, float y) { final Cell cell = checkForNewHit(x, y); if (cell != null) { // check for gaps in existing pattern Cell fillInGapCell = null; final ArrayList<Cell> pattern = mPattern; if (!pattern.isEmpty()) { final Cell lastCell = pattern.get(pattern.size() - 1); int dRow = cell.row - lastCell.row; int dColumn = cell.column - lastCell.column; int fillInRow = lastCell.row; int fillInColumn = lastCell.column; if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); } if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); } fillInGapCell = Cell.of(fillInRow, fillInColumn); } if (fillInGapCell != null && !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { addCellToPattern(fillInGapCell); } addCellToPattern(cell); if (mEnableHapticFeedback) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } return cell; } return null; } private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.row][newCell.column] = true; mPattern.add(newCell); if (!mInStealthMode) { startCellActivatedAnimation(newCell); } notifyCellAdded(); } private void startCellActivatedAnimation(Cell cell) { final CellState cellState = mCellStates[cell.row][cell.column]; startSizeAnimation(mDotSize, mDotSizeActivated, 96, mLinearOutSlowInInterpolator, cellState, new Runnable() { @Override public void run() { startSizeAnimation(mDotSizeActivated, mDotSize, 192, mFastOutSlowInInterpolator, cellState, null); } }); startLineEndAnimation(cellState, mInProgressX, mInProgressY, getCenterXForColumn(cell.column), getCenterYForRow(cell.row)); } private void startLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY) { /* * Currently this animation looks unclear, we don't really need it... */ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) return; ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); state.lineEndX = (1 - t) * startX + t * targetX; state.lineEndY = (1 - t) * startY + t * targetY; invalidate(); } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { state.lineAnimator = null; } }); valueAnimator.setInterpolator(mFastOutSlowInInterpolator); valueAnimator.setDuration(100); valueAnimator.start(); state.lineAnimator = valueAnimator; } private void startSizeAnimation(float start, float end, long duration, Interpolator interpolator, final CellState state, final Runnable endRunnable) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { FloatAnimator animator = new FloatAnimator(start, end, duration); animator.addEventListener(new FloatAnimator.SimpleEventListener() { @Override public void onAnimationUpdate(FloatAnimator animator) { state.size = (Float) animator.getAnimatedValue(); invalidate(); }// onAnimationUpdate() @Override public void onAnimationEnd(FloatAnimator animator) { if (endRunnable != null) endRunnable.run(); }// onAnimationEnd() }); animator.start(); } // API < 11 else { ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { state.size = (Float) animation.getAnimatedValue(); invalidate(); } }); if (endRunnable != null) { valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (endRunnable != null) endRunnable.run(); } }); } valueAnimator.setInterpolator(interpolator); valueAnimator.setDuration(duration); valueAnimator.start(); } // API 11+ }// startSizeAnimation() // helper method to find which cell a point maps to private Cell checkForNewHit(float x, float y) { final int rowHit = getRowHit(y); if (rowHit < 0) { return null; } final int columnHit = getColumnHit(x); if (columnHit < 0) { return null; } if (mPatternDrawLookup[rowHit][columnHit]) { return null; } return Cell.of(rowHit, columnHit); } /** * Helper method to find the row that y falls into. * * @param y The y coordinate * @return The row that y falls in, or -1 if it falls in no row. */ private int getRowHit(float y) { final float squareHeight = mSquareHeight; float hitSize = squareHeight * mHitFactor; float offset = getPaddingTop() + (squareHeight - hitSize) / 2f; for (int i = 0; i < MATRIX_WIDTH; i++) { final float hitTop = offset + squareHeight * i; if (y >= hitTop && y <= hitTop + hitSize) { return i; } } return -1; } /** * Helper method to find the column x fallis into. * * @param x The x coordinate. * @return The column that x falls in, or -1 if it falls in no column. */ private int getColumnHit(float x) { final float squareWidth = mSquareWidth; float hitSize = squareWidth * mHitFactor; float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f; for (int i = 0; i < MATRIX_WIDTH; i++) { final float hitLeft = offset + squareWidth * i; if (x >= hitLeft && x <= hitLeft + hitSize) { return i; } } return -1; } @Override public boolean onHoverEvent(MotionEvent event) { if (((AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)) .isTouchExplorationEnabled()) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_HOVER_ENTER: event.setAction(MotionEvent.ACTION_DOWN); break; case MotionEvent.ACTION_HOVER_MOVE: event.setAction(MotionEvent.ACTION_MOVE); break; case MotionEvent.ACTION_HOVER_EXIT: event.setAction(MotionEvent.ACTION_UP); break; } onTouchEvent(event); event.setAction(action); } return super.onHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mInputEnabled || !isEnabled()) { return false; } if (event.getAction() == MotionEvent.ACTION_MOVE && collectSensor == true) { float x = event.getX(); float y = event.getY(); RawXY xy = new RawXY(x, y, event.getEventTime()); rawXYList.add(xy); } int index = event.getActionIndex(); int pointerId = event.getPointerId(index); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: handleActionDown(event); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } mVelocityTracker.addMovement(event); return true; case MotionEvent.ACTION_UP: handleActionUp(event); return true; case MotionEvent.ACTION_MOVE: handleActionMove(event); mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000); float x = VelocityTrackerCompat.getXVelocity(mVelocityTracker, pointerId); float y = VelocityTrackerCompat.getYVelocity(mVelocityTracker, pointerId); Velocity xy = new Velocity(x, y, event.getEventTime()); velocityList.add(xy); return true; case MotionEvent.ACTION_CANCEL: /* * Original source check for mPatternInProgress == true first before * calling next three lines. But if we do that, there will be * nothing happened when the user taps at empty area and releases * the finger. We want the pattern to be reset and the message will * be updated after the user did that. */ mVelocityTracker.recycle(); mPatternInProgress = false; resetPattern(); notifyPatternCleared(); if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } return true; } return false; } private void handleActionMove(MotionEvent event) { // Handle all recent motion events so we don't skip any cells even when // the device // is busy... final float radius = mPathWidth; final int historySize = event.getHistorySize(); mTmpInvalidateRect.setEmpty(); boolean invalidateNow = false; for (int i = 0; i < historySize + 1; i++) { final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); Cell hitCell = detectAndAddHit(x, y); final int patternSize = mPattern.size(); if (hitCell != null && patternSize == 1) { mPatternInProgress = true; notifyPatternStarted(); } // note current x and y for rubber banding of in progress patterns final float dx = Math.abs(x - mInProgressX); final float dy = Math.abs(y - mInProgressY); if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) { invalidateNow = true; } if (mPatternInProgress && patternSize > 0) { final ArrayList<Cell> pattern = mPattern; final Cell lastCell = pattern.get(patternSize - 1); float lastCellCenterX = getCenterXForColumn(lastCell.column); float lastCellCenterY = getCenterYForRow(lastCell.row); // Adjust for drawn segment from last cell to (x,y). Radius // accounts for line width. float left = Math.min(lastCellCenterX, x) - radius; float right = Math.max(lastCellCenterX, x) + radius; float top = Math.min(lastCellCenterY, y) - radius; float bottom = Math.max(lastCellCenterY, y) + radius; // Invalidate between the pattern's new cell and the pattern's // previous cell if (hitCell != null) { final float width = mSquareWidth * 0.5f; final float height = mSquareHeight * 0.5f; final float hitCellCenterX = getCenterXForColumn(hitCell.column); final float hitCellCenterY = getCenterYForRow(hitCell.row); left = Math.min(hitCellCenterX - width, left); right = Math.max(hitCellCenterX + width, right); top = Math.min(hitCellCenterY - height, top); bottom = Math.max(hitCellCenterY + height, bottom); } // Invalidate between the pattern's last cell and the previous // location mTmpInvalidateRect.union(Math.round(left), Math.round(top), Math.round(right), Math.round(bottom)); } } mInProgressX = event.getX(); mInProgressY = event.getY(); // To save updates, we only invalidate if the user moved beyond a // certain amount. if (invalidateNow) { mInvalidate.union(mTmpInvalidateRect); invalidate(mInvalidate); mInvalidate.set(mTmpInvalidateRect); } } private void sendAccessEvent(int resId) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { setContentDescription(getContext().getString(resId)); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); setContentDescription(null); } else announceForAccessibility(getContext().getString(resId)); } private void handleActionUp(MotionEvent event) { // report pattern detected if (!mPattern.isEmpty()) { mPatternInProgress = false; cancelLineAnimations(); notifyPatternDetected(); invalidate(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } } private void cancelLineAnimations() { for (int i = 0; i < MATRIX_WIDTH; i++) { for (int j = 0; j < MATRIX_WIDTH; j++) { CellState state = mCellStates[i][j]; if (state.lineAnimator != null) { state.lineAnimator.cancel(); state.lineEndX = Float.MIN_VALUE; state.lineEndY = Float.MIN_VALUE; } } } } private void handleActionDown(MotionEvent event) { resetPattern(); final float x = event.getX(); final float y = event.getY(); final Cell hitCell = detectAndAddHit(x, y); if (hitCell != null) { mPatternInProgress = true; mPatternDisplayMode = DisplayMode.Correct; notifyPatternStarted(); } else { /* * Original source check for mPatternInProgress == true first before * calling this block. But if we do that, there will be nothing * happened when the user taps at empty area and releases the * finger. We want the pattern to be reset and the message will be * updated after the user did that. */ mPatternInProgress = false; notifyPatternCleared(); } if (hitCell != null) { final float startX = getCenterXForColumn(hitCell.column); final float startY = getCenterYForRow(hitCell.row); final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), (int) (startX + widthOffset), (int) (startY + heightOffset)); } mInProgressX = x; mInProgressY = y; if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } } private float getCenterXForColumn(int column) { return getPaddingLeft() + column * mSquareWidth + mSquareWidth / 2f; } private float getCenterYForRow(int row) { return getPaddingTop() + row * mSquareHeight + mSquareHeight / 2f; } @Override protected void onDraw(Canvas canvas) { final ArrayList<Cell> pattern = mPattern; final int count = pattern.size(); final boolean[][] drawLookup = mPatternDrawLookup; if (mPatternDisplayMode == DisplayMode.Animate) { // figure out which circles to draw // + 1 so we pause on complete pattern final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; final int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart) % oneCycle; final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; clearPatternDrawLookup(); for (int i = 0; i < numCircles; i++) { final Cell cell = pattern.get(i); drawLookup[cell.row][cell.column] = true; } // figure out in progress portion of ghosting line final boolean needToUpdateInProgressPoint = numCircles > 0 && numCircles < count; if (needToUpdateInProgressPoint) { final float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / MILLIS_PER_CIRCLE_ANIMATING; final Cell currentCell = pattern.get(numCircles - 1); final float centerX = getCenterXForColumn(currentCell.column); final float centerY = getCenterYForRow(currentCell.row); final Cell nextCell = pattern.get(numCircles); final float dx = percentageOfNextCircle * (getCenterXForColumn(nextCell.column) - centerX); final float dy = percentageOfNextCircle * (getCenterYForRow(nextCell.row) - centerY); mInProgressX = centerX + dx; mInProgressY = centerY + dy; } // TODO: Infinite loop here... invalidate(); } final Path currentPath = mCurrentPath; currentPath.rewind(); // draw the circles for (int i = 0; i < MATRIX_WIDTH; i++) { float centerY = getCenterYForRow(i); for (int j = 0; j < MATRIX_WIDTH; j++) { CellState cellState = mCellStates[i][j]; float centerX = getCenterXForColumn(j); float size = cellState.size * cellState.scale; float translationY = cellState.translateY; drawCircle(canvas, (int) centerX, (int) centerY + translationY, size, drawLookup[i][j], cellState.alpha); } } // TODO: the path should be created and cached every time we hit-detect // a cell // only the last segment of the path should be computed here // draw the path of the pattern (unless we are in stealth mode) final boolean drawPath = !mInStealthMode; if (drawPath) { mPathPaint.setColor(getCurrentColor(true /* partOfPattern */)); boolean anyCircles = false; float lastX = 0f; float lastY = 0f; for (int i = 0; i < count; i++) { Cell cell = pattern.get(i); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[cell.row][cell.column]) { break; } anyCircles = true; float centerX = getCenterXForColumn(cell.column); float centerY = getCenterYForRow(cell.row); if (i != 0) { CellState state = mCellStates[cell.row][cell.column]; currentPath.rewind(); currentPath.moveTo(lastX, lastY); if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) { currentPath.lineTo(state.lineEndX, state.lineEndY); } else { currentPath.lineTo(centerX, centerY); } canvas.drawPath(currentPath, mPathPaint); } lastX = centerX; lastY = centerY; } // draw last in progress section if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) && anyCircles) { currentPath.rewind(); currentPath.moveTo(lastX, lastY); currentPath.lineTo(mInProgressX, mInProgressY); mPathPaint.setAlpha( (int) (calculateLastSegmentAlpha(mInProgressX, mInProgressY, lastX, lastY) * 255f)); canvas.drawPath(currentPath, mPathPaint); } } } private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) { float diffX = x - lastX; float diffY = y - lastY; float dist = (float) Math.sqrt(diffX * diffX + diffY * diffY); float frac = dist / mSquareWidth; return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f)); } private int getCurrentColor(boolean partOfPattern) { if (!partOfPattern || mInStealthMode || mPatternInProgress) { // unselected circle return mRegularColor; } else if (mPatternDisplayMode == DisplayMode.Wrong) { // the pattern is wrong return mErrorColor; } else if (mPatternDisplayMode == DisplayMode.Correct || mPatternDisplayMode == DisplayMode.Animate) { return mSuccessColor; } else { throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); } } /** * @param partOfPattern Whether this circle is part of the pattern. */ private void drawCircle(Canvas canvas, float centerX, float centerY, float size, boolean partOfPattern, float alpha) { mPaint.setColor(getCurrentColor(partOfPattern)); mPaint.setAlpha((int) (alpha * 255)); canvas.drawCircle(centerX, centerY, size / 2, mPaint); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, LockPatternUtils.patternToString(mPattern), mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode, mEnableHapticFeedback); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setPattern(DisplayMode.Correct, LockPatternUtils.stringToPattern(ss.getSerializedPattern())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInStealthMode = ss.isInStealthMode(); mEnableHapticFeedback = ss.isTactileFeedbackEnabled(); } /** * The parecelable for saving and restoring a lock pattern view. */ private static class SavedState extends BaseSavedState { private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final boolean mInStealthMode; private final boolean mTactileFeedbackEnabled; /** * Constructor called from {@link LockPatternView#onSaveInstanceState()} */ private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInStealthMode = inStealthMode; mTactileFeedbackEnabled = tactileFeedbackEnabled; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInStealthMode = (Boolean) in.readValue(null); mTactileFeedbackEnabled = (Boolean) in.readValue(null); } public String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { return mInStealthMode; } public boolean isTactileFeedbackEnabled() { return mTactileFeedbackEnabled; } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeValue(mInStealthMode); dest.writeValue(mTactileFeedbackEnabled); } @SuppressWarnings("unused") public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }