本文转载于  huachao1001的专栏

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

生成心形线

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

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

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

  1. public Point getHeartPoint(float angle) {
  2. float t = (float) (angle / Math.PI);
  3. float x = (float) (19.5 * (16 * Math.pow(Math.sin(t), 3)));
  4. float y = (float) (-20 * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)));
  5. return new Point(offsetX + (int) x, offsetY + (int) y);
  6. }

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

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

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

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. float angle = 10;
  4. while (angle < 180) {
  5. Point p = getHeartPoint(angle);
  6. canvas.drawPoint(p.x, p.y, paint);
  7. angle = angle + 0.02f;
  8. }
  9. }
  1. 运行结果如下:

绘制花瓣原理

我们想要的并不是简单绘制一个桃心线,要的是将花朵在桃心线上摆放。首先,得要知道怎么绘制花朵,而花朵是由一个个花瓣组成。因此绘制花朵的核心是绘制花瓣。绘制花瓣的原理是: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. }

花瓣类是最重要的类,因为真正绘制在屏幕上的是一个个小花瓣。每个花朵包含一系列花瓣,花朵类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. public Bloom(Point point, int radius, int color, int petalCount) {
  22. this.point = point;
  23. this.radius = radius;
  24. this.color = color;
  25. petals = new ArrayList<>(petalCount);
  26.  
  27. float angle = 360f / petalCount;
  28. int startAngle = MyUtil.randomInt(0, 90);
  29. for (int i = 0; i < petalCount; i++) {
  30. //随机产生第一个控制点的拉伸倍数
  31. float stretchA = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
  32. //随机产生第二个控制地的拉伸倍数
  33. float stretchB = MyUtil.random(Garden.Options.minPetalStretch, Garden.Options.maxPetalStretch);
  34. //计算每个花瓣的起始角度
  35. int beginAngle = startAngle + (int) (i * angle);
  36. //随机产生每个花瓣的增长因子(即绽放速度)
  37. float growFactor = MyUtil.random(Garden.Options.minGrowFactor, Garden.Options.maxGrowFactor);
  38. //创建一个花瓣,并添加到花瓣列表中
  39. this.petals.add(new Petal(stretchA, stretchB, beginAngle, angle, color, growFactor));
  40. }
  41. }
  42.  
  43. //绘制花朵
  44. public void draw(Canvas canvas) {
  45. Petal p;
  46. for (int i = 0; i < this.petals.size(); i++) {
  47. p = petals.get(i);
  48. //渲染每朵花朵
  49. p.render(point, this.radius, canvas);
  50.  
  51. }
  52.  
  53. }
  54.  
  55. public int getColor() {
  56. return color;
  57. }
  58. }

接下来是花园类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. }

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

还有两个比较重要的工具类
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. }

工具类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. }

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

