5 分钟了解机器学习的特征工程

介绍

在我们进一步研究之前,我们需要定义机器学习中的特征。

如果您不熟悉机器学习,那么特征就是机器学习算法模型的输入。

%title插图%num

什么是特征工程?

特征工程使用数学、统计学和领域知识从原始数据中提取有用的特征的方法。

例如,如果两个数字特征的比率对分类实例很重要,那么计算该比率并将其作为特征包含可能会提高模型质量。

例如有两个特征:平方米和公寓价格。您可能需要通过获取每平方米价格来创建特征以改进您的模型。

%title插图%num

如何做特征工程?

让我们看看特征工程的不同策略。在本文中,我们不会看到所有方法,而是*流行的方法。添加和删除特征:

假设我们确实具有以下特征:

%title插图%num

如果我们想预测公寓的价格,植物的数量可能无关紧要。在这种情况下,我们需要从机器学习模型中删除此功能,以免添加额外的噪音。

这种噪音被称为维度灾难。这意味着随着数据中特征数量的增加,构建良好模型所需的数据点数量呈指数增长。

我们需要选择哪些特征与我们的模型*相关。

将多个特征组合成一个特征:

%title插图%num

在上面的例子中,我们可以看到平方米和平方英尺实际上是相同的数据,但不是相同的单位。如果我们将其提供给我们的算法,它将必须了解平方米和平方英尺是相关的并且实际上是相同的特征。

这就是为什么我们需要决定采用哪种测量并只保留一个。

我们也可以有两个特征,狗的数量和猫的数量,并在动物数量下将它们组合起来。

%title插图%num

尽管如此,结合这些功能并不是每次都是一个好主意。例如,在日期特征的情况下,可能是星期几很重要。

你需要记住质量胜于数量。

清理现有特征:

您需要保留您认为与模型相关的特征,以获取数据中的正确信号。

为此,您可以:

  1. 估算缺失值。
  2. 删除不尝试使用不具有代表性的数据点进行训练的异常值。
  3. 摆脱比例尺,例如,如果您有以厘米为单位的要素而其他一些以米为单位的要素,请尝试将所有要素都以厘米为单位进行转换。这称为规范化。
  4. 由于更容易的分布,转换倾斜的数据以使其更适合我们的模型。

分箱:

分箱是指您进行数值测量并将其转换为类别。

以下是房屋销售的示例:

%title插图%num

在那个例子中,我们可以假设销售价格取决于有游泳池的事实。

然后我们可以通过预处理数据并用布尔未来替换游泳池长度来简化我们的模型。

%title插图%num

独热(One-hot)编码:

独热编码是一种以机器学习算法能够理解的方式表示分类数据的方式。

我们的模型理解数字但不理解字符串,这就是我们需要将字符串转换为数字的原因。但是,我们不能为我们的字符串分配随机数,因为我们的模型可能比小数字更重视大数字。这就是为什么我们要使用 one-hot 编码的原因。

以下是有关房屋销售的示例:

%title插图%num

One-hot 编码对于用机器学习模型能够理解的简单数字数据替换分类数据很有用。

总结

特征工程将帮助您:

借助适当的特征,解决适当的业务案例问题。

提高机器学习算法的性能。

用 YOLO v5+DeepSORT,打造实时多目标跟踪模型

目标跟踪 (Object Tracking) 是机器视觉领域的重要课题,根据跟踪目标的数量,可分为单目标跟踪 (Single Object Tracking,简称 SOT) 和多目标跟踪 (Multi Object Tracking,简称 MOT)。

多目标跟踪往往因为跟踪 ID 众多、遮挡频繁等,容易出现目标跟丢的现象。借助跟踪器 DeepSORT 与检测器 YOLO v5,可以打造一个高性能的实时多目标跟踪模型。

本文将对单目标跟踪和多目标跟踪分别进行介绍,文末将详解 YOLO v5+DeepSORT 的实现过程及具体代码。

单目标跟踪详解

定义 

单目标跟踪 SOT 是指在视频首帧给出目标,根据上下文信息,在后续帧定位出目标位置,建立跟踪模型对目标的运动状态进行预测。

应用场景 

SOT 在智能视频监控、自动驾驶、机器人导航、人机交互等领域应用广泛。

%title插图%num

足球比赛中利用 SOT 预测足球运动轨迹

研究难点

*主要的三个难点:目标背景的变化、物体本身的变化、光照强度变化。

主流算法(基于深度学习) 

解决 SOT 问题主要有两种方法:判别式跟踪及生成式跟踪,随着深度学习在图像分类、目标检测等机器视觉相关任务中的成功应用,深度学习也开始大量应用于目标跟踪算法中。

本文主要围绕基于深度学习的 SOT 算法进行介绍。

%title插图%num

各时间节点的代表性目标跟踪算法

2012 年后以 AlexNet 为代表的深度学习方法

被引入到目标跟踪领域中

关键算法:SiamFC

与传统目标跟踪中所用的在线学习方法不同,SiamFC 侧重于在离线阶段学习强嵌入。

它将一个基本跟踪算法,与一个在 ILSVRC15 数据集上进行端到端训练的新型全卷积孪生网络 (fully-convolutional Siamese network) 相结合,用于视频中的目标检测。

%title插图%num

全卷积孪生网络架构示意图

实验证明,在模型测试和训练期间,孪生全卷积深度网络对已有数据的利用更加高效。

SiamFC 开创了将孪生网络结构应用于目标跟踪领域的先河,显著提高了深度学习方法跟踪器的跟踪速度,结构简单性能优异。

相关论文:

https://arxiv.org/pdf/1606.09549.pdf

相关衍生算法 

1、StructSiam

提出了 local structure learning method,同时考虑目标的 local pattern 和结构关系。为此,作者设计了一个局部模式检测模块,来自动识别目标物体的辨别区域。

该模型可以以端到端的方式进行训练。

相关论文:

https://openaccess.thecvf.com/content_ECCV_2018/papers/Yunhua_Zhang_Structured_Siamese_Network_ECCV_2018_paper.pdf

2、SiamFC-tri

作者提出了一种全新的 triplet loss,用于提取跟踪物体的 expressive deep feature。在不增加输入的情况下,该方法可以利用更多元素进行训练,通过组合原始样本,实现更强大的特征。

相关论文:

https://openaccess.thecvf.com/content_ECCV_2018/papers/Xingping_Dong_Triplet_Loss_with_ECCV_2018_paper.pdf

3、DSiam

作者提出了动态孪生网络,通过一个快速转换学习模型,能够有效地在线学习目标的外观变化和先前帧的背景压制。同时作者还提出了元素多层融合,利用多层深度特征自适应地整合网络输出。

DSiam 允许使用任何可行的通用或经过特殊训练的特征,如 SiamFC 和 VGG,且动态孪生网络可以直接在标记的视频序列上进行整合训练,充分利用移动物体丰富的时空信息。

JupyterLab 3.0,*其强大的下一代Notebook!

JupyterLab 是广受欢迎的 Jupyter Notebook「新」界面。它是一个交互式的开发环境,可用于 notebook、代码或数据,因此它的扩展性非常强。用户可以使用它编写 notebook、操作终端、编辑 markdown 文本、打开交互模式、查看 csv 文件及图片等。除此以外,JupyterLab 还具有灵活而强大的用户界面。就在近日,这款好用的工具发布了新版本 JupyterLab 3.0。

%title插图%num

JupyterLab 3.0 在以下几个方面进行了改进:

  • 可视化调试器;
  • 支持多种显示语言;
  • notebook 目录;
  • 扩展系统。

3 种安装方式

JupyterLab 3.0 的安装方式有 3 种,*种采用 pip 方式进行安装,代码如下:

  1. pip install jupyterlab==3

第 2 种采用 mamba(快速跨平台软件包管理器)方式进行安装,代码如下:

  1. mamba install -c conda-forge jupyterlab=3

第 3 种采用 conda 方式进行安装,代码如下:

  1. conda install -c conda-forge jupyterlab=3

需要注意,为了兼容 JupyterLab 3.0,许多第三方扩展仍在更新中,所以用户需要检查自己使用的扩展,必要时也可以更新这些扩展。接下来详细介绍 JupyterLab 3.0 在面向用户使用方面的一些主要改进。

JupyterLab 3.0 新特性

可视化调试器

JupyterLab 3.0 现在具备可视化调试器功能了。为了使用可视化调试器,用户首先需要一个支持调试器的内核。Xeus-Python 内核是*个支持 Python 代码调试的 Jupyter 内核。展示如下:

%title插图%num

在 JupyterLab 3.0 中使用可视化调试器进入 Python 程序。

更多详细文档请参阅:

https://jupyterlab.readthedocs.io/en/stable/user/debugger.html

目录扩展

现在 JupyterLab 3.0 提供了目录扩展,使得用户更方便地查看和浏览文档结构。展示如下:

%title插图%num

在 JupyterLab 3.0 使用目录功能

支持多种语言显示

JupyterLab 3.0 提供了设置用户界面显示语言的功能。若要使用这种功能,用户需要将语言包作为单独的 Python 包安装。语言包在 GitHub 项目中已经分组,采用 pip 的方式就可以安装。例如,使用以下代码可以安装简体中文语言包:

pip install jupyterlab-language-pack-zh-CN

%title插图%num

以简体中文显示的 JupyterLab 3.0 界面

关于添加新语言包请参考:https://jupyterlab.readthedocs.io/en/stable/user/language.html

简单交互界面模式的改进

JupyterLab 3.0 对简单交互界面模式(即以往的单文档显示模式)进行了更新,使交互界面模式更流畅、更能面向文档。用户可以使用状态栏中的开关切换简单交互界面模式,也可以从视图菜单或命令面板中切换或者使用默认快捷键「Ctrl/Cmd+Shift+D」。

%title插图%num

启用和禁用简单交互界面模式

JupyterLab 3.0 对移动设备的支持也得到了很大的改进。用户可以对窗口进行缩展,使布局更加紧凑。当窗口缩小时,JupyterLab 自动切换到简单交互界面模式。

%title插图%num

JupyterLab 在屏幕缩小时自动切换到简单交互界面模式

目前这项功能正在不断的迭代更新,使得这个交互界面在移动设备上更容易访问。

使用 pip 和 conda/mamba 方式安装新的扩展

JupyterLab 扩展现在可以作为预构建的扩展进行分发,而不需要用户重新构建 JupyterLab 或安装 Node.js。用户可以使用熟悉的包管理器(如 pip、conda 和 mamba)将预构建的扩展作为 Python 包分发,从而使得安装和使用扩展更快更方便。

%title插图%num

采用 pip 方式安装新的扩展

预构建的扩展可以作为单独的包发布到 PyPI 和 conda-forge 中,或者捆绑到带有 Jupyter 服务器扩展和 Classic Notebook 扩展的包中。这些有助于整个系统的一致性。

例如:使用 pip 或 conda 方式安装新的 ipywidgets 7.6.0,以在典型的 Jupyter Notebook 和 JupyterLab3.0 中自动启用 ipywidgets—无需额外的步骤或者重建 JupyterLab。

%title插图%num

在 JupyterLab 3.0 中自动安装 ipywidgets

改进 Extension Author 的工作流程

新的预构建扩展对于 Extension Author 来说开发起来非常方便。TypeScript 扩展 cookiecutter 已经更新为默认情况下开发预构建的扩展,并提供了所有必要的工具来快速从头开始创建新的扩展。

关于扩展的更多信息,请参考:

  • https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
  • https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html

如果你正在寻找示例来学习如何制作自己的扩展,请查看 GitHub 上的扩展示例库。这些示例已经更新兼容 JupyterLab 3.0,并提供了开发扩展的手动方法。

扩展示例库地址:

https://github.com/jupyterlab/extension-examples

变更日志

上述内容仅仅概述了 JupyterLab 3.0 的新功能。如果你想要浏览更完整的变更列表,包括错误修复等,请查看详细变更日志。

  • 详细变更日志地址:https://jupyterlab.readthedocs.io/en/stable/getting_started/changelog.html#v3-0
  • JupyterLab 3.0 测试地址:https://mybinder.org/v2/gh/jupyterlab/jupyterlab-demo/3818244?urlpath=lab

原文链接:

https://blog.jupyter.org/jupyterlab-3-0-is-out-4f58385e25bb

每公里配速9分18秒,双足机器人完成5公里慢跑

近日,来自美国俄勒冈州立大学的知名机器人研究团队 Agility Robotics 打造的双足机器人 Cassie ,耗时 53 分钟完成了一段 5 公里慢跑,引发了大家的关注。

配速接近人类,5公里不用充电

研究团队 Agility Robotics 发布了一段视频,视频记录了这段五公里跑的过程和花絮。

%title插图%num

慢跑全程在无视觉输入的情况下,由机器人自主完成

视频里的 Cassie 形似鸵鸟,有着灵巧的关节运动和稳健的步伐。

Agility robotics 的创始人,同时也是俄勒冈州立大学的教授 Jonathan Hurst 表示:Cassie 在完成这五公里的慢跑中没有充电,只有两次停机调整,一次是因为过热,还有一次因为弯道过速,这两次调整花了 6 分半钟的时间。

也就是说,除去 6 分半钟的停机调整, Cassie 花了大约 46:30 的时间完成了五公里慢跑,每公里配速达到了 9m18s/km。

%title插图%num

团队跟随 Cassie 记录跑步过程

据团队介绍,Cassie 是*个“在户外场地使用机器学习控制跑步步态”的双足机器人。

基于*近的研究,团队完成了《Blind Bipedal Stair Traversal via Sim-to-Real Reinforcement Learning》(无视觉双足机器人通过模拟到现实的强化学习,完成楼梯爬行)论文,还入选了今年的 Robotics: Science and Systems(RSS)大会。

%title插图%num

%title插图%num

论文地址:https://arxiv.org/pdf/2105.08328.pdf

通过强化学习,训练双足机器人模拟及适应不同高度的地面变化,而且只需要稍加改变现有的平地强化学习框架,就可以在楼梯、不平的地面等场景中稳健地行走。

值得一提的是,无论是慢跑还是楼梯场景, Cassie 都是在无视觉输入的情境下,完全靠下肢的动能传导进行运动计算。

双足机器人,更像人

大家熟悉的四足机器人网红波士顿动力机器狗,在过去的几年中,已经用跑跳、上下楼梯甚至跳绳和跳舞的表现,成功找到了使用场景。

但相比于常被设计成动物形象的四足机器人,双足机器人在外型上则更像人,功能上也更接近于人。

%title插图%num

双足机器人在受到撞击时,

比四足机器人更难保持平衡和稳定

人类的行走是通过大脑发出指令,驱动各个关节完成动作。而双足机器人,需要模拟人类的胯部和脚掌,从而来支持机器人的行走与稳定。只有充分理解机器人动力学特性,才能驱动机器人完成高效复杂的运动。

