安卓-電子簽名signature
阿新 • • 發佈:2018-12-11
1.先上效果圖:
2.首先需要繪製自定義的view,用於電子簽名:
import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import com.xiaoying.mysignature.R; import java.util.ArrayList; import java.util.List; /** * 繪製SignaturePad的View 用於簽名 */ public class SignaturePad extends View { //檢視狀態 private List<TimedPoint> mPoints; private boolean mIsEmpty; private float mLastTouchX; private float mLastTouchY; private float mLastVelocity; private float mLastWidth; private RectF mDirtyRect; private final SvgBuilder mSvgBuilder = new SvgBuilder(); //隱藏狀態 private List<TimedPoint> mPointsCache = new ArrayList<>(); private ControlTimedPoints mControlTimedPointsCached = new ControlTimedPoints(); private Bezier mBezierCached = new Bezier(); //可以配置的引數 private int mMinWidth; private int mMaxWidth; private float mVelocityFilterWeight; private OnSignedListener mOnSignedListener; private boolean mClearOnDoubleClick; //單次點選值 private long mFirstClick; private int mCountClick; private static final int DOUBLE_CLICK_DELAY_MS = 200; //預設的屬性值 private final int DEFAULT_ATTR_PEN_MIN_WIDTH_PX = 3; private final int DEFAULT_ATTR_PEN_MAX_WIDTH_PX = 7; private final int DEFAULT_ATTR_PEN_COLOR = Color.BLACK; private final float DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT = 0.9f; private final boolean DEFAULT_ATTR_CLEAR_ON_DOUBLE_CLICK = false; private Paint mPaint = new Paint(); private Bitmap mSignatureBitmap = null; private Canvas mSignatureBitmapCanvas = null; public SignaturePad(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.SignaturePad, 0, 0); //可以配置的引數 try { mMinWidth = a.getDimensionPixelSize(R.styleable.SignaturePad_penMinWidth, convertDpToPx(DEFAULT_ATTR_PEN_MIN_WIDTH_PX)); mMaxWidth = a.getDimensionPixelSize(R.styleable.SignaturePad_penMaxWidth, convertDpToPx(DEFAULT_ATTR_PEN_MAX_WIDTH_PX)); mPaint.setColor(a.getColor(R.styleable.SignaturePad_penColor, DEFAULT_ATTR_PEN_COLOR)); mVelocityFilterWeight = a.getFloat(R.styleable.SignaturePad_velocityFilterWeight, DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT); mClearOnDoubleClick = a.getBoolean(R.styleable.SignaturePad_clearOnDoubleClick, DEFAULT_ATTR_CLEAR_ON_DOUBLE_CLICK); } finally { a.recycle(); } //固定的配置引數 mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); //更新矩形內的部分檢視 mDirtyRect = new RectF(); clear(); } /** * 從資源中設定電子筆顯示的顏色,預設為黑色 * * @param colorRes */ public void setPenColorRes(int colorRes) { try { setPenColor(getResources().getColor(colorRes)); } catch (Resources.NotFoundException ex) { setPenColor(Color.parseColor("#000000")); } } /** * 設定給定的電子筆顯示的顏色 */ public void setPenColor(int color) { mPaint.setColor(color); } /** * 設定畫素中筆畫的最小寬度 * * @param minWidth 單位 dp */ public void setMinWidth(float minWidth) { mMinWidth = convertDpToPx(minWidth); } /** * 設定畫素中筆畫的最大寬度 * * @param maxWidth 單位 dp */ public void setMaxWidth(float maxWidth) { mMaxWidth = convertDpToPx(maxWidth); } /** * 設定速度過濾器的權重 */ public void setVelocityFilterWeight(float velocityFilterWeight) { mVelocityFilterWeight = velocityFilterWeight; } public void clear() { mSvgBuilder.clear(); mPoints = new ArrayList<>(); mLastVelocity = 0; mLastWidth = (mMinWidth + mMaxWidth) / 2; if (mSignatureBitmap != null) { mSignatureBitmap = null; ensureSignatureBitmap(); } setIsEmpty(true); invalidate(); } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) return false; float eventX = event.getX(); float eventY = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); mPoints.clear(); if (isDoubleClick()) break; mLastTouchX = eventX; mLastTouchY = eventY; addPoint(getNewPoint(eventX, eventY)); if (mOnSignedListener != null) mOnSignedListener.onStartSigning(); case MotionEvent.ACTION_MOVE: resetDirtyRect(eventX, eventY); addPoint(getNewPoint(eventX, eventY)); break; case MotionEvent.ACTION_UP: resetDirtyRect(eventX, eventY); addPoint(getNewPoint(eventX, eventY)); getParent().requestDisallowInterceptTouchEvent(true); setIsEmpty(false); break; default: return false; } //invalidate(); invalidate( (int) (mDirtyRect.left - mMaxWidth), (int) (mDirtyRect.top - mMaxWidth), (int) (mDirtyRect.right + mMaxWidth), (int) (mDirtyRect.bottom + mMaxWidth)); return true; } @Override protected void onDraw(Canvas canvas) { if (mSignatureBitmap != null) { canvas.drawBitmap(mSignatureBitmap, 0, 0, mPaint); } } public void setOnSignedListener(OnSignedListener listener) { mOnSignedListener = listener; } public boolean isEmpty() { return mIsEmpty; } public String getSignatureSvg() { int width = getTransparentSignatureBitmap().getWidth(); int height = getTransparentSignatureBitmap().getHeight(); return mSvgBuilder.build(width, height); } public Bitmap getSignatureBitmap() { Bitmap originalBitmap = getTransparentSignatureBitmap(); Bitmap whiteBgBitmap = Bitmap.createBitmap(originalBitmap.getWidth(), originalBitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(whiteBgBitmap); canvas.drawColor(Color.WHITE); canvas.drawBitmap(originalBitmap, 0, 0, null); return whiteBgBitmap; } public void setSignatureBitmap(final Bitmap signature) { //制定view if (ViewCompat.isLaidOut(this)) { clear(); ensureSignatureBitmap(); RectF tempSrc = new RectF(); RectF tempDst = new RectF(); int dWidth = signature.getWidth(); int dHeight = signature.getHeight(); int vWidth = getWidth(); int vHeight = getHeight(); //生成可替換的變數 tempSrc.set(0, 0, dWidth, dHeight); tempDst.set(0, 0, vWidth, vHeight); Matrix drawMatrix = new Matrix(); drawMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER); Canvas canvas = new Canvas(mSignatureBitmap); canvas.drawBitmap(signature, drawMatrix, null); setIsEmpty(false); invalidate(); } else { getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { //移除佈局偵聽器 ViewTreeObserverCompat.removeOnGlobalLayoutListener(getViewTreeObserver(), this); //電子簽名圖 setSignatureBitmap(signature); } }); } } public Bitmap getTransparentSignatureBitmap() { ensureSignatureBitmap(); return mSignatureBitmap; } public Bitmap getTransparentSignatureBitmap(boolean trimBlankSpace) { if (!trimBlankSpace) { return getTransparentSignatureBitmap(); } ensureSignatureBitmap(); int imgHeight = mSignatureBitmap.getHeight(); int imgWidth = mSignatureBitmap.getWidth(); int backgroundColor = Color.TRANSPARENT; int xMin = Integer.MAX_VALUE, xMax = Integer.MIN_VALUE, yMin = Integer.MAX_VALUE, yMax = Integer.MIN_VALUE; boolean foundPixel = false; //x軸最小值 for (int x = 0; x < imgWidth; x++) { boolean stop = false; for (int y = 0; y < imgHeight; y++) { if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { xMin = x; stop = true; foundPixel = true; break; } } if (stop) break; } //電子圖片為空 if (!foundPixel) return null; //y軸最小值 for (int y = 0; y < imgHeight; y++) { boolean stop = false; for (int x = xMin; x < imgWidth; x++) { if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { yMin = y; stop = true; break; } } if (stop) break; } //x軸最大值 for (int x = imgWidth - 1; x >= xMin; x--) { boolean stop = false; for (int y = yMin; y < imgHeight; y++) { if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { xMax = x; stop = true; break; } } if (stop) break; } //y軸最大值 for (int y = imgHeight - 1; y >= yMin; y--) { boolean stop = false; for (int x = xMin; x <= xMax; x++) { if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { yMax = y; stop = true; break; } } if (stop) break; } return Bitmap.createBitmap(mSignatureBitmap, xMin, yMin, xMax - xMin, yMax - yMin); } private boolean isDoubleClick() { if (mClearOnDoubleClick) { if (mFirstClick != 0 && System.currentTimeMillis() - mFirstClick > DOUBLE_CLICK_DELAY_MS) { mCountClick = 0; } mCountClick++; if (mCountClick == 1) { mFirstClick = System.currentTimeMillis(); } else if (mCountClick == 2) { long lastClick = System.currentTimeMillis(); if (lastClick - mFirstClick < DOUBLE_CLICK_DELAY_MS) { this.clear(); return true; } } } return false; } private TimedPoint getNewPoint(float x, float y) { int mCacheSize = mPointsCache.size(); TimedPoint timedPoint; if (mCacheSize == 0) { //快取為空時建立一個物件 timedPoint = new TimedPoint(); } else { //從快取中獲取 timedPoint = mPointsCache.remove(mCacheSize - 1); } return timedPoint.set(x, y); } private void recyclePoint(TimedPoint point) { mPointsCache.add(point); } private void addPoint(TimedPoint newPoint) { mPoints.add(newPoint); int pointsCount = mPoints.size(); if (pointsCount > 3) { ControlTimedPoints tmp = calculateCurveControlPoints(mPoints.get(0), mPoints.get(1), mPoints.get(2)); TimedPoint c2 = tmp.c2; recyclePoint(tmp.c1); tmp = calculateCurveControlPoints(mPoints.get(1), mPoints.get(2), mPoints.get(3)); TimedPoint c3 = tmp.c1; recyclePoint(tmp.c2); Bezier curve = mBezierCached.set(mPoints.get(1), c2, c3, mPoints.get(2)); TimedPoint startPoint = curve.startPoint; TimedPoint endPoint = curve.endPoint; float velocity = endPoint.velocityFrom(startPoint); velocity = Float.isNaN(velocity) ? 0.0f : velocity; velocity = mVelocityFilterWeight * velocity + (1 - mVelocityFilterWeight) * mLastVelocity; //對應的比劃寬度 float newWidth = strokeWidth(velocity); //開始和結束的繪製點 addBezier(curve, mLastWidth, newWidth); mLastVelocity = velocity; mLastWidth = newWidth; //從列表中刪除第一個元素,總長度不超過4個繪製點 recyclePoint(mPoints.remove(0)); recyclePoint(c2); recyclePoint(c3); } else if (pointsCount == 1) { TimedPoint firstPoint = mPoints.get(0); mPoints.add(getNewPoint(firstPoint.x, firstPoint.y)); } } private void addBezier(Bezier curve, float startWidth, float endWidth) { mSvgBuilder.append(curve, (startWidth + endWidth) / 2); ensureSignatureBitmap(); float originalWidth = mPaint.getStrokeWidth(); float widthDelta = endWidth - startWidth; float drawSteps = (float) Math.floor(curve.length()); for (int i = 0; i < drawSteps; i++) { //計算這個步驟的 Bezier(x,y) 座標 float t = ((float) i) / drawSteps; float tt = t * t; float ttt = tt * t; float u = 1 - t; float uu = u * u; float uuu = uu * u; float x = uuu * curve.startPoint.x; x += 3 * uu * t * curve.control1.x; x += 3 * u * tt * curve.control2.x; x += ttt * curve.endPoint.x; float y = uuu * curve.startPoint.y; y += 3 * uu * t * curve.control1.y; y += 3 * u * tt * curve.control2.y; y += ttt * curve.endPoint.y; //設定增量筆畫寬度和繪製 mPaint.setStrokeWidth(startWidth + ttt * widthDelta); mSignatureBitmapCanvas.drawPoint(x, y, mPaint); expandDirtyRect(x, y); } mPaint.setStrokeWidth(originalWidth); } private ControlTimedPoints calculateCurveControlPoints(TimedPoint s1, TimedPoint s2, TimedPoint s3) { float dx1 = s1.x - s2.x; float dy1 = s1.y - s2.y; float dx2 = s2.x - s3.x; float dy2 = s2.y - s3.y; float m1X = (s1.x + s2.x) / 2.0f; float m1Y = (s1.y + s2.y) / 2.0f; float m2X = (s2.x + s3.x) / 2.0f; float m2Y = (s2.y + s3.y) / 2.0f; float l1 = (float) Math.sqrt(dx1 * dx1 + dy1 * dy1); float l2 = (float) Math.sqrt(dx2 * dx2 + dy2 * dy2); float dxm = (m1X - m2X); float dym = (m1Y - m2Y); float k = l2 / (l1 + l2); if (Float.isNaN(k)) k = 0.0f; float cmX = m2X + dxm * k; float cmY = m2Y + dym * k; float tx = s2.x - cmX; float ty = s2.y - cmY; return mControlTimedPointsCached.set(getNewPoint(m1X + tx, m1Y + ty), getNewPoint(m2X + tx, m2Y + ty)); } private float strokeWidth(float velocity) { return Math.max(mMaxWidth / (velocity + 1), mMinWidth); } /** * 記錄歷史畫布上繪製的區域 * * @param historicalX 上次x軸點 * @param historicalY 上次y軸點 */ private void expandDirtyRect(float historicalX, float historicalY) { if (historicalX < mDirtyRect.left) { mDirtyRect.left = historicalX; } else if (historicalX > mDirtyRect.right) { mDirtyRect.right = historicalX; } if (historicalY < mDirtyRect.top) { mDirtyRect.top = historicalY; } else if (historicalY > mDirtyRect.bottom) { mDirtyRect.bottom = historicalY; } } /** * 當動作事件發生時重置已繪製的區域 * * @param eventX 當前x軸點 * @param eventY 當前y軸點 */ private void resetDirtyRect(float eventX, float eventY) { mDirtyRect.left = Math.min(mLastTouchX, eventX); mDirtyRect.right = Math.max(mLastTouchX, eventX); mDirtyRect.top = Math.min(mLastTouchY, eventY); mDirtyRect.bottom = Math.max(mLastTouchY, eventY); } private void setIsEmpty(boolean newValue) { mIsEmpty = newValue; if (mOnSignedListener != null) { if (mIsEmpty) { mOnSignedListener.onClear(); } else { mOnSignedListener.onSigned(); } } } private void ensureSignatureBitmap() { if (mSignatureBitmap == null) { mSignatureBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); mSignatureBitmapCanvas = new Canvas(mSignatureBitmap); } } private int convertDpToPx(float dp) { return Math.round(getContext().getResources().getDisplayMetrics().density * dp); } public interface OnSignedListener { void onStartSigning(); void onSigned(); void onClear(); } }
3.在/src/res/values目錄下新增attr.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SignaturePad"> <attr name="penMinWidth" format="dimension" /> <attr name="penMaxWidth" format="dimension" /> <attr name="penColor" format="color" /> <attr name="velocityFilterWeight" format="float" /> <attr name="clearOnDoubleClick" format="boolean" /> </declare-styleable> </resources>
4.新增xml檔案
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.xiaoying.mysignature.signature.SignaturePad android:id="@+id/signaturePad" android:layout_width="match_parent" android:layout_height="500dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="510dp" android:orientation="horizontal"> <TextView android:id="@+id/btnClear" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:background="@null" android:enabled="false" android:gravity="center" android:paddingBottom="11dp" android:paddingTop="11dp" android:text="清除簽名" android:textColor="@color/colorPrimary" android:textSize="16sp" /> <TextView android:id="@+id/btnSave" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:background="@null" android:enabled="false" android:gravity="center" android:paddingBottom="11dp" android:paddingTop="11dp" android:text="儲存簽名" android:textColor="@color/colorPrimary" android:textSize="16sp" /> </LinearLayout> </FrameLayout>
5.獲取主要的java檔案
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.xiaoying.mysignature.signature.SignaturePad;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class MainActivity extends AppCompatActivity implements SignaturePad.OnSignedListener {
@BindView(R.id.signaturePad)
SignaturePad signaturePad;
@BindView(R.id.btnClear)
TextView btnClear;
@BindView(R.id.btnSave)
TextView btnSave;
private File photo;
private Bitmap signatureBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
signaturePad.setOnSignedListener(this);
}
@OnClick({R.id.btnClear, R.id.btnSave})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btnClear:
//清除電子簽名
signaturePad.clear();
break;
case R.id.btnSave:
signatureBitmap = signaturePad.getSignatureBitmap();
if (addJpgSignatureToGallery(signatureBitmap)) {
Toast.makeText(this, "儲存簽名成功", Toast.LENGTH_LONG).show();
//跳轉至系統相簿
skipPictures();
}
break;
}
}
private Intent skipPictures() {
/* 開啟Pictures畫面Type設定為image */
Intent intent = new Intent();
intent.setType("image/*");
/* 使用Intent.ACTION_GET_CONTENT這個Action */
intent.setAction(Intent.ACTION_GET_CONTENT);
/* 取得相片後返回本畫面 */
startActivityForResult(intent, 1);
//(在onActivityResult方法裡,返回的意圖裡獲取圖片uri,在通過uri,結合內容提供者在查出圖片的路徑)
return intent;
}
@Override
public void onStartSigning() {
}
@Override
public void onSigned() {
btnClear.setEnabled(true);
btnSave.setEnabled(true);
}
@Override
public void onClear() {
btnClear.setEnabled(false);
btnSave.setEnabled(false);
}
//將.jpg 簽名新增到 Gallery 中
private boolean addJpgSignatureToGallery(Bitmap signature) {
boolean result = false;
try {
photo = new File(getAlbumStorageDir("SignaturePad"), String.format("Signature_%d.jpg", System.currentTimeMillis()));
saveBitmapToJPG(signature, photo);
scanMediaFile(photo);
result = true;
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private File getAlbumStorageDir(String albumName) {
//電子簽名圖片儲存目錄
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e("SignaturePad", "Directory not created");
}
return file;
}
//將點陣圖儲存格式為.jpg
private void saveBitmapToJPG(Bitmap bitmap, File photo) throws IOException {
Bitmap newBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newBitmap);
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(bitmap, 0, 0, null);
OutputStream stream = new FileOutputStream(photo);
newBitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream);
stream.close();
}
//媒體檔案掃描
private void scanMediaFile(File photo) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(photo);
mediaScanIntent.setData(contentUri);
this.sendBroadcast(mediaScanIntent);
}
}
6. 附上原始碼 歡迎留言討論