android画板---涂鸦,缩放,旋转,贴纸实现

前言

最近有需求要做一个画布,这个画布以一个图片为背景,可以实现缩放,涂鸦以及贴纸的功能,缩放和涂鸦要兼顾,于是就想到了可以加入手势和多点触控,大致就是两只手指头可以拖动或者旋转或者放大,单只手指可以涂鸦画东西之类的,恩,具体的需求在这里先描述了,然后看下大致的实现。

效果展示

自定义view.gif

思路

  1. 思考一
    通过继承ImageView,类似PhotoView 的实现,因为photoivew 已经实现了旋转和缩放的功能,在其基础上继承拓展,只需要复写onDraw方法,将触摸的轨迹转化为Path 直接draw到canvas上即可。可以实现的,但是要注意一点,那就是坐标转化:你的单个手指移动的轨迹坐标点们是相对于这个view的位置的,当你旋转或者缩放这个view 的时候,结果是先前保存的坐标轨迹是无法匹配到当前旋转或缩放处理后的view,这个时候就需要你将坐标轨迹进行映射处理
  2. 思 考二
    则是直接复写View控件,通过将图片直接转换为bitmap后,draw到view 的画布上。整个过程就是先在bitmap上新建一个画布,然后将轨迹坐标draw到这个bitmap的canvas上,也就是这个bitmap上,最后在onDraw的回调里面,将这个bitmap 画到整个View 的canvas上,当然,最后要自行实现bitmap的缩放,旋转等坐标转换功能,好处是先前的涂鸦会一直保持。

预先准备

这个时候就必须要提一下Martix,Andorid 贴心的给我们提供了这样一个工具类,我们完全可以摆脱坐标点计算之苦啦。
在这里强烈推荐大家看下 android matrix 最全方法详解与进阶(完整篇),原理以及api介绍的相当详细。

实现

考虑到要有贴图,并且贴图支持大小缩放的功能,拖动功能,采用了第二种方式,其实感觉采用第一种方式应该会更简单点(微笑脸),好了,下面介绍下具体实现
首先要处理这个view 的touch事件:

 if (actionMode == ACTION_DRAG) {
      onDragAction(curX - preX, curY - preY, event);//拖动监听
  } else if (actionMode == ACTION_ROTATE) {
      onRotateAction(curPhotoRecord);//旋转监听
  } else if (actionMode == ACTION_SCALE) {
      mScaleGestureDetector.onTouchEvent(event); //缩放监听
  }          

  1. 涂鸦
    就是将拇指略过之处的所以坐标连接起来,而这个坐标id呢,不是绝对坐标,而是对于这个view 的相对坐标(毕竟还要支持缩放和撤销操作的),单是缩放则不用过多约束,只要将path画到bitmap Canvas上,显示出来即可,但是需要支持撤销,这就要求必须要保持每一个笔画的坐标点组啦,缩放或者旋转时,相对于这个view 的坐标肯定会发生变化,大致给下代码:
