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
英文地址,請注明出處:http://en.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;與當前讀相…