Cassie 系列机器人在动力学部分参考了鸟类步态,尤其是鸵鸟。Cassie 的髋部设计了多个自由角度,但膝盖只能单向弯曲,脚踝被设计得柔韧有力,整体非常轻盈,能以比较自然的方式去减缓震动,类似人类走路。

俄勒冈大学的 The Dynamic Robotics Laboratory(动态机器人实验室)的 Agility Robotics 团队在机器人界久负盛名。他们从 2017 年开始研发 Cassie 系列双足机器人,历经多个版本,到目前已经有了不少成果,在近期还收到了 DARPA 的 100 万美金的研究资金。

团队表示,这次五公里无充电慢跑的成功给了他们很大的信心,未来将继续对 Cassie 的技术改造和加强,以进入到物流配送等应用场景中。

Nature计算社会科学特刊:如何对21世纪人类社会进行有意义的度量?

摘要

科学很少会超出科学家所能观察和测量的范围,但有时观察会远超科学理解的范围。21世纪正为人类社会的研究提供了这样一个时刻。今天观察到的人类行为比20世纪末所能想象的要多得多。我们的人际交往、行动和许多日常行为,都有可能被用于科学研究;有时通过有目的的仪器来实现科研目标(例如卫星图像),但更多时候,这些目标实际上是事后才有的想法(例如Twitter数据流)。

在本文中,我们评估了这种大规模仪器的潜力,即通过科学测量及其原理的视角,创造结构化表示和量化人类行为的技术。我们尤其关注的问题是,如何从数据中提取科学性的意义,尽管这些数据往往不是为这些目的而创建的。这些数据提出了概念上、计算上的和伦理上的挑战,需要我们的科学理论复兴,以跟上快速变化的社会现实和捕捉它们的能力。换言之,我们需要新的方法来管理、使用和分析数据。

传感器技术的应用已经在人类活动的许多领域成倍增长,从汽车追踪设备到在线浏览。卫星会定期扫描地球并将其数字化。计算机科学家开发的处理非结构化数据(如文本、图像、音频和视频)技术的发展,使诸如书籍[1]、广播[2]和电视节目[3]等转换成数据成为可能。在21世纪,人们的行为——从流动到信息消费,再到各种各样的人际交往,越来越多地被记录并且有可能被计算处理。过去的通信技术,从邮件、印刷品再到传真,通常只留下很少的耐用、可获得的人工制品;而在过去十年左右的时间里,随着相关的实物被数字化,它们已经成为可以通过计算获得的东西。书籍的数字化就是一个例子,它使我们能够对几百年前的人类表达的大量语料库进行计算分析[4]。

人们常常将这些新数据流的出现和望远镜的发展做类比。正如罗伯特·默顿(Robert Merton,1997年诺贝尔经济学*得主)的名言:“也许社会学还没有为爱因斯坦做好准备,因为它还没有找到它的开普勒……”[5]默顿这句挑衅性的话是指,社会学还没有建立伟大理论的经验基础。对此,邓肯·瓦茨(Duncan Watts,计算社会科学家)在62年后回应道:“……通过将不可测量的变量变得可测量,移动、网络和互联网通信领域的技术革命有可能彻底改变我们对自己和我们与世界如何互动的理解。默顿是对的:社会科学仍然没有找到它的开普勒。亚历山大·蒲柏(Alexander Pope,18世纪英国诗人)三百年前就曾提出,人类理应研究的对象不在天上,而在我们自己,如今我们终于找到了我们的望远镜[6]。”

我们相信,数字资料有改变社会科学的潜力。然而,将工具化社会的数据流比喻为“望远镜”在一些重要方面是具有误导性的。首先,对社会的研究不同于对星星的研究,因为刻画人类行为的模式在不同时间和地点通常是不同的。第二,由这些数据流中建立起来的测量是值得怀疑的,因为这些数据源建立时并没有考虑到科学目标,因此必须对其进行积*审视。现在我们谈谈*点;本文的其余部分将讨论第二个问题。

1. 社会和测量的不稳定逻辑

经验性社会科学主要侧重于发现人类行为中可被概括的模式,而不是普遍模式。社会科学中旨在从人类行为中发现这种普遍模式的部分(如演化心理学)相对于整个领域来说是微不足道的。管理人类社会规则的不稳定性问题由于收集人们数据的社会技术系统而加剧,这些系统正在积*地(在某些情况下有意地)改变社会科学将要研究的社会世界。通过社会科学家所称的自反性和自我实现的预言,人类通过对所获得的知识(部分是通过仪器)采取行动,积*地改变他们所观察到的世界[78]。

自反性是指将社会现实与我们设计用于解释社会现实的理论和量度联系起来的循环。例如,在选举政治分析中,人们早就发现了“从众”和“劣势”效应,以解释民意调查和预测对投票行为的影响。如果候选人被预测为可能的赢家,更多的人可能会决定投票给他们(从众效应);或者相反,更多的人可能动员起来,增加对被预测会输的候选人的支持(劣势效应)[7]。这些效应反映了测量对态度和行为的影响[8,9],以及我们的测量如何扭曲他们本被设计用来监测的现象。反过来,公共卫生、执法、量刑、教育和招聘等领域的算法决策也会放大这些扭曲。[10,11]

自反性还表现为观察者效应的形式,当人们知道自己被观察时就会改变自己的行为[12,13]。数字技术创造了自反性问题的新版本,放大了社会指标中固有的表现方面。当谷歌在2008年启动流感趋势项目(Flu Trends Project)时,目标是使用搜索查询来估计流感症状在人群中的流行程度。然而,在2013年,这个项目大大高估了流感的峰值水平。其中一个原因是有缺陷的假设,即搜索行为是由外部事件(比如流感症状)驱动的。事实上,谷歌的算法也在推动这些模式:通过尝试通过推荐的搜索词来预测用户的意图,谷歌扭曲了用户原本可以看到的信息[14]。换句话说,对观察到的现象的反应改变了现象本身。

模糊处理策略代表了观察者效应的另一个版本:我们现在可以通过故意添加模糊或误导性信息来干扰数据收集,从而中断测量。模糊处理的例子包括编辑轮廓照片以防止面部识别;在浏览网页时使用虚拟专用网(*)来隐藏自己的位置;或者使用群组身份(例如,许多人共用一个用户帐户)来掩盖单个用户行为的细节[15]。这里的自反性循环是因人们意识到行为痕迹被反馈到测量和监视中而产生的,因此该行为的含义是有意被改变的。但在更大的范围上,这类似于受访者对调查人员撒谎。而且,由于了解正在发生的监视以及如何实施模糊处理来解决这一问题所需的技能并不是随机分布在人群中的,因此将数据以此方式改变的个人也不会是随机的。

许多数字测量的不明显性质表明,总体而言,与过去相比,这些新数据源的观察者效应可能不太成问题,例如,进行访谈的人的性别、年龄和种族可能会*大地改变受访者提供的答案[16]。然而,将社会现实与我们用于分析社会现实而设计的量度联系起来的循环已经得到加强——反性现在已经被嵌入用于监测和预测人类行为的工具中。这就好像哈勃望远镜在观测恒星的同时组织了恒星的位置和行为。例如,社交媒体不仅捕捉人类行为,也有可能改变人类社会的重要模式:例如信息流动的速度、媒体制作的范围以及负责界定舆论的行为者。

组织人类社会的原则是有流动性的,因此一个特定测量的意义也会演变。社会科学必须适应新型数据的部分原因是,新兴的社会技术系统正在降低一些用于衡量人类行为的旧科学工具的重要性。国内生产总值(GDP)和地域流动性等关键概念的现有衡量标准仍受制于20世纪数据的优缺点。如果我们只对旧的衡量标准进行评估,只会复制它们的缺点,把20世纪的金本位误认为是客观真理。例如,考虑一下美国国家选举研究所(American National Election Studies)[17]对有关选举的电台消费的一个标准问题(*初源于1978年):“你会说你在电台听了很多次,几次,或者仅仅是一两次演讲,或者关于‘选举’的讨论?”

这种由有限离散单元组成的“媒体消费”结构是广播时代技术的产物。这个问题与今天人们如何访问数字媒体没有多大关系。试图通过问一些问题来捕捉社交媒体的行为是徒劳的,比如“你今天看到了多少条推特?”或者“你的信息流中出现了哪些推特账户?”在定量社会科学早期发展起来的许多衡量行为的方法是:(1)有必要的,考虑到当时的测量限制;(2)立足于明显不同的社会现实。

%title插图%num

图1. 社会科学中的测量。测量是连接科学动机、数据和洞察、应用的桥梁。

图1总结了测量如何适应一般科学过程。我们将在下面讨论,将这些社会技术系统的数据转化为科学测量的核心挑战。这个讨论将包括两个数据流的启发性例子,它们是许多社会科学研究的基础:来自手机的位置数据和Twitter上的社交媒体帖子。我们现在要讨论的关键问题是,当前需要用大量仪器化的人类行为来衡量什么对象,注意方框1中总结的衡量的关键原则。

方框1 度量的核心原则

度量应该遵循重要问题的定义

度量被观察的现象是以确定相关问题为前提的。重要的问题是由研究问题驱动的,这些问题可能由规范性目标、理论辩论或经验难题所驱动。

度量必须是从数据中积*构建的

为研究目的而设计的仪器常常产生科学数据。但是,为了科学研究以外的目的而收集的数据也经常被学者们改变用途。数据本身并没有意义,无法成为某些理论结构的度量,它们必须通过各种方法进行转换,使它们系统地相互关联,并与科学理论相关联。

科学度量在不断发展的循环中遵循上述原则

科学动机指导研究人员设计数据收集协议,使用第三方数据或开发出这两者的某种融合。在原始格式中,数据提供了经过处理成度量的观察结果,这些观察结果可以用来测试预先设想的假设(以演绎的方式)或从探索性分析中得出新的假设(以归纳的、数据驱动的方式)。这些演绎和归纳分析旨在提供见解,然后反馈到科学动机中,为政策干预提供信息,或者更广泛地来说,推动基础和应用研究。

2. 跟踪数据测量什么

使用行为跟踪数据进行测量的目的是,从仪器产生的原始数据中提取意义。所有的科学数据仪器都面临着这个问题,但是当我们使用从为其他目的设计的系统中回收而来的数据时,从原始数据到有意义的度量往往跨度非常大[18]。例如,未经处理的、报告特定纬度和经度的移动电话的移动数据在很大程度上是无趣的,而数据的处理使得我们能够测量接近度、移动和其他与社会相关的概念。

关键挑战是,我们的度量是否准确地捕捉到我们想要检查的构想。它是否与同一对象的其他度量紧密相配?构想和概念之间的潜在偏离是什么(例如,如果测量手机的物理活动,那么遗漏的静态活动(如跑步机)有多重要?)。当我们检查假定不相关的构想时,我们的度量是否反映了预期的关联缺失?总的来说,21世纪的观测数据不是为研究而设计的,在能够利用这些数据回答科学研究问题之前,我们需要把这些观测数据与已知的概念联系起来。

度量的意义部分来自于理论。应用现有知识解释数字信号的理论驱动设计可以克服使用仪器化行为数据的许多问题。相反,缺乏理论化的特殊操作会使研究结果难以解释,并且在不同的研究中呈现出不一致。正如前面提到的[19],正规理论不仅有助于产生假设,而且有助于选择一种合适的方式来用大数据度量构想。

举个例子,让我们考虑使用移动流动性数据来研究新冠肺炎的传播。多个研究使用实时旅行数据来追踪武汉到中国其他省份的人员移动[20,21]。研究人员发现,来自武汉的人口流动对于冠状病毒是否流入一个地区具有强烈的预测性。于是当地疾控人员预测了病毒后续的传播。在这些研究中,有一个被很好地理论化过程,其假设是病毒的传播是由个体的接近所驱动的。而选择的理论框架反之也会表明,这些研究结果可以对其他案例有何程度的普适性。也就是说,我们可能会预想到在美国[22]出现类似的模式,但不会是澳大利亚,因为澳大利亚对游客实施了严格的测试和隔离程序。任何给定的实证研究结果在时间和空间上都必然是局部的;需要理论来将任何度量适当地移动到新的地理或时间背景[23,24]。

随着我们使用大量、复杂的数据源和格式进行更多的研究,为新度量的有效性提供见解的方法变得特别有价值。一个很有前景的方法是研究经典的、已被验证的自我汇报量表时,和度量相关概念的新方法相结合。例如,自我汇报的新闻关注度和曝光度可以与眼球追踪结合使用,以捕捉对在线内容的视觉关注[25]。类似三角测量的测量方法也有助于确认新行为构想的有效性和稳健性[26]。研究人员利用手机数据设计了一种基于接近度的测量方法,用来记录人们接近彼此的时间[27]。这些指标可以用于各种各样有用的目的。它们可以作为关系强度的指标,也可以作为一种追踪病毒传染途径的方法。但是,这里有错误的可能性:例如,两个蓝牙信标显示设备互相接近的人,他们可能中间隔着一堵墙,或者可能只是从同一个插座给手机充电。在这样的情况下,三角测量可以通过包含自我汇报的数据来实现,比如向某人的手机发送信息,询问当时附近还有谁。

对于基于互联网的研究而言,人们对数字平台上用户行为的基本群体特征和潜在机制的了解相对较少。许多基本概念仍然难以衡量,即使是在为研究人员提供便捷数据访问的在线平台上。尽管近年来有数千篇基于Twitter数据的论文,但社交媒体学者仍然发现,识别个人用户的人口统计特征仍然是一个巨大的挑战。此外,研究人员仍然无法可靠地区分人类和非人类(例如,机器人、集体账户或组织),尽管在这方面已经取得了重要进展[28,29]。因此,Twitter的大部分研究都是对账户或推文进行推断;很少有Twitter的研究可以合理地宣称是在对人类的行为进行陈述。对于关注Twitter上人类行为的研究问题,将用户帐户与管理数据或调查结果联系起来的方法有望在Twitter上识别人类及其人口统计属性[30]。

即使人类是特定行为的来源,但将特定行为归因于特定的人也可能会遇到挑战。例如,在广播电视的早期,受众研究遇到了多成员家庭的挑战[31]。这些案例的数据表明,有人同时喜欢儿童漫画和有线新闻,事实上这涉及两个不同的个体。因此,当行为是人(两个人使用同一个Netflix帐户)或设备(在智能手机和桌面上查看Twitter的同一个人)之间的共享时,技术设备可能会产生误导。进一步加剧的问题是,设备-人的不匹配可能随着时间的推移迅速演变。例如,基于桌面浏览数据的研究表明,新闻消费已经系统地改变,而这可能只是新闻消费从桌面浏览器向移动app的逐步转变的产物[32]。人们对这些不同系统(和设备)的使用缺乏稳定性,这可能使这种随时间推移的比较基本上变得不可能。

