幾條曲線構(gòu)建Android表白程序

想起之前看到的一段H5動(dòng)畫,在Android平臺(tái)“臨摹”了一遍。
效果如下圖:其構(gòu)圖還是比較簡單的,樹枝加上由心形花瓣構(gòu)成的心形樹冠(后面做成動(dòng)畫之后會(huì)有隨機(jī)的花瓣飄落)。


一、樹枝

樹枝是通過貝塞爾曲線來構(gòu)造的,二階貝塞爾曲線。

準(zhǔn)備數(shù)據(jù)
getBranches()函數(shù)中,定義各個(gè)樹枝的位置和形狀,最終返回樹干。
繪制的時(shí)候,先繪制樹干,然后繪制其分支,最后繪制分支的分支(只有三層)。

public static Branch getBranches() {
        // 共10列,分別是id, parentId, 貝塞爾曲線控制點(diǎn)(3點(diǎn),6列), 最大半徑, 長度
        int[][] data = new int[][]{
                {0, -1, 217, 490, 252, 60, 182, 10, 30, 100},
                {1, 0, 222, 310, 137, 227, 22, 210, 13, 100},
                {2, 1, 132, 245, 116, 240, 76, 205, 2, 40},
                {3, 0, 232, 255, 282, 166, 362, 155, 12, 100},
                {4, 3, 260, 210, 330, 219, 343, 236, 3, 80},
                {5, 0, 221, 91, 219, 58, 216, 27, 3, 40},
                {6, 0, 228, 207, 95, 57, 10, 54, 9, 80},
                {7, 6, 109, 96, 65, 63, 53, 15, 2, 40},
                {8, 6, 180, 155, 117, 125, 77, 140, 4, 60},
                {9, 0, 228, 167, 290, 62, 360, 31, 6, 100},
                {10, 9, 272, 103, 328, 87, 330, 81, 2, 80}
        };
        int n = data.length;

        Branch[] branches = new Branch[n];
        for (int i = 0; i < n; i++) {
            branches[i] = new Branch(data[i]);
            int parent = data[i][1];
            if (parent != -1) {
                branches[parent].addChild(branches[i]);
            }
        }
        return branches[0];
    }

封裝Branch類
主要包含樹枝的構(gòu)建(構(gòu)造函數(shù),addChild函數(shù)),以及繪制。
繪制樹枝時(shí),不斷地調(diào)用grow函數(shù),繪制點(diǎn)(currLen)逐漸靠近末端(maxLen), 樹枝的半徑逐漸變?。?br> 最終控制點(diǎn)到達(dá)樹枝末端(currLen==maxLen), 繪制結(jié)束。
如果是繪制靜態(tài)畫面,while循環(huán)直到grow返回false;
如果是繪制動(dòng)畫, 可通過調(diào)用postInvalidate(),不斷地對(duì)回調(diào)繪制函數(shù), 每一幀樹枝成長一截。

public class Branch {
    private static final int BRANCH_COLOR = Color.rgb(35, 31, 32);

    // control point
    Point[] cp = new Point[3];
    int currLen;
    int maxLen;
    float radius;
    float part;

    float growX;
    float growY;

    LinkedList<Branch> childList;

    public Branch(int[] a){
        cp[0] = new Point(a[2], a[3]);
        cp[1] = new Point(a[4], a[5]);
        cp[2] = new Point(a[6], a[7]);
        radius = a[8];
        maxLen = a[9];
        part = 1.0f / maxLen;
    }

    public boolean grow(Canvas canvas, float scareFactor){
        if(currLen <= maxLen){
            bezier(part * currLen);
            draw(canvas, scareFactor);
            currLen++;
            radius *= 0.97f;
            return true;
        }else{
            return false;
        }
    }

    private void draw(Canvas canvas, float scareFactor){
        Paint paint = CommonUtil.getPaint();
        paint.setColor(BRANCH_COLOR);

        canvas.save();
        canvas.scale(scareFactor, scareFactor);
        canvas.translate(growX, growY);
        canvas.drawCircle(0,0, radius, paint);
        canvas.restore();
    }

    private void bezier(float t) {
        float c0 = (1 - t) * (1 - t);
        float c1 = 2 * t * (1 - t);
        float c2 = t * t;
        growX =  c0 * cp[0].x + c1 * cp[1].x + c2* cp[2].x;
        growY =  c0 * cp[0].y + c1 * cp[1].y + c2* cp[2].y;
    }

    public void addChild(Branch branch){
        if(childList == null){
            childList = new LinkedList<>();
        }
        childList.add(branch);
    }
}

