自定义View-旋转变色圆角三角形的绘制

8760bfbe10f644409aeefa541ca7fd40.jpeg

f82fe27a055a3dff3776b5345bf4415f.gif

本文字数:3151

预计阅读时间:20分钟

4dbe7f0133ae250877db3e6686368a51.png

在现代设计中,动效图在APP的UI界面中所起到的作用无疑是显著的。相比于静态的界面,动效更符合人类的自然认知体系,它有效地降低了用户的认知负载,UI动效俨然已经成为了不可或缺的一部分。

那么在开发过程中,当遇到UI提供的动态物料满足不了内存以及效果的要求时,我们程序员就不得不通过代码自己去实现效果,这就引出了我们今天要实现的这个旋转变色圆角三角形。当时接到需求的时候,我也以为只要UI同学提供物料即可,但是实验的时候发现如果想呈现动效效果好一点的话,在不同的手机上可能会发生丢帧,OOM以及图形的严重锯齿化等一系列问题,最后实现这个效果的重任就落到了我们程序员的头上(很头大)。

记得当时为了实现这个动画效果,也是发挥了我尘封已久的初中数学知识,还有朋友的帮忙,才能顺利完成开发任务。所以虽然时间过去了很久,依然想做个记录,供大家参考探讨!废话说完终于要进入正题了,大家可以看一下这个UI动效,见下图:

a6b26cf5d57dfc04f2a586c413ca9c5b.gif

首先我们仔细看这个动画,将它进行静态化拆分,大概如下图:

9f40d9cbef4ba0e9db84d9b933ddd1b6.png

其实它的组成还是相对简单的,就是由以下这几个元素构成:黑色的三角形,沿着黑色三角形运动的红色/黑色轨迹,还有整体的旋转。那么我们就按部就班地一步一步来就行了。

第一步:画出一个“被放倒”的等边黑色三角形

问题来了,它三个角的坐标怎么确定呢?这里我们可以先考虑一下,它是要旋转的,等边三角形旋转的轨迹肯定是一个圆,那么呼之欲出的,到这里就需要我们拿出初中的数学知识了——三角形的外接圆,通过它,我们就能够确认出这个三角形的坐标系,也就是三个顶点的坐标位置,自然就可以连线出这个三角形的形状,具体做法如下:

fb544cef04299bf1f0f97db98d27d0e3.png

通过上面这个图,相信能唤醒一些数学基本知识了。我们以o点为坐标系的原点,边长我们命名为a,外接圆半径是r,那么:

A点的x坐标就应该是负的外接圆半径除以2,y坐标是负的三角形的边长除以2,即(-r/2,-a/2);

B点的x坐标应该是负的外接圆半径/2, y坐标是三角形的边长除以2,即(-r/2,a/2);

C点的x坐标应该是外接圆半径,y坐标是0,即(r,0)。

在这个过程中又一个问题出现了,外周圆的半径怎么确认呢?数学公式已经忘得差不多了,但是百度出真知,果然,度娘给出了我们想要的答案:r=√3/3*a,所以我们现在就可以确认这三个点的坐标值了这个过程转换成代码我们就实现了第一步了,具体代码如下:

1、自定义一个View,然后定义出我们所需要的这些变量:

private Paint mPaint;  //画笔
private Path mPath;   //三个点连成线的路径

2、确定这个view的大小,我们可以暂定设置为100dp(当然这个是可以xml动态设置的):

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
    int childHeightSize = DisplayUtils.dipToPx(getContext(), 100);
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
    int childHeightWidth = DisplayUtils.dipToPx(getContext(), 100);
    widthMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightWidth, MeasureSpec.EXACTLY);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

3、在初始化的时候,我们按照上面分析的数学步骤先初始化它的坐标系:

private void initCoordinate() {
    x0 = -(float) (Math.sqrt(3) * mWidth / 6);
    y0 = -mWidth / 2;
    x1 = (float) (Math.sqrt(3) * mWidth / 3);
    y1 = 0;
    x2 = -(float) (Math.sqrt(3) * mWidth / 6);
    y2 = mWidth / 2;
}

4、我们初始化它的画笔,注意我们这个三角形的拐角部分都是圆角,所以需要特别设置一下:

private void initPaint() {
    mPaint = new Paint();
    mPaint.setAntiAlias(true);//抗锯齿
mPaint.setStyle(Paint.Style.STROKE);//画线模式  mPaint.setStrokeWidth(DisplayUtils.dipToPx(mContext, 2));
    //设置所有拐角处变成圆角
    mPaint.setPathEffect(new CornerPathEffect(10));
    mColorBlack = mContext.getResources().getColor(R.color.bg_333333);
    mPaint.setColor(mColorBlack);
 }

