com.achep.acdisplay.ui.widgets.CircleView.java Source code

Java tutorial

Introduction

Here is the source code for com.achep.acdisplay.ui.widgets.CircleView.java

Source

/*
 * Copyright (C) 2014 AChep@xda <artemchep@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.achep.acdisplay.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.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Property;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;

import com.achep.acdisplay.Config;
import com.achep.acdisplay.R;
import com.achep.acdisplay.ui.CornerHelper;
import com.achep.acdisplay.ui.drawables.CornerIconDrawable;
import com.achep.base.async.WeakHandler;
import com.achep.base.tests.Check;
import com.achep.base.utils.FloatProperty;
import com.achep.base.utils.MathUtils;
import com.achep.base.utils.RefCacheBase;
import com.achep.base.utils.ResUtils;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;

import static com.achep.acdisplay.ui.preferences.ColorPickerPreference.getColor;
import static com.achep.base.Build.DEBUG;

/**
 * Created by achep on 19.04.14.
 */
public class CircleView extends View {

    private static final String TAG = "CircleView";

    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;

    @NonNull
    private static final Property<CircleView, Float> RADIUS_PROPERTY = new FloatProperty<CircleView>("setRadius") {
        @Override
        public void setValue(CircleView cv, float value) {
            cv.setRadius(value);
        }

        @Override
        public Float get(CircleView cv) {
            return cv.mRadius;
        }
    };

    /**
     * The current touch point.
     */
    private float[] mPoint = new float[2];

    /**
     * Real radius of the circle, measured by touch.
     */
    private float mRadius;

    /**
     * Radius of the drawn circle.
     *
     * @see #setRadiusDrawn(float)
     */
    private float mRadiusDrawn;
    // Target
    private float mRadiusTarget;
    private boolean mRadiusTargetAimed;
    // Decreasing detection
    private float mRadiusMaxPeak;
    private float mRadiusDecreaseThreshold;

    private boolean mCanceled;
    private float mDarkening;

    private float mCornerMargin;
    @DrawableRes
    private int mDrawableResourceId = -1;
    private ColorFilter mInverseColorFilter;
    private CornerIconDrawable mDrawableLeftTopCorner;
    private CornerIconDrawable mDrawableRightTopCorner;
    private CornerIconDrawable mDrawableLeftBottomCorner;
    private CornerIconDrawable mDrawableRightBottomCorner;
    private Drawable mDrawable;
    private Paint mPaint;
    @NonNull
    private RefCacheBase<Drawable> mDrawableCache = new RefCacheBase<Drawable>() {
        @NonNull
        @Override
        protected Reference<Drawable> onCreateReference(@NonNull Drawable object) {
            return new WeakReference<>(object);
        }
    };

    // 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;
    private int mCornerActionId;

    public interface Callback {

        void onCircleEvent(float radius, float ratio, int event, int actionId);
    }

    public interface Supervisor {

        boolean isAnimationEnabled();

