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.pswp.cn/diannao/95934.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/95934.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

25. for 循环区别

1. 基本 for 循环 for (let i 0; i < 10; i) {console.log(i); }特点&#xff1a; 适用于已知循环次数的情况使用数字索引进行迭代可以精确控制循环过程性能最好&#xff0c;开销最小 2. for…in 循环 // 数组示例 for (let i in [1, 2, 3]) {console.log(i, typeof i); //…

Trae 辅助下的 uni-app 跨端小程序工程化开发实践分享

大家好&#xff0c;我是不如摸鱼去&#xff0c;欢迎来到我的AI编程分享专栏。 这次来分享一下&#xff0c;我使用 Trae 作为主要AI编程工具&#xff0c;开发 uni-app 跨平台小程序的完整实践经验。我在实际的开发过程中&#xff0c;探索了 Trae 辅助开发的具体应用场景和效果&…

Vue3 + Element Plus 人员列表搜索功能实现

设计思路使用Element Plus的el-table组件展示人员数据 在姓名表头添加搜索图标按钮 点击按钮弹出搜索对话框 在对话框中输入姓名进行搜索 实现搜索功能并高亮匹配项下面是完整的实现代码&#xff1a;<!DOCTYPE html> <html lang"zh-CN"> <head><…

告别手动优化!React Compiler 自动记忆化技术深度解析

概述 React Compiler 是 React 团队开发的一个全新编译器&#xff0c;目前处于 RC&#xff08;Release Candidate&#xff09;阶段。这是一个仅在构建时使用的工具&#xff0c;可以自动优化 React 应用程序&#xff0c;无需重写任何代码即可使用。 核心特性 自动记忆化优化 …

【从零开始学习Redis】项目实战-黑马点评D2

商户查询缓存 为什么用缓存&#xff1f;作用模型缓存流程按照流程编写代码如下 Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {Resourceprivate StringRedisTemplate stringRedisTemplate;Overridepublic Resul…

后端Web实战-MySQL数据库

目录 1.MySQL概述 1.1 安装 1.1.1 版本 1.1.2 安装 1.1.3 连接 1.2 数据模型 1.3 SQL简介 1.3.1 分类 1.3.2 SQL通用语法 2.DDL 2.1 数据库操作 2.2 图形化工具 2.2.1 使用 2.3 表操作 2.3.1 创建表 2.3.1.1约束 2.3.1.2 数据类型 2.3.1.3 案例 2.3.2 DDL&am…

开源数据发现平台:Amundsen 本地环境安装

Amundsen 是一个数据发现和元数据引擎&#xff0c;旨在提高数据分析师、数据科学家和工程师与数据交互时的生产力。目前&#xff0c;它通过索引数据资源&#xff08;表格、仪表板、数据流等&#xff09;并基于使用模式&#xff08;例如&#xff0c;查询频率高的表格会优先于查询…

ubuntu18.04部署cephfs

比起君子讷于言而敏于行&#xff0c;我更喜欢君子善于言且敏于行。 目录 一. 准备工作&#xff08;所有节点&#xff09; 1. /etc/hosts 2. 安装python2 3. 配置普户免密sudo 4. 准备好四块盘&#xff0c;一块hddsdd为一组&#xff0c;一台设备上有一组 5. 添加源 二. 安…

VMD+皮尔逊+降噪+重构(送报告+PPT)Matlab程序

1.程序介绍:以含白噪声信号为例&#xff1a;1.对信号进行VMD分解2.通过皮尔逊进行相关性计算3.通过设定阈值将噪声分量和非噪声分量分别提取出4.对非噪声信号进行重构达到降噪效果包含评价指标&#xff1a;% SNR&#xff1a;信噪比% MSE&#xff1a;均方误差% NCC&#xff1a;波…

UE5多人MOBA+GAS 45、制作冲刺技能

