Android實現網頁動態背景“五彩蛛網”

文淑發表於2019-03-27

前言

《都挺好》迎來了大結局,相信看哭了很多人。在大結局中,所有之前讓人氣的牙癢癢的人設,比如 “你們太讓我失望” 的蘇明哲,還有媽寶男蘇明成,包括一天不作就難受的蘇大強,最終都成功洗白。一家人最終化解恩怨,和和氣氣的過日子。還有誰也喜歡《都挺好》這部劇嗎?

在劇中,蘇明哲同我們一樣也是一名程式設計師,一味地遷就老爹,搞得最後差點與老婆離婚,看來程式設計師不能一根筋啊。轉變下思維來看看網頁版動態背景「五彩蛛網」是怎麼實現的?

先來看看效果圖:

在這裡插入圖片描述
在這裡插入圖片描述

初步分析

在效果圖中,可以看到許多「小點」在螢幕中勻速運動並與「鄰近的點」相連,每條連線的顏色隨機,「小點」觸碰到螢幕邊緣則回彈;還有一個效果就是,手指在螢幕中移動、拖拽,與手指觸控點連線的點向觸控點靠攏。何為「鄰近的點」,與某點的距離小於特定的閾值的點稱為「鄰近的點」。

提到運動,「運動」在物理學中指物體在空間中的相對位置隨著時間而變化。

那麼大家還記得「位移」與「速度」公式嗎?

位移 = 初位移 + 速度 * 時間
速度 = 初速度 + 加速度
複製程式碼

時間、位移、速度、加速度構成了現代科學的運動體系。我們使用 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;
                        }
                    } 
		....... // 省略相關程式碼    
複製程式碼

看看效果圖:

在這裡插入圖片描述
「五彩蛛網」控制元件差不多就講到這裡,有什麼疑問,請留言討論?

結束語

熬夜寫的文章,有道不明的,還請多多包涵。同時也希望各位小夥伴都能過得都挺好。

原始碼如下:

github.com/HpWens/MeiW…

github.com/HpWens/Spid…

希望有志之士能夠與我一起維護「控制元件人生」公眾號。

qrcode_for_gh_232b5a56667d_258.jpg

掃一掃 關注我的公眾號
想了解更多炫酷控制元件嗎~

相關文章