├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── 5d5d18d3aa7f18e85c6c61c309b24463.mp4 ├── README.md ├── app ├── .gitignore ├── app-release.apk ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── shiming │ │ └── pen │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── shiming │ │ │ └── pen │ │ │ ├── Bezier.java │ │ │ ├── MainActivity.java │ │ │ ├── field_character │ │ │ ├── BitmapDrawUtils.java │ │ │ ├── DeletableEditText.java │ │ │ ├── DrawPenView.java │ │ │ ├── DrawViewLayout.java │ │ │ ├── Executor.java │ │ │ ├── FieldCharacterShapeActivity.java │ │ │ ├── HandRichTextEditor.java │ │ │ ├── HandViewUtils.java │ │ │ ├── SystemUtils.java │ │ │ └── WordSandPictures.java │ │ │ ├── new_code │ │ │ ├── BasePen.java │ │ │ ├── BasePenExtend.java │ │ │ ├── BrushPen.java │ │ │ ├── IPenConfig.java │ │ │ ├── MotionElement.java │ │ │ ├── NewDrawPenView.java │ │ │ ├── PenUtils.java │ │ │ └── SteelPen.java │ │ │ ├── old_code │ │ │ ├── ControllerPoint.java │ │ │ ├── OldDrawPenView.java │ │ │ └── StrokePen.java │ │ │ └── view │ │ │ └── OldDemoActivity.java │ └── res │ │ ├── drawable │ │ ├── hand_draw_delete_selector.xml │ │ ├── hand_draw_kb_show_selector.xml │ │ ├── hand_draw_new_line_selector.xml │ │ ├── hand_draw_space_selector.xml │ │ ├── icon_delete_no_press.png │ │ ├── icon_delete_press.png │ │ ├── icon_pen_new_line_no_press.png │ │ ├── icon_pen_new_line_press.png │ │ ├── icon_pen_space_no_press.png │ │ ├── icon_pen_space_press.png │ │ ├── letter_key.xml │ │ ├── letter_key_.png │ │ ├── letter_key_nor.xml │ │ ├── letter_key_nor_.png │ │ ├── letter_key_nor_prs.png │ │ └── letter_key_prs.png │ │ ├── layout │ │ ├── activity_field_character_shape_layout.xml │ │ ├── activity_main.xml │ │ ├── activity_old_demo_layout.xml │ │ ├── brush_weight_layout.xml │ │ ├── draw_view_layout.xml │ │ └── hand_rich_edittext.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 │ │ ├── icon_demo.jpg │ │ └── icon_draw_bg.png │ │ ├── mipmap-xxxhdpi │ │ ├── brush.png │ │ ├── cicrle.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ ├── san.png │ │ ├── six.png │ │ ├── timg.jpg │ │ └── tranglie.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── shiming │ └── pen │ └── ExampleUnitTest.java ├── build.gradle ├── code.png ├── 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/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /5d5d18d3aa7f18e85c6c61c309b24463.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/5d5d18d3aa7f18e85c6c61c309b24463.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 此Demo介绍的两个地址: 2 | 3 | * 安卓画笔笔锋的实现探索(一):http://www.jianshu.com/p/6746d68ef2c3 4 | * 安卓画笔笔锋的实现探索(二):http://www.jianshu.com/p/3ee259f2caf7# 5 | 6 | ##### 看效果 7 | 8 | * 最新的效果图 9 | ![微信图片_20180207180824.jpg](http://upload-images.jianshu.io/upload_images/5363507-8fa5d3ee5a21287a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | * 设置笔宽度为60,效果如下 13 | ![微信图片_20170910184918.png](http://upload-images.jianshu.io/upload_images/5363507-f1d4934949530f78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | ![image.png](http://upload-images.jianshu.io/upload_images/5363507-8b622187caa4fca5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 16 | 17 | * 这个效果明显一点,哈哈,是不是很有大师的写字风格 18 | ![微信图片_20170902142924.jpg](http://upload-images.jianshu.io/upload_images/5363507-76230a0761ef9dda.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 19 | #### 实现这个效果,在git上有个哥们用opengGl3.0实现比我这个更牛逼的效果,但是发现在低端手机上会报错,原因是不支持openGL3.0,导致Apk装入失败,1.0的api有看不懂,你说我能怎么办,我也很绝望啊!同时感觉opengl更加节手机性能,but我错了,在低端手机上使用opengl简直就是噩梦,卡的一逼,算了不提了,此功能的实现还是基于安卓的Paint,通过事件去绘制路径。 20 | 21 | ## 1.创建DrawPenView类继承View 22 | 23 | ![image.png](http://upload-images.jianshu.io/upload_images/5363507-7094aaa5fa65811f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 24 | 25 | #### 初始化笔,笔锋的效果,我个人尝试了使用三个笔,每次绘制的时候,三个笔一起绘制,根据手指的滑动速率的快慢去使其中的某个笔不用绘制,但是这个效果稀烂,所以view的还是用一只笔即可, 26 | 27 | ``` 28 | mPaint = new Paint(); 29 | mPaint.setColor(Color.parseColor("#FF4081")); 30 | mPaint.setStrokeWidth(14); 31 | mPaint.setStyle(Paint.Style.STROKE); 32 | mPaint.setStrokeCap(Paint.Cap.ROUND);//结束的笔画为圆心 33 | mPaint.setStrokeJoin(Paint.Join.ROUND);//连接处元 34 | mPaint.setAlpha(0xFF); 35 | mPaint.setAntiAlias(true); 36 | mPaint.setStrokeMiter(1.0f); 37 | ``` 38 | 39 | #### 初始化bitmap,和画布,画布在这里主要是生成一张bitmap的 40 | 41 | private void initParameter(Context context) { 42 | mContext = context; 43 | DisplayMetrics dm = new DisplayMetrics(); 44 | ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); 45 | mBitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888); 46 | //笔的控制类 47 | mVisualStrokePen=new VisualStrokePen(mContext); 48 | initPaint(mContext); 49 | initCanvas(); 50 | } 51 | 52 | private void initCanvas() { 53 | mCanvas = new Canvas(mBitmap); 54 | //设置画布的颜色的问题 55 | mCanvas.drawColor(Color.TRANSPARENT); 56 | } 57 | 58 | #### 重写onDraw()方法:由于项目需要,在这里我仅仅提供了两个方法:清除画布和绘制。扩展的功能有:返回上一步的绘制步骤,设置画笔的属性,mark笔,毛笔,钢笔,圆珠笔,铅笔等一切的控制都在这里进行 59 | 60 | @Override 61 | protected void onDraw(Canvas canvas) { 62 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 63 | switch (mCanvasCode) { 64 | case CANVAS_NORMAL: 65 | mVisualStrokePen.draw(canvas); 66 | break; 67 | case CANVAS_RESET: 68 | reset(); 69 | break; 70 | default: 71 | Log.e(TAG, "onDraw" + Integer.toString(mCanvasCode)); 72 | break; 73 | } 74 | super.onDraw(canvas); 75 | } 76 | 77 | ## 2.认识MotionEvent对象 78 | 79 | #### 当用户触摸屏幕时,将创建一个MontionEvent对象。MotionEvent包含了关于发生触摸的位置和时间的信息,以及触摸事件的其他细节。 80 | 81 | /** 82 | event.getAction() //获取触控动作比如ACTION_DOWN 83 | event.getPointerCount(); //获取触控点的数量,比如2则可能是两个手指同时按压屏幕 84 | event.getPointerId(nID); //对于每个触控的点的细节,我们可以通过一个循环执行getPointerId方法获取索引 85 | event.getX(nID); //获取第nID个触控点的x位置,记录的第一个点为getX,getY 86 | event.getY(nID); //获取第nID个点触控的y位置 87 | event.getPressure(nID); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的 88 | event.getDownTime() //按下开始时间 89 | event.getEventTime() // 事件结束时间 90 | event.getEventTime()-event.getDownTime()); //总共按下时花费时间 91 | * @param event 92 | * @return 93 | */ 94 | @Override 95 | public boolean onTouchEvent(MotionEvent event) { 96 | //测试过程中,当使用到event的时候,产生了没有收到事件的问题,所以在这里需要obtian的一下 97 | MotionEvent event2 = MotionEvent.obtain(event); 98 | switch (event2.getActionMasked()) { 99 | case MotionEvent.ACTION_DOWN: 100 | setCanvasCode(CANVAS_NORMAL); 101 | mVisualStrokePen.onDown(mVisualStrokePen.createMotionElement(event2)); 102 | break; 103 | case MotionEvent.ACTION_MOVE: 104 | mVisualStrokePen.onMove(mVisualStrokePen.createMotionElement(event2)); 105 | break; 106 | case MotionEvent.ACTION_UP: 107 | long time = System.currentTimeMillis(); 108 | mVisualStrokePen.onUp(mVisualStrokePen.createMotionElement(event2),mCanvas); 109 | break; 110 | default: 111 | break; 112 | } 113 | invalidate(); 114 | return true; 115 | } 116 | 117 | #### 在这里我需要提到一个MotionEvent的api:motionEvent.getToolType(0);返回的以下四种的值, 118 | 119 | * TOOL_TYPE_UNKNOWN :不知道什么画的 120 | * TOOL_TYPE_FINGER :手指 121 | * TOOL_TYPE_STYLUS :笔画的 122 | * TOOL_TYPE_MOUSE :该工具是一个鼠标或触控板 123 | * TOOL_TYPE_ERASER :工具是一块橡皮或一笔用于倒立的姿势 124 | * 看见没,卧槽,以前都不知道,这个类知道我们用什么属性在写字, 125 | * event.getPressure(); //可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的,我的手机上为1 126 | * motionEvent.getEventTime():事件发生的事件,在我此时的事件是shiming==8359650,而且是跟随着系统的时间而定 127 | 128 | ``` 129 | 130 | 131 | 132 | /** 133 | * Tool type constant: Unknown tool type. 134 | * This constant is used when the tool type is not known or is not relevant, 135 | * such as for a trackball or other non-pointing device. 136 | * 137 | * @see #getToolType 138 | */ 139 | public static final int TOOL_TYPE_UNKNOWN = 0; 140 | 141 | /** 142 | * Tool type constant: The tool is a finger. 143 | * 144 | * @see #getToolType 145 | */ 146 | public static final int TOOL_TYPE_FINGER = 1; 147 | 148 | /** 149 | * Tool type constant: The tool is a stylus. 150 | * 151 | * @see #getToolType 152 | */ 153 | public static final int TOOL_TYPE_STYLUS = 2; 154 | 155 | /** 156 | * Tool type constant: The tool is a mouse or trackpad. 157 | * 158 | * @see #getToolType 159 | */ 160 | public static final int TOOL_TYPE_MOUSE = 3; 161 | 162 | /** 163 | * Tool type constant: The tool is an eraser or a stylus being used in an inverted posture. 164 | * 165 | * @see #getToolType 166 | */ 167 | public static final int TOOL_TYPE_ERASER = 4: 168 | ####关于MotionElement 类:记录下五个参数:坐标x y,压力值,什么在屏幕上写的,还有事件发生的时间。 169 | 170 | public static class MotionElement { 171 | 172 | public float x; 173 | public float y; 174 | public float pressure; 175 | public int tooltype; 176 | public long timestamp; 177 | 178 | public MotionElement(float mx, float my, float mp, int ttype, long mt) { 179 | x = mx; 180 | y = my; 181 | pressure = mp; 182 | tooltype = ttype; 183 | timestamp = mt; 184 | } 185 | 186 | } 187 | 188 | 189 | /** 190 | * event.getPressure(); //LCD可以感应出用户的手指压力,当 然具体的级别由驱动和物理硬件决定的,我的手机上为1 191 | * @param motionEvent 192 | * @return 193 | */ 194 | public MotionElement createMotionElement(MotionEvent motionEvent) { 195 | System.out.println("shiming== 0000=="+motionEvent.getToolType(0)); 196 | System.out.println("shiming=="+motionEvent.getPressure()); 197 | System.out.println("shiming=="+motionEvent.getEventTime()); 198 | MotionElement motionElement = new MotionElement(motionEvent.getX(), motionEvent.getY(), 199 | motionEvent.getPressure(), motionEvent.getToolType(0), 200 | motionEvent.getEventTime()); 201 | return motionElement; 202 | } 203 | ``` 204 | 205 | ## 3.清除画布 206 | 207 | * Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种把paint.setXfermode( 208 | Xfermode xfermode)的模式设置为clear,使用我们新建的canvas去drapaint这个笔,记得清除完了,要把mode设置为null 209 | 有偏文档介绍的很好,我在这里抛砖引玉一下,就不班门弄斧了: http://www.cnblogs.com/tianzhijiexian/p/4297172.html 210 | 211 | ``` 212 | *清除画布,记得清除点的集合 213 | */ 214 | public void reset() { 215 | mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 216 | mCanvas.drawPaint(mPaint); 217 | mPaint.setXfermode(null); 218 | mVisualStrokePen.clear(); 219 | } 220 | ``` 221 | 222 | ## 4.关于Bezier曲线 223 | 224 | * 先发个图,嘿嘿,我自己手画的,看不清没关系,只需知道4个点的关系,想象一下曲线就行 225 | 226 | ![微信图片_20170826183403.jpg](http://upload-images.jianshu.io/upload_images/5363507-7dd06af8c72132e2.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 227 | 228 | ![image.png](http://upload-images.jianshu.io/upload_images/5363507-df08ff751b79f650.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 229 | 230 | #### 知道两点连接起来是直线,当我们不断的求出两个点的控制点,把无数的控制点绘制在一起就是一条完美的曲线,反正我这样子理解的,当然我在这里也做了一个width的控制,和这种的原理差不多。 231 | 232 | public void init(float lastx, float lasty, float lastWidth, float x, float y, float width) 233 | { 234 | //资源点设置,最后的点的为资源点 235 | mSource.set(lastx, lasty, lastWidth); 236 | float xmid = getMid(lastx, x); 237 | float ymid = getMid(lasty, y); 238 | float wmid = getMid(lastWidth, width); 239 | //距离点为平均点 240 | mDestination.set(xmid, ymid, wmid); 241 | //控制点为当前的距离点 242 | mControl.set(getMid(lastx,xmid),getMid(lasty,ymid),getMid(lastWidth,wmid)); 243 | //下个控制点为当前点 244 | mNextControl.set(x, y, width); 245 | } 246 | 247 | /** 248 | * 249 | * @param x1 一个点的x 250 | * @param x2 一个点的x 251 | * @return 252 | */ 253 | /** 254 | * 255 | * @param x1 一个点的x 256 | * @param x2 一个点的x 257 | * @return 258 | */ 259 | private float getMid(float x1, float x2) { 260 | return (float)((x1 + x2) / 2.0); 261 | } 262 | 263 | private double getWidth(double w0, double w1, double t){ 264 | return w0 + (w1 - w0) * t; 265 | } 266 | 267 | #### 以上记得知道个步骤,才能方便理解,当这个点是我们资源点的时候,或者是当前点,那么它下一步就会成为一个新的资源点,需要不断的替换当前的起点和终点,那么才可以形成一个曲线 268 | 269 | /** 270 | * 替换就的点,原来的距离点变换为资源点,控制点变为原来的下一个控制点,距离点取原来控制点的和新的的一半 271 | * 下个控制点为新的点 272 | * @param x 新的点的坐标 273 | * @param y 新的点的坐标 274 | * @param width 275 | */ 276 | public void addNode(float x, float y, float width){ 277 | mSource.set(mDestination); 278 | mControl.set(mNextControl); 279 | mDestination.set(getMid(mNextControl.x, x), getMid(mNextControl.y, y), getMid(mNextControl.width, width)); 280 | mNextControl.set(x, y, width); 281 | } 282 | 283 | ``` 284 | #### 是不是看不懂,对,看不懂就对了,去下面看代码,记得在本子上多画几个点,想象一下这样变换的位置,然后就会明白了这真的是一个美妙的曲线,比女朋友还漂亮,哈哈,扯皮了 285 | #### 关于手指抬起来的时候的方法: 结合手指抬起来的动作,告诉现在的曲线控制点也必须变化,其实在这里也不需要结合着up事件使用因为在down的事件中,所有点都会被重置,然后设置这个没有多少意义,但是可以改变下个事件的朝向改变先留着,因为后面如果需要控制整个颜色的改变的话,我的依靠这个方法,还有按压的时间的变化 286 | ``` 287 | 288 | /** 289 | 290 | * 结合手指抬起来的动作,告诉现在的曲线控制点也必须变化,其实在这里也不需要结合着up事件使用 * 因为在down的事件中,所有点都会被重置,然后设置这个没有多少意义,但是可以改变下个事件的朝向改变 291 | * 先留着,因为后面如果需要控制整个颜色的改变的话,我的依靠这个方法,还有按压的时间的变化 292 | */ /** 293 | * 结合手指抬起来的动作,告诉现在的曲线控制点也必须变化,其实在这里也不需要结合着up事件使用 * 因为在down的事件中,所有点都会被重置,然后设置这个没有多少意义,但是可以改变下个事件的朝向改变 294 | * 先留着,因为后面如果需要控制整个颜色的改变的话,我的依靠这个方法,还有按压的时间的变化 295 | */ public void end() { mSource.set(mDestination); float x = getMid(mNextControl.x, mSource.x); 296 | float y = getMid(mNextControl.y, mSource.y); float w = getMid(mNextControl.width, mSource.width); 297 | mControl.set(x, y, w); mDestination.set(mNextControl); } 298 | 299 | #### 还有个方法:我的提一句,是不是想一个一元二次的方程,哈哈!这个不是我写的,这个是基于git上开源的写的,是不是有点高中数学的影响了,哈哈,对就是这样的, 300 | 301 | ![image.png](http://upload-images.jianshu.io/upload_images/5363507-502f1a6068d1fc73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 302 | 303 | * 三阶曲线的控制点 304 | * @param p0 305 | * @param p1 306 | * @param p2 307 | * @param t 308 | * @return 309 | */ 310 | private double getValue(double p0, double p1, double p2, double t){ 311 | double A = p2 - 2 * p1 + p0; 312 | double B = 2 * (p1 - p0); 313 | double C = p0; 314 | return A * t * t + B * t + C; 315 | } 316 | 317 | ## 5.关于StrokePen,这个类才是所有的关键,如图分析:其实原理就是,通过安卓事件收集一个点的集合,这个点的集合的第一点和第二个点,绘制一个椭圆一个椭圆。 318 | 319 | ![image.png](http://upload-images.jianshu.io/upload_images/5363507-61b7202ef15ed062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 320 | 321 | private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint){ 322 | //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园 323 | double curDis = Math.hypot(x0-x1, y0-y1); 324 | int steps = 1; 325 | if(paint.getStrokeWidth() < 6){ 326 | steps = 1+(int)(curDis/2); 327 | }else if(paint.getStrokeWidth() > 60){ 328 | steps = 1+(int)(curDis/4); 329 | }else{ 330 | steps = 1+(int)(curDis/3); 331 | } 332 | double deltaX=(x1-x0)/steps; 333 | double deltaY=(y1-y0)/steps; 334 | double deltaW=(w1-w0)/steps; 335 | double x=x0; 336 | double y=y0; 337 | double w=w0; 338 | 339 | for(int i=0;i 60){ 360 | steps = 1+(int)(curDis/4); 361 | }else{ 362 | steps = 1+(int)(curDis/3); 363 | } 364 | 365 | * 关于Rext和RexF的区别:Rect是使用int类型作为数值,RectF是使用float类型作为数值。很明显这里我们需要更高的精度 366 | * 绘制的原理,就是每个记录下的点绘制一个椭圆,当无数个的椭圆重合在一起就是一个线,这个线的宽度和椭圆的形状有关系 367 | 368 | RectF oval = new RectF(); 369 | oval.set((float)(x-w/4.0f), (float)(y-w/2.0f), (float)(x+w/4.0f), (float)(y+w/2.0f)); 370 | //最基本的实现,通过点控制线,绘制椭圆 371 | canvas.drawOval(oval, paint); 372 | 373 | 374 | * 375 | 376 | 这里需要在view中的onDraw中调用,本来我开始是想说能不能再一开始的时候,down事件的时候,给他画个园,但是这个园的半径我控制不好,所以在代码中我留下这个问题,以后需要做更难的效果的时候,我来把这个开始的步骤补上。 377 | 378 | /** 379 | * 早onDraw需要调用 380 | * @param canvas 画布 381 | */ 382 | public void draw(Canvas canvas) { 383 | mPaint.setStyle(Paint.Style.FILL); 384 | //点的集合少 不去绘制 385 | if (mHWPointList == null || mHWPointList.size() < 1) 386 | return; 387 | //当控制点的集合很少的时候,需要画个小圆,但是需要算法 388 | if (mHWPointList.size() < 2) { 389 | ControllerPoint point = mHWPointList.get(0); 390 | //由于此问题在算法上还没有实现,所以暂时不给他画圆圈 391 | //canvas.drawCircle(point.x, point.y, point.width, mPaint); 392 | } else { 393 | curPoint = mHWPointList.get(0); 394 | for (int i = 1; i < mHWPointList.size(); i++) { 395 | ControllerPoint point = mHWPointList.get(i); 396 | drawToPoint(canvas, point, mPaint); 397 | curPoint = point; 398 | } 399 | } 400 | } 401 | 402 | #### Down事件处理 403 | 404 | * 手指的down事件 405 | * @param mElement 406 | */ 407 | public void onDown(MotionElement mElement) { 408 | mPaint.setXfermode(null); 409 | mPath = new Path(); 410 | mPointList.clear(); 411 | mHWPointList.clear(); 412 | //记录down的控制点的信息 413 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 414 | //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇, 415 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 416 | mLastWidth = mElement.pressure * mBaseWidth; 417 | } else { 418 | //如果是手指画的,我们取他的0.8 419 | mLastWidth = 0.8 * mBaseWidth; 420 | } 421 | //down下的点的宽度 422 | curPoint.width = (float) mLastWidth; 423 | mLastVel = 0; 424 | 425 | mPointList.add(curPoint); 426 | //记录当前的点 427 | mLastPoint = curPoint; 428 | //绘制起点 429 | mPath.moveTo(mElement.x, mElement.y); 430 | } 431 | 432 | #### Move事件的处理 433 | 434 | public void onMove(MotionElement mElement) { 435 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 436 | 437 | double deltaX = curPoint.x - mLastPoint.x; 438 | double deltaY = curPoint.y - mLastPoint.y; 439 | //deltaX和deltay平方和的二次方根 想象一个例子 1+1的平方根为1.4 (x²+y²)开根号 440 | double curDis = Math.hypot(deltaX, deltaY); 441 | //我们求出的这个值越小,画的点或者是绘制椭圆形越多,这个值越大的话,绘制的越少,笔就越细,宽度越小 442 | double curVel = curDis * DIS_VEL_CAL_FACTOR; 443 | System.out.println("shiming==="+curDis+" "+curVel+" "+deltaX+" "+deltaY); 444 | double curWidth; 445 | //点的集合少,我们得必须改变宽度,每次点击的down的时候,这个事件 446 | if (mPointList.size() < 2) { 447 | System.out.println("shiming==dian shao"); 448 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 449 | curWidth = mElement.pressure * mBaseWidth; 450 | } else { 451 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 452 | mLastWidth); 453 | } 454 | curPoint.width = (float) curWidth; 455 | mBezier.Init(mLastPoint, curPoint); 456 | } else { 457 | System.out.println("shiming==dian duo"); 458 | mLastVel = curVel; 459 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 460 | curWidth = mElement.pressure * mBaseWidth; 461 | } else { 462 | //由于我们手机是触屏的手机,滑动的速度也不慢,所以,一般会走到这里来 463 | //阐明一点,当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着mlastWidth不断的变换 464 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 465 | mLastWidth); 466 | System.out.println("shiming=="+curVel+" "+mLastVel+" "+curDis+" " +mLastWidth); 467 | System.out.println("shiming==dian duo"+curWidth); 468 | } 469 | curPoint.width = (float) curWidth; 470 | mBezier.AddNode(curPoint); 471 | } 472 | //每次移动的话,这里赋值新的值 473 | mLastWidth = curWidth; 474 | 475 | mPointList.add(curPoint); 476 | 477 | int steps = 1 + (int) curDis / STEPFACTOR; 478 | System.out.println("shiming-- steps"+steps); 479 | double step = 1.0 / steps; 480 | for (double t = 0; t < 1.0; t += step) { 481 | ControllerPoint point = mBezier.GetPoint(t); 482 | mHWPointList.add(point); 483 | } 484 | 485 | mPath.quadTo(mLastPoint.x, mLastPoint.y, 486 | (mElement.x + mLastPoint.x) / 2, 487 | (mElement.y + mLastPoint.y) / 2); 488 | 489 | mLastPoint = curPoint; 490 | } 491 | 492 | * Up事件的处理:当需要关心我们画的这个bitmap的时候,记得在up结束的时候,需要把这个绘制的东西需要重新绘制到我们自定义View的画布上,这个画笔是自己定义的,而不是View里面onDraw( 493 | cavns)里面的画布 494 | 495 | public void onUp(MotionElement mElement, Canvas canvas) { 496 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 497 | double deltaX = curPoint.x - mLastPoint.x; 498 | double deltaY = curPoint.y - mLastPoint.y; 499 | double curDis = Math.hypot(deltaX, deltaY); 500 | 501 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 502 | curPoint.width = (float) (mElement.pressure * mBaseWidth); 503 | } else { 504 | curPoint.width = 0; 505 | } 506 | 507 | mPointList.add(curPoint); 508 | 509 | mBezier.AddNode(curPoint); 510 | 511 | int steps = 1 + (int) curDis / STEPFACTOR; 512 | double step = 1.0 / steps; 513 | for (double t = 0; t < 1.0; t += step) { 514 | ControllerPoint point = mBezier.GetPoint(t); 515 | mHWPointList.add(point); 516 | } 517 | // 518 | mBezier.End(); 519 | for (double t = 0; t < 1.0; t += step) { 520 | ControllerPoint point = mBezier.GetPoint(t); 521 | mHWPointList.add(point); 522 | } 523 | 524 | mPath.quadTo(mLastPoint.x, mLastPoint.y, 525 | (mElement.x + mLastPoint.x) / 2, 526 | (mElement.y + mLastPoint.y) / 2); 527 | mPath.lineTo(mElement.x, mElement.y); 528 | // 手指up 我画到纸上上 529 | draw(canvas); 530 | 531 | } 532 | 533 | * 其实这里才是关键的地方,通过画布画椭圆,每一个点都是一个椭圆,这个椭圆的所有细节,逐渐构建出一个完美的笔尖 534 | 和笔锋的效果,我觉得在这里需要大量的测试,其实就对低端手机进行排查,看我们绘制的笔的宽度是多少,绘制多少个椭圆然后在低端手机上不会那么卡,当然你哪一个N年前的手机给我,那也的卡,只不过需要适中的范围里面 535 | 536 | private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint){ 537 | double curDis = Math.hypot(x0-x1, y0-y1); 538 | int steps = 1; 539 | if(paint.getStrokeWidth() < 6){ 540 | steps = 1+(int)(curDis/2); 541 | }else if(paint.getStrokeWidth() > 60){ 542 | steps = 1+(int)(curDis/4); 543 | }else{ 544 | steps = 1+(int)(curDis/3); 545 | } 546 | double deltaX=(x1-x0)/steps; 547 | double deltaY=(y1-y0)/steps; 548 | double deltaW=(w1-w0)/steps; 549 | double x=x0; 550 | double y=y0; 551 | double w=w0; 552 | 553 | for(int i=0;i 577 | mMinimumVelocity那么我们的页面就需要翻页了,下面是ViewPager的实现的代码,很明显就知道 578 | 579 | final float density = context.getResources().getDisplayMetrics().density; 580 | 581 | mTouchSlop = configuration.getScaledPagingTouchSlop(); 582 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 583 | 584 | 585 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 586 | int targetPage; 587 | if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 588 | targetPage = velocity > 0 ? currentPage : currentPage + 1; 589 | } else { 590 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; 591 | targetPage = currentPage + (int) (pageOffset + truncator); 592 | } 593 | 594 | if (mItems.size() > 0) { 595 | final ItemInfo firstItem = mItems.get(0); 596 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 597 | 598 | // Only let the user target pages we have items for 599 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 600 | } 601 | 602 | return targetPage; 603 | } 604 | 605 | * 606 | 607 | 我就一直纠结啊,这种可以啊,没毛病啊,老铁,我就一直做啊做,实现的效果,就是一直Move事件中的笔的宽度都是一样的,是不是崩溃啊,的确很崩溃,最后我在想,能不能拿到按压值MotionEvent.getPressure() 608 | ;但是最后通过一查,这个方法的返回值是这样决定的:感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的,我一直用手写,这个值永远不变,奔溃,又一次崩溃,最后在研究一个opengl写的Demo的时候,我发现了一个真理:那就是,我要画多长,是用户手指决定的,但是它的Move事件中接受到的点的数量是和这个距离没有相对应的关系,啊哈哈,对不对,我接受了这个多点,但是我要画很长的线,是不是我的线就细了,但Move中的接受到的点数量一样,我画的距离短了,是不是线就粗了,这就是这个Demo的原理,顿时豁然开朗,春暖花开! 609 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/app-release.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion "29.0.2" 6 | defaultConfig { 7 | applicationId "com.shiming.pen" 8 | minSdkVersion 26 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner 'androidx.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 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('androidx.test.espresso:espresso-core:3.1.0', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | implementation 'androidx.appcompat:appcompat:1.1.0' 28 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha1' 29 | testImplementation '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:\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/shiming/pen/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.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.shiming.pen", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/Bezier.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen; 2 | 3 | import com.shiming.pen.old_code.ControllerPoint; 4 | 5 | /** 6 | * @author shiming 7 | * @version v1.0 create at 2017/8/24 8 | * @des 对点的位置和宽度控制的bezier曲线,主要是两个点,都包含了宽度和点的坐标 9 | */ 10 | public class Bezier { 11 | //控制点的, 12 | private ControllerPoint mControl = new ControllerPoint(); 13 | //距离 14 | private ControllerPoint mDestination = new ControllerPoint(); 15 | //下一个需要控制点 16 | private ControllerPoint mNextControl = new ControllerPoint(); 17 | //资源的点 18 | private ControllerPoint mSource = new ControllerPoint(); 19 | 20 | public Bezier() { 21 | } 22 | 23 | /** 24 | * 初始化两个点, 25 | * 26 | * @param last 最后的点的信息 27 | * @param cur 当前点的信息,当前点的信息,当前点的是根据事件获得,同时这个当前点的宽度是经过计算的得出的 28 | */ 29 | public void init(ControllerPoint last, ControllerPoint cur) { 30 | init(last.x, last.y, last.width, cur.x, cur.y, cur.width); 31 | } 32 | 33 | public void init(float lastx, float lasty, float lastWidth, float x, float y, float width) { 34 | //资源点设置,最后的点的为资源点 35 | mSource.set(lastx, lasty, lastWidth); 36 | float xmid = getMid(lastx, x); 37 | float ymid = getMid(lasty, y); 38 | float wmid = getMid(lastWidth, width); 39 | //距离点为平均点 40 | mDestination.set(xmid, ymid, wmid); 41 | //控制点为当前的距离点 42 | mControl.set(getMid(lastx, xmid), getMid(lasty, ymid), getMid(lastWidth, wmid)); 43 | //下个控制点为当前点 44 | mNextControl.set(x, y, width); 45 | } 46 | 47 | public void addNode(ControllerPoint cur) { 48 | addNode(cur.x, cur.y, cur.width); 49 | } 50 | 51 | /** 52 | * 替换就的点,原来的距离点变换为资源点,控制点变为原来的下一个控制点,距离点取原来控制点的和新的的一半 53 | * 下个控制点为新的点 54 | * 55 | * @param x 新的点的坐标 56 | * @param y 新的点的坐标 57 | * @param width 58 | */ 59 | public void addNode(float x, float y, float width) { 60 | mSource.set(mDestination); 61 | mControl.set(mNextControl); 62 | mDestination.set(getMid(mNextControl.x, x), getMid(mNextControl.y, y), getMid(mNextControl.width, width)); 63 | mNextControl.set(x, y, width); 64 | } 65 | 66 | /** 67 | * 结合手指抬起来的动作,告诉现在的曲线控制点也必须变化,其实在这里也不需要结合着up事件使用 68 | * 因为在down的事件中,所有点都会被重置,然后设置这个没有多少意义,但是可以改变下个事件的朝向改变 69 | * 先留着,因为后面如果需要控制整个颜色的改变的话,我的依靠这个方法,还有按压的时间的变化 70 | */ 71 | public void end() { 72 | mSource.set(mDestination); 73 | float x = getMid(mNextControl.x, mSource.x); 74 | float y = getMid(mNextControl.y, mSource.y); 75 | float w = getMid(mNextControl.width, mSource.width); 76 | mControl.set(x, y, w); 77 | mDestination.set(mNextControl); 78 | } 79 | 80 | /** 81 | * @param t 孔子 82 | * @return 83 | */ 84 | public ControllerPoint getPoint(double t) { 85 | float x = (float) getX(t); 86 | float y = (float) getY(t); 87 | float w = (float) getW(t); 88 | ControllerPoint point = new ControllerPoint(); 89 | point.set(x, y, w); 90 | return point; 91 | } 92 | 93 | /** 94 | * 三阶曲线的控制点 95 | * 96 | * @param p0 97 | * @param p1 98 | * @param p2 99 | * @param t 100 | * @return 101 | */ 102 | private double getValue(double p0, double p1, double p2, double t) { 103 | double A = p2 - 2 * p1 + p0; 104 | double B = 2 * (p1 - p0); 105 | double C = p0; 106 | return A * t * t + B * t + C; 107 | } 108 | 109 | private double getX(double t) { 110 | return getValue(mSource.x, mControl.x, mDestination.x, t); 111 | } 112 | 113 | private double getY(double t) { 114 | return getValue(mSource.y, mControl.y, mDestination.y, t); 115 | } 116 | 117 | private double getW(double t) { 118 | return getWidth(mSource.width, mDestination.width, t); 119 | } 120 | 121 | /** 122 | * @param x1 一个点的x 123 | * @param x2 一个点的x 124 | * @return 125 | */ 126 | private float getMid(float x1, float x2) { 127 | return (float) ((x1 + x2) / 2.0); 128 | } 129 | 130 | private double getWidth(double w0, double w1, double t) { 131 | return w0 + (w1 - w0) * t; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import com.shiming.pen.field_character.FieldCharacterShapeActivity; 11 | import com.shiming.pen.view.OldDemoActivity; 12 | 13 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 14 | 15 | private Button mBtnStrokePen; 16 | private Button mBrushPen; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | findViews(); 23 | doSomeThing(); 24 | } 25 | 26 | private void doSomeThing() { 27 | mBtnStrokePen.setOnClickListener(this); 28 | mBrushPen.setOnClickListener(this); 29 | } 30 | 31 | private void findViews() { 32 | mBtnStrokePen = (Button) findViewById(R.id.btn_stroke_pen); 33 | mBrushPen = (Button) findViewById(R.id.btn_brush_pen); 34 | 35 | } 36 | 37 | @Override 38 | public void onClick(View v) { 39 | Intent intent = null; 40 | switch (v.getId()) { 41 | case R.id.btn_stroke_pen://oldDemo 42 | intent = new Intent(MainActivity.this, OldDemoActivity.class); 43 | startActivity(intent); 44 | break; 45 | case R.id.btn_brush_pen://田字格的Demo 46 | intent = new Intent(MainActivity.this, FieldCharacterShapeActivity.class); 47 | startActivity(intent); 48 | break; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/BitmapDrawUtils.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Matrix; 5 | 6 | 7 | public class BitmapDrawUtils { 8 | /** 9 | * 图片缩放 10 | * 11 | * @param originalBitmap 原始的Bitmap 12 | * @param newWidth 自定义宽度 13 | * @return 缩放后的Bitmap 14 | */ 15 | public static Bitmap resizeImage(Bitmap originalBitmap, int newWidth, int newHeight) { 16 | if (originalBitmap == null || originalBitmap.getWidth() == 0 || originalBitmap.getHeight() == 0) { 17 | return null; 18 | } 19 | int width = originalBitmap.getWidth(); 20 | int height = originalBitmap.getHeight(); 21 | //定义欲转换成的宽、高 22 | //计算宽、高缩放率 23 | float scanleWidth = (float) newWidth / width; 24 | float scanleHeight = (float) newHeight / height; 25 | //创建操作图片用的matrix对象 Matrix 26 | Matrix matrix = new Matrix(); 27 | matrix.postScale(scanleWidth, scanleHeight); 28 | // 创建新的图片Bitmap 29 | Bitmap resizedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, width, height, matrix, true); 30 | return resizedBitmap; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/DeletableEditText.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.util.AttributeSet; 6 | import android.view.KeyEvent; 7 | import android.view.inputmethod.EditorInfo; 8 | import android.view.inputmethod.InputConnection; 9 | import android.view.inputmethod.InputConnectionWrapper; 10 | import android.widget.EditText; 11 | 12 | /** 13 | * 这个是从stackOverFlow上面找到的解决方案,主要用途是处理软键盘回删按钮backSpace时回调OnKeyListener 14 | */ 15 | @SuppressLint("AppCompatCustomView") 16 | public class DeletableEditText extends EditText { 17 | 18 | public DeletableEditText(Context context, AttributeSet attrs, int defStyle) { 19 | super(context, attrs, defStyle); 20 | } 21 | 22 | public DeletableEditText(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | public DeletableEditText(Context context) { 27 | super(context); 28 | } 29 | 30 | @Override 31 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 32 | return new DeleteInputConnection(super.onCreateInputConnection(outAttrs), 33 | true); 34 | } 35 | 36 | private class DeleteInputConnection extends InputConnectionWrapper { 37 | public DeleteInputConnection(InputConnection target, boolean mutable) { 38 | super(target, mutable); 39 | } 40 | 41 | @Override 42 | public boolean sendKeyEvent(KeyEvent event) { 43 | return super.sendKeyEvent(event); 44 | } 45 | 46 | @Override 47 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { 48 | if (beforeLength == 1 && afterLength == 0) { 49 | return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 50 | KeyEvent.KEYCODE_DEL)) 51 | && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, 52 | KeyEvent.KEYCODE_DEL)); 53 | } 54 | return super.deleteSurroundingText(beforeLength, afterLength); 55 | } 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/DrawPenView.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PaintFlagsDrawFilter; 10 | import android.graphics.PorterDuff; 11 | import android.graphics.PorterDuffXfermode; 12 | import android.util.AttributeSet; 13 | import android.util.DisplayMetrics; 14 | import android.util.Log; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | 18 | import com.shiming.pen.old_code.StrokePen; 19 | 20 | 21 | /** 22 | * @author shiming 23 | * @version v1.0 create at 2017/8/24 24 | * @des DrawPenView实现手写关键类,目前只提供了,手绘的功能和清除画布,后期根据业务逻辑可以动态的设置方法 25 | */ 26 | public class DrawPenView extends View { 27 | private static final String TAG = "DrawPenView"; 28 | private Paint mPaint;//画笔 29 | private Canvas mCanvas;//画布 30 | private Bitmap mBitmap; 31 | public static final int CANVAS_NORMAL = 0; 32 | public static final int CANVAS_RESET = 1;//全部清除 33 | private StrokePen mVisualStrokePen; 34 | private Context mContext; 35 | public static int mCanvasCode = CANVAS_NORMAL; 36 | private String mPaintColor; 37 | private int mSize; 38 | private boolean mIsCanvasDraw; 39 | 40 | public DrawPenView(Context context) { 41 | this(context, null); 42 | } 43 | 44 | public DrawPenView(Context context, AttributeSet attrs) { 45 | this(context, attrs, 0); 46 | } 47 | 48 | public DrawPenView(Context context, AttributeSet attrs, int defStyleAttr) { 49 | super(context, attrs, defStyleAttr); 50 | initParameter(context); 51 | } 52 | 53 | public void setCanvasCode(int canvasCode) { 54 | mCanvasCode = canvasCode; 55 | invalidate(); 56 | } 57 | 58 | private void initParameter(Context context) { 59 | mContext = context; 60 | DisplayMetrics dm = new DisplayMetrics(); 61 | ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); 62 | int i = dp2px(mContext, 40); 63 | int i1 = dp2px(mContext, 280); 64 | mBitmap = Bitmap.createBitmap(dm.widthPixels - i - i, i1, Bitmap.Config.ARGB_8888); 65 | mVisualStrokePen = new StrokePen(mContext); 66 | initPaint(); 67 | initCanvas(); 68 | } 69 | 70 | public static int dp2px(Context context, float dpValue) { 71 | final float scale = context.getResources().getDisplayMetrics().density; 72 | return (int) (dpValue * scale + 0.5f); 73 | } 74 | 75 | private void initPaint() { 76 | mSize = 65; 77 | mPaintColor = "#3B3635"; 78 | mPaint = new Paint(); 79 | mPaint.setColor(Color.parseColor(mPaintColor)); 80 | mPaint.setStrokeWidth(mSize); 81 | mPaint.setStyle(Paint.Style.STROKE); 82 | mPaint.setStrokeCap(Paint.Cap.ROUND);//结束的笔画为圆心 83 | mPaint.setStrokeJoin(Paint.Join.ROUND);//连接处元 84 | mPaint.setAlpha(0xFF); 85 | mPaint.setAntiAlias(true); 86 | mPaint.setStrokeMiter(1.0f); 87 | mPaint.setFilterBitmap(true); 88 | mVisualStrokePen.setPaint(mPaint); 89 | } 90 | 91 | private void initCanvas() { 92 | mCanvas = new Canvas(mBitmap); 93 | mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG)); 94 | //设置画布的颜色的问题 95 | mCanvas.drawColor(Color.TRANSPARENT); 96 | } 97 | 98 | public void changePaintColor(int color) { 99 | mPaint.setColor(color); 100 | } 101 | 102 | /** 103 | * 当设置完成了,需要告诉其他的控件我们的笔的宽度发生了改变 104 | * 105 | * @param paintColor 颜色 106 | * @param width 宽度 107 | */ 108 | public void changePaintSize(String paintColor, float width) { 109 | mPaint.setStrokeWidth(width); 110 | mPaint.setColor(Color.parseColor(paintColor)); 111 | mVisualStrokePen.setPaint(mPaint); 112 | } 113 | 114 | 115 | @Override 116 | protected void onDraw(Canvas canvas) { 117 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 118 | switch (mCanvasCode) { 119 | case CANVAS_NORMAL: 120 | mVisualStrokePen.draw(canvas); 121 | break; 122 | case CANVAS_RESET: 123 | reset(); 124 | break; 125 | default: 126 | Log.e(TAG, "onDraw" + Integer.toString(mCanvasCode)); 127 | break; 128 | } 129 | super.onDraw(canvas); 130 | } 131 | 132 | @Override 133 | public boolean onTouchEvent(MotionEvent event) { 134 | mIsCanvasDraw = true; 135 | //测试过程中,当使用到event的时候,产生了没有收到事件的问题,所以在这里需要obtian的一下 136 | MotionEvent event2 = MotionEvent.obtain(event); 137 | switch (event2.getActionMasked()) { 138 | case MotionEvent.ACTION_DOWN: 139 | //每次都是这个笔,因为项目里面就只有这个笔,如果多了,这里需要改动 140 | setCanvasCode(CANVAS_NORMAL); 141 | mVisualStrokePen.onDown(mVisualStrokePen.createMotionElement(event2)); 142 | mGetTimeListner.stopTime(); 143 | break; 144 | case MotionEvent.ACTION_MOVE: 145 | mVisualStrokePen.onMove(mVisualStrokePen.createMotionElement(event2)); 146 | mGetTimeListner.stopTime(); 147 | break; 148 | case MotionEvent.ACTION_UP: 149 | long time = System.currentTimeMillis(); 150 | mGetTimeListner.getTime(time); 151 | mVisualStrokePen.onUp(mVisualStrokePen.createMotionElement(event2), mCanvas); 152 | break; 153 | default: 154 | break; 155 | } 156 | invalidate(); 157 | return true; 158 | } 159 | 160 | /** 161 | * 清除画布,记得清除点的集合 162 | */ 163 | public void reset() { 164 | mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 165 | mCanvas.drawPaint(mPaint); 166 | mPaint.setXfermode(null); 167 | mIsCanvasDraw = false; 168 | mVisualStrokePen.clear(); 169 | } 170 | 171 | /** 172 | * @return 判断是否有绘制内容在画布上 173 | */ 174 | public boolean getHasDraw() { 175 | return mIsCanvasDraw; 176 | } 177 | 178 | public void setCurrentState(int currentState) { 179 | mCanvasCode = currentState; 180 | } 181 | 182 | private int mBackColor = Color.TRANSPARENT; 183 | 184 | /** 185 | * 逐行扫描 清楚边界空白。功能是生成一张bitmap位于正中间,不是位于顶部,此关键的是我们画布需要 186 | * 成透明色才能生效 187 | * 188 | * @param blank 边距留多少个像素 189 | * @return tks github E-signature 190 | */ 191 | public Bitmap clearBlank(int blank) { 192 | if (mBitmap != null) { 193 | int HEIGHT = mBitmap.getHeight();//1794 194 | int WIDTH = mBitmap.getWidth();//1080 195 | int top = 0, left = 0, right = 0, bottom = 0; 196 | int[] pixs = new int[WIDTH]; 197 | boolean isStop; 198 | for (int y = 0; y < HEIGHT; y++) { 199 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 200 | isStop = false; 201 | for (int pix : pixs) { 202 | if (pix != mBackColor) { 203 | 204 | top = y; 205 | isStop = true; 206 | break; 207 | } 208 | } 209 | if (isStop) { 210 | break; 211 | } 212 | } 213 | for (int y = HEIGHT - 1; y >= 0; y--) { 214 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 215 | isStop = false; 216 | for (int pix : pixs) { 217 | if (pix != mBackColor) { 218 | bottom = y; 219 | isStop = true; 220 | break; 221 | } 222 | } 223 | if (isStop) { 224 | break; 225 | } 226 | } 227 | pixs = new int[HEIGHT]; 228 | for (int x = 0; x < WIDTH; x++) { 229 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 230 | isStop = false; 231 | for (int pix : pixs) { 232 | if (pix != mBackColor) { 233 | left = x; 234 | isStop = true; 235 | break; 236 | } 237 | } 238 | if (isStop) { 239 | break; 240 | } 241 | } 242 | for (int x = WIDTH - 1; x > 0; x--) { 243 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 244 | isStop = false; 245 | for (int pix : pixs) { 246 | if (pix != mBackColor) { 247 | right = x; 248 | isStop = true; 249 | break; 250 | } 251 | } 252 | if (isStop) { 253 | break; 254 | } 255 | } 256 | if (blank < 0) { 257 | blank = 0; 258 | } 259 | left = left - blank > 0 ? left - blank : 0; 260 | top = top - blank > 0 ? top - blank : 0; 261 | right = right + blank > WIDTH - 1 ? WIDTH - 1 : right + blank; 262 | bottom = bottom + blank > HEIGHT - 1 ? HEIGHT - 1 : bottom + blank; 263 | return Bitmap.createBitmap(mBitmap, left, top, right - left, bottom - top); 264 | } else { 265 | return null; 266 | } 267 | } 268 | 269 | public Bitmap getBitmap() { 270 | return mBitmap; 271 | } 272 | 273 | public TimeListener mGetTimeListner; 274 | 275 | public void setGetTimeListener(TimeListener l) { 276 | mGetTimeListner = l; 277 | } 278 | 279 | public interface TimeListener { 280 | void getTime(long l); 281 | 282 | void stopTime(); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/DrawViewLayout.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.util.AttributeSet; 6 | import android.view.LayoutInflater; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.view.ViewStub; 10 | import android.widget.FrameLayout; 11 | import android.widget.ImageView; 12 | import android.widget.RelativeLayout; 13 | import android.widget.Toast; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.annotation.Nullable; 17 | 18 | import com.shiming.pen.R; 19 | import com.shiming.pen.new_code.IPenConfig; 20 | import com.shiming.pen.new_code.NewDrawPenView; 21 | 22 | import static com.shiming.pen.new_code.IPenConfig.STROKE_TYPE_ERASER; 23 | 24 | 25 | /** 26 | * @author shiming 27 | * @version v1.0 create at 2017/8/24 28 | * @des DrawViewLayout的一些封装 后续优化的点是:页面不初始化,尽量等着用户来选择 29 | */ 30 | public class DrawViewLayout extends FrameLayout implements View.OnClickListener, View.OnLongClickListener { 31 | 32 | private RelativeLayout mShowKeyboard; 33 | private RelativeLayout mGotoPreviousStep; 34 | private RelativeLayout mClearCanvas; 35 | private NewDrawPenView mDrawView; 36 | private RelativeLayout mSaveBitmap; 37 | private ViewStub mViewStub; 38 | private View mChild; 39 | private Context mContext; 40 | private ImageView mUpOrDownIcon; 41 | private LayoutInflater mInflater; 42 | private int mPenConfig; 43 | private boolean mIsShowKeyB; 44 | 45 | public DrawViewLayout(@NonNull Context context) { 46 | this(context, null); 47 | } 48 | 49 | 50 | public DrawViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 51 | super(context, attrs); 52 | mContext = context; 53 | initView(); 54 | 55 | } 56 | 57 | private void initView() { 58 | mInflater = LayoutInflater.from(getContext()); 59 | mChild = mInflater.inflate(R.layout.brush_weight_layout, this, false); 60 | addView(mChild); 61 | mShowKeyboard = (RelativeLayout) findViewById(R.id.rll_show_keyb_container); 62 | mGotoPreviousStep = (RelativeLayout) findViewById(R.id.rll_show_space_container);//空格 63 | mClearCanvas = (RelativeLayout) findViewById(R.id.rll_show_newline_container); 64 | mSaveBitmap = (RelativeLayout) findViewById(R.id.rll_show_delete_container); 65 | mViewStub = (ViewStub) findViewById(R.id.draw_view); 66 | //需要关心的selector的id 67 | mUpOrDownIcon = (ImageView) findViewById(R.id.rll_show_keyb_container_icon); 68 | setOnClickListenerT(); 69 | } 70 | 71 | 72 | @SuppressLint("ClickableViewAccessibility") 73 | private void setOnClickListenerT() { 74 | mShowKeyboard.setOnClickListener(this); 75 | mGotoPreviousStep.setOnClickListener(this); 76 | mClearCanvas.setOnClickListener(this); 77 | mSaveBitmap.setOnClickListener(this); 78 | mSaveBitmap.setOnLongClickListener(this); 79 | mSaveBitmap.setOnTouchListener(new OnTouchListener() { 80 | @Override 81 | public boolean onTouch(View v, MotionEvent event) { 82 | if (event.getAction() == MotionEvent.ACTION_UP) { 83 | Executor.INSTANCE.stop(); 84 | } 85 | return false; 86 | } 87 | }); 88 | } 89 | 90 | private void setDrawViewConfig() { 91 | mDrawView = (NewDrawPenView) findViewById(R.id.myglsurface_view); 92 | mDrawView.setCanvasCode(IPenConfig.STROKE_TYPE_BRUSH); 93 | mPenConfig = IPenConfig.STROKE_TYPE_BRUSH; 94 | mDrawView.setPenconfig(mPenConfig); 95 | mDrawView.setGetTimeListener(new NewDrawPenView.TimeListener() { 96 | @Override 97 | public void getTime(long l) { 98 | mIActionCallback.getUptime(l); 99 | } 100 | 101 | @Override 102 | public void stopTime() { 103 | mIActionCallback.stopTime(); 104 | } 105 | }); 106 | } 107 | 108 | 109 | @Override 110 | public void onClick(View v) { 111 | int id = v.getId(); 112 | switch (id) { 113 | case R.id.rll_show_keyb_container: 114 | showOrHideMySurfaceView(); 115 | break; 116 | case R.id.rll_show_space_container: 117 | Toast.makeText(mContext, "增加空格", Toast.LENGTH_SHORT).show(); 118 | mIActionCallback.needSpace(); 119 | break; 120 | case R.id.rll_show_newline_container: 121 | Toast.makeText(mContext, "换行", Toast.LENGTH_SHORT).show(); 122 | mIActionCallback.creatNewLine(); 123 | break; 124 | case R.id.rll_show_delete_container: 125 | Toast.makeText(mContext, "删除或者长按删除", Toast.LENGTH_SHORT).show(); 126 | mIActionCallback.deleteOnClick(); 127 | break; 128 | } 129 | } 130 | 131 | /** 132 | * 使用Viewstub的在不需要弹出键盘的时候,渲染不占内存不 133 | */ 134 | private void showOrHideMySurfaceView() { 135 | if (mViewStub.getParent() != null) { 136 | mViewStub.inflate(); 137 | } 138 | if (mDrawView == null) { 139 | setDrawViewConfig(); 140 | } 141 | if (mDrawView.getVisibility() == GONE) { 142 | mIsShowKeyB = true; 143 | mViewStub.setVisibility(VISIBLE); 144 | mUpOrDownIcon.setSelected(true); 145 | Toast.makeText(mContext, "显示键盘", Toast.LENGTH_SHORT).show(); 146 | mDrawView.setVisibility(VISIBLE); 147 | } else if (mDrawView.getVisibility() == VISIBLE) { 148 | mIsShowKeyB = false; 149 | Toast.makeText(mContext, "隐藏键盘", Toast.LENGTH_SHORT).show(); 150 | mDrawView.setVisibility(GONE); 151 | mViewStub.setVisibility(GONE); 152 | mUpOrDownIcon.setSelected(false); 153 | } 154 | 155 | } 156 | 157 | 158 | public void clearScreen() { 159 | if (mDrawView == null) return; 160 | mDrawView.setCanvasCode(STROKE_TYPE_ERASER);//z注意变量的来源 161 | } 162 | 163 | public void showBk() { 164 | if (!getIsShowKeyB()) { 165 | if (mViewStub.getParent() != null) { 166 | mViewStub.inflate(); 167 | } 168 | if (mDrawView == null) { 169 | setDrawViewConfig(); 170 | } 171 | mIsShowKeyB = true; 172 | mViewStub.setVisibility(VISIBLE); 173 | mUpOrDownIcon.setSelected(true); 174 | mIActionCallback.showkeyB(true); 175 | mDrawView.setVisibility(VISIBLE); 176 | } 177 | } 178 | 179 | 180 | public IActionCallback mIActionCallback; 181 | 182 | public void setActionCallback(IActionCallback a) { 183 | mIActionCallback = a; 184 | } 185 | 186 | public boolean getIsShowKeyB() { 187 | return mIsShowKeyB; 188 | } 189 | 190 | 191 | /** 192 | * 长按事件的启动定时器 193 | * 194 | * @param v 195 | * @return 196 | */ 197 | @Override 198 | public boolean onLongClick(View v) { 199 | Executor.INSTANCE.setCallback(mIActionCallback); 200 | Executor.INSTANCE.upData(v.getId()); 201 | return true; 202 | } 203 | 204 | public NewDrawPenView getSaveBitmap() { 205 | return mDrawView; 206 | } 207 | 208 | public int getPenConfig() { 209 | return mPenConfig; 210 | } 211 | 212 | public void setPenConfig(int penConfig) { 213 | mDrawView.setCanvasCode(penConfig); 214 | mPenConfig = penConfig; 215 | } 216 | 217 | public interface IActionCallback { 218 | 219 | void creatNewLine(); 220 | 221 | void getUptime(long l); 222 | 223 | void stopTime(); 224 | 225 | void needSpace(); 226 | 227 | 228 | void deleteOnClick(); 229 | 230 | void showkeyB(boolean flag); 231 | 232 | void deleteOnLongClick(); 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/Executor.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | 7 | import com.shiming.pen.R; 8 | 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | 14 | /** 15 | * @author shiming 16 | * @version v1.0 create at 2017/9/13 17 | * @des 使用模式为enum的单利模式 18 | */ 19 | public enum Executor { 20 | INSTANCE; 21 | private ScheduledExecutorService mScheduledExecutorService; 22 | private DrawViewLayout.IActionCallback mCallback; 23 | 24 | public void setCallback(DrawViewLayout.IActionCallback callback) { 25 | mCallback = callback; 26 | } 27 | 28 | @SuppressLint("HandlerLeak") 29 | private Handler handler = new Handler() { 30 | @Override 31 | public void handleMessage(Message msg) { 32 | int viewId = msg.what; 33 | switch (viewId) { 34 | case R.id.rll_show_delete_container: 35 | if (mCallback == null) 36 | return; 37 | mCallback.deleteOnLongClick(); 38 | break; 39 | } 40 | } 41 | }; 42 | 43 | public void upData(int id) { 44 | final int vid = id; 45 | //只有一个线程,用来调度执行将来的任务 46 | mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 47 | //多少时间执行一次 48 | mScheduledExecutorService.scheduleWithFixedDelay(new Runnable() { 49 | @Override 50 | public void run() { 51 | Message msg = new Message(); 52 | msg.what = vid; 53 | handler.sendMessage(msg); 54 | } 55 | }, 0, 100, TimeUnit.MILLISECONDS); //每间隔100ms发送Message 56 | } 57 | 58 | public void stop() { 59 | if (mScheduledExecutorService != null) { 60 | mScheduledExecutorService.shutdownNow(); 61 | mScheduledExecutorService = null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/FieldCharacterShapeActivity.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | 4 | import android.Manifest; 5 | import android.annotation.SuppressLint; 6 | import android.content.pm.PackageManager; 7 | import android.graphics.Bitmap; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.drawable.ColorDrawable; 11 | import android.os.Bundle; 12 | import android.os.Environment; 13 | import android.os.Handler; 14 | import android.os.Message; 15 | import android.text.Editable; 16 | import android.text.Spannable; 17 | import android.text.SpannableString; 18 | import android.text.style.ImageSpan; 19 | import android.util.DisplayMetrics; 20 | import android.view.View; 21 | import android.widget.Button; 22 | import android.widget.EditText; 23 | import android.widget.Toast; 24 | 25 | import androidx.annotation.Nullable; 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.core.app.ActivityCompat; 28 | import androidx.core.content.ContextCompat; 29 | 30 | import com.shiming.pen.R; 31 | import com.shiming.pen.new_code.IPenConfig; 32 | import com.shiming.pen.new_code.NewDrawPenView; 33 | 34 | import java.io.File; 35 | import java.util.Timer; 36 | import java.util.TimerTask; 37 | 38 | 39 | /** 40 | * author: Created by shiming on 2018/1/20 15:08 41 | * mailbox:lamshiming@sina.com 42 | * 田字格的Demo 43 | */ 44 | public class FieldCharacterShapeActivity extends AppCompatActivity implements DrawViewLayout.IActionCallback, View.OnClickListener { 45 | private DrawViewLayout mDrawViewLayout; 46 | private Bitmap mBitmap; 47 | private Bitmap mBitmapResize; 48 | private HandRichTextEditor mRetContent; 49 | private long mOldTime; 50 | /** 51 | * 数据库Id 52 | */ 53 | private long draftId = 0L; 54 | /** 55 | * 图片命名 56 | */ 57 | private static String full_name = ""; 58 | private static final String LAST_NAME = "word_"; 59 | /** 60 | * 文件保存的路径 61 | */ 62 | private String mPath = null; 63 | private boolean mIsCreateBitmap = false; 64 | private Bitmap mCreatBimap; 65 | /** 66 | * 自动保存Timer 67 | */ 68 | private Timer mTimerSave; 69 | /** 70 | * add shiming 手写体的生成图片的时间 71 | */ 72 | public static final int HADN_DRAW_TIME = 700; 73 | public static final String FONT_NAME_HEAD = "[font]"; 74 | public static final String FONT_NAME_TAIL = "[/font]"; 75 | public static int mAllHandDrawSize; 76 | public static int mEmotionSize; 77 | private Button mChangePen; 78 | 79 | @Override 80 | protected void onCreate(@Nullable Bundle savedInstanceState) { 81 | super.onCreate(savedInstanceState); 82 | setContentView(R.layout.activity_field_character_shape_layout); 83 | //这里两个值关系到手写的所有的一切 84 | DisplayMetrics dm = this.getResources().getDisplayMetrics(); 85 | mAllHandDrawSize = (int) (60.0 * dm.density); 86 | mEmotionSize = (int) (dm.density * 27.0); 87 | findViews(); 88 | mDrawViewLayout.setActionCallback(this); 89 | mDrawViewLayout.showBk(); 90 | initData(); 91 | audioSave(); 92 | } 93 | 94 | 95 | private void initData() { 96 | try { 97 | mPath = getHandPath(draftId); 98 | mRetContent.setPath(mPath); 99 | } catch (Exception e) { 100 | e.printStackTrace(); 101 | } 102 | } 103 | 104 | public static final String ROOT_PATH = File.separator + "cn.shiming.fieldcharactershap"; 105 | 106 | /** 107 | * 当前操作文件的保存路径 108 | */ 109 | public String getHandPath(long draftId) { 110 | String path = Environment.getExternalStorageDirectory().getPath() + ROOT_PATH + File.separator + "handdraw" + File.separator + "shiming" + File.separator + draftId + File.separator; 111 | return path; 112 | } 113 | 114 | public void audioSave() { 115 | mTimerSave = new Timer(); 116 | mTimerSave.schedule(task, 60000, 20000); 117 | } 118 | 119 | TimerTask task = new TimerTask() { 120 | @Override 121 | public void run() { 122 | Message msg = mHandler.obtainMessage(); 123 | msg.obj = false; 124 | mHandler.sendMessage(msg); 125 | } 126 | }; 127 | final Runnable runnable = new Runnable() { 128 | @Override 129 | public void run() { 130 | long l1 = System.currentTimeMillis(); 131 | if ((l1 - mOldTime) > HADN_DRAW_TIME) { 132 | mHandler.removeCallbacks(runnable); 133 | Message msg = mHandler.obtainMessage(); 134 | msg.obj = true; 135 | msg.what = 0x123; 136 | mHandler.sendMessage(msg); 137 | } else { 138 | mHandler.postDelayed(this, 100); 139 | } 140 | 141 | } 142 | }; 143 | @SuppressLint("HandlerLeak")//麻痹 144 | private Handler mHandler = new Handler() { 145 | @Override 146 | public void handleMessage(Message msg) { 147 | super.handleMessage(msg); 148 | int what = msg.what; 149 | switch (what) { 150 | case 0x123: 151 | try { 152 | boolean obj = (boolean) msg.obj; 153 | if (obj) { 154 | NewDrawPenView view = mDrawViewLayout.getSaveBitmap(); 155 | if (view != null) { 156 | //边距强行扫描 157 | mBitmap = view.clearBlank(100); 158 | mHandler.post(runnableUi); 159 | } 160 | } 161 | } catch (Exception e) { 162 | 163 | } finally { 164 | mHandler.removeCallbacks(runnable); 165 | } 166 | break; 167 | case 0x124: 168 | mRetContent.setVisibilityEdit(View.VISIBLE); 169 | mRetContent.setVisibilityClose(View.VISIBLE); 170 | mRetContent.getLastFocusEdit().setCursorVisible(true); 171 | mRetContent.getLastFocusEdit().requestFocus(); 172 | break; 173 | case 0x125: 174 | break; 175 | } 176 | } 177 | }; 178 | 179 | private void findViews() { 180 | mChangePen = (Button) findViewById(R.id.btn_change_pen); 181 | mRetContent = (HandRichTextEditor) findViewById(R.id.et_handdraw_content); 182 | mDrawViewLayout = (DrawViewLayout) findViewById(R.id.brush_weight); 183 | testStorage(); 184 | mRetContent.setOnHandRichEditTextHasFocus(new HandRichTextEditor.onHandRichEditTextHasFocus() { 185 | @Override 186 | public void hasFocus(View view) { 187 | mDrawViewLayout.showBk(); 188 | } 189 | 190 | @Override 191 | public void onClickChange(View v) { 192 | mDrawViewLayout.showBk(); 193 | } 194 | }); 195 | mChangePen.setOnClickListener(this); 196 | } 197 | 198 | private Bitmap creatBimap() { 199 | ColorDrawable drawable = new ColorDrawable(Color.TRANSPARENT); 200 | DisplayMetrics dm = new DisplayMetrics(); 201 | getWindowManager().getDefaultDisplay().getMetrics(dm); 202 | Bitmap bitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888); 203 | Canvas canvas = new Canvas(bitmap); 204 | drawable.draw(canvas); 205 | return bitmap; 206 | } 207 | 208 | Runnable runnableUi = new Runnable() { 209 | @Override 210 | public void run() { 211 | if (mIsCreateBitmap) { 212 | //110 213 | mBitmapResize = BitmapDrawUtils.resizeImage(mCreatBimap, mAllHandDrawSize, mAllHandDrawSize); 214 | mIsCreateBitmap = false; 215 | } else { 216 | mBitmapResize = BitmapDrawUtils.resizeImage(mBitmap, mAllHandDrawSize, mAllHandDrawSize); 217 | } 218 | if (mBitmapResize != null) { 219 | //根据Bitmap对象创建ImageSpan对象 220 | ImageSpan imageSpan = new ImageSpan(FieldCharacterShapeActivity.this, mBitmapResize); 221 | //创建一个SpannableString对象,以便插入用ImageSpan对象封装的图像 222 | full_name = LAST_NAME + System.currentTimeMillis(); 223 | String s = FONT_NAME_HEAD + full_name + FONT_NAME_TAIL; 224 | SpannableString spannableString = new SpannableString(s); 225 | // 用ImageSpan对象替换face 226 | spannableString.setSpan(imageSpan, 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 227 | //将选择的图片追加到EditText中光标所在位置 228 | // EditText ed = mSvContent.getFocusEditText(); 229 | EditText ed = mRetContent.getLastFocusEdit(); 230 | int index = ed.getSelectionStart(); //获取光标所在位置 231 | Editable edit_text = ed.getEditableText(); 232 | if (index < 0 || index >= edit_text.length()) { 233 | edit_text.append(spannableString); 234 | } else { 235 | edit_text.insert(index, spannableString); 236 | } 237 | testStorage(); 238 | } 239 | mDrawViewLayout.clearScreen(); 240 | } 241 | 242 | }; 243 | 244 | /** 245 | * 测试是否有储存权限 246 | */ 247 | public void testStorage() { 248 | if (ContextCompat.checkSelfPermission(this, 249 | Manifest.permission.WRITE_EXTERNAL_STORAGE) 250 | != PackageManager.PERMISSION_GRANTED) { 251 | ActivityCompat.requestPermissions(this, 252 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 253 | 1); 254 | } else { 255 | 256 | } 257 | } 258 | 259 | @Override 260 | protected void onResume() { 261 | super.onResume(); 262 | EditText ed = mRetContent.getLastFocusEdit(); 263 | ed.requestFocus(); 264 | ed.setCursorVisible(true); 265 | } 266 | 267 | 268 | @Override 269 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 270 | if (requestCode == 1) { 271 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 272 | 273 | } else { 274 | // Permission Denied 275 | Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show(); 276 | } 277 | return; 278 | } 279 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 280 | } 281 | 282 | /** 283 | * 这里是换行的需要 284 | */ 285 | @Override 286 | public void creatNewLine() { 287 | EditText ed = mRetContent.getLastFocusEdit(); 288 | int index = ed.getSelectionStart(); 289 | Editable editable = ed.getText(); 290 | editable.insert(index, "\n"); 291 | } 292 | 293 | @Override 294 | public void getUptime(long l) { 295 | mOldTime = l; 296 | mHandler.postDelayed(runnable, 100); 297 | } 298 | 299 | @Override 300 | public void stopTime() { 301 | mHandler.removeCallbacks(runnable); 302 | } 303 | 304 | /** 305 | * 需要空格 306 | */ 307 | @Override 308 | public void needSpace() { 309 | NewDrawPenView view = mDrawViewLayout.getSaveBitmap(); 310 | if (view != null) { 311 | if (view.getHasDraw()) { 312 | mBitmap = view.getBitmap(); 313 | mHandler.post(runnableUi); 314 | //保持一个联系 315 | mHandler.postDelayed(new Runnable() { 316 | @Override 317 | public void run() { 318 | mIsCreateBitmap = true; 319 | if (mCreatBimap == null) { 320 | mCreatBimap = creatBimap(); 321 | } 322 | mHandler.post(runnableUi); 323 | } 324 | }, 100); 325 | } else { 326 | mIsCreateBitmap = true; 327 | if (mCreatBimap == null) { 328 | mCreatBimap = creatBimap(); 329 | } 330 | mHandler.post(runnableUi); 331 | } 332 | } 333 | mHandler.removeCallbacks(runnable); 334 | } 335 | 336 | 337 | @Override 338 | public void deleteOnClick() { 339 | if (mRetContent.getLastFocusEdit().getSelectionStart() == 0) { 340 | mRetContent.onBackspacePress(mRetContent.getLastFocusEdit()); 341 | mHandler.removeCallbacks(runnable); 342 | } else { 343 | SystemUtils.sendKeyCode(67); 344 | } 345 | 346 | } 347 | 348 | @Override 349 | public void deleteOnLongClick() { 350 | if (mRetContent.getLastFocusEdit().getSelectionStart() == 0) { 351 | mRetContent.onBackspacePress(mRetContent.getLastFocusEdit()); 352 | if (mHandler != null) { 353 | mHandler.removeCallbacks(runnable); 354 | } 355 | } else { 356 | SystemUtils.sendKeyCode(67); 357 | } 358 | } 359 | 360 | /** 361 | * @param flag 下面的键盘是否在显示了 362 | */ 363 | @Override 364 | public void showkeyB(boolean flag) { 365 | if (flag) { 366 | mRetContent.getLastFocusEdit().requestFocus(); 367 | } 368 | mRetContent.getLastFocusEdit().setCursorVisible(true); 369 | } 370 | 371 | @Override 372 | public void onDestroy() { 373 | super.onDestroy(); 374 | if (null != mHandler) { 375 | mHandler = null; 376 | } 377 | mTimerSave.cancel(); 378 | } 379 | 380 | @Override 381 | public void onClick(View v) { 382 | int penConfig = mDrawViewLayout.getPenConfig(); 383 | switch (v.getId()) { 384 | case R.id.btn_change_pen: 385 | if (penConfig == IPenConfig.STROKE_TYPE_PEN) { 386 | penConfig = IPenConfig.STROKE_TYPE_BRUSH; 387 | } else { 388 | penConfig = IPenConfig.STROKE_TYPE_PEN; 389 | } 390 | mDrawViewLayout.setPenConfig(penConfig); 391 | break; 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/HandRichTextEditor.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.animation.LayoutTransition; 4 | import android.content.Context; 5 | import android.text.SpannableStringBuilder; 6 | import android.text.TextUtils; 7 | import android.util.AttributeSet; 8 | import android.util.TypedValue; 9 | import android.view.ActionMode; 10 | import android.view.KeyEvent; 11 | import android.view.LayoutInflater; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.view.inputmethod.EditorInfo; 17 | import android.widget.EditText; 18 | import android.widget.LinearLayout; 19 | import android.widget.RelativeLayout; 20 | import android.widget.ScrollView; 21 | 22 | import com.shiming.pen.R; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | /** 28 | * author: Created by shiming on 2018/1/20 15:08 29 | * mailbox:lamshiming@sina.com 30 | * 可编辑富文本 31 | */ 32 | public class HandRichTextEditor extends ScrollView { 33 | private static final int EDIT_PADDING = 0; // edittext常规padding是10dp 34 | 35 | private String mPATH = null; // 保存的路径,需要每次从外面传进来 36 | private int viewTagIndex = 1; // 新生的view都会打一个tag,对每个view来说,这个tag是唯一的。 37 | private LinearLayout allLayout; // 这个是所有子view的容器,scrollView内部的唯一一个ViewGroup 38 | private LayoutInflater inflater; 39 | private OnKeyListener keyListener; // 所有EditText的软键盘监听器 40 | private ArrayList btnCloseList; //所有关闭按钮的集合 41 | private ArrayList viewCloseList; //所有空白View,需要和关闭按钮一起隐藏 42 | private ArrayList editViewList; //用来收集所有edit 43 | private OnFocusChangeListener focusListener; // 所有EditText的焦点监听listener 44 | private EditText lastFocusEdit; // 最近被聚焦的EditText 45 | private LayoutTransition mTransitioner; // 只在图片View添加或remove时,触发transition动画 46 | private int editNormalPadding = 5; // 47 | 48 | 49 | public HandRichTextEditor(Context context) { 50 | this(context, null); 51 | } 52 | 53 | public HandRichTextEditor(Context context, AttributeSet attrs) { 54 | this(context, attrs, 0); 55 | } 56 | 57 | public HandRichTextEditor(Context context, AttributeSet attrs, int defStyleAttr) { 58 | super(context, attrs, defStyleAttr); 59 | inflater = LayoutInflater.from(context); 60 | btnCloseList = new ArrayList(); 61 | viewCloseList = new ArrayList(); 62 | editViewList = new ArrayList(); 63 | init(); 64 | } 65 | 66 | /** 67 | * 设置空白EditText属性 68 | */ 69 | public void setVisibilityEdit(int visibility) { 70 | for (EditText view : editViewList) { 71 | if (view != null && TextUtils.isEmpty(view.getText())) { 72 | view.setVisibility(visibility); 73 | } 74 | } 75 | } 76 | 77 | public void setVisibilityClose(int visibility) { 78 | for (View view : btnCloseList) { 79 | view.setVisibility(visibility); 80 | } 81 | for (View view : viewCloseList) { 82 | view.setVisibility(visibility); 83 | } 84 | } 85 | 86 | public void setPath(String path) { 87 | mPATH = path; 88 | } 89 | 90 | private void init() { 91 | // 1. 初始化allLayout 92 | allLayout = new LinearLayout(getContext()); 93 | allLayout.setOrientation(LinearLayout.VERTICAL); 94 | setupLayoutTransitions(); 95 | LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 96 | LayoutParams.WRAP_CONTENT); 97 | allLayout.setPadding(5, 15, 5, 15);//设置间距,防止生成图片时文字太靠边,不能用margin,否则有黑边 98 | addView(allLayout, layoutParams); 99 | // 2. 初始化键盘退格监听 100 | // 主要用来处理点击回删按钮时,view的一些列合并操作 101 | keyListener = new OnKeyListener() { 102 | 103 | @Override 104 | public boolean onKey(View v, int keyCode, KeyEvent event) { 105 | if (event.getAction() == KeyEvent.ACTION_DOWN 106 | && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 107 | } 108 | return false; 109 | } 110 | }; 111 | 112 | focusListener = new OnFocusChangeListener() { 113 | 114 | @Override 115 | public void onFocusChange(View v, boolean hasFocus) { 116 | if (hasFocus) { 117 | lastFocusEdit = (EditText) v; 118 | if (mOnHandRichEditTextHasFocus == null) return; 119 | mOnHandRichEditTextHasFocus.hasFocus(v); 120 | } 121 | } 122 | }; 123 | createFirstEditText(); 124 | } 125 | 126 | public onHandRichEditTextHasFocus mOnHandRichEditTextHasFocus; 127 | 128 | /** 129 | * 处理软键盘backSpace回退事件 130 | * 131 | * @param editTxt 光标所在的文本输入框 132 | */ 133 | public void onBackspacePress(EditText editTxt) { 134 | int startSelection = editTxt.getSelectionStart(); 135 | // 只有在光标已经顶到文本输入框的最前方,在判定是否删除之前的图片,或两个View合并 136 | if (startSelection == 0) { 137 | int editIndex = allLayout.indexOfChild(editTxt); 138 | View preView = allLayout.getChildAt(editIndex - 1); // 如果editIndex-1<0, 139 | // 则返回的是null 140 | if (null != preView) { 141 | if (preView instanceof RelativeLayout) { 142 | // 光标EditText的上一个view对应的是图片 143 | //onImageCloseClick(preView); 144 | } else if (preView instanceof EditText) { 145 | // 光标EditText的上一个view对应的还是文本框EditText 146 | String str1 = editTxt.getText().toString(); 147 | EditText preEdit = (EditText) preView; 148 | String str2 = preEdit.getText().toString(); 149 | 150 | allLayout.removeView(editTxt); 151 | editViewList.remove(editTxt); 152 | // 文本合并 153 | String str3 = str2 + str1; 154 | List list = HandViewUtils.setRelcenote(str3); 155 | SpannableStringBuilder strbuilder = HandViewUtils.getRelcenote(str3, list); 156 | SpannableStringBuilder strbuilder2 = HandViewUtils.getEditImg(getContext(), strbuilder, mPATH); 157 | preEdit.setText(strbuilder2); 158 | preEdit.requestFocus(); 159 | preEdit.setSelection(str2.length(), str2.length()); 160 | lastFocusEdit = preEdit; 161 | } 162 | } 163 | } 164 | } 165 | 166 | public interface onHandRichEditTextHasFocus { 167 | void hasFocus(View view); 168 | 169 | void onClickChange(View v); 170 | } 171 | 172 | public void setOnHandRichEditTextHasFocus(onHandRichEditTextHasFocus li) { 173 | mOnHandRichEditTextHasFocus = li; 174 | } 175 | 176 | /** 177 | * 首次创建EditText 178 | */ 179 | private void createFirstEditText() { 180 | LinearLayout.LayoutParams firstEditParam = new LinearLayout.LayoutParams( 181 | LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 182 | //editNormalPadding = dip2px(EDIT_PADDING); 183 | EditText firstEdit = createEditText("", dip2px(getContext(), EDIT_PADDING)); 184 | allLayout.addView(firstEdit, firstEditParam); 185 | lastFocusEdit = firstEdit; 186 | editViewList.add(firstEdit); 187 | } 188 | 189 | /** 190 | * 初始化transition动画 191 | */ 192 | private void setupLayoutTransitions() { 193 | mTransitioner = new LayoutTransition(); 194 | allLayout.setLayoutTransition(mTransitioner); 195 | mTransitioner.addTransitionListener(new LayoutTransition.TransitionListener() { 196 | 197 | @Override 198 | public void startTransition(LayoutTransition transition, 199 | ViewGroup container, View view, int transitionType) { 200 | 201 | } 202 | 203 | @Override 204 | public void endTransition(LayoutTransition transition, 205 | ViewGroup container, View view, int transitionType) { 206 | if (!transition.isRunning() 207 | && transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 208 | // transition动画结束,合并EditText 209 | // mergeEditText(); 210 | } 211 | } 212 | }); 213 | mTransitioner.setDuration(300); 214 | } 215 | 216 | public int dip2px(Context context, float dipValue) { 217 | float m = context.getResources().getDisplayMetrics().density; 218 | return (int) (dipValue * m + 0.5f); 219 | } 220 | 221 | public void clearAllLayout() { 222 | allLayout.removeAllViews(); 223 | btnCloseList.clear(); 224 | viewCloseList.clear(); 225 | } 226 | 227 | /** 228 | * 生成文本输入框 229 | */ 230 | public EditText createEditText(String hint, int paddingTop) { 231 | EditText editText = (EditText) inflater.inflate(R.layout.hand_rich_edittext, null); 232 | editText.setOnKeyListener(keyListener); 233 | editText.setTag(viewTagIndex++); 234 | editText.setPadding(editNormalPadding, paddingTop, editNormalPadding, paddingTop); 235 | editText.setHint(hint); 236 | editText.setOnFocusChangeListener(focusListener); 237 | editText.requestFocus(); 238 | editText.setCursorVisible(true); 239 | editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 60); 240 | /**禁止长按,选择和隐藏键盘*/ 241 | editText.setLongClickable(false); 242 | editText.setTextIsSelectable(false); 243 | editText.setOnClickListener(onClickListener); 244 | editText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);////点击EditText时,不会弹出一个全屏输入窗口 245 | editText.setCustomSelectionActionModeCallback(callback); 246 | SystemUtils.hideSoftInputMethod(editText); 247 | return editText; 248 | } 249 | 250 | OnClickListener onClickListener = new OnClickListener() { 251 | 252 | @Override 253 | public void onClick(View v) { 254 | if (mOnHandRichEditTextHasFocus == null) return; 255 | mOnHandRichEditTextHasFocus.onClickChange(v); 256 | } 257 | }; 258 | 259 | /** 260 | * 禁止长按和选择 261 | */ 262 | ActionMode.Callback callback = new ActionMode.Callback() { 263 | @Override 264 | public boolean onCreateActionMode(ActionMode mode, Menu menu) { 265 | return false; 266 | } 267 | 268 | @Override 269 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 270 | return false; 271 | } 272 | 273 | @Override 274 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 275 | return false; 276 | } 277 | 278 | @Override 279 | public void onDestroyActionMode(ActionMode mode) { 280 | 281 | } 282 | }; 283 | 284 | public EditText getLastFocusEdit() { 285 | return lastFocusEdit; 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/HandViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.text.SpannableStringBuilder; 7 | import android.text.Spanned; 8 | import android.text.style.ImageSpan; 9 | 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | import static com.shiming.pen.field_character.FieldCharacterShapeActivity.FONT_NAME_HEAD; 18 | import static com.shiming.pen.field_character.FieldCharacterShapeActivity.FONT_NAME_TAIL; 19 | import static com.shiming.pen.field_character.FieldCharacterShapeActivity.mEmotionSize; 20 | 21 | /** 22 | * author: Created by shiming on 2017/8/20 15:08 23 | * mailbox:lamshiming@sina.com 24 | * 手写view通用工具 25 | */ 26 | public class HandViewUtils { 27 | /** 28 | * 表情解析需要用的 29 | */ 30 | public static List setRelcenote(String relcenote) { 31 | ArrayList listRelce = new ArrayList(); 32 | return listRelce; 33 | } 34 | 35 | public static SpannableStringBuilder getRelcenote(String content, List listRelce) { 36 | return getRelcenote(content, listRelce, mEmotionSize, mEmotionSize); 37 | } 38 | 39 | public static SpannableStringBuilder getRelcenote(String content, List listRelce, double newWidth, double newHeight) { 40 | SpannableStringBuilder spannableString = new SpannableStringBuilder(content); 41 | return spannableString; 42 | } 43 | 44 | 45 | public static SpannableStringBuilder getEditImg(Context context, SpannableStringBuilder txt, String path) { 46 | SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(txt); 47 | Pattern pattern = Pattern.compile("\\" + FONT_NAME_HEAD + "(\\S+?)\\" + FONT_NAME_TAIL + "");//匹配[xx]的字符串 48 | Matcher matcher = pattern.matcher(txt); 49 | while (matcher.find()) { 50 | int start = matcher.start(); 51 | int end = matcher.end(); 52 | String group = matcher.group(); 53 | group = group.substring(FONT_NAME_HEAD.length(), group.length() - FONT_NAME_TAIL.length()); 54 | Bitmap bitmap = getSdBitmap(path + group); 55 | ImageSpan imageSpan = new ImageSpan(context, bitmap); 56 | spannableStringBuilder.setSpan(imageSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 57 | } 58 | return spannableStringBuilder; 59 | } 60 | 61 | /*** 62 | * 获得SD卡bitmap 63 | */ 64 | public static Bitmap getSdBitmap(String pathname) { 65 | FileInputStream fis = null; 66 | try { 67 | fis = new FileInputStream(pathname); 68 | return BitmapFactory.decodeStream(fis); 69 | } catch (FileNotFoundException e) { 70 | e.printStackTrace(); 71 | } 72 | return null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/SystemUtils.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import android.app.Instrumentation; 4 | import android.text.InputType; 5 | import android.widget.EditText; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | /** 11 | * 调用系统的一些工具方法 12 | */ 13 | 14 | public class SystemUtils { 15 | /** 16 | *
17 |      * 使用Instrumentation接口:对于非自行编译的安卓系统,无法获取系统签名,只能在前台模拟按键,不能后台模拟
18 |      * 注意:调用Instrumentation的sendKeyDownUpSync方法必须另起一个线程,否则无效
19 |      * @param keyCode
20 |      *            按键事件(KeyEvent)的按键值
21 |      * 
22 | */ 23 | public static void sendKeyCode(final int keyCode) { 24 | new Thread(new Runnable() { 25 | @Override 26 | public void run() { 27 | try { 28 | // 创建一个Instrumentation对象 29 | Instrumentation inst = new Instrumentation(); 30 | // 调用inst对象的按键模拟方法 31 | inst.sendKeyDownUpSync(keyCode); 32 | } catch (Exception e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | }).start(); 37 | } 38 | 39 | 40 | /** 41 | * 隐藏系统键盘 42 | */ 43 | public static void hideSoftInputMethod(EditText editText) { 44 | int currentVersion = android.os.Build.VERSION.SDK_INT; 45 | String methodName = null; 46 | if (currentVersion >= 16) { 47 | // 4.2 48 | methodName = "setShowSoftInputOnFocus"; 49 | } else if (currentVersion >= 14) { 50 | // 4.0 51 | methodName = "setSoftInputShownOnFocus"; 52 | } 53 | 54 | if (methodName == null) { 55 | editText.setInputType(InputType.TYPE_NULL); 56 | } else { 57 | Class cls = EditText.class; 58 | Method setShowSoftInputOnFocus; 59 | try { 60 | setShowSoftInputOnFocus = cls.getMethod(methodName, boolean.class); 61 | setShowSoftInputOnFocus.setAccessible(true); 62 | setShowSoftInputOnFocus.invoke(editText, false); 63 | } catch (NoSuchMethodException e) { 64 | editText.setInputType(InputType.TYPE_NULL); 65 | e.printStackTrace(); 66 | } catch (InvocationTargetException e) { 67 | e.printStackTrace(); 68 | } catch (IllegalAccessException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/field_character/WordSandPictures.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.field_character; 2 | 3 | import java.io.Serializable; 4 | 5 | public class WordSandPictures implements Serializable { 6 | public int start; 7 | public int end; 8 | public String path = ""; 9 | public String allPath = ""; 10 | public int type = -1; 11 | public int faceIndex; 12 | 13 | @Override 14 | public String toString() { 15 | return "WordSandPictures{" + 16 | "allPath='" + allPath + '\'' + 17 | ", start=" + start + 18 | ", end=" + end + 19 | ", path='" + path + '\'' + 20 | ", type=" + type + 21 | ", faceIndex=" + faceIndex + 22 | '}'; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/BasePen.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.graphics.Canvas; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * @author shiming 8 | * @version v1.0 create at 2017/10/17 9 | * @des 处理draw和touch事件的基类 10 | */ 11 | public abstract class BasePen { 12 | 13 | /** 14 | * 绘制 15 | * 16 | * @param canvas 17 | */ 18 | public abstract void draw(Canvas canvas); 19 | 20 | /** 21 | * 接受并处理onTouchEvent 22 | * 23 | * @param event 24 | * @return 25 | */ 26 | public boolean onTouchEvent(MotionEvent event, Canvas canvas) { 27 | return false; 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/BasePenExtend.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.view.MotionEvent; 7 | 8 | import com.shiming.pen.Bezier; 9 | import com.shiming.pen.old_code.ControllerPoint; 10 | 11 | import java.util.ArrayList; 12 | 13 | /** 14 | * @author shiming 15 | * @version v1.0 create at 2017/10/10 16 | * @des 17 | */ 18 | public abstract class BasePenExtend extends BasePen { 19 | 20 | public ArrayList mHWPointList = new ArrayList<>(); 21 | 22 | public ArrayList mPointList = new ArrayList(); 23 | 24 | public ControllerPoint mLastPoint = new ControllerPoint(0, 0); 25 | 26 | public Paint mPaint; 27 | //笔的宽度信息 28 | public double mBaseWidth; 29 | 30 | public double mLastVel; 31 | 32 | public double mLastWidth; 33 | 34 | public Bezier mBezier = new Bezier(); 35 | 36 | protected ControllerPoint mCurPoint; 37 | protected Context mContext; 38 | 39 | public BasePenExtend(Context context) { 40 | mContext = context; 41 | } 42 | 43 | public void setPaint(Paint paint) { 44 | mPaint = paint; 45 | mBaseWidth = paint.getStrokeWidth(); 46 | } 47 | 48 | @Override 49 | public void draw(Canvas canvas) { 50 | mPaint.setStyle(Paint.Style.FILL); 51 | //点的集合少 不去绘制 52 | if (mHWPointList == null || mHWPointList.size() < 1) 53 | return; 54 | //当控制点的集合很少的时候,需要画个小圆,但是需要算法 55 | if (mHWPointList.size() < 2) { 56 | ControllerPoint point = mHWPointList.get(0); 57 | //由于此问题在算法上还没有实现,所以暂时不给他画圆圈 58 | //canvas.drawCircle(point.x, point.y, point.width, mPaint); 59 | } else { 60 | mCurPoint = mHWPointList.get(0); 61 | drawNeetToDo(canvas); 62 | } 63 | } 64 | 65 | 66 | @Override 67 | public boolean onTouchEvent(MotionEvent event, Canvas canvas) { 68 | // event会被下一次事件重用,这里必须生成新的,否则会有问题 69 | MotionEvent event2 = MotionEvent.obtain(event); 70 | switch (event.getActionMasked()) { 71 | case MotionEvent.ACTION_DOWN: 72 | onDown(createMotionElement(event2)); 73 | return true; 74 | case MotionEvent.ACTION_MOVE: 75 | onMove(createMotionElement(event2)); 76 | return true; 77 | case MotionEvent.ACTION_UP: 78 | onUp(createMotionElement(event2), canvas); 79 | return true; 80 | default: 81 | break; 82 | } 83 | return super.onTouchEvent(event, canvas); 84 | } 85 | 86 | /** 87 | * 按下的事件 88 | * 89 | * @param mElement 90 | */ 91 | public void onDown(MotionElement mElement) { 92 | if (mPaint == null) { 93 | throw new NullPointerException("paint 笔不可能为null哦"); 94 | } 95 | if (getNewPaint(mPaint) != null) { 96 | Paint paint = getNewPaint(mPaint); 97 | mPaint = paint; 98 | //当然了,不要因为担心内存泄漏,在每个变量使用完成后都添加xxx=null, 99 | // 对于消除过期引用的最好方法,就是让包含该引用的变量结束生命周期,而不是显示的清空 100 | paint = null; 101 | System.out.println("shiming 当绘制的时候是否为新的paint" + mPaint + "原来的对象是否销毁了paint==" + paint); 102 | } 103 | mPointList.clear(); 104 | //如果在brush字体这里接受到down的事件,把下面的这个集合清空的话,那么绘制的内容会发生改变 105 | //不清空的话,也不可能 106 | mHWPointList.clear(); 107 | //记录down的控制点的信息 108 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 109 | //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇, 110 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 111 | mLastWidth = mElement.pressure * mBaseWidth; 112 | } else { 113 | //如果是手指画的,我们取他的0.8 114 | mLastWidth = 0.8 * mBaseWidth; 115 | } 116 | //down下的点的宽度 117 | curPoint.width = (float) mLastWidth; 118 | mLastVel = 0; 119 | mPointList.add(curPoint); 120 | //记录当前的点 121 | mLastPoint = curPoint; 122 | } 123 | 124 | protected Paint getNewPaint(Paint paint) { 125 | return null; 126 | } 127 | 128 | /** 129 | * 手指移动的事件 130 | * 131 | * @param mElement 132 | */ 133 | public void onMove(MotionElement mElement) { 134 | 135 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 136 | double deltaX = curPoint.x - mLastPoint.x; 137 | double deltaY = curPoint.y - mLastPoint.y; 138 | //deltaX和deltay平方和的二次方根 想象一个例子 1+1的平方根为1.4 (x²+y²)开根号 139 | //同理,当滑动的越快的话,deltaX+deltaY的值越大,这个越大的话,curDis也越大 140 | double curDis = Math.hypot(deltaX, deltaY); 141 | //我们求出的这个值越小,画的点或者是绘制椭圆形越多,这个值越大的话,绘制的越少,笔就越细,宽度越小 142 | double curVel = curDis * IPenConfig.DIS_VEL_CAL_FACTOR; 143 | double curWidth; 144 | //点的集合少,我们得必须改变宽度,每次点击的down的时候,这个事件 145 | if (mPointList.size() < 2) { 146 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 147 | curWidth = mElement.pressure * mBaseWidth; 148 | } else { 149 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 150 | mLastWidth); 151 | } 152 | curPoint.width = (float) curWidth; 153 | mBezier.init(mLastPoint, curPoint); 154 | } else { 155 | mLastVel = curVel; 156 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 157 | curWidth = mElement.pressure * mBaseWidth; 158 | } else { 159 | //由于我们手机是触屏的手机,滑动的速度也不慢,所以,一般会走到这里来 160 | //阐明一点,当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着mlastWidth不断的变换 161 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 162 | mLastWidth); 163 | } 164 | curPoint.width = (float) curWidth; 165 | mBezier.addNode(curPoint); 166 | } 167 | //每次移动的话,这里赋值新的值 168 | mLastWidth = curWidth; 169 | mPointList.add(curPoint); 170 | moveNeetToDo(curDis); 171 | mLastPoint = curPoint; 172 | } 173 | 174 | 175 | /** 176 | * 手指抬起来的事件 177 | * 178 | * @param mElement 179 | * @param canvas 180 | */ 181 | public void onUp(MotionElement mElement, Canvas canvas) { 182 | 183 | mCurPoint = new ControllerPoint(mElement.x, mElement.y); 184 | double deltaX = mCurPoint.x - mLastPoint.x; 185 | double deltaY = mCurPoint.y - mLastPoint.y; 186 | double curDis = Math.hypot(deltaX, deltaY); 187 | //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇,这个是不会变的 188 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 189 | mCurPoint.width = (float) (mElement.pressure * mBaseWidth); 190 | } else { 191 | mCurPoint.width = 0; 192 | } 193 | 194 | mPointList.add(mCurPoint); 195 | 196 | mBezier.addNode(mCurPoint); 197 | 198 | int steps = 1 + (int) curDis / IPenConfig.STEPFACTOR; 199 | double step = 1.0 / steps; 200 | for (double t = 0; t < 1.0; t += step) { 201 | ControllerPoint point = mBezier.getPoint(t); 202 | mHWPointList.add(point); 203 | } 204 | // 205 | mBezier.end(); 206 | for (double t = 0; t < 1.0; t += step) { 207 | ControllerPoint point = mBezier.getPoint(t); 208 | mHWPointList.add(point); 209 | } 210 | 211 | // 手指up 我画到纸上上 212 | draw(canvas); 213 | //每次抬起手来,就把集合清空,在水彩笔的那个地方,如果啊,我说如果不清空的话,每次抬起手来, 214 | // 在onDown下去的话,最近画的线的透明度有改变,所以这里clear下线的集合 215 | clear(); 216 | } 217 | 218 | /** 219 | * @param curVel 220 | * @param lastVel 221 | * @param curDis 222 | * @param factor 223 | * @param lastWidth 224 | * @return 225 | */ 226 | public double calcNewWidth(double curVel, double lastVel, double curDis, 227 | double factor, double lastWidth) { 228 | double calVel = curVel * 0.6 + lastVel * (1 - 0.6); 229 | //返回指定数字的自然对数 230 | //手指滑动的越快,这个值越小,为负数 231 | double vfac = Math.log(factor * 2.0f) * (-calVel); 232 | //此方法返回值e,其中e是自然对数的基数。 233 | //Math.exp(vfac) 变化范围为0 到1 当手指没有滑动的时候 这个值为1 当滑动很快的时候无线趋近于0 234 | //在次说明下,当手指抬起来,这个值会变大,这也就说明,抬起手太慢的话,笔锋效果不太明显 235 | //这就说明为什么笔锋的效果不太明显 236 | double calWidth = mBaseWidth * Math.exp(vfac); 237 | 238 | //滑动的速度越快的话,mMoveThres也越大 239 | double mMoveThres = curDis * 0.01f; 240 | //对之值最大的地方进行控制 241 | if (mMoveThres > IPenConfig.WIDTH_THRES_MAX) { 242 | mMoveThres = IPenConfig.WIDTH_THRES_MAX; 243 | } 244 | // TODO: 2018/2/24 以下的方法 可以删除掉 原因是抽取了一下 ,本来不应该在这里的出现的 不好意思 245 | // //滑动越慢的情况下,得到的calWidth 和上面的calwidth 相差的值不一样 246 | // 247 | // //滑动的越快的话,第一个判断会走 248 | // if (Math.abs(calWidth - mBaseWidth) / mBaseWidth > mMoveThres) { 249 | // if (calWidth > mBaseWidth) { 250 | // calWidth = mBaseWidth * (1 + mMoveThres); 251 | // } else { 252 | // calWidth = mBaseWidth * (1 - mMoveThres); 253 | // } 254 | // //滑动的越慢的话,第二个判断会走 基本上在屏幕上手指基本上没有走动的时候 ,就会走这个方法 255 | // } else if (Math.abs(calWidth - lastWidth) / lastWidth > mMoveThres) { 256 | // if (calWidth > lastWidth) { 257 | // calWidth = lastWidth * (1 + mMoveThres); 258 | // } else { 259 | // calWidth = lastWidth * (1 - mMoveThres); 260 | // } 261 | // } 262 | return calWidth; 263 | } 264 | 265 | /** 266 | * event.getPressure(); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的,我的手机上为1 267 | * 268 | * @param motionEvent 269 | * @return 270 | */ 271 | public MotionElement createMotionElement(MotionEvent motionEvent) { 272 | MotionElement motionElement = new MotionElement(motionEvent.getX(), motionEvent.getY(), 273 | motionEvent.getPressure(), motionEvent.getToolType(0)); 274 | return motionElement; 275 | } 276 | 277 | public void clear() { 278 | mPointList.clear(); 279 | mHWPointList.clear(); 280 | } 281 | 282 | /** 283 | * 当现在的点和触摸点的位置在一起的时候不用去绘制 284 | * 但是这里也可以优化,当一直处于onDown事件的时候,其实这个方法一只在走 285 | * 286 | * @param canvas 287 | * @param point 288 | * @param paint 289 | */ 290 | // TODO: 2017/10/18 这里可以优化 当一直处于onDown事件的时候,其实这个方法一直在走,优化的点是,处于down事件,这里不需要走 291 | protected void drawToPoint(Canvas canvas, ControllerPoint point, Paint paint) { 292 | if ((mCurPoint.x == point.x) && (mCurPoint.y == point.y)) { 293 | return; 294 | } 295 | //水彩笔的效果和钢笔的不太一样,交给自己去实现 296 | doNeetToDo(canvas, point, paint); 297 | } 298 | 299 | /** 300 | * 判断笔是否为空 节约性能,每次切换笔的时候就不用重复设置了 301 | * 302 | * @return 303 | */ 304 | public boolean isNull() { 305 | return mPaint == null; 306 | } 307 | 308 | /** 309 | * 移动的时候,这里由于需要透明度的处理,交给子类 310 | * 311 | * @param 312 | */ 313 | protected abstract void moveNeetToDo(double f); 314 | 315 | /** 316 | * 这里交给子类,一个是绘制椭圆,一个是绘制bitmap 317 | * 318 | * @param canvas 319 | * @param point 320 | * @param paint 321 | */ 322 | protected abstract void doNeetToDo(Canvas canvas, ControllerPoint point, Paint paint); 323 | 324 | /** 325 | * 这里由于在设置笔的透明度,会导致整个线,或者说整个画布的的颜透明度随着整个笔的透明度而变化, 326 | * 所以在这里考虑是不是说,绘制毛笔的时候,每次都给它new 一个paint ,但是这里我还没有找到更好的办法 327 | * 328 | * @param canvas 329 | */ 330 | // TODO: 2017/10/17 这个问题 待解决 331 | protected abstract void drawNeetToDo(Canvas canvas); 332 | 333 | 334 | } 335 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/BrushPen.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.graphics.Rect; 12 | import android.graphics.RectF; 13 | 14 | import com.shiming.pen.R; 15 | import com.shiming.pen.old_code.ControllerPoint; 16 | 17 | 18 | /** 19 | * @author shiming 20 | * @version v1.0 create at 2017/10/10 21 | * @des 水彩笔 22 | */ 23 | public class BrushPen extends BasePenExtend { 24 | 25 | private Bitmap mBitmap; 26 | //第一个Rect 代表要绘制的bitmap 区域, 27 | protected Rect mOldRect = new Rect(); 28 | //第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方 29 | protected RectF mNeedDrawRect = new RectF(); 30 | protected Bitmap mOriginBitmap; 31 | 32 | public BrushPen(Context context) { 33 | super(context); 34 | initTexture(); 35 | } 36 | 37 | /** 38 | * 由于需要画笔piant中的一些信息,就不能让paint为null,所以setBitmap需要有paint的时候设置 39 | * 40 | * @param paint 41 | */ 42 | @Override 43 | public void setPaint(Paint paint) { 44 | super.setPaint(paint); 45 | setBitmap(mOriginBitmap); 46 | } 47 | 48 | /** 49 | * 感谢公司的ui大哥 小伍哥 免费给的切图 50 | * R.mipmap.tranglie 设置的时候有点像三角形的笔锋 51 | * R.mipmap.cicrle 圆形的笔锋效果 52 | * R.mipmap.six 六边形有点怪怪的,可以测试一下 53 | * R.drawable.brush 这个才是用起来比较舒服,如果你的笔锋要很尖的话,叫ui爸爸给你裁剪这种图 越尖越好 54 | */ 55 | private void initTexture() { 56 | //通过资源文件生成的原始的bitmap区域 后面的资源图有些更加有意识的东西 57 | mOriginBitmap = BitmapFactory.decodeResource( 58 | mContext.getResources(), R.mipmap.brush); 59 | } 60 | 61 | /** 62 | * 主要是得到需要绘制的rect的区域 63 | * 64 | * @param bitmap 65 | */ 66 | private void setBitmap(Bitmap bitmap) { 67 | Canvas canvas = new Canvas(); 68 | mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), 69 | Bitmap.Config.ARGB_8888); 70 | //用指定的方式填充位图的像素。 71 | mBitmap.eraseColor(Color.rgb(Color.red(mPaint.getColor()), 72 | Color.green(mPaint.getColor()), Color.blue(mPaint.getColor()))); 73 | //用画布制定位图绘制 74 | canvas.setBitmap(mBitmap); 75 | Paint paint = new Paint(); 76 | // 设置混合模式 (只在源图像和目标图像相交的地方绘制目标图像) 77 | //最常见的应用就是蒙板绘制,利用源图作为蒙板“抠出”目标图上的图像。 78 | //如果把这行代码注释掉这里生成的东西更加有意思 79 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); 80 | canvas.drawBitmap(bitmap, 0, 0, paint); 81 | 82 | //src 代表需要绘制的区域 83 | mOldRect.set(0, 0, mBitmap.getWidth() / 4, mBitmap.getHeight() / 4); 84 | } 85 | 86 | 87 | /** 88 | * 更具笔的宽度的变化,笔的透明度要和发生变化 89 | * 90 | * @param point 91 | * @return 92 | */ 93 | private ControllerPoint getWithPointAlphaPoint(ControllerPoint point) { 94 | ControllerPoint nPoint = new ControllerPoint(); 95 | nPoint.x = point.x; 96 | nPoint.y = point.y; 97 | nPoint.width = point.width; 98 | int alpha = (int) (255 * point.width / mBaseWidth / 2); 99 | if (alpha < 10) { 100 | alpha = 10; 101 | } else if (alpha > 255) { 102 | alpha = 255; 103 | } 104 | nPoint.alpha = alpha; 105 | return nPoint; 106 | } 107 | 108 | 109 | @Override 110 | protected void doNeetToDo(Canvas canvas, ControllerPoint point, Paint paint) { 111 | drawLine(canvas, mCurPoint.x, mCurPoint.y, mCurPoint.width, 112 | mCurPoint.alpha, point.x, point.y, point.width, point.alpha, 113 | paint); 114 | } 115 | 116 | /** 117 | * 感谢作者,请教下怎么实时获取笔锋的宽度? 118 | * 119 | * @param canvas 120 | * @param x0 121 | * @param y0 122 | * @param w0 123 | * @param a0 124 | * @param x1 125 | * @param y1 126 | * @param w1 127 | * @param a1 128 | * @param paint 129 | */ 130 | protected void drawLine(Canvas canvas, double x0, double y0, double w0, 131 | int a0, double x1, double y1, double w1, int a1, Paint paint) { 132 | double curDis = Math.hypot(x0 - x1, y0 - y1); 133 | int factor = 2; 134 | if (paint.getStrokeWidth() < 6) { 135 | factor = 1; 136 | } else if (paint.getStrokeWidth() > 60) { 137 | factor = 3; 138 | } 139 | int steps = 1 + (int) (curDis / factor); 140 | double deltaX = (x1 - x0) / steps; 141 | double deltaY = (y1 - y0) / steps; 142 | double deltaW = (w1 - w0) / steps; 143 | double deltaA = (a1 - a0) / steps; 144 | double x = x0; 145 | double y = y0; 146 | double w = w0; 147 | double a = a0; 148 | 149 | for (int i = 0; i < steps; i++) { 150 | if (w < 1.5) 151 | w = 1.5; 152 | //根据点的信息计算出需要把bitmap绘制在什么地方 153 | mNeedDrawRect.set((float) (x - w / 2.0f), (float) (y - w / 2.0f), 154 | (float) (x + w / 2.0f), (float) (y + w / 2.0f)); 155 | //每次到这里来的话,这个笔的透明度就会发生改变,但是呢,这个笔不用同一个的话,有点麻烦 156 | //我在这里做了个不是办法的办法,每次呢?我都从新new了一个新的笔,每次循环就new一个,内存就有很多的笔了 157 | //这里new 新的笔 我放到外面去做了 158 | //Paint newPaint = new Paint(paint); 159 | //当这里很小的时候,透明度就会很小,个人测试在3.0左右比较靠谱 160 | paint.setAlpha((int) (a / 3.0f)); 161 | //第一个Rect 代表要绘制的bitmap 区域,第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方 162 | canvas.drawBitmap(mBitmap, mOldRect, mNeedDrawRect, paint); 163 | x += deltaX; 164 | y += deltaY; 165 | w += deltaW; 166 | a += deltaA; 167 | } 168 | } 169 | 170 | 171 | @Override 172 | protected void drawNeetToDo(Canvas canvas) { 173 | for (int i = 1; i < mHWPointList.size(); i++) { 174 | ControllerPoint point = mHWPointList.get(i); 175 | drawToPoint(canvas, point, mPaint); 176 | mCurPoint = point; 177 | } 178 | } 179 | 180 | @Override 181 | protected void moveNeetToDo(double curDis) { 182 | //水彩笔的效果 183 | int steps = 1 + (int) curDis / IPenConfig.STEPFACTOR; 184 | double step = 1.0 / steps; 185 | for (double t = 0; t < 1.0; t += step) { 186 | ControllerPoint point = mBezier.getPoint(t); 187 | point = getWithPointAlphaPoint(point); 188 | mHWPointList.add(point); 189 | } 190 | 191 | } 192 | 193 | //对每个笔设置了透明度 如果这里不设置一个新的笔的话,每次down事件发生了,就会把一起的绘制完成的东西,透明度也发生改变, 194 | //这里还有想到更好的方法, 195 | // TODO: 2017/10/18 196 | //虽然这样设置了,但是还是有问题,每次down的时候,虽然是一根新的笔,但是原来的笔始终有点小小的问题 197 | @Override 198 | protected Paint getNewPaint(Paint paint) { 199 | return new Paint(paint); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/IPenConfig.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.graphics.Color; 4 | 5 | /** 6 | * @author shiming 7 | * @version v1.0 create at 2017/10/10 8 | * @des 笔的设置,但是有些笔的设置最好不要放在这里,不要笔的颜色和宽度 9 | */ 10 | public interface IPenConfig { 11 | 12 | /** 13 | * 清除画布 14 | */ 15 | int STROKE_TYPE_ERASER = 0; 16 | 17 | /** 18 | * 钢笔 19 | */ 20 | int STROKE_TYPE_PEN = 1;// 钢笔 21 | /** 22 | * 毛笔 23 | */ 24 | int STROKE_TYPE_BRUSH = 2;// 毛笔 25 | 26 | //设置笔的宽度 27 | int PEN_WIDTH = 60; 28 | //笔的颜色 29 | int PEN_CORLOUR = Color.parseColor("#FF4081"); 30 | 31 | //这个控制笔锋的控制值 32 | float DIS_VEL_CAL_FACTOR = 0.02f; 33 | //手指在移动的控制笔的变化率 这个值越大,线条的粗细越加明显 34 | //float WIDTH_THRES_MAX = 0.6f; 35 | float WIDTH_THRES_MAX = 10f; 36 | //绘制计算的次数,数值越小计算的次数越多,需要折中 37 | int STEPFACTOR = 10; 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/MotionElement.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | /** 4 | * @author shiming 5 | * @version v1.0 create at 2017/10/10 6 | * @des 7 | */ 8 | public class MotionElement { 9 | 10 | public float x; 11 | public float y; 12 | //压力值 物理设备决定的,和设计的设备有关系,在此Demo中没有用到 ,但是这个坑 记录下 13 | public float pressure; 14 | //绘制的工具是否是手指或者是笔(触摸笔) 15 | public int tooltype; 16 | 17 | public MotionElement(float mx, float my, float mp, int ttype) { 18 | x = mx; 19 | y = my; 20 | pressure = mp; 21 | tooltype = ttype; 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/NewDrawPenView.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.util.AttributeSet; 12 | import android.util.DisplayMetrics; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | 17 | import com.shiming.pen.field_character.DrawPenView; 18 | 19 | import static com.shiming.pen.new_code.IPenConfig.PEN_WIDTH; 20 | 21 | 22 | /** 23 | * @author shiming 24 | * @version v1.0 create at 2017/8/24 25 | * @des DrawPenView实现手写关键类,目前只提供了,手绘的功能和清除画布,后期根据业务逻辑可以动态的设置方法 26 | */ 27 | public class NewDrawPenView extends View { 28 | private static final String TAG = "DrawPenView"; 29 | private Paint mPaint;//画笔 30 | private Canvas mCanvas;//画布 31 | private Bitmap mBitmap; 32 | private Context mContext; 33 | public static int mCanvasCode = IPenConfig.STROKE_TYPE_PEN; 34 | private BasePenExtend mStokeBrushPen; 35 | private boolean mIsCanvasDraw; 36 | private int mPenconfig; 37 | 38 | public NewDrawPenView(Context context) { 39 | super(context); 40 | initParameter(context); 41 | } 42 | 43 | public NewDrawPenView(Context context, AttributeSet attrs) { 44 | super(context, attrs); 45 | initParameter(context); 46 | } 47 | 48 | public NewDrawPenView(Context context, AttributeSet attrs, int defStyleAttr) { 49 | super(context, attrs, defStyleAttr); 50 | initParameter(context); 51 | } 52 | 53 | private void initParameter(Context context) { 54 | mContext = context; 55 | DisplayMetrics dm = new DisplayMetrics(); 56 | ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); 57 | mBitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888); 58 | mStokeBrushPen = new SteelPen(context); 59 | initPaint(); 60 | initCanvas(); 61 | } 62 | 63 | 64 | private void initPaint() { 65 | mPaint = new Paint(); 66 | mPaint.setColor(IPenConfig.PEN_CORLOUR); 67 | mPaint.setStrokeWidth(PEN_WIDTH); 68 | mPaint.setStyle(Paint.Style.STROKE); 69 | mPaint.setStrokeCap(Paint.Cap.ROUND);//结束的笔画为圆心 70 | mPaint.setStrokeJoin(Paint.Join.ROUND);//连接处元 71 | mPaint.setAlpha(0xFF); 72 | mPaint.setAntiAlias(true); 73 | mPaint.setStrokeMiter(1.0f); 74 | mStokeBrushPen.setPaint(mPaint); 75 | } 76 | 77 | private void initCanvas() { 78 | mCanvas = new Canvas(mBitmap); 79 | //设置画布的颜色的问题 80 | mCanvas.drawColor(Color.TRANSPARENT); 81 | } 82 | 83 | @Override 84 | protected void onDraw(Canvas canvas) { 85 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 86 | switch (mCanvasCode) { 87 | case IPenConfig.STROKE_TYPE_PEN: 88 | case IPenConfig.STROKE_TYPE_BRUSH: 89 | mStokeBrushPen.draw(canvas); 90 | break; 91 | case IPenConfig.STROKE_TYPE_ERASER: 92 | reset(); 93 | break; 94 | default: 95 | Log.e(TAG, "onDraw" + Integer.toString(mCanvasCode)); 96 | break; 97 | } 98 | super.onDraw(canvas); 99 | } 100 | 101 | 102 | public void setCanvasCode(int canvasCode) { 103 | mCanvasCode = canvasCode; 104 | switch (mCanvasCode) { 105 | case IPenConfig.STROKE_TYPE_PEN: 106 | mStokeBrushPen = new SteelPen(mContext); 107 | break; 108 | case IPenConfig.STROKE_TYPE_BRUSH: 109 | mStokeBrushPen = new BrushPen(mContext); 110 | break; 111 | 112 | } 113 | //设置 114 | if (mStokeBrushPen.isNull()) { 115 | mStokeBrushPen.setPaint(mPaint); 116 | } 117 | invalidate(); 118 | } 119 | 120 | /** 121 | * event.getAction() //获取触控动作比如ACTION_DOWN 122 | * event.getPointerCount(); //获取触控点的数量,比如2则可能是两个手指同时按压屏幕 123 | * event.getPointerId(nID); //对于每个触控的点的细节,我们可以通过一个循环执行getPointerId方法获取索引 124 | * event.getX(nID); //获取第nID个触控点的x位置,记录的第一个点为getX,getY 125 | * event.getY(nID); //获取第nID个点触控的y位置 126 | * event.getPressure(nID); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的 127 | * event.getDownTime() //按下开始时间 128 | * event.getEventTime() // 事件结束时间 129 | * event.getEventTime()-event.getDownTime()); //总共按下时花费时间 130 | * 131 | * @param event 132 | * @return 133 | */ 134 | @Override 135 | public boolean onTouchEvent(MotionEvent event) { 136 | mIsCanvasDraw = true; 137 | MotionEvent event2 = MotionEvent.obtain(event); 138 | mStokeBrushPen.onTouchEvent(event2, mCanvas); 139 | //event会被下一次事件重用,这里必须生成新的,否则会有问题 140 | //getActionMask:触摸的动作,按下,抬起,滑动,多点按下,多点抬起 141 | switch (event2.getActionMasked()) { 142 | case MotionEvent.ACTION_DOWN: 143 | if (mGetTimeListner != null) 144 | mGetTimeListner.stopTime(); 145 | break; 146 | case MotionEvent.ACTION_MOVE: 147 | if (mGetTimeListner != null) 148 | mGetTimeListner.stopTime(); 149 | break; 150 | case MotionEvent.ACTION_UP: 151 | long time = System.currentTimeMillis(); 152 | if (mGetTimeListner != null) 153 | mGetTimeListner.getTime(time); 154 | break; 155 | default: 156 | break; 157 | } 158 | invalidate(); 159 | return true; 160 | } 161 | 162 | /** 163 | * @return 判断是否有绘制内容在画布上 164 | */ 165 | public boolean getHasDraw() { 166 | return mIsCanvasDraw; 167 | } 168 | 169 | /** 170 | * 清除画布,记得清除点的集合 171 | */ 172 | public void reset() { 173 | mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 174 | mCanvas.drawPaint(mPaint); 175 | mPaint.setXfermode(null); 176 | mIsCanvasDraw = false; 177 | mStokeBrushPen.clear(); 178 | //这里处理的不太好 需要优化 179 | mCanvasCode = mPenconfig; 180 | 181 | } 182 | 183 | public TimeListener mGetTimeListner; 184 | 185 | public void setGetTimeListener(TimeListener l) { 186 | mGetTimeListner = l; 187 | } 188 | 189 | public Bitmap getBitmap() { 190 | return mBitmap; 191 | } 192 | 193 | public void setPenconfig(int penconfig) { 194 | mPenconfig = penconfig; 195 | 196 | } 197 | 198 | public interface TimeListener { 199 | void getTime(long l); 200 | 201 | void stopTime(); 202 | } 203 | 204 | private int mBackColor = Color.TRANSPARENT; 205 | 206 | /** 207 | * 逐行扫描 清楚边界空白。功能是生成一张bitmap位于正中间,不是位于顶部,此关键的是我们画布需要 208 | * 成透明色才能生效 209 | * 210 | * @param blank 边距留多少个像素 211 | * @return tks github E-signature 212 | */ 213 | public Bitmap clearBlank(int blank) { 214 | if (mBitmap != null) { 215 | int HEIGHT = mBitmap.getHeight();//1794 216 | int WIDTH = mBitmap.getWidth();//1080 217 | int top = 0, left = 0, right = 0, bottom = 0; 218 | int[] pixs = new int[WIDTH]; 219 | boolean isStop; 220 | for (int y = 0; y < HEIGHT; y++) { 221 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 222 | isStop = false; 223 | for (int pix : pixs) { 224 | if (pix != mBackColor) { 225 | 226 | top = y; 227 | isStop = true; 228 | break; 229 | } 230 | } 231 | if (isStop) { 232 | break; 233 | } 234 | } 235 | for (int y = HEIGHT - 1; y >= 0; y--) { 236 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 237 | isStop = false; 238 | for (int pix : pixs) { 239 | if (pix != mBackColor) { 240 | bottom = y; 241 | isStop = true; 242 | break; 243 | } 244 | } 245 | if (isStop) { 246 | break; 247 | } 248 | } 249 | pixs = new int[HEIGHT]; 250 | for (int x = 0; x < WIDTH; x++) { 251 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 252 | isStop = false; 253 | for (int pix : pixs) { 254 | if (pix != mBackColor) { 255 | left = x; 256 | isStop = true; 257 | break; 258 | } 259 | } 260 | if (isStop) { 261 | break; 262 | } 263 | } 264 | for (int x = WIDTH - 1; x > 0; x--) { 265 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 266 | isStop = false; 267 | for (int pix : pixs) { 268 | if (pix != mBackColor) { 269 | right = x; 270 | isStop = true; 271 | break; 272 | } 273 | } 274 | if (isStop) { 275 | break; 276 | } 277 | } 278 | if (blank < 0) { 279 | blank = 0; 280 | } 281 | left = left - blank > 0 ? left - blank : 0; 282 | top = top - blank > 0 ? top - blank : 0; 283 | right = right + blank > WIDTH - 1 ? WIDTH - 1 : right + blank; 284 | bottom = bottom + blank > HEIGHT - 1 ? HEIGHT - 1 : bottom + blank; 285 | return Bitmap.createBitmap(mBitmap, left, top, right - left, bottom - top); 286 | } else { 287 | return null; 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/PenUtils.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Color; 5 | 6 | /** 7 | * @author shiming 8 | * @version v1.0 create at 2017/10/10 9 | * @des 10 | */ 11 | public class PenUtils { 12 | 13 | private int mBackColor = Color.TRANSPARENT; 14 | 15 | /** 16 | * 逐行扫描 清楚边界空白。功能是生成一张bitmap位于正中间,不是位于顶部,此关键的是我们画布需要 17 | * 成透明色才能生效 18 | * 19 | * @param blank 边距留多少个像素 20 | * @return tks github E-signature 21 | */ 22 | public Bitmap clearBlank(Bitmap mBitmap, int blank) { 23 | if (mBitmap != null) { 24 | int HEIGHT = mBitmap.getHeight(); 25 | int WIDTH = mBitmap.getWidth(); 26 | int top = 0, left = 0, right = 0, bottom = 0; 27 | int[] pixs = new int[WIDTH]; 28 | boolean isStop; 29 | for (int y = 0; y < HEIGHT; y++) { 30 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 31 | isStop = false; 32 | for (int pix : pixs) { 33 | if (pix != mBackColor) { 34 | 35 | top = y; 36 | isStop = true; 37 | break; 38 | } 39 | } 40 | if (isStop) { 41 | break; 42 | } 43 | } 44 | for (int y = HEIGHT - 1; y >= 0; y--) { 45 | mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1); 46 | isStop = false; 47 | for (int pix : pixs) { 48 | if (pix != mBackColor) { 49 | bottom = y; 50 | isStop = true; 51 | break; 52 | } 53 | } 54 | if (isStop) { 55 | break; 56 | } 57 | } 58 | pixs = new int[HEIGHT]; 59 | for (int x = 0; x < WIDTH; x++) { 60 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 61 | isStop = false; 62 | for (int pix : pixs) { 63 | if (pix != mBackColor) { 64 | left = x; 65 | isStop = true; 66 | break; 67 | } 68 | } 69 | if (isStop) { 70 | break; 71 | } 72 | } 73 | for (int x = WIDTH - 1; x > 0; x--) { 74 | mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT); 75 | isStop = false; 76 | for (int pix : pixs) { 77 | if (pix != mBackColor) { 78 | right = x; 79 | isStop = true; 80 | break; 81 | } 82 | } 83 | if (isStop) { 84 | break; 85 | } 86 | } 87 | if (blank < 0) { 88 | blank = 0; 89 | } 90 | left = left - blank > 0 ? left - blank : 0; 91 | top = top - blank > 0 ? top - blank : 0; 92 | right = right + blank > WIDTH - 1 ? WIDTH - 1 : right + blank; 93 | bottom = bottom + blank > HEIGHT - 1 ? HEIGHT - 1 : bottom + blank; 94 | return Bitmap.createBitmap(mBitmap, left, top, right - left, bottom - top); 95 | } else { 96 | return null; 97 | } 98 | } 99 | 100 | //**将rgb色彩值转成16进制代码** 101 | public String convertRGBToHex(int r, int g, int b) { 102 | String rFString, rSString, gFString, gSString, 103 | bFString, bSString, result; 104 | int red, green, blue; 105 | int rred, rgreen, rblue; 106 | red = r / 16; 107 | rred = r % 16; 108 | if (red == 10) 109 | rFString = "A"; 110 | else if (red == 11) 111 | rFString = "B"; 112 | else if (red == 12) 113 | rFString = "C"; 114 | else if (red == 13) 115 | rFString = "D"; 116 | else if (red == 14) 117 | rFString = "E"; 118 | else if (red == 15) 119 | rFString = "F"; 120 | else 121 | rFString = String.valueOf(red); 122 | 123 | if (rred == 10) 124 | rSString = "A"; 125 | else if (rred == 11) 126 | rSString = "B"; 127 | else if (rred == 12) 128 | rSString = "C"; 129 | else if (rred == 13) 130 | rSString = "D"; 131 | else if (rred == 14) 132 | rSString = "E"; 133 | else if (rred == 15) 134 | rSString = "F"; 135 | else 136 | rSString = String.valueOf(rred); 137 | 138 | rFString = rFString + rSString; 139 | 140 | green = g / 16; 141 | rgreen = g % 16; 142 | 143 | if (green == 10) 144 | gFString = "A"; 145 | else if (green == 11) 146 | gFString = "B"; 147 | else if (green == 12) 148 | gFString = "C"; 149 | else if (green == 13) 150 | gFString = "D"; 151 | else if (green == 14) 152 | gFString = "E"; 153 | else if (green == 15) 154 | gFString = "F"; 155 | else 156 | gFString = String.valueOf(green); 157 | 158 | if (rgreen == 10) 159 | gSString = "A"; 160 | else if (rgreen == 11) 161 | gSString = "B"; 162 | else if (rgreen == 12) 163 | gSString = "C"; 164 | else if (rgreen == 13) 165 | gSString = "D"; 166 | else if (rgreen == 14) 167 | gSString = "E"; 168 | else if (rgreen == 15) 169 | gSString = "F"; 170 | else 171 | gSString = String.valueOf(rgreen); 172 | 173 | gFString = gFString + gSString; 174 | 175 | blue = b / 16; 176 | rblue = b % 16; 177 | 178 | if (blue == 10) 179 | bFString = "A"; 180 | else if (blue == 11) 181 | bFString = "B"; 182 | else if (blue == 12) 183 | bFString = "C"; 184 | else if (blue == 13) 185 | bFString = "D"; 186 | else if (blue == 14) 187 | bFString = "E"; 188 | else if (blue == 15) 189 | bFString = "F"; 190 | else 191 | bFString = String.valueOf(blue); 192 | 193 | if (rblue == 10) 194 | bSString = "A"; 195 | else if (rblue == 11) 196 | bSString = "B"; 197 | else if (rblue == 12) 198 | bSString = "C"; 199 | else if (rblue == 13) 200 | bSString = "D"; 201 | else if (rblue == 14) 202 | bSString = "E"; 203 | else if (rblue == 15) 204 | bSString = "F"; 205 | else 206 | bSString = String.valueOf(rblue); 207 | bFString = bFString + bSString; 208 | result = "#" + rFString + gFString + bFString; 209 | return result; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/new_code/SteelPen.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.new_code; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.RectF; 7 | 8 | import com.shiming.pen.old_code.ControllerPoint; 9 | 10 | import static com.shiming.pen.new_code.IPenConfig.STEPFACTOR; 11 | 12 | /** 13 | * @author shiming 14 | * @version v1.0 create at 2017/10/17 15 | * @des 钢笔 16 | */ 17 | public class SteelPen extends BasePenExtend { 18 | 19 | public SteelPen(Context context) { 20 | super(context); 21 | } 22 | 23 | @Override 24 | protected void drawNeetToDo(Canvas canvas) { 25 | for (int i = 1; i < mHWPointList.size(); i++) { 26 | ControllerPoint point = mHWPointList.get(i); 27 | drawToPoint(canvas, point, mPaint); 28 | mCurPoint = point; 29 | } 30 | } 31 | 32 | @Override 33 | protected void moveNeetToDo(double curDis) { 34 | int steps = 1 + (int) curDis / STEPFACTOR; 35 | double step = 1.0 / steps; 36 | for (double t = 0; t < 1.0; t += step) { 37 | ControllerPoint point = mBezier.getPoint(t); 38 | mHWPointList.add(point); 39 | } 40 | } 41 | 42 | @Override 43 | protected void doNeetToDo(Canvas canvas, ControllerPoint point, Paint paint) { 44 | drawLine(canvas, mCurPoint.x, mCurPoint.y, mCurPoint.width, point.x, 45 | point.y, point.width, paint); 46 | } 47 | 48 | /** 49 | * 其实这里才是关键的地方,通过画布画椭圆,每一个点都是一个椭圆,这个椭圆的所有细节,逐渐构建出一个完美的笔尖 50 | * 和笔锋的效果,我觉得在这里需要大量的测试,其实就对低端手机进行排查,看我们绘制的笔的宽度是多少,绘制多少个椭圆 51 | * 然后在低端手机上不会那么卡,当然你哪一个N年前的手机给我,那也的卡,只不过需要适中的范围里面 52 | * 53 | * @param canvas 54 | * @param x0 55 | * @param y0 56 | * @param w0 57 | * @param x1 58 | * @param y1 59 | * @param w1 60 | * @param paint 61 | */ 62 | private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint) { 63 | //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园 64 | double curDis = Math.hypot(x0 - x1, y0 - y1); 65 | int steps = 1; 66 | if (paint.getStrokeWidth() < 6) { 67 | steps = 1 + (int) (curDis / 2); 68 | } else if (paint.getStrokeWidth() > 60) { 69 | steps = 1 + (int) (curDis / 4); 70 | } else { 71 | steps = 1 + (int) (curDis / 3); 72 | } 73 | double deltaX = (x1 - x0) / steps; 74 | double deltaY = (y1 - y0) / steps; 75 | double deltaW = (w1 - w0) / steps; 76 | double x = x0; 77 | double y = y0; 78 | double w = w0; 79 | 80 | for (int i = 0; i < steps; i++) { 81 | //都是用于表示坐标系中的一块矩形区域,并可以对其做一些简单操作 82 | //精度不一样。Rect是使用int类型作为数值,RectF是使用float类型作为数值。 83 | // Rect rect = new Rect(); 84 | RectF oval = new RectF(); 85 | oval.set((float) (x - w / 4.0f), (float) (y - w / 2.0f), (float) (x + w / 4.0f), (float) (y + w / 2.0f)); 86 | // oval.set((float)(x+w/4.0f), (float)(y+w/4.0f), (float)(x-w/4.0f), (float)(y-w/4.0f)); 87 | //最基本的实现,通过点控制线,绘制椭圆 88 | canvas.drawOval(oval, paint); 89 | x += deltaX; 90 | y += deltaY; 91 | w += deltaW; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/old_code/ControllerPoint.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.old_code; 2 | 3 | 4 | /** 5 | * @author shiming 6 | * @version v1.0 create at 2017/8/24 7 | * @des 每个点的控制,关心三个因素:笔的宽度,坐标,透明数值 8 | */ 9 | public class ControllerPoint { 10 | public float x; 11 | public float y; 12 | 13 | public float width; 14 | public int alpha = 255; 15 | 16 | public ControllerPoint() { 17 | } 18 | 19 | public ControllerPoint(float x, float y) { 20 | this.x = x; 21 | this.y = y; 22 | } 23 | 24 | 25 | public void set(float x, float y, float w) { 26 | this.x = x; 27 | this.y = y; 28 | this.width = w; 29 | } 30 | 31 | 32 | public void set(ControllerPoint point) { 33 | this.x = point.x; 34 | this.y = point.y; 35 | this.width = point.width; 36 | } 37 | 38 | 39 | public String toString() { 40 | String str = "X = " + x + "; Y = " + y + "; W = " + width; 41 | return str; 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/old_code/OldDrawPenView.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.old_code; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.util.AttributeSet; 12 | import android.util.DisplayMetrics; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | 17 | import com.shiming.pen.new_code.IPenConfig; 18 | import com.shiming.pen.new_code.BrushPen; 19 | 20 | import static com.shiming.pen.new_code.IPenConfig.PEN_WIDTH; 21 | 22 | 23 | /** 24 | * @author shiming 25 | * @version v1.0 create at 2017/8/24 26 | * @des DrawPenView实现手写关键类,目前只提供了,手绘的功能和清除画布,后期根据业务逻辑可以动态的设置方法 27 | */ 28 | public class OldDrawPenView extends View { 29 | private static final String TAG = "DrawPenView"; 30 | private Paint mPaint;//画笔 31 | private Canvas mCanvas;//画布 32 | private Bitmap mBitmap; 33 | private StrokePen mVisualStrokePen; 34 | private Context mContext; 35 | public static int mCanvasCode = IPenConfig.STROKE_TYPE_PEN; 36 | private BrushPen mStokeBrushPen; 37 | 38 | public OldDrawPenView(Context context) { 39 | super(context); 40 | initParameter(context); 41 | } 42 | 43 | public OldDrawPenView(Context context, AttributeSet attrs) { 44 | super(context, attrs); 45 | initParameter(context); 46 | } 47 | 48 | public OldDrawPenView(Context context, AttributeSet attrs, int defStyleAttr) { 49 | super(context, attrs, defStyleAttr); 50 | initParameter(context); 51 | } 52 | 53 | public void setCanvasCode(int canvasCode) { 54 | mCanvasCode = canvasCode; 55 | invalidate(); 56 | } 57 | 58 | private void initParameter(Context context) { 59 | mContext = context; 60 | DisplayMetrics dm = new DisplayMetrics(); 61 | ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); 62 | mBitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888); 63 | //笔的控制类 64 | mVisualStrokePen = new StrokePen(mContext); 65 | initPaint(mContext); 66 | initCanvas(); 67 | } 68 | 69 | 70 | private void initPaint(Context context) { 71 | mPaint = new Paint(); 72 | mPaint.setColor(IPenConfig.PEN_CORLOUR); 73 | mPaint.setStrokeWidth(PEN_WIDTH); 74 | mPaint.setStyle(Paint.Style.STROKE); 75 | mPaint.setStrokeCap(Paint.Cap.ROUND);//结束的笔画为圆心 76 | mPaint.setStrokeJoin(Paint.Join.ROUND);//连接处元 77 | mPaint.setAlpha(0xFF); 78 | mPaint.setAntiAlias(true); 79 | mPaint.setStrokeMiter(1.0f); 80 | mStokeBrushPen.setPaint(mPaint); 81 | mVisualStrokePen.setPaint(mPaint); 82 | 83 | 84 | } 85 | 86 | private void initCanvas() { 87 | mCanvas = new Canvas(mBitmap); 88 | //设置画布的颜色的问题 89 | mCanvas.drawColor(Color.TRANSPARENT); 90 | } 91 | 92 | public void changePaintColor(int color) { 93 | mPaint.setColor(color); 94 | } 95 | 96 | public void changePaintSize(float width) { 97 | mPaint.setStrokeWidth(width); 98 | } 99 | 100 | 101 | @Override 102 | protected void onDraw(Canvas canvas) { 103 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 104 | switch (mCanvasCode) { 105 | case IPenConfig.STROKE_TYPE_PEN: 106 | mVisualStrokePen.draw(canvas); 107 | break; 108 | case IPenConfig.STROKE_TYPE_ERASER: 109 | reset(); 110 | break; 111 | case IPenConfig.STROKE_TYPE_BRUSH: 112 | mStokeBrushPen.draw(canvas); 113 | break; 114 | default: 115 | Log.e(TAG, "onDraw" + Integer.toString(mCanvasCode)); 116 | break; 117 | } 118 | super.onDraw(canvas); 119 | } 120 | 121 | /** 122 | * event.getAction() //获取触控动作比如ACTION_DOWN 123 | * event.getPointerCount(); //获取触控点的数量,比如2则可能是两个手指同时按压屏幕 124 | * event.getPointerId(nID); //对于每个触控的点的细节,我们可以通过一个循环执行getPointerId方法获取索引 125 | * event.getX(nID); //获取第nID个触控点的x位置,记录的第一个点为getX,getY 126 | * event.getY(nID); //获取第nID个点触控的y位置 127 | * event.getPressure(nID); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的 128 | * event.getDownTime() //按下开始时间 129 | * event.getEventTime() // 事件结束时间 130 | * event.getEventTime()-event.getDownTime()); //总共按下时花费时间 131 | * 132 | * @param event 133 | * @return 134 | */ 135 | @Override 136 | public boolean onTouchEvent(MotionEvent event) { 137 | //event会被下一次事件重用,这里必须生成新的,否则会有问题 138 | MotionEvent event2 = MotionEvent.obtain(event); 139 | //getActionMask:触摸的动作,按下,抬起,滑动,多点按下,多点抬起 140 | switch (event2.getActionMasked()) { 141 | case MotionEvent.ACTION_DOWN: 142 | setCanvasCode(IPenConfig.STROKE_TYPE_PEN); 143 | mVisualStrokePen.onDown(mVisualStrokePen.createMotionElement(event2)); 144 | break; 145 | case MotionEvent.ACTION_MOVE: 146 | mVisualStrokePen.onMove(mVisualStrokePen.createMotionElement(event2)); 147 | break; 148 | case MotionEvent.ACTION_UP: 149 | mVisualStrokePen.onUp(mVisualStrokePen.createMotionElement(event2), mCanvas); 150 | break; 151 | default: 152 | break; 153 | } 154 | invalidate(); 155 | return true; 156 | } 157 | 158 | /** 159 | * 清除画布,记得清除点的集合 160 | */ 161 | public void reset() { 162 | mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 163 | mCanvas.drawPaint(mPaint); 164 | mPaint.setXfermode(null); 165 | mVisualStrokePen.clear(); 166 | } 167 | 168 | public void setCurrentState(int currentState) { 169 | mCanvasCode = currentState; 170 | } 171 | 172 | 173 | public Bitmap getBitmap() { 174 | return mBitmap; 175 | } 176 | 177 | public void onResume() { 178 | 179 | } 180 | 181 | public TimeListener mGetTimeListner; 182 | 183 | public void setGetTimeListener(TimeListener l) { 184 | mGetTimeListner = l; 185 | } 186 | 187 | public interface TimeListener { 188 | void getTime(long l); 189 | 190 | void stopTime(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/old_code/StrokePen.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.old_code; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.RectF; 7 | import android.view.MotionEvent; 8 | 9 | import com.shiming.pen.Bezier; 10 | import com.shiming.pen.new_code.MotionElement; 11 | 12 | import java.util.ArrayList; 13 | 14 | 15 | /** 16 | * @author shiming 17 | * @version v1.0 create at 2017/8/24 18 | * @des 画笔的类() 19 | */ 20 | public class StrokePen { 21 | //这个控制笔锋的控制值 22 | protected float DIS_VEL_CAL_FACTOR = 0.02f; 23 | // protected float DIS_VEL_CAL_FACTOR =2000f; 24 | //手指在移动的控制笔的变化率 25 | // protected float WIDTH_THRES_MAX = 0.6f; 26 | //线的粗细的,这个值越大,线的粗细越加明显 27 | protected float WIDTH_THRES_MAX = 10f; 28 | //绘制计算的次数,数值越小计算的次数越多,需要折中 29 | protected int STEPFACTOR = 10; 30 | 31 | private ArrayList mPointList; 32 | private ArrayList mHWPointList; 33 | private Bezier mBezier; 34 | private ControllerPoint mLastPoint; 35 | //笔的宽度信息 36 | private double mBaseWidth; 37 | private double mLastVel; 38 | private double mLastWidth; 39 | private ControllerPoint curPoint; 40 | private Paint mPaint; 41 | 42 | public void clear() { 43 | mPointList.clear(); 44 | mHWPointList.clear(); 45 | 46 | } 47 | 48 | public StrokePen(Context context) { 49 | mPointList = new ArrayList(); 50 | mHWPointList = new ArrayList(); 51 | mBezier = new Bezier(); 52 | mLastPoint = new ControllerPoint(0, 0); 53 | } 54 | 55 | /** 56 | * 早onDraw需要调用 57 | * 58 | * @param canvas 画布 59 | */ 60 | public void draw(Canvas canvas) { 61 | mPaint.setStyle(Paint.Style.FILL); 62 | //点的集合少 不去绘制 63 | if (mHWPointList == null || mHWPointList.size() < 1) 64 | return; 65 | //当控制点的集合很少的时候,需要画个小圆,但是需要算法 66 | if (mHWPointList.size() < 2) { 67 | ControllerPoint point = mHWPointList.get(0); 68 | //由于此问题在算法上还没有实现,所以暂时不给他画圆圈 69 | //canvas.drawCircle(point.x, point.y, point.width, mPaint); 70 | } else { 71 | curPoint = mHWPointList.get(0); 72 | for (int i = 1; i < mHWPointList.size(); i++) { 73 | ControllerPoint point = mHWPointList.get(i); 74 | drawToPoint(canvas, point, mPaint); 75 | curPoint = point; 76 | } 77 | } 78 | } 79 | 80 | 81 | /** 82 | * event.getPressure(); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的,我的手机上为1 83 | * 84 | * @param motionEvent 85 | * @return 86 | */ 87 | public MotionElement createMotionElement(MotionEvent motionEvent) { 88 | System.out.println("shiming== 0000==" + motionEvent.getToolType(0)); 89 | System.out.println("shiming==" + motionEvent.getPressure()); 90 | System.out.println("shiming==" + motionEvent.getEventTime()); 91 | MotionElement motionElement = new MotionElement(motionEvent.getX(), motionEvent.getY(), 92 | motionEvent.getPressure(), motionEvent.getToolType(0)); 93 | return motionElement; 94 | } 95 | 96 | /** 97 | * 手指的down事件 98 | * 99 | * @param mElement 100 | */ 101 | public void onDown(MotionElement mElement) { 102 | mPaint.setXfermode(null); 103 | mPointList.clear(); 104 | mHWPointList.clear(); 105 | //记录down的控制点的信息 106 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 107 | //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇, 108 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 109 | mLastWidth = mElement.pressure * mBaseWidth; 110 | } else { 111 | //如果是手指画的,我们取他的0.8 112 | mLastWidth = 0.8 * mBaseWidth; 113 | } 114 | //down下的点的宽度 115 | curPoint.width = (float) mLastWidth; 116 | mLastVel = 0; 117 | 118 | mPointList.add(curPoint); 119 | //记录当前的点 120 | mLastPoint = curPoint; 121 | } 122 | 123 | public void onMove(MotionElement mElement) { 124 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 125 | double deltaX = curPoint.x - mLastPoint.x; 126 | double deltaY = curPoint.y - mLastPoint.y; 127 | //deltaX和deltay平方和的二次方根 想象一个例子 1+1的平方根为1.4 (x²+y²)开根号 128 | double curDis = Math.hypot(deltaX, deltaY); 129 | //我们求出的这个值越小,画的点或者是绘制椭圆形越多,这个值越大的话,绘制的越少,笔就越细,宽度越小 130 | double curVel = curDis * DIS_VEL_CAL_FACTOR; 131 | System.out.println("shiming===" + curDis + " " + curVel + " " + deltaX + " " + deltaY); 132 | double curWidth; 133 | //点的集合少,我们得必须改变宽度,每次点击的down的时候,这个事件 134 | if (mPointList.size() < 2) { 135 | System.out.println("shiming==dian shao"); 136 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 137 | curWidth = mElement.pressure * mBaseWidth; 138 | } else { 139 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 140 | mLastWidth); 141 | } 142 | curPoint.width = (float) curWidth; 143 | mBezier.init(mLastPoint, curPoint); 144 | } else { 145 | System.out.println("shiming==dian duo"); 146 | mLastVel = curVel; 147 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 148 | curWidth = mElement.pressure * mBaseWidth; 149 | } else { 150 | //由于我们手机是触屏的手机,滑动的速度也不慢,所以,一般会走到这里来 151 | //阐明一点,当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着mlastWidth不断的变换 152 | curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, 153 | mLastWidth); 154 | System.out.println("shiming==" + curVel + " " + mLastVel + " " + curDis + " " + mLastWidth); 155 | System.out.println("shiming==dian duo" + curWidth); 156 | } 157 | curPoint.width = (float) curWidth; 158 | mBezier.addNode(curPoint); 159 | } 160 | //每次移动的话,这里赋值新的值 161 | mLastWidth = curWidth; 162 | 163 | mPointList.add(curPoint); 164 | 165 | int steps = 1 + (int) curDis / STEPFACTOR; 166 | System.out.println("shiming-- steps" + steps); 167 | double step = 1.0 / steps; 168 | for (double t = 0; t < 1.0; t += step) { 169 | ControllerPoint point = mBezier.getPoint(t); 170 | mHWPointList.add(point); 171 | } 172 | 173 | mLastPoint = curPoint; 174 | } 175 | 176 | public void onUp(MotionElement mElement, Canvas canvas) { 177 | ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); 178 | double deltaX = curPoint.x - mLastPoint.x; 179 | double deltaY = curPoint.y - mLastPoint.y; 180 | double curDis = Math.hypot(deltaX, deltaY); 181 | 182 | if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { 183 | curPoint.width = (float) (mElement.pressure * mBaseWidth); 184 | } else { 185 | curPoint.width = 0; 186 | } 187 | 188 | mPointList.add(curPoint); 189 | 190 | mBezier.addNode(curPoint); 191 | 192 | int steps = 1 + (int) curDis / STEPFACTOR; 193 | double step = 1.0 / steps; 194 | for (double t = 0; t < 1.0; t += step) { 195 | ControllerPoint point = mBezier.getPoint(t); 196 | mHWPointList.add(point); 197 | } 198 | // 199 | mBezier.end(); 200 | for (double t = 0; t < 1.0; t += step) { 201 | ControllerPoint point = mBezier.getPoint(t); 202 | mHWPointList.add(point); 203 | } 204 | 205 | // 手指up 我画到纸上上 206 | draw(canvas); 207 | 208 | } 209 | 210 | /** 211 | * 通过点去绘制一条线,当现在的点和触摸点的位置在一起的时候不用去绘制 212 | * 213 | * @param canvas 214 | * @param point 215 | * @param paint 216 | */ 217 | private void drawToPoint(Canvas canvas, ControllerPoint point, Paint paint) { 218 | if ((curPoint.x == point.x) && (curPoint.y == point.y)) { 219 | return; 220 | } 221 | drawLine(canvas, curPoint.x, curPoint.y, curPoint.width, point.x, 222 | point.y, point.width, paint); 223 | } 224 | 225 | /** 226 | * @param curVel 227 | * @param lastVel 228 | * @param curDis 229 | * @param factor 230 | * @param lastWidth 231 | * @return 232 | */ 233 | private double calcNewWidth(double curVel, double lastVel, double curDis, 234 | double factor, double lastWidth) { 235 | double calVel = curVel * 0.6 + lastVel * (1 - 0.6); 236 | //返回指定数字的自然对数 237 | double vfac = Math.log(factor * 2.0f) * (-calVel); 238 | //此方法返回值e,其中e是自然对数的基数。 239 | double calWidth = mBaseWidth * Math.exp(vfac); 240 | 241 | double mMoveThres = curDis * 0.01f; 242 | if (mMoveThres > WIDTH_THRES_MAX) { 243 | mMoveThres = WIDTH_THRES_MAX; 244 | } 245 | if (Math.abs(calWidth - mBaseWidth) / mBaseWidth > mMoveThres) { 246 | if (calWidth > mBaseWidth) { 247 | calWidth = mBaseWidth * (1 + mMoveThres); 248 | } else { 249 | calWidth = mBaseWidth * (1 - mMoveThres); 250 | } 251 | } else if (Math.abs(calWidth - lastWidth) / lastWidth > mMoveThres) { 252 | if (calWidth > lastWidth) { 253 | calWidth = lastWidth * (1 + mMoveThres); 254 | } else { 255 | calWidth = lastWidth * (1 - mMoveThres); 256 | } 257 | } 258 | return calWidth; 259 | } 260 | 261 | /** 262 | * 其实这里才是关键的地方,通过画布画椭圆,每一个点都是一个椭圆,这个椭圆的所有细节,逐渐构建出一个完美的笔尖 263 | * 和笔锋的效果,我觉得在这里需要大量的测试,其实就对低端手机进行排查,看我们绘制的笔的宽度是多少,绘制多少个椭圆 264 | * 然后在低端手机上不会那么卡,当然你哪一个N年前的手机给我,那也的卡,只不过需要适中的范围里面 265 | * 266 | * @param canvas 267 | * @param x0 268 | * @param y0 269 | * @param w0 270 | * @param x1 271 | * @param y1 272 | * @param w1 273 | * @param paint 274 | */ 275 | private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint) { 276 | //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园 277 | double curDis = Math.hypot(x0 - x1, y0 - y1); 278 | int steps = 1; 279 | if (paint.getStrokeWidth() < 6) { 280 | steps = 1 + (int) (curDis / 2); 281 | } else if (paint.getStrokeWidth() > 60) { 282 | steps = 1 + (int) (curDis / 4); 283 | } else { 284 | steps = 1 + (int) (curDis / 3); 285 | } 286 | double deltaX = (x1 - x0) / steps; 287 | double deltaY = (y1 - y0) / steps; 288 | double deltaW = (w1 - w0) / steps; 289 | double x = x0; 290 | double y = y0; 291 | double w = w0; 292 | 293 | for (int i = 0; i < steps; i++) { 294 | //都是用于表示坐标系中的一块矩形区域,并可以对其做一些简单操作 295 | //精度不一样。Rect是使用int类型作为数值,RectF是使用float类型作为数值。 296 | // Rect rect = new Rect(); 297 | RectF oval = new RectF(); 298 | oval.set((float) (x - w / 4.0f), (float) (y - w / 2.0f), (float) (x + w / 4.0f), (float) (y + w / 2.0f)); 299 | // oval.set((float)(x+w/4.0f), (float)(y+w/4.0f), (float)(x-w/4.0f), (float)(y-w/4.0f)); 300 | //最基本的实现,通过点控制线,绘制椭圆 301 | canvas.drawOval(oval, paint); 302 | x += deltaX; 303 | y += deltaY; 304 | w += deltaW; 305 | } 306 | } 307 | 308 | public void setPaint(Paint paint) { 309 | mPaint = paint; 310 | mBaseWidth = paint.getStrokeWidth(); 311 | } 312 | 313 | } 314 | 315 | -------------------------------------------------------------------------------- /app/src/main/java/com/shiming/pen/view/OldDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.shiming.pen.view; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Button; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import com.shiming.pen.R; 11 | import com.shiming.pen.new_code.IPenConfig; 12 | import com.shiming.pen.new_code.NewDrawPenView; 13 | 14 | /** 15 | * author: Created by shiming on 2018/1/20 16:20 16 | * mailbox:lamshiming@sina.com 17 | */ 18 | 19 | public class OldDemoActivity extends AppCompatActivity implements View.OnClickListener { 20 | 21 | private Button mBtnStrokePen; 22 | private Button mBtnClearCanvas; 23 | private NewDrawPenView mDrawPenView; 24 | private Button mBrushPen; 25 | 26 | @Override 27 | protected void onCreate(@Nullable Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_old_demo_layout); 30 | findViews(); 31 | doSomeThing(); 32 | } 33 | 34 | private void doSomeThing() { 35 | mBtnStrokePen.setOnClickListener(this); 36 | mBtnClearCanvas.setOnClickListener(this); 37 | mBrushPen.setOnClickListener(this); 38 | } 39 | 40 | private void findViews() { 41 | mBtnStrokePen = (Button) findViewById(R.id.btn_stroke_pen); 42 | mDrawPenView = (NewDrawPenView) findViewById(R.id.draw_pen_view); 43 | mBtnClearCanvas = (Button) findViewById(R.id.btn_clear_canvas); 44 | mBrushPen = (Button) findViewById(R.id.btn_brush_pen); 45 | 46 | } 47 | 48 | @Override 49 | public void onClick(View v) { 50 | switch (v.getId()) { 51 | case R.id.btn_stroke_pen: 52 | mDrawPenView.setCanvasCode(IPenConfig.STROKE_TYPE_PEN); 53 | break; 54 | case R.id.btn_clear_canvas: 55 | mDrawPenView.setCanvasCode(IPenConfig.STROKE_TYPE_ERASER); 56 | break; 57 | case R.id.btn_brush_pen: 58 | mDrawPenView.setCanvasCode(IPenConfig.STROKE_TYPE_BRUSH); 59 | break; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/hand_draw_delete_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hand_draw_kb_show_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hand_draw_new_line_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hand_draw_space_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_delete_no_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_delete_no_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_delete_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_delete_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_pen_new_line_no_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_pen_new_line_no_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_pen_new_line_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_pen_new_line_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_pen_space_no_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_pen_space_no_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_pen_space_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/icon_pen_space_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/letter_key_.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key_nor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key_nor_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/letter_key_nor_.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key_nor_prs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/letter_key_nor_prs.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/letter_key_prs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shimingli/WritingPen/9625404b44b563419ff3e3833204953d4a70a1e7/app/src/main/res/drawable/letter_key_prs.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_field_character_shape_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 20 | 21 | 29 | 30 | 31 |