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

智能推荐

leetcode 172. 阶乘后的零-程序员宅基地

文章浏览阅读63次。题目给定一个整数 n,返回 n! 结果尾数中零的数量。解题思路每个0都是由2 * 5得来的,相当于要求n!分解成质因子后2 * 5的数目,由于n中2的数目肯定是要大于5的数目,所以我们只需要求出n!中5的数目。C++代码class Solution {public: int trailingZeroes(int n) { ...

Day15-【Java SE进阶】IO流(一):File、IO流概述、File文件对象的创建、字节输入输出流FileInputStream FileoutputStream、释放资源。_outputstream释放-程序员宅基地

文章浏览阅读992次,点赞27次,收藏15次。UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。文件字节输入流:每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1。注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。UTF-8字符集:汉字占3个字节,英文、数字占1个字节。GBK字符集:汉字占2个字节,英文、数字占1个字节。GBK规定:汉字的第一个字节的第一位必须是1。_outputstream释放

jeecgboot重新登录_jeecg 登录自动退出-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏3次。解决jeecgboot每次登录进去都会弹出请重新登录问题,在utils文件下找到request.js文件注释这段代码即可_jeecg 登录自动退出

数据中心供配电系统负荷计算实例分析-程序员宅基地

文章浏览阅读3.4k次。我国目前普遍采用需要系数法和二项式系数法确定用电设备的负荷,其中需要系数法是国际上普遍采用的确定计算负荷的方法,最为简便;而二项式系数法在确定设备台数较少且各台设备容量差..._数据中心用电负荷统计变压器

HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板_网页设计成品百度网盘-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏46次。HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他 等网页设计题目, A+水平作业_网页设计成品百度网盘

【Jailhouse 文章】Look Mum, no VM Exits_jailhouse sr-iov-程序员宅基地

文章浏览阅读392次。jailhouse 文章翻译,Look Mum, no VM Exits!_jailhouse sr-iov

随便推点

chatgpt赋能python:Python怎么删除文件中的某一行_python 删除文件特定几行-程序员宅基地

文章浏览阅读751次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python 删除文件特定几行

Java过滤特殊字符的正则表达式_java正则表达式过滤特殊字符-程序员宅基地

文章浏览阅读2.1k次。【代码】Java过滤特殊字符的正则表达式。_java正则表达式过滤特殊字符

CSS中设置背景的7个属性及简写background注意点_background设置背景图片-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏17次。css中背景的设置至关重要,也是一个难点,因为属性众多,对应的属性值也比较多,这里详细的列举了背景相关的7个属性及对应的属性值,并附上演示代码,后期要用的话,可以随时查看,那我们坐稳开车了······1: background-color 设置背景颜色2:background-image来设置背景图片- 语法:background-image:url(相对路径);-可以同时为一个元素指定背景颜色和背景图片,这样背景颜色将会作为背景图片的底色,一般情况下设置背景..._background设置背景图片

Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏8次。Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程

PyCharm2021安装教程-程序员宅基地

文章浏览阅读10w+次,点赞653次,收藏3k次。Windows安装pycharm教程新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入下载安装PyCharm1、进入官网PyCharm的下载地址:http://www.jetbrains.com/pycharm/downl_pycharm2021

《跨境电商——速卖通搜索排名规则解析与SEO技术》一一1.1 初识速卖通的搜索引擎...-程序员宅基地

文章浏览阅读835次。本节书摘来自异步社区出版社《跨境电商——速卖通搜索排名规则解析与SEO技术》一书中的第1章,第1.1节,作者: 冯晓宁,更多章节内容可以访问云栖社区“异步社区”公众号查看。1.1 初识速卖通的搜索引擎1.1.1 初识速卖通搜索作为速卖通卖家都应该知道,速卖通经常被视为“国际版的淘宝”。那么请想一下,普通消费者在淘宝网上购买商品的时候,他的行为应该..._跨境电商 速卖通搜索排名规则解析与seo技术 pdf

推荐文章

热门文章

相关标签