Android Studio Canvas 实现鼠标贝塞尔曲线拖尾特效_flutter canvas画拖尾效果-程序员宅基地

技术标签: bezier curve  java  android  特效!  android studio  贝塞尔曲线  AS问题汇总  

Android Studio Canvas 实现鼠标贝塞尔曲线拖尾特效

特效预览图

在这里插入图片描述

什么是贝塞尔曲线?

百度百科:

​ 贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。

​ 贝塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau演算法开发,以稳定数值的方法求出贝兹曲线。

​ 贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。其中起重要作用的是位于曲线中央的控制线。这条线是虚拟的,中间与贝塞尔曲线交叉,两端是控制端点。移动两端的端点时贝塞尔曲线改变曲线的曲率(弯曲的程度);移动中间点(也就是移动虚拟的控制线)时,贝塞尔曲线在起始点和终止点锁定的情况下做均匀移动。注意,贝塞尔曲线上的所有控制点、节点均可编辑。这种“智能化”的矢量线条为艺术家提供了一种理想的图形编辑与创造的工具。本文即用贝塞尔曲线实现鼠标拖尾特效。


理解贝塞尔曲线

贝塞尔曲线数学理解、推导方法:怎么理解贝塞尔曲线? - 知乎

本文bezier求点公式参考论文:Finding a Point on a Bézier Curve: De Casteljau’s Algorithm

引用 贝塞尔曲线简单介绍_xiaozhangcsdn的博客-程序员宅基地_bezier曲线 对贝塞尔曲线理解关键点进行简要介绍:

对于贝塞尔曲线,最重要的点是数据点和控制点。
数据点: 指一条路径的起始点和终止点。
控制点:控制点决定了一条路径的弯曲轨迹
根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。

特点一:曲线通过始点和终点,并与特征多边形首末两边相切于始点和终点,中间点将曲线拉向自己。
特点二:平面离散点控制曲线的形状,改变一个离散点的坐标,曲线的形状将随之改变(点对曲线具有整体控制性)。
特点三:曲线落在特征多边形的凸包之内,它比特征多边形更趋于光滑。


贝塞尔曲线图示

引用自 贝塞尔曲线的数学原理_程序人生-程序员宅基地_贝塞尔曲线原理

一阶贝塞尔曲线(线段):
在这里插入图片描述
在这里插入图片描述
意义:由 P0 至 P1 的连续点, 描述的一条线段

二阶贝塞尔曲线(抛物线):
在这里插入图片描述
在这里插入图片描述
原理:由 P0 至 P1 的连续点 Q0,描述一条线段。
由 P1 至 P2 的连续点 Q1,描述一条线段。
由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。

经验:P1-P0为曲线在P0处的切线。

三阶贝塞尔曲线:
在这里插入图片描述
在这里插入图片描述
通用公式:
在这里插入图片描述
高阶贝塞尔曲线:

4阶曲线:
在这里插入图片描述
5阶曲线:
在这里插入图片描述

好了,了解完以上贝塞尔曲线的知识,我们该如何用贝塞尔曲线在Android Studio上做特效呢?以本文所提鼠标拖尾特效为例:


第一步

创建新工程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9THZ1H0Z-1639291917480)(116052019057 刘睿 Android Studio Canvas 实现鼠标贝塞尔曲线拖尾特效.assets/image-20211212143658584.png)]
直接创建空白 activity 即可

package com.example.drawimagery;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
}

第二步

因为 java 好像并未提供n阶贝塞尔函数的 api,所以我们就自己写一个

创建 Bezier 文件在这里插入图片描述

Bezier 文件作为主函数体文件,保存了 beizer(计算n阶贝塞尔曲线上点的位置)、factorial(阶乘)、rainBow(彩虹色)三个函数。其中贝塞尔曲线公式是运用下图公式计算得出

公式引用自 贝塞尔曲线 WPF MVVM N阶实现 公式详解+源代码下载 - ARM830 - 博客园
在这里插入图片描述在这里插入图片描述

  • n=有效坐标点数量
  • i=坐标点的下标
  • P=坐标
  • t=时间百分比,在0~1之间,覆盖了整条曲线

