美文网首页
转载:谁说程序员不浪漫---Android爱心表白

转载:谁说程序员不浪漫---Android爱心表白

作者: 拿头撞鸡 | 来源:发表于2016-05-25 16:45 被阅读578次

    实验室的小伙伴写的,这里友情转载下,前几天刚刚看到的贝塞尔曲线,小伙伴就花了一天时间搞了出来。应小伙伴要求,特此转载至简书中。
    原文链接CSDN 打造浪漫的Android表白程序
    简书链接打造浪漫的Android表白程序

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

    最终效果

    生成心形线

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

    $x=16 \times sin^3 \alpha $
    $y=13 \times cos \alpha -5\times cos 2\alpha-2 \times cos 3\alpha -cos 4\alpha$

    如果生成的桃心线不够大,可以吧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<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;
        }
    }
    
    
    

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

    考虑到刷新的比较频繁,选择使用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<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) {
    
        }
    }
    
    
    

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

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

    相关文章

      网友评论

          本文标题:转载:谁说程序员不浪漫---Android爱心表白

          本文链接:https://www.haomeiwen.com/subject/ewetdttx.html