使用基于其他数据的模型可以促进对特定行为的测量。例如,某个人用的某一个设备可以从其他数据中建模,且该模型的输出和关于设备用户身份的离散假设相比更不敏感。有线新闻的浏览者可能是祖父母,而Xbox用户可能是孙辈。然而,这些模型中包含的数据总是来自过去,而且度量之间的关系本身就是不稳定的。这是归纳法的基本问题,如果没有形而上学的革命,就无法克服这个问题,我们建议不断更新的度量和模型应该代表我们对这个问题的*佳改进。也就是说,我们应该为我们衡量的偏差做好计划,并对度量如何具体反映当前社会现实进行持续评估。例如,衡量通货膨胀的指标需要评估人们消费的一系列商品是如何随时间变化的。这是一个有用的重新校准,尽管它也说明了这种方法的局限性,因为全新产品的出现(例如2000年时没有人购买智能手机)使得跨时间的消费天生无可比拟。

在互联网的推动下,通信技术的发展也导致了行为的分裂,形成了不同的数据仓库。让我们考虑一个研究问题:“异地同步语音中介通信”对于减少社会孤立感是否很重要。在过去半个世纪里,这种行为不断地分裂成不同的体系,从政府授权的垄断企业(例如,美国的马贝尔(Ma Bell))到寡头垄断企业,再到数不清的互联网提供商。此外,这些系统捕捉到的数据存在系统性偏差是很合理的——任何一个与你通过手机交谈的人都可能与你通过Zoom、Skype或WhatsApp交谈的人有系统性的不同[33]。连上述使用的折磨人的语言结构也反映了社会技术的复杂性:不久前,“异地同步语音中介通信”已被简单地描述为“电话通话”。这种技术碎片化的一个重要后果是,依赖于单一数字设备或服务的测量应相当谨慎地进行解释。我们发现的答案可能与我们在类似但不同的技术中通过测量行为得到的答案有所不同。讽刺地是,由于这种复杂性,通过简单的调查问题可能比通过单一平台的记录更好地准确了解一个人通常与谁交谈。

相反,在不同的数据仓库中观察到的看似相似的行为,实际上可能捕捉到截然不同的现象。正如在调查中用来生成联系人列表的各种名称生成器可以识别不同的社交纽带一样[34],Facebook上的朋友与Twitter追随者或LinkedIn联系人并不代表相同的关系。此外,尽管这些概念之间很可能存在着一些强有力的统计联系,但这些关系中没有一个表示口语或科学上使用的“朋友”。进一步说,这些系统会随着时间的推移而改变,它们允许用户做的事情也会发生变化。这反过来说明,我们网络社会行为、关系和结构背后的因果过程在不断变化。因此,我们现在必须意识到度量的系统变化特性,例如时间有效性和系统间有效性。然后,挑战就变成了发展度量,为给定的研究问题提供随时间或跨系统的某种程度的通用性。

另一个深层次的问题是度量的算法混乱[35]。这里的混乱指的是我们无法区分代表典型人类行为的信号和数字平台的规则产生的信号。在不知道系统是如何设计的情况下,我们很容易将社会动机归因于由算法决定驱动的行为。如果Twitter的信息流突然开始将体育内容的优先级提高,在用户对体育的潜在兴趣没有任何变化时也可能会发现谁赢得了奥运会比赛。这样的变化往往很难被发现,这既是因为它们有时是在未经通知的情况下引入的,也是因为它们可能会不均匀地展开,先影响某些用户群体。这种机制也以更微妙的方式发挥作用,例如,算法提示如何增强自然人类倾向。例如,如果Twitter系统性地建议你回关那些已经关注你的人,那就可以促进我们回报社会关系的自然倾向[36]。更普遍地说,互联网公司的目标是操纵人类行为,以增加其平台上的参与度(如Facebook、Twitter和Instagram)或在其产品上的支出(如Amazon和Ebay)。这些基于机器学习的操作是普遍存在的,任何从平台数据发展度量的努力都需要评估算法扭曲度量和下游分析的程度。由于其重要性,这些算法值得进一步研究[11,37]。

尽管对因果推理的深入讨论超出了本文的范围,但我们应该注意到,本文确定的一些度量问题为旨在确定因果关系的研究提出了一个特殊的问题。例如,随着时间的推移,度量缺乏稳定性可能会导致研究人员将焦点结果的变化归因于不相关的外部事件。上文关于谷歌流感趋势的讨论也与此相关。在这种情况下,有一个隐含的假设,即流感与流感相关的谷歌搜索存在因果关系。然而,如果2013年左右,谷歌在流感季节提出流感相关搜索,因为它在其复杂的算法机制中推断这是流感季节,那么对2013年和2008年相同行为的衡量将意味着截然不同的东西。

人类表达和语言的可塑性也对从语言和图像数据中推断态度和观点提出了普遍的挑战[38]。众所周知,Twitter上的情感表达方式很难被计算机解码,因为它们通常会被讥讽、反语和夸张所阻碍[39]。问题的严重程度取决于噪音的结构,同样也取决于什么是重要的,也就是研究问题。

3. 追踪数据测量的对象

人类行为是一个多层次的概念,它通常需要个人层面的度量,以推断行为、态度和属性在集体层面的分布。研究问题应该明确一个特定研究的目标群体是什么。这些人口可以包括各种类型的人,也可以是特定于某个地理区域(城市或国家)、特定社区(兴趣团体或公司)或无数其他亚群体(青年、移民或政治家)。特别地,当涉及整个人口群体时,无论是从逻辑还是财务的角度来说,收集关于每个人的数据是不可行的。在这种情况下,研究人员*好收集关于随机样本的数据,这意味着人口的每个成员都有相同的概率出现在样本中。这种理想从来没有被完全实现,在实际世界中,人们对调查请求的答复率低于10%,不同招聘方式的人员可及率参差不齐,与这种理想甚至更不相关[40]。

对于系统级数据,人们可能会认为每个人都被数据代表了,因为所有用户的操作都在数据集中。然而,在这种情况下,采样针对的是那些收集数据的系统的用户和*活跃的成员[41]。这充其量只是对被调查平台的“便利普查”,而不是对整个人口的普查[42]。如果科学目标是对平台上的人做出陈述,那么这个普查可能是令人信服的。然而,任何跨越这一平台的概括都必须被更加批判性地看待。这是Twitter研究的一个特殊问题,Twitter是*常被引用的新兴数据来源,尽管只有约20%的美国人使用它,并且在大多数其他国家甚至更不流行[43,44,45]。重要的是,社交媒体平台的用户并不反映互联网用户的总体人口特征[41],也不反映兴趣等其他属性[41,44,46]。鉴于*近在促进研究人群在其他领域的代表性方面取得的进展[42,47],必须在社交媒体领域仔细思考这些问题[47]。我们还注意到,当应用于大规模数据时,重新校准数据以做出合理总体水平推断的方法可能尤为强大[48]。

当只研究平台用户群体的一个子集时,泛化问题就会被放大。关键的问题是,样本的性质是否以及如何影响推断。例如,正因此,一项对那些在个人资料中包含姓名和位置的Twitter用户的研究[49]提出了一个问题:这些发现是否适用于没有透露这些细节的Twitter用户?类似地,另一项研究[50]也对政治信息的消费模式进行了调查,调查对象是在个人资料中提供党派标签的少数Facebook用户,但调查结果是否适用于那些不透露自己政治倾向的个人?按照社会科学标准,这些研究的样本量相对较大,但这并不能缓解人们对该样本不能代表使用该平台的人群的担忧[51]。随着时间的推移,使用平台的人有时会发生很大的变化(Facebook曾经是哈佛大学本科生的专属领地),这就加剧了这个问题,在这种情况下,这些人口结构的变化本身也会影响平台上发生的事。

其他在普适性方面的关键问题还包括不同的平台引发系统性的不同行为。例如,同一个人在Facebook和Twitter上的表现往往不同[52]。更一般地说,一些人类行为高度依赖于环境,如果我们只能在工作、家庭或宗教环境中观察同一个人,我们可能会对人性做出完全不同的结论。普适性不仅是关于人口的函数,而且是特定观测环境的函数。根据研究问题的不同,这可能是问题,也可能不是问题。一个被明确定义的问题和人群将有助于确定度量结果与研究意图的吻合程度。

*后,我们注意到关键度量问题,即关于抽样的系统偏差是什么。一般来说,我们的数据收集系统偏向于远离少数群体,尤其是边缘人群;此外,我们关于人口的理论问题通常集中在分布的中间。代表性对于理解人类而言是一个*其重要的问题,无论是现在还是过去。有研究分析谷歌图书(Google Books,人类知识*大的数字化集合)的文本,希望得出关于这些文本在几个世纪内的语言变化如何与民族情感的转变相一致的结论[4]。这一语料库作为语言使用的一种表现形式受到了影响,因为它的组成随着时间的推移发生了系统性的变化(例如,在二十世纪,科学文本的代表性要高得多)[1],并且因为即使是一套精心策划的书也会反映出不具代表性的精英的现实。即使是有史以来规模*大的图书馆也无法发现那些虽然在出版文献中没有代表,但仍然有能力采取行动并改变历史进程的人。

这些代表性问题是二十世纪社会科学方法中的一个主要关切。通过邮件联系受访者系统地排除了无家可归的人群,电话调查排除了没有电话的人群,亲自进行的调查取决于人们对与陌生人这种互动方式的舒适度和信任度。

观察行为流可能受到类似偏差的影响。首先,收集数据的仪器通常是个人拥有的消费品(例如,移动电话或计算机),因此成本是一个障碍。其次,这些工具通常是由针对有钱人的企业商业模式驱动的。第三,当人们选择不使用隐私服务时,那些更关心或更了解隐私问题的人在跟踪行为系统中的代表性可能会降低。

然而,这些数据流具有一些关键的补偿特性。传感器技术可能会填补重要的数据空白,让那些原本会被从地图上抹去的人变得可见。例如,在没有家庭收入和消费调查的情况下,卫星图像被用于建立全球南方(Global South)的财富和贫困指标[53]。现代技术的普遍性意味着,在许多情况下,代表是优于传统的数据收集机制的——拥有一部手机比拥有一个家要便宜。这与杜波依斯(W. E. B. Du Bois )在 19 世纪末和 20 世纪初用于研究非裔美国人个人的行政数据有相似之处[54]。一个实行种族等级制度的行政国家的数据肯定不是中立的,但在提供社会中*危险地位的人的可见度方面仍然具有关键价值。

此外,大样本量允许我们观察数据子集的行为,例如,少数群体(一般意义上的)和统计上不常见但会引发后果的事件(如仇恨言论或错误信息)[49,55,56],在这些案例中,样本量和我们放大较小人群和罕见数据点的能力比样本的代表性更重要[57]。正如帕累托(Pareto)很久以前所观察到的那样,人类的许多行为都集中在人口的微小部分[58];然而,20世纪的方法通常不适合研究这种社会现实。也许21世纪的社会理论将能够利用微观层面的行为数据来理解相互依存的结构如何产生某些宏观层面的模式[59]。

4. 测量的获取与道德

与哈勃望远镜的数据相比,来自社会技术系统的新兴数据流提出了两个额外的挑战。首先,哈勃望远镜是由科学机构控制的,其目标想必是回答科学问题。Twitter等平台的制度目标显然不是回答科学问题。因此,*个问题是,什么是可以被度量的?第二,人类作为研究的参与者提出了道德问题,而遥远的星系显然没有。所以接下来的问题是,应该度量什么?我们将依次解决这两个问题。

根据生成数据的系统的不同,可以测量的内容会有很大不同。设计一个小规模的、依赖于同意参与者的数据收集系统是有可能的[60];然而,访问数百万人的数据通常需要与平台合作。基于互联网的通信数据有着广泛的可用性,其访问规则在不同的数据持有者和时间下有很大的差异。在限制*少的一端,像Reddit和Wikipedia这样的平台允许获取几乎所有的那些终端用户以机器可读的格式可查看的内容。相比之下,像Facebook和Twitter等公司提供的限制访问机制要多得多,它们受到时间、数据量的限制,而且并非所有公开可见的数据都可以通过编程方式访问。值得注意的是,目前没有一个主要的平台提供关于人们关注的个人层面的数据,这在目前基于互联网的度量中是一个非常大的缺口[61]。此外,没有一个平台能够提供广泛的随机对照试验(以AB试验的形式)的信息访问,这在原则上可以推断其算法对个体的影响[62]。一般来说,任何控制研究人员感兴趣的数据的私人机构,在没有相反规定的情况下,都可以根据自己的选择决定数据访问的条款。像Twitter和Facebook这种平台的行为是公众关心的科学问题的焦点(思考一下:一个平台是否放大了错误信息的传播?平台对仇恨言论的应对举措是什么?),这使这种控制成为一个严重的问题[63]。学术界在这些领域的一项职责就是向公众提供关于这些重要问题的信息。关于什么可以被度量的一个推论必须是:如果被质疑的权力控制着对用来构建“真相”的数据的访问,那么有可能对权力说真话吗?如果没有,是否有可能信任任何允许从给定系统中提取的度量?

新兴的数据来源也带来了新的道德挑战。我们关注的是那些与度量相关的问题,尤其是那些能够且应该被度量的问题。关于跟踪数据伦理以及数据访问的替代模型的更广泛讨论,可在他处获得[18,64,65,66];在此,我们简要介绍五个特别紧迫的问题。

首先,尽管知情同意是对人类参与者研究的基础,但第三方获得的匿名数据通常不被视为“人类参与者数据”,因此不受机构审查委员会的审视。研究人员在收集数据时考虑数据收集场景时的道德义务是什么?在一个*近的例子中,来自*右翼社交网络Parler的超过70Gb的数据在2021年1月初被公开发布,包括GPS衍生的位置数据[67]。研究人员是否能够道德地分析这一数据集是一个引发持续争论的话题,特别是考虑到该网站被用于策划2021年1月6日美国国会大厦暴动。更普遍地说,人们可能不知道不同的系统是如何跟踪他们的,无论是通过手机的移动数据[68],或者浏览数据。那么,当第三方追踪的目标*多只是名义上意识到这一事实时,使用第三方追踪数据的道德准则是什么呢?

第二,行为数据集中的详细程度意味着,对于重新识别工作来说,可靠的匿名化通常实际上是困难的或不可能的[69]。需要注意的是,取消标识的匿名数据可以是无法重新标识的类型,或是可以重新标识的类型。围绕“差分隐私”出现了一些允许向数据集添加噪声的方法,从而一定程度上保证了数据的匿名性,使其能够可靠地进行重新识别[70,71]。然而这里存在一个折衷,因为增强隐私的噪声添加会降低数据的效用。这是Social Science One project 中采用的方法,该项目提供了对Facebook数据[72](方框2)的分析访问。被授予访问权限的团队面临的难题之一是,结果数据是否保留了回答他们问题的价值(注:一些作者参与了Social Science One和Facebook 2020 Election Research Project)。

