浪漫桃心的Android表白程序

本文转载于  huachao1001的专栏

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

这里写图片描述

生成心形线

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

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

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

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);
 }

 

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

 offsetX = width / 2;
 offsetY = height / 2 - 55;

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

 @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;
        }
   }

 

运行结果如下:

显示的心形线

绘制花瓣原理

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

这里写图片描述

将花朵绘制到桃心线上

一大波代码来袭

首先定义花瓣类Petal:

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

 

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

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

 

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

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

 

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

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

 

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

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

 

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

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

 

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

posted @ 2016-08-18 11:29  Sharley  阅读(5768)  评论(2编辑  收藏  举报