flutter 实现按轨迹移动与旋转

最终效果:


完成需要三个要素:

1.画一条贝塞尔曲线。

2.根据贝塞尔计算小车的移动轨迹。

3.计算小车的角度。

还是挺简单的吧。

开搞。。

首先我们来画一个简单的布局和一些基础的变量

画线

这里用到了CustomPainter。线不是必须的,只是为了好观察

计算小车的位置

这里主要靠一个公式,来求出贝塞尔曲线的路径点

var t = animation.value;

x偏移量=(pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) .toDouble()

y偏移量 =(pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) .toDouble();

计算出新的位置之后就可以了。这里可以根据动画的进度,计算出相应的贝塞尔曲线路径点。我们知道,动画的默认值是0-1 ,所以这就意味着,如果你想手动控制车辆在路径上的位置,而不是通过动画,那么你知道设置这个值就可以了,比如0.5,就是移动到路径的中间位置,0是开始位置,1是结束位置。

效果如下(车没了,用这个凑活一下 。)

按轨迹移动已经做完了,那我们完活收工!

开玩笑的。这东西是不是和学了三年动画的兄弟做出来的有一拼了。

现在还有两个问题

车辆的位置和车辆的角度。

可以发现偏移量的基准点是车组件的左上角。那我们计算的时候是不是可以把车辆的宽度计算进去是不是就可以了呢? 是的,就是这么简单 。

现在来调整一下在计算车辆偏移量的地方

已知车辆组件的宽度是50,那我们把它减去一半,就可以得到它的中心点

x偏移量=(pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) -25.toDouble()

y偏移量 =(pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) -25.toDouble();

来看效果


我们成功了,让小车的中间移动到了我们想要的位置。

接下来解决转向的问题,我的解题思路是这样的:

线是由点构成的,所以我们上面计算出了贝塞尔曲线的路径点,并将其运用到了移动当中。

那么我们是否只需要计算两个移动点之间相差的是否就可以了呢 


例如我们有A、B、C、D4个点,a、b两条线,A点在a线上,B点在b线上(点画的有点大,示意图,大家凑合一下),我们的初始位置是A,然后我们把AB两点连线,然后计算AB线与a线的夹角,这样我们就知道了,B点在A点多少度的位置。然后B的角度,就是我们需要转向的角度。

思路大概就是这样。下面开始实操。其实数学基础好的同学,到这里,就已经知道该怎么做了,自己就可以动工了。我得出这个结论之后,又找了半天才知道原来有这样的一个函数,可以帮助计算。

这个函数是atan2。下边是百度的解释。

atan2是一个函数,在C语言里返回的是指方位角,C 语言中atan2的函数原型为 double atan2(double y, double x) ,返回以弧度表示的 y/x 的反正切。y 和 x 的值的符号决定了正确的象限。也可以理解为计算复数 x+yi 的辐角,计算时atan2 比 atan 稳定。 

嗯~懂得都懂。 

不懂的咱就先说结果 ,这个函数能帮助我们计算出我们所需要的角度,即:AB线与a线的夹角

atan2的结果范围是-3.14-3.14 ,也就是正负圆周率,它是一个弧度。

还得用一张图,借用一下百度的:


图中的y和x可以对应到atan2(y,x)中,这样可以计算出图中x轴和r半径的夹角,是不是和我们的需求一摸一样。

我们需要做的就是计算出这个y和x,然后利用函数就可以了。

现在来更新一下整理一下我们在刷新的时候需要做的事情,1.计算偏移量,2.计算角度。

整合一下,代码如下

运行一下


新的风暴出现了,偏移量发生了问题,不知道偏移到哪去了。

这个问题的原因是因为我们用的Transform组件,这个组件旋转的时候会有一个默认轴线(组件的左上角),围绕这个进行旋转,所以我们只需要设置一下他的中心点就可以了 有两个设置方法,一个是自动一个是手动

以下是完整代码,运行环境是 flutter2.2.3