第三,对于公开可见的行为,比如推特,什么样的隐私期望是合理的?研究人员必须要履行什么样的义务来掩盖这些行为?例如,研究人员什么时候应该避免提及(在出版物或演示中)诸如用户名和完整的社交媒体消息之类的信息,因为可能引起负面关注或骚扰?一些人认为,自动匿名公开数据可能也不是正确的方法,相反,应该咨询内容创作者他们的偏好[73]。

第四,出于两个原因,对个人自主原则的依赖在本质上是有限的。在一个信息和洞察网络化的世界里,一个人透露给其他人的信息经常会溢出。网络媒体的功能,顾名思义,就是促进人际间的可视性[74]。例如,共享电子邮件数据的个人必须提供来自其他个人的信息。剑桥分析公司(Cambridge Analytica)的丑闻表明了这种网络信息披露的危险性,在这件事中,个人使用了Facebook应用,而Facebook应用又提供了访问这些用户朋友行为数据的途径。然而,信息溢出的风险是一个更为普遍的原则,这在数字追踪数据中并不新鲜:个人披露几乎总存在潜在的溢出。例如,一个人的基因数据有可能提供关于其近亲的线索[75];并且几乎所有关于个人的数据都提供了关于他人的信息。一个人对其政治偏好的回应可以让人了解到其他家庭成员的偏好,关于一个人吸毒的信息可以让人了解对此人朋友的潜在吸毒情况。

同样还有个体内的信息推断,这其中所提供的信息(可能是经过同意的)能够做出个人意料之外的推断[76]。实际中的道德结果不可能是禁止所有可能存在信息披露外溢或推断的研究;然而,这确实意味着数据共享和数据可见性通常需要受到重要的限制。数据安全的重要性也需要被重视。

在我们关于“度量对象”讨论的基础上,在试图将基于跟踪数据的研究结果推广到研究平台以外的人群以及参与者的实际生活时,必须格外谨慎[41]。重要的是要找到办法让数字代表性不足的参与者参与进来,特别是当这类研究被用来为广泛的社会或公司政策提供决策依据时。

相反,与传统的二十世纪方法相比,当数字形式的度量能够更好地代表边缘群体时,我们的道德义务就应该是使用它们,正如上文提到的卫星数据的例子所强调的那样。社会面临的选择不是数字技术是否将被用来度量人类行为,而是公司或国家监控之外的任何人何时、如何以及是否能够获得这些数据。理想的情况是,大规模的数字数据源将流入各项度量,为详细政策和有针对性的干预措施提供信息,而不仅仅是一刀切的举措,因为这些举措往往对少数群体效果不佳。

*后,该领域有责任对实践中有问题的度量步骤所产生的决策进行批评。先前发表的一项研究表明,许多医院使用的一种算法存在种族偏见,而这种偏见是由度量误差造成的,这是一个很好的例子,既说明了自动化决策中有缺陷的度量所造成的危险,也说明了良好的科学有可能帮助纠正这些问题[11]。

方框2 数据获取和道德问题

平台控制数据访问的可能影响

  • 研究工具可能会因平台对数据访问的更改而在未经告知的情况下过时。
  • 私人数据持有者可能要求外部研究人员与其密切合作,作为获取数据的一个条件。此外,这种合作成果在出版前可以接受私人数据持有者的审查。在这种平台的直接控制下进行的研究不能成为相关平台的关键见解的来源。
  • 如果研究人员的工作超出了数据持有者的兴趣范围,或者不愿意直接与数据持有者合作,那么他们可能会退而求其次,使用违反平台服务条款的方法。
  • 在保持研究人员独立性的同时,促进获取平台数据的初代范例包括:

Social Science One:这项工作涉及到外部批准和对应用差分隐私的总体Facebook数据的研究资助[72]。

The Facebook 2020 Election Research Project:该项目涉及外部研究人员与Facebook的合作,其中Facebook研究人员使用预先备案的分析计划和外部专家界定的度量进行数据分析,外部专家还监督分析的执行,并享有完全的结果解释权[77]。

道德问题

  • 研究人员在考虑数据收集场景时(例如通过泄密或黑客攻击)的道德义务是什么?
  • 研究团体如何解决数据匿名化技术(例如通过添加噪声)带来的权衡问题?
  • 对于公开可见的行为(如社交媒体帖子),何种隐私期望是合理的?
  • 我们如何管理信息溢出,即从同意的个人收集的数据泄露了关于他人的信息,且他人并不知情或不同意?
  • 我们如何确保边缘群体在研究中得到充分和准确的代表?

5. 展望

方框3总结了本文的基本论点。全球社会的大量工具拥有巨大潜力来改变我们对社会世界的理解。然而,人类行为工具化的革命要求了人类行为度量的变革。任何新的度量制度都需要与新旧社会理论的可能性相匹配,在这些高度工具化的社会技术系统中应对度量人类的不稳定性的本质,并开发一种新的人类参与者伦理研究模式,平衡个人权利和集体利益。

方框3 度量的关键问题

什么是重要的?

我们可以度量很多概念。我们必须明确引领我们选择研究问题的思想、价值观、优先次序和原则,以及我们如何构建一个值得研究的课题。

度量的时间、空间、结构和文化完整性是什么?

表面相似的结构可以用非常不同的方式来度量,同样的度量标准可以随着度量它的系统的变化而变化,或者在不同的地理、人口和文化群体中不一致。

谁被度量?

选择使用像社交媒体平台这样的各种系统的人并不是随机的,他们使用这些系统的哪些部分以及使用的积*程度也不是随机的。因此,构成许多研究基础的此类系统用户的跟踪数据可能无法推广到更广泛的人群,甚至无法推广到其他看似相似的平台。

谁有权限度量?

科学受到数据访问权限的限制。这些数据受到创造它们的机构和技术的功能、目的和协议的限制。这些限制永远不会得到完全解决,因此应该成为该领域的核心关切。

度量什么是合乎道德的?

数字数据触及的人比以往任何时候都多,不仅可以收集到在研究中的人的信息,还可以收集到在这些人周围但本身并不在研究中的人的信息。信息外溢问题是网络世界中所有研究的固有问题,它破坏了当前的研究伦理框架中个人自主的基本原则,并大大增加了研究人员维护数据安全的责任。关于同意、隐私和保密的标准和实践必须考虑到这些现实。

论文题目:

Meaningful measures of human society in the twenty-first century

论文地址:

https://www.nature.com/articles/s41586-021-03660-7

android添加购物车动画 参考

android添加购物车动画实现

%title插图%num

商品列表Adapter

  1. import android.content.Context;
  2. import android.graphics.drawable.Drawable;
  3. import android.view.LayoutInflater;
  4. import android.view.View;
  5. import android.view.ViewGroup;
  6. import android.widget.BaseAdapter;
  7. import android.widget.Button;
  8. import android.widget.ImageView;
  9. import android.widget.LinearLayout;
  10. /**
  11. * 商品列表Adapter
  12. * @author antkingwei
  13. *
  14. */
  15. public class GoodAdapter extends BaseAdapter{
  16. private Context mContext;
  17. private LayoutInflater layoutInflater;
  18. private HolderClickListener mHolderClickListener;
  19. final class ViewHolder {
  20. ImageView imgview;
  21. Button button;
  22. }
  23. public GoodAdapter(Context context){
  24. this.mContext = context;
  25. layoutInflater = LayoutInflater.from(mContext);
  26. }
  27. @Override
  28. public int getCount() {
  29. // TODO Auto-generated method stub
  30. return 16;
  31. }
  32. @Override
  33. public Object getItem(int position) {
  34. // TODO Auto-generated method stub
  35. return null;
  36. }
  37. @Override
  38. public long getItemId(int position) {
  39. // TODO Auto-generated method stub
  40. return position;
  41. }
  42. @Override
  43. public View getView(int position, View convertView, ViewGroup parent) {
  44. // TODO Auto-generated method stub
  45. final int selectedId = position;
  46. final ViewHolder viewHolder;
  47. if(convertView ==null){
  48. viewHolder = new ViewHolder();
  49. convertView = layoutInflater.inflate(R.layout.adapter_listview, null);
  50. viewHolder.imgview = (ImageView)convertView.findViewById(R.id.item_img);
  51. viewHolder.button = (Button)convertView.findViewById(R.id.item_button);
  52. convertView.setTag(viewHolder);
  53. }else{
  54. viewHolder =(ViewHolder)convertView.getTag();
  55. }
  56. viewHolder.button.setOnClickListener(new View.OnClickListener() {
  57. @Override
  58. public void onClick(View v) {
  59. // TODO Auto-generated method stub
  60. if(mHolderClickListener!=null){
  61. int[] start_location = new int[2];
  62. viewHolder.imgview.getLocationInWindow(start_location);//获取点击商品图片的位置
  63. Drawable drawable = viewHolder.imgview.getDrawable();//复制一个新的商品图标
  64. mHolderClickListener.onHolderClick(drawable,start_location);
  65. }
  66. }
  67. });
  68. return convertView;
  69. }
  70. public void SetOnSetHolderClickListener(HolderClickListener holderClickListener){
  71. this.mHolderClickListener = holderClickListener;
  72. }
  73. public interface HolderClickListener{
  74. public void onHolderClick(Drawable drawable,int[] start_location);
  75. }
  76. }

活动类

  1. package com.example.addshopcart;
  2. import com.example.addshopcart.GoodAdapter.HolderClickListener;
  3. import android.os.Bundle;
  4. import android.os.Handler;
  5. import android.os.Message;
  6. import android.app.Activity;
  7. import android.content.Context;
  8. import android.graphics.drawable.Drawable;
  9. import android.view.Menu;
  10. import android.view.View;
  11. import android.view.ViewGroup;
  12. import android.view.animation.Animation;
  13. import android.view.animation.Animation.AnimationListener;
  14. import android.view.animation.AnimationSet;
  15. import android.view.animation.RotateAnimation;
  16. import android.view.animation.ScaleAnimation;
  17. import android.view.animation.TranslateAnimation;
  18. import android.widget.Button;
  19. import android.widget.FrameLayout;
  20. import android.widget.ImageView;
  21. import android.widget.ListView;
  22. /**
  23. *
  24. * @author antkingwei
  25. *
  26. */
  27. public class MainActivity extends Activity {
  28. private ListView listView;
  29. private Button cart_btn;
  30. private GoodAdapter goodAdapter;
  31. //动画时间
  32. private int AnimationDuration = 1000;
  33. //正在执行的动画数量
  34. private int number = 0;
  35. //是否完成清理
  36. private boolean isClean = false;
  37. private FrameLayout animation_viewGroup;
  38. private Handler myHandler = new Handler(){
  39. public void handleMessage(Message msg){
  40. switch(msg.what){
  41. case 0:
  42. //用来清除动画后留下的垃圾
  43. try{
  44. animation_viewGroup.removeAllViews();
  45. }catch(Exception e){
  46. }
  47. isClean = false;
  48. break;
  49. default:
  50. break;
  51. }
  52. }
  53. };
  54. @Override
  55. protected void onCreate(Bundle savedInstanceState) {
  56. super.onCreate(savedInstanceState);
  57. setContentView(R.layout.activity_main);
  58. listView = (ListView)this.findViewById(R.id.listview);
  59. cart_btn = (Button)this.findViewById(R.id.button);
  60. animation_viewGroup = createAnimLayout();
  61. goodAdapter = new GoodAdapter(this);
  62. goodAdapter.SetOnSetHolderClickListener(new HolderClickListener(){
  63. @Override
  64. public void onHolderClick(Drawable drawable,int[] start_location) {
  65. // TODO Auto-generated method stub
  66. doAnim(drawable,start_location);
  67. }
  68. });
  69. listView.setAdapter(goodAdapter);
  70. }
  71. private void doAnim(Drawable drawable,int[] start_location){
  72. if(!isClean){
  73. setAnim(drawable,start_location);
  74. }else{
  75. try{
  76. animation_viewGroup.removeAllViews();
  77. isClean = false;
  78. setAnim(drawable,start_location);
  79. }catch(Exception e){
  80. e.printStackTrace();
  81. }
  82. finally{
  83. isClean = true;
  84. }
  85. }
  86. }
  87. /**
  88. * @Description: 创建动画层
  89. * @param
  90. * @return void
  91. * @throws
  92. */
  93. private FrameLayout createAnimLayout(){
  94. ViewGroup rootView = (ViewGroup)this.getWindow().getDecorView();
  95. FrameLayout animLayout = new FrameLayout(this);
  96. FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT);
  97. animLayout.setLayoutParams(lp);
  98. animLayout.setBackgroundResource(android.R.color.transparent);
  99. rootView.addView(animLayout);
  100. return animLayout;
  101. }
  102. /**
  103. * @deprecated 将要执行动画的view 添加到动画层
  104. * @param vg
  105. * 动画运行的层 这里是frameLayout
  106. * @param view
  107. * 要运行动画的View
  108. * @param location
  109. * 动画的起始位置
  110. * @return
  111. */
  112. private View addViewToAnimLayout(ViewGroup vg,View view,int[] location){
  113. int x = location[0];
  114. int y = location[1];
  115. vg.addView(view);
  116. FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
  117. dip2px(this,90),dip2px(this,90));
  118. lp.leftMargin = x;
  119. lp.topMargin = y;
  120. view.setPadding(5, 5, 5, 5);
  121. view.setLayoutParams(lp);
  122. return view;
  123. }
  124. /**
  125. * dip,dp转化成px 用来处理不同分辨路的屏幕
  126. * @param context
  127. * @param dpValue
  128. * @return
  129. */
  130. private int dip2px(Context context,float dpValue){
  131. float scale = context.getResources().getDisplayMetrics().density;
  132. return (int)(dpValue*scale +0.5f);
  133. }
  134. /**
  135. * 动画效果设置
  136. * @param drawable
  137. * 将要加入购物车的商品
  138. * @param start_location
  139. * 起始位置
  140. */
  141. private void setAnim(Drawable drawable,int[] start_location){
  142. Animation mScaleAnimation = new ScaleAnimation(1.5f,0.0f,1.5f,0.0f,Animation.RELATIVE_TO_SELF,0.1f,Animation.RELATIVE_TO_SELF,0.1f);
  143. mScaleAnimation.setDuration(AnimationDuration);
  144. mScaleAnimation.setFillAfter(true);
  145. final ImageView iview = new ImageView(this);
  146. iview.setImageDrawable(drawable);
  147. final View view = addViewToAnimLayout(animation_viewGroup,iview,start_location);
  148. view.setAlpha(0.6f);
  149. int[] end_location = new int[2];
  150. cart_btn.getLocationInWindow(end_location);
  151. int endX = end_location[0];
  152. int endY = end_location[1]-start_location[1];
  153. Animation mTranslateAnimation = new TranslateAnimation(0,endX,0,endY);
  154. Animation mRotateAnimation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
  155. mRotateAnimation.setDuration(AnimationDuration);
  156. mTranslateAnimation.setDuration(AnimationDuration);
  157. AnimationSet mAnimationSet = new AnimationSet(true);
  158. mAnimationSet.setFillAfter(true);
  159. mAnimationSet.addAnimation(mRotateAnimation);
  160. mAnimationSet.addAnimation(mScaleAnimation);
  161. mAnimationSet.addAnimation(mTranslateAnimation);
  162. mAnimationSet.setAnimationListener(new AnimationListener(){
  163. @Override
  164. public void onAnimationStart(Animation animation) {
  165. // TODO Auto-generated method stub
  166. number++;
  167. }
  168. @Override
  169. public void onAnimationEnd(Animation animation) {
  170. // TODO Auto-generated method stub
  171. number–;
  172. if(number==0){
  173. isClean = true;
  174. myHandler.sendEmptyMessage(0);
  175. }
  176. }
  177. @Override
  178. public void onAnimationRepeat(Animation animation) {
  179. // TODO Auto-generated method stub
  180. }
  181. });
  182. view.startAnimation(mAnimationSet);
  183. }
  184. /**
  185. * 内存过低时及时处理动画产生的未处理冗余
  186. */
  187. @Override
  188. public void onLowMemory() {
  189. // TODO Auto-generated method stub
  190. isClean = true;
  191. try{
  192. animation_viewGroup.removeAllViews();
  193. }catch(Exception e){
  194. e.printStackTrace();
  195. }
  196. isClean = false;
  197. super.onLowMemory();
  198. }
  199. @Override
  200. public boolean onCreateOptionsMenu(Menu menu) {
  201. // Inflate the menu; this adds items to the action bar if it is present.
  202. getMenuInflater().inflate(R.menu.main, menu);
  203. return true;
  204. }
  205. }