因为
在这里插入图片描述
可得
在这里插入图片描述
硬转换成 java 后其代码如下:

(因为本文项目使用 Queue 进行数据存储所以使用 LinkedList 结构)

package com.example.drawimagery;

import java.util.LinkedList;

public class Bezier {
    

    public static float[] bezier(LinkedList<Float> theArrayX, LinkedList<Float> theArrayY, float t){
     //贝塞尔公式调用
        float x = 0;
        float y = 0;
        //控制点数组
        int n = theArrayX.size() - 1;
        int size = theArrayX.size();
        for (int index = 0; index < size; index ++) {
    
            float itemX = theArrayX.get(index);
            float itemY = theArrayY.get(index);
            if(index == 0){
    
                x += itemX * Math.pow(( 1 - t ), n - index) * Math.pow(t, index);
                y += itemY * Math.pow(( 1 - t ), n - index) * Math.pow(t, index);
            }else{
    
                //factorial为阶乘函数
                x += factorial(n) / factorial(index) / factorial(n - index) * itemX * Math.pow((1 - t), n - index) * Math.pow(t, index);
                y += factorial(n) / factorial(index) / factorial(n - index) * itemY * Math.pow((1 - t), n - index) * Math.pow(t, index);
            }
        }
        return new float[] {
    x, y};
    }

    public static long factorial(int num) {
    
        if (num < 0) {
    
            return -1;
        } else if (num == 0 || num == 1) {
    
            return 1;
        } else {
    
            return (num * factorial(num - 1));
        }
    }

    public static int[] rainBow(float t) {
    
        int red, green, blue;
        if (t < 0.334) {
    
            red = (int)(255 - t * 3 * 255);
            green = (int)(t * 3 * 255);
            blue = 0;
        } else if (t < 0.667) {
    
            red = 0;
            green = (int)(255 - (t - 0.334) * 3 * 255);
            blue = (int)((t - 0.334) * 3 * 255);
        } else {
    
            red = (int)((t - 0.667) * 3 * 255);
            green = 0;
            blue = (int)(255 - (t - 0.667) * 3 * 255);
        }
        return new int[] {
    red, green, blue};
    }

}

Bezier 代码解析

  • public static float[] bezier(LinkedList<Float> theArrayX, LinkedList<Float> theArrayY, float t):输入鼠标X轨迹、鼠标Y轨迹、所求点在曲线上的位置(百分比表示),返回所求点x,y坐标
    
  • public static long factorial(int num):求num的阶乘
    
  • public static int[] rainBow(float t):输入彩虹色谱百分比(0~1),返回彩虹色rgb(数组表示)
    

第三步

创建 MainCanvas 文件在这里插入图片描述

Main Canvas 文件继承自 View ,作为本项目的主画布 view

其代码如下:

package com.example.drawimagery;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.Calendar;
import java.util.LinkedList;
import java.util.Queue;

public class MainCanvas extends View {
    
    private Paint mPaintMouse;//鼠标拖尾画笔

    private boolean mouse_begin = false;//鼠标是否按下

    private float mouseCurrentX = 0;//当前鼠标位置X
    private float mouseCurrentY = 0;//当前鼠标位置Y
    Queue<Float> mouseX = new LinkedList<Float>();//保存鼠标轨迹X
    Queue<Float> mouseY = new LinkedList<Float>();//保存鼠标轨迹Y

    private int time = 0;//累加时间

    Handler handler = new Handler();
    Runnable runnable = new Runnable() {
    
        @Override
        public void run() {
    
            time++;
            invalidate();//告诉主线程重新绘制
            if (mouseX.peek() != null) {
    
                boolean is_add_mouse = Math.abs(mouseX.peek() - mouseCurrentX) < 0.01;//鼠标不动时不记录坐标
                if (!is_add_mouse) {
    
                    mouseX.offer(mouseCurrentX);
                    mouseY.offer(mouseCurrentY);
                }
                if (mouseX.size() > 20 || is_add_mouse) {
    
                    mouseX.poll();
                    mouseY.poll();
                }
            } else if (mouse_begin) {
    
                mouseX.offer(mouseCurrentX);
                mouseY.offer(mouseCurrentY);
            }
            handler.postDelayed(this, 20);//每20ms循环一次,50fps
        }
    };