5、将三点连线:

private void initPath() {
    mPath = new Path();
    mPath.moveTo(x0, y0);
    mPath.lineTo(x1, y1);
    mPath.lineTo(x2, y2);
    mPath.close();
 }

6、在onDraw方法中 把坐标中心挪到画布的中心 然后把他们画出来:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //移动坐标系
  canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
    canvas.drawPath(mPath, mPaint);
 }

到这一步 我们run一下代码,果然整个静态黑色圆角三角形 就可以呈现出来了,如下图:

87120824c65aca515bb9694ce08942c7.png

第二步:进行红/黑线走向的绘制

仔细观察可以得出,红/黑线是onebyone,在等同的时间内,走完这个静态三角形轨迹的,属性动画应该可以搞定,动画不难,难的是利用进度进行计算的这个过程,我们下面主要说说获取到动画进度后,怎么计算红/黑线的坐标数值。

首先,我们要定义一个动态的点(x,y)用来记录红/黑线这个走向的坐标点,接下来,我们就要根据这个黑色三角形的三个顶点,通过属性动画计算(x,y)的路径,步骤如下:

1、我们确定画笔的起点以及终点也就是三个顶点,如第一段的起点就是(x0,y0)终点就是(x1,y1),第二段的起点就是(x1,y1)终点就是(x2,y2)以此类推第三个点的坐标也很容易确认;

2、因为是等边三角形,所以这个边的动画进度应该是总进度的三分之一,第一段应该就是0至1/3  第二段就是1/3至1/32 , 第三段就是1/32至1;

3、利用path.lineTo进行两点的连接以上几步转换成代码如下所示:

if (progress < 0.33333333f) { //第一段
float progressInLine = progress / 0.33333333f;
path.lineTo(x + Math.abs(x1 - x) * progressInLine, y + Math.abs(y1 - y) * progressInLine);
} else if (progress < 0.66666666f) {//第二段
float progressInLine = (progress - 0.33333333f) / 0.33333333f;
path.lineTo(x1, y1);
path.lineTo(x1 - Math.abs(x2 - x1) * progressInLine, y1 + Math.abs(y2 - y1) * progressInLine);
} else if (progress < 1) {//第三段
float progressInLine = (progress - 0.66666666f) / 0.33333333f;
path.lineTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x2, y2 - Math.abs(y2 - y0) * progressInLine);
} else if (progress >= 1f) {
path.lineTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x0, y0);
path.close();
}

但是,这样计算之后,你会发现,这个动画是有瑕疵的,见下图:

d720ab202b2c7315a37ac8f604508170.gif

就是在画线的时候,你会发现在角的地方会突出去一点,这是为什么呢?还记得咱们之前设置过一个圆弧半径10吗,就是这个小小的弧度导致了我们要连接坐标的实际距离其实是缩短了的,具体可以观察以下两个图(弧度弄得有点夸张,这样比较明显):

a25410b518ac4739f643034954c418c3.png

很明显两个三角形的边长是不一样的,所以,我们不能单纯的只是连接这个三角形的顶点,还需要计算出圆弧导致的偏移量,这时候又要搬出三角函数了,已知圆弧半径,我们可以利用余弦和正弦分别求出x和y的偏移值(具体公式可以百度),代码如下:

float offsetX= (float) (10* Math.sin(Math.PI * 60 / 180));
float offsetY = (float) (10* Math.cos(Math.PI * 60 / 180));

那么刚才的代码步骤就会变成:

if (progress < 0.33333333f) { //第一段
float fromX1 = x1 - offsetX;
float fromY1 = y1 - offsetY;
float progressInLine = progress / 0.33333333f;
path.lineTo(x + Math.abs(fromX1 - x) * progressInLine, y + Math.abs(fromY1 - y) * progressInLine);
} else if (progress < 0.66666666f) {//第二段
float progressInLine = (progress - 0.33333333f) / 0.33333333f;
float fromX1 = x1 - offsetX;
float fromY1 = y1 + offsetY;
float desX2 = x2 + offsetX;
float desY2 = y2 - offsetY;
path.lineTo(x1, y1);
path.lineTo(fromX1 - Math.abs(desX2 - fromX1) * progressInLine, fromY1 + Math.abs(desY2 - fromY1) * progressInLine);
} else if (progress <1) {//第三段
float progressInLine = (progress - 0.66666666f) / 0.33333333f;
float fromY2 = y2 - offsetY;
float desY0 = y0 + offsetY;
path.lineTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x2, fromY2 - Math.abs(fromY2 - desY0) * progressInLine);
} else if (progress >= 1f) {
......
}