//缩放处理描点位置
    private void convertDrawedPoiontsPosition(float scaleX, float scaleY, float x, float y) {
        curTextSize = curTextSize * scaleX;
        textPaint.setTextSize(curTextSize);

        Matrix pointsMatrix = new Matrix();
        pointsMatrix.postScale(scaleX,scaleY,x,y); //scaleX 为 x方向缩放参数,scaleY为y轴缩放参数,(x,y)为缩放中心点坐标
        for( Object object :curSketchData.drawPathList){//drawPathList为存放坐标的数组
            if(object instanceof SketchData.Angle){
                SketchData.Angle angle = (SketchData.Angle)object;
                float[] photoCornersSrc = new float[6];
                float[] photoCorners = new float[6];
                photoCornersSrc[0] = angle.start.x;
                photoCornersSrc[1] = angle.start.y;
                photoCornersSrc[2] = angle.middle.x;
                photoCornersSrc[3] = angle.middle.y;
                photoCornersSrc[4] = angle.end.x;
                photoCornersSrc[5] = angle.end.y;
                //angle.matrix.mapPoints(photoCorners, photoCornersSrc);
                pointsMatrix.mapPoints(photoCorners, photoCornersSrc);
                angle.start.x = photoCorners[0];
                angle.start.y = photoCorners[1];
                angle.middle.x = photoCorners[2];
                angle.middle.y = photoCorners[3];
                angle.end.x = photoCorners[4];
                angle.end.y = photoCorners[5];
            }else if(object instanceof SketchData.Length){
                SketchData.Length length = (SketchData.Length)object;
                float[] photoCornersSrc = new float[4];
                float[] photoCorners = new float[4];
                photoCornersSrc[0] = length.start.x;
                photoCornersSrc[1] = length.start.y;
                photoCornersSrc[2] = length.end.x;
                photoCornersSrc[3] = length.end.y;
                //angle.matrix.mapPoints(photoCorners, photoCornersSrc);
                pointsMatrix.mapPoints(photoCorners, photoCornersSrc);
                length.start.x = photoCorners[0];
                length.start.y = photoCorners[1];
                length.end.x = photoCorners[2];
                length.end.y = photoCorners[3];
            }

        }
    
        drawDrawedPosition();

    }
  1. 缩放
    那么如何得到缩放的中心点呢?实现ScaleGestureDetector 实例,调用onTouchEvent,此时会回调onScale(ScaleGestureDetector detector),我们来看下使用这个detector 的具体逻辑
private void onScaleAction(ScaleGestureDetector detector) {
            Log.e("shang", "onscale :" + detector.getScaleFactor());
            float[] photoCorners = calculateBgCorners(backgroundSrcRect);//获取现阶段底图的标志坐标点
            //目前图片对角线长度
            float len = (float) Math.sqrt(Math.pow(photoCorners[0] - photoCorners[4], 2) + Math.pow(photoCorners[1] - photoCorners[5], 2));
            double photoLen = Math.sqrt(Math.pow(backgroundSrcRect.width(), 2) + Math.pow(backgroundSrcRect.height(), 2));
            float scaleFactor = detector.getScaleFactor();
            //设置Matrix缩放参数
            if ((scaleFactor < 1 && len >= photoLen * SCALE_MIN && len >= SCALE_MIN_LEN) || (scaleFactor > 1 && len <= photoLen * SCALE_MAX)) {
                Log.e(scaleFactor + "", scaleFactor + "");
                convertDrawedPoiontsPosition(scaleFactor, scaleFactor, photoCorners[8], photoCorners[9]);//涂鸦点坐标转换
                currentDrawedBgM.postScale(scaleFactor, scaleFactor, photoCorners[8], photoCorners[9]);//底图矩阵缩放
                apply2DrawedCanvas();
                mScaleValue = scaleFactor * mScaleValue;
                Log.e("shang", "scale :" + mScaleValue);
                drawDrawedPosition();
            }

    }

其中

 private float[] calculateBgCorners(RectF rectF) {
        float[] photoCornersSrc = new float[10];//0,1代表左上角点XY,2,3代表右上角点XY,4,5代表右下角点XY,6,7代表左下角点XY,8,9代表中心点XY
        float[] photoCorners = new float[10];//0,1代表左上角点XY,2,3代表右上角点XY,4,5代表右下角点XY,6,7代表左下角点XY,8,9代表中心点XY
        photoCornersSrc[0] = rectF.left;
        photoCornersSrc[1] = rectF.top;
        photoCornersSrc[2] = rectF.right;
        photoCornersSrc[3] = rectF.top;
        photoCornersSrc[4] = rectF.right;
        photoCornersSrc[5] = rectF.bottom;
        photoCornersSrc[6] = rectF.left;
        photoCornersSrc[7] = rectF.bottom;
        photoCornersSrc[8] = rectF.centerX();
        photoCornersSrc[9] = rectF.centerY();
        currentDrawedBgM.mapPoints(photoCorners, photoCornersSrc);//现阶段的底图的矩阵
        return photoCorners;
    }

其中

  private void apply2DrawedCanvas() {
        Matrix matrix = new Matrix();
        currentDrawedBgM.invert(matrix);
        mBGCanvas.setMatrix(matrix);//mBGCanvas为底图bitmap所在的canvas
    }
  1. 拖动
    拖动和缩放类似,都是对当前涂鸦坐标做转换,另对底图矩阵做变换