效果圖如下:


二、花瓣

花瓣的繪制,是通過一條曲線實(shí)現(xiàn)的:本文的主角,自帶愛情故事的心形線
心形線有很多種,有的用標(biāo)準(zhǔn)方程表示,有的用參數(shù)方程表示。
對(duì)于繪制曲線來說,參數(shù)方程更方便一些。
在網(wǎng)站wolframalpha上,可以輸入方程直接預(yù)覽曲線。

計(jì)算心形線
因?yàn)橐L制很多花瓣,所以可以將其形狀預(yù)先計(jì)算好,緩存起來。
或許是因?yàn)榫鹊脑颍?如果直接采樣上圖的點(diǎn),繪制時(shí)如果有scale(縮放)操作,可能會(huì)顯示不平滑;
所以在采樣心形線的點(diǎn)時(shí)我們放大一定比率(SCALE_FACTOR )。
就像一張圖片,如果分辨率是200x200, 縮小到100x100顯示,圖片還是清晰的,如果放大到400x400,可能會(huì)模糊。

public class Heart {
    private static final Path PATH = new Path();

    private static final float SCALE_FACTOR = 10f;
    private static final float RADIUS = 18 * SCALE_FACTOR;

    static {
        // x = 16 sin^3 t
        // y = 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t
        // http://www.wolframalpha.com/input/?i=x+%3D+16+sin%5E3+t%2C+y+%3D+(13+cos+t+-+5+cos+2t+-+2+cos+3t+-+cos+4t)
        int n = 101;
        Point[] points = new Point[n];
        float t = 0f;
        float d = (float) (2 * Math.PI / (n - 1));
        for (int i = 0; i < n; i++) {
            float x = (float) (16 * Math.pow(Math.sin(t), 3));
            float y = (float) (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
            points[i] = new Point(SCALE_FACTOR * x  , -SCALE_FACTOR * y );
            t += d;
        }

        PATH.moveTo(points[0].x, points[0].y);
        for (int i = 1; i < n; i++) {
            PATH.lineTo(points[i].x, points[i].y);
        }
        PATH.close();
    }

    public static Path getPath(){
        return PATH;
    }

    public static float getRadius(){
        return RADIUS;
    }
}

封裝Bloom類
一片花瓣,除了形狀之外,還有方位,顏色,方向,大小等參數(shù)。
故此,和Branch一樣,封裝了一個(gè)類。
花瓣的顏色和方向參數(shù)是隨機(jī)初始化的。
顏色方面,ARGB中Red通道固定為最大值0xff, 效果就是花瓣的顏色為紅,紫,黃,白等。
因?yàn)橐m應(yīng)移動(dòng)設(shè)備的多分辨率,所以一些參數(shù)要根據(jù)分辨率來動(dòng)態(tài)設(shè)置。

public class Bloom {
    protected static float sMaxScale = 0.2f;
    protected static int sMaxRadius = Math.round(sMaxScale * Heart.getRadius());
    protected static float sFactor;

    /**
     * 初始化顯示參數(shù)
     * @param resolutionFactor 根據(jù)屏幕分辨率設(shè)定縮放因子
     */
    public static void initDisplayParam(float resolutionFactor){
        sFactor = resolutionFactor;
        sMaxScale = 0.2f * resolutionFactor;
        sMaxRadius = Math.round(sMaxScale * Heart.getRadius());
    }

    Point position;
    int color;
    float angle;
    float scale;

    // 調(diào)速器,控制開花動(dòng)畫的快慢
    int governor = 0;

    public Bloom(Point position) {
        this.position = position;
        this.color = Color.argb(CommonUtil.random(76, 255), 0xff, CommonUtil.random(255), CommonUtil.random(255));
        this.angle = CommonUtil.random(360);
    }

    public boolean grow(Canvas canvas) {
        if (scale <= sMaxScale) {
            if((governor & 1) == 0) {
                scale += 0.0125f * sFactor;
                draw(canvas);
            }
            governor++;
            return true;
        } else {
            return false;
        }
    }

    protected float getRadius() {
        return Heart.getRadius() * scale;
    }

    private void draw(Canvas canvas) {
        Paint paint = CommonUtil.getPaint();
        paint.setColor(color);
        float r = getRadius();

        canvas.save();
        canvas.translate(position.x, position.y);
        canvas.saveLayerAlpha(-r, -r, r, r, Color.alpha(color));
        canvas.save();
        canvas.rotate(angle);
        canvas.scale(scale, scale);
        canvas.drawPath(Heart.getPath(), paint);
        canvas.restore();
        canvas.restore();
        canvas.restore();
    }
}

