Bootstrap

android 动态背景_Android 实现动态背景“五彩蛛网”特效,让你大开眼界

本文由 文淑 授权投稿
作者公众号:「控件人生」

01 前言

《都挺好》迎来了大结局,相信看哭了很多人。在大结局中,所有之前让人气的牙痒痒的人设,比如 “你们太让我失望” 的苏明哲,还有妈宝男苏明成,包括一天不作就难受的苏大强,最终都成功洗白。一家人最终化解恩怨,和和气气的过日子。还有谁也喜欢《都挺好》这部剧吗?

在剧中,苏明哲同我们一样也是一名程序员,一味地迁就老爹,搞得最后差点与老婆离婚,看来程序员不能一根筋啊。转变下思维来看看网页版动态背景「五彩蛛网」是怎么实现的?

先来看看效果图:

bedacf9ff653bf344ad515bbd3d61ea7.gif

249b607e6965528c0bb5033d39ef4112.gif

02 初步分析

在效果图中,可以看到许多「小点」在屏幕中匀速运动并与「邻近的点」相连,每条连线的颜色随机,「小点」触碰到屏幕边缘则回弹;还有一个效果就是,手指在屏幕中移动、拖拽,与手指触摸点连线的点向触摸点靠拢。何为「邻近的点」,与某点的距离小于特定的阈值的点称为「邻近的点」。

提到运动,「运动」在物理学中指物体在空间中的相对位置随着时间而变化。

那么大家还记得「位移」与「速度」公式吗?

1位移 = 初位移 + 速度 * 时间
2速度 = 初速度 + 加速度

时间、位移、速度、加速度构成了现代科学的运动体系。我们使用 view 来模拟物体的运动。

  • 时间:在 view 的 onDraw 方法中调用 invalidate 方法,达到无限刷新来模拟时间流,每次刷新间隔,记为:1U

  • 位移:物体在屏幕中的像素位置,每个像素距离为:1px

  • 速度:默认设置一个值,单位(px / U)

  • 加速度:默认设置一个值,单位(px / U^2)

模拟「蛛网点」物体类:

 1public class SpiderPoint extends Point {
2
3    // x 方向加速度
4    public int aX;
5
6    // y 方向加速度
7    public int aY;
8
9    // 小球颜色
10    public int color;
11
12    // 小球半径
13    public int r;
14
15    // x 轴方向速度
16    public float vX;
17
18    // y 轴方向速度
19    public float vY;
20
21    // 点
22    public float x;
23    public float y;
24
25    public SpiderPoint(int x, int y) {
26        super(x, y);
27    }
28}
蛛网点匀速直线运动

搭建测试 View,初始位置 (0,0) ,x 方向速度 10、y 方向速度 0 的蛛网点:

  1public class MoveView extends View {
2
3    // 画笔
4    private Paint mPointPaint;
5    // 蛛网点对象(类似小球)
6    private SpiderPoint mSpiderPoint;
7    // 坐标系
8    private Point mCoordinate;
9
10    // 蛛网点 默认小球半径
11    private int pointRadius = 20;
12    // 默认颜色
13    private int pointColor = Color.RED;
14    // 默认x方向速度
15    private float pointVX = 10;
16    // 默认y方向速度
17    private float pointVY = 0;
18    // 默认 小球加速度
19    private int pointAX = 0;
20    private int pointAY = 0;
21
22    // 是否开始运动
23    private boolean startMove = false;
24
25    public MoveView(Context context) {
26        this(context, null);
27    }
28
29    public MoveView(Context context, @Nullable AttributeSet attrs) {
30        this(context, attrs, 0);
31    }
32
33    public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
34        super(context, attrs, defStyleAttr);
35        initData();
36        initPaint();
37    }
38
39    private void initData() {
40        mCoordinate = new Point(500, 500);
41        mSpiderPoint = new SpiderPoint();
42        mSpiderPoint.color = pointColor;
43        mSpiderPoint.vX = pointVX;
44        mSpiderPoint.vY = pointVY;
45        mSpiderPoint.aX = pointAX;
46        mSpiderPoint.aY = pointAY;
47        mSpiderPoint.r = pointRadius;
48    }
49
50    // 初始化画笔
51    private void initPaint() {
52        mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
53        mPointPaint.setColor(pointColor);
54    }
55
56    @Override
57    protected void onDraw(Canvas canvas) {
58        super.onDraw(canvas);
59
60        canvas.save();
61        canvas.translate(mCoordinate.x, mCoordinate.y);
62        drawSpiderPoint(canvas, mSpiderPoint);
63        canvas.restore();
64
65        // 刷新视图 再次调用onDraw方法模拟时间流
66        if (startMove) {
67            updateBall();
68            invalidate();
69        }
70    }
71
72    /** 73     * 绘制蛛网点 74     * 75     * @param canvas 76     * @param spiderPoint 77     */
78    private void drawSpiderPoint(Canvas canvas, SpiderPoint spiderPoint) {
79        mPointPaint.setColor(spiderPoint.color);
80        canvas.drawCircle(spiderPoint.x, spiderPoint.y, spiderPoint.r, mPointPaint);
81    }
82
83    /** 84     * 更新小球 85     */
86    private void updateBall() {
87        //TODO --运动数据都由此函数变换
88    }
89
90    @Override
91    public boolean onTouchEvent(MotionEvent event) {
92        switch (event.getAction()) {
93            case MotionEvent.ACTION_DOWN:
94                // 开启时间流
95                startMove = true;
96                invalidate();
97                break;
98            case MotionEvent.ACTION_UP:
99                // 暂停时间流
100                startMove = false;
101                invalidate();
102                break;
103        }
104        return true;
105    }
106}

