几条曲线构建Android表白程序

简介:

每年的情人节和七夕,甜蜜与痛苦的日子,做点什么好呢?
写诗画画送礼物,逛街吃饭看电影?
作为搬砖爱好者,写个表白脚本或者动画什么的吧。
想起之前看到的一段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%5E3-x%5E2*y%5E3%3D0)。
我们需要做的,是在心形内部选取位置,而非绘制曲线,故此,标准方程相对于参数方程更合适。

坐标系中的点(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
相关文章
|
20天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
2天前
|
JavaScript Java Maven
云效产品使用常见问题之android sdk 构建出aar后,上传到私有maven仓库失败如何解决
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
5天前
|
缓存 移动开发 Android开发
构建高效Android应用:从优化用户体验到提升性能表现
【4月更文挑战第18天】 在移动开发的世界中,打造一个既快速又流畅的Android应用并非易事。本文深入探讨了如何通过一系列创新的技术策略来提升应用性能和用户体验。我们将从用户界面(UI)设计的简约性原则出发,探索响应式布局和Material Design的实践,再深入剖析后台任务处理、内存管理和电池寿命优化的技巧。此外,文中还将讨论最新的Android Jetpack组件如何帮助开发者更高效地构建高质量的应用。此内容不仅适合经验丰富的开发者深化理解,也适合初学者构建起对Android高效开发的基础认识。
2 0
|
5天前
|
移动开发 Android开发 开发者
构建高效Android应用:采用Kotlin进行内存优化的策略
【4月更文挑战第18天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,由于设备和版本的多样性,确保应用流畅运行且占用资源少是一大挑战。本文将探讨使用Kotlin语言开发Android应用时,如何通过内存优化来提升应用性能。我们将从减少不必要的对象创建、合理使用数据结构、避免内存泄漏等方面入手,提供实用的代码示例和最佳实践,帮助开发者构建更加高效的Android应用。
5 0
|
7天前
|
缓存 移动开发 Java
构建高效的Android应用:内存优化策略
【4月更文挑战第16天】 在移动开发领域,尤其是针对资源有限的Android设备,内存优化是提升应用性能和用户体验的关键因素。本文将深入探讨Android应用的内存管理机制,分析常见的内存泄漏问题,并提出一系列实用的内存优化技巧。通过这些策略的实施,开发者可以显著减少应用的内存占用,避免不必要的后台服务,以及提高垃圾回收效率,从而延长设备的电池寿命并确保应用的流畅运行。
|
11天前
|
存储 数据库 Android开发
构建高效安卓应用:采用Jetpack架构组件优化用户体验
【4月更文挑战第12天】 在当今快速发展的数字时代,Android 应用程序的流畅性与响应速度对用户满意度至关重要。为提高应用性能并降低维护成本,开发者需寻求先进的技术解决方案。本文将探讨如何利用 Android Jetpack 中的架构组件 — 如 LiveData、ViewModel 和 Room — 来构建高质量的安卓应用。通过具体实施案例分析,我们将展示这些组件如何协同工作以实现数据持久化、界面与逻辑分离,以及确保数据的即时更新,从而优化用户体验并提升应用的可维护性和可测试性。
|
13天前
|
XML 移动开发 Android开发
构建高效安卓应用:采用Jetpack Compose实现动态UI
【4月更文挑战第10天】 在现代移动开发中,用户界面的流畅性和响应性对于应用的成功至关重要。随着技术的不断进步,安卓开发者寻求更加高效和简洁的方式来构建动态且吸引人的UI。本文将深入探讨Jetpack Compose这一革新性技术,它通过声明式编程模型简化了UI构建过程,并提升了性能与跨平台开发的可行性。我们将从基本概念出发,逐步解析如何利用Jetpack Compose来创建具有数据动态绑定能力的安卓应用,同时确保应用的高性能和良好用户体验。
15 0
|
15天前
|
监控 API Android开发
构建高效安卓应用:探究Android 12中的新特性与性能优化
【4月更文挑战第8天】 在本文中,我们将深入探讨Android 12版本引入的几项关键技术及其对安卓应用性能提升的影响。不同于通常的功能介绍,我们专注于实际应用场景下的性能调优实践,以及开发者如何利用这些新特性来提高应用的响应速度和用户体验。文章将通过分析内存管理、应用启动时间、以及新的API等方面,为读者提供具体的技术实现路径和代码示例。
|
16天前
|
移动开发 API Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【4月更文挑战第7天】 在移动开发领域,性能优化和应用响应性的提升一直是开发者追求的目标。近年来,Kotlin语言因其简洁性和功能性在Android社区中受到青睐,特别是其对协程(Coroutines)的支持,为编写异步代码和处理并发任务提供了一种更加优雅的解决方案。本文将探讨Kotlin协程在Android开发中的应用,揭示其在提高应用性能和简化代码结构方面的潜在优势,并展示如何在实际项目中实现和优化协程。
|
16天前
|
XML 开发工具 Android开发
构建高效的安卓应用:使用Jetpack Compose优化UI开发
【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。