准备

高级涂鸦涉及到图片操作,包括对图片进行缩放移动、涂鸦等,这里涉及到矩阵的变换。关于矩阵变换的知识,请查看我的另一篇文章《浅谈矩阵变换——Matrix》。根据文中的介绍,接下来使用变换坐标系的空间想象去理解涂鸦中涉及到的矩阵变换。

高级涂鸦

高级涂鸦支持对图片涂鸦, 可移动缩放图片。思路如下:创建自定义View: AdvancedDoodleView,由外部创建时传入Bitmap图像对象。

在View大小确定时候的回调onSizeChanged()中进行初始化操作,计算图片居中显示的所需参数,如图片缩放倍数和偏移值。

定义PathItem类,封装涂鸦轨迹,包括Path和偏移值等信息。class PathItem {

Path mPath = new Path(); // 涂鸦轨迹

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

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

监听双指缩放手势,计算图片缩放的倍数。

(4-6中涉及到的触摸坐标要换算成对应图片坐标系中的坐标,稍后详细讲解 )在AdvancedDoodleView的onDraw方法中,根据图片缩放倍数和偏移值绘制图片;绘制每个PathItem之前根据偏移值移动画布。

坐标映射

选择画布和图片共用一个坐标系,了解图片的位置信息后,最后需要处理的就是,屏幕坐标系与图片(画布)坐标系的映射,即把屏幕上滑动的轨迹投射到图片中。

image

从上图的分析中,我们可以得出如下映射关系:图片坐标x=(屏幕坐标x-图片在屏幕坐标系x轴上的偏移量)/图片缩放倍数

图片坐标y=(屏幕坐标y-图片在屏幕坐标系y轴上的偏移量)/图片缩放倍数

(注意,图片是以左上角为中心进行缩放的)

对应代码:/**

* 将屏幕触摸坐标x转换成在图片中的坐标x

*/public final float toX(float touchX) {    return (touchX - mBitmapTransX) / mBitmapScale;

}/**

* 将屏幕触摸坐标y转换成在图片中的坐标y

*/public final float toY(float touchY) {    return (touchY - mBitmapTransY) / mBitmapScale;

}

可见,屏幕坐标投射到图片上时,需要减去偏移量,因为图片的位置是一直不变的,我们对图片进行偏移,其实是对View的画布进行偏移。

最终实现效果如下:

image

代码如下:public class AdvancedDoodleView extends View {    private final static String TAG = "AdvancedDoodleView";    private Paint mPaint = new Paint();    private List mPathList = new ArrayList<>(); // 保存涂鸦轨迹的集合

private TouchGestureDetector mTouchGestureDetector; // 触摸手势监听

private float mLastX, mLastY;    private PathItem mCurrentPathItem; // 当前的涂鸦轨迹

private PathItem mSelectedPathItem; // 选中的涂鸦轨迹

private Bitmap mBitmap;    private float mBitmapTransX, mBitmapTransY, mBitmapScale = 1;    public AdvancedDoodleView(Context context, Bitmap bitmap) {        super(context);

mBitmap = bitmap;        // 设置画笔

mPaint.setColor(Color.RED);

mPaint.setStyle(Paint.Style.STROKE);

mPaint.setStrokeWidth(20);

mPaint.setAntiAlias(true);

mPaint.setStrokeCap(Paint.Cap.ROUND);        // 由手势识别器处理手势

mTouchGestureDetector = new TouchGestureDetector(getContext(), new TouchGestureDetector.OnTouchGestureListener() {

RectF mRectF = new RectF();            // 缩放手势操作相关

Float mLastFocusX;

Float mLastFocusY;            float mTouchCentreX, mTouchCentreY;            @Override

public boolean onScaleBegin(ScaleGestureDetectorApi27 detector) {

Log.d(TAG, "onScaleBegin: ");

mLastFocusX = null;

mLastFocusY = null;                return true;

}            @Override

public void onScaleEnd(ScaleGestureDetectorApi27 detector) {

Log.d(TAG, "onScaleEnd: ");

}            @Override

public boolean onScale(ScaleGestureDetectorApi27 detector) { // 双指缩放中

Log.d(TAG, "onScale: ");                // 屏幕上的焦点

mTouchCentreX = detector.getFocusX();

mTouchCentreY = detector.getFocusY();                if (mLastFocusX != null && mLastFocusY != null) { // 焦点改变

float dx = mTouchCentreX - mLastFocusX;                    float dy = mTouchCentreY - mLastFocusY;                    // 移动图片

mBitmapTransX = mBitmapTransX + dx;

mBitmapTransY = mBitmapTransY + dy;

}                // 缩放图片

mBitmapScale = mBitmapScale * detector.getScaleFactor();                if (mBitmapScale

mBitmapScale = 0.1f;

}

invalidate();

mLastFocusX = mTouchCentreX;

mLastFocusY = mTouchCentreY;                return true;

}            @Override

public boolean onSingleTapUp(MotionEvent e) { // 单击选中

float x = toX(e.getX()), y = toY(e.getY());                boolean found = false;                for (PathItem path : mPathList) { // 绘制涂鸦轨迹

path.mPath.computeBounds(mRectF, true); // 计算涂鸦轨迹的矩形范围

mRectF.offset(path.mX, path.mY); // 加上偏移

if (mRectF.contains(x, y)) { // 判断是否点中涂鸦轨迹的矩形范围内

found = true;

mSelectedPathItem = path;                        break;

}

}                if (!found) { // 没有点中任何涂鸦

mSelectedPathItem = null;

}

invalidate();                return true;

}            @Override

public void onScrollBegin(MotionEvent e) { // 滑动开始

Log.d(TAG, "onScrollBegin: ");                float x = toX(e.getX()), y = toY(e.getY());                if (mSelectedPathItem == null) {

mCurrentPathItem = new PathItem(); // 新的涂鸦

mPathList.add(mCurrentPathItem); // 添加的集合中

mCurrentPathItem.mPath.moveTo(x, y);

}

mLastX = x;

mLastY = y;

invalidate(); // 刷新

}            @Override

public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 滑动中

Log.d(TAG, "onScroll: " + e2.getX() + " " + e2.getY());                float x = toX(e2.getX()), y = toY(e2.getY());                if (mSelectedPathItem == null) { // 没有选中的涂鸦

mCurrentPathItem.mPath.quadTo(

mLastX,

mLastY,

(x + mLastX) / 2,

(y + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑

} else { // 移动选中的涂鸦

mSelectedPathItem.mX = mSelectedPathItem.mX + x - mLastX;

mSelectedPathItem.mY = mSelectedPathItem.mY + y - mLastY;

}

mLastX = x;

mLastY = y;

invalidate(); // 刷新

return true;

}            @Override

public void onScrollEnd(MotionEvent e) { // 滑动结束

Log.d(TAG, "onScrollEnd: ");                float x = toX(e.getX()), y = toY(e.getY());                if (mSelectedPathItem == null) {

mCurrentPathItem.mPath.quadTo(

mLastX,

mLastY,

(x + mLastX) / 2,

(y + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑

mCurrentPathItem = null; // 轨迹结束

}

invalidate(); // 刷新

}

});        // 针对涂鸦的手势参数设置

// 下面两行绘画场景下应该设置间距为大于等于1,否则设为0双指缩放后抬起其中一个手指仍然可以移动

mTouchGestureDetector.setScaleSpanSlop(1); // 手势前识别为缩放手势的双指滑动最小距离值

mTouchGestureDetector.setScaleMinSpan(1); // 缩放过程中识别为缩放手势的双指最小距离值

mTouchGestureDetector.setIsLongpressEnabled(false);

mTouchGestureDetector.setIsScrollAfterScaled(false);

}    @Override

protected void onSizeChanged(int width, int height, int oldw, int oldh) { //view绘制完成时 大小确定

super.onSizeChanged(width, height, oldw, oldh);        int w = mBitmap.getWidth();        int h = mBitmap.getHeight();        float nw = w * 1f / getWidth();        float nh = h * 1f / getHeight();        float centerWidth, centerHeight;        // 1.计算使图片居中的缩放值

if (nw > nh) {

mBitmapScale = 1 / nw;

centerWidth = getWidth();

centerHeight = (int) (h * mBitmapScale);

} else {

mBitmapScale = 1 / nh;

centerWidth = (int) (w * mBitmapScale);

centerHeight = getHeight();

}        // 2.计算使图片居中的偏移值

mBitmapTransX = (getWidth() - centerWidth) / 2f;

mBitmapTransY = (getHeight() - centerHeight) / 2f;

invalidate();

}    /**

* 将屏幕触摸坐标x转换成在图片中的坐标

*/

public final float toX(float touchX) {        return (touchX - mBitmapTransX) / mBitmapScale;

}    /**

* 将屏幕触摸坐标y转换成在图片中的坐标

*/

public final float toY(float touchY) {        return (touchY - mBitmapTransY) / mBitmapScale;

}    @Override

public boolean dispatchTouchEvent(MotionEvent event) {        boolean consumed = mTouchGestureDetector.onTouchEvent(event); // 由手势识别器处理手势

if (!consumed) {            return super.dispatchTouchEvent(event);

}        return true;

}    @Override

protected void onDraw(Canvas canvas) {        // 画布和图片共用一个坐标系,只需要处理屏幕坐标系到图片(画布)坐标系的映射关系(toX toY)

canvas.translate(mBitmapTransX, mBitmapTransY);

canvas.scale(mBitmapScale, mBitmapScale);        // 绘制图片

canvas.drawBitmap(mBitmap, 0, 0, null);        for (PathItem path : mPathList) { // 绘制涂鸦轨迹

canvas.save();

canvas.translate(path.mX, path.mY); // 根据涂鸦轨迹偏移值,偏移画布使其画在对应位置上

if (mSelectedPathItem == path) {

mPaint.setColor(Color.YELLOW); // 点中的为黄色

} else {

mPaint.setColor(Color.RED); // 其他为红色

}

canvas.drawPath(path.mPath, mPaint);

canvas.restore();

}

}    /**

* 封装涂鸦轨迹对象

*/

private static class PathItem {

Path mPath = new Path(); // 涂鸦轨迹

float mX, mY; // 轨迹偏移值

}

}

使用时通过如下代码添加到父容器中:// 高级级涂鸦

ViewGroup advancedContainer = findViewById(R.id.container_advanced_doodle);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thelittleprince2);

AdvancedDoodleView advancedDoodleView = new AdvancedDoodleView(this, bitmap);

advancedContainer.addView(advancedDoodleView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

后续

涂鸦最核心的原理就是这样,希望各位能理解透。至于如何在图片中添加文字图片或者其他类似涂鸦的,其实跟代码中定义的PathItem代表涂鸦轨迹一样,我们用新的类封装新的涂鸦类型即可,然后保存相关信息,最终在画布上绘制出来即可。

作者:远方的风景2018

链接:https://www.jianshu.com/p/aa9b3dd6e954

android画板过程分析,Android涂鸦画板原理详解——从初级到高级(二)相关推荐

  1. Android涂鸦画板原理详解——从初级到高级(二)

    前言 前面写了<Android涂鸦画板原理详解--从初级到高级(一)>,讲了涂鸦原理初级和中级的应用,现在讲解高级应用.如果没有看过前面一篇文章的同学,建议先去看看哈. 准备 高级涂鸦涉及 ...

  2. 【Android架构师java原理详解】二;反射原理及动态代理模式

    前言: 本篇为Android架构师java原理专题二:反射原理及动态代理模式 大公司面试都要求我们有扎实的Java语言基础.而很多Android开发朋友这一块并不是很熟练,甚至半路初级底子很薄,这给我 ...

  3. Android面试Hash原理详解二

    Hash系列目录 Android面试Hash原理详解一 Android面试Hash原理详解二 Android面试Hash常见算法 Android面试Hash算法案例 Android面试Hash原理详解 ...

  4. 【胖虎的逆向之路】02——Android整体加壳原理详解实现

    [胖虎的逆向之路](02)--Android整体加壳原理详解&实现 Android Apk的加壳原理流程及详解 文章目录 [胖虎的逆向之路](02)--Android整体加壳原理详解& ...

  5. Android 颜色渲染(九) PorterDuff及Xfermode详解

    Android 颜色渲染(九)  PorterDuff及Xfermode详解 之前已经讲过了除ComposeShader之外Shader的全部子类, 在讲ComposeShader(组合渲染)之前,  ...

  6. Android消息传递之EventBus 3.0使用详解

    前言: 前面两篇不仅学习了子线程与UI主线程之间的通信方式,也学习了如何实现组件之间通信,基于前面的知识我们今天来分析一下EventBus是如何管理事件总线的,EventBus到底是不是最佳方案?学习 ...

  7. Android 8.0学习(32)---Android 8.0源码目录结构详解

    Android 8.0源码目录结构详解 android的移植按如下流程:     (1)android linux 内核的普通驱动移植,让内核可以在目标平台上运行起来.     (2)正确挂载文件系统 ...

  8. Android 7.0 Audio的Resample过程详解

    Android 7.0 Audio的Resample过程详解 Qidi 2017.02.23 (Markdown & Haroopad) [前言] 处理过音频文件的工程师都知道音频数据存在采样 ...

  9. Android 各大厂面试题汇总与详解(持续更新)

    介绍 目前网络中出现了好多各种面试题的汇总,有真实的也有虚假的,所以今年我将会汇总各大公司面试比较常见的问题,逐一进行解答.会一直集成,也会收集大家提供的面试题,如有错误,请大家指出,经过排查存在,会 ...

  10. Android进阶笔记:Messenger源码详解

    Messenger可以理解为一个是用于发送消息的一个类用法也很多,这里主要分析一下再跨进程的情况下Messenger的实现流程与源码分析.相信结合前面两篇关于aidl解析文章能够更好的对aidl有一个 ...

最新文章

  1. Windows 7官方主题之“海上航行”
  2. [嵌入式]Bootloader的作用
  3. hession调用json解析异常 com.caucho.hessian.io.HessianProtocolException: expected integer at 0x74 java.util
  4. 跨站点请求伪造_十大常见web漏洞——跨站点请求伪造(CSRF)
  5. 【记录】Docker安装后出现 Cannot connect to the Docker daemon
  6. (213)initial是否可综合?
  7. 做生意、做营销常犯的10个错误和对策
  8. jquery获取php生成的元素,jquery怎么生成元素
  9. html 字体思源_CSS3嵌入字体@font-face调用字体(思源宋体regula/PingFang SC/ttf/woff)...
  10. **容易混淆的4中park变换**(转载)
  11. 【基于UML软件建模的选课系统】
  12. mysql 异常码1903_Mysql 异常。 寻求帮助
  13. 微信小程序的websocket使用stomp协议--简单实用的npm包
  14. springboot之ice规则引擎探索
  15. JS实现抖音小姐姐表白源码
  16. pyspider安装问题解决Please specify --curl-dir=/path/to/built/libcurl
  17. 执行cmake .. 报错:Cannot find llvm-lit
  18. 西部矿业(601168):整合湖北铅锌资源
  19. 智能家居设备可能被利用变成家庭虐待的工具
  20. 汉字简体与繁体互相转换

热门文章

  1. 八大流行的微服务架构设计模式探究
  2. flash player 11.2 64位 linux,Adobe Flash Player 11.2.202 Beta 1支持 64位操作系统
  3. 有道云笔记分享_有道云笔记的使用分享
  4. 利用pdfbox和itext包将pdf转换为图片
  5. 国外IDC管理系统哪个比较好用
  6. python 循环语句 教学设计_pythonwhile循环教案
  7. 平面设计是做什么的?平面设计工作内容有哪些?
  8. 最新可用ip地址查询接口
  9. Unity 手机VR GoogleVR 详细配置教程
  10. pageadmin CMS网站建设教程:模板中获取自定义文件的url节点值