《都挺好》迎來了大結局,相信看哭了不少人。在大結局中,全部以前讓人氣的牙癢癢的人設,好比 「大家太讓我失望」 的蘇明哲,還有媽寶男蘇明成,包括一天不做就難受的蘇大強,最終都成功洗白。一家人最終化解恩怨,和和睦氣的過日子。還有誰也喜歡《都挺好》這部劇嗎?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 軸方向初速度:// 默認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;
}
}
....... // 省略相關代碼
複製代碼
看看效果圖:
「五彩蛛網」控件差很少就講到這裏,有什麼疑問,請留言討論?熬夜寫的文章,有道不明的,還請多多包涵。同時也但願各位小夥伴都能過得都挺好。
源碼以下:
但願有志之士可以與我一塊兒維護「控件人生」公衆號。