三、樹冠

樹冠是由數(shù)百片花瓣構(gòu)成,關(guān)鍵點(diǎn)在于確定這些花瓣的位置。
這里用到另一條心形線(x^2 + y^2 -1)^3 - x^2 * y^3 = 0。
我們需要做的,是在心形內(nèi)部選取位置,而非繪制曲線,故此,標(biāo)準(zhǔn)方程相對(duì)于參數(shù)方程更合適。

坐標(biāo)系中的點(diǎn)(x,y), 計(jì)算ax+by, 大于0和小于0分別在直線的兩側(cè), x^2 + y^2 - r^2 則分別在圓外和圓內(nèi);
這個(gè)現(xiàn)象還蠻奇妙的,雖然我不知道這在數(shù)學(xué)中叫什么-_-。
類似的,在x=[-c, c], y=[-c,c]的范圍內(nèi)隨機(jī)選取(x^2 + y^2 -1)^3 - x^2 * y^3<0的點(diǎn),即可使得花瓣的位置錯(cuò)落于心形線中。

    private static float r;
    private static float c;

    /**
     * 初始化參數(shù)
     * @param canvasHeight 畫布的高度
     * @param crownRadiusFactor 樹冠半徑的縮放因子
     */
    public static void init(int canvasHeight, float crownRadiusFactor){
        r = canvasHeight * crownRadiusFactor;
        c = r * 1.35f;
    }

    public static void fillBlooms(List<Bloom> blooms, int num) {
        int n = 0;
        while (n < num) {
            float x = CommonUtil.random(-c, c);
            float y = CommonUtil.random(-c, c);
            if (inHeart(x, y, r)) {
                blooms.add(new Bloom(new Point(x, -y)));
                n++;
            }
        }
    }

    private static boolean inHeart(float px, float py, float r) {
        //  (x^2+y^2-1)^3-x^2*y^3=0
        float x = px / r;
        float y = py / r;
        float sx = x * x;
        float sy = y * y;
        float a = sx + sy - 1;
        return a * a * a - sx * sy * y < 0;
    }

繪制動(dòng)畫

不斷地觸發(fā)onDraw()回調(diào),在每一幀里面改變繪制參數(shù),就形成動(dòng)畫了。
在這個(gè)例子中,劃分了幾個(gè)動(dòng)畫階段,每個(gè)階段各自變化自己的參數(shù),到達(dá)一定的狀態(tài)就切換到下一階段。
總之,就是分而治之,然后串聯(lián)起來。

public class TreeView  extends View {
    private static Tree tree;

    public TreeView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(tree == null){
            tree = new Tree(getWidth(), getHeight());
        }
        tree.draw(canvas);

        // 這個(gè)函數(shù)只是標(biāo)記view為invalidate狀態(tài),并不會(huì)馬上觸發(fā)重繪;
        // 標(biāo)記invalidate狀態(tài)后,下一個(gè)繪制周期(約16s), 會(huì)回調(diào)onDraw();
        // 故此,要想動(dòng)畫平滑流暢,tree.draw(canvas)需在16s內(nèi)完成。
        postInvalidate();
    }
}

public void draw(Canvas canvas) {
        // 繪制背景顏色
        canvas.drawColor(0xffffffee);

        // 繪制動(dòng)畫元素
        canvas.save();
        canvas.translate(snapshotDx + xOffset, 0);
        switch (step) {
            case BRANCHES_GROWING:
                drawBranches();
                drawSnapshot(canvas);
                break;
            case BLOOMS_GROWING:
                drawBlooms();
                drawSnapshot(canvas);
                break;
            case MOVING_SNAPSHOT:
                movingSnapshot();
                drawSnapshot(canvas);
                break;
            case BLOOM_FALLING:
                drawSnapshot(canvas);
                drawFallingBlooms(canvas);
                break;
            default:
                break;
        }
        canvas.restore();
}

后記

  • 調(diào)整參數(shù)消耗不少時(shí)間,寫代碼比較客觀,調(diào)參數(shù)則比較主觀:方位擺放,顯示大小,動(dòng)畫快慢……
  • 構(gòu)圖中左上角有留白,可以在那里輸出一些表白文字。
  • 考慮到移動(dòng)端的流量,動(dòng)圖部分只截取最后一個(gè)階段的動(dòng)畫。
  • 篇幅限制,文中只是貼了部分代碼,完整代碼可到github下載HeartTree。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容