1、水平运行运动:

cb57514de9babfadfecb15bdc9a8a83c.gif

根据上文中的位移公式,位移 = 初位移 + 速度 * 时间 ,这里的时间为 1U,更新小球位置的相关代码如下:

1    /**2     * 更新小球3     */
4    private void updateBall() {
5        //TODO --运动数据都由此函数变换
6        mSpiderPoint.x += mSpiderPoint.vX;
7    }

2、回弹效果

回弹,速度取反,x 轴方向大于 400 则回弹:

8d5624eaec00ad72d352ec263812742c.gif

3、无限回弹,回弹变色

5e048501bb4b8d10676d4bdcb9578689.gif

相关代码如下:

 1    /** 2     * 更新小球 3     */
4    private void updateBall() {
5        //TODO --运动数据都由此函数变换
6        mSpiderPoint.x += mSpiderPoint.vX;
7        if (mSpiderPoint.x > 400) {
8            // 更改颜色
9            mSpiderPoint.color = randomRGB();
10            mSpiderPoint.vX = -mSpiderPoint.vX;
11        }
12        if (mSpiderPoint.x -400) {
13            mSpiderPoint.vX = -mSpiderPoint.vX;
14            // 更改颜色
15            mSpiderPoint.color = randomRGB();
16        }
17    }

randomRGB 方法的代码如下:

1    /**2     * @return 获取到随机颜色值3     */
4    private int randomRGB() {
5        Random random = new Random();
6        return Color.rgb(random.nextInt(255), random.nextInt(255), random.nextInt(255));
7    }

3、箱式弹跳

小球在 y 轴方向的平移与 x 轴方向的平移一致,这里不再讲解,看一下 x ,y 轴同时具有初速度,即速度斜向的情况。

60189c25faaaee86769e517f35507165.png

改变 y 轴方向初速度:

1    // 默认y方向速度
2    private float pointVY = 6;

在 updateBall 方法中增加对 y 方向的修改:

 1    /** 2     * 更新小球 3     */
4    private void updateBall() {
5        //TODO --运动数据都由此函数变换
6        mSpiderPoint.x += mSpiderPoint.vX;
7        mSpiderPoint.y += mSpiderPoint.vY;
8        if (mSpiderPoint.x > 400) {
9            // 更改颜色
10            mSpiderPoint.color = randomRGB();
11            mSpiderPoint.vX = -mSpiderPoint.vX;
12        }
13        if (mSpiderPoint.x -400) {
14            mSpiderPoint.vX = -mSpiderPoint.vX;
15            // 更改颜色
16            mSpiderPoint.color = randomRGB();
17        }
18
19        if (mSpiderPoint.y > 400) {
20            // 更改颜色
21            mSpiderPoint.color = randomRGB();
22            mSpiderPoint.vY = -mSpiderPoint.vY;
23        }
24        if (mSpiderPoint.y -400) {
25            mSpiderPoint.vY = -mSpiderPoint.vY;
26            // 更改颜色
27            mSpiderPoint.color = randomRGB();
28        }
29    }

效果如下图:

1c4dab7a2c5285ce9ebe9479b7538b82.gif

蛛网「小点」并没有涉及到变速运动,有关变速运动可以链接以下地址进行查阅:

Android原生绘图之让你了解View的运动

03 构思代码

通过观察网页「蛛网」动态效果,可以细分为以下几点:

  • 绘制一定数量的小球(蛛网点)

  • 小球斜向运动(具有 x,y 轴方向速度),越界回弹

  • 遍历所有小球,若小球 A 与其他小球的距离小于一定值,则两小球连线,反之则不连线

  • 若小球 A 先与小球 B 连线,为了提高性能,防止过度绘制,小球 B 不再与小球 A 连线

  • 在手指触摸点绘制小球,同连线规则一致,连线其他小球,若手指移动,连线的所有小球向触摸点靠拢

接下来,具体看看代码该怎么写。

04 编写代码
起名字

取名是一门学问,好的名字能够让你记忆犹新,那就叫 SpiderWebView (蛛网控件)。

创建SpiderWebView

先是成员变量:

 1    // 控件宽高
2    private int mWidth;
3    private int mHeight;
4    // 画笔
5    private Paint mPointPaint;
6    private Paint mLinePaint;
7    private Paint mTouchPaint;
8    // 触摸点坐标
9    private float mTouchX = -1;
10    private float mTouchY = -1;
11    // 数据源
12    private List mSpiderPointList;13    // 相关参数配置14    private SpiderConfig mConfig;15    // 随机数16    private Random mRandom;17    // 手势帮助类 用于处理滚动与拖拽18    private GestureDetector mGestureDetector;

然后是构造函数:

 1    // view 的默认构造函数 参数不做讲解
2    public SpiderWebView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
3        super(context, attrs, defStyleAttr);
4        // setLayerType(LAYER_TYPE_HARDWARE, null);
5        mSpiderPointList = new ArrayList<>();
6        mConfig = new SpiderConfig();
7        mRandom = new Random();
8        mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
9        // 画笔初始化
10        initPaint();
11    }

接着按着「构思代码」中的效果逐一实现。

绘制一定数量的小球

指定数量为 50,每个小球的位置、颜色随机,并且具有不同的加速度。相关代码如下:

1    @Override
2    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
3        super.onSizeChanged(w, h, oldw, oldh);
4        mWidth = w;
5        mHeight = h;
6    }

先获取控件到控件的宽高。然后初始化小球集合:

 1    /** 2     * 初始化小点 3     */
4    private void initPoint() {
5        for (int i = 0; i  6            int width = (int) (mRandom.nextFloat() * mWidth);
7            int height = (int) (mRandom.nextFloat() * mHeight);
8
9            SpiderPoint point = new SpiderPoint(width, height);
10            int aX = 0;
11            int aY = 0;
12            // 获取加速度
13            while (aX == 0) {
14                aX = (int) ((mRandom.nextFloat() - 0.5F) * mConfig.pointAcceleration);
15            }
16            while (aY == 0) {
17                aY = (int) ((mRandom.nextFloat() - 0.5F) * mConfig.pointAcceleration);
18            }
19            point.aX = aX;
20            point.aY = aY;
21            // 颜色随机
22            point.color = randomRGB();
23            mSpiderPointList.add(point);
24        }
25    }

mConfig 表示配置参数,具体有以下成员变量:

 1public class SpiderConfig {
2    // 小点半径 1
3    public int pointRadius = DEFAULT_POINT_RADIUS;
4    // 小点之间连线的粗细(宽度) 2
5    public int lineWidth = DEFAULT_LINE_WIDTH;
6    // 小点之间连线的透明度 150
7    public int lineAlpha = DEFAULT_LINE_ALPHA;
8    // 小点数量 50
9    public int pointNum = DEFAULT_POINT_NUMBER;
10    // 小点加速度 7
11    public int pointAcceleration = DEFAULT_POINT_ACCELERATION;
12    // 小点之间最长直线距离 280
13    public int maxDistance = DEFAULT_MAX_DISTANCE;
14    // 触摸点半径 1
15    public int touchPointRadius = DEFAULT_TOUCH_POINT_RADIUS;
16    // 引力大小 50
17    public int gravitation_strength = DEFAULT_GRAVITATION_STRENGTH;
18}

