Flutter:从零开始开发一款[骑行记录仪.APK],大佬勿喷!

458 阅读5分钟

本文将分享我在Flutter框架下开发这款应用的全过程,包括环境搭建、界面设计、视频录制、获取运动信息、速度获取以及视频绘制等关键技术点。

一次偶然的机会,我看到一位大佬开发一款能够在骑行时录制视频并实时显示速度的IOS应用。

作为一名骑行爱好者,我深知这是一个用来骑行秀腿功的好机会。但是这位大佬还没开发安卓应用,那只能我自己动手了。很多抖音博主,他们发布的视频上方都带有实时速度显示,他们一般是通过佳明手表的运动数据文件后期通过视频二合一,这即费钱、又费时间。所以说开发这样一款软件还是有必要的。

先上我的第一版,大家可以边体验边看文章:

github.com/GreenJeff1/…

Flutter || UniApp 选哪个?

首次开发手机应用、不懂任何相关语言的我,最开始是想尝试 UNI,因为我比较擅长使用VUE,而且也有过几次UNI开发小程序的经验,但是发现UNI的插件市场好像并不能满足我的需求,大部分能用上的插件都收费很高,能用的也少。(这里我并不是恶意的喷uni,确实没法满足我的需求。/(ㄒoㄒ)/~~)

Flutter,之前经常有看到相关的分享文章,了解了他们的依赖市场后,感觉能满足软件的大部分需求,那我只好乘着机会来了解一下了。

环境搭建

Flutter是一个由Google开发的开源移动UI框架,它允许开发者使用Dart语言快速在iOS和Android上构建高质量的原生界面。我的第一步是搭建开发环境:

  • 下载并安装Flutter SDK。
  • 配置Android Studio,确保其支持Flutter开发。
  • 安装必要的插件,Dart和Flutter。

这一步骤,他的安装不像 electron 直接node,npm一下就能完事、也不像python 直接下载安装包安装就可以搞定。我还是建议大家安装前,多去看几篇环境搭建的相关文章,再去动手也不迟,而且最好是看最新发布的文章!

界面设计

不得不说,这个界面绘制对于初学者,学习起来还是比较困难的。好在之前有接触过 Python Thinter,布局概念其实差不多,比较好理解。

应用程序的界面设计包括主页和录制页两、视频展示页面三个部分。我的审美很差,不懂UI设计,在主页上就是一副图三个按钮。录制页则是应用的核心功能所在,用户可以在这里开始/结束录制骑行视频,还展示了一些基本信息,如用户当前的速度、行程里程等。

为了实现这些界面,我利用Flutter提供的丰富Widget组件进行布局和美化。比如使用Scaffold组件作为应用程序的基础框架,AppBar作为顶部导航栏,ColumnRow组件进行界面元素的垂直和水平排布。

image.png

首页绘制代码如下:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('骑行日记'),
    ),
    body: Padding(
      padding: const EdgeInsets.all(20.0), 
      child: Column(
        children: [
          Expanded(
            child: Container(
              width: double.infinity,
              color: Colors.grey[200],
              child: Image.asset(
                'assets/img/wallpaper.webp', 
                fit: BoxFit.fitWidth, // 调整图片填充方式
              ),
            ),
          ),
          SizedBox(height: 40), // 在图片区域和按钮之间添加间距
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              ElevatedButton(
                onPressed: () {
                  EasyLoading.showToast("开发中");
                },
                child: Icon(Icons.settings,
                    color: Colors.black), 
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.white,
                  shape: CircleBorder(), 
                  padding: EdgeInsets.all(12), 
                ),
              ),
              ElevatedButton(
                onPressed: () async {
                  // 检查并请求相机和位置权限
                  Map<Permission, PermissionStatus> statuses = await [
                    Permission.camera,
                    Permission.location,
                  ].request();

                  if (statuses[Permission.camera]!.isGranted &&
                      statuses[Permission.location]!.isGranted) {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => CameraPage()),
                    );
                  } else {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('需要相机和位置权限来使用此功能')),
                    );
                  }
                },
                child: Text('GO', style: TextStyle(fontSize: 20)),
                style: ElevatedButton.styleFrom(
                    shape: CircleBorder(), padding: EdgeInsets.all(24)),
              ),
              ElevatedButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => VideoPage()),
                  );
                },
                child: Icon(Icons.movie_rounded,
                    color: Colors.black), // 使用Icon组件并设置图标
                style: ElevatedButton.styleFrom(
                    shape: CircleBorder(),
                    backgroundColor: Colors.white,
                    padding: EdgeInsets.all(12)),
              ),
            ],
          ),
          SizedBox(height: 48), // 在图片区域和按钮之间添加间距
        ],
      ),
    ),
  );
}

视频录制

视频录制界面,我不光仅仅需求能够调用手机摄像头,帮我实现录制功能,还要他能够在 视频的上方,能够实时的显示 运动信息。由于这个功能的需求,我放弃了对UNI的选择。

image.png

import 'package:camera/camera.dart';

我选择使用Flutter camera插件来实现。

需要申请相机权限,并在界面上展示预览画面。camera插件提供了简单的API,让我可以快速集成摄像头功能。在用户点击录制按钮时,我会开始录制视频,并将视频文件保存到本地。

