赞
踩
我们可以定义一个粒子类:
class Particle(
var x:Float,//X坐标
var y:Float,//Y坐标
var radius:Float,//半径
var speed:Float,//速度
var alpha: Int//透明度
)
由于我们的这个效果看起来就像是水波一样的涟漪,我给自定义View起名为涟漪,也就是dimple
我们来定义这个自定义View把
class DimpleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
//定义一个粒子的集合
private var particleList = mutableListOf()
//定义画笔
var paint = Paint()
}
一开始就直接圆形生产粒子着实有些难度,我先考虑考虑如何实现生产粒子把。
先不断生产粒子,然后再考虑圆形的事情。
而且生产一堆粒子比较麻烦,我先实现从上到下生产一个粒子。
那么如何生产一个粒子呢?前面也说了,粒子就是个很小的点,所以用canvas的drawCircle就可以。
那我们来吧
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.WHITE
paint.isAntiAlias = true
var particle=Particle(0f,0f,2f,2f,100)
canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}
画画嘛,就要在onDraw方法中进行了。我们先new一个Particle,然后画出来。
实际上这样并没有什么效果。为啥呢?
我们的背景是白色的,粒子默认是白色的,你当然看不到了。所以我们需要先做个测试,为了能看出效果。这里啊,我们把背景换成黑色。同时,为了方便测试,先把Imageview设置成不可见。然后我们看下效果
没错,就是没什么效果。你什么都看不出来。
先不急,慢慢来,且听我吹,啊不,且听我和你慢慢道来。
我们在这里只花了一个圆,而且是在坐标原点画了一个半径为2的点,可以说很小很小了。自然就看不到了。
什么,你不知道原点在哪?
棕色部分就是我们的屏幕,所以原点就是左上角。
现在我们需要做的事情只有两个,要么把点变大,要么改变点的位置。
粒子粒子的,当然不能变大,所以我们把它放到屏幕中心去。
所以我们定义一个屏幕中心的坐标,centerX,centerY。并且在onSizeChanged方法中给它们赋值
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX= (w/2).toFloat()
centerY= (h/2).toFloat()
}
那我们改一下上面的画点的代码:
override fun onDraw(canvas: Canvas) {
…
var particle=Particle(centerX,centerY,2f,2f,100)
canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}
如此,可以看到这个点了,虽然很小很小,但是也胜过没有呀
可是这时候有人跳出来了,说你这不对啊,一个点有啥用?还那么小,我本来就近视,你这搞得我更看不清了。你是不是眼睛店派来的叛徒!
那好吧,我们多加几个。可是该怎么加?效果图中是圆形的,可是我不会啊,我只能先试试一横排添加。看看这样可不可以呢?我们知道,横排的话就是y值不变,x变。好,但是为了避免我们画出一条线,我们x值随机增加,这样的话看起来也比较不规则一些。
那么代码就应该是这样了
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.WHITE
paint.isAntiAlias = true
for (i in 0…50){
var random= Random()
var nextX=random.nextInt((centerX*2).toInt())
var particle=Particle(nextX.toFloat(),centerY,2f,2f,100)
canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}
}
由于centerX是屏幕的中心,所以它的值是屏幕宽度的一半,这里的话X的值就是在屏幕宽度内随机选一个值。那么效果看起来是下面这样
效果看起来不错了。
但是总有爱搞事的小伙伴又跳出来了,说你会不会写代码?onDraw方法一直被调用,不能定义对象你不知道么?很容易引发频繁的GC,造成内存抖动的。而且你这还搞个循环,性能能行不?
这个小伙伴你说的非常对,是我错了!
确实,在ondraw方法中不适合定义对象,尤其是for循环中就更不能了。段时间看,我们50个粒子好像对性能的开销不是很大,但是一旦粒子数量很多,性能开销就会十分的大。而且,为了不掉帧,我们需要在16ms之内完成绘制。这个不明白的话我后续会有性能优化的专题,可以关注一下我~
这里我们测量一下50个粒子的绘制时间和5000个粒子的绘制时间。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.WHITE
paint.isAntiAlias = true
var time= measureTimeMillis {
for (i in 0…50){
var random= Random()
var nextX=random.nextInt((centerX*2).toInt())
var particle=Particle(nextX.toFloat(),centerY,2f,2f,100)
canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}
}
Log.i(“dimple”,“绘制时间$time ms”)
}
结果如下:50个粒子的绘制时间
5000个粒子的绘制时间:
可以看到,明显超了16ms。所以我们需要优化,怎么优化?很简单,就是不在ondraw方法中创建对象就好了,那我们选择在哪里呢?
构造方法可以吗?好像不可以呢,这个时候还没办法获得屏幕宽高,嘿嘿嘿,onSizeChanged方法就决定是你了!
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX= (w/2).toFloat()
centerY= (h/2).toFloat()
val random= Random()
var nextX=0
for (i in 0…5000){
nextX=random.nextInt((centerX*2).toInt())
particleList.add(Particle(nextX.toFloat(),centerY,2f,2f,100))
}
}
我们再来看看onDraw方法中绘制时间是多少:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.WHITE
paint.isAntiAlias = true
var time= measureTimeMillis {
particleList.forEach {
canvas.drawCircle(it.x,it.y,it.radius,paint)
}
}
Log.i(“dimple”,“绘制时间$time ms”)
}
emmmm,好像是低于16ms了,可是这也太危险了吧,你这分分钟就超过了16ms啊。
确实是这样子,但是实际情况下,我们并不需要5000个这么多的粒子。又有人问,,万一真的需要怎么办?那就得看surfaceView了。这里就不讲了
我们还是回过头来,先把粒子数量变成50个。
现在粒子也有了,该实现动起来的效果了。
动起来,我们想想,应该怎么做呢?效果图是类似圆一样的扩散,我现在做不到,我往下掉这应该不难吧?
说动就动,搞起!至于怎么动,那肯定是属性动画呀。
private var animator = ValueAnimator.ofFloat(0f, 1f)
init {
animator.duration = 2000
animator.repeatCount = -1
animator.interpolator = LinearInterpolator()
animator.addUpdateListener {
updateParticle(it.animatedValue as Float)
invalidate()//重绘界面
}
}
我在这里啊,定义了一个方法updateParticle,每次动画更新的时候啊就去更新粒子的状态。
updateParticle方法应该去做什么事情呢?我们来开动小脑筋想想。
如果说是粒子不断往下掉的话,那应该是y值不断地增加就可以了,嗯,非常有道理。
我们来实现一下这个方法
private fun updateParticle(value: Float) {
particleList.forEach {
it.y += it.speed
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
…
animator.start()//别忘了启动动画
}
那我们现在来看一下效果如何
emmmm看起来有点雏形了,不过效果图里的粒子速度似乎是随机的,咱们这里是同步的呀。
没关系,我们可以让粒子的速度变成随机的速度。我们修改添加粒子这里的代码
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX = (w / 2).toFloat()
centerY = (h / 2).toFloat()
val random = Random()
var nextX = 0
var speed=0 //定义一个速度
for (i in 0…50) {
nextX = random.nextInt((centerX * 2).toInt())
speed= random.nextInt(10)+5 //速度从5-15不等
particleList.add(
Particle(nextX.toFloat(), centerY, 2f, speed.toFloat(), 100)
)
}
animator.start()
}
这是效果,看起来有点样子了。不过问题又来了,人家的粒子是一直散发的,你这个粒子怎么没了就是没了呢?
有道理,所以我觉得我们需要设置一个粒子移动的最大距离,一旦超出这个最大距离,我们啊就让它回到初始的位置。
class Particle(
var x:Float,//X坐标
var y:Float,//Y坐标
var radius:Float,//半径
var speed:Float,//速度
var alpha: Int, //透明度
var maxOffset:Float=300f//最大移动距离
)
如上,我们添加了一个最大移动距离。但是有时候我们往往最大移动距离都是固定的,所以我们这里给设置了一个默认值,如果哪个粒子想特立独行也不是不可以。
有了最大的移动距离,我们就得判定,一旦移动的距离超过了这个值,我们就让它回到起点。这个判定在哪里做呢?当然是在更新位置的地方啦
private fun updateParticle(value: Float) {
particleList.forEach {
if(it.y - centerY >it.maxOffset){
it.y=centerY //重新设置Y值
it.x = random.nextInt((centerX * 2).toInt()).toFloat() //随机设置X值
it.speed= (random.nextInt(10)+5).toFloat() //随机设置速度
}
it.y += it.speed
}
}
本来呀,我想慢慢来,先随机Y,在随机X和速度。
但是我觉得可以放在一起讲,因为一个粒子一旦超出这个最大距离,那么它就相当于被回收重新生成一个新的粒子了,而一个新的粒子,必然X,Y,速度都是重新生成的,这样才能看起来效果不错。
那我们运行起来看看效果把。
emmm似乎还不错的样子?不过人家的粒子看起来很多呀,没关系,我们这里设置成300个粒子再试试?
看起来已经不错了。那我们接下来该怎么办呢?是不是还有个透明度没搞呀。
透明度的话,我们想想该如何去设置呢?首先,应该是越远越透明,直到最大值,完全透明。这就是了,透明度和移动距离是息息相关的。
private fun updateParticle(value: Float) {
particleList.forEach {
…
//设置粒子的透明度
it.alpha= ((1f - (it.y-centerY) / it.maxOffset) * 225f).toInt()
…
}
}
override fun onDraw(canvas: Canvas) {
…
var time = measureTimeMillis {
particleList.forEach {
//设置画笔的透明度
paint.alpha=it.alpha
canvas.drawCircle(it.x, it.y, it.radius, paint)
}
}
…
}
再看一下效果。。。
看起来不错了,有点意思了哦~~不过好像不够密集,我们把粒子数量调整到500就会好很多哟。而且,不知道大家有没有发现在动画刚刚加载的时候,那个效果是很不好的。因为所有的例子起始点是一样的,速度也难免会有一样的,所以效果不是很好,只需要在添加粒子的时候,Y值也初始化即可。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
…
var nextY=0f
for (i in 0…500) {
…
//初始化Y值,这里是以起始点作为最低值,最大距离作为最大值
nextY= random.nextInt(400)+centerY
speed= random.nextInt(10)+5
particleList.add(
Particle(nextX.toFloat(), nextY, 2f, speed.toFloat(), 100)
)
}
animator.start()
}
这样一来,效果就会很好了,没有一点问题了。现在看来,似乎除了不是圆形以外,没有什么太大的问题了。那我们下一步就该思考如何让它变成圆形那样生成粒子呢?
首先这个圆形是圆,但又不能画出来。
什么意思?
就是说,虽然是圆形生成粒子,但是不能够画出来这个圆,所以这个圆只是个路径而已。
路径是什么?没错,就是Path。
熟悉的小伙伴们就知道,Path可以添加各种各样的路径,由圆,线,曲线等。所以我们这里就需要一个圆的路径。
定义一个Path,添加圆。注意,我们上面讲的性能优化,不要再onDraw中定义哦。
var path = Path()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
…
path.addCircle(centerX, centerY, 280f, Path.Direction.CCW)
…
}
在onSizeChanged中我们添加了一个圆,参数的意思我就不讲了,小伙伴应该都明白。
现在我们已经定义了这个Path,但是我们又不画,那我们该怎么办呢?
我们思考一下,我们如果想要圆形生产粒子的话,是不是得需要这个圆上的任意一点的X,Y值有了这个X,Y值,我们才能够将粒子的初始位置给确定呢?看看有没人有知道怎么确定位置啊,知道的小伙伴举手示意一下
啊,等了十几分钟也没见有小伙伴举手,看来是没人了。
好汉饶命!
我说,我说,其实就是PathMeasure这个类,它可以帮助我们得到在这个路径上任意一点的位置和方向。不会用的小伙伴赶紧谷歌一下用法吧~或者看我代码也很好理解的。
private val pathMeasure = PathMeasure()//路径,用于测量扩散圆某一处的X,Y值
private var pos = FloatArray(2) //扩散圆上某一点的x,y
private val tan = FloatArray(2)//扩散圆上某一点切线
这里我们定义了三个变量,首当其冲的就是PathMeasure类,第二个和第三个变量是一个float数组,pos是用来保存圆上某一点的位置信息的,其中pos[0]是X值,pos[1]是Y值。
第二个变量tan是某一点的切线值,你可以暂且理解为是某一点的角度。不过我们这个效果用不到,只是个凑参数的。
PathMeasure有个很重要的方法就是getPosTan方法。
boolean getPosTan (float distance, float[] pos, float[] tan)
方法各个参数释义:
| 参数 | 作用 | 备注 |
| — | — | — |
| 返回值(boolean) | 判断获取是否成功 | true表示成功,数据会存入 pos 和 tan 中, false 表示失败,pos 和 tan 不会改变 |
| distance | 距离 Path 起点的长度 | 取值范围: 0 <= distance <= getLength |
| pos | 该点的坐标值 | 当前点在画布上的位置,有两个数值,分别为x,y坐标。 |
| tan | 该点的正切值 | 当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。 |
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
详细整理在GitHub:Android架构视频+BAT面试专题PDF+学习笔记
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
*,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-1EXdcHd7-1711031966098)]
详细整理在GitHub:Android架构视频+BAT面试专题PDF+学习笔记
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-sVZBegYF-1711031966099)]
[外链图片转存中…(img-0UNPm7V4-1711031966099)]
[外链图片转存中…(img-1BEN3KYM-1711031966100)]
[外链图片转存中…(img-hW3O5uix-1711031966101)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-rPUz3eXD-1711031966101)]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。