第三步:在黑色走线时使三角形转起来

首先我们要确认旋转的角度,由于等边三角形的三个角完全相同,旋转时,只要使下一个角对准原角,就能重合,所以一圈360度除以3,就得到120度.也就是说旋转120度就可以跟原三角形完全重合,但是我们的效果是 a角转到b角 所以 我们直接除以3 也就是旋转40度即可到达我们的预想效果了,转换成代码如下:

private void computePath(float progress) {
    float progressInRotate = progress;
    mDegree = 40 * progressInRotate;//黑线旋转使用
......
 }

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //移动坐标系
    canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
    //旋转  注意顺序
    canvas.rotate(mDegree);
    canvas.drawPath(mPath, mPaint);
}

到此 这个旋转变色的圆角三角形就绘制完成了,虽然效果看上去比较简单,但是实际开发起来还是有一些细节需要注意的,仅此给大家提供一个实现类似效果的小思路,也希望大家可以在评论区提出自己更好的想法,如果需要源码,可以联系我~

e2615f0481c2b2debdcae3a13f2584ee.jpeg

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

汽车新四化,会发生什么?

北京国际汽车展览会正如火如荼地进行中,作为国内外汽车行业瞩目的盛会&#xff0c;众多车企纷纷亮出了自家的“杀手锏”。 这场汽车的盛宴不仅集中展示了众多汽车品牌的最新技术和产品&#xff0c;更深刻体现了汽车新四化的发展趋势。汽车新四化&#xff0c;即电动化、网联化、…

DS进阶:AVL树和红黑树

一、AVL树 1.1 AVL树的概念 二叉搜索树&#xff08;BST&#xff09;虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。因此&#xff0c;两位俄罗斯的数学家G.M.Adelson-…

使用Keil移植工程时修改单片机型号参数

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 当使用Keil对STM32系列单片机开发时&#xff0c;如果使用的是库函数&#xff0c;那么不同型号单片机的工程项目文件是可以直接移植的。只需要按照下面的步骤修改对应的芯片&#xff0c;就可以直接将工程移植过去&a…

JVM垃圾收集器--分区收集器

G1收集器 属性 G1&#xff08;Garbage-First Garbage Collector&#xff09;在 JDK 1.7 时引入&#xff0c;在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性&#xff1a;分代、增量、并行、标记整理、STW。 分代 G1收集器 将内部分为多个大小相等的区域&#x…

Java8 Stream常见用法

Stream流的常见用法&#xff1a; 1.利用stream流特性把数组转list集合 //定义一个数组Integer[] array {5,2,1,6,4,3};//通过stream特性把数组转list集合List<Integer> list Arrays.stream(array).collect(Collectors.toList());//打印结果System.out.println(list);…

全球数据爬取的解决方案-国外数据爬取

引言 随着经济的持续低迷和对外贸易的需求扩大&#xff0c;各个公司为了更好的了解海外客户情况&#xff0c;最简单直接的办法就是从全球收集公共的网络数据。 无论是海外电商用户的消费习惯还是训练自己的通用人工智能chatgpt&#xff0c;都是需要海量和多种类型数据的支持。…

【Linux】进程间通信(共享内存、消息队列、信号量)

一、System V —— 共享内存&#xff08;详解&#xff09; 共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间&#xff0c;这些进程间数据传递不再涉及到内核&#xff0c;换句话说&#xff0c;就是进程不再通过执行进入内核的系统调用来传递彼此的数…

[NSSCTF]prize_p5

