打造浪漫的Android表白程序

来源:互联网 发布:台湾政治知乎 编辑:程序博客网 时间:2024/04/30 20:14

几年前,看到过有个牛人用HTML5绘制了浪漫的爱心表白动画。地址在这:浪漫程序员 HTML5爱心表白动画。发现原来程序员也是可以很浪……漫…..的(PS:刚过520,被妹子骂不够浪漫)。那么在Android怎么打造如此这个效果呢?参考了一下前面HTML5的算法,在Android中实现了类似的效果。先贴上最终效果图:

这里写图片描述

生成心形线

心形线的表达式可以参考:桃心线。里面对桃心线的表达式解析的挺好。可以通过使用极坐标的方式,传入角度和距离(常量)计算出对应的坐标点。其中距离是常量值,不需改变,变化的是角度。
桃心线极坐标方程式为:

x=16×sin3α
y=13×cosα?5×cos2α?2×cos3α?cos4α

如果生成的桃心线不够大,可以吧x、y乘以一个常数,使之变大。考虑到大部分人都不愿去研究具体的数学问题,我们直接把前面HTML5的JS代码直接翻译成Java代码就好。代码如下:

?
1
2
3
4
5
6
<code class="language-java hljs ">publicPoint getHeartPoint(floatangle) {
  floatt = (float) (angle / Math.PI);
  floatx = (float) (19.5* (16 * Math.pow(Math.sin(t), 3)));
  floaty = (float) (-20* (13 * Math.cos(t) - 5 * Math.cos(2 * t) -2 * Math.cos(3* t) - Math.cos(4* t)));
   returnnew Point(offsetX + (int) x, offsetY + (int) y);
 }</code>

其中offsetX和offsetY是偏移量。使用偏移量主要是为了能让心形线处于中央。offsetX和offsetY的值分别为:

?
1
2
<code class="language-java hljs "> offsetX = width /2;
 offsetY = height /2 - 55;</code>

通过这个函数,我们可以将角度从(0,180)变化,不断取点并画点将这个心形线显示出来。好了,我们自定义一个View,然后把这个心形线画出来吧!

?
1
2
3
4
5
6
7
8
9
<code class="language-java hljs "@Override
  protectedvoid onDraw(Canvas canvas) {
       floatangle = 10;
       while(angle < 180) {
           Point p = getHeartPoint(angle);
           canvas.drawPoint(p.x, p.y, paint);
           angle = angle +0.02f;
        }
   }</code>

运行结果如下:
显示的心形线

绘制花瓣原理

我们想要的并不是简单绘制一个桃心线,要的是将花朵在桃心线上摆放。首先,得要知道怎么绘制花朵,而花朵是由一个个花瓣组成。因此绘制花朵的核心是绘制花瓣。绘制花瓣的原理是:3次贝塞尔曲线。三次贝塞尔曲线是由两个端点和两个控制点决定。假设花芯是一个圆,有n个花瓣,那么两个端点与花芯的圆心连线之间的夹角即为360/n。因此可以根据花瓣数量和花芯半径确定每个花瓣的位置。将两个端点与花芯的圆心连线的延长线分别确定另外两个控制点。通过随机生成花芯半径、每个花瓣的起始角以及随机确定延长线得到两个控制点,可以绘制一个随机的花朵。参数的改变如下图所示:

这里写图片描述

将花朵绘制到桃心线上

一大波代码来袭

首先定义花瓣类Petal:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<code class="language-java hljs ">
 packagecom.hc.testheart;
 
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/25.
 */
public class Petal {
    privatefloat stretchA;//第一个控制点延长线倍数
    privatefloat stretchB;//第二个控制点延长线倍数
    privatefloat startAngle;//起始旋转角,用于确定第一个端点
    privatefloat angle;//两条线之间夹角,由起始旋转角和夹角可以确定第二个端点
    privateint radius = 2;//花芯的半径
    privatefloat growFactor;//增长因子,花瓣是有开放的动画效果,这个参数决定花瓣展开速度
    privateint color;//花瓣颜色
    privateboolean isFinished = false;//花瓣是否绽放完成
    privatePath path = newPath();//用于保存三次贝塞尔曲线
    privatePaint paint = newPaint();//画笔
    //构造函数,由花朵类调用
    publicPetal(float stretchA, float stretchB, float startAngle, float angle, int color,float growFactor) {
        this.stretchA = stretchA;
        this.stretchB = stretchB;
        this.startAngle = startAngle;
        this.angle = angle;
        this.color = color;
        this.growFactor = growFactor;
        paint.setColor(color);
    }
    //用于渲染花瓣,通过不断更改半径使得花瓣越来越大
    publicvoid render(Point p, int radius, Canvas canvas) {
        if(this.radius <= radius) {
            this.radius += growFactor;// / 10;
        }else {
            isFinished =true;
        }
        this.draw(p, canvas);
    }
 
