Skip to content

HpWens/SpiderWebView

Folders and files

NameName
Last commit message
Last commit date

Latest commit

c457060 · Mar 27, 2019

History

4 Commits
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019
Mar 27, 2019

Repository files navigation

SpiderWebView

前言

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

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

先来看看效果图:

在这里插入图片描述 在这里插入图片描述

初步分析

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

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

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

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

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

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

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

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

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

模拟「蛛网点」物体类:

public class SpiderPoint extends Point {

    // x 方向加速度
    public int aX;

    // y 方向加速度
    public int aY;

    // 小球颜色
    public int color;
    
    // 小球半径
    public int r;

    // x 轴方向速度
    public float vX;

    // y 轴方向速度
    public float vY;
    
    // 点
    public float x;
    public float y;

    public SpiderPoint(int x, int y) {
        super(x, y);
    }
}

蛛网点匀速直线运动

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

public class MoveView extends View {

    // 画笔
    private Paint mPointPaint;
    // 蛛网点对象(类似小球)
    private SpiderPoint mSpiderPoint;
    // 坐标系
    private Point mCoordinate;

    // 蛛网点 默认小球半径
    private int pointRadius = 20;
    // 默认颜色
    private int pointColor = Color.RED;
    // 默认x方向速度
    private float pointVX = 10;
    // 默认y方向速度
    private float pointVY = 0;
    // 默认 小球加速度
    private int pointAX = 0;
    private int pointAY = 0;

    // 是否开始运动
    private boolean startMove = false;

    public MoveView(Context context) {
        this(context, null);
    }

    public MoveView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData();
        initPaint();
    }

    private void initData() {
        mCoordinate = new Point(500, 500);
        mSpiderPoint = new SpiderPoint();
        mSpiderPoint.color = pointColor;
        mSpiderPoint.vX = pointVX;
        mSpiderPoint.vY = pointVY;
        mSpiderPoint.aX = pointAX;
        mSpiderPoint.aY = pointAY;
        mSpiderPoint.r = pointRadius;
    }

    // 初始化画笔
    private void initPaint() {
        mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPointPaint.setColor(pointColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.translate(mCoordinate.x, mCoordinate.y);
        drawSpiderPoint(canvas, mSpiderPoint);
        canvas.restore();

        // 刷新视图 再次调用onDraw方法模拟时间流
        if (startMove) {
            updateBall();
            invalidate();
        }
    }

    /**
     * 绘制蛛网点
     *
     * @param canvas
     * @param spiderPoint
     */
    private void drawSpiderPoint(Canvas canvas, SpiderPoint spiderPoint) {
        mPointPaint.setColor(spiderPoint.color);
        canvas.drawCircle(spiderPoint.x, spiderPoint.y, spiderPoint.r, mPointPaint);
    }

    /**
     * 更新小球
     */
    private void updateBall() {
        //TODO --运动数据都由此函数变换
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 开启时间流
                startMove = true;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                // 暂停时间流
                startMove = false;
                invalidate();
                break;
        }
        return true;
    }
}

1、水平运行运动:

在这里插入图片描述

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

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

2、回弹效果

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

在这里插入图片描述

3、无限回弹,回弹变色

在这里插入图片描述

相关代码如下:

    /**
     * 更新小球
     */
    private void updateBall() {
        //TODO --运动数据都由此函数变换
        mSpiderPoint.x += mSpiderPoint.vX;
        if (mSpiderPoint.x > 400) {
            // 更改颜色
            mSpiderPoint.color = randomRGB();
            mSpiderPoint.vX = -mSpiderPoint.vX;
        }
        if (mSpiderPoint.x < -400) {
            mSpiderPoint.vX = -mSpiderPoint.vX;
            // 更改颜色
            mSpiderPoint.color = randomRGB();
        }
    }

randomRGB 方法的代码如下:

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

3、箱式弹跳

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

图源网络,侵权必删

改变 y 轴方向初速度:

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

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

    /**
     * 更新小球
     */
    private void updateBall() {
        //TODO --运动数据都由此函数变换
        mSpiderPoint.x += mSpiderPoint.vX;
        mSpiderPoint.y += mSpiderPoint.vY;
        if (mSpiderPoint.x > 400) {
            // 更改颜色
            mSpiderPoint.color = randomRGB();
            mSpiderPoint.vX = -mSpiderPoint.vX;
        }
        if (mSpiderPoint.x < -400) {
            mSpiderPoint.vX = -mSpiderPoint.vX;
            // 更改颜色
            mSpiderPoint.color = randomRGB();
        }

        if (mSpiderPoint.y > 400) {
            // 更改颜色
            mSpiderPoint.color = randomRGB();
            mSpiderPoint.vY = -mSpiderPoint.vY;
        }
        if (mSpiderPoint.y < -400) {
            mSpiderPoint.vY = -mSpiderPoint.vY;
            // 更改颜色
            mSpiderPoint.color = randomRGB();
        }
    }

效果如下图:

在这里插入图片描述

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

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

构思代码

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

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

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

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

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

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

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

编写代码

起名字

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

创建SpiderWebView

先是成员变量:

    // 控件宽高
    private int mWidth;
    private int mHeight;
    // 画笔
    private Paint mPointPaint;
    private Paint mLinePaint;
    private Paint mTouchPaint;
    // 触摸点坐标
    private float mTouchX = -1;
    private float mTouchY = -1;
    // 数据源
    private List<SpiderPoint> mSpiderPointList;
    // 相关参数配置
    private SpiderConfig mConfig;
    // 随机数
    private Random mRandom;
    // 手势帮助类 用于处理滚动与拖拽
    private GestureDetector mGestureDetector;

