准备
高级涂鸦涉及到图片操作,包括对图片进行缩放移动、涂鸦等,这里涉及到矩阵的变换。关于矩阵变换的知识,请查看我的另一篇文章《浅谈矩阵变换——Matrix》。根据文中的介绍,接下来使用变换坐标系的空间想象去理解涂鸦中涉及到的矩阵变换。
高级涂鸦
高级涂鸦支持对图片涂鸦, 可移动缩放图片。思路如下:
创建自定义View: AdvancedDoodleView,由外部创建时传入Bitmap图像对象。
在View大小确定时候的回调onSizeChanged()中进行初始化操作,计算图片居中显示的所需参数,如图片缩放倍数和偏移值。
定义PathItem类,封装涂鸦轨迹,包括Path和偏移值等信息。
class PathItem { Path mPath = new Path(); // 涂鸦轨迹 float mX, mY; // 轨迹偏移值}
单击时需要判断是否点中某个涂鸦,Path提供了接口computeBounds()计算当前图形的矩形范围,可以通过判断单击的点是否在矩形范围内判断。使用TouchGestureDetector识别单击和滑动手势。(TouchGestureDetector在我另一个项目Androids中,使用时需要导入依赖)
滑动过程中需要判断当前是否有选中的涂鸦,如果有则对该涂鸦进行移动,把偏移值记录在PathItem中;没有则绘制新的涂鸦轨迹。
监听双指缩放手势,计算图片缩放的倍数。
(4-6中涉及到的触摸坐标要换算成对应图片坐标系中的坐标,稍后详细讲解 )
在AdvancedDoodleView的onDraw方法中,根据图片缩放倍数和偏移值绘制图片;绘制每个PathItem之前根据偏移值移动画布。
坐标映射
选择画布和图片共用一个坐标系,了解图片的位置信息后,最后需要处理的就是,屏幕坐标系与图片(画布)坐标系的映射,即把屏幕上滑动的轨迹投射到图片中。
image
从上图的分析中,我们可以得出如下映射关系:
图片坐标x=(屏幕坐标x-图片在屏幕坐标系x轴上的偏移量)/图片缩放倍数 图片坐标y=(屏幕坐标y-图片在屏幕坐标系y轴上的偏移量)/图片缩放倍数
(注意,图片是以左上角为中心进行缩放的)
对应代码:
/** * 将屏幕触摸坐标x转换成在图片中的坐标x */public final float toX(float touchX) { return (touchX - mBitmapTransX) / mBitmapScale; }/** * 将屏幕触摸坐标y转换成在图片中的坐标y */public final float toY(float touchY) { return (touchY - mBitmapTransY) / mBitmapScale; }
可见,屏幕坐标投射到图片上时,需要减去偏移量,因为图片的位置是一直不变的,我们对图片进行偏移,其实是对View的画布进行偏移。
最终实现效果如下:
image
代码如下:
public class AdvancedDoodleView extends View { private final static String TAG = "AdvancedDoodleView"; private Paint mPaint = new Paint(); private List<PathItem> mPathList = new ArrayList<>(); // 保存涂鸦轨迹的集合 private TouchGestureDetector mTouchGestureDetector; // 触摸手势监听 private float mLastX, mLastY; private PathItem mCurrentPathItem; // 当前的涂鸦轨迹 private PathItem mSelectedPathItem; // 选中的涂鸦轨迹 private Bitmap mBitmap; private float mBitmapTransX, mBitmapTransY, mBitmapScale = 1; public AdvancedDoodleView(Context context, Bitmap bitmap) { super(context); mBitmap = bitmap; // 设置画笔 mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(20); mPaint.setAntiAlias(true); mPaint.setStrokeCap(Paint.Cap.ROUND); // 由手势识别器处理手势 mTouchGestureDetector = new TouchGestureDetector(getContext(), new TouchGestureDetector.OnTouchGestureListener() { RectF mRectF = new RectF(); // 缩放手势操作相关 Float mLastFocusX; Float mLastFocusY; float mTouchCentreX, mTouchCentreY; @Override public boolean onScaleBegin(ScaleGestureDetectorApi27 detector) { Log.d(TAG, "onScaleBegin: "); mLastFocusX = null; mLastFocusY = null; return true; } @Override public void onScaleEnd(ScaleGestureDetectorApi27 detector) { Log.d(TAG, "onScaleEnd: "); } @Override public boolean onScale(ScaleGestureDetectorApi27 detector) { // 双指缩放中 Log.d(TAG, "onScale: "); // 屏幕上的焦点 mTouchCentreX = detector.getFocusX(); mTouchCentreY = detector.getFocusY(); if (mLastFocusX != null && mLastFocusY != null) { // 焦点改变 float dx = mTouchCentreX - mLastFocusX; float dy = mTouchCentreY - mLastFocusY; // 移动图片 mBitmapTransX = mBitmapTransX + dx; mBitmapTransY = mBitmapTransY + dy; } // 缩放图片 mBitmapScale = mBitmapScale * detector.getScaleFactor(); if (mBitmapScale < 0.1f) { mBitmapScale = 0.1f; } invalidate(); mLastFocusX = mTouchCentreX; mLastFocusY = mTouchCentreY; return true; } @Override public boolean onSingleTapUp(MotionEvent e) { // 单击选中 float x = toX(e.getX()), y = toY(e.getY()); boolean found = false; for (PathItem path : mPathList) { // 绘制涂鸦轨迹 path.mPath.computeBounds(mRectF, true); // 计算涂鸦轨迹的矩形范围 mRectF.offset(path.mX, path.mY); // 加上偏移 if (mRectF.contains(x, y)) { // 判断是否点中涂鸦轨迹的矩形范围内 found = true; mSelectedPathItem = path; break; } } if (!found) { // 没有点中任何涂鸦 mSelectedPathItem = null; } invalidate(); return true; } @Override public void onScrollBegin(MotionEvent e) { // 滑动开始 Log.d(TAG, "onScrollBegin: "); float x = toX(e.getX()), y = toY(e.getY()); if (mSelectedPathItem == null) { mCurrentPathItem = new PathItem(); // 新的涂鸦 mPathList.add(mCurrentPathItem); // 添加的集合中 mCurrentPathItem.mPath.moveTo(x, y); } mLastX = x; mLastY = y; invalidate(); // 刷新 } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 滑动中 Log.d(TAG, "onScroll: " + e2.getX() + " " + e2.getY()); float x = toX(e2.getX()), y = toY(e2.getY()); if (mSelectedPathItem == null) { // 没有选中的涂鸦 mCurrentPathItem.mPath.quadTo( mLastX, mLastY, (x + mLastX) / 2, (y + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑 } else { // 移动选中的涂鸦 mSelectedPathItem.mX = mSelectedPathItem.mX + x - mLastX; mSelectedPathItem.mY = mSelectedPathItem.mY + y - mLastY; } mLastX = x; mLastY = y; invalidate(); // 刷新 return true; } @Override public void onScrollEnd(MotionEvent e) { // 滑动结束 Log.d(TAG, "onScrollEnd: "); float x = toX(e.getX()), y = toY(e.getY()); if (mSelectedPathItem == null) { mCurrentPathItem.mPath.quadTo( mLastX, mLastY, (x + mLastX) / 2, (y + mLastY) / 2); // 使用贝塞尔曲线 让涂鸦轨迹更圆滑 mCurrentPathItem = null; // 轨迹结束 } invalidate(); // 刷新 } }); // 针对涂鸦的手势参数设置 // 下面两行绘画场景下应该设置间距为大于等于1,否则设为0双指缩放后抬起其中一个手指仍然可以移动 mTouchGestureDetector.setScaleSpanSlop(1); // 手势前识别为缩放手势的双指滑动最小距离值 mTouchGestureDetector.setScaleMinSpan(1); // 缩放过程中识别为缩放手势的双指最小距离值 mTouchGestureDetector.setIsLongpressEnabled(false); mTouchGestureDetector.setIsScrollAfterScaled(false); } @Override protected void onSizeChanged(int width, int height, int oldw, int oldh) { //view绘制完成时 大小确定 super.onSizeChanged(width, height, oldw, oldh); int w = mBitmap.getWidth(); int h = mBitmap.getHeight(); float nw = w * 1f / getWidth(); float nh = h * 1f / getHeight(); float centerWidth, centerHeight; // 1.计算使图片居中的缩放值 if (nw > nh) { mBitmapScale = 1 / nw; centerWidth = getWidth(); centerHeight = (int) (h * mBitmapScale); } else { mBitmapScale = 1 / nh; centerWidth = (int) (w * mBitmapScale); centerHeight = getHeight(); } // 2.计算使图片居中的偏移值 mBitmapTransX = (getWidth() - centerWidth) / 2f; mBitmapTransY = (getHeight() - centerHeight) / 2f; invalidate(); } /** * 将屏幕触摸坐标x转换成在图片中的坐标 */ public final float toX(float touchX) { return (touchX - mBitmapTransX) / mBitmapScale; } /** * 将屏幕触摸坐标y转换成在图片中的坐标 */ public final float toY(float touchY) { return (touchY - mBitmapTransY) / mBitmapScale; } @Override public boolean dispatchTouchEvent(MotionEvent event) { boolean consumed = mTouchGestureDetector.onTouchEvent(event); // 由手势识别器处理手势 if (!consumed) { return super.dispatchTouchEvent(event); } return true; } @Override protected void onDraw(Canvas canvas) { // 画布和图片共用一个坐标系,只需要处理屏幕坐标系到图片(画布)坐标系的映射关系(toX toY) canvas.translate(mBitmapTransX, mBitmapTransY); canvas.scale(mBitmapScale, mBitmapScale); // 绘制图片 canvas.drawBitmap(mBitmap, 0, 0, null); for (PathItem path : mPathList) { // 绘制涂鸦轨迹 canvas.save(); canvas.translate(path.mX, path.mY); // 根据涂鸦轨迹偏移值,偏移画布使其画在对应位置上 if (mSelectedPathItem == path) { mPaint.setColor(Color.YELLOW); // 点中的为黄色 } else { mPaint.setColor(Color.RED); // 其他为红色 } canvas.drawPath(path.mPath, mPaint); canvas.restore(); } } /** * 封装涂鸦轨迹对象 */ private static class PathItem { Path mPath = new Path(); // 涂鸦轨迹 float mX, mY; // 轨迹偏移值 } }
使用时通过如下代码添加到父容器中:
// 高级级涂鸦 ViewGroup advancedContainer = findViewById(R.id.container_advanced_doodle); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thelittleprince2); AdvancedDoodleView advancedDoodleView = new AdvancedDoodleView(this, bitmap); advancedContainer.addView(advancedDoodleView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
后续
涂鸦最核心的原理就是这样,希望各位能理解透。至于如何在图片中添加文字图片或者其他类似涂鸦的,其实跟代码中定义的PathItem代表涂鸦轨迹一样,我们用新的类封装新的涂鸦类型即可,然后保存相关信息,最终在画布上绘制出来即可。
作者:远方的风景2018
链接:https://www.jianshu.com/p/aa9b3dd6e954
共同学习,写下你的评论
评论加载中...
作者其他优质文章