获取到小球集合,最后绘制小球:

1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4        // 绘制小球
5        mPointPaint.setColor(spiderPoint.color);
6        canvas.drawCircle(spiderPoint.x, spiderPoint.y, mConfig.pointRadius, mPointPaint);
7        }

效果图如下:

3c48a9d99cff4362f587031bafe127af.png

小球斜向运动,越界回弹

根据位移与速度公式 位移 = 初位移 + 速度 * 时间 ,速度 = 初速度 + 加速度 ,由于初速度为 0 ,时间为 1U,得到 位移 = 初位移 + 加速度

1    spiderPoint.x += spiderPoint.aX;
2    spiderPoint.y += spiderPoint.aY;

判定越界,原理在上文中已经提到:

 1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4        for (SpiderPoint spiderPoint : mSpiderPointList) {
5
6            spiderPoint.x += spiderPoint.aX;
7            spiderPoint.y += spiderPoint.aY;
8
9            // 越界反弹
10            if (spiderPoint.x <= mConfig.pointRadius) {
11                spiderPoint.x = mConfig.pointRadius;
12                spiderPoint.aX = -spiderPoint.aX;
13            } else if (spiderPoint.x >= (mWidth - mConfig.pointRadius)) {
14                spiderPoint.x = (mWidth - mConfig.pointRadius);
15                spiderPoint.aX = -spiderPoint.aX;
16            }
17
18            if (spiderPoint.y <= mConfig.pointRadius) {
19                spiderPoint.y = mConfig.pointRadius;
20                spiderPoint.aY = -spiderPoint.aY;
21            } else if (spiderPoint.y >= (mHeight - mConfig.pointRadius)) {
22                spiderPoint.y = (mHeight - mConfig.pointRadius);
23                spiderPoint.aY = -spiderPoint.aY;
24            }
25        }
26    }

效果图如下:

f74415bff7e672d87a85f6b11cdb6668.gif

两球连线

循环遍历所有小球,若小球 A 与其他小球的距离小于一定值,则两小球连线,反之则不连线。双层遍历会导致一个问题,如果小球数量过多,双层遍历效率极低,从而引起界面卡顿,目前并没有找到更好的算法来解决这个问题,为了防止卡顿,对小球的数量有所控制,不能超过 150 个。

 1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4        for (SpiderPoint spiderPoint : mSpiderPointList) {
5            // 绘制连线
6            for (int i = 0; i  7                SpiderPoint point = mSpiderPointList.get(i);
8                // 判定当前点与其他点之间的距离
9                if (spiderPoint != point) {
10                    int distance = disPos2d(point.x, point.y, spiderPoint.x, spiderPoint.y);
11                    if (distance 12                        // 绘制小点间的连线
13                        int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);
14
15                        mLinePaint.setColor(point.color);
16                        mLinePaint.setAlpha(alpha);
17                        canvas.drawLine(spiderPoint.x, spiderPoint.y, point.x, point.y, mLinePaint);
18                    }
19                }
20            }
21        }
22        invalidate();
23    }

disPos2d 方法用于计算两点之间的距离:

1    /**2     * 两点间距离函数3     */
4    public static int disPos2d(float x1, float y1, float x2, float y2) {
5        return (int) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
6    }

如果两小球的距离在 maxDistance 范围内,距离越近透明度越小:

1    int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);

一起来看看两球连线的效果:

7390e1f5b5ff0f47b87843d6965732f8.gif

防止过度绘制

由于双层遍历,若小球 A 先与小球 B 连线,为了提高性能,防止过度绘制,小球 B 不再与小球 A 连线。最开始的想法是记录小球 A 与其他小球的连线状态,当其他小球与小球 A 连线时,根据状态判定是否连线,如果小球 A 先与许多小球连线,必然会在小球 A 对象内部维护一个集合,用于存储小球 A 已经与哪些小球连线,这样效率并不高,反而把简单的问题变复杂了。最后用了一个取巧的办法:记录第一次循环的索引值,第二次循环从当前的索引值开始,这样就避免了两小球之间的多次连线。相关代码如下:

 1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4        int index = 0;
