在移动应用开发中,动态视觉效果往往能为用户带来更优质的体验。本文将解析一个自定义的粒子连线视图ParticleLineView,该视图能够展示动态移动的粒子、粒子间的连接线连线以及粒子连线视图常用于应用的背景展示、欢迎页装饰等场景,通过粒子的随机运动和连线效果营造出科技感和动态美感。
一、核心功能与结构
ParticleLineView主要包含两大核心元素:
- 粒子(Particle):用于计算和绘制连线
- 漂浮圆点(FloatingDot):独立的灰色圆点装饰元素
整个视图的实现遵循Android自定义View的标准流程,主要包含以下几个部分:
- 自定义属性定义与获取
- 画笔(Paint)初始化
- 粒子与漂浮圆点的初始化
- 动态更新与绘制逻辑
- 对外接口(Setter方法)
二、实现细节解析
1. 自定义属性配置
为了使视图具备灵活性,通过attrs.xml定义了一系列可配置属性:
xml
<!-- 示例属性定义 -->
<declare-styleable name="ParticleLineView">
<attr name="particleCount" format="integer"/>
<attr name="particleSize" format="dimension"/>
<attr name="lineColor" format="color"/>
<!-- 更多属性... -->
</declare-styleable>
在代码中通过TypedArray获取这些属性值:
java
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.ParticleLineView);
particleCount = ta.getInt(R.styleable.ParticleLineView_particleCount, particleCount);
particleSize = ta.getDimension(R.styleable.ParticleLineView_particleSize, particleSize);
// 其他属性获取...
ta.recycle();
2. 画笔初始化
分别为线条和漂浮圆点创建了不同的画笔:
java
// 线条画笔配置
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineWidth);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
// 圆点画笔配置
dotPaint.setColor(dotColor);
dotPaint.setStyle(Paint.Style.FILL);
dotPaint.setAntiAlias(true);
3. 粒子系统初始化
等待视图尺寸确定后(通过OnGlobalLayoutListener),初始化粒子和漂浮圆点:
java
// 粒子初始化
private void initParticles() {
particles.clear();
int width = getWidth();
int height = getHeight();
for (int i = 0; i < particleCount; i++) {
Particle particle = new Particle();
particle.x = random.nextInt(width);
particle.y = random.nextInt(height);
// 随机速度(-max ~ max)
particle.vx = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particle.vy = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particles.add(particle);
}
}
漂浮圆点的初始化逻辑类似,但增加了大小随机化:
java
dot.size = minDotSize + random.nextFloat() * (maxDotSize - minDotSize);
4. 动态绘制逻辑
核心逻辑在onDraw()方法中实现,主要包含三个步骤:
- 更新位置:
java
// 更新粒子位置并检测边界
for (Particle particle : particles) {
particle.x += particle.vx;
particle.y += particle.vy;
// 边界反弹
if (particle.x < 0 || particle.x > width) {
particle.vx = -particle.vx;
}
if (particle.y < 0 || particle.y > height) {
particle.vy = -particle.vy;
}
}
- 绘制连线:
java
// 只绘制距离小于maxLineDistance的粒子间连线
for (int i = 0; i < particles.size(); i++) {
Particle p1 = particles.get(i);
for (int j = i + 1; j < particles.size(); j++) {
Particle p2 = particles.get(j);
float distance = calculateDistance(p1, p2);
if (distance < maxLineDistance) {
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, linePaint);
}
}
}
- 绘制漂浮圆点:
java
for (FloatingDot dot : floatingDots) {
canvas.drawCircle(dot.x, dot.y, dot.size, dotPaint);
}
- 动画循环 :
通过postInvalidateDelayed(16)实现约60fps的动画效果:
java
// 重绘(继续动画)
postInvalidateDelayed(16);
5. 对外接口设计
提供了一系列Setter方法允许动态调整视图效果:
java
// 调整线条颜色
public void setLineColor(int color) {
this.lineColor = color;
linePaint.setColor(color);
invalidate();
}
// 调整粒子速度
public void setMaxParticleSpeed(int speed) {
this.maxParticleSpeed = speed;
// 更新所有粒子速度
for (Particle particle : particles) {
particle.vx = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particle.vy = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
}
invalidate();
}
三、使用方法
- 在布局文件中添加:
xml
<com.example.ddddddddd.ParticleLineView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:particleCount="50"
app:lineColor="@color/gray"
app:maxLineDistance="250"/>
- 在代码中动态调整:
java
ParticleLineView particleView = findViewById(R.id.particleView);
particleView.setLineColor(Color.BLUE);
particleView.setMaxParticleSpeed(5);
四、优化建议
-
性能优化:当前粒子连线采用双重循环计算,当粒子数量较多时(>100)可能影响性能,可考虑使用空间分区算法减少距离计算次数。
-
功能扩展:
- 添加粒子点击交互效果
- 支持粒子连线的渐变色
- 增加粒子聚合/扩散动画
-
属性完善:可增加更多自定义属性,如粒子颜色、连线透明度范围等。
五、完整代码
java
package com.example.ddddddddd;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ParticleLineView extends View {
/*** 粒子列表 用于计算线条位置 */
private List<Particle> particles = new ArrayList<>();
/*** 漂浮的灰色圆点列表 */
private List<FloatingDot> floatingDots = new ArrayList<>();
/*** 画笔 */
private Paint linePaint = new Paint();
/*** 灰色圆点画笔 */
private Paint dotPaint = new Paint();
/*** 粒子属性 */
private int particleCount = 50;
private float particleSize = 4f;
/*** 线条属性 */
private int lineColor = Color.GRAY;
private float lineWidth = 1f;
private int maxLineDistance = 250;
/*** 粒子移动速度 */
private int maxParticleSpeed = 3;
/*** 圆点数量 */
private int dotCount = 30;
/*** 最小圆点大小 */
private float minDotSize = 2f;
/*** 最大圆点大小 */
private float maxDotSize = 6f;
/*** 圆点颜色 */
private int dotColor = Color.GRAY;
/*** 圆点最大移动速度 */
private int maxDotSpeed = 2;
private Random random = new Random();
private boolean isInitialized = false;
public ParticleLineView(Context context) {
super(context);
init(null);
}
public ParticleLineView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public ParticleLineView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
// 读取自定义属性
if (attrs != null) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.ParticleLineView);
particleCount = ta.getInt(R.styleable.ParticleLineView_particleCount, particleCount);
particleSize = ta.getDimension(R.styleable.ParticleLineView_particleSize, particleSize);
lineColor = ta.getColor(R.styleable.ParticleLineView_lineColor, lineColor);
lineWidth = ta.getDimension(R.styleable.ParticleLineView_lineWidth, lineWidth);
maxLineDistance = ta.getInt(R.styleable.ParticleLineView_maxLineDistance, maxLineDistance);
maxParticleSpeed = ta.getInt(R.styleable.ParticleLineView_maxParticleSpeed, maxParticleSpeed);
// 灰色圆点的自定义属性
dotCount = ta.getInt(R.styleable.ParticleLineView_dotCount, dotCount);
minDotSize = ta.getDimension(R.styleable.ParticleLineView_minDotSize, minDotSize);
maxDotSize = ta.getDimension(R.styleable.ParticleLineView_maxDotSize, maxDotSize);
dotColor = ta.getColor(R.styleable.ParticleLineView_dotColor, dotColor);
maxDotSpeed = ta.getInt(R.styleable.ParticleLineView_maxDotSpeed, maxDotSpeed);
ta.recycle();
}
// 配置线条画笔 - 设置固定不透明
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineWidth);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
linePaint.setAlpha(255); // 强制设置为完全不透明
// 配置灰色圆点画笔
dotPaint.setColor(dotColor);
dotPaint.setStyle(Paint.Style.FILL);
dotPaint.setAntiAlias(true);
// 等待视图尺寸确定后初始化
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (!isInitialized) {
initParticles();
initFloatingDots();
isInitialized = true;
// 移除监听器
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// 开始动画
postInvalidateDelayed(16);
}
}
});
}
// 初始化粒子(用于计算线条位置)
private void initParticles() {
particles.clear();
int width = getWidth();
int height = getHeight();
for (int i = 0; i < particleCount; i++) {
Particle particle = new Particle();
particle.x = random.nextInt(width);
particle.y = random.nextInt(height);
particle.vx = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particle.vy = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particles.add(particle);
}
}
// 初始化漂浮的灰色圆点
private void initFloatingDots() {
floatingDots.clear();
int width = getWidth();
int height = getHeight();
for (int i = 0; i < dotCount; i++) {
FloatingDot dot = new FloatingDot();
dot.x = random.nextInt(width);
dot.y = random.nextInt(height);
dot.size = minDotSize + random.nextFloat() * (maxDotSize - minDotSize);
dot.vx = (random.nextFloat() - 0.5f) * 2 * maxDotSpeed;
dot.vy = (random.nextFloat() - 0.5f) * 2 * maxDotSpeed;
floatingDots.add(dot);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (particles.isEmpty() || floatingDots.isEmpty()) {
return;
}
int width = getWidth();
int height = getHeight();
// 更新粒子位置
for (Particle particle : particles) {
particle.x += particle.vx;
particle.y += particle.vy;
// 边界检测
if (particle.x < 0 || particle.x > width) {
particle.vx = -particle.vx;
}
if (particle.y < 0 || particle.y > height) {
particle.vy = -particle.vy;
}
particle.x = Math.max(0, Math.min(particle.x, width));
particle.y = Math.max(0, Math.min(particle.y, height));
}
// 更新灰色圆点位置
for (FloatingDot dot : floatingDots) {
dot.x += dot.vx;
dot.y += dot.vy;
// 边界检测
if (dot.x < 0 || dot.x > width) {
dot.vx = -dot.vx;
}
if (dot.y < 0 || dot.y > height) {
dot.vy = -dot.vy;
}
dot.x = Math.max(0, Math.min(dot.x, width));
dot.y = Math.max(0, Math.min(dot.y, height));
}
// 绘制连接线 - 移除透明度变化,保持统一宽度和颜色
for (int i = 0; i < particles.size(); i++) {
Particle p1 = particles.get(i);
for (int j = i + 1; j < particles.size(); j++) {
Particle p2 = particles.get(j);
float distance = calculateDistance(p1, p2);
if (distance < maxLineDistance) {
// 移除随距离变化的透明度设置,保持画笔初始设置
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, linePaint);
}
}
}
// 绘制漂浮的灰色圆点
for (FloatingDot dot : floatingDots) {
canvas.drawCircle(dot.x, dot.y, dot.size, dotPaint);
}
// 重绘(继续动画)
postInvalidateDelayed(16);
}
// 计算两个粒子之间的距离
private float calculateDistance(Particle p1, Particle p2) {
float dx = p1.x - p2.x;
float dy = p1.y - p2.y;
return (float) Math.sqrt(dx * dx + dy * dy);
}
// 粒子类(用于计算线条位置)
private class Particle {
float x;
float y;
float vx; // x方向速度
float vy; // y方向速度
}
// 漂浮的灰色圆点类
private class FloatingDot {
float x;
float y;
float size; // 圆点大小
float vx; // x方向速度
float vy; // y方向速度
}
// Setter方法 - 保持原有的调整方法
public void setParticleCount(int count) {
this.particleCount = count;
initParticles();
invalidate();
}
public void setParticleSize(float size) {
this.particleSize = size;
invalidate();
}
// 调整线条颜色
public void setLineColor(int color) {
this.lineColor = color;
linePaint.setColor(color);
invalidate();
}
// 调整线条宽度
public void setLineWidth(float width) {
this.lineWidth = width;
linePaint.setStrokeWidth(width);
invalidate();
}
public void setMaxLineDistance(int distance) {
this.maxLineDistance = distance;
invalidate();
}
public void setMaxParticleSpeed(int speed) {
this.maxParticleSpeed = speed;
for (Particle particle : particles) {
particle.vx = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
particle.vy = (random.nextFloat() - 0.5f) * 2 * maxParticleSpeed;
}
invalidate();
}
// 灰色圆点的控制方法
public void setDotCount(int count) {
this.dotCount = count;
initFloatingDots();
invalidate();
}
public void setDotColor(int color) {
this.dotColor = color;
dotPaint.setColor(color);
invalidate();
}
public void setMaxDotSpeed(int speed) {
this.maxDotSpeed = speed;
for (FloatingDot dot : floatingDots) {
dot.vx = (random.nextFloat() - 0.5f) * 2 * maxDotSpeed;
dot.vy = (random.nextFloat() - 0.5f) * 2 * maxDotSpeed;
}
invalidate();
}
}
六、总结
ParticleLineView通过面向对象的设计思想,将粒子和漂浮圆点封装为独立的数据结构,实现了一个可配置、可扩展的动态粒子连线效果。该实现遵循了Android自定义View的最佳实践,通过属性配置和Setter方法提供了良好的灵活性,适合作为应用中的动态背景使用。
这种动态视觉效果的实现核心在于理解View的绘制流程和动画循环机制,通过不断更新元素位置并触发重绘,从而形成连续的动画效果。