美文网首页Android技术知识Android开发经验谈Android开发
Android涂鸦画板原理详解——从初级到高级(二)

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

作者: 远方的风景2018 | 来源:发表于2018-10-26 20:10 被阅读67次

    前言

    前面写了《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!一个功能强大,可自定义和可扩展的涂鸦框架

    相关文章

      网友评论

        本文标题:Android涂鸦画板原理详解——从初级到高级(二)

        本文链接:https://www.haomeiwen.com/subject/mwiltqtx.html