5        for (SpiderPoint spiderPoint : mSpiderPointList) {
6            // 绘制连线
7            for (int i = index; i  8                SpiderPoint point = mSpiderPointList.get(i);
9                // 判定当前点与其他点之间的距离
10                if (spiderPoint != point) {
11                    int distance = disPos2d(point.x, point.y, spiderPoint.x, spiderPoint.y);
12                    if (distance 13                        // 绘制小点间的连线
14                        int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);
15
16                        mLinePaint.setColor(point.color);
17                        mLinePaint.setAlpha(alpha);
18                        canvas.drawLine(spiderPoint.x, spiderPoint.y, point.x, point.y, mLinePaint);
19                    }
20                }
21            }
22          index++;
23        }
24        invalidate();
25    }
手势处理

还记得吗?在文章 第一站小红书图片裁剪控件,深度解析大厂炫酷控件 已经讲解了手势的处理流程。在网页版中触摸点(鼠标按下点)跟随鼠标移动而移动,在手机屏幕中「触摸点」(手指按下点)跟随手指移动而移动,从而需要重写手势类的 onScroll 方法:

 1        @Override
2        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
3            // 单根手指操作
4            if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {
5                mTouchX = e2.getX();
6                mTouchY = e2.getY();
7                return true;
8            }
9            return super.onScroll(e1, e2, distanceX, distanceY);
10        }

onFling 方法与 onScroll 方法处理方式一致,实时获取到「触摸点」位置。获取到了位置,绘制触摸点:

1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4        // 绘制触摸点
5        if (mTouchY != -1 && mTouchX != -1) {
6            canvas.drawPoint(mTouchX, mTouchY, mTouchPaint);
7        }
8    }

若「触摸点」与其他小球的距离小于一定值,则两小球连线,反之则不连线:

 1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4            // 绘制触摸点与其他点的连线
5            if (mTouchX != -1 && mTouchY != -1) {
6                int offsetX = (int) (mTouchX - spiderPoint.x);
7                int offsetY = (int) (mTouchY - spiderPoint.y);
8                int distance = (int) Math.sqrt(offsetX * offsetX + offsetY * offsetY);
9                if (distance 10                    int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);
11                    mLinePaint.setColor(spiderPoint.color);
12                    mLinePaint.setAlpha(alpha);
13                    canvas.drawLine(spiderPoint.x, spiderPoint.y, mTouchX, mTouchY, mLinePaint);
14                }
15            }
16    }

同时还具有与「触摸点」连线的所有小球向「触摸点」靠拢的效果,可采用「位移相对减少」的方案来实现靠拢的效果,相关代码如下:

 1    @Override
2    protected void onDraw(Canvas canvas) {
3        super.onDraw(canvas);
4            // 绘制触摸点与其他点的连线
5            if (mTouchX != -1 && mTouchY != -1) {
6               ....... // 省略相关代码          
7                if (distance  8                    if (distance >= (mConfig.maxDistance - mConfig.gravitation_strength)) {
9                        // x 轴方向位移减少
10                        if (spiderPoint.x > mTouchX) {
11                            spiderPoint.x -= 0.03F * -offsetX;
12                        } else {
13                            spiderPoint.x += 0.03F * offsetX;
14                        }
15                        // y 轴方向位移减少
16                        if (spiderPoint.y > mTouchY) {
17                            spiderPoint.y -= 0.03F * -offsetY;
18                        } else {
19                            spiderPoint.y += 0.03F * offsetY;
20                        }
21                    } 
22        ....... // 省略相关代码    

看看效果图:

f295537388aefe651a31c888ac7ae574.gif

「五彩蛛网」控件差不多就讲到这里,有什么疑问,请留言讨论?

05 结束语

熬夜写的文章,有道不明的,还请多多包涵。同时也希望各位小伙伴都能过得都挺好。

源码如下:

https://github.com/HpWens/MeiWidgetView

https://github.com/HpWens/SpiderWebView

推荐阅读
写给每一个渴望「成长」的你(程序员必读)
一篇不得不看的Flutter好文
推荐一个强大的毛玻璃特效开源库

编程·思维·职场
欢迎扫码关注

4f2b31e2271b8e0ddc9e57f21bec5f43.png

;