文章目录添加技能需要的东西添加本地播放GC添加冲刺tag添加一个新的TA用于检测敌方单位添加冲刺GA到角色中监听加速移动速度的回调创建蒙太奇添加GE添加到数据表中添加到角色中纠错添加技能需要的东西 添加本地播放GC 在UCAbilitySystemStatics中添加 /*** 在本地触发指定的游…

分库分表和sql的进阶用法总结

说下你对分库分表的理解分库分表是⼀种常⽤的数据库⽔平扩展&#xff08;Scale Out&#xff09;技术&#xff0c;⽤于解决单⼀数据库性能瓶颈和存储容量限制的问题。在分库分表中&#xff0c;数据库会根据某种规则将数据分散存储在多个数据库实例和表中&#xff0c;从⽽提⾼数据…

紫金桥RealSCADA:国产工业大脑,智造安全基石

在工业4.0时代&#xff0c;数字化转型已成为企业提升竞争力的核心路径。作为工业信息化的基石&#xff0c;监控组态软件在智能制造、物联网、大数据等领域发挥着关键作用。紫金桥软件积极响应国家“两化融合”战略&#xff0c;依托多年技术积淀与行业经验&#xff0c;重磅推出跨…

朗空量子与 Anolis OS 完成适配,龙蜥获得抗量子安全能力

近日&#xff0c;苏州朗空后量子科技有限公司&#xff08;以下简称“朗空量子”&#xff09;签署了 CLA&#xff08;Contributor License Agreement&#xff0c;贡献者许可协议&#xff09;&#xff0c;加入龙蜥社区&#xff08;OpenAnolis&#xff09;。 朗空量子是一家后量子…

C#WPF实战出真汁08--【消费开单】--餐桌面板展示

1、功能介绍在这节里&#xff0c;需要实现餐桌类型展示&#xff0c;类型点击切换事件&#xff0c;餐桌面板展示功能&#xff0c;细节很多&#xff0c;流程是UI设计布局-》后台业务逻辑-》视图模型绑定-》运行测试2、UI设计布局TabControl&#xff0c;StackPanel&#xff0c;Gri…

2025年机械制造、机器人与计算机工程国际会议(MMRCE 2025)

&#x1f916;&#x1f3ed;&#x1f4bb; 探索未来&#xff1a;机械制造、机器人与计算机工程的交汇点——2025年机械制造、机器人与计算机工程国际会议&#x1f31f;MMRCE 2025将汇聚全球顶尖专家、学者及行业领袖&#xff0c;聚焦机械制造、机器人和计算机工程领域的前沿议题…

Vue Router 嵌套路由与布局系统详解:从新手到精通

在Vue单页应用开发中&#xff0c;理解Vue Router的嵌套路由机制是构建现代管理后台的关键。本文将通过实际案例&#xff0c;深入浅出地解释Vue Router如何实现布局与内容的分离&#xff0c;以及<router-view>的嵌套渲染原理。什么是嵌套路由&#xff1f;嵌套路由是Vue Ro…

Grafana 与 InfluxDB 可视化深度集成(二)

四、案例实操&#xff1a;以服务器性能监控为例 4.1 模拟数据生成 为了更直观地展示 Grafana 与 InfluxDB 的集成效果&#xff0c;我们通过 Python 脚本模拟生成服务器性能相关的时间序列数据。以下是一个简单的 Python 脚本示例&#xff0c;用于生成 CPU 使用率和内存使用量…

.net印刷线路板进销存PCB材料ERP财务软件库存贸易生产企业管理系统

# 印刷线路板进销存PCB材料ERP财务软件库存贸易生产企业管理系统 # 开发背景 本软件原为给苏州某企业开发的pcb ERP管理软件&#xff0c;后来在2021年深圳某pcb 板材公司买了我们的软件然后在此基础上按他行业的需求多次修改后的软件&#xff0c;适合pcb板材行业使用。 # 功能…

基于飞算JavaAI的可视化数据分析集成系统项目实践:从需求到落地的全流程解析

引言&#xff1a;为什么需要“可视化AI”的数据分析系统&#xff1f; 在数字化转型浪潮中&#xff0c;企业/团队每天产生海量数据&#xff08;如用户行为日志、销售记录、设备传感器数据等&#xff09;&#xff0c;但传统数据分析存在三大痛点&#xff1a; 技术门槛高&#xff…

MqSQL中的《快照读》和《当前读》

目录 1、MySQL读取定义 1.1、锁的分类 1.2、快照读与当前读 1.3、使用场景 1.4、区别 2、实现机制 2.1、实现原理 2.2、隔离级别和快照联系 1、隔离级别 2、快照读 2.3、快照何时生成 3、SQL场景实现 3.1、快照读 3.2、当前读 4、锁的细节&#xff08;与当前读相…