其实就是自定义一套触摸事件规则,加上对Matrix的使用即可。
首先定义基类,首先不同类型的图元,例如Bitmap或者文本,需要的缩放、移动、测量、绘制方式可能都不一致,所以做成抽象函数顶个接口规范,等待子类自己实现:
package com.chenjiezhu.waterMarkShape;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
import android.view.MotionEvent;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
public abstract class BaseShape {
private PointF mCurrentCenter = new PointF();
private PointF mPrevCurrentCenter = null;
private float mPrevDistance = Float.MIN_VALUE;
private float mAvergeX = 0, mAvergeY = 0;
private int mPrevPointCount = 0;
/**是否已经缩放过**/
private boolean mIsScaled = false;
private Queue<Float> mTouchDistanceQueue = new LinkedBlockingQueue<>();
private PointF mPrevLocation;
/**水印类型**/
public enum ShapeType {
BITMAP,
TEXT
}
private ShapeType mShapeType = null;
public void setmShapeType(ShapeType mShapeType) {
this.mShapeType = mShapeType;
}
public ShapeType getmShapeType() {
Paint paint;
return mShapeType;
}
/**移动代码实现**/
public abstract void translate(float dx, float dy);
/**缩放代码实现**/
public abstract void scale(float scaleXRatio, float scaleYRatio, float scaleCenterX, float scaleCenterY);
/**绘制代码实现**/
public abstract void draw(Canvas canvas);
/**了解显示范围**/
public abstract RectF getRange();
/**设置显示效果**/
public abstract void setPaint(Paint paint, Shader shader);
/**了解缩放量**/
public abstract PointF getScale();
/**了解位置**/
public abstract PointF getXY();
/**触摸处理**/
public boolean touchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevDistance = 0;
mPrevPointCount = event.getPointerCount();
//算出移动中心坐标、点间距离
for (int i = 0; i < event.getPointerCount(); i++) {
mAvergeX += event.getX(i);
mAvergeY += event.getY(i);
if (i + 1 < event.getPointerCount()) {
mPrevDistance += Math.sqrt(Math.pow(event.getX(i + 1) - event.getX(i), 2) + Math.pow(event.getY(i + 1) - event.getY(i), 2));
}
}
mAvergeX /= event.getPointerCount();
mAvergeY /= event.getPointerCount();
mPrevLocation = getXY();
if (mPrevCurrentCenter == null) {
mPrevCurrentCenter = new PointF(mAvergeX, mAvergeY);
} else {
mPrevCurrentCenter.set(mAvergeX, mAvergeY);
}
break;
case MotionEvent.ACTION_MOVE:
mAvergeX = 0;
mAvergeY = 0;
float nowDistance = 0;
//算出移动中心坐标、点间距离
for (int i = 0; i < event.getPointerCount(); i++) {
mAvergeX += event.getX(i);
mAvergeY += event.getY(i);
if (i + 1 < event.getPointerCount()) {
nowDistance += Math.sqrt(Math.pow(event.getX(i + 1) - event.getX(i), 2) + Math.pow(event.getY(i + 1) - event.getY(i), 2));
}
}
//现在的点间距离 除以 上次点间距离 这次得到缩放比例
mAvergeX /= event.getPointerCount();
mAvergeY /= event.getPointerCount();
if ((mPrevPointCount != event.getPointerCount()) || event.getPointerCount() <= 1 || mPrevPointCount <= 1) { //触摸点数突然改变 或者 触摸点不超过2,不允许缩放
mPrevDistance = nowDistance = 0;
}
//如果缩放数据有效,则进行平均平滑化并且进行缩放
if (mPrevDistance > 0 && nowDistance > 0) {
mTouchDistanceQueue.add(nowDistance / mPrevDistance);
if (mTouchDistanceQueue.size() >= 6) {
Float point[] = new Float[mTouchDistanceQueue.size()];
mTouchDistanceQueue.toArray(point);
float avergDistance = 0;
for (int i = 0; i < point.length; i++) {
avergDistance += point[i];
}
avergDistance /= point.length;
// scale((float) Math.sqrt(avergDistance), (float) Math.sqrt(avergDistance), mAvergeX, mAvergeY);
scale((float) Math.sqrt(avergDistance), (float) Math.sqrt(avergDistance), event.getX(0), event.getY(0));
mIsScaled = true;
while (mTouchDistanceQueue.size() > 6) {
mTouchDistanceQueue.poll();
}
}
}
mPrevPointCount = event.getPointerCount();
mPrevDistance = nowDistance;
//当前坐标 - 上次坐标 = 偏移值,然后进行位置偏移
if (mPrevCurrentCenter == null) {
mPrevCurrentCenter = new PointF(mAvergeX, mAvergeY);
} else {
if (!mIsScaled && event.getPointerCount() == 1) {
translate(mAvergeX - mPrevCurrentCenter.x, mAvergeY - mPrevCurrentCenter.y);
}
mPrevCurrentCenter.set(mAvergeX, mAvergeY);
}
break;
case MotionEvent.ACTION_UP:
//抬起,清理干净数据
mAvergeX = 0;
mAvergeY = 0;
mTouchDistanceQueue.clear();
mIsScaled = false;
break;
}
return true;
}
}
Bitmap类型子类,通过matrix控制图片移动:
package com.chenjiezhu.waterMarkShape;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
public class BitmapShape extends BaseShape {
private Matrix mMatrix;
private Bitmap mBitmap;
private RectF mRange;
public BitmapShape() {
setmShapeType(ShapeType.BITMAP);
mMatrix = new Matrix();
}
public void setmBitmap(Bitmap b) {
this.mBitmap = b;
}
@Override
public void translate(float dx, float dy) {
mMatrix.postTranslate(dx, dy);
}
@Override
public void scale(float scaleXRatio, float scaleYRatio, float scaleCenterX, float scaleCenterY) {
mMatrix.postScale(scaleXRatio, scaleYRatio, scaleCenterX, scaleCenterY);
}
@Override
public void draw(Canvas canvas) {
if (mBitmap != null) {
canvas.drawBitmap(mBitmap, mMatrix, null);
//debug code:
// Paint paint = new Paint();
// paint.setStrokeWidth(5f);
// paint.setColor(Color.RED);
// paint.setStyle(Paint.Style.STROKE);
// canvas.drawRect(getRange(), paint);
}
}
@Override
public RectF getRange() {
//cjz: you can see this to know why I do that:https://www.jianshu.com/p/c83f59613c18
float matrix[] = new float[9];
mMatrix.getValues(matrix);
mRange = new RectF(
matrix[2],
matrix[5],
matrix[2] + mBitmap.getWidth() * matrix[0],
matrix[5] + mBitmap.getHeight() * matrix[4]);
return mRange;
}
@Override
public void setPaint(Paint paint, Shader shader) {
}
@Override
public PointF getScale() {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
return new PointF(matrix[0], matrix[4]);
}
@Override
public PointF getXY() {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
return new PointF(matrix[2] + (mBitmap.getWidth() * matrix[0]) / 2f, matrix[5] + (mBitmap.getHeight() * matrix[4]) / 2f);
}
}
文本子类,较为复杂,因为文本不能直接应用Matrix进行形变,所以通过绘制时对Canvas应用Matrix进行调整来实现该功能。难点在于测量方面,大小的计算是通过字符量×画笔大小×缩放量确定的,这样确定的图元范围实测可以准确根据移动位置和缩放大小进行精确调整。
package com.chenjiezhu.waterMarkShape;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
public class TextShape extends BaseShape {
private String mText;
private Matrix mMatrix;
private Paint mPaint;
private RectF mRange;
public TextShape() {
mMatrix = new Matrix();
}
public void setText(String text) {
this.mText = text;
}
@Override
public void translate(float dx, float dy) {
mMatrix.postTranslate(dx, dy);
}
@Override
public void scale(float scaleXRatio, float scaleYRatio, float scaleCenterX, float scaleCenterY) {
mMatrix.postScale(scaleXRatio, scaleYRatio, scaleCenterX, scaleCenterY);
}
@Override
public void draw(Canvas canvas) {
if (mText != null && mPaint != null) {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
canvas.save();
canvas.translate(matrix[2], matrix[5]);
canvas.scale(matrix[0], matrix[4]);
canvas.drawText(mText, 0, 0, mPaint);
canvas.restore();
//debug code:
// Paint paint = new Paint();
// paint.setStrokeWidth(5f);
// paint.setColor(Color.BLUE);
// paint.setStyle(Paint.Style.STROKE);
// canvas.drawRect(getRange(), paint);
}
}
@Override
public RectF getRange() {
//cjz: you can see this to know why I do that:https://www.jianshu.com/p/c83f59613c18
if (mText != null && mPaint != null) {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
mRange = new RectF(
matrix[2],
matrix[5] - mPaint.getTextSize() * matrix[4],
matrix[2] + (mText.length() * mPaint.getTextSize()) * matrix[0],
matrix[5] + mPaint.getTextSize() / 2f * matrix[4]);
return mRange;
}
return null;
}
@Override
public void setPaint(Paint paint, Shader shader) {
this.mPaint = new Paint(paint);
if (shader != null) {
this.mPaint.setShader(shader);
}
}
@Override
public PointF getScale() {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
return new PointF(matrix[0], matrix[4]);
}
@Override
public PointF getXY() {
float matrix[] = new float[9];
mMatrix.getValues(matrix);
return new PointF(matrix[2],
matrix[5] - mPaint.getTextSize() * matrix[4]);
}
}
绘制View,通过遍历BaseShape并调用onDraw方法绘制图元,注意事件传递后调整图层层叠关系时,应该从顶(链表尾)到底(链表头)进行遍历,而绘制时则反之,才可以实现点击时,被点击的图元置顶的效果。另外刚刚设计的测量范围的方法在这里就可以派上用场了,可以用于确定是哪个图元被点击,就像UI SDK的设计:
package com.example.android.camera2basic;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.chenjiezhu.waterMarkShape.BaseShape;
import com.chenjiezhu.waterMarkShape.BitmapShape;
import com.chenjiezhu.waterMarkShape.TextShape;
import java.util.LinkedList;
import java.util.List;
public class WaterMarkView extends View {
private Bitmap mBitmap;
private List<BaseShape> mShapesList = new LinkedList<>();
private BitmapShape mBitmapShape;
private BaseShape mCurrentTouchShape;
public WaterMarkView(Context context) {
super(context);
init();
}
public WaterMarkView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public WaterMarkView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void init() {
mBitmap = BitmapFactory.decodeResource(getResources(), android.R.mipmap.sym_def_app_icon);
for(int i = 0; i < 3; i++) {
BitmapShape bs = new BitmapShape();
bs.setmBitmap(mBitmap);
bs.translate(50 * i, 50 * i);
mShapesList.add(bs);
}
TextShape textShape = new TextShape();
Paint paint = new Paint();
paint.setStrokeWidth(5f);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setTextSize(150f);
textShape.setPaint(paint, null);
textShape.setText("测试测试测试");
textShape.translate(200, 200);
mShapesList.add(textShape);
textShape = new TextShape();
paint = new Paint();
paint.setStrokeWidth(5f);
paint.setColor(Color.argb(0.5f, 0.1f, 0.5f, 0.5f));
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setTextSize(100f);
textShape.setPaint(paint, null);
textShape.setText("水印编辑器Demo");
textShape.translate(400, 500);
mShapesList.add(textShape);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
for (int i = mShapesList.size() - 1; i >=0; i--) { //从顶传递到底
BaseShape shape = mShapesList.get(i);
RectF shapeRange = shape.getRange();
if (shapeRange != null && shapeRange.contains(event.getX(), event.getY())) {
mCurrentTouchShape = shape;
//置顶选中图案
mShapesList.remove(mCurrentTouchShape);
mShapesList.add(mShapesList.size(), mCurrentTouchShape);
mCurrentTouchShape.touchEvent(event);
break;
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mCurrentTouchShape != null) {
mCurrentTouchShape.touchEvent(event);
break;
}
}
case MotionEvent.ACTION_UP: {
if (mCurrentTouchShape != null) {
mCurrentTouchShape.touchEvent(event);
mCurrentTouchShape = null;
break;
}
}
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (BaseShape view : mShapesList) {
view.draw(canvas);
}
}
}
实际效果如下: