当前位置: 首页 > news >正文

Android 项目:画图白板APP开发(二)——历史点、数学方式推导点

        上一章我们讲解了如何绘制顺滑、优美的曲线,为本项目的绘图功能打下了基础。本章我们将深入探讨两个关键功能的实现:历史点数学方式推导点。这些功能将大幅提升我们白板应用的专业性和用户体验。

一、History点

之前在onTouchEvent中获取的MotionEvent,其实不是一个点的信息,而是一个触摸事件的封装

(1)基本概念

        在 Android 中,当用户触摸屏幕时,系统会生成一系列 MotionEvent 对象。为了提高效率,系统不会为每一个微小的移动都生成一个新事件,而是会将多个触摸点"打包"在一个 MotionEvent 中。

(2)代码示例

@Override
public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();//返回当前 MotionEvent 中包含的历史触摸点数量final int historySize = event.getHistorySize();for (int h = 0; h < historySize; h++) {float historicalX = event.getHistoricalX(h);float historicalY = event.getHistoricalY(h);long historicalTime = event.getHistoricalEventTime(h);// 处理历史点processPoint(historicalX, historicalY, historicalTime);}// 处理当前点float currentX = event.getX();float currentY = event.getY();processPoint(currentX, currentY, event.getEventTime());return true;
}

(3)差异展示

1.手写无history效果

点跟点之间的间隔很大,速度快了之后显得越发不密集

2.手写有history效果

其中黑色的为原始点,红色的为history点。这个对比上面就密集了很多,可是黑点和红点虽然轨迹一样,但是两者的分布间隔又长又短,甚至有的黑点和红点重叠了。这个对吗?我们再把红的单独点显示下

3.手写有history效果(无原始点)

这样看着就顺眼多了啊。

问题1:为什么将原始点和history点两者叠加显示会参差不齐。但是两者分开又各自的轨迹是连贯平滑的。

