引子
自定义View是android高级UI知识体系的重要一环。也是区分中高级开发者的分水岭。高级开发者,知识体系完善,但凡能够语言描述出来的特效,他们总能给出解决方案。而中级开发者由于眼界受限,往往遇到复杂需求就无从下手。
一些看似复杂的特效,其实android已经为我们提供了一套解决方案,这是中级进阶高级的必学知识。
本文给出完整攻略,保证一篇入魂。= =!
(顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找)
https://github.com/xiangjiana/Android-MS
效果图
下图中可以看到,首先我们看到了一个心形,然后有波浪在跳动,最后绿色填满了整个心形
乍一看
诶?心形是怎么绘制的?诶?波浪是怎么画出来的,又是如何动起来的?诶? 文字是怎么呈现出同一时刻的两种颜色的?
不知道是不是有人有这样的疑惑````请继续往下看.
效果拆解
拿到一个复杂特效,第一件事不要慌,先仔细分析一下,这个特效里面具体有哪些细节可以拆分出来。复杂的东西都是由简单的细节 组合而成。
开始拆解
1、绘制区域是一个心形
2、波浪从最下面开始, 逐渐用绿色填充了整个心形
3、中间有文字内容“ 一条大灰狼”,并且在波浪增长的过程中,文字存在一段时间的上下两部分 颜色不同的状态.
本案例用到的知识点:
1、 canvas.clipPath 画布裁剪
2、 canvas.save 画布状态保存
3、 canvas.restore 恢复
4、 canvas.translate 画布平移
5、 path.rCubicTo 构建三阶贝塞尔曲线(相当于上一个点位置)
6、属性动画 ValueAnimator / AnimatorSet
开始撸码
第 1步:构建一个心形区域
当一个复杂图形摆在我们面前,而且还是不规则图形,我们首先应该想到的,就是 android.graphics.Path 类,它可以记录复杂图形的全部点组成的路径。关键代码:
/**
* 构建心形
* <p>
* 注意,它这个是以 矩形区域中心点为基准的图形,所以绘制的时候,必须先把坐标轴移动到 区域中心
*/
private void initHeartPath(Path path) {
List<PointF> pointList = new ArrayList<>();
pointList.add(new PointF(0,Utils.dp2px(-38)));
pointList.add(new PointF(Utils.dp2px(50),Utils.dp2px(-103)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-61)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-12)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(37)));
pointList.add(new PointF(Utils.dp2px(51),Utils.dp2px(90)));
pointList.add(new PointF(0,Utils.dp2px(129)));
pointList.add(new PointF(Utils.dp2px(-51),Utils.dp2px(90)));
pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(37)));
pointList.add(new PointF(Utils.dp2px(-112), Utils.dp2px(-12)));
pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(-61)));
pointList.add(new PointF(Utils.dp2px(-50),Utils.dp2px(-103)));
path.reset();
for(int i =0; i <4; i++) {
if (i ==0) {
path.moveTo(pointList.get(i *3).x, pointList.get(i *3).y);
} else {
path.lineTo(pointList.get(i * 3).x, pointList.get(i *3).y);
}
int endPointIndex;
if (i ==3) {
endPointIndex = 0;
} else {
endPointIndex = i *3+3;
}
path.cubicTo(pointList.get(i *3+1).x, pointList.get(i *3+1).y,
pointList.get(i *3+2).x, pointList.get(i *3+2).y,
pointList.get(endPointIndex).x, pointList.get(endPointIndex).y);
//你的心形就是用贝塞尔曲线来画的吗
}
path.close();
path.computeBounds(mHeartRect,false);
//把path所占据的最小矩形区域,返回出去
}
传入一个 Path引用,然后在方法内部对 path进行各种 api调用改变其属性. 这里需要提及一个重点:最后一行代码
path.computeBounds(mHeartRect,false);
意思是,无论什么样的 path,它都会占据一个最小矩形区域,computeBounds
方法可以获取这个矩形区域,设置给入参mHeartRect
.
第 2步:将心形区域裁剪出来, 裁剪之后,后续的绘制都只会显示在这个区域之内
(为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央)
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
canvas.translate(width / 2, height /2);
//为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央
...省略无关代码
canvas.clipPath(mMainPath);
//裁剪心形区域
canvas.save();
//保存画布状态
...省略无关代码
}
第 3步:绘制波浪区域
这里有两点细节
1)波浪区域分为两块, top和 bottom 上下两块
- 整个波浪区域的长度为 心形矩形范围宽度的 2倍 ( ?为什么是2倍?因为上面的波浪动画,其实是整个波浪区域平移造成的视觉效果,为了让这个动画可以无限执行,设计两倍宽度,当一半的宽度向右移动刚好触及心形矩形区域的右边框的时候,让它还原到原始位置,这样就能无缝衔接。)
关键代码1 - 波浪path的构建
/**
* @param ifTop 是否是上部分; 上下部分的封口位置不一样
* @param r 心形的矩形区域
* @param process 当前进度值
*/
private void resetWavePath(boolean ifTop,RectF r,float process,Pathpath) {
final float width = r.width();
final float height = r.width();
path.reset();
if( ifTop) {
path.moveTo(r.left - width, r.top);
} else {
path.moveTo(r.left - width, r.bottom);
//下部,初始位置点在 下
}
float waveHeight = height /8f;//波动的最大幅度
//找到矩形区域的左边线中点
path.lineTo(r.left - width,r.bottom - height * process);
//做两个周期的贝塞尔曲线
for (int i =0; i < 2; i++) {
float px1, py1, px2, py2, px3, py3;
px1 = width /4;
py1 = -waveHeight;
px2 = width /4*3;
py2 = waveHeight;
px3 = width;
py3 = 0;
path.rCubicTo(px1, py1, px2, py2, px3, py3);
}
if (ifTop) {
path.lineTo(r.right, r.top);
} else {
path.lineTo(r.right, r.bottom);
}
path.close();
}
关键代码2- 属性动画改变两个全局变量波浪的向上增长系数以及横向波浪动画系数:
AnimatorSet animatorSet;
// 动起来
public void startAnimator() {
if(animatorSet == null) {
animatorSet = new AnimatorSet();
ValueAnimator growAnimator = ValueAnimator.ofFloat(0f, 1f);
growAnimator.addUpdateListener(animation -> growProcess =(float) animation.getAnimatedValue());
growAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.cancel();
}
});
growAnimator.setInterpolator(new DecelerateInterpolator());
growAnimator.setDuration((long)(4000/ animatorSpeedCoefficient));
ValueAnimator waveAnimator = ValueAnimator.ofFloat(0f,1f);
waveAnimator.setRepeatCount(ValueAnimator.INFINITE);
waveAnimator.setRepeatMode(ValueAnimator.RESTART);
waveAnimator.addUpdateListener(animation -> {
waveProcess = (float) animation.getAnimatedValue();
invalidate();
});
waveAnimator.setInterpolator(new LinearInterpolator());
waveAnimator.setDuration((long)(1000/ animatorSpeedCoefficient));
animatorSet.playTogether(growAnimator, waveAnimator);
animatorSet.start();
} else {
animatorSet.cancel();
animatorSet.start();
}
}
关键代码3- 利用属性动画改变的全局变量,构建动态效果
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
canvas.translate(width /2, height /2);
//为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央
curXOffset = waveProcess * mHeartRect.width();
//当前X轴方向上 波浪偏移量
canvas.clipPath(mMainPath);
canvas.save();
mainRect = new Rect();
...省略无关代码
// 上波浪区域
resetWavePath(true, mHeartRect, growProcess, topWavePath);
canvas.translate(curXOffset,0);
canvas.clipPath(topWavePath);
canvas.drawPath(topWavePath, mTopPaint);
...省略无关代码
//下波浪区域
resetWavePath(false, mHeartRect, growProcess, bottomWavePath);
canvas.restore();
canvas.translate(curXOffset,0);
canvas.clipPath(bottomWavePath);
canvas.drawPath(bottomWavePath, mBottomPaint);
...省略无关代码
}
第 4步:绘制“一条大灰狼” 到心形中央,并且达成双色效果
这里有两个细节:
canvas.drawText
, 就算你把paint 设置了 .setTextAlign(Paint.Align.CENTER);
它也未必会在你给的 x,y为中心 绘制。原因就不解释了,谷歌大佬就是这么设计的。解决方法:利用paint.getTextBounds
,获得文字的矩形区域。然后在真正canvas.drawText
,计算y的时候考虑这个矩形区域,就像下面这样如下
mainRect = new Rect();
textBottomPaint.getTextBounds(text,0, text.length(), mainRect);
- 由于之前波浪的横向移动,坐标轴产生了平移,所以我绘制文字,要将平移的距离减去,再绘制,保证居中,且文字位置不随着波浪的横向移动而变化。
完整代码如下(此步骤的关键代码已经标红):
结语
来解答 乍一看里面提出的3个问题:
诶?心形是怎么绘制的?答:构建Path,然后
canvas.clipPath
裁剪画布,裁剪之后,所有的作图效果就只在这个心形区域内可见
诶?波浪是怎么画出来的,又是如何动起来的?答:波浪,或者说波浪区域,也是 Path构建,主要由一根波浪线以及三根直线组成,是一个封闭区域. 让波浪动起来,其实就是 canvas平移操作,利用属性动画+双倍宽度的波浪区域,形成无缝无限循环动画.
诶? 文字是怎么呈现出同一时刻的两种颜色的?答:在两个相邻的波浪区域,使用不一样的颜色绘制两次文字。视觉效果上还是一串文字,但是实际上是两次绘制的组合效果。神奇吗?神奇个屁,其实就是 同一位置绘制两次文字,后面的覆盖前面的......话粗理不粗- -!
话题延伸
要想随心所欲地掌控自定义View,需要有完整的知识体系。
view的树形结构概念
测量,布局,绘制流程
事件分发/滑动冲突核心原理
CanvasPaintPath绘制常用api
Bitmap位图
属性动画
如果与 系统的某些View发生交互,还有可能需要你了解 系统源码
但是要想随心所欲地使用 自定义View,仅仅如此还不够,还需要:良好的数学基础
因为大部分的不规则图形,可能都需要数学公式思想的辅助,像是:
心形path的构建
无限波浪的设计思路
后续文章将会 提到的 贝塞尔曲线的使用
都离不开多年前数学课上的时候养成的数学思维,如果数学基础比较糟糕,做起这些特效,往往会比较困难.
(顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找)
https://github.com/xiangjiana/Android-MS
相关推荐
本文将根据提供的标题“手把手教你做一个自定义表格标签”和标签“源码 工具”,探讨如何通过编写源代码创建一个自定义表格组件,并可能使用的工具。 首先,我们要理解表格标签在HTML中的基本结构,通常是`<table>`...
本示例将深入探讨如何实现一个自定义的“画圆环”功能,即创建一个能够显示进度的圆环视图。这个过程涉及到多个关键知识点,包括图形绘制、动画效果以及自定义View的基本原理。 首先,我们要理解自定义View的基本...
首先,自定义ViewGroup意味着你需要扩展`android.view.ViewGroup`类,这是一个容器,可以包含多个子视图(Views)。ViewGroup负责布局管理,包括子视图的位置和大小计算。在自定义过程中,我们通常会重写以下几个...
然而,这只是一个基础流程,实际的实现可能涉及到更复杂的逻辑,如令牌结构、命令格式的判断以及ATCop处理流程等。需要注意的是,不同高通平台的代码可能会有所差异,本示例基于SDM660平台。 总之,添加自定义AT...
文档里面是详细教程和代码,不用什么基础都可以看懂,代码复制就可以用,立马解决你创建菜单的问题。这部分是微信公众账号自定义菜单的创建【完整的】。下一次教你高级进阶,内容在文档里底部。
本教程将详细解释如何自定义一个`ExpandableListView`,让你能够根据自己的需求定制功能。 首先,我们要了解`ExpandableListView`的基本结构。它由两部分组成:父项(Group)和子项(Child)。每个父项可以包含多个...
在C#编程中,开发一个具有透明效果、自定义边框、可拖动以及可以放大缩小的窗口是一项常见的任务,特别是在构建用户界面时。本文将深入讲解如何实现这些功能。 首先,我们需要创建一个新的Windows Forms应用程序...
1. **创建JavaBean类**:首先,你需要创建一个继承自JComponent(对于Swing)或Control(对于SWT)的类。这个类应该包含至少一个公共的无参数构造函数,以便IDE能够实例化。 2. **定义属性**:为你的控件添加自定义...
你需要创建一个类,继承自`Command<TAppSession>`,其中`TAppSession`是你的应用程序会话类型,通常是`AppSession`或其子类。 - 实现`ExecuteCommand`方法,此方法负责解析接收到的字符串并执行相应的操作。在这里...
GZ-2022-信息安全管理与评估赛题(FW BC WS RS 带你手把手敲)
我们需要定义一个列表,用于存储物体的坐标值,其中总高度可以本人自定义,之后水平上的位置和垂直高度的位置都可以通过相关公式进行计算,将每次计算得到的点集追加到列表之后,之后调用作图函数进行绘制抛物线。...
在某些情况下,可能需要在同一张图表上绘制多条折线,这时只需多次调用plot函数并将它们绘制在同一个坐标系中即可。如果想要让图表更加复杂,可以使用子图功能,即创建多个子图。每个子图可以有自己的坐标轴,可以...
【标题】"222-手把手带你写一个MiniSpring" 在编程世界中,Spring框架是Java企业级应用开发的基石,它以其依赖注入(Dependency Injection, DI)和面向切面编程(Aspect-Oriented Programming, AOP)的核心特性,...
通过简单又具有一般性的案例教你快速学习catia
手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F...
内容:手把手教你springboot整合bootstrap实现自定义条件查询和模态框编辑(实战项目), 文章链接:https://nickbears.blog.csdn.net/article/details/124392266
其内存映射机制是理解DSP架构的关键,本文将基于“DSP内存映射手把手讲解”的内容,深入探讨DSP内存映射的基本原理、结构特点以及工作模式,旨在帮助读者全面掌握DSP内存管理的核心概念。 #### DSP内存映射概述 ...
这本书以其全面且易懂的特性,为读者提供了一个深入理解数字信号处理及其在嵌入式系统中应用的平台。以下将详细介绍该书涵盖的一些核心知识点。 1. **DSP基础知识**:首先,书本会介绍数字信号处理的基础概念,包括...
当你运行这段代码并输入适当的初速度和总高度值时,它将生成一个描绘平抛运动轨迹的实线抛物线图。你可以根据需要调整时间间隔和其他参数以获得更精确或更平滑的图像。 总的来说,本文不仅介绍了如何利用Python和...
tekla自定义组件