程序员“520”专属表达方式

232 阅读16分钟
原文链接: mp.weixin.qq.com
码个蛋(codeegg)第 628 次推文

作者:huachao1001

博客:https://juejin.cn/post/6844903437600030728

码仔,你知道今天是什么日子吗?

嘿嘿~当然知道!5.2.0~

哟~ 这也知道!

那是~~还能给你show一段!

看好了!

几年前,看到过有个牛人用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:

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

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

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

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

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

考虑到刷新的比较频繁,选择使用SurfaceView作为显示视图。

自定义一个HeartView继承SurfaceView。

代码如下:

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

还有两个比较重要的工具类 

Point.java保存点信息,或者说是向量信息。包含向量的基本运算。

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

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

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

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

码仔只能帮你到这里了 !

(源码地址请查看原文 ~)

近期文章:

今日问题:

你有没有写过浪漫的程序?

快来码仔社群解锁新姿势吧!社群升级:Max你的学习效率