浪漫桃心的Android表白程序的更多相关文章

  1. 表白程序源代码,android

    弄了一个表白程序,还是不错的,内容能够自己设置.并附上源代码:http://download.csdn.net/detail/a358763471/7803571 看下效果图吧.是动画的哦...

  2. 零基础入门C/C++实现你的浪漫表白:浪漫流星雨表白程序

    想要讨女朋友欢心也巩固自己所学的知识,各位小伙伴有自己的想法了吗?准备好想要怎样实施了吗?有什么美好的计划了吗?如果没有的话那么别慌,我知道,在座的各位肯定都是有自己的心仪的姑娘,那么今天就教大家一招 ...

  3. android表白app

    一.前言 马上就要520和521了,是不是还有像我一样的单身狗啊.我就知道有,所以这两天简单写了这个小程序(其实是替别人写的),虽然我并不会用去骗女孩子(因为最近太忙了,实习完之后要搞毕设,要搞论文啊 ...

  4. 与Android应用程序相关的文件目录都有哪些?(转载)

    与Android应用程序相关的文件目录都有哪些? | 浏览:1312 | 更新:2014-09-28 19:43 | 标签:android 一.方法介绍:   每个Android应用程序都可以通过Co ...

  5. [转]Android应用程序框架思路整理

    一.一般Android应用程序架构(Book,购彩,Market). 普通的应用程序由于只需要用到Android的联网与显示的功能,所以应用程序大体上是呈现为UI(Activities)与网络(Net ...

  6. android的程序运行数据存放在哪里?

    Android应用开发中,给我们提供了5种数据的存储方式1 使用SharedPreferences存储数据2 文件存储数据3 SQLite数据库存储数据4 使用ContentProvider存储数据5 ...

  7. Android卸载程序之后跳转到指定的反馈页面

    一个应用被用户卸载肯定是有理由的,而开发者却未必能得知这一重要的理由,毕竟用户很少会主动反馈建议,多半就是用得不爽就卸,如果能在被卸载后获取到用户的一些反馈,那对开发者进一步改进应用是非常有利的.目前 ...

  8. 使用 Eclipse PhoneGap 构建 Android 应用程序入门

    Eclipse 是一种支持多种技术的开源集成开发环境 (IDE),但本文重点介绍 Java 支持,这也是 Android 应用程序的“母语”.Android 是 Google 发布的开源移动操作系统. ...

  9. IONIC 开发的Android应用程序签名(或重新签名)详解

    完全通过DOS命令来完成apk签名 给apk签名一共要用到3个工具,或者说3个命令,分别是:keytool.jarsigner和zipalign,下面是对这3个工具的简单介绍:            ...

随机推荐

  1. TatukGIS-TGIS_Editor.CreateShape

    procedure CreateShape(const _layer: TObject; const _ptg: TGIS_Point3D; const _type: TGIS_ShapeType; ...

  2. Asp.net MVC分页实例

    分页是网页基本功能,这里主要讨论在Asp.net MVC环境下分页的前端实现,不涉及后台分页.实现效果如下图显示: Step 1.建立分页信息类 public class PagingInfo { p ...

  3. 省队集训Day3 light

    [问题描述] “若是万一琪露诺(俗称 rhl)进行攻击,什么都好,冷静地回答她的问题来吸引她.对方表现出兴趣的话,那就慢慢地反问.在她考虑答案的时候,趁机逃吧.就算是很简单的问题,她一定也答不上来.” ...

  4. Mac电脑手动清理

    Xcode的无用的文件如图找到DerivedData文件夹,删除里面文件就可以了(先找到隐藏的文件)

  5. WPF学习笔记-如何按ESC关闭窗口

    如何按ESC关闭窗口? 在InitializeComponent();下面增加KeyDown事件,如: public ModifyPrice() { InitializeComponent(); th ...

  6. nginx下搭建fastcgi的开发环境

    在上一章最简单理解CGI,FastCGI,WSGI  我们将fastcgi规范类比HTTP.下面我们通过一个案例更加明白fastcgi 我们使用的是 nginx作为前端 代理,我们包装了gevent_ ...

  7. db2索引创建和删除

    在表trpt_jv_rpt_log_td的tim列上创建索引 create index dw_rpt.irpt_jv_rpt_log_td_tim on dw_rpt.trpt_jv_rpt_log_ ...

  8. Keil MDK与h-jtag联调

    keil MDK也是可以借助h-jtag进行单步调试,写出来与大家一起分享一下. keil MDK编译器使用V4.01版本,下载地址:http://www.embedinfo.com/down-lis ...

  9. J2EE和EJB有什么关系?

    其实j2EE就是java的企业版,与javaSE(标准版)有是有区别的,所以运行环境会有差异(简单点可以说,这是两组不同的接口),我们一般所熟悉的tomcat仅仅只实现了j2ee的一小部分规范,它只是 ...

  10. Java 8 中的 Streams API 详解

    为什么需要 Stream Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念.它也不同于 StAX 对 ...