引入了 flutter_svg 插件,用于使用svg,可以把内容换成其他的。


import 'dart:math';

import 'package:byb/page/test/xian.dart';

import 'package:flutter/animation.dart';

import 'package:flutter/material.dart';

import 'package:flutter_svg/svg.dart';

class ActiveScreen extends StatefulWidget {

  static String routeName = 'activeName';

  @override

  _ActiveScreenState createState() => _ActiveScreenState();

}

class _ActiveScreenState extends State<ActiveScreen>

    with TickerProviderStateMixin {

  var p0;

  var p1;

  var p2;

  late DataModle dataModle;

  late AnimationController animationController;

  late Animation animation;

  @override

  void initState() {

    // 开始点

    p0 = Offset(100, 100);

    // 控制点

    p1 = Offset(50, 150);

    //结束点

    p2 = Offset(300, 300);

    dataModle = DataModle();

    // 定义动画控制器

    animationController =

        AnimationController(vsync: this, duration: Duration(seconds: 3));

    // 定义动画

    animation =

        // Tween(begin: Offset(100, 100), end: Offset(100, 201))

        Tween(begin: 0.0, end: 1.0)

            .chain(CurveTween(curve: Curves.linear))

            .animate(animationController);

    animation.addListener(() {

      updateOffset();

    });

    updateOffset();

    super.initState();

  }

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        actions: [

          IconButton(

              icon: Icon(Icons.play_arrow),

              onPressed: () {

                animationController.reset();

                animationController.forward();

              })

        ],

      ),

      body: Stack(

        children: [

          Transform(

            origin: Offset(25, 25), // 手动调整,适合内容不是正方形的场景

            // alignment: Alignment.center, // 自动居中,内容是正方形的时候可以用这个

            transform: Matrix4.identity()

              ..translate(dataModle.left, dataModle.top, 0.0)

              ..rotateZ(dataModle.rotate),

            child: Container(

              height: 50,

              width: 50,

              color: Colors.red,

              child: SvgPicture.asset("assets/icons/move_right.svg"),

            ),

          ),

          CustomPaint(

            painter: Xian(p0: p0, p1: p1, p2: p2),

          ),

        ],

      ),

    );

  }

  updateOffset() {

    // 获取动画的当前值

    var t = animation.value;

    // 得到组件当前的位置

    Offset currentOffset = Offset(dataModle.left, dataModle.top);

    // 计算并重新赋值

    dataModle.left =

        (pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) -

            25.toDouble();

    dataModle.top =

        (pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) -

            25.toDouble();

    // 求出旋转弧度 下一个点对于现在这个点的弧度是多少?

    double rorate = atan2(

        dataModle.top - currentOffset.dy, dataModle.left - currentOffset.dx);

    // double huDu = atan2(

    //    currentOffset.dy - dataModle.top, currentOffset.dx - dataModle.left);

    double angle = rorate / pi * 180;

    print("角度====$angle  弧度====$rorate");

    if (rorate != 0.0) {

      dataModle.rotate = rorate;

    }

    setState(() {});

  }

}

class DataModle {

  double left;

  double top;

  double rotate;

  DataModle({this.left = 0.0, this.top = 0.0, this.rotate = 0.0});

}



import 'package:flutter/material.dart';

class Xian extends CustomPainter {

  final p0;

  final p1;

  final p2;

  Xian({this.p0, this.p1, this.p2});

  @override

  void paint(Canvas canvas, Size size) {

    Paint paint = Paint()

      ..isAntiAlias = true

      ..strokeWidth = .3

      ..style = PaintingStyle.stroke

      ..color = Colors.black

      ..invertColors = false;

    Path path = Path();

    // 移动到起点

    path.moveTo(p0.dx, p0.dy);

    // 以p1为控制点,画一个到p2的曲线

    path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);

    // 绘画

    canvas.drawPath(path, paint);

  }

  @override

  bool shouldRepaint(Xian oldDelegate) {

    return this != oldDelegate;

  }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容