activity_main.xml

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2. xmlns:tools=“http://schemas.android.com/tools”
  3. android:layout_width=“match_parent”
  4. android:layout_height=“match_parent”
  5. android:paddingBottom=“@dimen/activity_vertical_margin”
  6. android:paddingLeft=“@dimen/activity_horizontal_margin”
  7. android:paddingRight=“@dimen/activity_horizontal_margin”
  8. android:paddingTop=“@dimen/activity_vertical_margin”
  9. tools:context=“.MainActivity” >
  10. <ListView
  11. android:layout_width=“match_parent”
  12. android:layout_height=“match_parent”
  13. android:id=“@+id/listview”
  14. ></ListView>
  15. <Button
  16. android:layout_alignBottom=“@+id/listview”
  17. android:layout_centerHorizontal=“true”
  18. android:layout_width=“wrap_content”
  19. android:layout_height=“wrap_content”
  20. android:id=“@+id/button”
  21. android:text=“购物车”
  22. />
  23. </RelativeLayout>

AdapterItem布局

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:layout_width=“match_parent”
  4. android:layout_height=“match_parent”
  5. >
  6. <ImageView
  7. android:id=“@+id/item_img”
  8. android:layout_width=“wrap_content”
  9. android:layout_height=“wrap_content”
  10. android:src=“@drawable/ic_launcher”
  11. android:layout_alignParentLeft=“true”
  12. />
  13. <Button
  14. android:id=“@+id/item_button”
  15. android:layout_width=“wrap_content”
  16. android:layout_height=“wrap_content”
  17. android:layout_alignParentRight=“true”
  18. android:text=“添加”
  19. >
  20. </Button>
  21. </RelativeLayout>

Android实现购物车页面及购物车效果

ShoppingCart
Android实现购物车页面及购物车效果(点击动画)

效果图如下:

思路:
(1)思考每个条目中的数字的更新原理。
(2)购物车的动画效果。
(3)购物清单怎么显示(这个我暂时没有写,如果需要的话,可以在我的简书下给我留言)。

1.因为进入页面,所有的商品个数都显示为零,所以我用 ArrayList

//下面把data都添加0,为了刚开始显示时,显示的是0
for (int i = 0; i < list.size(); i++) {
HashMap<String, Object> myhashmap = new HashMap<String, Object>();
myhashmap.put(“number”, “” + 0);
data.add(myhashmap);
}

然后把data传入Adapter:

adapter = new MyAdapter(data);
1
当我们对商品进行增减时,我们可以通过hashmap来更改,如下是增加商品的部分代码:

b = Integer.parseInt((String) data.get(position).get(
“number”));
data.get(position).put(“number”, “” + (b + 1));

2.购物车动画效果:
首先获取点击时的XY坐标,并且设置动画图片:

// ball是个imageview
startLocation = new int[2];// 一个整型数组,用来存储按钮的在屏幕的X、Y坐标
view.getLocationInWindow(startLocation);// 这是获取购买按钮的在屏幕的X、Y坐标(这也是动画开始的坐标)
ball = new ImageView(MainActivity.this);
ball.setImageResource(R.mipmap.sign);// 设置动画的图片我的是一个小球(R.mipmap.sign)

然后是开始执行动画:

