如果我们需要在屏幕上增加轨迹特效,也就是当用户在界面上的滑动时需要有一个跟随手指滑动轨迹,有各种粒子飞舞扩散的效果,最终的实现效果如图(gif图片超出限制无法上传),这里我们使用的素材贴图是星星。当然我们要求是这个粒子的贴图可更换,所以我们需要自定义一个控件来支持这个特效。
整个轨迹粒子实现的大致示意图如上,T是当前用户在屏幕上触摸的轨迹,P1/P2/P3对应的是我们在用户触摸轨迹上获取的点,获取到这些点后,我们会在这个触摸点对应的区域A1/A2/A3区域中生成对应的随机点,这些随机点其实就是我们粒子的起始点,这时候我们就会在这个位置生成一个粒子,随后粒子Px又开始生成他的扩散轨迹Tx并沿着轨迹进行飞舞,这样看到的就是触摸之后生成一个粒子飞舞的轨迹特效,整个的实现原理是这样的。那实现这个效果需要的是三个步骤:记录轨迹、生成粒子,粒子飞舞。
记录轨迹,记录轨迹其实就是要记录用户手指的滑动轨迹,在View的OnTouch方法中或者Move事件,然后获取相应的触摸点的坐标(X,Y),如下代码示例,这里面我们在Move的时候获取坐标点,然后会去获取当前坐标,Math.abs(x
- X) >=
BORDER,这边这个处理x当前的触摸点坐标X,X是我们上次的触摸点坐标X,那么求绝对值后看是否大于等于BORDER,这个也就是为了避免手指很微小的抖动也会绘制新的粒子束,所以增加了一个阈值BORDER,小于这个阈值我们不去产生新的粒子束。避免粒子束过于密集,同时满足条件后,调用traceBloom产生粒子束,生成粒子动画。
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case
MotionEvent.ACTION_DOWN:
case
MotionEvent.ACTION_MOVE:
if (Math.abs(x - X) >=
BORDER || Math.abs(y - Y) >= BORDER) {
X = x;
Y = y;
traceBloom();
}
break;
case
MotionEvent.ACTION_UP:
break;
default:
return
super.onTouchEvent(event);
}
return true;
}
生成粒子,获取轨迹后开始生成粒子,我们记录了轨迹点这边获取到前一个触摸点(preX,preY),和当前的触摸点(X,Y),这边会去生成一个粒子生成区域,如上图的A1/A2/A3区域,对应的就是相应的粒子束生成区域,这边会根据轨迹方向生成对应的Rect,然后第一个触摸点由于没有前一个触摸点,所以会以其为中心点生成一个正方形粒子区域。区域构建完成后我们开始生成对应粒子的动画,这边同步将粒子的贴图数组mParticleResList传递过去。相关示例代码如下:
public void traceBloom() {
float offsetX = X -
preX;
float offsetY = Y - preY;
double range = Math.sqrt(regionWidth *
regionWidth / 2);
Rect rect = new Rect((int)(X - range), (int)(Y -
range), (int)(X + range), (int)(Y + range));
if(offsetX > 0 && offsetY > 0)
{
rect.left
= (int)preX;
rect.top =
(int)preY;
rect.bottom = (int)Y;
rect.right
= (int)X;
} else if(offsetX > 0 && offsetY <
0) {
rect.left
= (int)preX;
rect.top =
(int)Y;
rect.bottom = (int)preY;
rect.right
= (int)X;
} else if(offsetX < 0 && offsetY >
0) {
rect.left
= (int)X;
rect.top =
(int)preY;
rect.bottom = (int)Y;
rect.right
= (int)preX;
} else if(offsetX < 0 && offsetY <
0) {
rect.left
= (int)X;
rect.top =
(int)Y;
rect.bottom = (int)preY;
rect.right
= (int)preX;
}
preX = X;
preY = Y;
final TraceAnimation animator = new
TraceAnimation(this, mParticleResList, rect , new Point((int)X,
(int)Y));
traceAnimators.add(animator);
animator.addListener(new
AnimatorListenerAdapter() {
@Override
public
void onAnimationStart(Animator animation) {
}
@Override
public
void onAnimationEnd(Animator animation) {
//动画结束时从动画集中移除
traceAnimators.remove(animation);
animation = null;
}
});
animator.start();
}
构建粒子束,默认的我们会在上面产生的区域中随机生成粒子,为了保证粒子的数量,默认的最低粒子数我们设计为4个,最多可达七个,也就是触摸点区域会随机生成4~7个粒子,然后就是开始构建粒子,以下是构建粒子束的代码示例:
private Particle[] generateParticles(ArrayList bitmaps, Rect
bound, Point center) {
int w = bound.width();
int h = bound.height();
int count = mRandom.nextInt(4) +
4;//随机粒子个数
Particle[] particles = new
Particle[count];
for (int index = 0; index < count; index ++)
{ //生成粒子
//粒子的颜色
int color
= Color.argb(255, mRandom.nextInt(128) + 128, mRandom.nextInt(128)
+ 128, mRandom.nextInt(128) + 128);
Point
point = Particle.getRandomPoint(bound.left + w / 2, bound.top + h /
2, w / 2, h / 2);
Math.sqrt(w * w + h * h);
Rect
distance = new Rect(point.x - DISTANCE , point.y - DISTANCE ,
point.x + DISTANCE, point.y + DISTANCE);
Bitmap
particle = null;
if(bitmaps
!= null && bitmaps.size() > 0) {
particle =
bitmaps.get(mRandom.nextInt(bitmaps.size()));
}
particles[index] = Particle.generateParticle(particle, color,
distance, Particle.getRandomPoint(center.x, center.y, 15,
15));
}
return particles;
}
对于一个粒子,他的基本属性我们定义为:默认粒子大小、粒子轨迹(起点、控制点1、控制点2、终点,这里使用的是三次贝塞尔曲线)、粒子贴图、粒子透明度、粒子颜色,如下:
private static final int
DEFAULT_WIDTH = 16;
Point start; // Bezier
start point
Point end;// Bezier end
point
Point c1;// Bezier
control point
Point c2;// Bezier
control point
float cx; //center x of
particle
float cy; //center y of
particle
Bitmap bitmap;
int color;
粒子扩散,粒子扩散其实主要是根据animation来的,我们在animation执行过程中,每次draw的时候,调用粒子的advance方法,改变粒子的相关属性(位置、透明度等),然后将其draw出来,最后invalid刷新。
public void draw(Canvas
canvas) {
if(!isStarted()) { //动画结束时停止
return;
}
for (Particle p : mParticles) { //绘制粒子
p.advance((Float) getAnimatedValue());
mPaint.setAntiAlias(true);
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(p.color);
mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
if(p.bitmap != null) {
canvas.drawBitmap(p.bitmap,
p.cx - p.radius / 2, p.cy - p.radius / 2, mPaint);
} else
{
// default particle
canvas.drawRect(p.cx -
p.radius / 2, p.cy - p.radius / 2, p.cx + p.radius / 2 ,p.cy +
p.radius / 2, mPaint);
}
}
mContainer.invalidate();
}
public void
advance(float factor) {
Point center = CalculateBezierPoint(factor,
start, c1, c2, end);
cx = center.x;
cy = center.y;
radius = radius * (1f - factor);
alpha = 1f - factor;
}
private Point
CalculateBezierPoint(float t, Point s, Point c1, Point c2, Point
e)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Point p = new Point((int) (s.x * uuu), (int)
(s.y * uuu));
p.x += 3 * uu * t * c1.x;
p.y += 3 * uu * t * c1.y;
p.x += 3 * u * tt * c2.x;
p.y += 3 * u * tt * c2.y;
p.x += ttt * e.x;
p.y += ttt * e.y;
return p;
}
完成了上面的这些内容之后,我们就能看到一个大概的效果了,这边控制粒子的密度和扩散的范围主要是通过生成了粒子数量和生成的区域来进行限制的,还有一点就是关于内存泄漏的问题,当用户一直滑动,导致粒子数量变多的时候就会导致占用内存增长,每个粒子其实都带着一直贴图的bitmap,当增长到一定值未及时回收就会出现崩溃,引发内存泄漏的问题。这里的处理是要及时的对bitmap进行回收,由于我们粒子是随机产生的,每次粒子完成了自己的整个过程是需要对这个粒子进行回收。当然我们在使用的bitmap贴图资源上也要保证图片的大小,这个方面也可进行适当的控制。