前言 之前就学过反序列化的字符串逃逸 但是没怎么做题 补一下窟窿 题目 <?phperror_reporting(0);class catalogue{public $class;public $data;public function __construct(){$this->class "error";$this->data "hacker";}public functi…

HTTP网络协议的请求方法,具体详解(2024-04-26)

1、HTTP 即超文本传输协议&#xff0c;是一种实现客户端和服务器之间通信的响应协议&#xff0c;它是用作客户端和服务器之间的请求 根据 HTTP 标准&#xff0c;HTTP 请求可以使用多种请求方法。 2、方法分类 HTTP1.0 定义了三种请求方法&#xff1a; GET, POST 和 HEAD 方…

表情识别 | 卷积神经网络(CNN)人脸表情识别(Matlab)

表情识别 | 卷积神经网络(CNN)人脸表情识别&#xff08;Matlab&#xff09; 目录 表情识别 | 卷积神经网络(CNN)人脸表情识别&#xff08;Matlab&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab使用卷积神经网络(CNN)&#xff0c;进行人脸表情情绪识别…

论文解读:(CAVPT)Dual Modality Prompt Tuning for Vision-Language Pre-Trained Model

v1文章名字&#xff1a;Dual Modality Prompt Tuning for Vision-Language Pre-Trained Model v2文章名字&#xff1a;Class-Aware Visual Prompt Tuning for Vision-Language Pre-Trained Model 文章汇总 对该文的改进&#xff1a;论文解读&#xff1a;(VPT)Visual Prompt …

STM32H7 HSE时钟的使用方法介绍

目录 概述 1 STM32H750 HSE时钟介绍 2 使用STM32Cube创建Project 3 认识HSE时钟 3.1 HSE时钟的特性 3.2 HSE的典型应用电路 4 STM32Cube中配置时钟 4.1 时钟需求 4.2 配置参数 4.2.1 使能外围资源 4.2.2 使用STM32Cube注意项 4.2.3 配置参数 5 总结 概述 本文主要…

ESP-IDF编译系统详解(1)

接前一篇文章&#xff1a;VSCode ESP-IDF安装与配置全过程 本文内容主要参考&#xff1a; 《ESP32-C3物联网工程开发实战》 —— 乐鑫科技 编著 特此致谢&#xff01; 前文已经详述了ESP-IDF开发环境的搭建&#xff0c;包括ESP-IDF的下载与安装完整流程&#xff0c;以及VSCo…

【Docker】Docker 实践(一):在 Docker 中部署第一个应用

Docker 实践&#xff08;一&#xff09;&#xff1a;在 Docker 中部署第一个应用 1.使用 YUM 方式安装 Docker2.验证 Docker 环境3.在 Docker 中部署第一个应用3.1 小插曲&#xff1a;docker pull 报 missing signature key 错误3.2 重新安装 Nginx 1.使用 YUM 方式安装 Docker…

MySQL中脏读与幻读

一般对于我们的业务系统去访问数据库而言&#xff0c;它往往是多个线程并发执行多个事务的&#xff0c;对于数据库而言&#xff0c;它会有多个事务同时执行&#xff0c;可能这多个事务还会同时更新和查询同一条数据&#xff0c;所以这里会有一些问题需要数据库来解决 我们来看…

IoT Scenario: Smart Retail System-Multiple Sources and Multiple Terminals

物联网/大数据可视化领域发文可以联系&#xff1a;nascimsina.com IoT Scenario: Smart Retail System Overview The use of IoT in the retail industry enhances customer experiences, optimizes inventory management, and provides valuable insights into consumer beh…

【HarmonyOS】Stage 模型 - 基本概念

一、项目结构 如图1所示&#xff1a; 图1 从项目结构来看&#xff0c;这个应用的内部包含了一个子模块叫 entry&#xff0c;模块是应用的基本功能单元&#xff0c;它里面包含源代码、资源、配置文件等。 像这样的模块在应用内部可以创建很多。但模块整体来讲就分成两大类&am…

js 实现记住密码功能

这是我弄得一点源码 使用 js 记住密码 选了半天最后 选择了 js.cookie.min.js 实现的 当然 也加了一点 加密手段 用的 crypto-js 这个 自己封装了一下 感觉还行 以后能重复用的 二话不说 先放资源 &#xff1a; 这么多资源 不得给个赞 关注一下的 当然 最简单的就是…

拖拽式工作流开发有什么突出优势?

想要实现高效率的办公方式&#xff0c;可以试着了解低代码技术平台及拖拽式工作流开发的优势特点。具有好操作、好维护、够灵活、可视化界面操作等优势特点的低代码技术平台可以助力企业实现流程化办公&#xff0c;在发展越来越快速的今天&#xff0c;拖拽式工作流开发得到了很…

陪丨玩丨系丨统前后端开发流程,APP小程序H5前后端源码交付支持二开!多人语音,开黑,线上线下两套操作可在一个系统完成!

100%全部源码出售 官网源码APP源码 管理系统源码 终身免费售后 产品免费更新 产品更新频率高 让您时刻立足于行业前沿 软件开发流程步骤及其作用&#xff1a; 软件开发是一个复杂而系统的过程&#xff0c;涉及多个环节&#xff0c;以下是软件开发的主要流程步骤及其作用…