然后是构造函数:

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

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

绘制一定数量的小球

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

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

    /**
     * 初始化小点
     */
    private void initPoint() {
        for (int i = 0; i < mConfig.pointNum; i++) {
            int width = (int) (mRandom.nextFloat() * mWidth);
            int height = (int) (mRandom.nextFloat() * mHeight);

            SpiderPoint point = new SpiderPoint(width, height);
            int aX = 0;
            int aY = 0;
            // 获取加速度
            while (aX == 0) {
                aX = (int) ((mRandom.nextFloat() - 0.5F) * mConfig.pointAcceleration);
            }
            while (aY == 0) {
                aY = (int) ((mRandom.nextFloat() - 0.5F) * mConfig.pointAcceleration);
            }
            point.aX = aX;
            point.aY = aY;
            // 颜色随机
            point.color = randomRGB();
            mSpiderPointList.add(point);
        }
    }

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

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

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

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

效果图如下:

在这里插入图片描述

小球斜向运动,越界回弹

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

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (SpiderPoint spiderPoint : mSpiderPointList) {

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

            // 越界反弹
            if (spiderPoint.x <= mConfig.pointRadius) {
                spiderPoint.x = mConfig.pointRadius;
                spiderPoint.aX = -spiderPoint.aX;
            } else if (spiderPoint.x >= (mWidth - mConfig.pointRadius)) {
                spiderPoint.x = (mWidth - mConfig.pointRadius);
                spiderPoint.aX = -spiderPoint.aX;
            }

            if (spiderPoint.y <= mConfig.pointRadius) {
                spiderPoint.y = mConfig.pointRadius;
                spiderPoint.aY = -spiderPoint.aY;
            } else if (spiderPoint.y >= (mHeight - mConfig.pointRadius)) {
                spiderPoint.y = (mHeight - mConfig.pointRadius);
                spiderPoint.aY = -spiderPoint.aY;
            }
		}
	}

效果图如下:

在这里插入图片描述

两球连线

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (SpiderPoint spiderPoint : mSpiderPointList) {
            // 绘制连线
            for (int i = 0; i < mSpiderPointList.size(); i++) {
                SpiderPoint point = mSpiderPointList.get(i);
                // 判定当前点与其他点之间的距离
                if (spiderPoint != point) {
                    int distance = disPos2d(point.x, point.y, spiderPoint.x, spiderPoint.y);
                    if (distance < mConfig.maxDistance) {
                        // 绘制小点间的连线
                        int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);

                        mLinePaint.setColor(point.color);
                        mLinePaint.setAlpha(alpha);
                        canvas.drawLine(spiderPoint.x, spiderPoint.y, point.x, point.y, mLinePaint);
                    }
                }
            }
        }
        invalidate();
    }

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

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

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

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

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

在这里插入图片描述

防止过度绘制

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int index = 0;
        for (SpiderPoint spiderPoint : mSpiderPointList) {
            // 绘制连线
            for (int i = index; i < mSpiderPointList.size(); i++) {
                SpiderPoint point = mSpiderPointList.get(i);
                // 判定当前点与其他点之间的距离
                if (spiderPoint != point) {
                    int distance = disPos2d(point.x, point.y, spiderPoint.x, spiderPoint.y);
                    if (distance < mConfig.maxDistance) {
                        // 绘制小点间的连线
                        int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);

                        mLinePaint.setColor(point.color);
                        mLinePaint.setAlpha(alpha);
                        canvas.drawLine(spiderPoint.x, spiderPoint.y, point.x, point.y, mLinePaint);
                    }
                }
            }
          index++;
        }
        invalidate();
    }

手势处理

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

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

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

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
            // 绘制触摸点与其他点的连线
            if (mTouchX != -1 && mTouchY != -1) {
                int offsetX = (int) (mTouchX - spiderPoint.x);
                int offsetY = (int) (mTouchY - spiderPoint.y);
                int distance = (int) Math.sqrt(offsetX * offsetX + offsetY * offsetY);
                if (distance < mConfig.maxDistance) {
                    int alpha = (int) ((1.0F - (float) distance / mConfig.maxDistance) * mConfig.lineAlpha);
                    mLinePaint.setColor(spiderPoint.color);
                    mLinePaint.setAlpha(alpha);
                    canvas.drawLine(spiderPoint.x, spiderPoint.y, mTouchX, mTouchY, mLinePaint);
                }
            }
	}

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
            // 绘制触摸点与其他点的连线
            if (mTouchX != -1 && mTouchY != -1) {
       		....... // 省略相关代码          
                if (distance < mConfig.maxDistance) {
                    if (distance >= (mConfig.maxDistance - mConfig.gravitation_strength)) {
                        // x 轴方向位移减少
                        if (spiderPoint.x > mTouchX) {
                            spiderPoint.x -= 0.03F * -offsetX;
                        } else {
                            spiderPoint.x += 0.03F * offsetX;
                        }
                        // y 轴方向位移减少
                        if (spiderPoint.y > mTouchY) {
                            spiderPoint.y -= 0.03F * -offsetY;
                        } else {
                            spiderPoint.y += 0.03F * offsetY;
                        }
                    } 
		....... // 省略相关代码    

看看效果图:

在这里插入图片描述

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

结束语

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

源码如下:

https://github.com/HpWens/MeiWidgetView

https://github.com/HpWens/SpiderWebView

希望有志之士能够与我一起维护「控件人生」公众号。

扫一扫 关注我的公众号
想了解更多炫酷控件吗~

About

Android实现网页动态背景“五彩蛛网”

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages