com.morninz.ninepinview.widget.NinePINView.java Source code

Java tutorial

Introduction

Here is the source code for com.morninz.ninepinview.widget.NinePINView.java

Source

/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2015 Mornin.Z
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.morninz.ninepinview.widget;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.morninz.ninepinview.R;

/**
 * A nine points graphic PIN view. User use finger to draw a graphic that
 * connect some points, and the string consist of the drawn points' indexes is
 * the PIN code. And each point can be drawn only once, eg: graphic 'Z'
 * represent PIN code "0124678".
 * <p>
 * The nine points' indexes are below:
 * <p>
 * <p>
 * 0 ---- 1 ---- 2
 * </p>
 * <p>
 * 3 ---- 4 ---- 5
 * </p>
 * <p>
 * 6 ---- 7 ---- 8
 * </p>
 * 
 * @author mornin.z
 * 
 */
public class NinePINView extends View {
    private final static String TAG = "NinePINView";

    protected final int POINT_COUNT = 9;
    protected final int DEFAULT_PADDING = 20;
    protected final int DEFAULT_POINT_COLOR = 0xFFFFFFFF;
    protected final int DEFAULT_CIRCLE_COLOR = 0xFF66CC99;
    protected final int DEFAULT_LINE_COLOR = 0x77FFFFFF;
    protected final int DEFAULT_WRONG_COLOR = 0xFFFF0000;
    private int mPointColor;
    private float mPointSize;
    private Paint mPointPaint;

    private int mCircleColor;
    private float mCircleWidth;
    private float mCircleRadius;
    private Paint mCirclePaint;

    private int mLineColor;
    private float mLineWidth;
    private Paint mLinePaint;

    private int mWrongColor;
    private Path[] mWrongPaths = new Path[POINT_COUNT];
    private Paint mWrongPaint;

    /**
     * The correct PIN.
     */
    private String mCorrectPIN = "00";
    /**
     * PIN is consist of points in the drawn shape.
     */
    private String mDrawnPIN;

    /**
     * 9 center points
     */
    private Point[] mCenterPoints = new Point[POINT_COUNT];

    /**
     * The all point have been drawn since last MotionEvent.ACTION_DOWN event
     * triggered.
     */
    private List<Point> mDrawnPoints = new ArrayList<Point>();

    /**
     * The last been drawn point.
     */
    private Point mLastDrawnPoint = null;

    private class Point {
        int index;// index of 9 center points
        float x;
        float y;

        @Override
        public String toString() {
            return "Point [index=" + index + ", x=" + x + ", y=" + y + "]";
        }
    }

    /**
     * The current X coordinate of finger
     */
    private float mCurrX;
    /**
     * The current Y coordinate of finger
     */
    private float mCurrY;

    protected boolean mWillDrawWrongTriangle;

    protected OnDrawListener mOnDrawListener;

    protected Mode mCurrMode = Mode.MODE_WORK;

    public static enum Mode {
        /**
         * Study correct PIN shape mode.
         */
        MODE_STUDY,

        /**
         * Inspect drawn PIN shape mode.
         */
        MODE_WORK
    }

    public NinePINView(Context context) {
        this(context, null);
    }

    public NinePINView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NinePINView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NinePINView);

        mPointColor = a.getColor(R.styleable.NinePINView_pointColor, DEFAULT_POINT_COLOR);
        mCircleColor = a.getColor(R.styleable.NinePINView_circleColor, DEFAULT_CIRCLE_COLOR);
        mLineColor = a.getColor(R.styleable.NinePINView_lineColor, DEFAULT_LINE_COLOR);
        mWrongColor = a.getColor(R.styleable.NinePINView_wrongColor, DEFAULT_WRONG_COLOR);

        mPointSize = a.getDimension(R.styleable.NinePINView_pointSize, 8.0f);
        mCircleWidth = a.getDimension(R.styleable.NinePINView_circleWidth, 5.0f);
        mLineWidth = a.getDimension(R.styleable.NinePINView_lineWidth, 5.0f);
        mCircleRadius = a.getDimension(R.styleable.NinePINView_circleRadius, 40.0f);

        a.recycle();

        // center point
        mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPointPaint.setColor(mPointColor);
        mPointPaint.setStyle(Style.FILL);

        // circle
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setColor(mCircleColor);
        mCirclePaint.setStrokeWidth(mCircleWidth);
        mCirclePaint.setStyle(Style.STROKE);

        // connection line
        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setColor(mLineColor);
        mLinePaint.setStrokeWidth(mLineWidth);
        mLinePaint.setStyle(Style.FILL);
        mLinePaint.setStrokeCap(Cap.ROUND);
        mLinePaint.setStrokeJoin(Join.ROUND);

        // wrong triangle path
        mWrongPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mWrongPaint.setColor(mWrongColor);
        mWrongPaint.setStyle(Style.FILL);

        setPadding(Math.max(getPaddingLeft(), DEFAULT_PADDING), Math.max(getPaddingTop(), DEFAULT_PADDING),
                Math.max(getPaddingRight(), DEFAULT_PADDING), Math.max(getPaddingBottom(), DEFAULT_PADDING));

        mWillDrawWrongTriangle = false;
    }

    public int getPointColor() {
        return mPointColor;
    }

    public void setPointColor(int color) {
        mPointColor = color;
        mPointPaint.setColor(color);
    }

    public void setCircleColor(int color) {
        mCircleColor = color;
        mCirclePaint.setColor(color);
    }

    public void setLineColor(int color) {
        mLineColor = color;
        mLinePaint.setColor(color);
    }

    public void setPointSize(int size) {
        mPointSize = size;
        mPointPaint.setStrokeWidth(size);
    }

    public void setCircleWidth(int width) {
        mCircleWidth = width;
        mCirclePaint.setStrokeWidth(width);
    }

    public void setCircleRadius(int raduis) {
        mCircleRadius = raduis;
        mCirclePaint.setStrokeWidth(raduis);
    }

    public void setLineWidth(int width) {
        mLineWidth = width;
        mLinePaint.setStrokeWidth(width);
    }

    /**
     * Set current work mode of NinePINView.
     * 
     * @param mode
     *            {@link Mode#MODE_STUDY} or {@link Mode#MODE_WORK}
     */
    public void setMode(Mode mode) {
        mCurrMode = mode;
    }

    /**
     * Get the current work mode of NinePINView.
     * 
     * @return
     */
    public Mode getMode() {
        return mCurrMode;
    }

    /**
     * 
     * @return PIN String has been drawn.
     */
    public String getDrawnPIN() {
        return mDrawnPIN;
    }

    /**
     * Set correct PIN String used to check whether the drawn shape is correct
     * or not.
     * <p>
     * You must call this method before begin draw.
     * </p>
     * 
     * @param pin
     *            the correct PIN String. 0 is index of the first point.
     *            <b>eg:</b> "012" PIN String represent that the shape connect
     *            one, two and three points in first row.
     * @throws IllegalArgumentException
     *             if parameter <b>pin</b> contains characters besides '0'-'8'
     *             or same charaters.
     */
    public void setCorrectPIN(String pin) {
        boolean repeat = false;
        for (int i = 0; i < pin.length() - 1; i++) {
            for (int j = i + 1; j < pin.length(); j++) {
                if (pin.charAt(i) == pin.charAt(j)) {
                    repeat = true;
                    break;
                }
            }
        }
        boolean match = Pattern.matches("[0-8]{1,9}", pin);
        if (repeat || !match) {
            throw new IllegalArgumentException("The pin must only contains characters '0'-'8' and not be repeat.");
        }
        mCorrectPIN = pin;
    }

    public void setOnDrawListener(OnDrawListener listener) {
        mOnDrawListener = listener;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        computePointsAndWrongTriangleCoordinate();
    }

    /**
     * Compute the coordinates of 9 center points and wrong triangles.
     */
    protected void computePointsAndWrongTriangleCoordinate() {
        int drawWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int drawHeight = getHeight() - getPaddingTop() - getPaddingBottom();

        float baseX = getPaddingLeft() + mCircleRadius;
        float baseY = getPaddingTop() + mCircleRadius;
        float gapX = drawWidth / 2.0f - mCircleRadius;
        float gapY = drawHeight / 2.0f - mCircleRadius;

        float r = mCircleRadius;

        for (int i = 0; i < POINT_COUNT; i++) {
            // compute center point's coordinate
            Point point = new Point();
            point.x = baseX + gapX * (i % 3);
            point.y = baseY + gapY * (i / 3);
            point.index = i;
            mCenterPoints[i] = point;
            // compute wrong triangle path of this point.
            Path path = new Path();
            float x1, y1, x2, y2, x3, y3;
            x1 = point.x + r;
            y1 = point.y;
            x2 = point.x + r * (2.0f / 3);
            y2 = point.y - r * (1.0f / 3);
            x3 = x2;
            y3 = point.y + r * (1.0f / 3);
            path.moveTo(x1, y1);
            path.lineTo(x2, y2);
            path.lineTo(x3, y3);
            path.lineTo(x1, y1);
            path.close();
            mWrongPaths[i] = path;
            Log.d(TAG,
                    "[ " + x1 + ", " + y1 + " ], " + "[ " + x2 + ", " + y2 + " ], " + "[ " + x3 + ", " + y3 + " ]");
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (MotionEventCompat.getActionMasked(e)) {
        case MotionEvent.ACTION_DOWN:
            onDrawStart();
            clearDrawn();
            kissSomePoint(e);
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            kissSomePoint(e);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            if (mLastDrawnPoint != null) {
                mCurrX = mLastDrawnPoint.x;
                mCurrY = mLastDrawnPoint.y;
            }

            StringBuilder sb = new StringBuilder();
            for (Point p : mDrawnPoints) {
                sb.append(p.index);
            }
            mDrawnPIN = sb.toString();
            Log.d(TAG, "drawn pin : " + mDrawnPIN + ", correct pin : " + mCorrectPIN);

            if (mCurrMode == Mode.MODE_STUDY) {
                onDrawComplete(true);
                clearDrawn();
            } else if (mCurrMode == Mode.MODE_WORK) {
                if (!mDrawnPIN.equals(mCorrectPIN)) {
                    mCirclePaint.setColor(mWrongColor);
                    mWillDrawWrongTriangle = true;
                    onDrawComplete(false);
                } else {
                    onDrawComplete(true);
                }
            }

            invalidate();
            break;
        }

        return true;
    }

    /**
     * Clear to status before drawn, there are only 9 points in canvas.
     */
    protected void clearDrawn() {
        mDrawnPoints.clear();
        mLastDrawnPoint = null;
        mCirclePaint.setColor(mCircleColor);
        mWillDrawWrongTriangle = false;
    }

    /**
     * Check whether finger has entered some point's area or not.
     * 
     * @param e
     *            motion envent.
     */
    protected boolean kissSomePoint(MotionEvent e) {
        // We just check the one point.
        mCurrX = e.getX(0);
        mCurrY = e.getY(0);

        for (int i = 0; i < POINT_COUNT; i++) {
            Point p = mCenterPoints[i];
            if (Math.sqrt(Math.pow(mCurrX - p.x, 2) + Math.pow(mCurrY - p.y, 2)) <= mCircleRadius) {
                if (!mDrawnPoints.contains(p)) {
                    Log.d(TAG, "kiss " + p);
                    // Check the point between last drawn point and kissed
                    // point, if not drawn, draw it.
                    // There are two appropriate situations:
                    // 1. The two points are in corner.
                    // 2. The connection line of the two points through the
                    // point 4.
                    if (mLastDrawnPoint != null) {
                        if ((isCornerPoint(mLastDrawnPoint) && isCornerPoint(p))
                                || (mLastDrawnPoint.index + p.index == 8)) {
                            int middlePointIndex = (mLastDrawnPoint.index + p.index) / 2;
                            Point middlePoint = mCenterPoints[middlePointIndex];
                            if (!mDrawnPoints.contains(middlePoint)) {
                                mDrawnPoints.add(middlePoint);
                            }
                        }
                    }
                    mLastDrawnPoint = p;
                    mDrawnPoints.add(p);
                    return true;
                } else {
                    // This point has been kissed, don't be greedy!
                    break;
                }
            }
        }

        return false;
    }

    private boolean isCornerPoint(Point p) {
        if (p.index == 0 || p.index == 2 || p.index == 6 || p.index == 8) {
            return true;
        } else {
            return false;
        }
    }

    protected void onDrawStart() {
        if (mOnDrawListener != null) {
            mOnDrawListener.onDrawStart(this);
        } else {
            Log.w(TAG, "You should call NinePINView.setOnDrawCompleteListener() method to set a draw listener.");
        }
    }

    protected void onDrawComplete(boolean correct) {
        // XXX: this is not required. The correct PIN should be set by user.
        if (mCurrMode == Mode.MODE_STUDY) {
            mCorrectPIN = mDrawnPIN;
        }

        if (mOnDrawListener != null) {
            mOnDrawListener.onDrawComplete(this, correct);
        } else {
            Log.w(TAG, "You should call NinePINView.setOnDrawCompleteListener() method to set a draw listener.");
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // draw circles around center points and connection line
        int drawnCount = mDrawnPoints.size();
        for (int j = 0; j < drawnCount; j++) {
            Point p1 = mDrawnPoints.get(j);
            canvas.drawCircle(p1.x, p1.y, mCircleRadius, mCirclePaint);
            if (j + 1 < drawnCount) {
                Point p2 = mDrawnPoints.get(j + 1);
                canvas.drawCircle(p2.x, p2.y, mCircleRadius, mCirclePaint);
                canvas.drawLine(p1.x, p1.y, p2.x, p2.y, mLinePaint);
                if (mWillDrawWrongTriangle) {
                    // compute the wrong triangle's direction of this point.
                    float angle = 0.f;
                    if (p2.y == p1.y) {// x-axis
                        angle = p2.x > p1.x ? 0.f : 180.f;
                    } else if (p2.x == p1.x) { // y-axis
                        angle = p2.y > p1.y ? 90.f : -90.f;
                    } else {// in quadrants
                        double tanA = ((double) p2.y - (double) p1.y) / ((double) p2.x - (double) p1.x);
                        // in 1 or 4 quadrant
                        angle = (float) (Math.atan(tanA) * 180 / Math.PI);
                        // in 2 or 3 quadrant
                        if (p2.x < p1.x) {
                            angle += 180.f;
                        }
                    }
                    Log.d(TAG, "angle " + angle);
                    canvas.save();
                    canvas.rotate(angle, p1.x, p1.y);
                    canvas.drawPath(mWrongPaths[p1.index], mWrongPaint);
                    canvas.restore();
                }
            }
        }

        // draw extra connection line
        if (mLastDrawnPoint != null) {
            canvas.drawLine(mLastDrawnPoint.x, mLastDrawnPoint.y, mCurrX, mCurrY, mLinePaint);
        }

        // draw 9 center points
        for (int i = 0; i < POINT_COUNT; i++) {
            Point p = mCenterPoints[i];
            canvas.drawCircle(p.x, p.y, mPointSize, mPointPaint);
        }
    }

    /**
     * Interface definition for a callback to be invoked when finger drawn is
     * complete.
     */
    public interface OnDrawListener {

        /**
         * Called when finger begin to draw.
         * 
         * @param ninePINView
         *            The ninePINView which is being drawing.
         */
        public void onDrawStart(NinePINView ninePINView);

        /**
         * Called when finger draw has been complete.
         * 
         * @param ninePINView
         *            The ninePINView has been drawn.
         * @param correct
         *            true if shape that has been drawn is correct, false if
         *            wrong.
         */
        public void onDrawComplete(NinePINView ninePINView, boolean correct);

    }
}