Android 自定义粒子连线动画视图实现:打造炫酷背景效果

在移动应用开发中,动态视觉效果往往能为用户带来更优质的体验。本文将解析一个自定义的粒子连线视图ParticleLineView,该视图能够展示动态移动的粒子、粒子间的连接线连线以及粒子连线视图常用于应用的背景展示、欢迎页装饰等场景,通过粒子的随机运动和连线效果营造出科技感和动态美感。

一、核心功能与结构

ParticleLineView主要包含两大核心元素:

  • 粒子(Particle):用于计算和绘制连线
  • 漂浮圆点(FloatingDot):独立的灰色圆点装饰元素

整个视图的实现遵循Android自定义View的标准流程,主要包含以下几个部分:

  1. 自定义属性定义与获取
  2. 画笔(Paint)初始化
  3. 粒子与漂浮圆点的初始化
  4. 动态更新与绘制逻辑
  5. 对外接口(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()方法中实现,主要包含三个步骤:

  1. 更新位置
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;
    }
}
  1. 绘制连线
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);
        }
    }
}
  1. 绘制漂浮圆点
java 复制代码
for (FloatingDot dot : floatingDots) {
    canvas.drawCircle(dot.x, dot.y, dot.size, dotPaint);
}
  1. 动画循环
    通过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();
}

三、使用方法

  1. 在布局文件中添加:
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"/>
  1. 在代码中动态调整:
java 复制代码
ParticleLineView particleView = findViewById(R.id.particleView);
particleView.setLineColor(Color.BLUE);
particleView.setMaxParticleSpeed(5);

四、优化建议

  1. 性能优化:当前粒子连线采用双重循环计算,当粒子数量较多时(>100)可能影响性能,可考虑使用空间分区算法减少距离计算次数。

  2. 功能扩展

    • 添加粒子点击交互效果
    • 支持粒子连线的渐变色
    • 增加粒子聚合/扩散动画
  3. 属性完善:可增加更多自定义属性,如粒子颜色、连线透明度范围等。

五、完整代码

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的绘制流程和动画循环机制,通过不断更新元素位置并触发重绘,从而形成连续的动画效果。

相关推荐
lxysbly2 小时前
安卓 PS1 模拟器,手机上也能玩经典 PlayStation 游戏
android·游戏·智能手机
Java天梯之路2 小时前
# Spring Boot 钩子全集实战(四):`SpringApplicationRunListener.environmentPrepared()` 详解
java·spring·面试
i757_w2 小时前
IDEA快捷键被占用
java·ide·intellij-idea
白鸽(二般)2 小时前
Spring 的配置文件没有小绿叶
java·后端·spring
zhangkaixuan4562 小时前
Paimon Action Jar 实现机制分析
java·大数据·flink·paimon·datalake
sheji34162 小时前
【开题答辩全过程】以 基于安卓平台的景点导游系统的设计与实现为例,包含答辩的问题和答案
android
龙之叶2 小时前
【Android Monkey源码解析一】-系统执行
android
Fate_I_C2 小时前
Kotlin 中 `@JvmField` 注解的使用
android·开发语言·kotlin
大大祥2 小时前
一个kotlin实现的视频播放器
android·开发语言·kotlin·音视频