//初始化视频插件
CameraController? _controller;

Future<void> _initializeCamera() async {
  cameras = await availableCameras();
  _controller = CameraController(cameras[0], ResolutionPreset.high);
  await _controller!.initialize();
  setState(() {});

}
//绘制到界面
Expanded(
  child: CameraPreview(_controller!),
),
//录制视频
await _controller!.startVideoRecording();

//停止录制,返回文件位置
final XFile videoFile = await _controller!.stopVideoRecording();

还需要在录制视频保持屏幕亮屏

import 'package:wakelock/wakelock.dart';

获取实时运动信息

这一个步骤是一个巨坑!!!

geolocator 是一个Flutter插件,提供了一个简单易用的API来访问特定平台的地理位置服务。它支持获取设备的最后已知位置、当前位置、当前速度、连续位置更新、检查设备上是否启用了位置服务,以及计算两点之间的距离和方位。

最开始我是通过这样来获取当前的速度和总里程:

Position position = await Geolocator.getCurrentPosition();
print('当前位置:${position.latitude}, ${position.longitude}');
print('当前速度:${position.speed}');

可是到我最后测试时,(我是在每次开车时,打开我的测试版软件,与自己的汽车码表进行对比)。我发现了问题:

软件显示速度与我汽车码表速度严重不匹

原因1:

这个很好发现,因为geolocator 获取的速度是 m/s,而汽车码表是KM/H,我们只需要 position.speed * 3.6 即可换算出 KM 单位的速度。

原因2:

发现即使转换单位后,依然与我实际的速度有偏差。而且偏差相当大,有20码左右,这样肯定是不行的。又是半天时间的查阅资料,在隔壁技术论坛发现了一位大佬的分享,再次感谢🙏

image.png

geolocator 在安卓手机是调用了谷歌的接口,而国内想要通过这个来获取位置信息,还是有较大偏差的。

没办法了,只有换一个收费的SDK,高德地图。目前据说是个人免费,也不确定后面会不会收费😄

import 'package:amap_flutter_location/amap_flutter_location.dart';

这个操作不再细讲,去开发者官网申请API,都是有教程的,换上之后,速度和汽车码表对比下来,基本一致。

// 通过调用高德定位api 可以打印当前经纬、速度、具体位置、时间等等
_locationListener = _locationPlugin
    .onLocationChanged()
    .listen((Map<String, Object> result) {
  setState(() {
    _locationResult = result;
    print(result);
  });
});

也不知道为啥汽车不用对接高德也能获取准确的当前速度,而安卓手机却要花钱对接高德才能准确获取。可能是我获取的方式错了?有懂的大神可以评论解答一下...

视频绘制实时运动信息

image.png

录制时,想在视频上面显示 实时速度信息,非常简单,只需要通过绘制界面的方式,漂浮视频上方即可。

但是,我需要保存视频时,也将这些信息,实时显示在视频上方,需要对视频进行二次处理。这又是一个难点。

我想到的第一个解决方案:

在录制视频时,软件通过代码进行自动屏幕录制,直接把拍摄的内容+悬浮信息,给录制下来,当点下停止按钮,则自动停止录制屏幕。这可能会影响到视频的输出清晰度,可能在很多人眼里非常搞笑,但确实这是一个小白的无奈之举。

方案二,操作起来坑点太多

ffmpeg_kit_flutter

ffmpeg 这个工具,大家应该经常能看到,大部分需要处理视频的软件,无论电脑添加背景音乐、服务器直播推流、都会用到他。

ffmpeg_kit_flutter 这个就是flutter的专用插件。我之前没有用过这工具来开发软件,这回用起来真的坑点太多。命令错误、字体无法调用、每秒添加不同的内容、解码器设置 等等,各种问题需要解决。各种心酸,真的是开发的时候太多次想放弃了!

好在我坚持了下来,目前效果还是不错的。我通过 每秒获取 当前的实时数据,记录在一个数组 locations 中,保存视频时,循环数组,将每个子数据,通过ffmpeg命令添加到视频中。

String ffmpegCommand =
    "-y -i $videoPath -vf "$watermarkBox$filters" -c:v libx264 -crf 28 -b:v 2M -c:a aac -threads 4 $outputPath";
print('FFmpeg logs:' + ffmpegCommand);

FFmpegKit.executeAsync(ffmpegCommand, (session) async {
  final returnCode = await session.getReturnCode();
  final logs = await session.getLogs();


  print('FFmpeg logs:');
  for (final log in logs) {
    print(log.getMessage());
  }


  if (ReturnCode.isSuccess(returnCode)) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => VideoPage()),
    );
  } else {
    print('Error occurred during video processing');
  }
});

保存成功后,就是内置一个视频列表页面,可以播放和保存视频到本地,这里比较简单,不想再写了,写的很累🤣

总结

通过开发这款安卓骑行视频录制应用,我不仅学习到了Flutter的相关知识,还积累了跨平台开发的宝贵经验。在界面搭建、视频录制、定位等方面的挑战,让我对Flutter的各种插件和API有了更深入的了解。

首次Flutter开发体验、首次发文章分享,求各位大佬不要喷!!