几条曲线构建Android表白程序

作者: 呼啸长风 | 来源:发表于2017-09-03 15:26 被阅读1751次

    每年的情人节和七夕,甜蜜与痛苦的日子,做点什么好呢?
    写诗画画送礼物,逛街吃饭看电影?
    作为搬砖爱好者,写个表白脚本或者动画什么的吧。
    想起之前看到的一段H5动画,在Android平台“临摹”了一遍。
    效果如下图:其构图还是比较简单的,树枝加上由心形花瓣构成的心形树冠(后面做成动画之后会有随机的花瓣飘落)。


    一、树枝

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

    准备数据
    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<Branch> 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);
        }
    }
    
    

    效果图如下:


    二、花瓣

    花瓣的绘制,是通过一条曲线实现的:本文的主角,自带爱情故事的心形线
    心形线有很多种,有的用标准方程表示,有的用参数方程表示。
    对于绘制曲线来说,参数方程更方便一些。
    在网站wolframalpha上,可以输入方程直接预览曲线。

    计算心形线
    因为要绘制很多花瓣,所以可以将其形状预先计算好,缓存起来。
    或许是因为精度的原因, 如果直接采样上图的点,绘制时如果有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
    我们需要做的,是在心形内部选取位置,而非绘制曲线,故此,标准方程相对于参数方程更合适。

    坐标系中的点(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<Bloom> 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();
    }
    

    后记

    • 本来打算七夕前的周末搞定它的,无奈很多知识忘记了,需要回头温习,没赶上。
      很多时候就是这样,学的时候不知道有什么用,用的时候又记不起来-_-
    • 调整参数也消耗不少时间,写代码比较客观,调参数则比较主观:方位摆放,显示大小,动画快慢……
    • 构图中左上角有留白,可以在那里输出一些表白文字。
    • 考虑到移动端的流量,动图部分只截取最后一个阶段的动画。
    • 篇幅限制,文中只是贴了部分代码,完整代码可到github下载HeartTree

    相关文章

      网友评论

      • a7e770b66356:大佬,这个程序只可以兼容到api19,不可以向下兼容了,请问应该修改哪里啊
      • quanCN:可以组件化么,在布局文件中声明就可以么?
        quanCN:@Horizon757 好滴,感谢😊
        呼啸长风:需要添加构造函数:
        public TreeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        }

        如果有自定义属性的话,需要添加:
        public TreeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        }
      • SharryChoo:哈哈哈, 漂亮的要死, 好棒呀~
        呼啸长风:@FrankChoo thanks
      • TeAmo_2df6:Android 新手请问 怎么在留白那写文字
        呼啸长风:canvas.drawText
      • 当个海贼多好:有没有Demo可不可以发下看看
        当个海贼多好:@冬眠99 谢谢
        呼啸长风:@jun莫笑 文章最后有github地址,可到github下载
      • sergiojune:666,我也想弄一个,只不过初学者看不懂,想问要学完哪些安卓知识可以做?
        sergiojune: @冬眠99 想问下,学这个暂时不需要用到openGL吧?
        sergiojune: @冬眠99 你说的我基本都没学过,现在在自学中
        呼啸长风:这个程序所用的Android知识不是很多,主要就是Canvas, Paint, Path等的基本概念和用法。
        然后就是一些基本的计算机图形学知识。
        还有就是思想,OpenGL,WebGL,和Android的Canvas绘图都有相通之处,建议可以去了解一下OpenGL。
        最重要的是实践,从简单地开始,通过操作去理解和感悟。
      • ZkJanus:666 明年七夕 可以用
        呼啸长风:@ZkJanus :smile:
      • 依然范特稀西:厉害了
        呼啸长风:@依然范特稀西 thanks
      • 3bc452e215ab:666
        呼啸长风:@师傅被大师兄捉走了 多谢捧场
      • 4e195e0f2241:高啊,光数学公式在动画的应用都够我学习很久的
        呼啸长风:@苦海迷路人 很多地方可以看到贝塞尔曲线的身影
      • 梦想编织者灬小楠:666
        呼啸长风:@梦想编织者灬小楠 谢谢支持
      • 莫离境:666
        呼啸长风:@莫离境 谢谢

      本文标题:几条曲线构建Android表白程序

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