解答1:history事件存在的本质是因为屏幕刷新率一般比触摸屏刷新率要小,触摸的move事件处理又是要跟随 VSYNC (由自身帧率决定)即刷新率一起的,所以导致在一个 VSYNC 周期时间内,就会有多个触摸事件产生,如果不使用history那么相当于绘制的轨迹采样率就是屏幕刷新率。

  • 触摸屏采样率(通常 100-1000Hz
    硬件以高频率上报触摸点坐标(如每 1-10ms 一个点)。

  • 屏幕刷新率(通常 60-120Hz,即每 8.3-16.6ms 一帧
    Android 的 UI 渲染和事件处理依赖 VSYNC 信号,MotionEvent 的分发会被对齐到最近的 VSYNC

问题2:历史点的本质?

解答2:在两次 VSYNC 之间(即一个屏幕刷新周期内),触摸屏可能产生多个数据点,但系统只会合成一个 MotionEvent 上报。所以history上面保存的是触摸屏采样率所采集的点。

以为这样就结束了吗?我们接下来看看笔触的效果

4.笔写有history效果

使用高采样率的电子笔就可以达到近乎这种完美的效果

5.笔写有history效果(原始点宽度缩小2倍)

当将原始点的宽度缩小后发现:原始点和历史点有的会重合;有的只显示原始点。从现象中表现:历史点丢了。这个咱就不探究了,可能跟硬件、系统、底层代码逻辑有关。

(4)小结

综合上面的效果目前可以定下来方案

  • 手写:使用history点即可。
  • 笔写:使用原始点history点相结合达到最好效果。(以具体情况为主)

如果感觉手写点的数量笔写点的数量差距很大,那有没有办法提升手写点的数量???

有的、有的、兄弟包有的!!!接下来介绍通过数学方式新增点的方法

二、数学方式推导点

目标:用户绘制过程中根据特定条件自动添加额外的控制点,从而改变原始的绘制路径。

效果图:

其中红色的点是原始点,黑色的点是推导新增的点。

View代码:

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;@SuppressLint("ViewConstructor")
public class DrawView_EventPoints_New extends View {////这个记录第一个点private float pre1X = -1f;private float pre1Y = -1f;//记录第二个点private float pre2X = -1f;private float pre2Y = -1f;//用于保存新算的点private float newX = -1f;private float newY = -1f;//垂足点和中点private float footX = -1f;private float footY = -1f;private float centerX = -1f;private float centerY = -1f;//用于保存两点之间的距离private float distance = -1f;//获取屏幕的宽度(在此只适用于横屏)int viewWidth ;private Path path = new Path();Paint paint = new Paint(Paint.DITHER_FLAG);private Bitmap cacheBitmap;//定义cacheBitmap上的Canvas对象private Canvas cacheCanvas = new Canvas();private Paint bmpPaint = new Paint();DrawView_EventPoints_New(Context context , int width, int height){super(context);//创建一个与该View具有相同大小的图片缓冲区cacheBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);//设置cacheCanva将会绘制到内存中的cacheBitmap上cacheCanvas.setBitmap(cacheBitmap);//设置画笔的颜色paint.setColor(Color.RED);//设置画笔的风格paint.setStyle(Paint.Style.STROKE);paint.setStrokeJoin(Paint.Join.ROUND);paint.setStrokeCap(Paint.Cap.ROUND);paint.setStrokeWidth(12);//反锯齿paint.setAntiAlias(true);paint.setDither(true);viewWidth = width;}@SuppressLint("ClickableViewAccessibility")@Overridepublic boolean onTouchEvent(MotionEvent event) {//获取拖动事件发生的位置float x = event.getX();float y = event.getY();//初步的思路://1.判断两点之间的距离是否大于 屏幕(宽或高)的一个百分比值//  感觉只需要一层判断就够了,手写的速度没有没有鼠标那么快,每个点的距离没有那么开//2.需要三个点才能开始添加点,同时需要三个缓存(两个缓存)//  开启条件:当前两个点满足大于的条件,等待第三个点的到来(第三个比较重要,每次使用完都赋值成-1)+ 三点之间形成的角要大于90度//  第三个点假如超出了屏幕范围之外,就丢弃。//  当第三个点两种情况:(1)取到了:按照公式计算//                    (2)没取到:收尾的两个点大于条件//3.开始画点:重要的是算法//算法思路:首先根据开始的两个点,确定添加的点在那个垂直线上//一点和三点//缺点:显示的点,在距离过大时会稍晚两个点的显示(只要跟手的速度给力,应该也不影响)switch (event.getAction()){case MotionEvent.ACTION_DOWN://第一个点path.moveTo(x,y);//DOWN的时候保存第一个点if(pre1X == -1f && pre1Y == -1f){pre1X = x;pre1Y = y;}//System.out.println("DOWN1 "+pre1X+"  "+pre1Y);cacheCanvas.drawPoint(pre1X,pre1Y,paint);break;case MotionEvent.ACTION_MOVE:// 从前一个点绘制到当前点之后,把当前点定义成下次绘制的前一个点//MOVE的时候定义第二个点,并更新第一个点//这个是保存move的第一个点:这个时间段根本没办法获取newX,newY。if(pre2X == -1f && pre2Y == -1f){pre2X = x;pre2Y = y;break;}//判断两个点是否过长distance = CalculatePointsDistance(pre1X,pre1Y,pre2X,pre2Y);if(distance >= (float)(viewWidth/22)){//System.out.println("AAAA  distance:"+distance+" viewWidth/10: "+viewWidth/22);//判断是锐角还是钝角xif(isBluntAngle(pre1X,pre1Y,pre2X,pre2Y,x,y)){//根据三点计算新点getNewPoints(pre1X,pre1Y,pre2X,pre2Y,x,y);path.lineTo(newX,newY);paint.setColor(Color.BLACK);cacheCanvas.drawPoint(newX,newY,paint);}}path.lineTo(pre2X,pre2Y);paint.setColor(Color.RED);cacheCanvas.drawPoint(pre2X,pre2Y,paint);//第二个点和第是三个点前移pre1X = pre2X;pre1Y = pre2Y;pre2X = x;pre2Y = y;//这个时候新点是必定更新的(清空)newX = -1f; newY = -1f;break;case MotionEvent.ACTION_UP://因为最后一个一直没有画出来,所以在up的时候显示。path.moveTo(x,y);cacheCanvas.drawPoint(x,y,paint);//不仅如此,还可以对尾部进行一个修饰//cacheCanvas.drawPath(path,paint);path.reset();//全部恢复初始状态pre1X = -1f; pre1Y = -1f;pre2X = -1f; pre2Y = -1f;newX = -1f; newY = -1f;break;}invalidate();return true;}//得到新点private void getNewPoints(float p1X, float p1Y, float p2X, float p2Y, float X, float Y) {//求前两个点的斜率,得到垂直平分线的斜率//根据中点,求得新点的值//---------------方法二------------------//获取高的坐标float dx = p1X - X;float dy = p1Y - Y;float u =(p2X-p1X)*dx +(p2Y-p1Y)*dy;u/=dx*dx+dy*dy;footX = p1X+u*dx;footY = p1Y+u*dy;//根据p1点求中footX = (p1X+footX)/2f;footY = (p1Y+footY)/2f;//高的坐标与第一个点的中点+开始的中点 反推的一个点就是目标点centerX = (p1X+p2X)/2f;centerY = (p1Y+p2Y)/2f;newX = centerX*2f-footX;newY = centerY*2f-footY;newX = (centerX+newX)/2f;newY = (centerY+newY)/2f;}//判断是否为钝角,假如为钝角则开辟新点private boolean isBluntAngle(float p1X, float p1Y, float p2X, float p2Y, float X, float Y) {//转换为求两个向量的夹角float x12 = p1X - p2X;float y12 = p1Y - p2Y;float x23 = X - p2X;float y23 = Y - p2Y;float mul_12_23 = x12*x23 + y12*y23;float dist_12 = (float) Math.sqrt(x12*x12+y12*y12);float dist_23 = (float) Math.sqrt(x23*x23+y23*y23);float cosValue = mul_12_23/(dist_12*dist_23);float angle = (float)((float) 180*Math.acos(cosValue)/Math.PI);//输出一下角度//System.out.println("AAAAA 角度:"+angle);//当角度为180度,也可以不用画了。同时可以确定可以组成一个三角形return angle >= 90f&& angle !=180f;}//计算两点之间的距离private float CalculatePointsDistance(float p1X, float p1Y, float p2X, float p2Y) {return (float) Math.sqrt(Math.abs((p1X-p2X)*(p1X-p2X)+(p1Y-p2Y)*(p1Y-p2Y)));}@Overrideprotected void onDraw(Canvas canvas) {//将cacheBitmap绘制到View上(传入这个bmpPaint一点用都没有)canvas.drawBitmap(cacheBitmap,0f,0f,null);}
}

原理:

只看上面的代码不一定能理解思路,我简单说说我当时的设计思路。

方法1:

        通过三个已知点,在其之间添加使其顺滑的新增点,我首先想到的是抛物线。我在抛物线上随便取哪个点,都具有使整体饱满圆润的效果。

我们可以在 P1 和 P2 之间取,也可以在 P2 和 (X,Y)  之间取,如果尝试取他们中点数据,效果应该是最好的,有了思路后接着看下面设计图。

在大多数情况下,三个确定位置的坐标点可以确定两条抛物线(垂直抛物线和水平抛物线。只有在点的排列限制了某一种形式的抛物线时,才可能只能确定一条。其实确定垂直抛物线就行。

这个方式太复杂了,要用代码操作计算量太大了。我直接pass掉了,不过按照道理来说是可行的,大家有意愿的话可以自己试试。

方法2:

这个方法就是代码中使用的,依旧使用图例讲解:


  • 其中 foot点 为 P2 到线段【P1,(X,Y)】的垂足。对应的代码为:
float dx = p1X - X;
float dy = p1Y - Y;float u =(p2X-p1X)*dx +(p2Y-p1Y)*dy;
u/=dx*dx+dy*dy;
footX = p1X+u*dx;
footY = p1Y+u*dy;
  • 下面代码也好理解,取 foot点 和 P1点 赋值给 foot ;去 P1 和 P2 的中点center。
//根据p1点求中
footX = (p1X+footX)/2f;
footY = (p1Y+footY)/2f;centerX = (p1X+p2X)/2f;
centerY = (p1Y+p2Y)/2f;

  • 之后的代码需要图解,上图片。模拟一个坐标系就很明朗了。
newX = centerX*2f-footX;
newY = centerY*2f-footY;newX = (centerX+newX)/2f;
newY = (centerY+newY)/2f;

  • 最终的结果其实只是为了让模拟点突出来一下,没想到实际效果还不错!!!

流程详解:

  1. 在onTouchEvent中获取基础点,当达到3个时开始计算目标点。
  2. 当距离大于固定长度时,开始计算。
  3. 当三个点组成的角为钝角时才开始计算,因为当为锐角的时,此时速度不快,没必要去计算新增点。
  4. 使用方法二,根据第三个点来预测出前两个点的新增点。当然你也可以根据这个方法来计算后两个点的新增点
http://www.lryc.cn/news/620797.html

相关文章:

  • 2.0t的涡轮增压器结构设计说明书cad【5张】设计说明说
  • OpenSatKit技术详解
  • 《Leetcode》-面试题-hot100-动态规划
  • C++实现序列匹配与分类处理
  • 深度学习-卷积神经网络CNN-批量归一化 BatchNorm
  • React和Vue
  • React 中播放HLS 视频流 ,超简单的组件高度复用
  • 2019 GPT2原文 Language Models are Unsupervised Multitask Learners - Reading Notes
  • 微美全息(WIMI.US)借区块链与聚类技术,开启物联网去中心化安全架构新纪元
  • C#WPF实战出真汁03--登录功能实现
  • 阿里云Spring Cloud架构分析
  • 无人机双目视觉设计要点概述!
  • .Net4.0 WPF中实现下拉框搜索效果
  • 4. 索引数据的增删改查
  • MyBatis Interceptor 深度解析与应用实践
  • Mybatis学习笔记(一)
  • 【密码学实战】基于SCTP的DTLS协议实验
  • springboot项目不同平台项目通过http接口AES加密传输
  • AR技术赋能电力巡检:智能化升级的“秘密武器”
  • MicroVM-as-a-Service 后端服务架构设计与实现
  • 顺序表插入删除
  • 常见的Jmeter压测问题
  • OpenCV 视频处理全解析
  • 力扣-295.数据流的中位数
  • 11、C 语言字符串函数总结
  • OpenCV 高斯模糊降噪
  • npm删除包
  • PyCharm性能优化与大型项目管理指南
  • C++:浅尝gdb
  • YouBallin正式上线:用Web3重塑创作者经济