《都挺好》迎来了大结局,相信看哭了不少人。在大结局中,全部以前让人气的牙痒痒的人设,好比 “大家太让我失望” 的苏明哲,还有妈宝男苏明成,包括一天不做就难受的苏大强,最终都成功洗白。一家人最终化解恩怨,和和睦气的过日子。还有谁也喜欢《都挺好》这部剧吗?java
在剧中,苏明哲同咱们同样也是一名程序员,一味地迁就老爹,搞得最后差点与老婆离婚,看来程序员不能一根筋啊。转变下思惟来看看网页版动态背景「五彩蛛网」是怎么实现的?git
先来看看效果图: 程序员
在效果图中,能够看到许多「小点」在屏幕中匀速运动并与「邻近的点」相连,每条连线的颜色随机,「小点」触碰到屏幕边缘则回弹;还有一个效果就是,手指在屏幕中移动、拖拽,与手指触摸点连线的点向触摸点靠拢。何为「邻近的点」,与某点的距离小于特定的阈值的点称为「邻近的点」。github
提到运动,「运动」在物理学中指物体在空间中的相对位置随着时间而变化。算法
那么你们还记得「位移」与「速度」公式吗?canvas
位移 = 初位移 + 速度 * 时间
速度 = 初速度 + 加速度
复制代码
时间、位移、速度、加速度构成了现代科学的运动体系。咱们使用 view 来模拟物体的运动。网络
时间:在 view 的 onDraw 方法中调用 invalidate 方法,达到无限刷新来模拟时间流,每次刷新间隔,记为:1Udom
位移:物体在屏幕中的像素位置,每一个像素距离为:1pxide
速度:默认设置一个值,单位(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 方向速度 十、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;
}
}
复制代码
一、水平运行运动:
位移 = 初位移 + 速度 * 时间
,这里的时间为 1U,更新小球位置的相关代码以下:
/** * 更新小球 */
private void updateBall() {
//TODO --运动数据都由此函数变换
mSpiderPoint.x += mSpiderPoint.vX;
}
复制代码
二、回弹效果
回弹,速度取反,x 轴方向大于 400 则回弹:
/** * 更新小球 */
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));
}
复制代码
三、箱式弹跳
小球在 y 轴方向的平移与 x 轴方向的平移一致,这里再也不讲解,看一下 x ,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();
}
}
复制代码
效果以下图:
经过观察网页「蛛网」动态效果,能够细分为如下几点:
绘制必定数量的小球(蛛网点)
小球斜向运动(具备 x,y 轴方向速度),越界回弹
遍历全部小球,若小球 A 与其余小球的距离小于必定值,则两小球连线,反之则不连线
若小球 A 先与小球 B 连线,为了提升性能,防止过分绘制,小球 B 再也不与小球 A 连线
在手指触摸点绘制小球,同连线规则一致,连线其余小球,若手指移动,连线的全部小球向触摸点靠拢
接下来,具体看看代码该怎么写。
取名是一门学问,好的名字可以让你记忆犹新,那就叫 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;
}
}
....... // 省略相关代码
复制代码
看看效果图:
熬夜写的文章,有道不明的,还请多多包涵。同时也但愿各位小伙伴都能过得都挺好。
源码以下:
但愿有志之士可以与我一块儿维护「控件人生」公众号。