Android:自定义沿着曲线轨迹移动

前言

前几天,后台有老铁留言,说有个需求,画两条曲线,中间是一个小球,沿着两条线中间的轨迹从左往右移动,让提供个思路,做为一个极度宠粉的博主,思路不仅要提供,实现方案也必须要给出,在互联网中玩的就是真实!

今天的文章大致如下:

1、最终实现效果

2、思路及主要源码剖析

3、全部源码

4、总结

一、最终实现效果

简单录制了一个Gif动画,如下图所示,虽然说是一个简单的Demo,但给出了相关属性和方法,像移动的Icon图标,曲线的的粗细及颜色,以及动画的时长和重复播放都可以动态控制,相关设置之后,即可满足粉丝提出的需求。

二、思路及主要源码剖析

实现这样的一个需求,最主要的是曲线的绘制,和如何设置移动的icon轨迹,这两部分攻克之后,这个需求也就完成了。

首先,如何绘制一条曲线呢?潜意识当中,肯定会想到贝塞尔曲线,有的老铁也许未接触过,觉得这是一个高大上的技术,其实,真正接触起来,也就那么回事,毕竟在Android当中原生的Api就给我们提供了绘制的方法,使用贝塞尔曲线这是再方便不过的。

对贝塞尔曲线做一个简单的概述吧,PhotoShop工具大家都用过吧,里面的”钢笔“功能,就是用贝塞尔曲线来作为计算基础绘制的。

 

贝塞尔曲线其原理主要是依靠顶点间的比例来计算,具体就是通过起始点和终点,以及若干控制点,通过调整控制点来形成的,在日常的开发中,最常见的就是二阶和三阶贝塞尔曲线,有的老铁可能会问,为啥没有一阶,因为一阶是没有控制点的,仅有两个数据点(A 和 B),也就是一条直线,这个没什么好说的。

二阶贝塞尔曲线

上图比较简单,模拟二阶贝塞尔曲线的运动轨迹,首先P₀和P₁形成了一条一阶贝塞尔曲线,Q₀就是这条线上匀速运动的点,P₁和P₂也是一条贝塞尔曲线,同样Q₁是这条线上匀速运动的点,两条一阶贝塞尔曲线,Q₀和Q₁相连后又生成了新的一条一阶贝塞尔曲线,而在这条线上匀速运动的点B,它的运动形态就是二阶贝塞尔曲线的最终形态,简而言之,之所以称B的运动轨迹为二阶贝塞尔曲线,是因为B的运动轨迹建立在两条一阶贝塞尔曲线之上。

动态的运动轨迹如下:

 

在Android中二阶贝塞尔绘制相关Api如下:

public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)

quadTo函数,其中前两个点为控制点坐标,也就是上面的P₁,而后两个点则是终点坐标,也就是P₂,至于起始点,也就是P₀,在开发中,使用path.moveTo()设置起始点。

rQuadTo函数,四个参数解释如下:dx1,控制点x坐标,相对于上一个终点x坐标的位移;dy1,控制点y坐标,相对于上一个终点y坐标的位移;dx2,终点x坐标,相对于上一个终点x坐标位移;dy2:终点y坐标,相对于上一个坐标y坐标的位移。

至于三阶贝塞尔曲线,暂时没有用到,先不做解释,后续有时间,再对贝塞尔曲线做详细解释吧。

简单的了解完贝塞尔曲线之后,那么绘制曲线就比较简单了,根据需求,上下两条曲线,由于路径不一样,则需要逐一创建,在这里,我单独创建了一个中间的路径,也就是两条曲线的中间位置,这个路径不做绘制,用于移动的View轨迹使用。

由于屏幕的宽度和自己设置的曲面宽度不一样,为了达到曲线展示的完整性,当小于屏幕的宽度时,我们进行循环绘制,曲线绘制代码如下:

override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)val centerHeight = height / 2//y轴中点val leftStartPoint = mCurveWidth / 2 //左边起始点//上面的路径起始点mPathTop.moveTo(0f, (centerHeight - mCurveMargin))//中间的路径起始点mPathCenter.moveTo(-mCurveThumpWidth, centerHeight.toFloat() - mCurveThumpHeight / 2)//下面的路径起始点mPathBottom.moveTo(0f, (centerHeight + mCurveMargin))var tempWidth = 0//循环,若大于View的宽度,就停止while (tempWidth < width) {val controlX = (leftStartPoint / 2).toFloat()//控制点X坐标val controlY = -100f//控制点Y坐标val endX = leftStartPoint.toFloat()//结束X坐标val endY = 0f//结束Y坐标setCurvePath(mPathTop, controlX, controlY, endX, endY)setCurvePath(mPathCenter, controlX, controlY, endX, endY)setCurvePath(mPathBottom, controlX, controlY, endX, endY)tempWidth += mCurveWidth}canvas?.drawPath(mPathTop, mPaint)canvas?.drawPath(mPathBottom, mPaint)}

因为需要连续绘制,这里使用的rQuadTo函数。

    /*** AUTHOR:AbnerMing* INTRODUCE:设置曲线路径*/private fun setCurvePath(path: Path, controlX: Float, controlY: Float, endX: Float, endY: Float) {path.rQuadTo(controlX, controlY, endX, endY)path.rQuadTo(controlX, 100f, endX, endY)}

动画的运动轨迹

一开始,始终想不到,让icon如何沿着曲线进行移动,利用贝塞尔曲线工具动态的计算Y轴吧,总计算不对,经过后面的不断调研,才发现,动画是可以传入路径的,真尴尬,感觉这么多年Android白做了,这个API竟然没想到。

mPathCenter就是上面的中间路径,记录两条曲线的中间运动轨迹。

    /*** AUTHOR:AbnerMing* INTRODUCE:启动动画*/fun startAnimate(view: View) {val mAnimator = ObjectAnimator.ofFloat(view, X, Y, mPathCenter)mAnimator.duration = mAnimateDurationmAnimator.repeatCount = mAnimateRepeatCountmAnimator.start()}

绘制的难度,基本上就以上两点,一个是曲线的绘制,一个是曲线运动轨迹动画,说实在话,这个动画虽然简单,但如果不知道,还真能被难住~

三、全部源码

源码没多少内容,很多都是一些简单的方法设置和属性设置,大家可以看到,这里我定义了两个View,一个是继承于View,一个继承于ViewGroup,继承于View没什么好说的,就是简单的用来绘制曲线,至于ViewGroup这个,主要用来,移动的Icon和曲线View相结合,其实这一步完全可以省略,完全可以把Icon这个View传递到曲线View中,但是,为了使用起来简单,这里就包了一层。

package com.abner.curveimport android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout/***AUTHOR:AbnerMing*DATE:2023/8/9*INTRODUCE:曲线动画*/class CurveAnimationView : LinearLayout {//动画时长private var mAnimateDuration = 5000//曲线颜色private var mCurveColor = Color.BLACK//曲线宽度private var mCurveWidth = 8fprivate var mCurveThumb: Int? = null//移动的Icon//移动的Thump宽度private var mCurveThumbWidth = 30f//移动的Thump高度private var mCurveThumbHeight = 30f//两条曲线间距private var mCurveTopBottomMargin = 50fprivate lateinit var mImageView: ImageViewprivate var mCurveAnimation: CurveAnimation? = nullconstructor(context: Context) : super(context) {init(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {context.obtainStyledAttributes(attrs, R.styleable.CurveAnimationView).apply {//曲线颜色mCurveColor =getColor(R.styleable.CurveAnimationView_curve_color, mCurveColor)//曲线宽度mCurveWidth = getDimension(R.styleable.CurveAnimationView_curve_width, mCurveWidth)//移动图标,默认mCurveThumb =getResourceId(R.styleable.CurveAnimationView_curve_thump,R.mipmap.ic_launcher)//移动时长mAnimateDuration =getInt(R.styleable.CurveAnimationView_curve_animate_duration, mAnimateDuration)//两个曲线上下距离mCurveTopBottomMargin = getDimension(R.styleable.CurveAnimationView_curve_top_bottom_margin,mCurveTopBottomMargin)//两个曲线上下距离mCurveThumbWidth = getDimension(R.styleable.CurveAnimationView_curve_thump_width,mCurveThumbWidth)//两个曲线上下距离mCurveThumbHeight = getDimension(R.styleable.CurveAnimationView_curve_thump_height,mCurveThumbHeight)}init(context)}private fun init(context: Context) {mCurveAnimation = CurveAnimation(context)addView(mCurveAnimation)//初始化图片资源mImageView = ImageView(context)addView(mImageView)setImageViewSize(mCurveThumbWidth.toInt(), mCurveThumbHeight.toInt())//默认图片大小mCurveAnimation?.setCurveThumpSize(mCurveThumbWidth, mCurveThumbHeight)//设置图片资源mCurveThumb?.let { setImageResource(it) }//设置曲线颜色setCurveColor(mCurveColor)//设置曲线宽度setStrokeWidth(mCurveWidth)//设置动画时长setAnimateDuration(mAnimateDuration)//设置两条曲线上下边距setCurveMargin(mCurveTopBottomMargin)}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线上下边距*/fun setCurveMargin(curveMargin: Float) {mCurveAnimation?.setCurveMargin(curveMargin)}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线颜色*/fun setCurveColor(curveColor: Int): CurveAnimationView {mCurveAnimation?.setCurveColor(curveColor)return this}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线宽度*/fun setStrokeWidth(curveWidth: Float): CurveAnimationView {mCurveAnimation?.setStrokeWidth(curveWidth)return this}/*** AUTHOR:AbnerMing* INTRODUCE:设置图片的资源*/fun setImageResource(resId: Int): CurveAnimationView {mImageView.setImageResource(resId)return this}/*** AUTHOR:AbnerMing* INTRODUCE:设置移动的图片宽高*/fun setImageViewSize(width: Int, height: Int): CurveAnimationView {val layoutParams = mImageView.layoutParams as LayoutParamslayoutParams.width = widthlayoutParams.height = heightmImageView.layoutParams = layoutParamsmCurveAnimation?.setCurveThumpSize(width.toFloat(), height.toFloat())return this}/*** AUTHOR:AbnerMing* INTRODUCE:设置动画时长*/fun setAnimateDuration(animateDuration: Int): CurveAnimationView {mCurveAnimation?.setAnimateDuration(animateDuration)return this}/*** AUTHOR:AbnerMing* INTRODUCE:设置动画重复次数*/fun setAnimateRepeatCount(repeatCount: Int): CurveAnimationView {mCurveAnimation?.setAnimateRepeatCount(repeatCount)return this}/*** AUTHOR:AbnerMing* INTRODUCE:启动动画*/fun startAnimate() {mCurveAnimation?.startAnimate(mImageView)}}private class CurveAnimation : View {constructor(context: Context) : super(context) {init()}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {init()}//上面曲线路径private lateinit var mPathTop: Path//中间路径,用于记录View的移动private lateinit var mPathCenter: Path//下面曲线路径private lateinit var mPathBottom: Path// 曲线的画笔private lateinit var mPaint: Paint//定义曲面宽度private var mCurveWidth = 600//定义上下路径的距离private var mCurveMargin = 50f/*** AUTHOR:AbnerMing* INTRODUCE:初始化*/private fun init() {// 消除锯齿mPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {strokeWidth = 8f//默认宽度style = Paint.Style.STROKEcolor = Color.BLACK}mPathTop = Path()mPathCenter = Path()mPathBottom = Path()}override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)val centerHeight = height / 2//y轴中点val leftStartPoint = mCurveWidth / 2 //左边起始点//上面的路径起始点mPathTop.moveTo(0f, (centerHeight - mCurveMargin))//中间的路径起始点mPathCenter.moveTo(-mCurveThumpWidth, centerHeight.toFloat() - mCurveThumpHeight / 2)//下面的路径起始点mPathBottom.moveTo(0f, (centerHeight + mCurveMargin))var tempWidth = 0//循环,若大于View的宽度,就停止while (tempWidth < width) {val controlX = (leftStartPoint / 2).toFloat()//控制点X坐标val controlY = -100f//控制点Y坐标val endX = leftStartPoint.toFloat()//结束X坐标val endY = 0f//结束Y坐标setCurvePath(mPathTop, controlX, controlY, endX, endY)setCurvePath(mPathCenter, controlX, controlY, endX, endY)setCurvePath(mPathBottom, controlX, controlY, endX, endY)tempWidth += mCurveWidth}canvas?.drawPath(mPathTop, mPaint)canvas?.drawPath(mPathBottom, mPaint)}private var mCurveThumpWidth = 0fprivate var mCurveThumpHeight = 0ffun setCurveThumpSize(thumpWidth: Float, thumpHeight: Float) {mCurveThumpWidth = thumpWidthmCurveThumpHeight = thumpHeightinvalidate()}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线颜色*/fun setCurveMargin(curveMargin: Float) {mCurveMargin = curveMargininvalidate()}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线颜色*/fun setCurveColor(curveColor: Int) {mPaint.color = curveColorinvalidate()}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线宽度*/fun setStrokeWidth(strokeWidth: Float) {mPaint.strokeWidth = strokeWidthinvalidate()}/*** AUTHOR:AbnerMing* INTRODUCE:设置曲线路径*/private fun setCurvePath(path: Path, controlX: Float, controlY: Float, endX: Float, endY: Float) {path.rQuadTo(controlX, controlY, endX, endY)path.rQuadTo(controlX, 100f, endX, endY)}/*** AUTHOR:AbnerMing* INTRODUCE:设置动画时长*/private var mAnimateDuration = 5000Lfun setAnimateDuration(animateDuration: Int) {mAnimateDuration = animateDuration.toLong()}/*** AUTHOR:AbnerMing* INTRODUCE:重复几次*/private var mAnimateRepeatCount = 0fun setAnimateRepeatCount(repeatCount: Int) {mAnimateRepeatCount = repeatCount}/*** AUTHOR:AbnerMing* INTRODUCE:启动动画*/fun startAnimate(view: View) {val mAnimator = ObjectAnimator.ofFloat(view, X, Y, mPathCenter)mAnimator.duration = mAnimateDurationmAnimator.repeatCount = mAnimateRepeatCountmAnimator.start()}}

 

属性文件

属性可以自行定义,目前我简单的定义其中几个,主要代码如下:

 <declare-styleable name="CurveAnimationView"><!--两个曲线上下距离--><attr name="curve_top_bottom_margin" format="dimension" /><!--曲线可移动的thump--><attr name="curve_thump" format="reference" /><!--曲线颜色--><attr name="curve_color" format="color" /><!--曲线宽度--><attr name="curve_width" format="dimension" /><!--thump移动时长--><attr name="curve_animate_duration" format="integer" /><!--曲线可移动的thump宽度--><attr name="curve_thump_width" format="dimension" /><!--曲线可移动的thump高度--><attr name="curve_thump_height" format="dimension" /></declare-styleable>

 

四、总结

绘制曲线的时候,如果控制点没找对,所绘制的曲线就没那么丝滑,这是在贝塞尔曲线绘制中需要注意的,当然了,关于贝塞尔曲线,还有很多需要了解的知识,后面有时间,会从回头到尾的阐述一下,大家可以后续关注。

其实关于后台的留言,我也并不是第一时间能够回复,毕竟我也有工作安排,也希望老铁们多包容与理解,但请相信,只要在自己掌握的范围之内,一定会给出一些相关思路,尽自己所能,帮助一些老铁,还是那句话,互联网中,玩的就是真实!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/92576.html

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

相关文章

八大排序超详解(动图+源码)

&#x1f493;博主个人主页:不是笨小孩&#x1f440; ⏩专栏分类:数据结构与算法&#x1f440; 刷题专栏&#x1f440; C语言&#x1f440; &#x1f69a;代码仓库:笨小孩的代码库&#x1f440; ⏩社区&#xff1a;不是笨小孩&#x1f440; &#x1f339;欢迎大家三连关注&…

【Linux】IO多路转接——select接口

目录 I/O多路转接之select select初识 select函数 socket就绪条件 select基本工作流程 select服务器 select的优点 select的缺点 select的适用场景 I/O多路转接之select select初识 select是系统提供的一个多路转接接口。 select系统调用可以让我们的程序同时监视多…

拦截器和过滤器的区别

&#x1f600;前言 本篇博文是关于拦截器VS 过滤器的分享&#xff0c;希望你能够喜欢&#x1f60a; &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是我…

【机器学习4】构建良好的训练数据集——数据预处理(一)处理缺失值及异常值

数据预处理 &#x1f4ab;数据预处理的重要性&#x1f4ab;处理缺失值⭐️识别表格中的数据⭐️计算每列缺失值的数量⭐️删除含有缺失值的样本或特征⭐️填充缺失值 &#x1f4ab;处理异常值⭐️异常值的鉴别⭐️异常值的处理 &#x1f4ab;将数据集划分为训练数据集和测试数据…

Jmeter-压力测试工具

文章目录 Jmeter快速入门1.1.下载1.2.解压1.3.运行 2.快速入门2.1.设置中文语言2.2.基本用法 Jmeter快速入门 1s内发送大量请求&#xff0c;模拟高QPS&#xff0c;用以测试网站能承受的压力有多大 Jmeter依赖于JDK&#xff0c;所以必须确保当前计算机上已经安装了JDK&#xff0…

使用ip2region获取客户端地区

目录 从gitee拉取ip2region.xdb资源文件 写测试类 注意要写对资源路径 本地测试结果 ​编辑 远端测试结果 从gitee拉取ip2region.xdb资源文件 git clone https://gitee.com/lionsoul/ip2region.git 将xdb放入resources资源文件夹 引入依赖 <dependency><groupId&…

Vue响应式数据的原理

在 vue2 的响应式中&#xff0c;存在着添加属性、删除属性、以及通过下标修改数组&#xff0c;但页面不会自动更新的问题。而这些问题在 vue3 中都得以解决。 vue3 采用了 proxy 代理&#xff0c;用于拦截对象中任意属性的变化&#xff0c;包括&#xff1a;属性的读写、属性的…

Pycharm社区版连接WSL2中的Mysql8.*

当前时间2023.08.13&#xff0c;Windows11中默认的WSL版本已经是2了&#xff0c;在WSL2中默认的Ubuntu版本已经是22.04&#xff0c;而Ubuntu22.04中默认的Mysql版本已经是8.*。 Wsl 2 中安装mysql WSL2中安装Mysql的方法参考自微软官方文档【开始使用适用于 Linux 的 Windows …

《论文阅读12》RandLA-Net: Efficient Semantic Segmentation of Large-Scale Point Clouds

一、论文 研究领域&#xff1a;全监督3D语义分割&#xff08;室内&#xff0c;室外RGB&#xff0c;kitti&#xff09;论文&#xff1a;RandLA-Net: Efficient Semantic Segmentation of Large-Scale Point Clouds CVPR 2020 牛津大学、中山大学、国防科技大学 论文链接论文gi…

C++储备

一、类的 三大特性 封装&#xff0c;继承&#xff0c;多态 二、虚函数 为啥要用到虚函数 C虚函数详解_Whitesad_的博客-CSDN博客 三、函数重载 四、封装的保护权限 1.public 成员类内&#xff0c;内外都可以访问 2.protected 成员&#xff0c;类内可以访问&#xff0c…

两只小企鹅(Python实现)

目录 1 和她浪漫的昨天 2 未来的旖旎风景 3 Python完整代码 1 和她浪漫的昨天 是的,春天需要你。经常会有一颗星等着你抬头去看&#xff1b; 和她一起吹晚风吗﹖在春天的柏油路夏日的桥头秋季的公园寒冬的阳台&#xff1b; 这世界不停开花&#xff0c;我想放进你心里一朵&am…

【软件工程】软件测试

软件测试的对象 软件程序文档 测试对象&#xff1a;各个阶段产生的源程序和文档。 软件测试的目的 基于不同的立场&#xff0c;对软件测试的目的存在着两种完全对立的观点。 &#xff08;1&#xff09;一种观点是通过测试暴露出软件中所包含的故障和缺陷(从用户的角度)&#xf…

语聚AI公测发布,大语言模型时代下新的生产力工具

语聚AI 公测发布 距离语聚AI内测上线已经过去近1个月。 这期间&#xff0c;我们共邀请了近百位资深用户与行业专家加入语聚AI产品体验。通过大家的热情参与积极反馈&#xff0c;我们不断优化并完善了语聚AI的功能与使用体验。 经过研发团队不懈的努力&#xff0c;今天语聚AI终…

深度学习实战基础案例——卷积神经网络(CNN)基于SqueezeNet的眼疾识别|第1例

文章目录 前言一、数据准备1.1 数据集介绍1.2 数据集文件结构 二、项目实战2.1 数据标签划分2.2 数据预处理2.3 构建模型2.4 开始训练2.5 结果可视化 三、数据集个体预测 前言 SqueezeNet是一种轻量且高效的CNN模型&#xff0c;它参数比AlexNet少50倍&#xff0c;但模型性能&a…

实战项目:基于主从Reactor模型实现高并发服务器

项目完整代码仿mudou库one thread one loop式并发服务器实现: 仿muduo库One Thread One Loop式主从Reactor模型实现⾼并发服务器&#xff1a;通过模拟实现的⾼并发服务器组件&#xff0c;可以简洁快速的完成⼀个⾼性能的服务器搭建。并且&#xff0c;通过组件内提供的不同应⽤层…

uniapp的UI框架组件库——uView

在写uniapp项目时候&#xff0c;官方所推荐的样式库并不能满足日常的需求&#xff0c;也不可能自己去写相应的样式&#xff0c;费时又费力&#xff0c;所以我们一般会去使用第三方的组件库UI&#xff0c;就像vue里我们所熟悉的elementUI组件库一样的道理&#xff0c;在uniapp中…

UVM学习知识点

UVM构建 include 和 import pkg区别.sv .svhhdl_top.sv和hvl_top.sv回顾uvm_config&#xff0c;以及自定义uvm_configverilog:parameter、defparam与 localparamtest_basebuild_phaseend_of_elaboration_phasefunction void configure_agentset_seqsend_of_elaboration_phaseuv…

Shell编程——弱数据类型的脚本语言快速入门指南

目录 Linux Shell 数据类型 变量类型 运算符 算术运算符 赋值运算符 拼接运算符 比较运算符 关系运算符 控制结构 顺序结构 条件分支结构 if 条件语句 case 分支语句 循环结构 for 循环 while 循环 until 循环 break 语句 continue语句 函数 函数定义 …

【数学建模】清风数模更新5 灰色关联分析

灰色关联分析综述 诸如经济系统、生态系统、社会系统等抽象系统都包含许多因素&#xff0c;系统整体的发展受各个因素共同影响。 为了更好地推动系统发展&#xff0c;我们需要清楚哪些因素是主要的&#xff0c;哪些是次要的&#xff0c;哪些是积极的&#xff0c;哪些是消极的…

@RequestHeader使用

RequestHeader 请求头参数的设置 GetMapping("paramTest/requestHeader")public String requestHeaderTest(RequestHeader("name") String name){return name;} 在Postman的Headers中添加请求头参数&#xff0c;不过貌似不能加中文