【Flutter 绘制番外】svg 终篇 - 路径指令


theme: cyanosis

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前情回顾

上两篇我们通过对 svg 路径 M/H/V/L/C/Q/Z 几个指令的解析。把 掘金 logosvg ,转化为 Flutter 的原生路径绘制,并且附加了一些绘制效果。

除了这些外,还有一些其他的指令。本篇的目的就是全面梳理一下 svgpath 标签下的路径命令。如下是一览表:

dart M/m (x,y)+ 移动当前位置 L/l (x,y)+ 直线 H/h (x)+ 水平线 V/v (x)+ 竖直线 Z/z 闭合路径 Q/q (x1,y1,x,y)+ 二次贝塞尔曲线 T/t (x,y)+ 光滑绘制二次贝塞尔曲线 C/c (x1,y1,x2,y2,x,y)+ 三次贝塞尔曲线 S/s (x2,y2,x,y)+ 光滑绘制三次贝塞尔曲线 A/a (rx,ry,xr,laf,sf,x,y) 弧线


一、绝对与相对指令

可能大家看到,每个指令都有 大写字母小写字母。其中 大写字母 表示其后的坐标是 绝对坐标 ,也就是以区域 左上角 为原点的坐标。绝对和相对 坐标是绘制中最基本的概念,很容易理解。本文中的 svg 示例文件源码于 【idraw/extra02svg/base】


1. 绝对坐标移动示例

绝对移动,使用大写字母。如下第二段 M30,30 表示将起点移到 (30,30) 点上,V60 表示横坐标不变,纵坐标到绝对的 60 点。

svg ---->[01_M.svg]---- <path d="M30,20 H80 V40Z M30,30 V60" stroke="#000082" />


2.相对坐标移动示例

而相对移动,使用小写字母。如下 m30,30 表示在以当前路径的 尾点 为参考,坐标移动了多少。在 m30,30 之前,路径的尾点是 (30,20) ;也就说明 m30,30 会把下一段的起点移到 (30+30,20+30) ,即 (60,50) 点。 v30 表示:以当前路径的 尾点 为参考,坐标竖直移动了多少,会直线到 (60,50+30) ,即 (60,80)

svg ---->[02_m_v.svg]---- <path d="M30,20 H80 V40Z m30,30 v30" stroke="#000082" />


3.绝对坐标和相对坐标使用场景

如果能够精确的知道某点的坐标,使用 绝对坐标 自然是最方便的。但很多时候,绝对坐标并能直接获取,比如下面 A 点右移 50,下移 20,到 B 点。 虽然可以根据数据计算出 B 的绝对坐标,但比较麻烦,特别是对于一些曲线路径,相对偏移会非常方便。

svg ---->[03_l.svg]---- <path d="M30,20 H80 V40Z m0,20 l50,20 V40 l-50,20Z" stroke="#000082" />


二、曲线路径

曲线路径包括:

  • 二次贝塞尔曲线 ,指令符为 Q/q ,已及光滑形式的 T/t
  • 三次贝塞尔曲线 ,指令符为 C/c ,已及光滑形式的 S/s
  • 弧形曲线,指令符为 A/a

1. 二次贝塞尔曲线 Q/q

每段 二次贝塞尔曲线由两个坐标构成,分别代表 控制点结束点 。下面两段贝塞尔曲线分别通过 绝对 Q相对 q 形成。比如上面一条的控制点是 70,10 ,它与起点和终点的连线和曲线相切,如虚线所示:

svg ---->[04_Qq.svg]---- <path d="M30,20 Q70,10 80,40" stroke="#000082" /> <path d="M80,40 q-40,30 -10,40" stroke="#FF743D" />


2.三次贝塞尔曲线 C/c

每段 三次贝塞尔曲线由三个坐标构成,分别代表 控制点1控制点2结束点 。下面两段贝塞尔曲线分别通过 绝对 C相对 c 形成。比如上面一条的控制点是 控制点1: (50,10)控制点2: (80,20)控制点1与起点 连线、控制点2与终点 连线和曲线相切,如虚线所示:

