现在很多的APP,尤其是对于图片处理的APP,都有关于拍照完之后添加水印的功能,我之前是在大名鼎鼎的Keep上看到了这种效果,当时就觉得体验非常不错。后来过了好长时间之后,回过头来再次思考这个问题的时候发现其实就是通过操作图片的bitmap最后生成合成的图片的业务逻辑。当然,我们在本篇中不讨论如何整合bitmap,我们的重点在于实现这种类似贴纸的效果。在上篇博客中,我已经分享了关于多点触控操作图片的技术分析,在本篇博客中,也会用到这些技术。如果有不明白的朋友,可以参考一步步教你实现图片放大、平移、旋转这篇文章。
大家先看看图片效果:
image.jpg
(我去,图片这么大!)
功能列表
通过图片,我来说一下我们要做哪些事情:
- 触摸右上角的图片,实现单指操作图片旋转。
- 触摸左下角的图片,实现单指操作图片缩放。
- 触摸照片,实现单指平移图片功能。
- 双指触摸照片,实现多点操作图片缩放。
实现步骤
根据制定的功能,接下来我来说说要实现的哪些东西:
1. Matrix
我们需要利用Matrix去操作矩阵的变换,通过变换矩阵来最终改变图片。我声明了三个变量:
// 绘制图片的矩阵
private Matrix matrix;
// 手指按下时图片的矩阵
private Matrix downMatrix = new Matrix();
// 手指移动时图片的矩阵
private Matrix moveMatrix = new Matrix();
2. Bitmap
我们要操作的是Bitmap,因此我们需要声明页面上图片的bitmap变量:
// 资源图片的位图
private Bitmap srcImage;
// 资源缩放图片的位图和矩形区域
private Bitmap srcImageResize;
private Rect rectResize;
// 资源旋转图片的位图和矩形区域
private Bitmap srcImageRotate;
private Rect rectRotate;
3. PointF
由于我们需要根据一个参照点去完成缩放,旋转的操作,因此我们需要去声明两个中心点:
// 多点触屏时的中心点
private PointF midPoint = new PointF();
// 图片的中心点坐标
private PointF imageMidPoint = new PointF();
在这里,需要指出,midPoint是手指间触屏后的中心点,而imageMidPoint是为了完成单指旋转和缩放的参考中心点。
4. Action Mode
我们要根据不同触摸事件完成不同的操作,因此需要定义一个操作模式:
// 触控模式
private int mode;
private static final int NONE = 0; // 无模式
private static final int TRANS = 1; // 拖拽模式
private static final int ROTATE = 2; // 单点旋转模式
private static final int ZOOM_SINGLE = 3; // 单点缩放模式
private static final int ZOOM_MULTI = 4; // 多点缩放模式
5. onTouchEvent()
我们的操作核心逻辑都在这个方法中。在上一节,我们已经详细分析过。
6. invalidate()
最终完成矩阵变换之后,我们得到最终的绘图矩阵,来回调onDraw()。
核心代码onTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
// 旋转手势验证
if (isInActionCheck(event, rectRotate)) {
mode = ROTATE;
imageMidPoint = getImageMidPoint();
oldRotation = getSpaceRotation(event);
downMatrix.set(matrix);
Log.d("onTouchEvent", "旋转手势");
}
// 单点缩放手势验证
else if (isInActionCheck(event, rectResize)) {
mode = ZOOM_SINGLE;
imageMidPoint = getImageMidPoint();
oldDistance = getSingleTouchDistance(event);
downMatrix.set(matrix);
Log.d("onTouchEvent", "单点缩放手势");
}
// 平移手势验证
else if (isTranslationActionCheck(srcImage, downX, downY)) {
mode = TRANS;
downMatrix.set(matrix);
Log.d("onTouchEvent", "平移手势");
}
break;
case MotionEvent.ACTION_POINTER_DOWN: // 多点触控
mode = ZOOM_MULTI;
oldDistance = getMultiTouchDistance(event);
midPoint = getMidPoint(event);
downMatrix.set(matrix);
break;
case MotionEvent.ACTION_MOVE:
// 单点旋转
if (mode == ROTATE) {
moveMatrix.set(downMatrix);
float deltaRotation = getSpaceRotation(event) - oldRotation;
moveMatrix.postRotate(deltaRotation, imageMidPoint.x, imageMidPoint.y);
matrix.set(moveMatrix);
invalidate();
}
// 单点缩放
else if (mode == ZOOM_SINGLE) {
moveMatrix.set(downMatrix);
float scale = getSingleTouchDistance(event) / oldDistance;
moveMatrix.postScale(scale, scale, imageMidPoint.x, imageMidPoint.y);
matrix.set(moveMatrix);
invalidate();
}
// 多点缩放
else if (mode == ZOOM_MULTI) {
moveMatrix.set(downMatrix);
float scale = getMultiTouchDistance(event) / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);
matrix.set(moveMatrix);
invalidate();
}
// 平移
else if (mode == TRANS) {
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - downX, event.getY() - downY);
matrix.set(moveMatrix);
invalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
mode = NONE;
break;
default:
break;
}
return true;
}
获取中心点
/**
* 获取手势中心点
*
* @param event
*/
private PointF getMidPoint(MotionEvent event) {
PointF point = new PointF();
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
return point;
}
/**
* 获取图片中心点
*/
private PointF getImageMidPoint() {
PointF point = new PointF();
float[] points = getBitmapPoints(srcImage, moveMatrix);
float x1 = points[0];
float x2 = points[2];
float y2 = points[3];
float y4 = points[7];
point.set((x1 + x2) / 2, (y2 + y4) / 2);
return point;
}
获取单点缩放和多点缩放的点与点之间距离
/**
* 【多点缩放】获取手指间的距离
*
* @param event
* @return
*/
private float getMultiTouchDistance(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
/**
* 【单点缩放】获取手指和图片中心点的距离
*
* @param event
* @return
*/
private float getSingleTouchDistance(MotionEvent event) {
float x = event.getX(0) - imageMidPoint.x;
float y = event.getY(0) - imageMidPoint.y;
return (float) Math.sqrt(x * x + y * y);
}
针对触摸事件的边界判断
/**
* 检查边界
*
* @param x
* @param y
* @return true - 在边界内 | false - 超出边界
*/
private boolean isTranslationActionCheck(Bitmap bitmap, float x, float y) {
if (bitmap == null) return false;
float[] points = getBitmapPoints(bitmap, moveMatrix);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
+ Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
+ Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
+ Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
return true;
}
return false;
}
/**
* 判断手指触摸的区域是否在顶点的操作按钮内
*
* @param event
* @param rect
* @return
*/
private boolean isInActionCheck(MotionEvent event, Rect rect) {
int left = rect.left;
int right = rect.right;
int top = rect.top;
int bottom = rect.bottom;
return event.getX(0) >= left && event.getX(0) <= right && event.getY(0) >= top && event.getY(0) <= bottom;
}
通过以上的逻辑,我们即可完成这种贴纸的简单效果。以下是源码:
StickerView
当然,本例依然存在一些手势触摸的问题。这些问题在大家的开发中肯定也会遇到,我会尽力修复这些问题。并且多多提供一些案例,与大家共同思考。
网友评论