打造浪漫的Android表白程序

原创 2016年08月31日 13:25:28

几年前,看到过有个牛人用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 ">public Point getHeartPoint(float angle) {
  float t = (float) (angle / Math.PI);
  float x = (float) (19.5 * (16 * Math.pow(Math.sin(t), 3)));
  float y = (float) (-20 * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)));
   return new 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
  protected void onDraw(Canvas canvas) {
       float angle = 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 ">
 package com.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 {
    private float stretchA;//第一个控制点延长线倍数
    private float stretchB;//第二个控制点延长线倍数
    private float startAngle;//起始旋转角,用于确定第一个端点
    private float angle;//两条线之间夹角,由起始旋转角和夹角可以确定第二个端点
    private int radius = 2;//花芯的半径
    private float growFactor;//增长因子,花瓣是有开放的动画效果,这个参数决定花瓣展开速度
    private int color;//花瓣颜色
    private boolean isFinished = false;//花瓣是否绽放完成
    private Path path = new Path();//用于保存三次贝塞尔曲线
    private Paint paint = new Paint();//画笔
    //构造函数,由花朵类调用
    public Petal(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);
    }
    //用于渲染花瓣,通过不断更改半径使得花瓣越来越大
    public void render(Point p, int radius, Canvas canvas) {
        if (this.radius <= radius) {
            this.radius += growFactor; // / 10;
        } else {
            isFinished = true;
        }
        this.draw(p, canvas);
    }
 
    //绘制花瓣,参数p是花芯的圆心的坐标
    private void 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 ">package com.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 {
    private int color;//整个花朵的颜色
    private Point point;//花芯圆心
    private int radius; //花芯半径
    private ArrayList<petal> petals;//用于保存花瓣
 
    public Point getPoint() {
        return point;
    }
 
 
    public Bloom(Point point, int radius, int color, int petalCount) {
        this.point = point;
        this.radius = radius;
        this.color = color;
        petals = new ArrayList<>(petalCount);
 
 
        float angle = 360f / petalCount;
        int startAngle = MyUtil.randomInt(0, 90);
        for (int i = 0; i < petalCount; i++) {
            //随机产生第一个控制点的拉伸倍数
            float stretchA = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
            //随机产生第二个控制地的拉伸倍数
            float stretchB = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
            //计算每个花瓣的起始角度
            int beginAngle = startAngle + (int) (i * angle);
            //随机产生每个花瓣的增长因子(即绽放速度)
            float growFactor = MyUtil.random(Garden.Options.minGrowFactor, Garden.Options.maxGrowFactor);
            //创建一个花瓣,并添加到花瓣列表中
            this.petals.add(new Petal(stretchA, stretchB, beginAngle, angle, color, growFactor));
        }
    }
 
    //绘制花朵
    public void draw(Canvas canvas) {
        Petal p;
        for (int i = 0; i < this.petals.size(); i++) {
            p = petals.get(i);
            //渲染每朵花朵
            p.render(point, this.radius, canvas);
 
        }
 
    }
 
    public int getColor() {
        return color;
    }
}
 
</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 ">package com.hc.testheart;
 
import java.util.ArrayList;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/24.
 */
public class Garden {
 
    //创建一个随机的花朵
    public Bloom createRandomBloom(int x, int y) {
        //创建一个随机的花朵半径
        int radius = MyUtil.randomInt(Options.minBloomRadius, Options.maxBloomRadius);
        //创建一个随机的花朵颜色
        int color = MyUtil.randomrgba(Options.minRedColor, Options.maxRedColor, Options.minGreenColor, Options.maxGreenColor, Options.minBlueColor, Options.maxBlueColor, Options.opacity);
        //创建随机的花朵中花瓣个数
        int petalCount = MyUtil.randomInt(Options.minPetalCount, Options.maxPetalCount);
        return createBloom(x, y, radius, color, petalCount);
    }
 
    //创建花朵
    public Bloom createBloom(int x, int y, int radius, int color, int petalCount) {
        return new Bloom(new Point(x, y), radius, color, petalCount);
    }
 
    static class Options {
        //用于控制产生随机花瓣个数范围
        public static int minPetalCount = 8;
        public static int maxPetalCount = 15;
        //用于控制产生延长线倍数范围
        public static float minPetalStretch = 2f;
        public static float maxPetalStretch = 3.5f;
        //用于控制产生随机增长因子范围,增长因子决定花瓣绽放速度
        public static float minGrowFactor = 1f;
        public static float maxGrowFactor = 1.1f;
        //用于控制产生花朵半径随机数范围
        public static int minBloomRadius = 8;
        public static int maxBloomRadius = 10;
        //用于产生随机颜色
        public static int minRedColor = 128;
        public static int maxRedColor = 255;
        public static int minGreenColor = 0;
        public static int maxGreenColor = 128;
        public static int minBlueColor = 0;
        public static int maxBlueColor = 128;
        //花瓣的透明度
        public static 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 ">package com.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 extends SurfaceView implements SurfaceHolder.Callback {
    SurfaceHolder surfaceHolder;
    int offsetX;
    int offsetY;
    private Garden garden;
    private int width;
    private int height;
    private Paint backgroundPaint;
    private boolean isDrawing = false;
    private Bitmap bm;
    private Canvas canvas;
    private int heartRadio = 1;
 
    public HeartView(Context context) {
        super(context);
        init();
    }
 
    public HeartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
 
    private void init() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        garden = new Garden();
        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.rgb(0xff, 0xff, 0xe0));
 
 
    }
 
    ArrayList<bloom> blooms = new ArrayList<>();
 
    public Point getHeartPoint(float angle) {
        float t = (float) (angle / Math.PI);
        float x = (float) (heartRadio * (16 * Math.pow(Math.sin(t), 3)));
        float y = (float) (-heartRadio * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)));
 
        return new Point(offsetX + (int) x, offsetY + (int) y);
    }
 
 
    //绘制列表里所有的花朵
    private void 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);
 
    }
 
    public void reDraw() {
        blooms.clear();
 
 
        drawOnNewThread();
    }
 
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
 
    }
 
    //开启一个新线程绘制
    private void drawOnNewThread() {
        new Thread() {
            @Override
            public void run() {
                if (isDrawing) return;
                isDrawing = true;
 
                float angle = 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();
    }
 
 
    private Bloom getBloom(float angle) {
 
        Point p = getHeartPoint(angle);
 
        boolean draw = true;
        /**循环比较新的坐标位置是否可以创建花朵,
         * 为了防止花朵太密集
         * */
        for (int i = 0; i < blooms.size(); i++) {
 
            Bloom b = blooms.get(i);
            Point bp = b.getPoint();
            float distance = (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);
            return bloom;
        }
        return null;
    }
 
 
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
 
 
    }
 
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 
        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
    public void 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 ">package com.hc.testheart;
 
/**
 * Package com.hc.testheart
 * Created by HuaChao on 2016/5/25.
 */
public class Point {
 
    public int x;
    public int y;
 
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    //旋转
    public Point rotate(float theta) {
        int x = this.x;
        int y = this.y;
        this.x = (int) (Math.cos(theta) * x - Math.sin(theta) * y);
        this.y = (int) (Math.sin(theta) * x + Math.cos(theta) * y);
        return this;
    }
 
    //乘以一个常数
    public Point mult(float f) {
        this.x *= f;
        this.y *= f;
        return this;
    }
 
    //复制
    public Point clone() {
        return new Point(this.x, this.y);
    }
 
    //该点与圆心距离
    public float length() {
        return (float) Math.sqrt(this.x * this.x + this.y * this.y);
    }
 
    //向量相减
    public Point subtract(Point p) {
        this.x -= p.x;
        this.y -= p.y;
        return this;
    }
 
    //向量相加
    public Point add(Point p) {
        this.x += p.x;
        this.y += p.y;
        return this;
    }
 
    public Point set(int x, int y) {
        this.x = x;
        this.y = y;
        return this;
    }
}
 
 
</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 ">package com.hc.testheart;
 
import android.graphics.Color;
 
/**
 * Package com.example.administrator.testrecyclerview
 * Created by HuaChao on 2016/5/25.
 */
public class MyUtil {
 
    public static float circle = (float) (2 * Math.PI);
 
    public static int rgba(int r, int g, int b, int a) {
        return Color.argb(a, r, g, b);
    }
 
    public static int randomInt(int min, int max) {
        return (int) Math.floor(Math.random() * (max - min + 1)) + min;
    }
 
    public static float random(float min, float max) {
        return (float) (Math.random() * (max - min) + min);
    }
 
    //产生随机的argb颜色
    public static int randomrgba(int rmin, int rmax, int gmin, int gmax, int bmin, int bmax, int a) {
        int r = Math.round(random(rmin, rmax));
        int g = Math.round(random(gmin, gmax));
        int b = Math.round(random(bmin, bmax));
        int limit = 5;
        if (Math.abs(r - g) <= limit && Math.abs(g - b) <= limit && Math.abs(b - r) <= limit) {
            return rgba(rmin, rmax, gmin, gmax);
        } else {
            return rgba(r, g, b, a);
        }
    }
 
    //角度转弧度
    public static float degrad(float angle) {
        return circle / 360 * angle;
    }
}
</code>

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

打造浪漫的Android表白程序

几年前,看到过有个牛人用HTML5绘制了浪漫的爱心表白动画。地址在这:浪漫程序员 HTML5爱心表白动画。发现原来程序员也是可以很浪……漫…..的(PS:刚过520,被妹子骂不够浪漫)。那么在Andr...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

C语言打造表白程序——教你做一个浪漫的程序员

这个程序的知识点如下: MFC GDI API 1.  Microsoft Foundation Classes Graphics Device Interface Application P...

一个有趣的C语言程序,谁说程序员不懂浪漫

浏览看雪论坛,发现有人讲了一个有趣的C语言程序。原帖地址:http://bbs.pediy.com/showthread.php?p=389887我做了一点点的修改,最后打印出 i O y(中间那个是...

程序员也是会浪漫的->打造浪漫的Android表白程序

一年前,看到过有个牛人用[HTML5](http://lib.csdn.net/base/html5)绘制了浪漫的爱心表白动画,后来又在[华超的这篇文章](http://blog.csdn.net/h...

程序猿的浪漫之二进制表白篇

那天情人节,我给她发了一串数字,她说我不懂浪漫,我
  • ithouse
  • ithouse
  • 2014年07月15日 12:42
  • 3448

不是程序猿不浪漫,而是你不懂

你的他是程序猿吗? 他为你写过怎样的情书? 深情款款,还是别具一格?
  • wdcxylg
  • wdcxylg
  • 2016年08月07日 23:49
  • 203

Android浪漫桃心线

  • 2016年05月25日 16:22
  • 6.97MB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:打造浪漫的Android表白程序
举报原因:
原因补充:

(最多只允许输入30个字)