        boolean isAnimationUnlockEnabled();

    }

    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();
        mCornerMargin = res.getDimension(R.dimen.circle_corner_margin);
        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);

        mDrawableLeftTopCorner = new CornerIconDrawable(Config.KEY_CORNER_ACTION_LEFT_TOP);
        mDrawableRightTopCorner = new CornerIconDrawable(Config.KEY_CORNER_ACTION_RIGHT_TOP);
        mDrawableLeftBottomCorner = new CornerIconDrawable(Config.KEY_CORNER_ACTION_LEFT_BOTTOM);
        mDrawableRightBottomCorner = new CornerIconDrawable(Config.KEY_CORNER_ACTION_RIGHT_BOTTOM);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        initInverseColorFilter();

        setRadius(0);
    }

    private void initInverseColorFilter() {
        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 onAttachedToWindow() {
        super.onAttachedToWindow();
        // Start tracking the corners' icons.
        Context context = getContext();
        mDrawableLeftTopCorner.start(context);
        mDrawableRightTopCorner.start(context);
        mDrawableLeftBottomCorner.start(context);
        mDrawableRightBottomCorner.start(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        final float ratio = calculateRatio();

        // Draw all corners
        drawCornerIcon(canvas, mDrawableLeftTopCorner, 0, 0 /* left top */);
        drawCornerIcon(canvas, mDrawableRightTopCorner, 1, 0 /* right top */);
        drawCornerIcon(canvas, mDrawableLeftBottomCorner, 0, 1 /* left bottom */);
        drawCornerIcon(canvas, mDrawableRightBottomCorner, 1, 1 /* right bottom */);

        // 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();
        }
    }

    private void drawCornerIcon(@NonNull Canvas canvas, @NonNull Drawable drawable, int xm, int ym) {
        int width = getMeasuredWidth() - drawable.getBounds().width();
        int height = getMeasuredHeight() - drawable.getBounds().height();
        float margin = (1 - 2 * xm) * mCornerMargin;
        // Draw
        canvas.save();
        canvas.translate(xm * width + margin, ym * height + margin);
        drawable.draw(canvas);
        canvas.restore();
    }

    @Override
    protected void onDetachedFromWindow() {
        cancelAndClearAnimator();
        mHandler.removeCallbacksAndMessages(null);
        mDrawableCache.clear();

        mDrawableLeftTopCorner.stop();
        mDrawableRightTopCorner.stop();
        mDrawableLeftBottomCorner.stop();
        mDrawableRightBottomCorner.stop();
        super.onDetachedFromWindow();
    }

    private void setInnerColor(int color, boolean needsColorReset) {
        if (mInnerColor == (mInnerColor = color) && !needsColorReset)
            return;

        // Inverse the drawable if needed
        boolean isBright = ColorUtils.calculateLuminance(color) > 0.5;
        mDrawable.setColorFilter(isBright ? mInverseColorFilter : null);
    }

    private void setOuterColor(int color) {
        mOuterColor = color;
    }

    /**
     * Updates the icon in center of the circle, to the once corresponding
     * with the current action.
     *
     * @see CornerHelper
     */
    private boolean updateIcon() {
        final int res = CornerHelper.getIconResource(mCornerActionId);
        if (res == mDrawableResourceId)
            return false; // No need to update
        mDrawableResourceId = res;

        label: {
            // Try to get from the cache.
            final CharSequence key = Integer.toString(res);
            mDrawable = mDrawableCache.get(key);
            if (mDrawable != null) {
                if (DEBUG)
                    Log.d(TAG, "Got an icon<" + key + "> from the cache.");
                break label;
            }

            // Load from resources.
            mDrawable = ResUtils.getDrawable(getContext(), res);
            assert mDrawable != null;
            mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
            mDrawable = mDrawable.mutate(); // don't affect the original drawable
            mDrawableCache.put(key, mDrawable);
        }
        // Update alpha
        float ratio = calculateRatio();
        mDrawable.setAlpha((int) (255 * Math.pow(ratio, 3)));
        return true;
    }

    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();
            Config config = Config.getInstance();

            // Corner actions
            int width = getWidth();
            int height = getHeight();
            int radius = Math.min(width, height) / 3;
            if (MathUtils.isInCircle(x, y, 0, 0, radius)) { // Top left
                mCornerActionId = config.getCornerActionLeftTop();
            } else if (MathUtils.isInCircle(x, y, -width, 0, radius)) { // Top right
                mCornerActionId = config.getCornerActionRightTop();
            } else if (MathUtils.isInCircle(x, y, 0, -height, radius)) { // Bottom left
                mCornerActionId = config.getCornerActionLeftBottom();
            } else if (MathUtils.isInCircle(x, y, -width, -height, radius)) { // Bottom right
                mCornerActionId = config.getCornerActionRightBottom();
            } else {
                // The default action is unlocking.
                mCornerActionId = Config.CORNER_UNLOCK;
            }

            // Update colors and icon drawable.
            boolean needsColorReset = updateIcon();
            setInnerColor(getColor(config.getCircleInnerColor()), needsColorReset);
            setOuterColor(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;
            }

            startUnlock();
            break;
        }
        return true;
    }

    private void cancelCircle() {
        cancelCircle(mSupervisor.isAnimationUnlockEnabled());
    }

    private void cancelCircle(boolean animate) {
        Check.getInstance().isFalse(mCanceled);

        mCanceled = true;
        mHandler.removeCallbacksAndMessages(null);
        mHandler.sendEmptyMessage(ACTION_CANCELED);

        if (animate) {
            startAnimatorBy(mRadius, 0f, mMediumAnimTime);
        } else {
            setRadius(0f);
        }
    }

    private void startUnlock() {
        startUnlock(mSupervisor.isAnimationUnlockEnabled());
    }

    private void startUnlock(boolean animate) {
        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);
            startAnimatorBy(mRadius, distance, mShortAnimTime);
        }

        final int delayUnlock = animate ? mShortAnimTime - 10 : 0;
        mHandler.removeCallbacksAndMessages(null);
        mHandler.sendEmptyMessage(ACTION_UNLOCK_START);
        mHandler.sendEmptyMessageDelayed(ACTION_UNLOCK, delayUnlock);
    }

    private void startAnimatorBy(float from, float to, int duration) {
        cancelAndClearAnimator();
        // Animate the circle
        mAnimator = ObjectAnimator.ofFloat(this, RADIUS_PROPERTY, from, to);
        mAnimator.setInterpolator(new FastOutLinearInInterpolator());
        mAnimator.setDuration(duration);
        mAnimator.start();
    }

    private void cancelAndClearAnimator() {
        if (mAnimator != null) {
            mAnimator.cancel();
            mAnimator = null;
        }
    }

    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;
                // Vibrate if the user is interacting with the device.
                if (isInTouchMode())
                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
            }
        }
        final float ratio = calculateRatio();
        int alpha;

        // Update unlock icon's transparency.
        if (mDrawable != null) {
            alpha = (int) (255 * Math.pow(ratio, 3));
            mDrawable.setAlpha(alpha);
        }

        // Update corners' icons transparency.
        alpha = (int) (50f * Math.pow(1f - ratio, 0.3f));
        mDrawableLeftTopCorner.setAlpha(alpha);
        mDrawableRightTopCorner.setAlpha(alpha);
        mDrawableLeftBottomCorner.setAlpha(alpha);
        mDrawableRightBottomCorner.setAlpha(alpha);

        // Update the size of the unlock circle.
        radius = (float) Math.sqrt(mRadius / 50f) * 50f;
        setRadiusDrawn(radius);
    }

    private void setRadiusDrawn(float radius) {
        mRadiusDrawn = radius;
        postInvalidateOnAnimation();
    }

    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) {
                    final float ratio = cv.calculateRatio();
                    final int actionId = cv.mCornerActionId;
                    cv.mCallback.onCircleEvent(cv.mRadius, ratio, msg.what, actionId);
                }
                break;
            default:
                throw new IllegalArgumentException();
            }
        }

    }

}