    public MainCanvas(Context context) {
    
        super(context);
    }

    public MainCanvas(Context context, AttributeSet attrs) {
    
        super(context, attrs);
        handler.postDelayed(runnable, 20);
        mPaintMouse = new Paint();//对画笔初始化
        mPaintMouse.setColor(Color.RED);//设置画笔颜色
        mPaintMouse.setStrokeWidth(10);//设置画笔宽度
        mPaintMouse.setAntiAlias(true);//设置抗锯齿
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    //设置触摸事件,手指按下进行记录,手指抬起停止记录
        mouseCurrentX = event.getX();
        mouseCurrentY = event.getY();
        switch (event.getAction()) {
    
            case MotionEvent.ACTION_DOWN:
                mouse_begin = true;
                break;
            case MotionEvent.ACTION_UP:
                mouse_begin = false;
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
    
        super.onDraw(canvas);

//        int[] color = Bezier.rainBow((float)time % 300 / 300); //画笔同一颜色随时间渐变

        int size = mouseX.size();
        float x1 = 0,x2 = 0,y1 = 0,y2 = 0;
        for (int i = 0; i < size; i++) {
    
            float percent = (float)i / size;
            float res[] = Bezier.bezier((LinkedList)mouseX, (LinkedList)mouseY, percent);
            x1 = res[0];
            y1 = res[1];
            if(i == 0){
    
                x2 = x1;
                y2 = y1;
                continue;
            }
            int[] color = Bezier.rainBow((time + percent * 300) % 300 / 300); //画笔不同颜色随时间渐变
            mPaintMouse.setColor(Color.argb(255, color[0], color[1], color[2]));
            mPaintMouse.setStrokeWidth((int)(percent * 20));
            canvas.drawLine(x1, y1, x2, y2, mPaintMouse);
            x2 = x1;
            y2 = y1;
            if (i == size - 1) canvas.drawLine(x1, y1, mouseCurrentX, mouseCurrentY, mPaintMouse);//连接最后一段与鼠标
        }
        canvas.drawCircle(mouseCurrentX, mouseCurrentY, 10, mPaintMouse);//绘制鼠标中心

    }
}

Main Canvas 代码解析&思路分析

在这里插入图片描述
首先创建所需变量(可将x,y转成一个对象方便操作,这里分开表述比较清晰)


在这里插入图片描述
然后设置画布触摸事件,每次按下屏幕、滑动屏幕时记录当前鼠标位置,并且设置鼠标按下与抬起事件的标记,方便记录鼠标轨迹。


在这里插入图片描述
接着通过 Handler 与 Runnable 的组合实现简单计时器,设定其每20ms循环一次,等同于每秒50帧(这种计时存在较大误差,在这里只是简单实现计时功能,若想精确计时请参考handler实现精确计时的两种方式_王温暖的博客-程序员宅基地_android handler计时

然后设置每帧在鼠标移动时对鼠标当前位置进行记录,用鼠标的延迟位置制作鼠标拖尾。数据用 Queue 进行保存,其先进先出的特性十分契合本项目需求。

设置当鼠标拖尾长度 mouseX.size() 大于20后每帧将队列尾部抛出,只保留最多20帧的鼠标拖尾。同时如果鼠标在原地不动时也将队列尾部抛出,这样下次触屏将生成新的贝塞尔曲线。

!!!注意在java中,mouseX.size() 的值不能设置太大。这与我们使用的贝塞尔算法和java的计算机制相关。因为我们是使用阶乘来进行坐标计算,在java中,虽然该阶乘结果数据是作为计算中间值参与运算,但当阶乘的参数n(也就是size)太大时数据仍会溢出(n!>long的范围),造成毁灭性的后果。作者在Lua、JS中的相同算法均未遇到此溢出,猜想是与java的计算方式有关。
在这里插入图片描述
我们定义的阶乘是返回值为long类型
在这里插入图片描述
在运算时这些阶乘的值会溢出,变成0或是其它数据


在这里插入图片描述
然后在画布构建时启动计时器,并且对画笔进行初始化


在这里插入图片描述
设置 onDraw 事件进行绘制。简单说就是将之前记录的每帧鼠标轨迹用不同色彩、不同粗细的线段进行连接。这里对几个要点进行说明。

  • 贝塞尔曲线函数接收的参数为鼠标X轨迹、鼠标Y轨迹和所求点在整段曲线的位置(用百分比表示),并以数组形式返回所求点坐标在这里插入图片描述
  • 这段不写鼠标中心与轨迹之间会存在一段空隙在这里插入图片描述
  • 此代码样式演示在这里插入图片描述
    在这里插入图片描述
  • 此代码样式演示
    在这里插入图片描述
    在这里插入图片描述

第四步

修改 activity_main.xml 文件

在这里插入图片描述
将我们上文所构建的 View 置入

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.drawimagery.MainCanvas
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.example.drawimagery.MainCanvas>

</androidx.constraintlayout.widget.ConstraintLayout>

OK,运行模拟器,按下鼠标,美丽的贝塞尔曲线-鼠标轨迹就生成啦:)

作者:刘睿

原文链接:Android Studio Canvas 实现鼠标贝塞尔曲线拖尾特效

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40517035/article/details/121887568

智能推荐

874计算机科学基础综合,2018年四川大学874计算机科学专业基础综合之计算机操作系统考研仿真模拟五套题...-程序员宅基地

文章浏览阅读1.1k次。一、选择题1. 串行接口是指( )。A. 接口与系统总线之间串行传送,接口与I/0设备之间串行传送B. 接口与系统总线之间串行传送,接口与1/0设备之间并行传送C. 接口与系统总线之间并行传送,接口与I/0设备之间串行传送D. 接口与系统总线之间并行传送,接口与I/0设备之间并行传送【答案】C2. 最容易造成很多小碎片的可变分区分配算法是( )。A. 首次适应算法B. 最佳适应算法..._874 计算机科学专业基础综合题型

XShell连接失败:Could not connect to '192.168.191.128' (port 22): Connection failed._could not connect to '192.168.17.128' (port 22): c-程序员宅基地

文章浏览阅读9.7k次,点赞5次,收藏15次。连接xshell失败,报错如下图,怎么解决呢。1、通过ps -e|grep ssh命令判断是否安装ssh服务2、如果只有客户端安装了,服务器没有安装,则需要安装ssh服务器,命令:apt-get install openssh-server3、安装成功之后,启动ssh服务,命令:/etc/init.d/ssh start4、通过ps -e|grep ssh命令再次判断是否正确启动..._could not connect to '192.168.17.128' (port 22): connection failed.

杰理之KeyPage【篇】_杰理 空白芯片 烧入key文件-程序员宅基地

文章浏览阅读209次。00000000_杰理 空白芯片 烧入key文件

一文读懂ChatGPT,满足你对chatGPT的好奇心_引发对chatgpt兴趣的表述-程序员宅基地

文章浏览阅读475次。2023年初,“ChatGPT”一词在社交媒体上引起了热议,人们纷纷探讨它的本质和对社会的影响。就连央视新闻也对此进行了报道。作为新传专业的前沿人士,我们当然不能忽视这一热点。本文将全面解析ChatGPT,打开“技术黑箱”,探讨它对新闻与传播领域的影响。_引发对chatgpt兴趣的表述

中文字符频率统计python_用Python数据分析方法进行汉字声调频率统计分析-程序员宅基地

文章浏览阅读259次。用Python数据分析方法进行汉字声调频率统计分析木合塔尔·沙地克;布合力齐姑丽·瓦斯力【期刊名称】《电脑知识与技术》【年(卷),期】2017(013)035【摘要】该文首先用Python程序,自动获取基本汉字字符集中的所有汉字,然后用汉字拼音转换工具pypinyin把所有汉字转换成拼音,最后根据所有汉字的拼音声调,统计并可视化拼音声调的占比.【总页数】2页(13-14)【关键词】数据分析;数据可..._汉字声调频率统计

linux输出信息调试信息重定向-程序员宅基地

文章浏览阅读64次。最近在做一个android系统移植的项目,所使用的开发板com1是调试串口,就是说会有uboot和kernel的调试信息打印在com1上(ttySAC0)。因为后期要使用ttySAC0作为上层应用通信串口,所以要把所有的调试信息都给去掉。参考网上的几篇文章,自己做了如下修改,终于把调试信息重定向到ttySAC1上了,在这做下记录。参考文章有:http://blog.csdn.net/longt..._嵌入式rootfs 输出重定向到/dev/console

随便推点

uniapp 引入iconfont图标库彩色symbol教程_uniapp symbol图标-程序员宅基地

文章浏览阅读1.2k次,点赞4次,收藏12次。1,先去iconfont登录,然后选择图标加入购物车 2,点击又上角车车添加进入项目我的项目中就会出现选择的图标 3,点击下载至本地,然后解压文件夹,然后切换到uniapp打开终端运行注:要保证自己电脑有安装node(没有安装node可以去官网下载Node.js 中文网)npm i -g iconfont-tools(mac用户失败的话在前面加个sudo,password就是自己的开机密码吧)4,终端切换到上面解压的文件夹里面,运行iconfont-tools 这些可以默认也可以自己命名(我是自己命名的_uniapp symbol图标

C、C++ 对于char*和char[]的理解_c++ char*-程序员宅基地

文章浏览阅读1.2w次,点赞25次,收藏192次。char*和char[]都是指针,指向第一个字符所在的地址,但char*是常量的指针,char[]是指针的常量_c++ char*

Sublime Text2 使用教程-程序员宅基地

文章浏览阅读930次。代码编辑器或者文本编辑器,对于程序员来说,就像剑与战士一样,谁都想拥有一把可以随心驾驭且锋利无比的宝剑,而每一位程序员,同样会去追求最适合自己的强大、灵活的编辑器,相信你和我一样,都不会例外。我用过的编辑器不少,真不少~ 但却没有哪款让我特别心仪的,直到我遇到了 Sublime Text 2 !如果说“神器”是我能给予一款软件最高的评价,那么我很乐意为它封上这么一个称号。它小巧绿色且速度非

对10个整数进行按照从小到大的顺序排序用选择法和冒泡排序_对十个数进行大小排序java-程序员宅基地

文章浏览阅读4.1k次。一、选择法这是每一个数出来跟后面所有的进行比较。2.冒泡排序法,是两个相邻的进行对比。_对十个数进行大小排序java

物联网开发笔记——使用网络调试助手连接阿里云物联网平台(基于MQTT协议)_网络调试助手连接阿里云连不上-程序员宅基地

文章浏览阅读2.9k次。物联网开发笔记——使用网络调试助手连接阿里云物联网平台(基于MQTT协议)其实作者本意是使用4G模块来实现与阿里云物联网平台的连接过程,但是由于自己用的4G模块自身的限制,使得阿里云连接总是无法建立,已经联系客服返厂检修了,于是我在此使用网络调试助手来演示如何与阿里云物联网平台建立连接。一.准备工作1.MQTT协议说明文档(3.1.1版本)2.网络调试助手(可使用域名与服务器建立连接)PS:与阿里云建立连解释,最好使用域名来完成连接过程,而不是使用IP号。这里我跟阿里云的售后工程师咨询过,表示对应_网络调试助手连接阿里云连不上

<<<零基础C++速成>>>_无c语言基础c++期末速成-程序员宅基地

文章浏览阅读544次,点赞5次,收藏6次。运算符与表达式任何高级程序设计语言中,表达式都是最基本的组成部分,可以说C++中的大部分语句都是由表达式构成的。_无c语言基础c++期末速成