├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── developerhaoz │ │ └── doodleview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── developerhaoz │ │ │ └── doodleview │ │ │ ├── BaseAction.java │ │ │ ├── DoodleView.java │ │ │ └── DoodleViewActivity.java │ └── res │ │ ├── layout │ │ └── activity_doodleview.xml │ │ ├── menu │ │ └── menu_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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── developerhaoz │ └── doodleview │ └── 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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | Android API 23 Platform 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | > 最近项目中需要用到涂鸦的功能,在 Github 上搜了一圈也没找到适合的库,索性就自己撸一个出来,正好复习一下自定义 View 的知识。写完之后怎么可以自己藏着呢,当然得写篇博客分享给大家。 3 | 4 | 在开始本文的内容之前,先展示一波最终的效果 5 | 6 | ![DoodleView](http://upload-images.jianshu.io/upload_images/4334738-43c36fbde1f6b882.gif?imageMogr2/auto-orient/strip) 7 | 8 | 可以看到这个这个自定义 View 的功能还是很丰富的,无论是设置画笔的形状、颜色、粗细,还是进行重置和保存,该有的 API,基本都已经实现了。有需要的读者直接 [点击这里](https://github.com/developerHaoz/DoodleView) ,希望帮忙点个 star,哈哈哈。 9 | 10 | ## 一、定义画笔的行为类 11 | ----- 12 | 这里所说的「行为」指的就是我们刚才看到的画笔的形状,无论是路径、直线、还是圆形,这些东西说到底都是画笔的行为。 13 | 14 | 所以我们先定义一个公共的父类,以便进行管理,减少代码量。 15 | ``` 16 | abstract class Action { 17 | public int color; 18 | 19 | Action() { 20 | color = Color.BLACK; 21 | } 22 | 23 | Action(int color) { 24 | this.color = color; 25 | } 26 | 27 | public abstract void draw(Canvas canvas); 28 | 29 | public abstract void move(float mx, float my); 30 | } 31 | ``` 32 | 33 | 可以看到这个类被定义成抽象类,里面有 draw() 和 move() 两个抽象方法,这两个方法就是留给子类进行继承和拓展的,子类只要实现这两个方法,确定好他们各自的行为,就能让画笔显示出各种各样的效果。 34 | 35 | 接下来举几个具体的子类来说明一下用法: 36 | ``` 37 | // 自由曲线 38 | class MyPath extends Action { 39 | private Path path; 40 | private int size; 41 | 42 | MyPath() { 43 | path = new Path(); 44 | size = 1; 45 | } 46 | 47 | MyPath(float x, float y, int size, int color) { 48 | super(color); 49 | path = new Path(); 50 | this.size = size; 51 | path.moveTo(x, y); 52 | path.lineTo(x, y); 53 | } 54 | 55 | public void draw(Canvas canvas) { 56 | Paint paint = new Paint(); 57 | paint.setAntiAlias(true); 58 | paint.setDither(true); 59 | paint.setColor(color); // 设置画笔颜色 60 | paint.setStrokeWidth(size); // 设置画笔粗细 61 | paint.setStyle(Paint.Style.STROKE); 62 | canvas.drawPath(path, paint); 63 | } 64 | 65 | public void move(float mx, float my) { 66 | path.lineTo(mx, my); 67 | } 68 | } 69 | 70 | // 直线 71 | class MyLine extends Action { 72 | private float startX; 73 | private float startY; 74 | private float stopX; 75 | private float stopY; 76 | private int size; 77 | 78 | MyLine() { 79 | startX = 0; 80 | startY = 0; 81 | stopX = 0; 82 | stopY = 0; 83 | } 84 | 85 | MyLine(float x, float y, int size, int color) { 86 | super(color); 87 | startX = x; 88 | startY = y; 89 | stopX = x; 90 | stopY = y; 91 | this.size = size; 92 | } 93 | 94 | public void draw(Canvas canvas) { 95 | Paint paint = new Paint(); 96 | paint.setAntiAlias(true); 97 | paint.setStyle(Paint.Style.STROKE); 98 | paint.setColor(color); 99 | paint.setStrokeWidth(size); 100 | canvas.drawLine(startX, startY, stopX, stopY, paint); 101 | } 102 | 103 | public void move(float mx, float my) { 104 | stopX = mx; 105 | stopY = my; 106 | } 107 | } 108 | ``` 109 | 110 | 就拿最常见的自由曲线来作为例子讲一下。我们定义 MyPath 这个类,继承自 BaseAction,然后添加了 Path 和 size 两个成员变量。其中的 size 是用来设置画笔的粗细。Path 是用来确定自由曲线的轨迹。 111 | 112 | 在 MyPath 的 draw() 方法中我们创建了一个 Paint 用于图形的描绘。最后将 path 和 paint 传给 canvas,实现图形的最终绘制。 113 | 114 | ``` 115 | public void draw(Canvas canvas) { 116 | Paint paint = new Paint(); 117 | paint.setAntiAlias(true); 118 | paint.setDither(true); 119 | paint.setColor(color); // 设置画笔颜色 120 | paint.setStrokeWidth(size); // 设置画笔粗细 121 | paint.setStyle(Paint.Style.STROKE); 122 | canvas.drawPath(path, paint); 123 | } 124 | ``` 125 | 126 | 其他子类都是按照这种思路来实现,具体的实现可以参考下 Github 上的源码 [DoodleView](https://github.com/developerHaoz/DoodleView)。 127 | 128 | ## 二、实现自定义的 DoodleView 129 | ----- 130 | 这个 DoodleView 是直接继承 SurfaceView 的。本来想继承 View 来写,后来仔细想了下最后还是用 SurfaceView 来进行实现。 131 | 132 | 这里简单说一下 View 和 SurfaceView 的区别。 133 | - View 在主线程中对页面进行刷新,而 SurfaceView 则是另外开了一个子线程对当前页面进行刷新。 134 | 135 | - View 适合用于主动更新的情况,而 SurfaceView 则适用于被动更新的情况,比如频繁刷新界面。 136 | 137 | 因为我们这个涂鸦的 View,是频繁进行刷新的,每次触摸屏幕都会进行相应的界面刷新,所以用 SurfaceView 来实现就比较合理了。 138 | 139 | 这里我直接结合代码来讲一下 DoodleView 的实现思路,因为我是继承自 SurfaceView 来写的,对于 SurfaceView 不是很了解的朋友,可以先看一下这篇文章 [Android中的SurfaceView详解](http://www.jianshu.com/p/b037249e6d31) 140 | 141 | ``` 142 | 143 | public class DoodleView extends SurfaceView implements SurfaceHolder.Callback { 144 | 145 | private SurfaceHolder mSurfaceHolder = null; 146 | 147 | // 当前所选画笔的形状 148 | private BaseAction curAction = null; 149 | // 默认画笔为黑色 150 | private int currentColor = Color.BLACK; 151 | // 画笔的粗细 152 | private int currentSize = 5; 153 | 154 | private Paint mPaint; 155 | 156 | private List mBaseActions; 157 | 158 | private Bitmap mBitmap; 159 | 160 | private ActionType mActionType = ActionType.Path; 161 | 162 | public DoodleView(Context context, AttributeSet attrs, int defStyle) { 163 | super(context, attrs, defStyle); 164 | init(); 165 | } 166 | 167 | private void init() { 168 | mSurfaceHolder = this.getHolder(); 169 | mSurfaceHolder.addCallback(this); 170 | this.setFocusable(true); 171 | 172 | mPaint = new Paint(); 173 | mPaint.setColor(Color.WHITE); 174 | mPaint.setStrokeWidth(currentSize); 175 | } 176 | 177 | @Override 178 | public void surfaceCreated(SurfaceHolder holder) { 179 | Canvas canvas = mSurfaceHolder.lockCanvas(); 180 | canvas.drawColor(Color.WHITE); 181 | mSurfaceHolder.unlockCanvasAndPost(canvas); 182 | mBaseActions = new ArrayList<>(); 183 | } 184 | 185 | @Override 186 | public boolean onTouchEvent(MotionEvent event) { 187 | int action = event.getAction(); 188 | if (action == MotionEvent.ACTION_CANCEL) { 189 | return false; 190 | } 191 | 192 | float touchX = event.getRawX(); 193 | float touchY = event.getRawY(); 194 | 195 | switch (action) { 196 | case MotionEvent.ACTION_DOWN: 197 | setCurAction(touchX, touchY); 198 | break; 199 | case MotionEvent.ACTION_MOVE: 200 | Canvas canvas = mSurfaceHolder.lockCanvas(); 201 | canvas.drawColor(Color.WHITE); 202 | for (BaseAction baseAction : mBaseActions) { 203 | baseAction.draw(canvas); 204 | } 205 | curAction.move(touchX, touchY); 206 | curAction.draw(canvas); 207 | mSurfaceHolder.unlockCanvasAndPost(canvas); 208 | break; 209 | case MotionEvent.ACTION_UP: 210 | mBaseActions.add(curAction); 211 | curAction = null; 212 | break; 213 | 214 | default: 215 | break; 216 | } 217 | return super.onTouchEvent(event); 218 | } 219 | 220 | /** 221 | * 得到当前画笔的类型,并进行实例化 222 | * 223 | * @param x 224 | * @param y 225 | */ 226 | private void setCurAction(float x, float y) { 227 | switch (mActionType) { 228 | case Path: 229 | curAction = new MyPath(x, y, currentSize, currentColor); 230 | break; 231 | case Line: 232 | curAction = new MyLine(x, y, currentSize, currentColor); 233 | break; 234 | default: 235 | break; 236 | } 237 | } 238 | 239 | /** 240 | * 设置画笔的颜色 241 | * 242 | * @param color 颜色 243 | */ 244 | public void setColor(String color) { 245 | this.currentColor = Color.parseColor(color); 246 | } 247 | 248 | /** 249 | * 设置画笔的粗细 250 | * 251 | * @param size 画笔的粗细 252 | */ 253 | public void setSize(int size) { 254 | this.currentSize = size; 255 | } 256 | 257 | /** 258 | * 设置画笔的形状 259 | * 260 | * @param type 画笔的形状 261 | */ 262 | public void setType(ActionType type) { 263 | this.mActionType = type; 264 | } 265 | 266 | /** 267 | * 将当前的画布转换成一个 Bitmap 268 | * 269 | * @return Bitmap 270 | */ 271 | public Bitmap getBitmap() { 272 | mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 273 | Canvas canvas = new Canvas(mBitmap); 274 | doDraw(canvas); 275 | return mBitmap; 276 | } 277 | 278 | /** 279 | * 保存涂鸦后的图片 280 | * 281 | * @param doodleView 282 | * @return 图片的保存路径 283 | */ 284 | public String saveBitmap(DoodleView doodleView) { 285 | String path = Environment.getExternalStorageDirectory().getAbsolutePath() 286 | + "/doodleview/" + System.currentTimeMillis() + ".png"; 287 | if (!new File(path).exists()) { 288 | new File(path).getParentFile().mkdir(); 289 | } 290 | savePicByPNG(doodleView.getBitmap(), path); 291 | return path; 292 | } 293 | 294 | /** 295 | * 将一个 Bitmap 保存在一个指定的路径中 296 | * 297 | * @param bitmap 298 | * @param filePath 299 | */ 300 | public static void savePicByPNG(Bitmap bitmap, String filePath) { 301 | FileOutputStream fileOutputStream; 302 | try { 303 | fileOutputStream = new FileOutputStream(filePath); 304 | if (null != fileOutputStream) { 305 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); 306 | fileOutputStream.flush(); 307 | fileOutputStream.close(); 308 | } 309 | } catch (FileNotFoundException e) { 310 | e.printStackTrace(); 311 | } catch (IOException e) { 312 | e.printStackTrace(); 313 | } 314 | } 315 | 316 | /** 317 | * 开始进行绘画 318 | * 319 | * @param canvas 320 | */ 321 | private void doDraw(Canvas canvas) { 322 | canvas.drawColor(Color.TRANSPARENT); 323 | for (BaseAction action : mBaseActions) { 324 | action.draw(canvas); 325 | } 326 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 327 | } 328 | 329 | 330 | /** 331 | * 回退 332 | * 333 | * @return 是否已经回退成功 334 | */ 335 | public boolean back(){ 336 | if(mBaseActions != null && mBaseActions.size() > 0){ 337 | mBaseActions.remove(mBaseActions.size() -1); 338 | Canvas canvas = mSurfaceHolder.lockCanvas(); 339 | canvas.drawColor(Color.WHITE); 340 | for (BaseAction action : mBaseActions) { 341 | action.draw(canvas); 342 | } 343 | mSurfaceHolder.unlockCanvasAndPost(canvas); 344 | return true; 345 | } 346 | return false; 347 | } 348 | 349 | /** 350 | * 重置签名 351 | */ 352 | public void reset(){ 353 | if(mBaseActions != null && mBaseActions.size() > 0){ 354 | mBaseActions.clear(); 355 | Canvas canvas = mSurfaceHolder.lockCanvas(); 356 | canvas.drawColor(Color.WHITE); 357 | for (BaseAction action : mBaseActions) { 358 | action.draw(canvas); 359 | } 360 | mSurfaceHolder.unlockCanvasAndPost(canvas); 361 | } 362 | } 363 | 364 | enum ActionType { 365 | Path, Line 366 | } 367 | } 368 | ``` 369 | 370 | 可以看到,我们先定义了一个枚举类,用于区分各种画笔的形状,为了让代码看起来更简洁,我这里只放了 Path 和 Line 两种类型的,如果你还想实现其他类型的形状,直接加进去就行了。 371 | 372 | 在类的一开始我们定义了一些必要的成员变量,如画笔的颜色、形状、粗细,以及保存画笔行为的 List,以及需要用到的画笔 Paint 373 | 374 | 准备工作搞定了之后就开始进行核心代码的实现了。 375 | 376 | #### 1、构造函数 377 | ``` 378 | public DoodleView(Context context, AttributeSet attrs, int defStyle) { 379 | super(context, attrs, defStyle); 380 | init(); 381 | } 382 | 383 | private void init() { 384 | mSurfaceHolder = this.getHolder(); 385 | mSurfaceHolder.addCallback(this); 386 | this.setFocusable(true); 387 | 388 | mPaint = new Paint(); 389 | mPaint.setColor(Color.WHITE); 390 | mPaint.setStrokeWidth(currentSize); 391 | } 392 | ``` 393 | 可以看到我们在构造函数中先进行了 SurfaceHolder 的一些设置,以及对 Paint 进行了必要的设置。 394 | 395 | 然后在 surfaceCreated(SurfaceHolder holder) 方法中对 Canas 进行了创建和提交,以及初始化了 List 396 | 397 | #### 2、触摸事件的处理 398 | 这个方法的实现可以说是这个 DoodleView 的核心了 399 | 400 | ``` 401 | @Override 402 | public boolean onTouchEvent(MotionEvent event) { 403 | int action = event.getAction(); 404 | if (action == MotionEvent.ACTION_CANCEL) { 405 | return false; 406 | } 407 | 408 | float touchX = event.getRawX(); 409 | float touchY = event.getRawY(); 410 | 411 | switch (action) { 412 | case MotionEvent.ACTION_DOWN: 413 | setCurAction(touchX, touchY); 414 | break; 415 | case MotionEvent.ACTION_MOVE: 416 | Canvas canvas = mSurfaceHolder.lockCanvas(); 417 | canvas.drawColor(Color.WHITE); 418 | for (BaseAction baseAction : mBaseActions) { 419 | baseAction.draw(canvas); 420 | } 421 | curAction.move(touchX, touchY); 422 | curAction.draw(canvas); 423 | mSurfaceHolder.unlockCanvasAndPost(canvas); 424 | break; 425 | case MotionEvent.ACTION_UP: 426 | mBaseActions.add(curAction); 427 | curAction = null; 428 | break; 429 | 430 | default: 431 | break; 432 | } 433 | return super.onTouchEvent(event); 434 | } 435 | ``` 436 | 我们先拿到触摸的横坐标和纵坐标,然后根据手势来进行相应的处理 437 | - ACTION_DOWN:当刚开始出触摸屏幕的时候,先设置画笔的形状 438 | 439 | - ACTION_MOVE:手开始移动的时候,调用 move() 和 draw() 对 Canvas 进行绘制,最后将 Canvas 的内容进行提交。 440 | 441 | - ACTION_UP:将手抬起来的时候,将当前画笔的形状添加到 List 中,并将 curAction(当前的画笔形状)设为 null. 442 | 443 | #### 3、其他的 API 444 | 除了一些核心方法的实现,为了拓展这个 DoodleView 的功能,我还添加了一些实用的 API。 445 | 446 | ##### 保存涂鸦后的图片 447 | ``` 448 | public String saveBitmap(DoodleView doodleView) { 449 | String path = Environment.getExternalStorageDirectory().getAbsolutePath() 450 | + "/doodleview/" + System.currentTimeMillis() + ".png"; 451 | if (!new File(path).exists()) { 452 | new File(path).getParentFile().mkdir(); 453 | } 454 | savePicByPNG(doodleView.getBitmap(), path); 455 | return path; 456 | } 457 | 458 | public static void savePicByPNG(Bitmap bitmap, String filePath) { 459 | FileOutputStream fileOutputStream; 460 | try { 461 | fileOutputStream = new FileOutputStream(filePath); 462 | if (null != fileOutputStream) { 463 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); 464 | fileOutputStream.flush(); 465 | fileOutputStream.close(); 466 | } 467 | } catch (FileNotFoundException e) { 468 | e.printStackTrace(); 469 | } catch (IOException e) { 470 | e.printStackTrace(); 471 | } 472 | } 473 | ``` 474 | 先创建一个用于保存图片的路径,判断路径是否存在,如果不存在的话,就创建一下。否则通过这个路径拿到对应的文件流,并将当前图片转换成 Bitmap 之后放进去。 475 | 476 | ##### 重置涂鸦的界面 477 | 我们进行涂鸦,难免会出现手误,这时候进行重置就显得相当重要了。 478 | ``` 479 | public void reset(){ 480 | if(mBaseActions != null && mBaseActions.size() > 0){ 481 | mBaseActions.clear(); 482 | Canvas canvas = mSurfaceHolder.lockCanvas(); 483 | canvas.drawColor(Color.WHITE); 484 | for (BaseAction action : mBaseActions) { 485 | action.draw(canvas); 486 | } 487 | mSurfaceHolder.unlockCanvasAndPost(canvas); 488 | } 489 | } 490 | ``` 491 | 这里直接获取 Canvas,然后将 List 进行 clear,因为 List 里面没有内容,Canvas 上自然也就没有任何东西,最后将 Canvas 进行提交。 492 | 493 | 以上便是本文的全部内容,有兴趣的同学可以 [点击这里](https://github.com/developerHaoz/DoodleView) 看一下具体实现,麻烦点个 star,谢谢了。 494 | 495 | ----- 496 | ### 猜你喜欢 497 | - [Android 一起来看看知乎开源的图片选择库](http://www.jianshu.com/p/382346bf0aa9) 498 | - [Android 能让你少走弯路的干货整理](http://www.jianshu.com/p/514656c383a2) 499 | - [Android 撸起袖子,自己封装 DialogFragment](http://www.jianshu.com/p/c9f20ec7277a) 500 | - [手把手教你从零开始做一个好看的 APP](http://www.jianshu.com/p/8d2d74d6046f) 501 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "com.example.developerhaoz.doodleview" 8 | minSdkVersion 15 9 | targetSdkVersion 25 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:25.3.1' 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 C:\Users\Administrator\AppData\Local\Android\Sdk/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/example/developerhaoz/doodleview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.developerhaoz.doodleview; 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.example.developerhaoz.doodleview", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/developerhaoz/doodleview/BaseAction.java: -------------------------------------------------------------------------------- 1 | package com.example.developerhaoz.doodleview; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | 8 | /** 9 | * 动作的基础类 10 | *

11 | * Created by developerHaoz on 2017/7/12. 12 | */ 13 | 14 | abstract class BaseAction { 15 | 16 | public int color; 17 | 18 | BaseAction() { 19 | color = Color.WHITE; 20 | } 21 | 22 | BaseAction(int color) { 23 | this.color = color; 24 | } 25 | 26 | public abstract void draw(Canvas canvas); 27 | 28 | public abstract void move(float mx, float my); 29 | 30 | } 31 | 32 | class MyPoint extends BaseAction { 33 | private float x; 34 | private float y; 35 | 36 | MyPoint(float px, float py, int color) { 37 | super(color); 38 | this.x = px; 39 | this.y = py; 40 | } 41 | 42 | @Override 43 | public void draw(Canvas canvas) { 44 | Paint paint = new Paint(); 45 | paint.setColor(color); 46 | canvas.drawPoint(x, y, paint); 47 | } 48 | 49 | @Override 50 | public void move(float mx, float my) { 51 | 52 | } 53 | } 54 | 55 | /** 56 | * 自由曲线 57 | */ 58 | class MyPath extends BaseAction { 59 | private Path path; 60 | private int size; 61 | 62 | MyPath() { 63 | path = new Path(); 64 | size = 1; 65 | } 66 | 67 | MyPath(float x, float y, int size, int color) { 68 | super(color); 69 | this.path = new Path(); 70 | this.size = size; 71 | path.moveTo(x, y); 72 | path.lineTo(x, y); 73 | } 74 | 75 | @Override 76 | public void draw(Canvas canvas) { 77 | Paint paint = new Paint(); 78 | paint.setAntiAlias(true); 79 | paint.setDither(true); 80 | paint.setColor(color); 81 | paint.setStrokeWidth(size); 82 | paint.setStyle(Paint.Style.STROKE); 83 | paint.setStrokeJoin(Paint.Join.ROUND); 84 | paint.setStrokeCap(Paint.Cap.ROUND); 85 | canvas.drawPath(path, paint); 86 | } 87 | 88 | @Override 89 | public void move(float mx, float my) { 90 | path.lineTo(mx, my); 91 | } 92 | } 93 | 94 | /** 95 | * 直线 96 | */ 97 | class MyLine extends BaseAction { 98 | private float startX; 99 | private float startY; 100 | private float stopX; 101 | private float stopY; 102 | private int size; 103 | 104 | MyLine() { 105 | startX = 0; 106 | startY = 0; 107 | stopX = 0; 108 | stopY = 0; 109 | } 110 | 111 | MyLine(float x, float y, int size, int color) { 112 | super(color); 113 | this.startX = x; 114 | this.startY = y; 115 | stopX = x; 116 | stopY = y; 117 | this.size = size; 118 | } 119 | 120 | @Override 121 | public void draw(Canvas canvas) { 122 | Paint paint = new Paint(); 123 | paint.setAntiAlias(true); 124 | paint.setStyle(Paint.Style.STROKE); 125 | paint.setColor(color); 126 | paint.setStrokeWidth(size); 127 | canvas.drawLine(startX, startY, stopX, stopY, paint); 128 | } 129 | 130 | @Override 131 | public void move(float mx, float my) { 132 | this.stopX = mx; 133 | this.stopY = my; 134 | } 135 | } 136 | 137 | /** 138 | * 方框 139 | */ 140 | class MyRect extends BaseAction { 141 | private float startX; 142 | private float startY; 143 | private float stopX; 144 | private float stopY; 145 | private int size; 146 | 147 | MyRect() { 148 | this.startX = 0; 149 | this.startY = 0; 150 | this.stopX = 0; 151 | this.stopY = 0; 152 | } 153 | 154 | MyRect(float x, float y, int size, int color) { 155 | super(color); 156 | this.startX = x; 157 | this.startY = y; 158 | this.stopX = x; 159 | this.stopY = y; 160 | this.size = size; 161 | } 162 | 163 | @Override 164 | public void draw(Canvas canvas) { 165 | Paint paint = new Paint(); 166 | paint.setAntiAlias(true); 167 | paint.setStyle(Paint.Style.STROKE); 168 | paint.setColor(color); 169 | paint.setStrokeWidth(size); 170 | canvas.drawRect(startX, startY, stopX, stopY, paint); 171 | } 172 | 173 | @Override 174 | public void move(float mx, float my) { 175 | stopX = mx; 176 | stopY = my; 177 | } 178 | } 179 | 180 | /** 181 | * 圆框 182 | */ 183 | class MyCircle extends BaseAction { 184 | private float startX; 185 | private float startY; 186 | private float stopX; 187 | private float stopY; 188 | private float radius; 189 | private int size; 190 | 191 | MyCircle() { 192 | startX = 0; 193 | startY = 0; 194 | stopX = 0; 195 | stopY = 0; 196 | radius = 0; 197 | } 198 | 199 | MyCircle(float x, float y, int size, int color) { 200 | super(color); 201 | this.startX = x; 202 | this.startY = y; 203 | this.stopX = x; 204 | this.stopY = y; 205 | this.radius = 0; 206 | this.size = size; 207 | } 208 | 209 | @Override 210 | public void draw(Canvas canvas) { 211 | Paint paint = new Paint(); 212 | paint.setAntiAlias(true); 213 | paint.setStyle(Paint.Style.STROKE); 214 | paint.setColor(color); 215 | paint.setStrokeWidth(size); 216 | canvas.drawCircle((startX + stopX) / 2, (startY + stopY) / 2, radius, paint); 217 | } 218 | 219 | @Override 220 | public void move(float mx, float my) { 221 | stopX = mx; 222 | stopY = my; 223 | radius = (float) ((Math.sqrt((mx - startX) * (mx - startX) 224 | + (my - startY) * (my - startY))) / 2); 225 | } 226 | } 227 | 228 | class MyFillRect extends BaseAction { 229 | private float startX; 230 | private float startY; 231 | private float stopX; 232 | private float stopY; 233 | private int size; 234 | 235 | MyFillRect() { 236 | this.startX = 0; 237 | this.startY = 0; 238 | this.stopX = 0; 239 | this.stopY = 0; 240 | } 241 | 242 | MyFillRect(float x, float y, int size, int color) { 243 | super(color); 244 | this.startX = x; 245 | this.startY = y; 246 | this.stopX = x; 247 | this.stopY = y; 248 | this.size = size; 249 | } 250 | 251 | @Override 252 | public void draw(Canvas canvas) { 253 | Paint paint = new Paint(); 254 | paint.setAntiAlias(true); 255 | paint.setStyle(Paint.Style.FILL); 256 | paint.setColor(color); 257 | paint.setStrokeWidth(size); 258 | canvas.drawRect(startX, startY, stopX, stopY, paint); 259 | } 260 | 261 | @Override 262 | public void move(float mx, float my) { 263 | stopX = mx; 264 | stopY = my; 265 | } 266 | } 267 | 268 | /** 269 | * 圆饼 270 | */ 271 | class MyFillCircle extends BaseAction { 272 | private float startX; 273 | private float startY; 274 | private float stopX; 275 | private float stopY; 276 | private float radius; 277 | private int size; 278 | 279 | 280 | public MyFillCircle(float x, float y, int size, int color) { 281 | super(color); 282 | this.startX = x; 283 | this.startY = y; 284 | this.stopX = x; 285 | this.stopY = y; 286 | this.radius = 0; 287 | this.size = size; 288 | } 289 | 290 | @Override 291 | public void draw(Canvas canvas) { 292 | Paint paint = new Paint(); 293 | paint.setAntiAlias(true); 294 | paint.setStyle(Paint.Style.FILL); 295 | paint.setColor(color); 296 | paint.setStrokeWidth(size); 297 | canvas.drawCircle((startX + stopX) / 2, (startY + stopY) / 2, radius, paint); 298 | } 299 | 300 | @Override 301 | public void move(float mx, float my) { 302 | stopX = mx; 303 | stopY = my; 304 | radius = (float) ((Math.sqrt((mx - startX) * (mx - startX) 305 | + (my - startY) * (my - startY))) / 2); 306 | } 307 | } 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/developerhaoz/doodleview/DoodleView.java: -------------------------------------------------------------------------------- 1 | package com.example.developerhaoz.doodleview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.os.Environment; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.SurfaceHolder; 12 | import android.view.SurfaceView; 13 | 14 | import java.io.File; 15 | import java.io.FileNotFoundException; 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * 自定义的用于涂鸦的 View 23 | *

24 | * Created by developerHaoz on 2017/7/12. 25 | */ 26 | 27 | public class DoodleView extends SurfaceView implements SurfaceHolder.Callback { 28 | 29 | private SurfaceHolder mSurfaceHolder = null; 30 | 31 | // 当前所选画笔的形状 32 | private BaseAction curAction = null; 33 | // 默认画笔为黑色 34 | private int currentColor = Color.BLACK; 35 | // 画笔的粗细 36 | private int currentSize = 5; 37 | 38 | private Paint mPaint; 39 | 40 | private List mBaseActions; 41 | 42 | private Bitmap mBitmap; 43 | 44 | private ActionType mActionType = ActionType.Path; 45 | 46 | public DoodleView(Context context) { 47 | super(context); 48 | init(); 49 | } 50 | 51 | public DoodleView(Context context, AttributeSet attrs, int defStyle) { 52 | super(context, attrs, defStyle); 53 | init(); 54 | } 55 | 56 | public DoodleView(Context context, AttributeSet attrs) { 57 | super(context, attrs); 58 | init(); 59 | } 60 | 61 | private void init() { 62 | mSurfaceHolder = this.getHolder(); 63 | mSurfaceHolder.addCallback(this); 64 | this.setFocusable(true); 65 | 66 | mPaint = new Paint(); 67 | mPaint.setColor(Color.WHITE); 68 | mPaint.setStrokeWidth(currentSize); 69 | } 70 | 71 | @Override 72 | public void surfaceCreated(SurfaceHolder holder) { 73 | Canvas canvas = mSurfaceHolder.lockCanvas(); 74 | canvas.drawColor(Color.WHITE); 75 | mSurfaceHolder.unlockCanvasAndPost(canvas); 76 | mBaseActions = new ArrayList<>(); 77 | } 78 | 79 | @Override 80 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 81 | 82 | } 83 | 84 | @Override 85 | public void surfaceDestroyed(SurfaceHolder holder) { 86 | 87 | } 88 | 89 | @Override 90 | public boolean onTouchEvent(MotionEvent event) { 91 | int action = event.getAction(); 92 | if (action == MotionEvent.ACTION_CANCEL) { 93 | return false; 94 | } 95 | 96 | float touchX = event.getX(); 97 | float touchY = event.getY(); 98 | 99 | switch (action) { 100 | case MotionEvent.ACTION_DOWN: 101 | setCurAction(touchX, touchY); 102 | break; 103 | case MotionEvent.ACTION_MOVE: 104 | Canvas canvas = mSurfaceHolder.lockCanvas(); 105 | canvas.drawColor(Color.WHITE); 106 | for (BaseAction baseAction : mBaseActions) { 107 | baseAction.draw(canvas); 108 | } 109 | curAction.move(touchX, touchY); 110 | curAction.draw(canvas); 111 | mSurfaceHolder.unlockCanvasAndPost(canvas); 112 | break; 113 | case MotionEvent.ACTION_UP: 114 | mBaseActions.add(curAction); 115 | curAction = null; 116 | break; 117 | 118 | default: 119 | break; 120 | } 121 | return true; 122 | } 123 | 124 | /** 125 | * 得到当前画笔的类型,并进行实例化 126 | * 127 | * @param x 128 | * @param y 129 | */ 130 | private void setCurAction(float x, float y) { 131 | switch (mActionType) { 132 | case Point: 133 | curAction = new MyPoint(x, y, currentColor); 134 | break; 135 | case Path: 136 | curAction = new MyPath(x, y, currentSize, currentColor); 137 | break; 138 | case Line: 139 | curAction = new MyLine(x, y, currentSize, currentColor); 140 | break; 141 | case Rect: 142 | curAction = new MyRect(x, y, currentSize, currentColor); 143 | break; 144 | case Circle: 145 | curAction = new MyCircle(x, y, currentSize, currentColor); 146 | break; 147 | case FillEcRect: 148 | curAction = new MyFillRect(x, y, currentSize, currentColor); 149 | break; 150 | case FilledCircle: 151 | curAction = new MyFillCircle(x, y, currentSize, currentColor); 152 | break; 153 | default: 154 | break; 155 | } 156 | } 157 | 158 | /** 159 | * 设置画笔的颜色 160 | * 161 | * @param color 颜色 162 | */ 163 | public void setColor(String color) { 164 | this.currentColor = Color.parseColor(color); 165 | } 166 | 167 | /** 168 | * 设置画笔的粗细 169 | * 170 | * @param size 画笔的粗细 171 | */ 172 | public void setSize(int size) { 173 | this.currentSize = size; 174 | } 175 | 176 | /** 177 | * 设置画笔的形状 178 | * 179 | * @param type 画笔的形状 180 | */ 181 | public void setType(ActionType type) { 182 | this.mActionType = type; 183 | } 184 | 185 | /** 186 | * 将当前的画布转换成一个 Bitmap 187 | * 188 | * @return Bitmap 189 | */ 190 | public Bitmap getBitmap() { 191 | mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 192 | Canvas canvas = new Canvas(mBitmap); 193 | doDraw(canvas); 194 | return mBitmap; 195 | } 196 | 197 | /** 198 | * 保存涂鸦后的图片 199 | * 200 | * @param doodleView 201 | * @return 图片的保存路径 202 | */ 203 | public String saveBitmap(DoodleView doodleView) { 204 | String path = Environment.getExternalStorageDirectory().getAbsolutePath() 205 | + "/doodleview/" + System.currentTimeMillis() + ".png"; 206 | if (!new File(path).exists()) { 207 | new File(path).getParentFile().mkdir(); 208 | } 209 | savePicByPNG(doodleView.getBitmap(), path); 210 | return path; 211 | } 212 | 213 | /** 214 | * 将一个 Bitmap 保存在一个指定的路径中 215 | * 216 | * @param bitmap 217 | * @param filePath 218 | */ 219 | public static void savePicByPNG(Bitmap bitmap, String filePath) { 220 | FileOutputStream fileOutputStream; 221 | try { 222 | fileOutputStream = new FileOutputStream(filePath); 223 | if (null != fileOutputStream) { 224 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); 225 | fileOutputStream.flush(); 226 | fileOutputStream.close(); 227 | } 228 | } catch (FileNotFoundException e) { 229 | e.printStackTrace(); 230 | } catch (IOException e) { 231 | e.printStackTrace(); 232 | } 233 | } 234 | 235 | /** 236 | * 开始进行绘画 237 | * 238 | * @param canvas 239 | */ 240 | private void doDraw(Canvas canvas) { 241 | canvas.drawColor(Color.TRANSPARENT); 242 | for (BaseAction action : mBaseActions) { 243 | action.draw(canvas); 244 | } 245 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 246 | } 247 | 248 | 249 | /** 250 | * 回退 251 | * 252 | * @return 是否已经回退成功 253 | */ 254 | public boolean back(){ 255 | if(mBaseActions != null && mBaseActions.size() > 0){ 256 | mBaseActions.remove(mBaseActions.size() -1); 257 | Canvas canvas = mSurfaceHolder.lockCanvas(); 258 | canvas.drawColor(Color.WHITE); 259 | for (BaseAction action : mBaseActions) { 260 | action.draw(canvas); 261 | } 262 | mSurfaceHolder.unlockCanvasAndPost(canvas); 263 | return true; 264 | } 265 | return false; 266 | } 267 | 268 | /** 269 | * 重置签名 270 | */ 271 | public void reset(){ 272 | if(mBaseActions != null && mBaseActions.size() > 0){ 273 | mBaseActions.clear(); 274 | Canvas canvas = mSurfaceHolder.lockCanvas(); 275 | canvas.drawColor(Color.WHITE); 276 | for (BaseAction action : mBaseActions) { 277 | action.draw(canvas); 278 | } 279 | mSurfaceHolder.unlockCanvasAndPost(canvas); 280 | } 281 | } 282 | 283 | enum ActionType { 284 | Point, Path, Line, Rect, Circle, FillEcRect, FilledCircle 285 | } 286 | } 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/developerhaoz/doodleview/DoodleViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.developerhaoz.doodleview; 2 | 3 | import android.content.DialogInterface; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.app.AlertDialog; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.util.Log; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.MotionEvent; 12 | import android.widget.Toast; 13 | 14 | /** 15 | * 用于展示 DoodleView 功能的 Activity 16 | * 17 | * Created by developerHaoz on 2017/7/14. 18 | */ 19 | 20 | public class DoodleViewActivity extends AppCompatActivity { 21 | 22 | private DoodleView mDoodleView; 23 | private AlertDialog mColorDialog; 24 | private AlertDialog mPaintDialog; 25 | private AlertDialog mShapeDialog; 26 | 27 | private static final String TAG = "DoodleViewActivity"; 28 | 29 | @Override 30 | protected void onCreate(@Nullable Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_doodleview); 33 | 34 | mDoodleView = (DoodleView) findViewById(R.id.doodle_doodleview); 35 | mDoodleView.setSize(dip2px(5)); 36 | } 37 | 38 | @Override 39 | public boolean onTouchEvent(MotionEvent event) { 40 | return mDoodleView.onTouchEvent(event); 41 | } 42 | 43 | @Override 44 | public boolean onCreateOptionsMenu(Menu menu) { 45 | getMenuInflater().inflate(R.menu.menu_main, menu); 46 | return true; 47 | } 48 | 49 | @Override 50 | public boolean onOptionsItemSelected(MenuItem item) { 51 | switch(item.getItemId()){ 52 | case R.id.main_color: 53 | showColorDialog(); 54 | break; 55 | case R.id.main_size: 56 | showSizeDialog(); 57 | break; 58 | case R.id.main_action: 59 | showShapeDialog(); 60 | break; 61 | case R.id.main_reset: 62 | mDoodleView.reset(); 63 | break; 64 | case R.id.main_save: 65 | String path = mDoodleView.saveBitmap(mDoodleView); 66 | Log.d(TAG, "onOptionsItemSelected: " + path); 67 | Toast.makeText(this, "保存图片的路径为:" + path, Toast.LENGTH_SHORT).show(); 68 | break; 69 | } 70 | return true; 71 | } 72 | 73 | /** 74 | * 显示选择画笔颜色的对话框 75 | */ 76 | private void showColorDialog() { 77 | if(mColorDialog == null){ 78 | mColorDialog = new AlertDialog.Builder(this) 79 | .setTitle("选择颜色") 80 | .setSingleChoiceItems(new String[]{"蓝色", "红色", "黑色"}, 0, 81 | new DialogInterface.OnClickListener() { 82 | @Override 83 | public void onClick(DialogInterface dialog, int which) { 84 | switch (which){ 85 | case 0: 86 | mDoodleView.setColor("#0000ff"); 87 | break; 88 | case 1: 89 | mDoodleView.setColor("#ff0000"); 90 | break; 91 | case 2: 92 | mDoodleView.setColor("#272822"); 93 | break; 94 | default:break; 95 | } 96 | dialog.dismiss(); 97 | } 98 | }).create(); 99 | } 100 | mColorDialog.show(); 101 | } 102 | 103 | /** 104 | * 显示选择画笔粗细的对话框 105 | */ 106 | private void showSizeDialog(){ 107 | if(mPaintDialog == null){ 108 | mPaintDialog = new AlertDialog.Builder(this) 109 | .setTitle("选择画笔粗细") 110 | .setSingleChoiceItems(new String[]{"细", "中", "粗"}, 0, 111 | new DialogInterface.OnClickListener() { 112 | @Override 113 | public void onClick(DialogInterface dialog, int which) { 114 | switch (which){ 115 | case 0: 116 | mDoodleView.setSize(dip2px(5)); 117 | break; 118 | case 1: 119 | mDoodleView.setSize(dip2px(10)); 120 | break; 121 | case 2: 122 | mDoodleView.setSize(dip2px(15)); 123 | break; 124 | default: 125 | break; 126 | } 127 | dialog.dismiss(); 128 | } 129 | }).create(); 130 | } 131 | mPaintDialog.show(); 132 | } 133 | 134 | /** 135 | * 显示选择画笔形状的对话框 136 | */ 137 | private void showShapeDialog(){ 138 | if(mShapeDialog == null){ 139 | mShapeDialog = new AlertDialog.Builder(this) 140 | .setTitle("选择形状") 141 | .setSingleChoiceItems(new String[]{"路径", "直线", "矩形", "圆形","实心矩形", "实心圆"}, 0, 142 | new DialogInterface.OnClickListener() { 143 | @Override 144 | public void onClick(DialogInterface dialog, int which) { 145 | switch (which){ 146 | case 0: 147 | mDoodleView.setType(DoodleView.ActionType.Path); 148 | break; 149 | case 1: 150 | mDoodleView.setType(DoodleView.ActionType.Line); 151 | break; 152 | case 2: 153 | mDoodleView.setType(DoodleView.ActionType.Rect); 154 | break; 155 | case 3: 156 | mDoodleView.setType(DoodleView.ActionType.Circle); 157 | break; 158 | case 4: 159 | mDoodleView.setType(DoodleView.ActionType.FillEcRect); 160 | break; 161 | case 5: 162 | mDoodleView.setType(DoodleView.ActionType.FilledCircle); 163 | break; 164 | default: 165 | break; 166 | } 167 | dialog.dismiss(); 168 | } 169 | }).create(); 170 | } 171 | mShapeDialog.show(); 172 | } 173 | 174 | private int dip2px(float dpValue){ 175 | final float scale = getResources().getDisplayMetrics().density; 176 | return (int)(dpValue * scale + 0.5f); 177 | } 178 | } 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_doodleview.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DoodleView 3 | 画笔颜色 4 | 画笔粗细 5 | 画笔形状 6 | 重画 7 | 保存 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/developerhaoz/doodleview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.developerhaoz.doodleview; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.1' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerHaoz/DoodleView/7e85d05b235d3365becc6abca2e0a555a2faf7d5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 12 15:10:52 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------