    //绘制花瓣,参数p是花芯的圆心的坐标
    privatevoid draw(Point p, Canvas canvas) {
        if(!isFinished) {
 
            path =new Path();
            //将向量(0,radius)旋转起始角度,第一个控制点根据这个旋转后的向量计算
            Point t =new Point(0,this.radius).rotate(MyUtil.degrad(this.startAngle));
            //第一个端点,为了保证圆心不会随着radius增大而变大这里固定为3
            Point v1 =new Point(0,3).rotate(MyUtil.degrad(this.startAngle));
            //第二个端点
            Point v2 = t.clone().rotate(MyUtil.degrad(this.angle));
            //延长线,分别确定两个控制点
            Point v3 = t.clone().mult(this.stretchA);
            Point v4 = v2.clone().mult(this.stretchB);
            //由于圆心在p点,因此,每个点要加圆心坐标点
            v1.add(p);
            v2.add(p);
            v3.add(p);
            v4.add(p);
            path.moveTo(v1.x, v1.y);
            //参数分别是:第一个控制点,第二个控制点,终点
            path.cubicTo(v3.x, v3.y, v4.x, v4.y, v2.x, v2.y);
        }
        canvas.drawPath(path, paint);
    }
 
 
}
 
</code>

花瓣类是最重要的类,因为真正绘制在屏幕上的是一个个小花瓣。每个花朵包含一系列花瓣,花朵类Bloom如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<code class="language-java hljs ">packagecom.hc.testheart;
 
import android.graphics.Canvas;
 
import java.util.ArrayList;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/25.
 */
public class Bloom {
    privateint color;//整个花朵的颜色
    privatePoint point;//花芯圆心
    privateint radius; //花芯半径
    privateArrayList<petal> petals;//用于保存花瓣
 
    publicPoint getPoint() {
        returnpoint;
    }
 
 
    publicBloom(Point point, intradius, int color, int petalCount) {
        this.point = point;
        this.radius = radius;
        this.color = color;
        petals =new ArrayList<>(petalCount);
 
 
        floatangle = 360f / petalCount;
        intstartAngle = MyUtil.randomInt(0,90);
        for(int i = 0; i < petalCount; i++) {
            //随机产生第一个控制点的拉伸倍数
            floatstretchA = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
            //随机产生第二个控制地的拉伸倍数
            floatstretchB = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
            //计算每个花瓣的起始角度
            intbeginAngle = startAngle + (int) (i * angle);
            //随机产生每个花瓣的增长因子(即绽放速度)
            floatgrowFactor = MyUtil.random(Garden.Options.minGrowFactor, Garden.Options.maxGrowFactor);
            //创建一个花瓣,并添加到花瓣列表中
            this.petals.add(newPetal(stretchA, stretchB, beginAngle, angle, color, growFactor));
        }
    }
 
    //绘制花朵
    publicvoid draw(Canvas canvas) {
        Petal p;
        for(int i = 0; i < this.petals.size(); i++) {
            p = petals.get(i);
            //渲染每朵花朵
            p.render(point,this.radius, canvas);
 
        }
 
    }
 
    publicint getColor() {
        returncolor;
    }
}
 
</petal></code>

接下来是花园类Garden,主要用于创建花朵以及一些相关配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<code class="language-java hljs ">packagecom.hc.testheart;
 
import java.util.ArrayList;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/24.
 */
public class Garden {
 
    //创建一个随机的花朵
    publicBloom createRandomBloom(intx, int y) {
        //创建一个随机的花朵半径
        intradius = MyUtil.randomInt(Options.minBloomRadius, Options.maxBloomRadius);
        //创建一个随机的花朵颜色
        intcolor = MyUtil.randomrgba(Options.minRedColor, Options.maxRedColor, Options.minGreenColor, Options.maxGreenColor, Options.minBlueColor, Options.maxBlueColor, Options.opacity);
        //创建随机的花朵中花瓣个数
        intpetalCount = MyUtil.randomInt(Options.minPetalCount, Options.maxPetalCount);
        returncreateBloom(x, y, radius, color, petalCount);
    }
 
    //创建花朵
    publicBloom createBloom(intx, int y, int radius, int color, intpetalCount) {
        returnnew Bloom(newPoint(x, y), radius, color, petalCount);
    }
 
    staticclass Options {
        //用于控制产生随机花瓣个数范围
        publicstatic int minPetalCount = 8;
        publicstatic int maxPetalCount = 15;
        //用于控制产生延长线倍数范围
        publicstatic float minPetalStretch = 2f;
        publicstatic float maxPetalStretch = 3.5f;
        //用于控制产生随机增长因子范围,增长因子决定花瓣绽放速度
        publicstatic float minGrowFactor = 1f;
        publicstatic float maxGrowFactor = 1.1f;
        //用于控制产生花朵半径随机数范围
        publicstatic int minBloomRadius = 8;
        publicstatic int maxBloomRadius = 10;
        //用于产生随机颜色
        publicstatic int minRedColor = 128;
        publicstatic int maxRedColor = 255;
        publicstatic int minGreenColor = 0;
        publicstatic int maxGreenColor = 128;
        publicstatic int minBlueColor = 0;
        publicstatic int maxBlueColor = 128;
        //花瓣的透明度
        publicstatic int opacity = 50;//0.1
    }
}
 
</code>

考虑到刷新的比较频繁,选择使用SurfaceView作为显示视图。自定义一个HeartView继承SurfaceView。代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<code class="language-java hljs ">packagecom.hc.testheart;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
import java.util.ArrayList;
 
/**
 * Package com.hc.testheart
 * Created by HuaChao on 2016/5/25.
 */
public class HeartView extendsSurfaceView implementsSurfaceHolder.Callback {
    SurfaceHolder surfaceHolder;
    intoffsetX;
    intoffsetY;
    privateGarden garden;
    privateint width;
    privateint height;
    privatePaint backgroundPaint;
    privateboolean isDrawing = false;
    privateBitmap bm;
    privateCanvas canvas;
    privateint heartRadio = 1;
 
    publicHeartView(Context context) {
        super(context);
        init();
    }
 
    publicHeartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
 
