前言
最近有需求要做一个画布,这个画布以一个图片为背景,可以实现缩放,涂鸦以及贴纸的功能,缩放和涂鸦要兼顾,于是就想到了可以加入手势和多点触控,大致就是两只手指头可以拖动或者旋转或者放大,单只手指可以涂鸦画东西之类的,恩,具体的需求在这里先描述了,然后看下大致的实现。
效果展示
思路
- 思考一
通过继承ImageView,类似PhotoView 的实现,因为photoivew 已经实现了旋转和缩放的功能,在其基础上继承拓展,只需要复写onDraw方法,将触摸的轨迹转化为Path 直接draw到canvas上即可。可以实现的,但是要注意一点,那就是坐标转化:你的单个手指移动的轨迹坐标点们是相对于这个view的位置的,当你旋转或者缩放这个view 的时候,结果是先前保存的坐标轨迹是无法匹配到当前旋转或缩放处理后的view,这个时候就需要你将坐标轨迹进行映射处理
。 - 思 考二
则是直接复写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); //缩放监听
}
-
涂鸦
就是将拇指略过之处的所以坐标连接起来,而这个坐标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(); }
-
缩放
那么如何得到缩放的中心点呢?实现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 }
-
拖动
拖动和缩放类似,都是对当前涂鸦坐标做转换,另对底图矩阵做变换private void onDragAction(float distanceX, float distanceY, MotionEvent event) { //底图变化 currentDrawedBgM.postTranslate((int) distanceX, (int) distanceY); apply2DrawedCanvas(); //涂鸦坐标转换 convertDrawedPointPosition(distanceX,distanceY); drawDrawedPosition(); }
-
旋转
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]); }
- 撤销
撤销就是你首先保存了涂鸦的坐标组,和原始的底图,将坐标组坐标减一,重新画到原始底图上。
恢复类似,代码我就不贴出来了。mBGCanvas.drawBitmap(curSketchData.backgroundBMOrigin, currentDrawedBgM, null); mBGCanvas.drawPath(mPath);
- 贴纸
其实贴纸的逻辑,和增加第一个底图的逻辑是一直的,只不过要加一个flag来标志操作的是贴纸 还是 底图。这里推荐大家看下这篇文章Android贴纸。
总结
在做图片处理时,首要理解坐标的转换,矩阵有着非常重要的地位,理解好android提供的Martix,很多类似的问题都会事倍功半。