Android 自定義手寫簽名並儲存到sdcard中(可以儲存背景圖)
阿新 • • 發佈:2019-01-24
這是效果圖
直接上程式碼咯。。。
—————————————— MainActivity
package com.signature.demo; import android.app.Activity; 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.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.example.hand.R; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class MainActivity extends Activity { private SignatureView mSignaturePad; private Button mClearButton; private Button mSaveButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSignaturePad = (SignatureView) findViewById(R.id.signature_pad); mSignaturePad.setBackgroundResource(R.drawable.a); mSignaturePad.setOnSignedListener(new SignatureView.OnSignedListener() { @Override public void onSigned() { mSaveButton.setEnabled(true); mClearButton.setEnabled(true); } @Override public void onClear() { mSaveButton.setEnabled(false); mClearButton.setEnabled(false); } }); mClearButton = (Button) findViewById(R.id.clear_button); mSaveButton = (Button) findViewById(R.id.save_button); mClearButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mSignaturePad.clear(); } }); mSaveButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Bitmap signatureBitmap = mSignaturePad.getSignatureBitmap(); if (addSignatureToGallery(signatureBitmap)) { Toast.makeText(MainActivity.this, "已儲存到:"+Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pictures/SignatureFile/"+System.currentTimeMillis()+".jpg", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "儲存失敗", Toast.LENGTH_SHORT).show(); } } }); } public File getAlbumStorageDir(String albumName) { // Get the directory for the user's public pictures directory. File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName); if (!file.mkdirs()) { Log.e("SignatureFile", "Directory not created"); } return file; } public 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(); } public boolean addSignatureToGallery(Bitmap signature) { boolean result = false; try { File photo = new File(getAlbumStorageDir("SignatureFile"), String.format("Signature_%d.jpg",System.currentTimeMillis())); saveBitmapToJPG(signature, photo); Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Uri contentUri = Uri.fromFile(photo); mediaScanIntent.setData(contentUri); MainActivity.this.sendBroadcast(mediaScanIntent); result = true; } catch (IOException e) { e.printStackTrace(); } return result; } }
—————————————— SignatureView
package com.signature.demo; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import com.example.hand.R; import java.util.ArrayList; import java.util.List; public class SignatureView extends View { // View state private List<TimedPoint> mPoints; private boolean mIsEmpty; private float mLastTouchX; private float mLastTouchY; private float mLastVelocity; private float mLastWidth; private RectF mDirtyRect; // Configurable parameters private int mMinWidth; private int mMaxWidth; private float mVelocityFilterWeight; private OnSignedListener mOnSignedListener; private Paint mPaint = new Paint(); private Path mPath = new Path(); private Bitmap mSignatureBitmap = null; private Canvas mSignatureBitmapCanvas = null; public SignatureView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SignatureView, 0, 0); // Configurable parameters try { mMinWidth = a.getDimensionPixelSize(R.styleable.SignatureView_minWidth, convertDpToPx(3)); mMaxWidth = a.getDimensionPixelSize(R.styleable.SignatureView_maxWidth, convertDpToPx(7)); mVelocityFilterWeight = a.getFloat(R.styleable.SignatureView_velocityFilterWeight, 0.9f); mPaint.setColor(a.getColor(R.styleable.SignatureView_penColor, Color.BLACK)); } finally { a.recycle(); } // Fixed parameters mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); // Dirty rectangle to update only the changed portion of the view mDirtyRect = new RectF(); clear(); } /** * Set the pen color from a given resource. If the resource is not found, * {@link android.graphics.Color#BLACK} is assumed. * * @param colorRes the color resource. */ @SuppressLint("ResourceType") public void setPenColorRes(int colorRes) { try { setPenColor(getResources().getColor(colorRes)); } catch (Resources.NotFoundException ex) { setPenColor(getResources().getColor(Color.BLACK)); } } /** * Set the pen color from a given color. * * @param color the color. */ public void setPenColor(int color) { mPaint.setColor(color); } /** * Set the minimum width of the stroke in pixel. * * @param minWidth the width in dp. */ public void setMinWidth(float minWidth) { mMinWidth = convertDpToPx(minWidth); } /** * Set the maximum width of the stroke in pixel. * * @param maxWidth the width in dp. */ public void setMaxWidth(float maxWidth) { mMaxWidth = convertDpToPx(maxWidth); } /** * Set the velocity filter weight. * * @param velocityFilterWeight the weight. */ public void setVelocityFilterWeight(float velocityFilterWeight) { mVelocityFilterWeight = velocityFilterWeight; } public void clear() { mPoints = new ArrayList<TimedPoint>(); mLastVelocity = 0; mLastWidth = (mMinWidth + mMaxWidth) / 2; mPath.reset(); 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(); mPath.moveTo(eventX, eventY); mLastTouchX = eventX; mLastTouchY = eventY; addPoint(new TimedPoint(eventX, eventY)); case MotionEvent.ACTION_MOVE: resetDirtyRect(eventX, eventY); addPoint(new TimedPoint(eventX, eventY)); break; case MotionEvent.ACTION_UP: resetDirtyRect(eventX, eventY); addPoint(new TimedPoint(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 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(Bitmap signature) { clear(); ensureSignatureBitmap(); RectF tempSrc = new RectF(); RectF tempDst = new RectF(); int dWidth = signature.getWidth(); int dHeight = signature.getHeight(); int vWidth = getWidth(); int vHeight = getHeight(); // Generate the required transform. 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(); } 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; // Find xMin 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; } // Image is empty... if (!foundPixel) return null; // Find yMin 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; } // Find xMax 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; } // Find yMax 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 void addPoint(TimedPoint newPoint) { mPoints.add(newPoint); if (mPoints.size() > 2) { // To reduce the initial lag make it work with 3 mPoints // by copying the first point to the beginning. if (mPoints.size() == 3) mPoints.add(0, mPoints.get(0)); ControlTimedPoints tmp = calculateCurveControlPoints(mPoints.get(0), mPoints.get(1), mPoints.get(2)); TimedPoint c2 = tmp.c2; tmp = calculateCurveControlPoints(mPoints.get(1), mPoints.get(2), mPoints.get(3)); TimedPoint c3 = tmp.c1; Bezier curve = new Bezier(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; // The new width is a function of the velocity. Higher velocities // correspond to thinner strokes. float newWidth = strokeWidth(velocity); // The Bezier's width starts out as last curve's final width, and // gradually changes to the stroke width just calculated. The new // width calculation is based on the velocity between the Bezier's // start and end mPoints. addBezier(curve, mLastWidth, newWidth); mLastVelocity = velocity; mLastWidth = newWidth; // Remove the first element from the list, // so that we always have no more than 4 mPoints in mPoints array. mPoints.remove(0); } } private void addBezier(Bezier curve, float startWidth, float endWidth) { ensureSignatureBitmap(); float originalWidth = mPaint.getStrokeWidth(); float widthDelta = endWidth - startWidth; float drawSteps = (float) Math.floor(curve.length()); for (int i = 0; i < drawSteps; i++) { // Calculate the Bezier (x, y) coordinate for this step. 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; // Set the incremental stroke width and draw. 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; TimedPoint m1 = new TimedPoint((s1.x + s2.x) / 2.0f, (s1.y + s2.y) / 2.0f); TimedPoint m2 = new TimedPoint((s2.x + s3.x) / 2.0f, (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 = (m1.x - m2.x); float dym = (m1.y - m2.y); float k = l2 / (l1 + l2); TimedPoint cm = new TimedPoint(m2.x + dxm * k, m2.y + dym * k); float tx = s2.x - cm.x; float ty = s2.y - cm.y; return new ControlTimedPoints(new TimedPoint(m1.x + tx, m1.y + ty), new TimedPoint(m2.x + tx, m2.y + ty)); } private float strokeWidth(float velocity) { return Math.max(mMaxWidth / (velocity + 1), mMinWidth); } /** * Called when replaying history to ensure the dirty region includes all * mPoints. * * @param historicalX the previous x coordinate. * @param historicalY the previous y coordinate. */ 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; } } /** * Resets the dirty region when the motion event occurs. * * @param eventX the event x coordinate. * @param eventY the event y coordinate. */ private void resetDirtyRect(float eventX, float eventY) { // The mLastTouchX and mLastTouchY were set when the ACTION_DOWN motion // event occurred. 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 int width, height; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); } public void ensureSignatureBitmap() { if (mSignatureBitmap == null) { mSignatureBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); mSignatureBitmapCanvas = new Canvas(mSignatureBitmap); //設定背景圖圖片 要指定圖片大小 否則下面註釋的方法會放大 mSignatureBitmapCanvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.a), new Rect(0, 0, BitmapFactory.decodeResource(getResources(), R.drawable.a).getWidth(), BitmapFactory.decodeResource(getResources(), R.drawable.a).getHeight()), new Rect(0, 0, width, height), mPaint); // mSignatureBitmapCanvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bg_splash), matrix, mPaint); } } // public void drawBitmap() { // if (mSignatureBitmapCanvas == null) { // mSignatureBitmapCanvas = new Canvas(); // } // mSignatureBitmapCanvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bg_splash), new Matrix(), mPaint); // // } private int convertDpToPx(float dp) { return Math.round(dp * (getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT)); } public interface OnSignedListener { public void onSigned(); public void onClear(); } }
—————————————— Bezier
package com.signature.demo; public class Bezier { public TimedPoint startPoint; public TimedPoint control1; public TimedPoint control2; public TimedPoint endPoint; public Bezier(TimedPoint startPoint, TimedPoint control1, TimedPoint control2, TimedPoint endPoint) { this.startPoint = startPoint; this.control1 = control1; this.control2 = control2; this.endPoint = endPoint; } public float length() { int steps = 10, length = 0; int i; float t; double cx, cy, px = 0, py = 0, xdiff, ydiff; for (i = 0; i <= steps; i++) { t = i / steps; cx = point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x); cy = point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y); if (i > 0) { xdiff = cx - px; ydiff = cy - py; length += Math.sqrt(xdiff * xdiff + ydiff * ydiff); } px = cx; py = cy; } return length; } public double point(float t, float start, float c1, float c2, float end) { return start * (1.0 - t) * (1.0 - t) * (1.0 - t) + 3.0 * c1 * (1.0 - t) * (1.0 - t) * t + 3.0 * c2 * (1.0 - t) * t * t + end * t * t * t; } }
—————————————— ControlTimedPoints
package com.signature.demo;
public class ControlTimedPoints {
public TimedPoint c1;
public TimedPoint c2;
public ControlTimedPoints(TimedPoint c1, TimedPoint c2) {
this.c1 = c1;
this.c2 = c2;
}
}
—————————————— TimedPoint
package com.signature.demo;
public class TimedPoint {
public final float x;
public final float y;
public final long timestamp;
public TimedPoint(float x, float y) {
this.x = x;
this.y = y;
this.timestamp = System.currentTimeMillis();
}
public float velocityFrom(TimedPoint start) {
float velocity = distanceTo(start) / (this.timestamp - start.timestamp);
if (velocity != velocity)
return 0f;
return velocity;
}
public float distanceTo(TimedPoint point) {
return (float) Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2));
}
}
—————————————— activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.signature.demo.MainActivity" >
<com.signature.demo.SignatureView
android:id="@+id/signature_pad"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/buttons_container" />
<LinearLayout
android:id="@+id/buttons_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:paddingTop="16dp" >
<Button
android:id="@+id/clear_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:text="清除" />
<Button
android:id="@+id/save_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:text="儲存" />
</LinearLayout>
</RelativeLayout>