├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wen │ │ └── asyl │ │ └── drawdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── wen │ │ │ └── asyl │ │ │ ├── drawdemo │ │ │ └── MainActivity.java │ │ │ ├── utils │ │ │ ├── Bezier.java │ │ │ ├── ControlTimedPoints.java │ │ │ └── TimedPoint.java │ │ │ └── views │ │ │ └── SignatureView.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── wen │ └── asyl │ └── drawdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 1.8 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "27.0.3" 6 | defaultConfig { 7 | applicationId "com.wen.asyl.drawdemo" 8 | minSdkVersion 15 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:26.+' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\ASsdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/wen/asyl/drawdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.drawdemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.wen.asyl.drawdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/wen/asyl/drawdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.drawdemo; 2 | 3 | import android.Manifest; 4 | import android.content.Intent; 5 | import android.content.pm.ActivityInfo; 6 | import android.content.pm.PackageManager; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | import android.os.Environment; 14 | import android.support.annotation.NonNull; 15 | import android.support.v4.app.ActivityCompat; 16 | import android.support.v4.content.ContextCompat; 17 | import android.support.v7.app.AppCompatActivity; 18 | import android.os.Bundle; 19 | import android.util.Log; 20 | import android.view.View; 21 | import android.widget.Button; 22 | import android.widget.Toast; 23 | 24 | import com.wen.asyl.views.SignatureView; 25 | 26 | import java.io.ByteArrayInputStream; 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.File; 29 | import java.io.FileOutputStream; 30 | import java.io.IOException; 31 | import java.io.OutputStream; 32 | import java.text.SimpleDateFormat; 33 | import java.util.Date; 34 | 35 | public class MainActivity extends AppCompatActivity { 36 | 37 | private String creatTime; 38 | 39 | @Override 40 | protected void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.activity_main); 43 | gainPermission(); 44 | gainCurrenTime(); 45 | initView(); 46 | } 47 | 48 | private void gainPermission() { 49 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { 50 | if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 51 | ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4); 52 | } else { 53 | Toast.makeText(this, "已开启权限", Toast.LENGTH_SHORT).show(); 54 | } 55 | } 56 | } 57 | 58 | private void gainCurrenTime() { 59 | SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddhhmmss"); 60 | Date curDate = new Date(System.currentTimeMillis());//获取当前时间 61 | creatTime = formatter.format(curDate); 62 | } 63 | 64 | private void initView() { 65 | final Button mClearButton=(Button) findViewById(R.id.clear_button); 66 | final Button mSaveButton=(Button) findViewById(R.id.save_button); 67 | final SignatureView mSignaturePad=(SignatureView) findViewById(R.id.signature_pad); 68 | mSignaturePad.setOnSignedListener(new SignatureView.OnSignedListener() { 69 | @Override 70 | public void onSigned() { 71 | mSaveButton.setEnabled(true); 72 | mClearButton.setEnabled(true); 73 | } 74 | @Override 75 | public void onClear() { 76 | mSaveButton.setEnabled(false); 77 | mClearButton.setEnabled(false); 78 | } 79 | }); 80 | 81 | mClearButton.setOnClickListener(new View.OnClickListener() { 82 | @Override 83 | public void onClick(View view) { 84 | mSignaturePad.clear(); 85 | } 86 | }); 87 | mSaveButton.setOnClickListener(new View.OnClickListener() { 88 | @Override 89 | public void onClick(View view) { 90 | Bitmap signatureBitmap = mSignaturePad.getSignatureBitmap(); 91 | Bitmap bitmap = compressScale(signatureBitmap); 92 | if (addSignatureToGallery(bitmap)) { 93 | Toast.makeText(MainActivity.this, "保存成功", Toast.LENGTH_SHORT).show(); 94 | } else { 95 | Toast.makeText(MainActivity.this, "保存失败", Toast.LENGTH_SHORT).show(); 96 | } 97 | } 98 | }); 99 | } 100 | public File getAlbumStorageDir(String albumName) { 101 | File file = new File(Environment.getExternalStorageDirectory(), albumName); 102 | if (!file.mkdirs()) { 103 | Log.e("SignaturePad", "Directory not created"); 104 | } 105 | return file; 106 | } 107 | public void saveBitmapToJPG(Bitmap bitmap, File photo) throws IOException { 108 | Bitmap newBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); 109 | Canvas canvas = new Canvas(newBitmap); 110 | canvas.drawColor(Color.WHITE); 111 | canvas.drawBitmap(bitmap, 0, 0, null); 112 | OutputStream stream = new FileOutputStream(photo); 113 | newBitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream); 114 | stream.close(); 115 | } 116 | public boolean addSignatureToGallery(Bitmap signature) { 117 | boolean result = false; 118 | try { 119 | final File photo = new File(getAlbumStorageDir("draw"), String.format(creatTime+".jpg", System.currentTimeMillis())); 120 | saveBitmapToJPG(signature,photo); 121 | Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 122 | Uri contentUri = Uri.fromFile(photo); 123 | mediaScanIntent.setData(contentUri); 124 | MainActivity.this.sendBroadcast(mediaScanIntent); 125 | result = true; 126 | } catch (IOException e) { 127 | e.printStackTrace(); 128 | } 129 | return result; 130 | } 131 | /** 132 | *   133 | *  * 图片按比例大小压缩方法  134 | *  *  135 | *  * @param image (根据Bitmap图片压缩)  136 | *  * @return  137 | *   138 | */ 139 | public static Bitmap compressScale(Bitmap image) { 140 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 141 | image.compress(Bitmap.CompressFormat.JPEG, 100, baos); 142 | if( baos.toByteArray().length / 1024>1024) {//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出 143 | baos.reset();//重置baos即清空baos 144 | image.compress(Bitmap.CompressFormat.JPEG, 50, baos);//这里压缩50%,把压缩后的数据存放到baos中 145 | } 146 | ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray()); 147 | BitmapFactory.Options newOpts = new BitmapFactory.Options(); 148 | //开始读入图片,此时把options.inJustDecodeBounds 设回true了 149 | newOpts.inJustDecodeBounds = true; 150 | Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, newOpts); 151 | newOpts.inJustDecodeBounds = false; 152 | int w = newOpts.outWidth; 153 | int h = newOpts.outHeight; 154 | //现在主流手机比较多是800*480分辨率,所以高和宽我们设置为 155 | float hh = 800f;//这里设置高度为800f 156 | float ww = 480f;//这里设置宽度为480f 157 | //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可 158 | int be = 1;//be=1表示不缩放 159 | if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放 160 | be = (int) (newOpts.outWidth / ww); 161 | } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放 162 | be = (int) (newOpts.outHeight / hh); 163 | } 164 | if (be <= 0) 165 | be = 1; 166 | newOpts.inSampleSize = be;//设置缩放比例 167 | //newOpts.inPreferredConfig = Bitmap.Config.RGB_565;//降低图片从ARGB888到RGB565 168 | //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了 169 | isBm = new ByteArrayInputStream(baos.toByteArray()); 170 | bitmap = BitmapFactory.decodeStream(isBm, null, newOpts); 171 | // return compressImage(bitmap);//压缩好比例大小后再进行质量压缩 172 | return bitmap; 173 | } 174 | @Override 175 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 176 | switch (requestCode){ 177 | case 4: 178 | if(grantResults.length>0 &&grantResults[0] == PackageManager.PERMISSION_GRANTED){ 179 | Toast.makeText(this, "已打开权限!", Toast.LENGTH_SHORT).show(); 180 | }else { 181 | Toast.makeText(this, "请打开权限!", Toast.LENGTH_SHORT).show(); 182 | } 183 | break; 184 | default: 185 | } 186 | } 187 | @Override 188 | protected void onResume() { 189 | // TODO Auto-generated method stub 190 | /** 191 | * 设置为横屏 192 | */ 193 | if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { 194 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 195 | } 196 | super.onResume(); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/src/main/java/com/wen/asyl/utils/Bezier.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.utils; 2 | 3 | public class Bezier { 4 | 5 | public TimedPoint startPoint; 6 | public TimedPoint control1; 7 | public TimedPoint control2; 8 | public TimedPoint endPoint; 9 | 10 | public Bezier(TimedPoint startPoint, TimedPoint control1, TimedPoint control2, TimedPoint endPoint) { 11 | this.startPoint = startPoint; 12 | this.control1 = control1; 13 | this.control2 = control2; 14 | this.endPoint = endPoint; 15 | } 16 | 17 | public float length() { 18 | int steps = 10, length = 0; 19 | int i; 20 | float t; 21 | double cx, cy, px = 0, py = 0, xdiff, ydiff; 22 | 23 | for (i = 0; i <= steps; i++) { 24 | t = i / steps; 25 | cx = point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x); 26 | cy = point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y); 27 | if (i > 0) { 28 | xdiff = cx - px; 29 | ydiff = cy - py; 30 | length += Math.sqrt(xdiff * xdiff + ydiff * ydiff); 31 | } 32 | px = cx; 33 | py = cy; 34 | } 35 | return length; 36 | 37 | } 38 | 39 | public double point(float t, float start, float c1, float c2, float end) { 40 | 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) 41 | * t * t + end * t * t * t; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/wen/asyl/utils/ControlTimedPoints.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.utils; 2 | 3 | /** 4 | * Created by gcacace on 28/02/14. 5 | */ 6 | public class ControlTimedPoints { 7 | 8 | public TimedPoint c1; 9 | public TimedPoint c2; 10 | 11 | public ControlTimedPoints(TimedPoint c1, TimedPoint c2) { 12 | this.c1 = c1; 13 | this.c2 = c2; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/wen/asyl/utils/TimedPoint.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.utils; 2 | 3 | public class TimedPoint { 4 | public final float x; 5 | public final float y; 6 | public final long timestamp; 7 | 8 | public TimedPoint(float x, float y) { 9 | this.x = x; 10 | this.y = y; 11 | this.timestamp = System.currentTimeMillis(); 12 | } 13 | 14 | public float velocityFrom(TimedPoint start) { 15 | float velocity = distanceTo(start) / (this.timestamp - start.timestamp); 16 | if (velocity != velocity) 17 | return 0f; 18 | return velocity; 19 | } 20 | 21 | public float distanceTo(TimedPoint point) { 22 | return (float) Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2)); 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wen/asyl/views/SignatureView.java: -------------------------------------------------------------------------------- 1 | package com.wen.asyl.views; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Matrix; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.RectF; 13 | import android.util.AttributeSet; 14 | import android.util.DisplayMetrics; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | 18 | import com.wen.asyl.drawdemo.R; 19 | import com.wen.asyl.utils.Bezier; 20 | import com.wen.asyl.utils.ControlTimedPoints; 21 | import com.wen.asyl.utils.TimedPoint; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | 27 | public class SignatureView extends View { 28 | // View state 29 | private List mPoints; 30 | private boolean mIsEmpty; 31 | private float mLastTouchX; 32 | private float mLastTouchY; 33 | private float mLastVelocity; 34 | private float mLastWidth; 35 | private RectF mDirtyRect; 36 | 37 | // Configurable parameters 38 | private int mMinWidth; 39 | private int mMaxWidth; 40 | private float mVelocityFilterWeight; 41 | private OnSignedListener mOnSignedListener; 42 | 43 | private Paint mPaint = new Paint(); 44 | private Path mPath = new Path(); 45 | private Bitmap mSignatureBitmap = null; 46 | private Canvas mSignatureBitmapCanvas = null; 47 | 48 | public SignatureView(Context context, AttributeSet attrs) { 49 | super(context, attrs); 50 | 51 | TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SignatureView, 0, 0); 52 | 53 | // Configurable parameters 54 | try { 55 | mMinWidth = a.getDimensionPixelSize(R.styleable.SignatureView_minWidth, convertDpToPx(3)); 56 | mMaxWidth = a.getDimensionPixelSize(R.styleable.SignatureView_maxWidth, convertDpToPx(20)); 57 | mVelocityFilterWeight = a.getFloat(R.styleable.SignatureView_velocityFilterWeight, 0.9f); 58 | mPaint.setColor(a.getColor(R.styleable.SignatureView_penColor, Color.BLACK)); 59 | } finally { 60 | a.recycle(); 61 | } 62 | 63 | // Fixed parameters 64 | mPaint.setAntiAlias(true); 65 | 66 | 67 | 68 | mPaint.setStyle(Paint.Style.STROKE); 69 | mPaint.setStrokeCap(Paint.Cap.ROUND); 70 | mPaint.setStrokeJoin(Paint.Join.ROUND); 71 | //mPaint.setStyle(Paint.Style.FILL); //画笔风格 72 | // mPaint.setAntiAlias(true); //抗锯齿 73 | // mPaint.setStrokeWidth(10); //画笔粗细 74 | // mPaint.setTextSize(100); //绘制文字大小,单位px 75 | //mPaint.setStrokeWidth(Paint.S); 76 | // Dirty rectangle to update only the changed portion of the view 77 | mDirtyRect = new RectF(); 78 | 79 | clear(); 80 | } 81 | 82 | /** 83 | * Set the pen color from a given resource. If the resource is not found, 84 | * {@link Color#BLACK} is assumed. 85 | * 86 | * @param colorRes 87 | * the color resource. 88 | */ 89 | public void setPenColorRes(int colorRes) { 90 | try { 91 | setPenColor(getResources().getColor(colorRes)); 92 | } catch (Resources.NotFoundException ex) { 93 | setPenColor(getResources().getColor(Color.BLACK)); 94 | } 95 | } 96 | 97 | /** 98 | * Set the pen color from a given color. 99 | * 100 | * @param color 101 | * the color. 102 | */ 103 | public void setPenColor(int color) { 104 | mPaint.setColor(color); 105 | } 106 | 107 | /** 108 | * Set the minimum width of the stroke in pixel. 109 | * 110 | * @param minWidth 111 | * the width in dp. 112 | */ 113 | public void setMinWidth(float minWidth) { 114 | mMinWidth = convertDpToPx(minWidth); 115 | } 116 | 117 | /** 118 | * Set the maximum width of the stroke in pixel. 119 | * 120 | * @param maxWidth 121 | * the width in dp. 122 | */ 123 | public void setMaxWidth(float maxWidth) { 124 | mMaxWidth = convertDpToPx(maxWidth); 125 | } 126 | 127 | /** 128 | * Set the velocity filter weight. 129 | * 130 | * @param velocityFilterWeight 131 | * the weight. 132 | */ 133 | public void setVelocityFilterWeight(float velocityFilterWeight) { 134 | mVelocityFilterWeight = velocityFilterWeight; 135 | } 136 | 137 | public void clear() { 138 | mPoints = new ArrayList(); 139 | mLastVelocity = 0; 140 | mLastWidth = (mMinWidth + mMaxWidth) / 2; 141 | mPath.reset(); 142 | 143 | if (mSignatureBitmap != null) { 144 | mSignatureBitmap = null; 145 | ensureSignatureBitmap(); 146 | } 147 | 148 | setIsEmpty(true); 149 | 150 | invalidate(); 151 | } 152 | 153 | @Override 154 | public boolean onTouchEvent(MotionEvent event) { 155 | if (!isEnabled()) 156 | return false; 157 | 158 | float eventX = event.getX(); 159 | float eventY = event.getY(); 160 | 161 | switch (event.getAction()) { 162 | case MotionEvent.ACTION_DOWN: 163 | getParent().requestDisallowInterceptTouchEvent(true); 164 | mPoints.clear(); 165 | mPath.moveTo(eventX, eventY); 166 | mLastTouchX = eventX; 167 | mLastTouchY = eventY; 168 | addPoint(new TimedPoint(eventX, eventY)); 169 | 170 | case MotionEvent.ACTION_MOVE: 171 | resetDirtyRect(eventX, eventY); 172 | addPoint(new TimedPoint(eventX, eventY)); 173 | break; 174 | 175 | case MotionEvent.ACTION_UP: 176 | resetDirtyRect(eventX, eventY); 177 | addPoint(new TimedPoint(eventX, eventY)); 178 | getParent().requestDisallowInterceptTouchEvent(true); 179 | setIsEmpty(false); 180 | break; 181 | 182 | default: 183 | return false; 184 | } 185 | 186 | // invalidate(); 187 | invalidate((int) (mDirtyRect.left - mMaxWidth), (int) (mDirtyRect.top - mMaxWidth), 188 | (int) (mDirtyRect.right + mMaxWidth), (int) (mDirtyRect.bottom + mMaxWidth)); 189 | 190 | return true; 191 | } 192 | 193 | @Override 194 | protected void onDraw(Canvas canvas) { 195 | if (mSignatureBitmap != null) { 196 | canvas.drawBitmap(mSignatureBitmap, 0, 0, mPaint); 197 | } 198 | } 199 | 200 | public void setOnSignedListener(OnSignedListener listener) { 201 | mOnSignedListener = listener; 202 | } 203 | 204 | public boolean isEmpty() { 205 | return mIsEmpty; 206 | } 207 | 208 | public Bitmap getSignatureBitmap() { 209 | Bitmap originalBitmap = getTransparentSignatureBitmap(); 210 | Bitmap whiteBgBitmap = Bitmap.createBitmap(originalBitmap.getWidth(), originalBitmap.getHeight(), 211 | Bitmap.Config.ARGB_8888); 212 | Canvas canvas = new Canvas(whiteBgBitmap); 213 | canvas.drawColor(Color.WHITE); 214 | canvas.drawBitmap(originalBitmap, 0, 0, null); 215 | return whiteBgBitmap; 216 | } 217 | 218 | public void setSignatureBitmap(Bitmap signature) { 219 | clear(); 220 | ensureSignatureBitmap(); 221 | 222 | RectF tempSrc = new RectF(); 223 | RectF tempDst = new RectF(); 224 | 225 | int dWidth = signature.getWidth(); 226 | int dHeight = signature.getHeight(); 227 | int vWidth = getWidth(); 228 | int vHeight = getHeight(); 229 | 230 | // Generate the required transform. 231 | tempSrc.set(0, 0, dWidth, dHeight); 232 | tempDst.set(0, 0, vWidth, vHeight); 233 | 234 | Matrix drawMatrix = new Matrix(); 235 | drawMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER); 236 | 237 | Canvas canvas = new Canvas(mSignatureBitmap); 238 | canvas.drawBitmap(signature, drawMatrix, null); 239 | setIsEmpty(false); 240 | invalidate(); 241 | } 242 | 243 | public Bitmap getTransparentSignatureBitmap() { 244 | ensureSignatureBitmap(); 245 | return mSignatureBitmap; 246 | } 247 | 248 | public Bitmap getTransparentSignatureBitmap(boolean trimBlankSpace) { 249 | 250 | if (!trimBlankSpace) { 251 | return getTransparentSignatureBitmap(); 252 | } 253 | 254 | ensureSignatureBitmap(); 255 | 256 | int imgHeight = mSignatureBitmap.getHeight(); 257 | int imgWidth = mSignatureBitmap.getWidth(); 258 | 259 | int backgroundColor = Color.TRANSPARENT; 260 | 261 | int xMin = Integer.MAX_VALUE, xMax = Integer.MIN_VALUE, yMin = Integer.MAX_VALUE, yMax = Integer.MIN_VALUE; 262 | 263 | boolean foundPixel = false; 264 | 265 | // Find xMin 266 | for (int x = 0; x < imgWidth; x++) { 267 | boolean stop = false; 268 | for (int y = 0; y < imgHeight; y++) { 269 | if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { 270 | xMin = x; 271 | stop = true; 272 | foundPixel = true; 273 | break; 274 | } 275 | } 276 | if (stop) 277 | break; 278 | } 279 | 280 | // Image is empty... 281 | if (!foundPixel) 282 | return null; 283 | 284 | // Find yMin 285 | for (int y = 0; y < imgHeight; y++) { 286 | boolean stop = false; 287 | for (int x = xMin; x < imgWidth; x++) { 288 | if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { 289 | yMin = y; 290 | stop = true; 291 | break; 292 | } 293 | } 294 | if (stop) 295 | break; 296 | } 297 | 298 | // Find xMax 299 | for (int x = imgWidth - 1; x >= xMin; x--) { 300 | boolean stop = false; 301 | for (int y = yMin; y < imgHeight; y++) { 302 | if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { 303 | xMax = x; 304 | stop = true; 305 | break; 306 | } 307 | } 308 | if (stop) 309 | break; 310 | } 311 | 312 | // Find yMax 313 | for (int y = imgHeight - 1; y >= yMin; y--) { 314 | boolean stop = false; 315 | for (int x = xMin; x <= xMax; x++) { 316 | if (mSignatureBitmap.getPixel(x, y) != backgroundColor) { 317 | yMax = y; 318 | stop = true; 319 | break; 320 | } 321 | } 322 | if (stop) 323 | break; 324 | } 325 | 326 | return Bitmap.createBitmap(mSignatureBitmap, xMin, yMin, xMax - xMin, yMax - yMin); 327 | } 328 | 329 | private void addPoint(TimedPoint newPoint) { 330 | mPoints.add(newPoint); 331 | if (mPoints.size() > 2) { 332 | // To reduce the initial lag make it work with 3 mPoints 333 | // by copying the first point to the beginning. 334 | if (mPoints.size() == 3) 335 | mPoints.add(0, mPoints.get(0)); 336 | 337 | ControlTimedPoints tmp = calculateCurveControlPoints(mPoints.get(0), mPoints.get(1), mPoints.get(2)); 338 | TimedPoint c2 = tmp.c2; 339 | tmp = calculateCurveControlPoints(mPoints.get(1), mPoints.get(2), mPoints.get(3)); 340 | TimedPoint c3 = tmp.c1; 341 | Bezier curve = new Bezier(mPoints.get(1), c2, c3, mPoints.get(2)); 342 | 343 | TimedPoint startPoint = curve.startPoint; 344 | TimedPoint endPoint = curve.endPoint; 345 | 346 | float velocity = endPoint.velocityFrom(startPoint); 347 | velocity = Float.isNaN(velocity) ? 0.0f : velocity; 348 | 349 | velocity = mVelocityFilterWeight * velocity + (1 - mVelocityFilterWeight) * mLastVelocity; 350 | 351 | // The new width is a function of the velocity. Higher velocities 352 | // correspond to thinner strokes. 353 | float newWidth = strokeWidth(velocity); 354 | 355 | // The Bezier's width starts out as last curve's final width, and 356 | // gradually changes to the stroke width just calculated. The new 357 | // width calculation is based on the velocity between the Bezier's 358 | // start and end mPoints. 359 | addBezier(curve, mLastWidth, newWidth); 360 | 361 | mLastVelocity = velocity; 362 | mLastWidth = newWidth; 363 | 364 | // Remove the first element from the list, 365 | // so that we always have no more than 4 mPoints in mPoints array. 366 | mPoints.remove(0); 367 | } 368 | } 369 | 370 | private void addBezier(Bezier curve, float startWidth, float endWidth) { 371 | ensureSignatureBitmap(); 372 | float originalWidth = mPaint.getStrokeWidth(); 373 | float widthDelta = endWidth - startWidth; 374 | float drawSteps = (float) Math.floor(curve.length()); 375 | 376 | for (int i = 0; i < drawSteps; i++) { 377 | // Calculate the Bezier (x, y) coordinate for this step. 378 | float t = ((float) i) / drawSteps; 379 | float tt = t * t; 380 | float ttt = tt * t; 381 | float u = 1 - t; 382 | float uu = u * u; 383 | float uuu = uu * u; 384 | 385 | float x = uuu * curve.startPoint.x; 386 | x += 3 * uu * t * curve.control1.x; 387 | x += 3 * u * tt * curve.control2.x; 388 | x += ttt * curve.endPoint.x; 389 | 390 | float y = uuu * curve.startPoint.y; 391 | y += 3 * uu * t * curve.control1.y; 392 | y += 3 * u * tt * curve.control2.y; 393 | y += ttt * curve.endPoint.y; 394 | 395 | // Set the incremental stroke width and draw. 396 | mPaint.setStrokeWidth(startWidth + ttt * widthDelta); 397 | mSignatureBitmapCanvas.drawPoint(x, y, mPaint); 398 | expandDirtyRect(x, y); 399 | } 400 | 401 | mPaint.setStrokeWidth(originalWidth); 402 | } 403 | 404 | private ControlTimedPoints calculateCurveControlPoints(TimedPoint s1, TimedPoint s2, TimedPoint s3) { 405 | float dx1 = s1.x - s2.x; 406 | float dy1 = s1.y - s2.y; 407 | float dx2 = s2.x - s3.x; 408 | float dy2 = s2.y - s3.y; 409 | 410 | TimedPoint m1 = new TimedPoint((s1.x + s2.x) / 2.0f, (s1.y + s2.y) / 2.0f); 411 | TimedPoint m2 = new TimedPoint((s2.x + s3.x) / 2.0f, (s2.y + s3.y) / 2.0f); 412 | 413 | float l1 = (float) Math.sqrt(dx1 * dx1 + dy1 * dy1); 414 | float l2 = (float) Math.sqrt(dx2 * dx2 + dy2 * dy2); 415 | 416 | float dxm = (m1.x - m2.x); 417 | float dym = (m1.y - m2.y); 418 | float k = l2 / (l1 + l2); 419 | TimedPoint cm = new TimedPoint(m2.x + dxm * k, m2.y + dym * k); 420 | 421 | float tx = s2.x - cm.x; 422 | float ty = s2.y - cm.y; 423 | 424 | return new ControlTimedPoints(new TimedPoint(m1.x + tx, m1.y + ty), new TimedPoint(m2.x + tx, m2.y + ty)); 425 | } 426 | 427 | private float strokeWidth(float velocity) { 428 | return Math.max(mMaxWidth / (velocity + 1), mMinWidth); 429 | } 430 | 431 | /** 432 | * Called when replaying history to ensure the dirty region includes all 433 | * mPoints. 434 | * 435 | * @param historicalX 436 | * the previous x coordinate. 437 | * @param historicalY 438 | * the previous y coordinate. 439 | */ 440 | private void expandDirtyRect(float historicalX, float historicalY) { 441 | if (historicalX < mDirtyRect.left) { 442 | mDirtyRect.left = historicalX; 443 | } else if (historicalX > mDirtyRect.right) { 444 | mDirtyRect.right = historicalX; 445 | } 446 | if (historicalY < mDirtyRect.top) { 447 | mDirtyRect.top = historicalY; 448 | } else if (historicalY > mDirtyRect.bottom) { 449 | mDirtyRect.bottom = historicalY; 450 | } 451 | } 452 | 453 | /** 454 | * Resets the dirty region when the motion event occurs. 455 | * 456 | * @param eventX 457 | * the event x coordinate. 458 | * @param eventY 459 | * the event y coordinate. 460 | */ 461 | private void resetDirtyRect(float eventX, float eventY) { 462 | 463 | // The mLastTouchX and mLastTouchY were set when the ACTION_DOWN motion 464 | // event occurred. 465 | mDirtyRect.left = Math.min(mLastTouchX, eventX); 466 | mDirtyRect.right = Math.max(mLastTouchX, eventX); 467 | mDirtyRect.top = Math.min(mLastTouchY, eventY); 468 | mDirtyRect.bottom = Math.max(mLastTouchY, eventY); 469 | } 470 | 471 | private void setIsEmpty(boolean newValue) { 472 | mIsEmpty = newValue; 473 | if (mOnSignedListener != null) { 474 | if (mIsEmpty) { 475 | mOnSignedListener.onClear(); 476 | } else { 477 | mOnSignedListener.onSigned(); 478 | } 479 | } 480 | } 481 | 482 | private void ensureSignatureBitmap() { 483 | if (mSignatureBitmap == null) { 484 | mSignatureBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 485 | mSignatureBitmapCanvas = new Canvas(mSignatureBitmap); 486 | } 487 | } 488 | 489 | private int convertDpToPx(float dp) { 490 | return Math.round(dp * (getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT)); 491 | } 492 | 493 | public interface OnSignedListener { 494 | public void onSigned(); 495 | 496 | public void onClear(); 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 |