Android中手写签名的实现

前言

本来这篇文章应该很早就发出来了,因为最近一直在忙项目,直到现在才发出来。本篇文章就是介绍如何在手机上进行签名。

设计思路

在画板上进行签名(其实就是绘制图片),裁剪图片,然后保存到本地相册。

效果图


代码

1.自己参考资料写了一个SignatureView继承View,实现签名

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(20));
         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);
      //mPaint.setStyle(Paint.Style.FILL);  //画笔风格
//    mPaint.setAntiAlias(true);          //抗锯齿
//    mPaint.setStrokeWidth(10);           //画笔粗细
//    mPaint.setTextSize(100);             //绘制文字大小,单位px
      //mPaint.setStrokeWidth(Paint.S);
      // 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 Color#BLACK} is assumed.
    *
    * @param colorRes
    *            the color resource.
    */
   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 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(dp * (getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
   }

   public interface OnSignedListener {
      public void onSigned();

      public void onClear();
   }
}

2.在Android6.0以后有些权限要动态添加,6.0以下可以在清单文件中添加

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
    if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4);
    } else {
        Toast.makeText(this, "已开启权限", Toast.LENGTH_SHORT).show();
    }
}

回调的方法

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode){
        case 4:
            if(grantResults.length>0 &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
                Toast.makeText(this, "已打开权限!", Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(this, "请打开权限!", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
    }
}

3.图片裁剪

public static Bitmap compressScale(Bitmap image) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    if( baos.toByteArray().length / 1024>1024) {//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
        baos.reset();//重置baos即清空baos
        image.compress(Bitmap.CompressFormat.JPEG, 50, baos);//这里压缩50%,把压缩后的数据存放到baos中
    }
    ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
    BitmapFactory.Options newOpts = new BitmapFactory.Options();
    //开始读入图片,此时把options.inJustDecodeBounds 设回true了
    newOpts.inJustDecodeBounds = true;
    Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);
    newOpts.inJustDecodeBounds = false;
    int w = newOpts.outWidth;
    int h = newOpts.outHeight;
    //现在主流手机比较多是800*480分辨率,所以高和宽我们设置为
    float hh = 800f;//这里设置高度为800f
    float ww = 480f;//这里设置宽度为480f
    //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
    int be = 1;//be=1表示不缩放
    if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放
        be = (int) (newOpts.outWidth / ww);
    } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放
        be = (int) (newOpts.outHeight / hh);
    }
    if (be <= 0)
        be = 1;
    newOpts.inSampleSize = be;//设置缩放比例
    //newOpts.inPreferredConfig = Bitmap.Config.RGB_565;//降低图片从ARGB888到RGB565
    //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
    isBm = new ByteArrayInputStream(baos.toByteArray());
    bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);
    // return compressImage(bitmap);//压缩好比例大小后再进行质量压缩
    return  bitmap;
}

4.签名保存图片

mSaveButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Bitmap signatureBitmap = mSignaturePad.getSignatureBitmap();
        Bitmap bitmap = compressScale(signatureBitmap);
        if (addSignatureToGallery(bitmap)) {
            Toast.makeText(MainActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(MainActivity.this, "保存失败", Toast.LENGTH_SHORT).show();
        }
    }
});
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 {
        final File photo = new File(getAlbumStorageDir("draw"), String.format(creatTime+".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;
}

总结

到这里差不多就已经完成,具体还有一些xml之类的,看demo吧

DEMO

https://download.csdn.net/download/wen_haha/10460115

GitHub

https://github.com/kongkongdaren/DrawDemo

猜你喜欢

转载自blog.csdn.net/wen_haha/article/details/80588414