private void onDragAction(float distanceX, float distanceY, MotionEvent event) {
        //底图变化
         currentDrawedBgM.postTranslate((int) distanceX, (int) distanceY);
         apply2DrawedCanvas();
        //涂鸦坐标转换
         convertDrawedPointPosition(distanceX,distanceY);
         drawDrawedPosition();

    }
  1. 旋转
    private void onRotateAction(PhotoRecord record) {
        float[] corners = calculateCorners(record);
        //放大
        //目前触摸点与图片显示中心距离
        float a = (float) Math.sqrt(Math.pow(curX - corners[8], 2) + Math.pow(curY - corners[9], 2));
        //目前上次旋转图标与图片显示中心距离
        float b = (float) Math.sqrt(Math.pow(corners[4] - corners[0], 2) + Math.pow(corners[5] - corners[1], 2)) / 2;
        //旋转
        //根据移动坐标的变化构建两个向量,以便计算两个向量角度.
        PointF preVector = new PointF();
        PointF curVector = new PointF();
        preVector.set(preX - corners[8], preY - corners[9]);//旋转后向量
        curVector.set(curX - corners[8], curY - corners[9]);//旋转前向量
        //计算向量长度
        double preVectorLen = getVectorLength(preVector);
        double curVectorLen = getVectorLength(curVector);
        //计算两个向量的夹角.
        double cosAlpha = (preVector.x * curVector.x + preVector.y * curVector.y)
                / (preVectorLen * curVectorLen);
        //由于计算误差,可能会带来略大于1的cos,例如
        if (cosAlpha > 1.0f) {
            cosAlpha = 1.0f;
        }
        //本次的角度已经计算出来。
        double dAngle = Math.acos(cosAlpha) * 180.0 / Math.PI;
        // 判断顺时针和逆时针.
        //判断方法其实很简单,这里的v1v2其实相差角度很小的。
        //先转换成单位向量
        preVector.x /= preVectorLen;
        preVector.y /= preVectorLen;
        curVector.x /= curVectorLen;
        curVector.y /= curVectorLen;
        //作curVector的逆时针垂直向量。
        PointF verticalVec = new PointF(curVector.y, -curVector.x);

        //判断这个垂直向量和v1的点积,点积>0表示俩向量夹角锐角。=0表示垂直,<0表示钝角
        float vDot = preVector.x * verticalVec.x + preVector.y * verticalVec.y;
        if (vDot > 0) {
            //v2的逆时针垂直向量和v1是锐角关系,说明v1在v2的逆时针方向。
        } else {
            dAngle = -dAngle;
        }
        currentDrawedBgM.postRotate((float) dAngle, corners[8], corners[9]);
    }
  1. 撤销
    撤销就是你首先保存了涂鸦的坐标组,和原始的底图,将坐标组坐标减一,重新画到原始底图上。
    恢复类似,代码我就不贴出来了。
  mBGCanvas.drawBitmap(curSketchData.backgroundBMOrigin, currentDrawedBgM, null);
   mBGCanvas.drawPath(mPath);
  1. 贴纸
    其实贴纸的逻辑,和增加第一个底图的逻辑是一直的,只不过要加一个flag来标志操作的是贴纸 还是 底图。这里推荐大家看下这篇文章Android贴纸

总结

在做图片处理时,首要理解坐标的转换,矩阵有着非常重要的地位,理解好android提供的Martix,很多类似的问题都会事倍功半。

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容

  • 手势图片控件 PinchImageView 点击图片框架 photoView packagecom.example...
    Ztufu阅读 687评论 0 1
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,663评论 0 33
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,214评论 0 17
  • 版权声明:本文为博主原创文章,未经博主允许不得转载 前言 Canvas 本意是画布的意思,然而将它理解为绘制工具一...
    cc荣宣阅读 41,326评论 1 47
  • 有的时候是我们把事情弄反了 现在这个年代,本来就是90后00后 干掉80后,移动互联网+人才辈出 模式更新,摩尔定...
    红日言知有理阅读 129评论 0 0