上周有点事情,所以拖到今天发。
这篇blog是学习 传送门的贴纸效果。

Github在这里:
Demo地址

先上个效果图:


是一个可以做平移、缩放、旋转、删除的一个简单的贴纸效果。

上周因为学习了关于矩阵的映射,所以在这个View里面就会做得相对轻松一些。

这周在运用到矩阵的基础上,也学会了去计算双指同时滑动时产生的向量

向量与旋转

其实做这个View比较核心的地方就是 平移、缩放和旋转了。
旋转因为之前没有触及到,所以这次学习通过 矩阵Matrix来进行旋转。
而旋转的参考系,就是 两个手指触摸点的连线 与 x轴的夹角为α,通过α的变化去改变图片的旋转系数

首先我们要创建两个向量,分别是:上次双指之间的向量,和当前双指之间的向量:

    //以PointF的形式来记录向量,其实就是触摸的点的 (x1,y1)和(x2,y2)的差值,向量是需要他们进行计算之后才能得出来的//记录双指之间的向量private PointF mCurrentVector = new PointF();//记录上次双指的向量private PointF mLastVector = new PointF();

接着就是在获取到MotionEvent的时候,对双指的操作进行数据的处理

 public void onTouch(MotionEvent event) {switch (event.getActionMasked()) {....case MotionEvent.ACTION_POINTER_DOWN://通过getPointCount()来判断几个手指触摸if (event.getPointerCount() == 2) {//双指来记录两个pointmFirstPoint.set(event.getX(0), event.getY(0));mSecondPoint.set(event.getX(1), event.getY(1));//记录双指间的向量mLastVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);}break;case MotionEvent.ACTION_MOVE:if (event.getPointerCount() == 2) {//记录双指点的位置mFirstPoint.set(event.getX(0), event.getY(0));mSecondPoint.set(event.getX(1), event.getY(1));//操作旋转mCurrentVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);float rotate = calculateDegrees(mLastVector, mCurrentVector);rotate(rotate);mLastVector.set(mCurrentVector.x, mCurrentVector.y);}break;}}

然后我们将 之前的向量和当前的向量,通过对比可以知道 旋转角度的偏移量

    /*** 计算旋转度数,通过tan2()和toDegree()来计算出旋转的角度*/private float calculateDegrees(PointF lastVector, PointF currentVector) {float lastDegrees = (float) Math.atan2(lastVector.y, lastVector.x);float currentDegrees = (float) Math.atan2(currentVector.y, currentVector.x);return (float) Math.toDegrees(currentDegrees - lastDegrees);}

最后通过rotate()来旋转矩阵,其实就是通过View的 矩阵的 postRotate()方法来旋转一定的度数:

    /*** 旋转操作* 使用matrix去做偏移* 完成偏移后还要更新 points坐标*/void rotate(float degrees) {//以中心为轴旋转mMatrix.postRotate(degrees, mCenterPoint.x, mCenterPoint.y);updatePoints();}

旋转完之后,matrix发生了变化,我们要映射其四周的点,便于之后做焦点图片时,会用框 框柱它:

   private void updatePoints() {//更新贴纸坐标,srcPoints是最初时的包裹View的点坐标,dstPoints是matrix变换之后的点坐标mMatrix.mapPoints(dstPoints, srcPoints);}

通过这样我们就能完成旋转了。

贴纸类

在本demo中,并没有把贴纸类做成一个“View”,而是单纯做成一个类,它是通过 矩阵 去实现View的变化,它有旋转、平移、缩放的函数。
那么它的绘制是在哪里呢,它的点击事件又是怎么触发的呢?

我们把容纳贴纸的 ViewGroup做成一个View,通过给这个View加“贴纸类”,然后让这个View去绘制出每一个贴纸,并且接受每一个点击事件,通过点击事件去控制 单个“贴纸的行为”,这样,就不用考虑滑动冲突的问题了。onTouchEvent直接传true就是了。
我们来看看贴纸类:

    //没有任何接触、与操作的模式public static final int MODE_NONE = 0;//单指按下的时的状态,并且可以移动public static final int MODE_SINGLE = 1;//双指按下的状态,可以缩放大小public static final int MODE_POINT = 2;//设置一个内边距值private static final int PADDING = 20;//贴纸图像private Bitmap mBitmap;//删除图标图像private Bitmap mDelBitmap;//贴纸边界private RectF mBitmapBound;//删除图标边界private RectF mDelBitmapBound;//图像矩阵private Matrix mMatrix;//该贴纸是否获得焦点private boolean isFocus;//bitmap的中心点private PointF mCenterPoint;//上次双指移动的距离private float mLastDoubleDistance;//上次触摸的点private PointF mLastPoint = new PointF();//双指触控下 当前触摸的点1private PointF mFirstPoint = new PointF();//双指触控下 当前触摸的点2private PointF mSecondPoint = new PointF();//记录矩阵的点坐标,矩阵变换后也要变更private float[] srcPoints;//因为矩阵变化后 原坐标也会变化,无法变回原来的,所以需要记录一个目标的矩阵private float[] dstPoints;//记录双指之间的向量private PointF mCurrentVector = new PointF();//记录上次双指的向量private PointF mLastVector = new PointF();//当前模式private int mMode;

构造时,需要确定 这个贴纸的 边框,即srcPoints,它便于我们画边框,并且要构造删除图标:

    public RikkaStickerView(Context context, Bitmap bitmap) {mMatrix = new Matrix();mCenterPoint = new PointF();this.mBitmap = bitmap;mBitmapBound = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());srcPoints = new float[]{//左上0, 0,//右上bitmap.getWidth(), 0,//左下0, bitmap.getHeight(),//右下bitmap.getWidth(), bitmap.getHeight(),//中点bitmap.getWidth() / 2f, bitmap.getHeight() / 2f};dstPoints = srcPoints.clone();//创建删除图标并定义边界,加上paddingmDelBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_delete);mDelBitmapBound = new RectF(0 - mDelBitmap.getWidth() / 2 - PADDING, 0 - mDelBitmap.getHeight() / 2 - PADDING,mDelBitmap.getWidth() / 2f + PADDING, mDelBitmap.getHeight() / 2f + PADDING);//将贴纸默认放在屏幕中间WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics displayMetrics = new DisplayMetrics();manager.getDefaultDisplay().getMetrics(displayMetrics);float dx = displayMetrics.widthPixels / 2f - mBitmap.getWidth() / 2f;float dy = displayMetrics.heightPixels / 2f - mBitmap.getHeight() / 2f;translate(dx, dy);}

接下来是三个操作:

/*** 平移操作,偏移量为dx,dy* 使用matrix去做偏移* 完成偏移后还要更新 points坐标*/void translate(float dx, float dy) {mMatrix.postTranslate(dx, dy);updatePoints();}/*** 缩放操作,跟平移操作一样*/void scale(float scale) {//以View的中点为轴放大mMatrix.postScale(scale, scale, mCenterPoint.x, mCenterPoint.y);updatePoints();}/*** 旋转操作*/void rotate(float degrees) {//以中心为轴旋转mMatrix.postRotate(degrees, mCenterPoint.x, mCenterPoint.y);updatePoints();}private void updatePoints() {//更新贴纸坐标mMatrix.mapPoints(dstPoints, srcPoints);// 更新贴纸中心坐标mCenterPoint.set(dstPoints[8], dstPoints[9]);}

接下来就是绘制,它自己本身当然没有绘制函数,也没有画布,所以这里的onDraw方法是由父View来调用的,Canvas也是父View传过来的:

 /*** 绘制贴纸本身,这个方法需要父View去调用* Canvas 是父View的canvas,paint也是*/public void onDraw(Canvas canvas, Paint paint) {//绘制贴纸,带上matrix参数canvas.drawBitmap(mBitmap, mMatrix, paint);//如果该贴纸是被选中的目标,则要绘制其边框,以及移除按钮if (isFocus) {//画点要用points画,因为图片会变化,所以必须要有记录号的点,所以points在这里就派上用场了canvas.drawLine(dstPoints[0] - PADDING, dstPoints[1] - PADDING, dstPoints[2] + PADDING, dstPoints[3] - PADDING, paint);canvas.drawLine(dstPoints[2] + PADDING, dstPoints[3] - PADDING, dstPoints[6] + PADDING, dstPoints[7] + PADDING, paint);canvas.drawLine(dstPoints[6] + PADDING, dstPoints[7] + PADDING, dstPoints[4] - PADDING, dstPoints[5] + PADDING, paint);canvas.drawLine(dstPoints[4] - PADDING, dstPoints[5] + PADDING, dstPoints[0] - PADDING, dstPoints[1] - PADDING, paint);//绘制删除按钮canvas.drawBitmap(mDelBitmap, dstPoints[0] - mDelBitmap.getWidth() / 2f - PADDING, dstPoints[1] - mDelBitmap.getHeight() / 2f - PADDING, paint);}}

接下来是onTouchEvent方法,当然本身他本身也是不会调用的,所以TouchEvent是由父View传下来的:

   /*** 自己定义onTouch方法,根据父View传的event来做各种操作* 既然走到这个方法了,那么就说明 已经触摸到贴纸了*/public void onTouch(MotionEvent event) {switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:mMode = MODE_SINGLE;//记录按下的位置mLastPoint.set(event.getX(), event.getY());break;case MotionEvent.ACTION_POINTER_DOWN:if (event.getPointerCount() == 2) {mMode = MODE_POINT;//双指来记录两个pointmFirstPoint.set(event.getX(0), event.getY(0));mSecondPoint.set(event.getX(1), event.getY(1));//计算双指之间的距离mLastDoubleDistance = calculateDistance(mFirstPoint, mSecondPoint);//记录双指间的向量mLastVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);}break;case MotionEvent.ACTION_MOVE://通过模式来确定行为if (mMode == MODE_SINGLE) {//如果是单指拖动,则移动到指定的位置translate(event.getX() - mLastPoint.x, event.getY() - mLastPoint.y);mLastPoint.set(event.getX(), event.getY());}if (mMode == MODE_POINT && event.getPointerCount() == 2) {//记录双指点的位置mFirstPoint.set(event.getX(0), event.getY(0));mSecondPoint.set(event.getX(1), event.getY(1));//操作自由缩放float distance = calculateDistance(mFirstPoint, mSecondPoint);//根据双指移动的距离获取缩放系数float scale = distance / mLastDoubleDistance;scale(scale);mLastDoubleDistance = distance;//操作旋转mCurrentVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);float rotate = calculateDegrees(mLastVector, mCurrentVector);rotate(rotate);mLastVector.set(mCurrentVector.x, mCurrentVector.y);}break;case MotionEvent.ACTION_UP:reset();break;}}/*** 通过两个坐标来计算他们的距离*/private float calculateDistance(PointF mFirstPoint, PointF mSecondPoint) {float x = mFirstPoint.x - mSecondPoint.x;float y = mFirstPoint.y - mSecondPoint.y;return (float) Math.sqrt(x * x + y * y);}

贴纸容器View

我们需要一个容器去放这些贴纸,所以容器必须继承自View或者ViewGroup,我这里就继承自View。
它最重要的就是做两件事情:

  1. 绘制贴纸
  2. 给贴纸传递点击事件

方法如下:

    /*** 点击事件,处理单指移动,双指缩放* 单指单击删除*/@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_POINTER_DOWN://先判断是否是点到删除按钮了stickerView = RikkaStickerManager.getInstance().getDelButton(event.getX(), event.getY());if (stickerView != null) {removeSticker(stickerView);return true;}//再判断是否摸到某一个贴纸stickerView = RikkaStickerManager.getInstance().getSticker(event.getX(), event.getY());if (stickerView == null) {//当不是单指的时候,可能会存在第二个手指摸到了贴纸(先按下两个,抬起一个的情况)if (event.getPointerCount() == 2) {stickerView = RikkaStickerManager.getInstance().getSticker(event.getX(1), event.getY(1));}}if (stickerView != null) {RikkaStickerManager.getInstance().setFocusSticker(stickerView);}break;default:break;}if (stickerView != null) {stickerView.onTouch(event);} else {//如果没有点击到,则取消所有贴纸的焦点RikkaStickerManager.getInstance().clearAllFocus();}invalidate();return true;}/*** 绘制所有的子贴纸,并且根据是否被选中来画*/@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);List<RikkaStickerView> stickerViews = RikkaStickerManager.getInstance().getmStickerList();for (int i = 0; i < stickerViews.size(); i++) {RikkaStickerView stickerView = stickerViews.get(i);stickerView.onDraw(canvas, mPaint);}}

贴纸管理类

看到上面的代码,还有一个专门用来管理 贴纸的Manager单例类,
它其实也很简单,就是维护一个 贴纸列表,每次传入一个坐标,就判断这个坐标在不在 这些贴纸里面。
而这个方法,在上周的 坐标映射里面就已经学会了:

/*** 贴纸管理类,使用单例模式* 对每个“贴纸”进行保存、增加、删除*/
public class RikkaStickerManager {private static final String TAG = "RikkaStickerManager";public static RikkaStickerManager instance;//贴纸List,统一进行管理private List<RikkaStickerView> mStickerList = new ArrayList<>();public static RikkaStickerManager getInstance() {if (instance == null) {synchronized (RikkaStickerManager.class) {if (instance == null) {instance = new RikkaStickerManager();}}}return instance;}/*** 添加贴纸,就是往List里面添加*/void addSticker(RikkaStickerView stickerView) {mStickerList.add(stickerView);}/*** 移除指定的贴纸*/void removeSticker(RikkaStickerView stickerView) {Bitmap bitmap = stickerView.getmBitmap();if (bitmap != null && !bitmap.isRecycled()) {//即使回收bitmap.recycle();}mStickerList.remove(stickerView);}/*** 移除所有贴纸*/void removeAllSticker() {for (int i = 0; i < mStickerList.size(); i++) {Bitmap bitmap = mStickerList.get(i).getmBitmap();if (bitmap != null && !bitmap.isRecycled()) {bitmap.recycle();}}mStickerList.clear();}/*** 设置当前贴纸为选中(焦点)贴纸*/void setFocusSticker(RikkaStickerView focusSticker) {for (int i = 0; i < mStickerList.size(); i++) {RikkaStickerView sticker = mStickerList.get(i);if (sticker == focusSticker) {sticker.setFocus(true);} else {sticker.setFocus(false);}}}/*** 全部设为没有焦点*/void clearAllFocus() {for (int i = 0; i < mStickerList.size(); i++) {RikkaStickerView stickerView = mStickerList.get(i);stickerView.setFocus(false);}}/*** 根据触摸的点来返回触摸的贴纸*/RikkaStickerView getSticker(float x, float y) {for (int i = mStickerList.size() - 1; i >= 0; i--) {RikkaStickerView sticker = mStickerList.get(i);//因为points 映射之后都会改变所以必须每次都重置float[] points = new float[]{x, y};//根据invert来做转换Matrix matrix = new Matrix();sticker.getmMatrix().invert(matrix);matrix.mapPoints(points);//根据边界来判断 点是否在该View中if (sticker.getmBitmapBound().contains(points[0], points[1])) {return sticker;}}return null;}/*** 根据触摸的点来判断是否点击到删除按钮,如果点到就会删除*/RikkaStickerView getDelButton(float x, float y) {for (int i = mStickerList.size() - 1; i >= 0; i--) {RikkaStickerView sticker = mStickerList.get(i);float[] points = new float[]{x, y};//根据invert来做转换Matrix matrix = new Matrix();sticker.getmMatrix().invert(matrix);matrix.mapPoints(points);//根据边界来判断 点是否在该View中if (sticker.getmDelBitmapBound().contains(points[0], points[1])) {return sticker;}}return null;}public List<RikkaStickerView> getmStickerList() {return mStickerList;}
}

Ok,到这里小轮子就做完了,因为上周的拖到了这周了,所以这周还要再多学一个。
不过反正AndroidBus和玩Android的轮子那么多,也不缺着学习哈哈哈哈。

每周一个小轮子之 贴纸效果相关推荐

  1. Android 每周一个小轮子之 学习仿网易云广场歌单的效果

    这一篇Blog是学习自:Android自定义ViewGroup第十三式之移花接木 小缘老哥太顶了,写的东西都巨棒,关注Ta很久了,我决定向他学习,学着去像他那样思考问题. 建议各位老哥都去关注他! 这 ...

  2. Android 每周一个小轮子之 学习仿网易云广场歌单的效果

    /** 这里要自己写一个ViewGroup的LayoutParams来记录 scale.alpha.from.to */ class RikkaLayoutParams extends MarginL ...

  3. 实现一个小轮子:用AOP实现异步上传

    文章来源:https://c1n.cn/2jnRk‍‍‍ 目录 背景 代码与实现 结语 背景 相信很多系统里都有这一种场景:用户上传 Excel,后端解析 Excel 生成相应的数据,校验数据并落库. ...

  4. 用RecyclerView做一个小清新的Gallery效果

    一.简介 RecyclerView现在已经是越来越强大,且不说已经被大家用到滚瓜烂熟的代替ListView的基础功能,现在RecyclerView还可以取代ViewPager实现Banner效果,当然 ...

  5. 每周学一个小轮子之 可以缩放的ScalableView(1),android开发者指南

    注意,我们需要在 onDown方法返回true,原理和onTouchEvent一样,如果不是true,就接收不到后面的事件了. ScaleGestureDetector 双指缩放的精髓类,它是Andr ...

  6. 【每周一个小技能】WSA 安装

    Windows11 UI yyds!( : 记录一下WSA安装过程 一.准备过程 1.一台笔记本 (废话!) 2.Windows11系统 (也是废话!) 3.WSA 微软商店链接 : https:// ...

  7. 【每周一个小技能】Obsidian配合Git实现笔记自动同步

    一.码云创建私有库 二.关闭安全模式,安装 Ob Git 三.设置自动同步时间,单位为 分钟

  8. 教你50行代码实现前端路由小轮子

    在SPA应用这么流行的当下,我们看到每个MV**框架都会有自己的路由插件用于实现单页应用的路由设置与监控,并且提供了一系列的生命周期来方便用户,那么到底它们都是怎么做到的呢,今天我会放上自己写的一个小 ...

  9. 贴纸UI效果如何制作,4个做贴纸效果的小技巧

    前段时间分享了第一次做的贴纸效果,今天总结一下啊做贴纸UI设计UI设计的小技巧 1. 每一个设计都要有点自己的小创意 做设计之前,是否有想过你的创意是什么?如果没有,那一定要好好深思下了,这是个习惯问 ...

最新文章

  1. FreeRTOS应用开发笔记之一:FreeRTOS在STM32的移植
  2. android跌倒检测,Android跌倒检测
  3. C++工作笔记-在项目中解决编码问题小技巧
  4. mysql针对特定表不做binlog_MySQL笔记--主从复制
  5. (七)OpenCV | 色度图
  6. 机器学习中的数学——结构化概率模型/图模型
  7. 计算机控制技术廖道争答案,2017年三峡大学电气与新能源学院专业目录及考试科目...
  8. 小新面试错题集,http1.0与1.1的区别?
  9. 公众号网站——微信登录
  10. Django restframework实现批量操作
  11. VirtualBox 安装增强功能失败 解决方法
  12. 动态内存的申请和非动态内存的申请_公安交管新举措咋解读?非营运七座车6年免检,70岁可申请驾照...
  13. MASM汇编入门:寄存器数据的使用
  14. 制作docker容器镜像
  15. session取不到的原因_游戏id不会取?来看看职业选手是如何取id的!满满的干货哦。...
  16. 服务网关-Zuul(二)
  17. 即将升级的LDK7.1支持云授权了
  18. 2022年哪些浏览器安全、速度快、好用又不卡?
  19. javaWeb中 servlet 、request 、response
  20. nginx rewrite if指令剖析

热门文章

  1. 【滤波器】基于低通滤波器语音信号加噪与去噪含Matlab源码
  2. Unity资源加载方式
  3. python推荐系统开源库_Python-recsys
  4. 百度深度学习paddlepaddle7日打卡——Python小白逆袭大神学习心得
  5. freemark 同一个模版用if else导出不同的word,word分页
  6. Sketch如何将文字转成图片或轮廓
  7. MTK电话本联系人备份加密与破解
  8. 性能稳定的android手机,3部性能稳定续航能力强的手机,认真玩游戏,拒绝坑队友!...
  9. linux 批量去除文件后缀,Linux 批量删除文件后缀
  10. 《设计模式之禅》第二次重印,窃喜