|
/** |
|
* Displays animated dots as pin entries. |
|
*/ |
|
public class PinDisplayView extends View { |
|
|
|
private static final int DEFAULT_DOT_COUNT = 4; |
|
private static final int DEFAULT_DOT_RADIUS = 6; |
|
private static final int DEFAULT_DOT_SPACING = 11; |
|
private static final int HEIGHT_MODIFIER = 6; |
|
private static final int VIBRATOR_DURATION = 500; |
|
private static final int ANIMATION_SEGMENT_DURATION = 50; |
|
|
|
private static final int THREE = 3; |
|
private static final float TWO_THIRDS = (float) 2 / (float) THREE; |
|
|
|
private Vibrator vibrator; |
|
private Paint darkPaint; |
|
private Paint lightPaint; |
|
private float dotRadius, smallDotRadius; |
|
private float dotSpacing; |
|
private int dotCount = DEFAULT_DOT_COUNT; |
|
private int desiredWidth; |
|
private Dot[] dots; |
|
|
|
public PinDisplayView(Context context) { |
|
super(context); |
|
init(context, null, 0); |
|
} |
|
|
|
public PinDisplayView(Context context, AttributeSet attrs) { |
|
super(context, attrs); |
|
init(context, attrs, 0); |
|
} |
|
|
|
public PinDisplayView(Context context, AttributeSet attrs, int defStyleAttr) { |
|
super(context, attrs, defStyleAttr); |
|
init(context, attrs, defStyleAttr); |
|
} |
|
|
|
private void init(Context context, AttributeSet attrs, int defStyleAttr) { |
|
this.darkPaint = new Paint(); |
|
this.lightPaint = new Paint(); |
|
|
|
final TypedValue colorControlActivated = new TypedValue(); |
|
getContext().getTheme().resolveAttribute(R.attr.colorControlActivated, colorControlActivated, true); |
|
final int defaultColorActivated = colorControlActivated.data; |
|
|
|
final TypedValue colorControlNormal = new TypedValue(); |
|
getContext().getTheme().resolveAttribute(R.attr.colorControlNormal, colorControlNormal, true); |
|
final int defaultColorNormal = colorControlNormal.data; |
|
|
|
this.dotRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
|
DEFAULT_DOT_RADIUS, getResources().getDisplayMetrics()); |
|
|
|
this.smallDotRadius = dotRadius * TWO_THIRDS; |
|
|
|
this.dotSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
|
DEFAULT_DOT_SPACING, getResources().getDisplayMetrics()); |
|
|
|
int attrColorActivated = 0, attrColorNormal = 0; |
|
if (attrs != null) { |
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PinDisplayView, defStyleAttr, 0); |
|
|
|
attrColorActivated = a.getColor(R.styleable.PinDisplayView_enteredColor, defaultColorActivated); |
|
attrColorNormal = a.getColor(R.styleable.PinDisplayView_emptyColor, defaultColorNormal); |
|
|
|
this.dotRadius = a.getDimension(R.styleable.PinDisplayView_dotRadius, this.dotRadius); |
|
|
|
this.smallDotRadius = dotRadius * TWO_THIRDS; |
|
|
|
this.dotSpacing = a.getDimension(R.styleable.PinDisplayView_dotSpacing, this.dotSpacing); |
|
|
|
this.dotCount = a.getInt(R.styleable.PinDisplayView_dotCount, DEFAULT_DOT_COUNT); |
|
|
|
a.recycle(); |
|
} |
|
|
|
darkPaint.setColor(attrColorActivated != 0 ? attrColorActivated : defaultColorActivated); |
|
darkPaint.setAntiAlias(true); |
|
lightPaint.setColor(attrColorNormal != 0 ? attrColorNormal : defaultColorNormal); |
|
lightPaint.setAntiAlias(true); |
|
|
|
dots = new Dot[dotCount]; |
|
for (int i = 0; i < dotCount; i++) { |
|
dots[i] = new Dot(0, 0); |
|
} |
|
|
|
if (!isInEditMode()) { |
|
this.vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); |
|
} |
|
} |
|
|
|
@Override |
|
protected void onDraw(Canvas canvas) { |
|
boolean shouldRedraw = false; |
|
for (Dot dot : dots) { |
|
final float useRadius = dot.enabled ? dotRadius : smallDotRadius; |
|
final Paint usePaint = dot.enabled ? darkPaint : lightPaint; |
|
|
|
if (dot.animate) { |
|
if (dot.transientRadius == 0) { |
|
dot.transientRadius = (int) (dotRadius * 2); |
|
} |
|
canvas.drawCircle(dot.x, dot.y, dot.transientRadius, usePaint); |
|
if (dot.transientRadius <= useRadius) { |
|
dot.transientRadius = 0; |
|
dot.animate = false; |
|
} else { |
|
shouldRedraw = true; |
|
dot.transientRadius -= 2; |
|
} |
|
} |
|
|
|
canvas.drawCircle(dot.x, dot.y, useRadius, usePaint); |
|
if (shouldRedraw) { |
|
invalidate(); |
|
} |
|
} |
|
} |
|
|
|
@Override |
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
|
this.desiredWidth = (int) ((dotRadius * 2 + dotSpacing) * dotCount); |
|
final int desiredHeight = (int) (dotRadius * HEIGHT_MODIFIER); |
|
|
|
final int[] resultingWidthAndHeight = new int[2]; |
|
|
|
ViewUtil.resolveMeasureSpec(widthMeasureSpec, heightMeasureSpec, |
|
desiredWidth, desiredHeight, resultingWidthAndHeight); |
|
|
|
//MUST CALL THIS |
|
setMeasuredDimension(resultingWidthAndHeight[0], resultingWidthAndHeight[1]); |
|
} |
|
|
|
/** |
|
* Creates {@link Dot} positioning in regards to supplied |
|
* {@code width} and {@code height}. Dots will always tend to gravitate |
|
* to the middle of the view. |
|
*/ |
|
@Override |
|
protected void onSizeChanged(int width, int height, int oldw, int oldh) { |
|
if (width > 0 && height > 0) { |
|
final int incrementalStep = (int) (dotRadius * 2 + dotSpacing); |
|
final int y = height / 2; |
|
|
|
final int startOffset = (int) ((width - desiredWidth + dotRadius + smallDotRadius + dotSpacing) / 2); |
|
for (int i = 0; i < dotCount; i++) { |
|
final int x = incrementalStep * i + startOffset; |
|
final Dot dot = new Dot(x, y); |
|
dots[i] = dot; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Activates first deactivated dot from the left. |
|
* This Dot enters an animation that transitions the dot |
|
* to the activated state. |
|
*/ |
|
public void addActive() { |
|
for (Dot dot : dots) { |
|
if (!dot.enabled) { |
|
dot.enabled = true; |
|
dot.animate = true; |
|
break; |
|
} |
|
} |
|
invalidate(); |
|
} |
|
|
|
/** |
|
* Deactivates the first activated dot from the right. |
|
* This dot enters an animation that transitions the dot |
|
* to the deactivated state. |
|
*/ |
|
public void removeActive() { |
|
for (int i = dots.length - 1; i >= 0; i --) { |
|
if (dots[i].enabled) { |
|
dots[i].enabled = false; |
|
dots[i].animate = true; |
|
break; |
|
} |
|
} |
|
invalidate(); |
|
} |
|
|
|
/** |
|
* Resets all dots to the deactivated state. All dots |
|
* enter a deactivate and shake animation. Devices that |
|
* support vibrations will be vibrated at this point. |
|
*/ |
|
public void shakeReset() { |
|
for (Dot dot : dots) { |
|
dot.animate = true; |
|
dot.enabled = false; |
|
} |
|
if (vibrator.hasVibrator()) { |
|
vibrator.vibrate(VIBRATOR_DURATION); |
|
} |
|
invalidate(); |
|
|
|
AnimatorSet animatorSet = new AnimatorSet(); |
|
animatorSet.playSequentially( |
|
ObjectAnimator.ofFloat(this, "translationX", dotRadius).setDuration(ANIMATION_SEGMENT_DURATION), |
|
ObjectAnimator.ofFloat(this, "translationX", - dotRadius * 2).setDuration(ANIMATION_SEGMENT_DURATION), |
|
ObjectAnimator.ofFloat(this, "translationX", dotRadius * 2).setDuration(ANIMATION_SEGMENT_DURATION), |
|
ObjectAnimator.ofFloat(this, "translationX", - dotRadius * 2).setDuration(ANIMATION_SEGMENT_DURATION), |
|
ObjectAnimator.ofFloat(this, "translationX", dotRadius).setDuration(ANIMATION_SEGMENT_DURATION) |
|
); |
|
animatorSet.start(); |
|
} |
|
|
|
/** |
|
* Does the same thing as {@link #shakeReset()} |
|
* but does not shake or animate the view. |
|
*/ |
|
public void reset() { |
|
for (Dot dot : dots) { |
|
dot.enabled = false; |
|
} |
|
invalidate(); |
|
} |
|
|
|
static class Dot { |
|
final int x; |
|
final int y; |
|
boolean enabled; |
|
boolean animate; |
|
int transientRadius; |
|
|
|
Dot(int x, int y) { |
|
this.x = x; |
|
this.y = y; |
|
} |
|
} |
|
|
|
} |