/*
* Copyright (C) 2014 AChep@xda <ynkr.wang@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.bullmobi.message.ui.widgets;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Property;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import com.bullmobi.message.Config;
import com.bullmobi.message.R;
import com.bullmobi.message.ui.preferences.ColorPickerPreference;
import com.bullmobi.base.async.WeakHandler;
import com.bullmobi.base.tests.Check;
import com.bullmobi.base.utils.FloatProperty;
import com.bullmobi.base.utils.ResUtils;
/**
* Created by achep on 19.04.14.
*/
public class CircleView extends View {
public static final int ACTION_START = 0;
public static final int ACTION_UNLOCK = 1;
public static final int ACTION_UNLOCK_START = 2;
public static final int ACTION_UNLOCK_CANCEL = 3;
public static final int ACTION_CANCELED = 4;
private static final int MSG_CANCEL = -1;
private static final Property<CircleView, Float> TRANSFORM =
new FloatProperty<CircleView>("setRadius") {
@Override
public void setValue(CircleView cv, float value) {
cv.setRadius(value);
}
@Override
public Float get(CircleView cv) {
return cv.mRadius;
}
};
private float[] mPoint = new float[2];
// Target
private boolean mRadiusTargetAimed;
private float mRadiusTarget;
// Decreasing detection
private float mRadiusDecreaseThreshold;
private float mRadiusMaxPeak;
/**
* Real radius of the circle, measured by touch.
*/
private float mRadius;
/**
* Radius of the drawn circle.
*
* @see #setRadiusDrawn(float)
*/
private float mRadiusDrawn;
private boolean mCanceled;
private float mDarkening;
private ColorFilter mInverseColorFilter;
private Drawable mDrawable;
private Paint mPaint;
// animation
private ObjectAnimator mAnimator;
private int mShortAnimTime;
private int mMediumAnimTime;
private Callback mCallback;
private Supervisor mSupervisor;
private H mHandler = new H(this);
private int mInnerColor;
private int mOuterColor;
public interface Callback {
void onCircleEvent(float radius, float ratio, int event);
}
public interface Supervisor {
boolean isAnimationEnabled();
boolean isAnimationUnlockEnabled();
}
private static class H extends WeakHandler<CircleView> {
public H(@NonNull CircleView cv) {
super(cv);
}
@Override
protected void onHandleMassage(@NonNull CircleView cv, Message msg) {
switch (msg.what) {
case MSG_CANCEL:
cv.cancelCircle();
break;
case ACTION_START:
case ACTION_UNLOCK:
case ACTION_UNLOCK_START:
case ACTION_UNLOCK_CANCEL:
case ACTION_CANCELED:
if (cv.mCallback != null) {
cv.mCallback.onCircleEvent(cv.mRadius, cv.calculateRatio(), msg.what);
}
break;
}
}
}
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
Resources res = getResources();
mRadiusTarget = res.getDimension(R.dimen.circle_radius_target);
mRadiusDecreaseThreshold = res.getDimension(R.dimen.circle_radius_decrease_threshold);
mShortAnimTime = res.getInteger(android.R.integer.config_shortAnimTime);
mMediumAnimTime = res.getInteger(android.R.integer.config_mediumAnimTime);
mPaint = new Paint();
mPaint.setAntiAlias(true);
initColorFilter();
// Load the drawable if needed
mDrawable = ResUtils.getDrawable(getContext(), R.drawable.ic_settings_keyguard_white);
mDrawable.setBounds(0, 0,
mDrawable.getIntrinsicWidth(),
mDrawable.getIntrinsicHeight());
mDrawable = mDrawable.mutate(); // don't affect the original drawable
setRadius(0);
}
private void initColorFilter() {
final float v = -1;
final float[] matrix = {
v, 0, 0, 0, 0,
0, v, 0, 0, 0,
0, 0, v, 0, 0,
0, 0, 0, 1, 0,
};
mInverseColorFilter = new ColorMatrixColorFilter(matrix);
}
public void setSupervisor(Supervisor supervisor) {
mSupervisor = supervisor;
}
public void setCallback(Callback callback) {
mCallback = callback;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final float ratio = calculateRatio();
// Darkening background
int alpha = (int) (mDarkening * 255);
alpha += (int) ((255 - alpha) * ratio * 0.7f); // Change alpha dynamically
canvas.drawColor(Color.argb(alpha,
Color.red(mOuterColor),
Color.green(mOuterColor),
Color.blue(mOuterColor)));
// Draw unlock circle
mPaint.setColor(mInnerColor);
mPaint.setAlpha((int) (255 * Math.pow(ratio, 1f / 3f)));
canvas.drawCircle(mPoint[0], mPoint[1], mRadiusDrawn, mPaint);
if (ratio >= 0.5f) {
// Draw unlock icon at the center of circle
float scale = 0.5f + 0.5f * ratio;
canvas.save();
canvas.translate(
mPoint[0] - mDrawable.getMinimumWidth() / 2 * scale,
mPoint[1] - mDrawable.getMinimumHeight() / 2 * scale);
canvas.scale(scale, scale);
mDrawable.draw(canvas);
canvas.restore();
}
}
@Override
protected void onDetachedFromWindow() {
clearAnimator();
mHandler.removeCallbacksAndMessages(null);
super.onDetachedFromWindow();
}
private void setInnerColor(int color) {
if (mInnerColor == (mInnerColor = color)) return;
// Inverse the drawable if needed
float[] innerHsv = new float[3];
Color.colorToHSV(mInnerColor, innerHsv);
float innerHsvValue = innerHsv[2];
mDrawable.setColorFilter(innerHsvValue > 0.5f ? mInverseColorFilter : null);
}
private void setOuterColor(int color) {
mOuterColor = color;
}
public boolean sendTouchEvent(@NonNull MotionEvent event) {
final int action = event.getActionMasked();
// If current circle is canceled then
// ignore all actions except of touch down (to reset state.)
if (mCanceled && action != MotionEvent.ACTION_DOWN) return false;
// Cancel the current circle on two-or-more-fingers touch.
if (event.getPointerCount() > 1) {
cancelCircle();
return false;
}
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
clearAnimation();
// Update colors
Config config = Config.getInstance();
setInnerColor(ColorPickerPreference.getColor(config.getCircleInnerColor()));
setOuterColor(ColorPickerPreference.getColor(config.getCircleOuterColor()));
// Initialize circle
mRadiusTargetAimed = false;
mRadiusMaxPeak = 0;
mPoint[0] = x;
mPoint[1] = y;
mCanceled = false;
if (mHandler.hasMessages(ACTION_UNLOCK)) {
// Cancel unlocking process.
mHandler.sendEmptyMessage(ACTION_UNLOCK_CANCEL);
}
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessageDelayed(MSG_CANCEL, 1000);
mHandler.sendEmptyMessage(ACTION_START);
break;
case MotionEvent.ACTION_MOVE:
setRadius(x, y);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (!mRadiusTargetAimed || action == MotionEvent.ACTION_CANCEL) {
cancelCircle();
break;
}
unlockCircle();
break;
}
return true;
}
private void clearAnimator() {
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
}
private void cancelCircle() {
Check.getInstance().isFalse(mCanceled);
mCanceled = true;
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessage(ACTION_CANCELED);
startAnimator(mRadius, 0f, mMediumAnimTime);
}
private void unlockCircle() {
boolean animate = mSupervisor.isAnimationUnlockEnabled();
if (animate) {
// Calculate longest distance between center of
// the circle and view's corners.
float distance = 0f;
int[] corners = new int[]{
0, 0, // top left
0, getHeight(), // bottom left
getWidth(), getHeight(), // bottom right
getWidth(), 0 // top right
};
for (int i = 0; i < corners.length; i += 2) {
double c = Math.hypot(
mPoint[0] - corners[i],
mPoint[1] - corners[i + 1]);
if (c > distance) distance = (float) c;
}
distance = (float) (Math.pow(distance / 50f, 2) * 50f);
startAnimator(mRadius, distance, mShortAnimTime);
}
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessage(ACTION_UNLOCK_START);
mHandler.sendEmptyMessageDelayed(ACTION_UNLOCK, animate
? mShortAnimTime - 10
: 0);
}
private void startAnimator(float from, float to, int duration) {
clearAnimator();
if (mSupervisor.isAnimationEnabled()) {
mAnimator = ObjectAnimator.ofFloat(this, TRANSFORM, from, to);
mAnimator.setInterpolator(new AccelerateInterpolator());
mAnimator.setDuration(duration);
mAnimator.start();
} else {
setRadius(to);
}
}
//-- BASICS ---------------------------------------------------------------
private float calculateRatio() {
return Math.min(mRadius / mRadiusTarget, 1f);
}
private void setRadius(float x, float y) {
double radius = Math.hypot(x - mPoint[0], y - mPoint[1]);
setRadius((float) radius);
}
/**
* Sets the radius of fake circle.
*
* @param radius radius to set
*/
private void setRadius(float radius) {
mRadius = radius;
if (!mCanceled) {
// Save maximum radius for detecting
// decreasing of the circle's size.
if (mRadius > mRadiusMaxPeak) {
mRadiusMaxPeak = mRadius;
} else if (mRadiusMaxPeak - mRadius > mRadiusDecreaseThreshold) {
cancelCircle();
return; // Cancelling circle will recall #setRadius
}
boolean aimed = mRadius >= mRadiusTarget;
if (mRadiusTargetAimed != aimed) {
mRadiusTargetAimed = aimed;
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); // vibrate
}
}
// Update unlock icon's transparency.
float ratio = calculateRatio();
mDrawable.setAlpha((int) (255 * Math.pow(ratio, 3)));
// Update the size of the unlock circle.
radius = (float) Math.sqrt(mRadius / 50f) * 50f;
setRadiusDrawn(radius);
}
private void setRadiusDrawn(float radius) {
mRadiusDrawn = radius;
postInvalidateOnAnimation();
}
}