android程序表白,几条曲线构建Android表白程序

每年的情人节和七夕,做点什么好呢?

写诗画画送礼物,逛街吃饭看电影?

作为搬砖爱好者,写个表白脚本或者动画什么的吧。

想起之前看到的一段H5动画,在Android平台“临摹”了一遍。

效果如下图:其构图还是比较简单的,树枝加上由心形花瓣构成的心形树冠(后面做成动画之后会有随机的花瓣飘落)。

e936a7d371aa

一、树枝

树枝是通过贝塞尔曲线来构造的,二阶贝塞尔曲线。

准备数据

getBranches()函数中,定义各个树枝的位置和形状,最终返回树干。

绘制的时候,先绘制树干,然后绘制其分支,最后绘制分支的分支(只有三层)。

public static Branch getBranches() {

// 共10列,分别是id, parentId, 贝塞尔曲线控制点(3点,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类

主要包含树枝的构建(构造函数,addChild函数),以及绘制。

绘制树枝时,不断地调用grow函数,绘制点(currLen)逐渐靠近末端(maxLen), 树枝的半径逐渐变小;

最终控制点到达树枝末端(currLen==maxLen), 绘制结束。

如果是绘制静态画面,while循环直到grow返回false;

如果是绘制动画, 可通过调用postInvalidate(),不断地对回调绘制函数, 每一帧树枝成长一截。

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

}

}

效果图如下:

e936a7d371aa

二、花瓣

花瓣的绘制,是通过一条曲线实现的:本文的主角,自带爱情故事的心形线。

心形线有很多种,有的用标准方程表示,有的用参数方程表示。

对于绘制曲线来说,参数方程更方便一些。

在网站wolframalpha上,可以输入方程直接预览曲线。

e936a7d371aa

计算心形线

因为要绘制很多花瓣,所以可以将其形状预先计算好,缓存起来。

或许是因为精度的原因, 如果直接采样上图的点,绘制时如果有scale(缩放)操作,可能会显示不平滑;

所以在采样心形线的点时我们放大一定比率(SCALE_FACTOR )。

就像一张图片,如果分辨率是200x200, 缩小到100x100显示,图片还是清晰的,如果放大到400x400,可能会模糊。

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类

一片花瓣,除了形状之外,还有方位,颜色,方向,大小等参数。

故此,和Branch一样,封装了一个类。

花瓣的颜色和方向参数是随机初始化的。

颜色方面,ARGB中Red通道固定为最大值0xff, 效果就是花瓣的颜色为红,紫,黄,白等。

因为要适应移动设备的多分辨率,所以一些参数要根据分辨率来动态设置。

public class Bloom {

protected static float sMaxScale = 0.2f;

protected static int sMaxRadius = Math.round(sMaxScale * Heart.getRadius());

protected static float sFactor;

/**

* 初始化显示参数

* @param resolutionFactor 根据屏幕分辨率设定缩放因子

*/

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;

// 调速器,控制开花动画的快慢

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

}

}

三、树冠

树冠是由数百片花瓣构成,关键点在于确定这些花瓣的位置。

这里用到另一条心形线(x^2 + y^2 -1)^3 - x^2 * y^3 = 0。

我们需要做的,是在心形内部选取位置,而非绘制曲线,故此,标准方程相对于参数方程更合适。

e936a7d371aa

坐标系中的点(x,y), 计算ax+by, 大于0和小于0分别在直线的两侧, x^2 + y^2 - r^2 则分别在圆外和圆内;

这个现象还蛮奇妙的,虽然我不知道这在数学中叫什么-_-。

类似的,在x=[-c, c], y=[-c,c]的范围内随机选取(x^2 + y^2 -1)^3 - x^2 * y^3<0的点,即可使得花瓣的位置错落于心形线中。

private static float r;

private static float c;

/**

* 初始化参数

* @param canvasHeight 画布的高度

* @param crownRadiusFactor 树冠半径的缩放因子

*/

public static void init(int canvasHeight, float crownRadiusFactor){

r = canvasHeight * crownRadiusFactor;

c = r * 1.35f;

}

public static void fillBlooms(List 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;

}

绘制动画

不断地触发onDraw()回调,在每一帧里面改变绘制参数,就形成动画了。

在这个例子中,划分了几个动画阶段,每个阶段各自变化自己的参数,到达一定的状态就切换到下一阶段。

总之,就是分而治之,然后串联起来。

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

// 这个函数只是标记view为invalidate状态,并不会马上触发重绘;

// 标记invalidate状态后,下一个绘制周期(约16s), 会回调onDraw();

// 故此,要想动画平滑流畅,tree.draw(canvas)需在16s内完成。

postInvalidate();

}

}

public void draw(Canvas canvas) {

// 绘制背景颜色

canvas.drawColor(0xffffffee);

// 绘制动画元素

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

}

e936a7d371aa

后记

本来打算七夕前的周末搞定它的,无奈很多知识忘记了,需要回头温习,没赶上。

很多时候就是这样,学的时候不知道有什么用,用的时候又记不起来-_-

调整参数也消耗不少时间,写代码比较客观,调参数则比较主观:方位摆放,显示大小,动画快慢……

构图中左上角有留白,可以在那里输出一些表白文字。

考虑到移动端的流量,动图部分只截取最后一个阶段的动画。

篇幅限制,文中只是贴了部分代码,完整代码可到github下载HeartTree。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值