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

作者 : 开心源码 本文共6793个字,预计阅读时间需要17分钟 发布时间: 2022-05-14 共182人阅读

前言

前面写了《Android涂鸦画板原理详解——从初级到高级(一)》,讲了涂鸦原理初级和中级的应用,现在讲解高级应用。假如没有看过前面一篇文章的同学,建议先去看看哈。

准备

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

高级涂鸦

高级涂鸦支持对图片涂鸦, 可移动缩放图片。思路如下:

  1. 创立自己设置View: AdvancedDoodleView,由外部创立时传入Bitmap图像对象。
  2. 在View大小确定时候的回调onSizeChanged()中进行初始化操作,计算图片居中显示的所需参数,如图片缩放倍数和偏移值。
  3. 定义PathItem类,封装涂鸦轨迹,包括Path和偏移值等信息。
class PathItem {    Path mPath = new Path(); // 涂鸦轨迹    float mX, mY; // 轨迹偏移值}
  1. 单击时需要判断能否点中某个涂鸦,Path提供了接口computeBounds()计算当前图形的矩形范围,可以通过判断单击的点能否在矩形范围内判断。使用TouchGestureDetector识别单击和滑动手势。(TouchGestureDetector在我另一个项目Androids中,使用时需要导入依赖)

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

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

4-6中涉及到的触摸坐标要换算成对应图片坐标系中的坐标,稍后详细讲解 )

  1. 在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<PathItem> 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 < 0.1f) {                    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代表涂鸦轨迹一样,我们用新的类封装新的涂鸦类型就可,而后保存相关信息,最终在画布上绘制出来就可。

涂鸦原理的系列文章终于讲完了!谢谢大家关注和支持!谢谢!!!

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

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

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android涂鸦画板原理详解——从初级到高级(二)

发表回复