网站首页 > 技术文章 正文
享学课堂诚邀作者:周周
转载请声明出处!
前言
手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果
如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。
学到老活到老,路漫漫其修远兮。与众君共勉 !
引子
自定义View是android高级UI知识体系的重要一环。也是区分中高级开发者的分水岭。高级开发者,知识体系完善,但凡能够语言描述出来的特效,他们总能给出解决方案。而中级开发者由于眼界受限,往往遇到复杂需求就无从下手。
一些看似复杂的特效,其实android已经为我们提供了一套解决方案,这是中级进阶高级的必学知识。
本文给出完整攻略,保证一篇入魂。= =!
github地址:[下载之后找到其中的 DrawOverlayDemoView]
https://github.com/18598925736/UiDrawTest
效果图
下图中可以看到,首先我们看到了一个心形,然后有波浪在跳动,最后绿色填满了整个心形
大灰狼变绿了
乍一看
诶?心形是怎么绘制的?
诶?波浪是怎么画出来的,又是如何动起来的?
诶? 文字是怎么呈现出同一时刻的两种颜色的?
不知道是不是有人有这样的疑惑````请继续往下看.
效果拆解
拿到一个复杂特效,第一件事不要慌,先仔细分析一下,这个特效里面具体有哪些细节可以拆分出来。复杂的东西都是由简单的细节 组合而成。
开始拆解。
1、绘制区域是一个心形
2、波浪从最下面开始,逐渐用绿色填充了整个心形
3、中间有文字内容“一条大灰狼”,并且在波浪增长的过程中,文字存在一段时间的上下两部分颜色不同的状态.
本案例用到的知识点:
1、canvas.clipPath 画布裁剪
2、canvas.save 画布状态保存
3、canvas.restore 恢复
4、canvas.translate 画布平移
6、path.rCubicTo 构建三阶贝塞尔曲线(相当于上一个点位置)
5、属性动画 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, Path path) { 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步:绘制”一条大灰狼“ 到心形中央,并且达成双色效果
这里有两个细节:
1.canvas.drawText, 就算你把paint 设置了.setTextAlign(Paint.Align.CENTER); 它也未必会在你给的x,y为中心 绘制。原因就不解释了,谷歌大佬就是这么设计的。
解决方法:利用paint.getTextBounds,获得文字的矩形区域。然后在真正canvas.drawText,计算y的时候考虑这个矩形区域,就像下面这样如下
mainRect = new Rect(); textBottomPaint.getTextBounds(text, 0, text.length(), mainRect);
2. 由于之前波浪的横向移动,坐标轴产生了平移,所以我绘制文字,要将平移的距离减去,再绘制,保证居中,且文字位置不随着波浪的横向移动而变化。
完整代码如下(此步骤的关键代码已经标红):
结语
来解答乍一看里面提出的3个问题:
诶?心形是怎么绘制的?
答:构建Path,然后canvas.clipPath裁剪画布,裁剪之后,所有的作图效果就只在这个心形区域内可见
诶?波浪是怎么画出来的,又是如何动起来的?
答:波浪,或者说波浪区域,也是Path构建,主要由一根波浪线以及三根直线组成,是一个封闭区域.
让波浪动起来,其实就是 canvas平移操作,利用属性动画+双倍宽度的波浪区域,形成无缝无限循环动画.
诶? 文字是怎么呈现出同一时刻的两种颜色的?
答:在两个相邻的波浪区域,使用不一样的颜色绘制两次文字。视觉效果上还是一串文字,但是实际上是两次绘制的组合效果。神奇吗?神奇个屁,其实就是 同一位置绘制两次文字,后面的覆盖前面的......话粗理不粗- -!
如果你的技术提升遇到瓶颈了,或者缺高级Android进阶视频学习提升自己,这有大量大厂面试题为你面试做准备!可以私聊我获取喔!
话题延伸
要想随心所欲地掌控自定义View,需要有完整的知识体系。
- view的树形结构概念
- 测量,布局,绘制流程
- 事件分发/滑动冲突核心原理
- Canvas Paint Path 绘制常用api
- Bitmap 位图
- 属性动画
- 如果与系统的某些View发生交互,还有可能需要你了解系统源码
但是要想随心所欲地使用自定义View,仅仅如此还不够,还需要: 良好的数学基础
因为大部分的不规则图形,可能都需要数学公式思想的辅助,像是:
- 心形path的构建
- 无限波浪的设计思路
- 后续文章将会 提到的贝塞尔曲线的使用
都离不开多年前数学课上的时候养成的数学思维,如果数学基础比较糟糕,做起这些特效,往往会比较困难.
你的赞和关注是我继续创作的动力~
- 上一篇: 仿小红书点赞红心效果实战- Android
- 下一篇: 前沿穿越!聊聊HTML5小游戏的制作技巧及经验
猜你喜欢
- 2024-10-24 初探animation中steps()属性(animation steps属性)
- 2024-10-24 HTML5(九)——超强的 SVG 动画(htmlsvg动画代码)
- 2024-10-24 自定义日历(二)(自定义日历控件)
- 2024-10-24 Flutter简单动画Animation运用(flutter 视频教程)
- 2024-10-24 css3中动画animation中的steps()函数
- 2024-10-24 移动端渲染原理浅析(移动端渲染原理浅析设计)
- 2024-10-24 iOS 事件处理机制与图像渲染过程(简述ios中的事件响应机制)
- 2024-10-24 Android 开机问题分析(android无法开机)
- 2024-10-24 GoogleCTF + zer0ptsCTF + ImaginaryCTF 2023 笔记
- 2024-10-24 决战“金三银四”,中高级Web前端大厂面试秘籍:CSS篇
- 11-26Win7\8\10下一条cmd命令可查得笔记本电脑连接过的Wifi密码
- 11-26一文搞懂MySQL行锁、表锁、间隙锁详解
- 11-26电脑的wifi密码忘记了?一招教你如何找回密码,简单明了,快收藏
- 11-26代码解决忘记密码问题 教你用CMD命令查看所有连接过的WIFI密码
- 11-26CMD命令提示符能干嘛?这些功能你都知道吗?
- 11-26性能测试之慢sql分析
- 11-26论渗透信息收集的重要性
- 11-26如何查看电脑连接过的所有WiFi密码
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)