svg ---->[05_Cc.svg]---- <path d="M30,20 C50,10 80,20 80,40" stroke="#000082" /> <path d="M80,40 c-40,10 -50,30 -10,40" stroke="#FF743D" />


3. 弧形曲线 A/a

这个指令有 7 个参数,看着是不是有点小崩溃。在 Flutter 中,其实它就是对应 Path#arcToPoint 方法,七个参数示意如下:

下面两段弧线分别通过 绝对 A相对 a 形成。弧线本质上是从 椭圆上截取弧线 ,前两个值是椭圆的两个半轴长度;第四个值表示是否取大圆弧,如下实线部位取大圆弧,虚线部位取小圆弧;第五个值代表是否顺时针,如下实线部顺时针,虚线部位逆时针;第六第七值代表结束点坐标。

svg ---->[06_Aa.svg]---- <path d="M30,20 A20 20 0 1 1 50 50 a15 20 0 1 1 20 30" stroke="#000082" />


另外单独说一下第三值,代表 旋转角度 ,注意这个值是 角度值 不是 弧度值 。如下橙色是旋转 45 的效果,旋转并不是以椭圆中心旋转,而是 y 轴的倾斜角度,同时需要满足椭圆过起止点。

dart ---->[07_Aa_rotate.svg]---- <path d="M40,50 A20 30 0 1 1 60 70 " stroke="#000082" /> <path d="M40,50 A20 30 45 1 1 60 70 " stroke="#FF743D" />


4. 光滑形三次贝塞尔曲线 S/s

每段 S 指令后面是两个坐标,但它是一个 三次贝塞尔曲线 。通过下面的例子可以看出它和 Q 的区别、与 C 的关系。在该案例中,S 表示 控制点1 和起点重合,控制点 240,70。如下的 SC 是同一条曲线:

svg ---->[08_SQC.svg]---- <path d="M20,10 C20,10 40,70 80,50" stroke="#F619FF"/> <path d="M20,10 S40,70 80,50" stroke="#000082"/> <path d="M20,10 Q40,70 80,50" stroke="#FF743D"/>


另外 S 最难把握的一点是:

若 S 的上一段曲线是三次贝塞尔曲线: S 的第一个控制点,是上个三次贝塞尔曲线 [第二控制点] 关于 [S 起点] 的对称点。 否则: S 的第一个控制点,为 S 起点。

如下所示,理解 S 就是理解下面的 Sp1 点是什么。

svg ---->[09_CS.svg]---- <path d="M10,40 C20,10 40,10 50,40 S90,70 90,20" stroke="#000082"/>


在数学上,如果 p0 点和 p1 点关于 p 点对称,那么它们的坐标应该满足下面的关系:

记: p0(x0,y0) p1(x1,y1) p(x,y) 若: p0 和 p1 点关于 p 对称 则坐标关系满足: (x0 + x1)/2 = x (y0 + y1)/2 = y 这样已知 p0 和 p 点坐标,就很容易求出 p1 的坐标: x1 = 2*x - x0 y1 = 2*y - y0

另外,s 表示相对坐标,效果同理。


5.光滑形二次贝塞尔曲线 T/t

T/t 指令也类似:

dart 若 T 的上一段曲线是二次贝塞尔曲线: T 的控制点,是上个二次贝塞尔曲线 [控制点] 关于 [S 起点] 的对称点。 否则: T 的控制点,为 T 起点。

下面是 T 上一段为 Q 的测试效果,思考一下 Tp 点是什么?

svg <path d="M10,40 Q30,60 50,40 T90,40" stroke="#000082" stroke-width="1"/>


三、本系列收获

iconfont 中可以下载 svg 类型的图标,

通过解析 svg 可以直接通过 Flutter 绘制的 api 进行绘制,如下所示:


通这三篇文章,实现了一个及其简陋的 svg 解析器。虽然没有什么实际的应用价值,但是我们认识了 svgpath 各指令的含义。这是更为基础的知识积累,通过 svg 路径与Flutter 绘制的联系,也可以锻炼 Flutter 的绘制技能。另外尝试对 svg 的解析,其中发现问题和解决问题的过程,更是个人经验的累积。对于正则表达式的使用,也得到了一定的训练。