    privatevoid init() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        garden =new Garden();
        backgroundPaint =new Paint();
        backgroundPaint.setColor(Color.rgb(0xff,0xff, 0xe0));
 
 
    }
 
    ArrayList<bloom> blooms =new ArrayList<>();
 
    publicPoint getHeartPoint(floatangle) {
        floatt = (float) (angle / Math.PI);
        floatx = (float) (heartRadio * (16* Math.pow(Math.sin(t), 3)));
        floaty = (float) (-heartRadio * (13* Math.cos(t) - 5* Math.cos(2 * t) - 2 * Math.cos(3* t) - Math.cos(4* t)));
 
        returnnew Point(offsetX + (int) x, offsetY + (int) y);
    }
 
 
    //绘制列表里所有的花朵
    privatevoid drawHeart() {
        canvas.drawRect(0,0, width, height, backgroundPaint);
        for(Bloom b : blooms) {
            b.draw(canvas);
        }
        Canvas c = surfaceHolder.lockCanvas();
 
        c.drawBitmap(bm,0, 0,null);
 
        surfaceHolder.unlockCanvasAndPost(c);
 
    }
 
    publicvoid reDraw() {
        blooms.clear();
 
 
        drawOnNewThread();
    }
 
    @Override
    publicvoid draw(Canvas canvas) {
        super.draw(canvas);
 
    }
 
    //开启一个新线程绘制
    privatevoid drawOnNewThread() {
        newThread() {
            @Override
            publicvoid run() {
                if(isDrawing) return;
                isDrawing =true;
 
                floatangle = 10;
                while(true) {
 
                    Bloom bloom = getBloom(angle);
                    if(bloom != null) {
                        blooms.add(bloom);
                    }
                    if(angle >= 30) {
                        break;
                    }else {
                        angle +=0.2;
                    }
                    drawHeart();
                    try{
                        sleep(20);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                isDrawing =false;
            }
        }.start();
    }
 
 
    privateBloom getBloom(floatangle) {
 
        Point p = getHeartPoint(angle);
 
        booleandraw = true;
        /**循环比较新的坐标位置是否可以创建花朵,
         * 为了防止花朵太密集
         * */
        for(int i = 0; i < blooms.size(); i++) {
 
            Bloom b = blooms.get(i);
            Point bp = b.getPoint();
            floatdistance = (float) Math.sqrt(Math.pow(p.x - bp.x,2) + Math.pow(p.y - bp.y,2));
            if(distance < Garden.Options.maxBloomRadius * 1.5) {
                draw =false;
                break;
            }
        }
        //如果位置间距满足要求,就在该位置创建花朵并将花朵放入列表
        if(draw) {
            Bloom bloom = garden.createRandomBloom(p.x, p.y);
            returnbloom;
        }
        returnnull;
    }
 
 
    @Override
    publicvoid surfaceCreated(SurfaceHolder holder) {
 
 
    }
 
    @Override
    publicvoid surfaceChanged(SurfaceHolder holder,int format, int width, intheight) {
 
        this.width = width;
        this.height = height;
        //我的手机宽度像素是1080,发现参数设置为30比较合适,这里根据不同的宽度动态调整参数
        heartRadio = width *30 / 1080;
 
        offsetX = width /2;
        offsetY = height /2 - 55;
        bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        canvas =new Canvas(bm);
        drawOnNewThread();
    }
 
    @Override
    publicvoid surfaceDestroyed(SurfaceHolder holder) {
 
    }
}
 
</bloom></code>

还有两个比较重要的工具类
Point.java保存点信息,或者说是向量信息。包含向量的基本运算。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<code class="language-java hljs ">packagecom.hc.testheart;
 
/**
 * Package com.hc.testheart
 * Created by HuaChao on 2016/5/25.
 */
public class Point {
 
    publicint x;
    publicint y;
 
    publicPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    //旋转
    publicPoint rotate(floattheta) {
        intx = this.x;
        inty = this.y;
        this.x = (int) (Math.cos(theta) * x - Math.sin(theta) * y);
        this.y = (int) (Math.sin(theta) * x + Math.cos(theta) * y);
        returnthis;
    }
 
    //乘以一个常数
    publicPoint mult(floatf) {
        this.x *= f;
        this.y *= f;
        returnthis;
    }
 
    //复制
    publicPoint clone() {
        returnnew Point(this.x,this.y);
    }
 
    //该点与圆心距离
    publicfloat length() {
        return(float) Math.sqrt(this.x *this.x + this.y * this.y);
    }
 
    //向量相减
    publicPoint subtract(Point p) {
        this.x -= p.x;
        this.y -= p.y;
        returnthis;
    }
 
    //向量相加
    publicPoint add(Point p) {
        this.x += p.x;
        this.y += p.y;
        returnthis;
    }
 
    publicPoint set(int x, int y) {
        this.x = x;
        this.y = y;
        returnthis;
    }
}
 
 
</code>

工具类MyUtil.java主要是产生随机数、颜色等

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<code class="language-java hljs ">packagecom.hc.testheart;
 
import android.graphics.Color;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/25.
 */
public class MyUtil {
 
    publicstatic float circle = (float) (2* Math.PI);
 
    publicstatic int rgba(int r, int g, intb, int a) {
        returnColor.argb(a, r, g, b);
    }
 
    publicstatic int randomInt(int min,int max) {
        return(int) Math.floor(Math.random() * (max - min +1)) + min;
    }
 
    publicstatic float random(float min,float max) {
        return(float) (Math.random() * (max - min) + min);
    }
 
    //产生随机的argb颜色
    publicstatic int randomrgba(int rmin, int rmax,int gmin, int gmax, intbmin, int bmax, int a) {
        intr = Math.round(random(rmin, rmax));
        intg = Math.round(random(gmin, gmax));
        intb = Math.round(random(bmin, bmax));
        intlimit = 5;
        if(Math.abs(r - g) <= limit && Math.abs(g - b) <= limit && Math.abs(b - r) <= limit) {
            returnrgba(rmin, rmax, gmin, gmax);
        }else {
            returnrgba(r, g, b, a);
        }
    }
 
    //角度转弧度
    publicstatic float degrad(float angle) {
        returncircle / 360 * angle;
    }
}
</code>

好了,目前为止,就可以得到上面的效果了。

1 0