“`
private void setAnim(final View v, int[] startLocation) {
anim_mask_layout = null;
anim_mask_layout = createAnimLayout(); //创建动画层
anim_mask_layout.addView(v);//把动画小球添加到动画层
final View view = addViewToAnimLayout(anim_mask_layout, v,
startLocation);
int[] endLocation = new int[2];// 存储动画结束位置的X、Y坐标
re_zhongcai_tanchu.getLocationInWindow(endLocation);// re_zhongcai_tanchu是那个抛物线*后掉落的控件

// 计算位移
int endX = 0 – startLocation[0] + 40;// 动画位移的X坐标
int endY = endLocation[1] – startLocation[1];// 动画位移的y坐标
TranslateAnimation translateAnimationX = new TranslateAnimation(0,
endX, 0, 0);
translateAnimationX.setInterpolator(new LinearInterpolator());
translateAnimationX.setRepeatCount(0);// 动画重复执行的次数
translateAnimationX.setFillAfter(true);

TranslateAnimation translateAnimationY = new TranslateAnimation(0, 0,
0, endY);
translateAnimationY.setInterpolator(new AccelerateInterpolator());
translateAnimationY.setRepeatCount(0);// 动画重复执行的次数
translateAnimationX.setFillAfter(true);

final AnimationSet set = new AnimationSet(false);
set.setFillAfter(false);
set.addAnimation(translateAnimationY);
set.addAnimation(translateAnimationX);
set.setDuration(800);// 动画的执行时间
view.startAnimation(set);
// 动画监听事件
set.setAnimationListener(new Animation.AnimationListener() {
// 动画的开始
@Override
public void onAnimationStart(Animation animation) {
v.setVisibility(View.VISIBLE);
// Log.e(“动画”,”asdasdasdasd”);
}

@Override
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub
}

// 动画的结束
@Override
public void onAnimationEnd(Animation animation) {
v.setVisibility(View.GONE);
set.cancel();
animation.cancel();
}
});

}

需要注意的是,当动画结束必须关闭动画:

v.setVisibility(View.GONE);
set.cancel();
animation.cancel();

Android 把商品添加到购物车的动画效果(贝塞尔曲线)

把商品添加到购物车的动画效果(贝塞尔曲线)

 

如图:

这里写图片描述

参考:

Android补间动画,属性动画实现购物车添加动画

思路:

  1. 确定动画的起终点
  2. 在起终点之间使用二次贝塞尔曲线填充起终点之间的点的轨迹
  3. 设置属性动画,ValueAnimator插值器,获取中间点的坐标
  4. 将执行动画的控件的x、y坐标设为上面得到的中间点坐标
  5. 开启属性动画
  6. 当动画结束时的操作

难点:

PathMeasure的使用
– getLength()
– boolean getPosTan(float distance, float[] pos, float[] tan) 的理解

涉及到的知识点:

  • 如何获取控件在屏幕中的*对坐标
//得到父布局的起始点坐标(用于辅助计算动画开始/结束时的点的坐标)
   int[] parentLocation = new int[2];
   rl.getLocationInWindow(parentLocation);
  • 如何使用贝塞尔曲线以及属性动画插值器ValueAnimator
//        四、计算中间动画的插值坐标(贝塞尔曲线)(其实就是用贝塞尔曲线来完成起终点的过程)
        //开始绘制贝塞尔曲线
        Path path = new Path();
        //移动到起始点(贝塞尔曲线的起点)
        path.moveTo(startX, startY);
        //使用二次萨贝尔曲线:注意*个起始坐标越大,贝塞尔曲线的横向距离就会越大,一般按照下面的式子取即可
        path.quadTo((startX + toX) / 2, startY, toX, toY);
        //mPathMeasure用来计算贝塞尔曲线的曲线长度和贝塞尔曲线中间插值的坐标,
        // 如果是true,path会形成一个闭环
        mPathMeasure = new PathMeasure(path, false);

        //★★★属性动画实现(从0到贝塞尔曲线的长度之间进行插值计算,获取中间过程的距离值)
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(1000);
        // 匀速线性插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 当插值计算进行时,获取中间的每个值,
                // 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
                float value = (Float) animation.getAnimatedValue();
                // ★★★★★获取当前点坐标封装到mCurrentPosition
                // boolean getPosTan(float distance, float[] pos, float[] tan) :
                // 传入一个距离distance(0<=distance<=getLength()),然后会计算当前距
                // 离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。
                mPathMeasure.getPosTan(value, mCurrentPosition, null);//mCurrentPosition此时就是中间距离点的坐标值
                // 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
                goods.setTranslationX(mCurrentPosition[0]);
                goods.setTranslationY(mCurrentPosition[1]);
            }
        });
//      五、 开始执行动画
        valueAnimator.start();

所有代码:

package cn.c.com.beziercurveanimater;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private ImageView cart;
    private ArrayList<Bitmap> bitmapList = new ArrayList<>();
    private RelativeLayout rl;
    private PathMeasure mPathMeasure;
    /**
     * 贝塞尔曲线中间过程的点的坐标
     */
    private float[] mCurrentPosition = new float[2];
    private TextView count;
    private int i = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initImg();
        MyAdapter myAdapter = new MyAdapter(bitmapList);
        mRecyclerView.setAdapter(myAdapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
    }

    private void initImg() {
        bitmapList.add(BitmapFactory.decodeResource(getResources(), R.drawable.coin));
        bitmapList.add(BitmapFactory.decodeResource(getResources(), R.drawable.coin1));
        bitmapList.add(BitmapFactory.decodeResource(getResources(), R.drawable.coin91));
    }

    private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        cart = (ImageView) findViewById(R.id.cart);
        rl = (RelativeLayout) findViewById(R.id.rl);
        count = (TextView) findViewById(R.id.count);
    }

    class MyAdapter extends RecyclerView.Adapter<MyVH> {
        private ArrayList<Bitmap> bitmapList;

        public MyAdapter(ArrayList<Bitmap> bitmapList) {
            this.bitmapList = bitmapList;
        }

        @Override
        public MyVH onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
            View itemView = inflater.inflate(R.layout.item, parent, false);
            MyVH myVH = new MyVH(itemView);
            return myVH;
        }

        @Override
        public void onBindViewHolder(final MyVH holder, final int position) {
            holder.iv.setImageBitmap(bitmapList.get(position));
            holder.buy.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    addCart(holder.iv);
                }
            });
        }

        @Override
        public int getItemCount() {
            return bitmapList.size();
        }
    }

    /**
     * ★★★★★把商品添加到购物车的动画效果★★★★★
     * @param iv
     */
    private void addCart( ImageView iv) {
//      一、创造出执行动画的主题---imageview
        //代码new一个imageview,图片资源是上面的imageview的图片
        // (这个图片就是执行动画的图片,从开始位置出发,经过一个抛物线(贝塞尔曲线),移动到购物车里)
        final ImageView goods = new ImageView(MainActivity.this);
        goods.setImageDrawable(iv.getDrawable());
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
        rl.addView(goods, params);

//        二、计算动画开始/结束点的坐标的准备工作
        //得到父布局的起始点坐标(用于辅助计算动画开始/结束时的点的坐标)
        int[] parentLocation = new int[2];
        rl.getLocationInWindow(parentLocation);

        //得到商品图片的坐标(用于计算动画开始的坐标)
        int startLoc[] = new int[2];
        iv.getLocationInWindow(startLoc);

        //得到购物车图片的坐标(用于计算动画结束后的坐标)
        int endLoc[] = new int[2];
        cart.getLocationInWindow(endLoc);


//        三、正式开始计算动画开始/结束的坐标
        //开始掉落的商品的起始点:商品起始点-父布局起始点+该商品图片的一半
        float startX = startLoc[0] - parentLocation[0] + iv.getWidth() / 2;
        float startY = startLoc[1] - parentLocation[1] + iv.getHeight() / 2;

        //商品掉落后的终点坐标:购物车起始点-父布局起始点+购物车图片的1/5
        float toX = endLoc[0] - parentLocation[0] + cart.getWidth() / 5;
        float toY = endLoc[1] - parentLocation[1];

//        四、计算中间动画的插值坐标(贝塞尔曲线)(其实就是用贝塞尔曲线来完成起终点的过程)
        //开始绘制贝塞尔曲线
        Path path = new Path();
        //移动到起始点(贝塞尔曲线的起点)
        path.moveTo(startX, startY);
        //使用二次萨贝尔曲线:注意*个起始坐标越大,贝塞尔曲线的横向距离就会越大,一般按照下面的式子取即可
        path.quadTo((startX + toX) / 2, startY, toX, toY);
        //mPathMeasure用来计算贝塞尔曲线的曲线长度和贝塞尔曲线中间插值的坐标,
        // 如果是true,path会形成一个闭环
        mPathMeasure = new PathMeasure(path, false);

        //★★★属性动画实现(从0到贝塞尔曲线的长度之间进行插值计算,获取中间过程的距离值)
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(1000);
        // 匀速线性插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 当插值计算进行时,获取中间的每个值,
                // 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
                float value = (Float) animation.getAnimatedValue();
                // ★★★★★获取当前点坐标封装到mCurrentPosition
                // boolean getPosTan(float distance, float[] pos, float[] tan) :
                // 传入一个距离distance(0<=distance<=getLength()),然后会计算当前距
                // 离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。
                mPathMeasure.getPosTan(value, mCurrentPosition, null);//mCurrentPosition此时就是中间距离点的坐标值
                // 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
                goods.setTranslationX(mCurrentPosition[0]);
                goods.setTranslationY(mCurrentPosition[1]);
            }
        });
//      五、 开始执行动画
        valueAnimator.start();

//      六、动画结束后的处理
        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            //当动画结束后:
            @Override
            public void onAnimationEnd(Animator animation) {
                // 购物车的数量加1
                i++;
                count.setText(String.valueOf(i));
                // 把移动的图片imageview从父布局里移除
                rl.removeView(goods);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }


    class MyVH extends RecyclerView.ViewHolder {

        private ImageView iv;
        private TextView buy;

        public MyVH(View itemView) {
            super(itemView);
            iv = (ImageView) itemView.findViewById(R.id.iv);
            buy = (TextView) itemView.findViewById(R.id.buy);
        }
    }


}

Android自定义控件-Path之贝赛尔曲线和手势轨迹、水波纹效果

Android自定义控件-Path之贝赛尔曲线和手势轨迹、水波纹效果

从这篇开始,我将延续androidGraphics系列文章把图片相关的知识给大家讲完,这一篇先稍微进阶一下,给大家把《android Graphics(二):路径及文字》略去的quadTo(二阶贝塞尔)函数,给大家补充一下。
本篇*终将以两个例子给大家演示贝塞尔曲线的强大用途:
1、手势轨迹
%title插图%num

利用贝塞尔曲线,我们能实现平滑的手势轨迹效果
2、水波纹效果

%title插图%num

电池充电时,有些手机会显示水波纹效果,就是这样做出来的。
废话不多说,开整吧

一、概述

在《android Graphics(二):路径及文字》中我们略去了有关所有贝赛尔曲线的知识,在Path中有四个函数与贝赛尔曲线有关:

[java]
  1. //二阶贝赛尔  
  2. public void quadTo(float x1, float y1, float x2, float y2)  
  3. public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  
  4. //三阶贝赛尔  
  5. public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  
  6. public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  

这里的四个函数的具体意义我们后面会具体详细讲解,我们这篇也就是利用这四个函数来实现我们的贝赛尔曲线相关的效果的。

1、贝赛尔曲线来源

在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线*初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

2、贝赛尔曲线公式

这部分是很有难度的,大家做好准备了哦

一阶贝赛尔曲线

其公式可概括为:
%title插图%num

对应动画演示为:

%title插图%num

P0为起点、P1为终点,t表示当前时间,B(t)表示公式的结果值。
注意,曲线的意义就是公式结果B(t)随时间的变化,其取值所形成的轨迹。在动画中,黑色点表示在当前时间t下公式B(t)的取值。而红色的那条线就不在各个时间点下不同取值的B(t)所形成的轨迹。
总而言之:对于一阶贝赛尔曲线,大家可以理解为在起始点和终点形成的这条直线上,匀速移动的点。

二阶贝赛尔曲线

同样,先来看看二阶贝赛尔曲线的公式(虽然看不懂,呵呵)

%title插图%num

大家也不用研究这个公式了,没一定数学功底也研究不出来了啥,咱还是看动画吧

%title插图%num

在这里P0是起始点,P2是终点,P1是控制点
假设将时间定在t=0.25的时刻,此时的状态如下图所示:

%title插图%num

首先,P0点和P1点形成了一条贝赛尔曲线,还记得我们上面对一阶贝赛尔曲线的总结么:就是一个点在这条直线上做匀速运动;所以P0-P1这条直线上的移动的点就是Q0;
同样,P1,P2形成了一条一阶贝赛尔曲线,在这条一阶贝赛尔曲线上,它们的随时间移动的点是Q1
*后,动态点Q0和Q1又形成了一条一阶贝赛尔曲线,在它们这条一阶贝赛尔曲线动态移动的点是B
而B的移动轨迹就是这个二阶贝赛尔曲线的*终形态。从上面的讲解大家也可以知道,之所以叫它二阶贝赛尔曲线是因为,B的移动轨迹是建立在两个一阶贝赛尔曲线的中间点Q0,Q1的基础上的。
在理解了二阶贝赛尔曲线的形成原理以后,我们就不难理解三阶贝赛尔曲线了

三阶贝赛尔曲线

同样,先列下基本看不懂的公式

%title插图%num

这玩意估计也看不懂,讲了也没什么意义,还是结合动画来吧

%title插图%num

同样,我们取其中一点来讲解轨迹的形成原理,当t=0.25时,此时状态如下:

%title插图%num

同样,P0是起始点,P3是终点;P1是*个控制点,P2是第二个控制点;
首先,这里有三条一阶贝赛尔曲线,分别是P0-P1,P1-P2,P2-P3;
他们随时间变化的点分别为Q0,Q1,Q2
然后是由Q0,Q1,Q2这三个点,再次连接,形成了两条一阶贝赛尔曲线,分别是Q0—Q1,Q1—Q2;他们随时间变化的点为R0,R1
同样,R0和R1同样可以连接形成一条一阶贝赛尔曲线,在R0—R1这条贝赛尔曲线上随时间移动的点是B
而B的移动轨迹就是这个三阶贝赛尔曲线的*终形状。
从上面的解析大家可以看出,所谓几阶贝赛尔曲线,全部是由一条条一阶贝赛尔曲线搭起来的;
在上图中,形成一阶贝赛尔曲线的直线是灰色的,形成二阶贝赛尔曲线线是绿色的,形成三阶贝赛尔曲线的线是蓝色的。
在理解了上面的二阶和三阶贝赛尔曲线以后,我们再来看几个贝赛尔曲线的动态图

四阶贝赛尔曲线

%title插图%num

五阶贝赛尔曲线

%title插图%num

这里就不再一一讲解形成原理了,大家理解了二阶和三阶贝赛尔曲线以后,这两条的看看就好了,想必大家也是能自己推出四阶贝赛尔曲线的形成原理的。

3、贝赛尔曲线与PhotoShop钢笔工具

如果有些同学不懂PhotoShop,这篇文章可能就会有些难度了,本篇文章主要是利用PhotoShop的钢笔工具来得出具体贝塞尔图像的
这么屌的贝赛尔曲线,在专业绘图工具PhotoShop中当然会有它的踪影,它就是钢笔工具,钢笔工具所使用的路径弯曲效果就是二阶贝赛尔曲线。
我来给大家演示一下钢笔工具的用法:

%title插图%num

我们拿*终成形的图形来看一下为什么钢笔工具是二阶贝赛尔曲线:

%title插图%num

右图演示的假设某一点t=0.25时,动态点B的位置图
同样,这里P0是起始点,P2是终点,P1是控制点;
P0-P1、P1-P2形成了*层的一阶贝赛尔曲线。它们随时间的动态点分别是Q0,Q1
动态点Q0,Q1又形成了第二层的一阶贝赛尔曲线,它们的动态点是B.而B的轨迹跟钢笔工具的形状是完全一样的。所以钢笔工具的拉伸效果是使用的二阶贝赛尔曲线!
这个图与上面二阶贝赛尔曲线t=0.25时的曲线差不多,大家理解起来难度也不大。
这里需要注意的是,我们在使用钢笔工具时,拖动的是P5点。其实二阶贝赛尔曲线的控制点是其对面的P1点,钢笔工具这样设计是当然是因为操作起来比较方便。
好了,对贝赛尔曲线的知识讲了那么多,下面开始实战了,看在代码中,贝赛尔曲线是怎么来做的。

二、Android中贝赛尔曲线之quadTo

在开篇中,我们已经提到,在Path类中有四个方法与贝赛尔曲线相关,分别是:

[java]
  1. //二阶贝赛尔  
  2. public void quadTo(float x1, float y1, float x2, float y2)  
  3. public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  
  4. //三阶贝赛尔  
  5. public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  
  6. public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  

在这四个函数中quadTo、rQuadTo是二阶贝赛尔曲线,cubicTo、rCubicTo是三阶贝赛尔曲线;我们这篇文章以二阶贝赛尔曲线的quadTo、rQuadTo为主,三阶贝赛尔曲线cubicTo、rCubicTo用的使用方法与二阶贝赛尔曲线类似,用处也比较少,这篇就不再细讲了。

1、quadTo使用原理

这部分我们先来看看quadTo函数的用法,其定义如下:

[java]
  1. public void quadTo(float x1, float y1, float x2, float y2)  

参数中(x1,y1)是控制点坐标,(x2,y2)是终点坐标
大家可能会有一个疑问:有控制点和终点坐标,那起始点是多少呢?
整条线的起始点是通过Path.moveTo(x,y)来指定的,而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;大家可能还是有点迷糊,下面我们就举个例子来看看
我们利用quadTo()来画下面的这条波浪线:
%title插图%num

*关键的是如何来确定控制点的位置!前面讲过,PhotoShop中的钢笔工具是二阶贝赛尔曲线,所以我们可以利用钢笔工具来模拟画出这条波浪线来辅助确定控制点的位置

%title插图%num

下面我们来看看这个路径轨迹中,控制点分别在哪个位置

%title插图%num

我们先看P0-P2这条轨迹,P0是起点,假设位置坐标是(100,300),P2是终点,假充位置坐标是(300,300);在以P0为起始点,P2为终点这条二阶贝赛尔曲线上,P1是控制点,很明显P1大概在P0,P2中间的位置,所以它的X坐标应该是200,关于Y坐标,我们无法确定,但很明显的是P1在P0,P2点的上方,也就是它的Y值比它们的小,所以根据钢笔工具上面的位置,我们让P1的比P0,P2的小100;所以P1的坐标是(200,200)
同理,不难求出在P2,P4这条二阶贝赛尔曲线上,它们的控制点P3的坐标位置应该是(400,400);P3的X坐标是400是,因为P3点是P2,P4的中间点;与P3与P1距离P0-P2-P4这条直线的距离应该是相等的。P1距离P0-P2的值为100;P3距离P2-P4的距离也应该是100,这样不难算出P3的坐标应该是(400,400);
下面开始是代码部分了。

2、示例代码

(1)、自定义View

我们知道在动画绘图时,会调用onDraw(Canvas canvas)函数,我们如果重写了onDraw(Canvas canvas)函数,那么我们利用canvas在上面画了什么,就会显示什么。所以我们自定义一个View

[java]
  1. public class MyView extends View {  
  2.     public MyView(Context context) {  
  3.         super(context);  
  4.     }
  5.     public MyView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.     }
  8.     @Override  
  9.     protected void onDraw(Canvas canvas) {  
  10.         super.onDraw(canvas);  
  11.         Paint paint = new Paint();  
  12.         paint.setStyle(Paint.Style.STROKE);
  13.         paint.setColor(Color.GREEN);
  14.         Path path = new Path();  
  15.         path.moveTo(100,300);  
  16.         path.quadTo(200,200,300,300);  
  17.         path.quadTo(400,400,500,300);  
  18.         canvas.drawPath(path,paint);
  19.     }
  20. }

这里*重要的就是在onDraw(Canvas canvas)中创建Path的过程,我们在上面已经提到,*个起始点是需要调用path.moveTo(100,300)来指定的,之后后一个path.quadTo的起始点是以前一个path.quadTo的终点为起始点的。有关控制点的位置如何查找,我们上面已经利用钢笔工具给大家讲解了,这里就不再细讲。
所以,大家在自定义控件的时候,要多跟UED沟通,看他们是如何来实现这个效果的,如果是用的钢笔工具,那我们也可以效仿使用二阶贝赛尔曲线来实现。

2、使用MyView

在自定义控件以后,然后直接把它引入到主布局文件中即可(main.xml)

[html]
  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.               android:orientation=“vertical”  
  4.               android:layout_width=“fill_parent”  
  5.               android:layout_height=“fill_parent”>  
  6.     <com.harvic.BlogBerzMovePath.MyView  
  7.             android:layout_width=“match_parent”  
  8.             android:layout_height=“match_parent”/>  
  9. </LinearLayout>  

由于直接做为控件显示,所以MainActivity不需要额外的代码即可显示,MainActivity代码如下:

[java]
  1. public class MyActivity extends Activity {  
  2.     /** 
  3.      * Called when the activity is first created. 
  4.      */  
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);
  9.     }
  10. }

源码在文章底部给出 
通过这个例子希望大家知道两点:

  • 整条线的起始点是通过Path.moveTo(x,y)来指定的,如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;
  • 而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;

三、手指轨迹

要实现手指轨迹其实是非常简单的,我们只需要在自定义中拦截OnTouchEvent,然后根据手指的移动轨迹来绘制Path即可。
要实现把手指的移动轨迹连接起来,*简单的方法就是直接使用Path.lineTo()就能实现把各个点连接起来。

1、实现方式一:Path.lineTo(x,y)

我们先来看看效果图:
%title插图%num

(1)、自定义View——MyView

首先,我们自定义一个View,完整代码如下:

[java]
  1. public class MyView extends View {  
  2.     private Path mPath = new Path();  
  3.     public MyView(Context context) {  
  4.         super(context);  
  5.     }
  6.     public MyView(Context context, AttributeSet attrs) {  
  7.         super(context, attrs);  
  8.     }
  9.     @Override  
  10.     public boolean onTouchEvent(MotionEvent event) {  
  11.         switch (event.getAction()){  
  12.             case MotionEvent.ACTION_DOWN: {  
  13.                 mPath.moveTo(event.getX(), event.getY());
  14.                 return true;  
  15.             }
  16.             case MotionEvent.ACTION_MOVE:  
  17.                 mPath.lineTo(event.getX(), event.getY());
  18.                 postInvalidate();
  19.                 break;  
  20.             default:  
  21.                 break;  
  22.         }
  23.         return super.onTouchEvent(event);  
  24.     }
  25.     @Override  
  26.     protected void onDraw(Canvas canvas) {  
  27.         super.onDraw(canvas);  
  28.         Paint paint = new Paint();  
  29.         paint.setColor(Color.GREEN);
  30.         paint.setStyle(Paint.Style.STROKE);
  31.         canvas.drawPath(mPath,paint);
  32.     }
  33.     public void reset(){  
  34.         mPath.reset();
  35.         invalidate();
  36.     }
  37. }

*重要的位置就是在重写onTouchEvent的位置:

[java]
  1. public boolean onTouchEvent(MotionEvent event) {  
  2.     switch (event.getAction()){  
  3.         case MotionEvent.ACTION_DOWN: {  
  4.             mPath.moveTo(event.getX(), event.getY());
  5.             return true;  
  6.         }
  7.         case MotionEvent.ACTION_MOVE:  
  8.             mPath.lineTo(event.getX(), event.getY());
  9.             postInvalidate();
  10.             break;  
  11.         default:  
  12.             break;  
  13.     }
  14.     return super.onTouchEvent(event);  
  15. }

当用户点击屏幕的时候,我们调用mPath.moveTo(event.getX(), event.getY());然后在用户移动手指时使用mPath.lineTo(event.getX(), event.getY());将各个点串起来。然后调用postInvalidate()重绘;
Path.moveTo()和Path.lineTo()的用法,大家如果看了《android Graphics(二):路径及文字》之后,理解起来应该没什么难度,但这里有两个地方需要注意
*:有关在case MotionEvent.ACTION_DOWN时return true的问题:return true表示当前控件已经消费了下按动作,之后的ACTION_MOVE、ACTION_UP动作也会继续传递到当前控件中;如果我们在case MotionEvent.ACTION_DOWN时return false,那么后序的ACTION_MOVE、ACTION_UP动作就不会再传到这个控件来了。有关动作拦截的知识,后续会在这个系列中单独来讲,大家先期待下吧。
第二:这里重绘控件使用的是postInvalidate();而我们以前也有用Invalidate()函数的。这两个函数的作用都是用来重绘控件的,但区别是Invalidate()一定要在UI线程执行,如果不是在UI线程就会报错。而postInvalidate()则没有那么多讲究,它可以在任何线程中执行,而不必一定要是主线程。其实在postInvalidate()就是利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,而不会出错。而正是因为它是通过发消息来实现的,所以它的界面刷新可能没有直接调Invalidate()刷的那么快。
所以在我们确定当前线程是主线程的情况下,还是以invalide()函数为主。当我们不确定当前要刷新页面的位置所处的线程是不是主线程的时候,还是用postInvalidate为好;
这里我是故意用的postInvalidate(),因为onTouchEvent()本来就是在主线程中的,使用Invalidate()是更合适的。当我们
有关OnDraw函数就没什么好讲的,就是把path给画出来:

[java]
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     Paint paint = new Paint();  
  4.     paint.setColor(Color.GREEN);
  5.     paint.setStyle(Paint.Style.STROKE);
  6.     canvas.drawPath(mPath,paint);
  7. }

*后,我还额外写了一个重置函数:

[java]
  1. public void reset(){  
  2.     mPath.reset();
  3.     invalidate();
  4. }

(2)、主布局

然后看看布局文件(mian.xml)

[html]
  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.               android:orientation=“vertical”  
  4.               android:layout_width=“fill_parent”  
  5.               android:layout_height=“fill_parent”>  
  6.     <Button  
  7.             android:id=“@+id/reset”  
  8.             android:layout_width=“match_parent”  
  9.             android:layout_height=“wrap_content”  
  10.             android:text=“reset”/>  
  11.     <com.harvic.BlogMovePath.MyView  
  12.             android:id=“@+id/myview”  
  13.             android:layout_width=“match_parent”  
  14.             android:layout_height=“match_parent”/>  
  15. </LinearLayout>  

没什么难度,就是把自定义控件添加到布局中

(3)、MyActivity

然后看MyActivity的操作:

[java]
  1. public class MyActivity extends Activity {  
  2.     @Override  
  3.     public void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.main);
  6.         final MyView myView = (MyView)findViewById(R.id.myview);  
  7.         findViewById(R.id.reset).setOnClickListener(new View.OnClickListener() {  
  8.             @Override  
  9.             public void onClick(View v) {  
  10.                 myView.reset();
  11.             }
  12.         });
  13.     }
  14. }

这里实现的就是当点击按钮时,调用 myView.reset()来重置画布;
源码在文章底部给出 

(4)、使用Path.lineTo()所存在问题

上面我们虽然实现了,画出手指的移动轨迹,但我们仔细来看看画出来的图:
%title插图%num

我们把S放大,明显看出,在两个点连接处有明显的转折,而且在S顶部位置横纵坐标变化比较快的位置,看起来跟图片这大后的马赛克一样;利用Path绘图,是不可能出现马赛克的,因为除了Bitmap以外的任何canvas绘图全部都是矢量图,也就是利用数学公式来作出来的图,无论放在多大屏幕上,都不可能会出现马赛克!这里利用Path绘图,在S顶部之所以看起来像是马赛克是因为这个S是由各个不同点之间连线写出来的,而之间并没有平滑过渡,所以当坐标变化比较剧烈时,线与线之间的转折就显得特别明显了。
所以要想优化这种效果,就得实现线与线之间的平滑过渡,很显然,二阶贝赛尔曲线就是干这个事的。下面我们就利用我们新学的Path.quadTo函数来重新实现下移动轨迹效果。

2、实现方式二(优化):使用Path.quadTo()函数实现过渡

(1)、原理概述

我们上面讲了,使用Path.lineTo()的*大问题就是线段转折处不够平滑。Path.quadTo()可以实现平滑过渡,但使用Path.quadTo()的*大问题是,如何找到起始点和结束点。
下图中,有用绿点表示的三个点,连成的两条直线,很明显他们转折处是有明显折痕的
%title插图%num
下面我们在PhotoShop中利用钢笔工具,看如何才能实现这两条线之间的转折

%title插图%num

%title插图%num

从这两个线段中可以看出,我们使用Path.lineTo()的时候,是直接把手指触点A,B,C给连起来。
而钢笔工具要实现这三个点间的流畅过渡,就只能将这两个线段的中间点做为起始点和结束点,而将手指的倒数第二个触点B做为控制点。
大家可能会觉得,那这样,在结束的时候,A到P0和P1到C1的这段距离岂不是没画进去?是的,如果Path*终没有close的话,这两段距离是被抛弃掉的。因为手指间滑动时,每两个点间的距离很小,所以P1到C之间的距离可以忽略不计。
下面我们就利用这种方法在photoshop中求证,在连接多个线段时,是否能行?

%title插图%num

在这个图形中,有很多点连成了弯弯曲曲的线段,我们利用上面我们讲的,将两个线段的中间做为二阶贝尔赛曲线的起始点和终点,把上一个手指的位置做为控制点,来看看是否真的能组成平滑的连线
整个连接过程如动画所示:

%title插图%num

在*终的路径中看来,各个点间的连线是非常平滑的。从这里也可以看出,在为了实现平滑效果,我们只能把开头的线段一半和结束的线段的一半抛弃掉。
在讲了原理之后,下面就来看看在代码中如何来实现吧。

(2)、自定义View

先贴出完整代码然后再细讲:

[java]
  1. public class MyView extends View {  
  2.     private Path mPath = new Path();  
  3.     private float mPreX,mPreY;  
  4.     public MyView(Context context) {  
  5.         super(context);  
  6.     }
  7.     public MyView(Context context, AttributeSet attrs) {  
  8.         super(context, attrs);  
  9.     }
  10.     @Override  
  11.     public boolean onTouchEvent(MotionEvent event) {  
  12.         switch (event.getAction()){  
  13.             case MotionEvent.ACTION_DOWN:{  
  14.                 mPath.moveTo(event.getX(),event.getY());
  15.                 mPreX = event.getX();
  16.                 mPreY = event.getY();
  17.                 return true;  
  18.             }
  19.             case MotionEvent.ACTION_MOVE:{  
  20.                 float endX = (mPreX+event.getX())/2;  
  21.                 float endY = (mPreY+event.getY())/2;  
  22.                 mPath.quadTo(mPreX,mPreY,endX,endY);
  23.                 mPreX = event.getX();
  24.                 mPreY =event.getY();
  25.                 invalidate();
  26.             }
  27.             break;  
  28.             default:  
  29.                 break;  
  30.         }
  31.         return super.onTouchEvent(event);  
  32.     }
  33.     @Override  
  34.     protected void onDraw(Canvas canvas) {  
  35.         super.onDraw(canvas);  
  36.         Paint paint = new Paint();  
  37.         paint.setStyle(Paint.Style.STROKE);
  38.         paint.setColor(Color.GREEN);
  39.         paint.setStrokeWidth(2);  
  40.         canvas.drawPath(mPath,paint);
  41.     }
  42.     public void reset(){  
  43.         mPath.reset();
  44.         postInvalidate();
  45.     }
  46. }

*难的部分依然是onTouchEvent函数这里:

[java]
  1. public boolean onTouchEvent(MotionEvent event) {  
  2.     switch (event.getAction()){  
  3.         case MotionEvent.ACTION_DOWN:{  
  4.             mPath.moveTo(event.getX(),event.getY());
  5.             mPreX = event.getX();
  6.             mPreY = event.getY();
  7.             return true;  
  8.         }
  9.         …………
  10.     }
  11.     return super.onTouchEvent(event);  
  12. }

在ACTION_DOWN的时候,利用 mPath.moveTo(event.getX(),event.getY())将Path的初始位置设置到手指的触点处,如果不调用mPath.moveTo的话,会默认是从(0,0)开始的。然后我们定义两个变量mPreX,mPreY来表示手指的前一个点。我们通过上面的分析知道,这个点是用来做控制点的。*后return true让ACTION_MOVE,ACTION_UP事件继续向这个控件传递。
在ACTION_MOVE时:

[java]
  1. case MotionEvent.ACTION_MOVE:{  
  2.     float endX = (mPreX+event.getX())/2;  
  3.     float endY = (mPreY+event.getY())/2;  
  4.     mPath.quadTo(mPreX,mPreY,endX,endY);
  5.     mPreX = event.getX();
  6.     mPreY =event.getY();
  7.     invalidate();
  8. }

我们先找到结束点,我们说了结束点是这个线段的中间位置,所以很容易求出它的坐标endX,endY;控制点是上一个手指位置即mPreX,mPreY;那有些同学可能会问了,那起始点是哪啊。在开篇讲quadTo()函数时,就已经说过,*个起始点是Path.moveTo(x,y)定义的,其它部分,一个quadTo的终点,是下一个quadTo的起始点。
所以这里的起始点,就是上一个线段的中间点。所以,这样就与钢笔工具绘制过程完全对上了:把各个线段的中间点做为起始点和终点,把终点前一个手指位置做为控制点。
后面的onDraw()和reset()函数就没什么难度了,上面的例子中也讲过了,就不再赘述了
*终的效果图如下:
%title插图%num
同样把lineTo和quadTo实现的S拿来对比下:
%title插图%num

从效果图中可以明显可以看出,通过quadTo实现的曲线更顺滑
源码在文章底部给出 
Ok啦,quadeTo的用法,到这里就结束了,下部分再来讲讲rQuadTo的用法及波浪动画效果

%title插图%num

四、Path.rQuadTo()

1、概述

该函数声明如下

[java] view plain copy 
  1. public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  

其中:

  • dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,可为负值,正值表示相加,负值表示相减;
  • dy1:控制点Y坐标,相对上一个终点Y坐标的位移坐标。同样可为负值,正值表示相加,负值表示相减;
  • dx2:终点X坐标,同样是一个相对坐标,相对上一个终点X坐标的位移值,可为负值,正值表示相加,负值表示相减;
  • dy2:终点Y坐标,同样是一个相对,相对上一个终点Y坐标的位移值。可为负值,正值表示相加,负值表示相减;

这四个参数都是传递的都是相对值,相对上一个终点的位移值。
比如,我们上一个终点坐标是(300,400)那么利用rQuadTo(100,-100,200,100);
得到的控制点坐标是(300+100,400-100)即(500,300)
同样,得到的终点坐标是(300+200,400+100)即(500,500)
所以下面这两段代码是等价的:
利用quadTo定义*对坐标

[java] view plain copy 
  1. path.moveTo(300,400);  
  2. path.quadTo(500,300,500,500);  

与利用rQuadTo定义相对坐标

[java] view plain copy 
  1. path.moveTo(300,400);  
  2. path.rQuadTo(100,-100,200,100)  

2、使用rQuadTo实现波浪线

在上篇中,我们使用quadTo实现了一个简单的波浪线:
%title插图%num

%title插图%num

各个点具体计算过程,在上篇已经计算过了,下面是上篇中onDraw的代码:

[java] view plain copy 
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     Paint paint = new Paint();  
  4.     paint.setStyle(Paint.Style.STROKE);
  5.     paint.setColor(Color.GREEN);
  6.     Path path = new Path();  
  7.     path.moveTo(100,300);  
  8.     path.quadTo(200,200,300,300);  
  9.     path.quadTo(400,400,500,300);  
  10.     canvas.drawPath(path,paint);
  11. }

下面我们将它转化为rQuadTo来重新实现下:

[java] view plain copy 
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     Paint paint = new Paint();  
  4.     paint.setStyle(Paint.Style.STROKE);
  5.     paint.setColor(Color.GREEN);
  6.     Path path = new Path();  
  7.     path.moveTo(100,300);  
  8.     path.rQuadTo(100,-100,200,0);  
  9.     path.rQuadTo(100,100,200,0);  
  10.     canvas.drawPath(path,paint);
  11. }

简单来讲,就是将原来的:

[java] view plain copy 
  1. path.moveTo(100,300);  
  2. path.quadTo(200,200,300,300);  
  3. path.quadTo(400,400,500,300);  

转化为:

[java] view plain copy 
  1. path.moveTo(100,300);  
  2. path.rQuadTo(100,-100,200,0);  
  3. path.rQuadTo(100,100,200,0);  

*句:path.rQuadTo(100,-100,200,0);是建立在(100,300)这个点基础上来计算相对坐标的。
所以
控制点X坐标=上一个终点X坐标+控制点X位移 = 100+100=200;
控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300-100=200;
终点X坐标 = 上一个终点X坐标+终点X位移 = 100+200=300;
终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
所以这句与path.quadTo(200,200,300,300);对等的
第二句:path.rQuadTo(100,100,200,0);是建立在它的前一个终点即(300,300)的基础上来计算相对坐标的!
所以
控制点X坐标=上一个终点X坐标+控制点X位移 = 300+100=200;
控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300+100=200;
终点X坐标 = 上一个终点X坐标+终点X位移 = 300+200=500;
终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
所以这句与path.quadTo(400,400,500,300);对等的
*终效果也是一样的。
通过这个例子,只想让大家明白一点:rQuadTo(float dx1, float dy1, float dx2, float dy2)中的位移坐标,都是以上一个终点位置为基准来做偏移的!

五、实现波浪效果

本节完成之后,将实现文章开头的波浪效果,如下。
%title插图%num

1、实现全屏波纹

上面我们已经能够实现一个波形,只要我们再多实现几个波形,就可以覆盖整个屏幕了。
%title插图%num
对应代码如下:

[java] view plain copy 
  1. public class MyView extends View {  
  2.     private Paint mPaint;  
  3.     private Path mPath;  
  4.     private int mItemWaveLength = 400;  
  5.     public MyView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mPath = new Path();  
  8.         mPaint = new Paint();  
  9.         mPaint.setColor(Color.GREEN);
  10.         mPaint.setStyle(Paint.Style.STROKE);
  11.     }
  12.     @Override  
  13.     protected void onDraw(Canvas canvas) {  
  14.         super.onDraw(canvas);  
  15.         mPath.reset();
  16.         int originY = 300;  
  17.         int halfWaveLen = mItemWaveLength/2;  
  18.         mPath.moveTo(-mItemWaveLength,originY);
  19.         for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  20.             mPath.rQuadTo(halfWaveLen/2,-50,halfWaveLen,0);  
  21.             mPath.rQuadTo(halfWaveLen/2,50,halfWaveLen,0);  
  22.         }
  23.         canvas.drawPath(mPath,mPaint);
  24.     }
  25. }

*难的部分依然是在onDraw函数中:

[java] view plain copy 
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     mPath.reset();
  4.     int originY = 300;  
  5.     int halfWaveLen = mItemWaveLength/2;  
  6.     mPath.moveTo(-mItemWaveLength,originY);
  7.     for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  8.         mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  9.         mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  10.     }
  11.     canvas.drawPath(mPath,mPaint);
  12. }

我们将mPath的起始位置向左移一个波长:

[java] view plain copy 
  1. mPath.moveTo(-mItemWaveLength,originY);

然后利用for循环画出当前屏幕中可能容得下的所有波:

[java] view plain copy 
  1. for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  2.     mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  3.     mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  4. }

mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);画的是一个波长中的前半个波,mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);画的是一个波长中的后半个波。大家在这里可以看到,屏幕左右都多画了一个波长的图形。这是为了波形移动做准备的。
到这里,我们是已经能画出来一整屏幕的波形了,下面我们把整体波形闭合起来
%title插图%num

其中,图中红色区域是我标出来利用lineTo闭合的区域

[java] view plain copy 
  1. public class MyView extends View {  
  2.     private Paint mPaint;  
  3.     private Path mPath;  
  4.     private int mItemWaveLength = 400;  
  5.     public MyView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mPath = new Path();  
  8.         mPaint = new Paint();  
  9.         mPaint.setColor(Color.GREEN);
  10.         mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
  11.     }
  12.     @Override  
  13.     protected void onDraw(Canvas canvas) {  
  14.         super.onDraw(canvas);  
  15.         mPath.reset();
  16.         int originY = 300;  
  17.         int halfWaveLen = mItemWaveLength/2;  
  18.         mPath.moveTo(-mItemWaveLength+dx,originY);
  19.         for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  20.             mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  21.             mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  22.         }
  23.         mPath.lineTo(getWidth(),getHeight());
  24.         mPath.lineTo(0,getHeight());  
  25.         mPath.close();
  26.         canvas.drawPath(mPath,mPaint);
  27.     }
  28. }

这段代码相比上面的代码,增加了两部分内容:
*,将paint设置为填充:mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
第二,将path闭合:

[java] view plain copy 
  1. mPath.moveTo(-mItemWaveLength+dx,originY);
  2. for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  3.     mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  4.     mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  5. }
  6. mPath.lineTo(getWidth(),getHeight());
  7. mPath.lineTo(0,getHeight());  
  8. mPath.close();

2、实现移动动画

让波纹动起来其实挺简单,利用调用在path.moveTo的时候,将起始点向右移动即可实现移动,而且只要我们移动一个波长的长度,波纹就会重合,就可以实现无限循环了。
为此我们定义一个动画:

[java] view plain copy 
  1. public void startAnim(){  
  2.     ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);  
  3.     animator.setDuration(2000);  
  4.     animator.setRepeatCount(ValueAnimator.INFINITE);
  5.     animator.setInterpolator(new LinearInterpolator());  
  6.     animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  7.         @Override  
  8.         public void onAnimationUpdate(ValueAnimator animation) {  
  9.             dx = (int)animation.getAnimatedValue();  
  10.             postInvalidate();
  11.         }
  12.     });
  13.     animator.start();
  14. }

动画的长度为一个波长,将当前值保存在类的成员变量dx中;
然后在画图的时候,在path.moveTo()中加上现在的移动值dx:mPath.moveTo(-mItemWaveLength+dx,originY);
完整的绘图代码如下:

[java] view plain copy 
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     mPath.reset();
  4.     int originY = 300;  
  5.     int halfWaveLen = mItemWaveLength/2;  
  6.     mPath.moveTo(-mItemWaveLength+dx,originY);
  7.     for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  8.         mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  9.         mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  10.     }
  11.     mPath.lineTo(getWidth(),getHeight());
  12.     mPath.lineTo(0,getHeight());  
  13.     mPath.close();
  14.     canvas.drawPath(mPath,mPaint);
  15. }

完整的MyView代码如下:

[java] view plain copy 
  1. public class MyView extends View {  
  2.     private Paint mPaint;  
  3.     private Path mPath;  
  4.     private int mItemWaveLength = 400;  
  5.     private int dx;  
  6.     public MyView(Context context, AttributeSet attrs) {  
  7.         super(context, attrs);  
  8.         mPath = new Path();  
  9.         mPaint = new Paint();  
  10.         mPaint.setColor(Color.GREEN);
  11.         mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
  12.     }
  13.     @Override  
  14.     protected void onDraw(Canvas canvas) {  
  15.         super.onDraw(canvas);  
  16.         mPath.reset();
  17.         int originY = 300;  
  18.         int halfWaveLen = mItemWaveLength/2;  
  19.         mPath.moveTo(-mItemWaveLength+dx,originY);
  20.         for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){  
  21.             mPath.rQuadTo(halfWaveLen/2,-100,halfWaveLen,0);  
  22.             mPath.rQuadTo(halfWaveLen/2,100,halfWaveLen,0);  
  23.         }
  24.         mPath.lineTo(getWidth(),getHeight());
  25.         mPath.lineTo(0,getHeight());  
  26.         mPath.close();
  27.         canvas.drawPath(mPath,mPaint);
  28.     }
  29.     public void startAnim(){  
  30.         ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);  
  31.         animator.setDuration(2000);  
  32.         animator.setRepeatCount(ValueAnimator.INFINITE);
  33.         animator.setInterpolator(new LinearInterpolator());  
  34.         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  35.             @Override  
  36.             public void onAnimationUpdate(ValueAnimator animation) {  
  37.                 dx = (int)animation.getAnimatedValue();  
  38.                 postInvalidate();
  39.             }
  40.         });
  41.         animator.start();
  42.     }
  43. }

然后在MyActivity中开始动画:

[java] view plain copy
  1. public class MyActivity extends Activity {  
  2.     /** 
  3.      * Called when the activity is first created. 
  4.      */  
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);
  9.         final MyView myView = (MyView)findViewById(R.id.myview);  
  10.         myView.startAnim();
  11.     }
  12. }

这样就实现了动画:
%title插图%num

如果把波长设置为1000,就可以实现本段开篇的动画了。
如果想让波纹像开篇时那要同时向下移动,大家只需要在path.moveTo(x,y)的时候,通过动画同时移动y坐标就可以了,代码比较简单,而且本文实在是太长了,具体实现就不再讲了

Path特效之PathMeasure打造万能路径动效

今天我们一起利用 Path 做个比较实用的小例子;

上一篇我们使用 Path 绘制了一个小桃心,我们这一篇继续围绕着这个小桃心进行展开:

%title插图%num

 

————————————————–

如果对这个桃心绘制有问题或有兴趣的同学,可以链接到 Path相关方法讲解(二),此时我们的需求是这样的:

假定我们现在是一个婚恋产品,有一个“心动”的功能,用户点击“心动”按钮的时候,有一个光点快速的沿着桃心转一圈,然后整个桃心泛起光晕!

针对这个需求,很多人可能会想到以下方案:

不就一个光点沿着桃心跑一圈么,既然桃心是使用贝塞尔曲线画出来的,那么我们就可以用对应的函数模拟出这条曲线,然后算出对应位置上的点,不断将光点绘制到对应的位置上!

这个思路当然没有问题,但我们还有相对简单的方式,那就是使用 PathMeasure:

我们主要使用它两个方法:

1.getLength() – 获取路径的长度

2.getPosTan(float distance, float pos[],float tan[]) - path 为 null ,返回 false

distance 为一个 0 - getLength() 之间的值,根据这个值 PathMeasure 会计算出当前点的坐标封装到 pos 中;

上面这句话我们可以这么来理解,不管实际 Path 多么的复杂,PathMeasure 都相当于做了一个事情,就是把 Path “拉直”,然后给了我们一个接口(getLength)告诉我们path的总长度,然后我们想要知道具体某一点的坐标,只需要用相对的distance去取即可,这样就省去了自己用函数模拟path,然后计算获取点坐标的过程;

接下来,我们用代码实现这一效果:

我们先创建一个 PathMeasure ,并将创建好的 path 作为参数传入

 

  1. mPath = new Path();  
  2. mPath.moveTo(START_POINT[0], START_POINT[1]);
  3. mPath.quadTo(RIGHT_CONTROL_POINT[0], RIGHT_CONTROL_POINT[1], BOTTOM_POINT[0],
  4.         BOTTOM_POINT[1]);
  5. mPath.quadTo(LEFT_CONTROL_POINT[0], LEFT_CONTROL_POINT[1], START_POINT[0], START_POINT[1]);
  6. mPathMeasure = new PathMeasure(mPath, true);  

然后用一个数组纪录点的坐标:

 

  1. private float[] mCurrentPosition = new float[2];  

向外暴露一个开启动效的接口:

 

  1. // 开启路径动画
  2. public void startPathAnim(long duration) {
  3.     // 0 - getLength()
  4.     ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());  
  5.     Log.i(TAG, “measure length = ” + mPathMeasure.getLength());  
  6.     valueAnimator.setDuration(duration);
  7.     // 减速插值器
  8.     valueAnimator.setInterpolator(new DecelerateInterpolator());
  9.     valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
  10.         @Override
  11.         public void onAnimationUpdate(ValueAnimator animation) {
  12.             float value = (Float) animation.getAnimatedValue();  
  13.             // 获取当前点坐标封装到mCurrentPosition
  14.             mPathMeasure.getPosTan(value, mCurrentPosition, null);
  15.             postInvalidate();
  16.         }
  17.     });
  18.     valueAnimator.start();
  19. }

实时获取到当前点之后,将目标绘制到对应位置:

 

  1. protected void onDraw(Canvas canvas) {
  2.     super.onDraw(canvas);
  3.     canvas.drawColor(Color.WHITE);
  4.     canvas.drawPath(mPath, mPaint);
  5.     canvas.drawCircle(RIGHT_CONTROL_POINT[0], RIGHT_CONTROL_POINT[1], 5, mPaint);
  6.     canvas.drawCircle(LEFT_CONTROL_POINT[0], LEFT_CONTROL_POINT[1], 5, mPaint);
  7.     // 绘制对应目标
  8.     canvas.drawCircle(mCurrentPosition[0], mCurrentPosition[1], 10, mPaint);
  9. }

到这里目标环绕 path 的效果就ok了,不管这条路径简单也好,复杂也罢,我们都可以如此简单的完成对应的效果,而不需要自己用简单或复杂函数模拟求解了;

完成了一步,自己提的需求还有一点就是光晕的问题,这个东西如何是好呢?切图?! 不需要,Android 已经给我们提供了一个好用的东西 MaskFilter ,后面我就不做了,大家有兴趣自己做的玩玩,只需要注意一点,MaskFilter 不支持硬件加速,记得关掉!

好了,PathMeasure 看似很简单,但着实很有用,有了它,再结合上 Path 、Shader、ColorMatrix  等利器,我们已经可以做出很多酷炫的效果了!

*后,完整的代码献上,请笑纳:

 

  1. public class DynamicHeartView extends View {
  2.     private static final String TAG = “DynamicHeartView”;  
  3.     private static final int PATH_WIDTH = 2;  
  4.     // 起始点
  5.     private static final int[] START_POINT = new int[] {  
  6.             300, 270
  7.     };
  8.     // 爱心下端点
  9.     private static final int[] BOTTOM_POINT = new int[] {  
  10.             300, 400
  11.     };
  12.     // 左侧控制点
  13.     private static final int[] LEFT_CONTROL_POINT = new int[] {  
  14.             450, 200
  15.     };
  16.     // 右侧控制点
  17.     private static final int[] RIGHT_CONTROL_POINT = new int[] {  
  18.             150, 200
  19.     };
  20.     private PathMeasure mPathMeasure;
  21.     private Paint mPaint;
  22.     private Path mPath;
  23.     private float[] mCurrentPosition = new float[2];  
  24.     public DynamicHeartView(Context context) {
  25.         super(context);
  26.         init();
  27.     }
  28.     private void init() {
  29.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  30.         mPaint.setStyle(Style.STROKE);
  31.         mPaint.setStrokeWidth(PATH_WIDTH);
  32.         mPaint.setColor(Color.RED);
  33.         mPath = new Path();  
  34.         mPath.moveTo(START_POINT[0], START_POINT[1]);
  35.         mPath.quadTo(RIGHT_CONTROL_POINT[0], RIGHT_CONTROL_POINT[1], BOTTOM_POINT[0],
  36.                 BOTTOM_POINT[1]);
  37.         mPath.quadTo(LEFT_CONTROL_POINT[0], LEFT_CONTROL_POINT[1], START_POINT[0], START_POINT[1]);
  38.         mPathMeasure = new PathMeasure(mPath, true);  
  39.         mCurrentPosition = new float[2];  
  40.     }
  41.     @Override
  42.     protected void onDraw(Canvas canvas) {
  43.         super.onDraw(canvas);
  44.         canvas.drawColor(Color.WHITE);
  45.         canvas.drawPath(mPath, mPaint);
  46.         canvas.drawCircle(RIGHT_CONTROL_POINT[0], RIGHT_CONTROL_POINT[1], 5, mPaint);
  47.         canvas.drawCircle(LEFT_CONTROL_POINT[0], LEFT_CONTROL_POINT[1], 5, mPaint);
  48.         // 绘制对应目标
  49.         canvas.drawCircle(mCurrentPosition[0], mCurrentPosition[1], 10, mPaint);
  50.     }
  51.     // 开启路径动画
  52.     public void startPathAnim(long duration) {
  53.         // 0 - getLength()
  54.         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());  
  55.         Log.i(TAG, “measure length = ” + mPathMeasure.getLength());  
  56.         valueAnimator.setDuration(duration);
  57.         // 减速插值器
  58.         valueAnimator.setInterpolator(new DecelerateInterpolator());
  59.         valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
  60.             @Override
  61.             public void onAnimationUpdate(ValueAnimator animation) {
  62.                 float value = (Float) animation.getAnimatedValue();  
  63.                 // 获取当前点坐标封装到mCurrentPosition
  64.                 mPathMeasure.getPosTan(value, mCurrentPosition, null);
  65.                 postInvalidate();
  66.             }
  67.         });
  68.         valueAnimator.start();
  69.     }
  70. }

以上代码的效果如下,其余效果大家自行补充:

%title插图%num

PathMeasure打造万能路径特效