其實就是自定義一套觸摸事件規則,加上對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);
}
}
}
實際效果如下: