package com.realtrackandroid.views.participationsactive.signinsheet; import java.util.ArrayList; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Path; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.realtrackandroid.R; /** * Custom view that allows the user to "write" a signature using their finger. Uses cubic Bezier * interpolation for curve smoothing. * * <p> * Here are two excellent resources: * <ul> * <li><a href="http://corner.squareup.com/2010/07/smooth-signatures.html">Smooth Signatures</a> * <li><a href="http://corner.squareup.com/2012/07/smoother-signatures.html">Smoother Signatures</a> * </ul> * * @author Raj */ public class SignatureView extends View { private ColorStateList mSignatureColor; private int mCurSignatureColor; private Path mSignaturePath; private RectF dirtyRectangle; private final static float STROKE_WIDTH = 5f; private Canvas mCanvas; Bitmap mOffScreenBitmap; private float mX, mY; private boolean mNothingDrawn; private ArrayList<MotionEvent> pathToRestore, pathToSave; private int xStart; private int xEnd; private int yStart; private int yEnd; private final static ShapeDrawable mHorizLine = new ShapeDrawable(new RectShape());; private Paint mSignaturePaint; private Bitmap mXmarkBitmap; private Paint mTextPaint; // stroke width? // rotation // velocity-based varying stroke width // save instance state on rotation public SignatureView(Context context, AttributeSet attrs) { super(context, attrs); setSaveEnabled(true); setFocusable(true); setFocusableInTouchMode(true); TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SignatureView, 0, 0); try { mSignatureColor = styledAttrs.getColorStateList(R.styleable.SignatureView_signatureColor); setSignatureColor(mSignatureColor == null ? ColorStateList.valueOf(Color.BLACK) : mSignatureColor); } finally { styledAttrs.recycle(); } init(); } private void init() { mSignaturePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mSignaturePaint.setColor(mCurSignatureColor); mSignaturePaint.setStyle(Paint.Style.STROKE); mSignaturePaint.setStrokeCap(Paint.Cap.BUTT); mSignaturePaint.setStrokeJoin(Paint.Join.ROUND); mSignaturePaint.setStrokeWidth(STROKE_WIDTH); mSignaturePath = new Path(); mNothingDrawn = true; dirtyRectangle = new RectF(); pathToSave = new ArrayList<MotionEvent>(); pathToRestore = new ArrayList<MotionEvent>(); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(getResources().getColor(R.color.lightgrey)); mTextPaint.setTextAlign(Align.CENTER); mTextPaint.setTextSize(25f); mHorizLine.getPaint().setColor(getResources().getColor(R.color.lightgrey)); mXmarkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.xmark); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (w == 0 || h == 0) return; super.onSizeChanged(w, h, oldw, oldh); createNewBitmap(); // don't put this in the constructor because getWidth and getHeight return 0 // before layout is finalized if (!pathToRestore.isEmpty()) { for (MotionEvent e : pathToRestore) onTouchEvent(e); pathToRestore.clear(); } xStart = 10; xEnd = getWidth() - 10; yStart = getHeight() - 45; yEnd = getHeight() - 43; mHorizLine.setBounds(xStart, yStart, xEnd, yEnd); } private void createNewBitmap() { mOffScreenBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mOffScreenBitmap); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mHorizLine.draw(canvas); canvas.drawText(getResources().getString(R.string.pleasesignhere), getWidth() / 2, yEnd + 25, mTextPaint); canvas.drawBitmap(mXmarkBitmap, xStart, yStart - mXmarkBitmap.getHeight(), mTextPaint); canvas.drawBitmap(mOffScreenBitmap, 0, 0, mSignaturePaint); } @Override public boolean onTouchEvent(MotionEvent e) { pathToSave.add(MotionEvent.obtain(e)); // important! use obtain or you'll get the same event // over and over again in the arraylist float x = e.getX(); float y = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_DOWN: mNothingDrawn = false; handleTouchDown(x, y); return true; case MotionEvent.ACTION_MOVE: handleTouchMove(e); break; case MotionEvent.ACTION_UP: handleTouchUp(x, y); break; default: return false; } postInvalidate((int) (dirtyRectangle.left - STROKE_WIDTH), (int) (dirtyRectangle.top - STROKE_WIDTH), (int) (dirtyRectangle.right + STROKE_WIDTH), (int) (dirtyRectangle.bottom + STROKE_WIDTH)); return true; } private void handleTouchDown(float x, float y) { mSignaturePath.reset(); mSignaturePath.moveTo(x, y); // start a new contour mSignaturePath.rLineTo(STROKE_WIDTH, STROKE_WIDTH); // single touches will create dots mX = x + STROKE_WIDTH; mY = y + STROKE_WIDTH; } private void handleTouchMove(MotionEvent e) { resetDirtyRectangle(e); collectPathHistory(e, mSignaturePath); mCanvas.drawPath(mSignaturePath, mSignaturePaint); } private void resetDirtyRectangle(MotionEvent e) { dirtyRectangle.left = Math.min(mX, e.getX()); dirtyRectangle.right = Math.max(mX, e.getX()); dirtyRectangle.top = Math.min(mY, e.getY()); dirtyRectangle.bottom = Math.max(mY, e.getY()); } private void collectPathHistory(MotionEvent e, Path path) { if (e.getHistorySize() > 1) { PointF s1 = new PointF(mX, mY); PointF s2 = new PointF(e.getHistoricalX(0), e.getHistoricalY(0)); mSignaturePath.rLineTo(s2.x - mX, s2.y - mY); mX = s2.x; mY = s2.y; updateDirtyRectangle(s2.x, s2.y); PointF controlPoint = s1; for (int i = 1; i < e.getHistorySize(); ++i) { PointF s3 = new PointF(e.getHistoricalX(i), e.getHistoricalY(i)); controlPoint = calculateBezierControlPoints(s1, s2, s3, controlPoint); s1 = s2; s2 = s3; mX = s3.x; mY = s3.y; updateDirtyRectangle(s3.x, s3.y); } mSignaturePath.lineTo(mX, mY); // important! take care of the last point or your lines will // turn out shaky } else { mSignaturePath.rLineTo(e.getX() - mX, e.getY() - mY); mX = e.getX(); mY = e.getY(); } } /** * Calculates Bezier control points given three points and then plots the Bezier curve using the * {@link android.graphics.Path#cubicTo(float, float, float, float, float, float)} method. * * <p> * For the algorithm itself, see the <a * href="http://www.antigrain.com/research/bezier_interpolation/"> the excellent explanation</a> * at the Anti-Grain Geometry Project web site. Another good resource is <a * href="http://www.benknowscode.com/2012/09/path-interpolation-using-cubic-bezier_9742.html">Ben * Olsen's Javascript implementation</a>. * <p> * Each triplet of points yields two control points. We use one and save the other for the next * call to this method. * * @param s1 * first point * @param s2 * second point * @param s3 * third point * @param controlPoint * control point to use * @return control point to be used in next call to this method. */ private PointF calculateBezierControlPoints(PointF s1, PointF s2, PointF s3, PointF controlPoint) { Line l1 = new Line(s1, s2); Line l2 = new Line(s2, s3); PointF m1 = l1.getMidPoint(); PointF m2 = l2.getMidPoint(); float k = l2.getLength() / (l1.getLength() + l2.getLength()); PointF cm = new PointF(m2.x + (m1.x - m2.x) * k, m2.y + (m1.y - m2.y) * k); float tx = s2.x - cm.x; float ty = s2.y - cm.y; PointF c1 = new PointF(m1.x + tx, m1.y + ty); PointF c2 = new PointF(m2.x + tx, m2.y + ty); mSignaturePath.cubicTo(controlPoint.x, controlPoint.y, c1.x, c1.y, s2.x, s2.y); return c2; } private void updateDirtyRectangle(float x, float y) { if (x < dirtyRectangle.left) dirtyRectangle.left = x; else if (x > dirtyRectangle.right) dirtyRectangle.right = x; // origin is top-left of screen if (y < dirtyRectangle.top) dirtyRectangle.top = y; else if (y > dirtyRectangle.bottom) dirtyRectangle.bottom = y; } private void handleTouchUp(float x, float y) { mSignaturePath.rLineTo(x - mX, y - mY); mX = x; mY = y; mCanvas.drawPath(mSignaturePath, mSignaturePaint); mSignaturePath.reset(); } public final ColorStateList getSignatureColors() { return mSignatureColor; } private static ColorStateList getSignatureColors(Context context, TypedArray attrs) { return attrs.getColorStateList(R.styleable.SignatureView_signatureColor); } public static int getTextColor(Context context, TypedArray attrs, int def) { ColorStateList colors = getSignatureColors(context, attrs); if (colors == null) { return def; } else { return colors.getDefaultColor(); } } public void setSignatureColor(ColorStateList signatureColor) { if (signatureColor == null) throw new NullPointerException(); this.mSignatureColor = signatureColor; updateSignatureColor(); } private void updateSignatureColor() { boolean inval = false; int color = mSignatureColor.getDefaultColor(); if (color != mCurSignatureColor) { mCurSignatureColor = color; inval = true; } if (inval) { refreshView(); } } public void eraseSignature() { mNothingDrawn = true; mSignaturePath.reset(); pathToRestore.clear(); pathToSave.clear(); createNewBitmap(); postInvalidate(); } public Bitmap getSignature() { if (mNothingDrawn) return null; else return Bitmap.createScaledBitmap(mOffScreenBitmap, getWidth() / 2, getHeight() / 2, false); } private void refreshView() { postInvalidate(); requestLayout(); } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("instanceState", super.onSaveInstanceState()); bundle.putBoolean("mNothingDrawn", mNothingDrawn); bundle.putInt("mCurSignatureColor", mCurSignatureColor); bundle.putParcelable("mSignatureColor", mSignatureColor); bundle.putParcelableArrayList("pathToRestore", pathToSave); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; mNothingDrawn = bundle.getBoolean("mNothingDrawn"); mCurSignatureColor = bundle.getInt("mCurSignatureColor"); mSignatureColor = bundle.getParcelable("mSignatureColor"); pathToRestore = bundle.getParcelableArrayList("pathToRestore"); pathToSave.clear(); state = bundle.getParcelable("instanceState"); } super.onRestoreInstanceState(state); } /** * Models a signature line. * * @author Raj */ private class Line { private PointF left, right; public Line(PointF left, PointF right) { this.left = left; this.right = right; } public float getLength() { return (float) Math.sqrt((left.x - right.x) * (left.x - right.x) + (left.y - right.y) * (left.y - right.y)); } public PointF getMidPoint() { return new PointF((left.x + right.x) / 2, (left.y + right.y) / 2); } } }