另外,对于 svg 的路径解析,pub已经 有了完善的包 pathdrawing ,已及基于该包,实现的 svg 文件显示包 fluttersvg 。每次看到其他大佬写的开源库,我都感觉自己写的代码像过家家一样。研究、思考和学习,希望有朝一日,我也可以像他们那样,看到他们眼中的风采。

最后附加一下使用 fluttersvg 展示的这只流传千古的虎头 svg:【extra02_svg/08】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Flutter调用Android原生方法,可以使用Flutter插件。Flutter插件是一个将Flutter应用程序与原生平台通信的桥梁。下面是一些步骤来创建一个Flutter插件并在其调用Android原生方法: 1. 使用Flutter插件模板创建一个Flutter插件: ``` flutter create --template=plugin <plugin-name> ``` 2. 在Flutter插件项目的`android`目录下,打开`build.gradle`文件,并添加以下代码: ``` dependencies { implementation 'io.flutter:flutter_embedding_v2.7.0' // 其他依赖项 } ``` 3. 在Flutter插件项目的`android/src/main`目录下,创建一个`java`包,并在其创建一个类,该类将包含您要调用的Android原生方法。例如,您可以创建一个名为`MyPlugin`的类,并在其添加以下代码: ``` package com.example.my_plugin; import android.content.Context; import android.widget.Toast; import io.flutter.embedding.engine.plugins.FlutterPlugin; public class MyPlugin implements FlutterPlugin { private Context context; @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { context = flutterPluginBinding.getApplicationContext(); } @Override public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) { context = null; } public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } } ``` 4. 在Flutter插件项目的`lib`目录下,创建一个文件夹,并在其创建一个`dart`文件,该文件将包含您要在Flutter调用的方法。例如,您可以创建一个名为`my_plugin.dart`的文件,并在其添加以下代码: ``` import 'package:flutter/services.dart'; class MyPlugin { static const MethodChannel _channel = const MethodChannel('my_plugin'); static Future<void> showToast(String message) async { try { await _channel.invokeMethod('showToast', {'message': message}); } on PlatformException catch (e) { print(e.message); } } } ``` 5. 在Flutter插件项目的`android/src/main`目录下,创建一个`res`目录,并在其创建一个`values`目录。在`values`目录,创建一个`strings.xml`文件,并添加以下代码: ``` <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">My Plugin</string> </resources> ``` 6. 在Flutter插件项目的`android/src/main`目录下,打开`AndroidManifest.xml`文件,并添加以下代码: ``` <application android:name="io.flutter.app.FlutterApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher"> <activity android:name="io.flutter.embedding.android.FlutterActivity" android:exported="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> ``` 7. 在Flutter插件项目的`android/src/main`目录下,打开`MyPlugin.java`文件,并添加以下代码: ``` package com.example.my_plugin; import android.content.Context; import android.widget.Toast; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry.Registrar; public class MyPlugin implements FlutterPlugin { private Context context; private MethodChannel channel; public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "my_plugin"); channel.setMethodCallHandler(new MyPlugin(registrar.context(), channel)); } private MyPlugin(Context context, MethodChannel channel) { this.context = context; this.channel = channel; } @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { context = flutterPluginBinding.getApplicationContext(); channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "my_plugin"); channel.setMethodCallHandler(new MyPlugin(context, channel)); } @Override public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) { context = null; channel.setMethodCallHandler(null); channel = null; } public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } private void onMethodCall(MethodCall call, MethodChannel.Result result) { if (call.method.equals("showToast")) { String message = call.argument("message"); showToast(message); result.success(null); } else { result.notImplemented(); } } } ``` 8. 在Flutter应用程序,导入您的Flutter插件,并使用以下代码调用Android原生方法: ``` import 'package:flutter/material.dart'; import 'package:my_plugin/my_plugin.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: ElevatedButton( onPressed: () { MyPlugin.showToast('Hello World!'); }, child: Text('Show Toast'), ), ), ), ); } } ``` 这样,您就可以在Flutter应用程序调用Android原生方法了!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值