当前位置:   article > 正文

Android涂鸦画板原理详解——从初级到高级(一)_android涂鸦原理

android涂鸦原理

准备

前段时间,发布了多功能画板&开源涂鸦框架Doodle,得到了一些小伙伴的关注。但由于框架代码较多,一开始较难理解,有不少人询问了相关的实现细节。我发现不少初学者对基本的涂鸦原理不熟悉,因此我决定写一些简单的例子,用于说明最基本的的涂鸦原理,这也是多功能画板&开源涂鸦框架Doodle最核心的地方。

好的,在讲解之前,我希望小伙伴们对View的绘制流程有一定的了解,还不熟悉的同学可以先看看我之前的文章《View的绘制流程》,因为下面的涂鸦我们用到了自定义View的知识。

初级涂鸦

我们要实现最简单的涂鸦,手指在屏幕上滑动时绘制滑动轨迹。思路如下:

  1. 创建自定义View: SimpleDoodleView
  2. 使用TouchGestureDetector识别滑动手势。(TouchGestureDetector在我另一个项目Androids中,使用时需要导入依赖
  3. 将手势滑动的点记录在系统类Path中。Path可以支持贝塞尔曲线等各种图形的绘制。
  4. 在自定义View的onDraw方法中通过Canvas.drawPath()绘制记录的Path,把涂鸦轨迹绘制出来。

实现效果:

代码如下:

  1. public class SimpleDoodleView extends View {
  2. private final static String TAG = "SimpleDoodleView";
  3. private Paint mPaint = new Paint();
  4. private List<Path> mPathList = new ArrayList<>(); // 保存涂鸦轨迹的集合
  5. private TouchGestureDetector mTouchGestureDetector; // 触摸手势监听
  6. private float mLastX, mLastY;
  7. private Path mCurrentPath; // 当前的涂鸦轨迹
  8. public SimpleDoodleView(Context context) {
  9. super(context);
  10. // 设置画笔
  11. mPaint.setColor(Color.RED);
  12. mPaint.setStyle(Paint.Style.STROKE);
  13. mPaint.setStrokeWidth(20);
  14. mPaint.setAntiAlias(true);
  15. mPaint.setStrokeCap(Paint.Cap.ROUND);
  16. // 由手势识别器处理手势
  17. mTouchGestureDetector = new TouchGestureDetector(getContext(), new TouchGestureDetector.OnTouchGestureListener() {
  18. @Override
  19. public void onScrollBegin(MotionEvent e) { // 滑动开始
  20. Log.d(TAG, "onScrollBegin: ");
  21. mCurrentPath = new Path(); // 新的涂鸦
  22. mPathList.add(mCurrentPath); // 添加的集合中
  23. mCurrentPath.moveTo(e.getX(), e.getY());
  24. mLastX = e.getX();
  25. mLastY = e.getY();
  26. invalidate(); // 刷新
  27. }
  28. @Override
  29. public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 滑动中
  30. Log.d(TAG, "onScroll: " + e2.getX() + " " + e2.getY());
  31. mCurrentPath.quadTo(
  32. mLastX,
  33. mLastY,
  34. (e2.getX() + mLastX) / 2,
  35. (e2.getY() + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑
  36. mLastX = e2.getX();
  37. mLastY = e2.getY();
  38. invalidate(); // 刷新
  39. return true;
  40. }
  41. @Override
  42. public void onScrollEnd(MotionEvent e) { // 滑动结束
  43. Log.d(TAG, "onScrollEnd: ");
  44. mCurrentPath.quadTo(
  45. mLastX,
  46. mLastY,
  47. (e.getX() + mLastX) / 2,
  48. (e.getY() + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑
  49. mCurrentPath = null; // 轨迹结束
  50. invalidate(); // 刷新
  51. }
  52. });
  53. }
  54. @Override
  55. public boolean dispatchTouchEvent(MotionEvent event) {
  56. boolean consumed = mTouchGestureDetector.onTouchEvent(event); // 由手势识别器处理手势
  57. if (!consumed) {
  58. return super.dispatchTouchEvent(event);
  59. }
  60. return true;
  61. }
  62. @Override
  63. protected void onDraw(Canvas canvas) {
  64. for (Path path : mPathList) { // 绘制涂鸦轨迹
  65. canvas.drawPath(path, mPaint);
  66. }
  67. }
  68. }

使用时直接在布局文件XML里添加自定义SimpleDoodleView,或者通过如下代码添加到父容器中:

  1. // 初级涂鸦
  2. ViewGroup simpleContainer = findViewById(R.id.container_simple_doodle);
  3. SimpleDoodleView simpleDoodleView = new SimpleDoodleView(this);
  4. simpleContainer.addView(simpleDoodleView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

代码很简单,这里没有涉及到坐标换算,直接就是滑到View的哪里就直接在该位置绘制涂鸦,希望小伙伴们把上面的代码手动敲一遍,接下来就开始讲中级涂鸦啦。

中级涂鸦

中级涂鸦要实现的效果:在初级涂鸦的基础上,单击时可以选择某个涂鸦,进行移动。思路如下:

  1. 创建自定义View: MiddleDoodleView
  2. 定义PathItem类,封装涂鸦轨迹,包括Path和偏移值等信息。
    1. class PathItem {
    2. Path mPath = new Path(); // 涂鸦轨迹
    3. float mX, mY; // 轨迹偏移值
    4. }

     

  3. 单击时需要判断是否点中某个涂鸦,Path提供了接口computeBounds()计算当前图形的矩形范围,可以通过判断单击的点是否在矩形范围内判断。使用TouchGestureDetector识别单击和滑动手势。(TouchGestureDetector在我另一个项目Androids中,使用时需要导入依赖

  4. 滑动过程中需要判断当前是否有选中的涂鸦,如果有则对该涂鸦进行移动,把偏移值记录在PathItem中;没有则绘制新的涂鸦轨迹。

  5. 在MiddleDoodleView的onDraw方法中,绘制每个PathItem之前根据偏移值移动画布。

实现效果:

代码如下:

  1. public class MiddleDoodleView extends View {
  2. private final static String TAG = "MiddleDoodleView";
  3. private Paint mPaint = new Paint();
  4. private List<PathItem> mPathList = new ArrayList<>(); // 保存涂鸦轨迹的集合
  5. private TouchGestureDetector mTouchGestureDetector; // 触摸手势监听
  6. private float mLastX, mLastY;
  7. private PathItem mCurrentPathItem; // 当前的涂鸦轨迹
  8. private PathItem mSelectedPathItem; // 选中的涂鸦轨迹
  9. public MiddleDoodleView(Context context) {
  10. super(context);
  11. // 设置画笔
  12. mPaint.setColor(Color.RED);
  13. mPaint.setStyle(Paint.Style.STROKE);
  14. mPaint.setStrokeWidth(20);
  15. mPaint.setAntiAlias(true);
  16. mPaint.setStrokeCap(Paint.Cap.ROUND);
  17. // 由手势识别器处理手势
  18. mTouchGestureDetector = new TouchGestureDetector(getContext(), new TouchGestureDetector.OnTouchGestureListener() {
  19. RectF mRectF = new RectF();
  20. @Override
  21. public boolean onSingleTapUp(MotionEvent e) { // 单击选中
  22. boolean found = false;
  23. for (PathItem path : mPathList) { // 绘制涂鸦轨迹
  24. path.mPath.computeBounds(mRectF, true); // 计算涂鸦轨迹的矩形范围
  25. mRectF.offset(path.mX, path.mY); // 加上偏移
  26. if (mRectF.contains(e.getX(), e.getY())) { // 判断是否点中涂鸦轨迹的矩形范围内
  27. found = true;
  28. mSelectedPathItem = path;
  29. break;
  30. }
  31. }
  32. if (!found) { // 没有点中任何涂鸦
  33. mSelectedPathItem = null;
  34. }
  35. invalidate();
  36. return true;
  37. }
  38. @Override
  39. public void onScrollBegin(MotionEvent e) { // 滑动开始
  40. Log.d(TAG, "onScrollBegin: ");
  41. if (mSelectedPathItem == null) {
  42. mCurrentPathItem = new PathItem(); // 新的涂鸦
  43. mPathList.add(mCurrentPathItem); // 添加的集合中
  44. mCurrentPathItem.mPath.moveTo(e.getX(), e.getY());
  45. mLastX = e.getX();
  46. mLastY = e.getY();
  47. }
  48. invalidate(); // 刷新
  49. }
  50. @Override
  51. public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 滑动中
  52. Log.d(TAG, "onScroll: " + e2.getX() + " " + e2.getY());
  53. if (mSelectedPathItem == null) { // 没有选中的涂鸦
  54. mCurrentPathItem.mPath.quadTo(
  55. mLastX,
  56. mLastY,
  57. (e2.getX() + mLastX) / 2,
  58. (e2.getY() + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑
  59. mLastX = e2.getX();
  60. mLastY = e2.getY();
  61. } else { // 移动选中的涂鸦
  62. mSelectedPathItem.mX = mSelectedPathItem.mX - distanceX;
  63. mSelectedPathItem.mY = mSelectedPathItem.mY - distanceY;
  64. }
  65. invalidate(); // 刷新
  66. return true;
  67. }
  68. @Override
  69. public void onScrollEnd(MotionEvent e) { // 滑动结束
  70. Log.d(TAG, "onScrollEnd: ");
  71. if (mSelectedPathItem == null) {
  72. mCurrentPathItem.mPath.quadTo(
  73. mLastX,
  74. mLastY,
  75. (e.getX() + mLastX) / 2,
  76. (e.getY() + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑
  77. mCurrentPathItem = null; // 轨迹结束
  78. }
  79. invalidate(); // 刷新
  80. }
  81. });
  82. }
  83. @Override
  84. public boolean dispatchTouchEvent(MotionEvent event) {
  85. boolean consumed = mTouchGestureDetector.onTouchEvent(event); // 由手势识别器处理手势
  86. if (!consumed) {
  87. return super.dispatchTouchEvent(event);
  88. }
  89. return true;
  90. }
  91. @Override
  92. protected void onDraw(Canvas canvas) {
  93. for (PathItem path : mPathList) { // 绘制涂鸦轨迹
  94. canvas.save(); // 1.保存画布状态,下面要变换画布
  95. canvas.translate(path.mX, path.mY); // 根据涂鸦轨迹偏移值,偏移画布使其画在对应位置上
  96. if (mSelectedPathItem == path) {
  97. mPaint.setColor(Color.YELLOW); // 点中的为黄色
  98. } else {
  99. mPaint.setColor(Color.RED); // 其他为红色
  100. }
  101. canvas.drawPath(path.mPath, mPaint);
  102. canvas.restore(); // 2.恢复画布状态,绘制完一个涂鸦轨迹后取消上面的画布变换,不影响下一个
  103. }
  104. }
  105. /**
  106. * 封装涂鸦轨迹对象
  107. */
  108. private static class PathItem {
  109. Path mPath = new Path(); // 涂鸦轨迹
  110. float mX, mY; // 轨迹偏移值
  111. }
  112. }

使用时直接在布局文件XML里添加自定义MiddleDoodleView,或者通过如下代码添加到父容器中:

  1. // 中级涂鸦
  2. ViewGroup middleContainer = findViewById(R.id.container_middle_doodle);
  3. MiddleDoodleView middleDoodleView = new MiddleDoodleView(this);
  4. middleContainer.addView(middleDoodleView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

中级涂鸦的代码也不多,由于先前的涂鸦可以移动,所以在绘制涂鸦时需要根据移动的偏移值偏移画布。这里简单应用了矩阵变换的知识,如果不太理解的小伙伴也不用着急,后面的高级涂鸦中会降到矩阵变换的知识。

后续

初中级的涂鸦并没有涉及到对图片的操作,所以相对简单点,希望大伙可以理解透他们的原理,后面的高级涂鸦讲涉及到图片操作,对图片进行缩放移动,就相对复杂很多,我会尽全力讲解明白的~因此后面会单独出一篇文章讲解,请大家多多关注和支持!谢谢!!!

上面的代码在我的开源框架的Demo里>>>>Doodle涂鸦原理教程代码

最后请大家多多支持我的项目>>>>开源项目Doodle!一个功能强大,可自定义和可扩展的涂鸦框架、多功能画板。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/598675
推荐阅读
相关标签
  

闽ICP备14008679号