Compare commits

...

3 Commits

Author SHA1 Message Date
bc3eb4621b Add latest feature/fixes from Lottie-Android (#209)
- Added support for rounded corners on shapes and rects
- Add support for text in dynamic properties (ValueDelegate)
- Improve stroke with offset
- Add support for reversed polystar paths
- Enforce order of operations to avoid rounding errors
2022-04-14 22:20:22 +02:00
08d7da747a Add pubignore 2022-02-14 10:07:39 +01:00
b2ebd2058d Fix new lints in Flutter 2.10 (#196) 2022-02-14 09:31:30 +01:00
89 changed files with 9899 additions and 193 deletions

View File

@ -10,11 +10,11 @@ jobs:
name: Flutter analyze
strategy:
matrix:
flutter: ['beta']
flutter: ['stable']
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
- uses: subosito/flutter-action@v2
with:
channel: ${{ matrix.flutter }}
- run: flutter doctor
@ -22,8 +22,8 @@ jobs:
- run: flutter pub get
working-directory: example
- run: flutter analyze
- run: flutter test test # https://github.com/flutter/flutter/issues/20907
- run: flutter test test
- run: flutter test
- run: flutter test
working-directory: example
- run: flutter pub run tool/prepare_submit.dart
- name: "check for uncommitted changes"
@ -38,10 +38,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
- uses: subosito/flutter-action@v2
with:
channel: 'beta'
- run: flutter config --enable-web
channel: 'stable'
- run: flutter precache web
- run: flutter pub get
working-directory: example

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ _*
!.gitignore
!.github
!.pubignore
**/failures/*.png

15
.pubignore Normal file
View File

@ -0,0 +1,15 @@
.dart_tool/
.packages
build/
doc/api/
*.iml
_*
.*
**/failures/*.png
test/golden/
test/goldens/
example/assets/
example/android/
example/ios/
example/macos/
example/web/

View File

@ -1,3 +1,28 @@
## [1.3.0]
- Added support for rounded corners on shapes and rects
- Add support for text in dynamic properties (`ValueDelegate`)
Example:
```dart
Lottie.asset(
'assets/DynamicText.json',
delegates: LottieDelegates(values: [
ValueDelegate.text(
['Text layer'], // The path to the text element to change
value: 'The new text',
),
]),
)
```
- Improve stroke with offset
- Add support for reversed polystar paths
- Enforce order of operations to avoid rounding errors
## [1.2.2]
- Internal maintenance: fix lints for Flutter 2.10
## [1.2.1]
- Fix: Revert Cubic to `PathInterpolator.cubic`

View File

@ -5,3 +5,9 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) {
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@ -26,23 +26,28 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
compileSdkVersion flutter.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
applicationId "com.github.xvrh.lottie.sample"
minSdkVersion 16
targetSdkVersion 28
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.github.xvrh.lottie.example"
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@ -60,7 +65,4 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.xvrh.lottie.sample">
package="com.github.xvrh.lottie.example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->

View File

@ -1,21 +1,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.xvrh.lottie.sample">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
package="com.github.xvrh.lottie.example">
<application
android:label="example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -0,0 +1,6 @@
package com.github.xvrh.lottie.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -1,12 +0,0 @@
package com.github.xvrh.lottie.sample
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,8 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.xvrh.lottie.sample">
package="com.github.xvrh.lottie.example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.6.10'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -14,7 +14,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

View File

@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@ -1,15 +1,11 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,288 @@
{
"v": "5.8.2",
"fr": 24,
"ip": 0,
"op": 94,
"w": 150,
"h": 150,
"nm": "Anim_load",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "LF03",
"sr": 1,
"ks": {
"o": {
"a": 0,
"k": 100,
"ix": 11
},
"r": {
"a": 0,
"k": -19,
"ix": 10
},
"p": {
"a": 0,
"k": [
75,
76.005,
0
],
"ix": 2,
"l": 2
},
"a": {
"a": 0,
"k": [
0,
0,
0
],
"ix": 1,
"l": 2
},
"s": {
"a": 0,
"k": [
19.986,
19.986,
100
],
"ix": 6,
"l": 2
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "sr",
"sy": 1,
"d": 3,
"pt": {
"a": 0,
"k": 14,
"ix": 3
},
"p": {
"a": 0,
"k": [
0,
0
],
"ix": 4
},
"r": {
"a": 0,
"k": 0,
"ix": 5
},
"ir": {
"a": 0,
"k": 91.612,
"ix": 6
},
"is": {
"a": 0,
"k": 0,
"ix": 8
},
"or": {
"a": 0,
"k": 113.225,
"ix": 7
},
"os": {
"a": 0,
"k": 0,
"ix": 9
},
"ix": 1,
"nm": "Polystar Path 1",
"mn": "ADBE Vector Shape - Star",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [
0.937254905701,
0.89411765337,
0.850980401039,
1
],
"ix": 3
},
"o": {
"a": 0,
"k": 100,
"ix": 4
},
"w": {
"a": 0,
"k": 30,
"ix": 5
},
"lc": 1,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-1.979,
-0.604
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Polystar 1",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": {
"a": 1,
"k": [
{
"i": {
"x": [
0.667
],
"y": [
1
]
},
"o": {
"x": [
0.333
],
"y": [
0
]
},
"t": -0.127,
"s": [
100
]
},
{
"t": 32,
"s": [
0
]
}
],
"ix": 1
},
"e": {
"a": 1,
"k": [
{
"i": {
"x": [
0.833
],
"y": [
1
]
},
"o": {
"x": [
0.333
],
"y": [
0
]
},
"t": 36,
"s": [
100
]
},
{
"t": 68,
"s": [
0
]
}
],
"ix": 2
},
"o": {
"a": 0,
"k": -43,
"ix": 3
},
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 94,
"st": 18,
"bm": 0
}
],
"markers": []
}

View File

@ -0,0 +1,390 @@
{
"v": "5.8.1",
"fr": 29.9700012207031,
"ip": 0,
"op": 900.000036657751,
"w": 1000,
"h": 1000,
"nm": "Rounded Corners",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Shape Layer 1",
"sr": 1,
"ks": {
"o": {
"a": 0,
"k": 100,
"ix": 11
},
"r": {
"a": 0,
"k": 0,
"ix": 10
},
"p": {
"a": 0,
"k": [
500,
500,
0
],
"ix": 2,
"l": 2
},
"a": {
"a": 0,
"k": [
0,
0,
0
],
"ix": 1,
"l": 2
},
"s": {
"a": 0,
"k": [
100,
100,
100
],
"ix": 6,
"l": 2
}
},
"ao": 0,
"shapes": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"o": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"v": [
[
-75,
-475
],
[
-75,
-75
],
[
-475,
-75
],
[
-475,
-475
]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "rc",
"d": 1,
"s": {
"a": 0,
"k": [
400,
400
],
"ix": 2
},
"p": {
"a": 0,
"k": [
275,
-275
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 4
},
"nm": "Rectangle Path 1",
"mn": "ADBE Vector Shape - Rect",
"hd": false
},
{
"ty": "sr",
"sy": 1,
"d": 1,
"pt": {
"a": 0,
"k": 5,
"ix": 3
},
"p": {
"a": 0,
"k": [
275,
275
],
"ix": 4
},
"r": {
"a": 0,
"k": 0,
"ix": 5
},
"ir": {
"a": 0,
"k": 107,
"ix": 6
},
"is": {
"a": 0,
"k": 0,
"ix": 8
},
"or": {
"a": 0,
"k": 275,
"ix": 7
},
"os": {
"a": 0,
"k": 0,
"ix": 9
},
"ix": 3,
"nm": "Polystar Path 1",
"mn": "ADBE Vector Shape - Star",
"hd": false
},
{
"ind": 3,
"ty": "sh",
"ix": 4,
"ks": {
"a": 0,
"k": {
"i": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"o": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"v": [
[
-275,
0
],
[
-212.107,
188.435
],
[
-13.459,
190.02
],
[
-173.237,
308.065
],
[
-113.359,
497.48
],
[
-275,
382
],
[
-436.641,
497.48
],
[
-376.763,
308.065
],
[
-536.541,
190.02
],
[
-337.893,
188.435
]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "rd",
"nm": "Round Corners 1",
"r": {
"a": 0,
"k": 136,
"ix": 1
},
"ix": 5,
"mn": "ADBE Vector Filter - RC",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
1,
0,
0,
1
],
"ix": 4
},
"o": {
"a": 0,
"k": 100,
"ix": 5
},
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
}
],
"ip": 0,
"op": 900.000036657751,
"st": 0,
"bm": 0
}
],
"markers": []
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:lottie/lottie.dart';
void main() => runApp(const MyApp());

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:lottie/lottie.dart';
void main() => runApp(const MyApp());

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:lottie/lottie.dart';
@ -72,7 +71,7 @@ class _AppState extends State<App> with TickerProviderStateMixin {
_color = newColor;
});
},
showLabel: false,
labelTypes: const [],
enableAlpha: false,
pickerAreaHeightPercent: 0.8,
),

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:lottie/lottie.dart';

View File

@ -7,14 +7,14 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
version: "3.3.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
version: "2.8.2"
boolean_selector:
dependency: transitive
description:
@ -28,14 +28,14 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.1"
clock:
dependency: transitive
description:
@ -89,14 +89,14 @@ packages:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "1.0.3"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "1.0.4"
flutter_test:
dependency: "direct dev"
description: flutter
@ -108,14 +108,14 @@ packages:
name: golden_toolkit
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0"
version: "0.13.0"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3"
version: "0.13.4"
http_parser:
dependency: transitive
description:
@ -136,28 +136,35 @@ packages:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
lottie:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "1.2.0"
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.7.0"
path:
dependency: transitive
description:
@ -171,63 +178,70 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.5"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.5"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
version: "2.0.5"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.2"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.1"
version: "4.2.4"
sky_engine:
dependency: transitive
description: flutter
@ -274,7 +288,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
version: "0.4.8"
typed_data:
dependency: transitive
description:
@ -288,21 +302,21 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.5"
version: "2.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
sdks:
dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.0"
dart: ">=2.15.0 <3.0.0"
flutter: ">=2.10.0"

BIN
example/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

35
example/web/manifest.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

View File

@ -196,7 +196,8 @@ abstract class BaseStrokeContent
void _applyTrimPath(
Canvas canvas, _PathGroup pathGroup, Matrix4 parentMatrix) {
L.beginSection('StrokeContent#applyTrimPath');
if (pathGroup.trimPath == null) {
var trimPath = pathGroup.trimPath;
if (trimPath == null) {
L.endSection('StrokeContent#applyTrimPath');
return;
}
@ -205,13 +206,24 @@ abstract class BaseStrokeContent
_path.addPath(pathGroup.paths[j].getPath(), Offset.zero,
matrix4: parentMatrix.storage);
}
var animStartValue = trimPath.start.value / 100;
var animEndValue = trimPath.end.value / 100;
var animOffsetValue = trimPath.offset.value / 360;
// If the start-end is ~100, consider it to be the full path.
if (animStartValue < 0.01 && animEndValue > 0.99) {
canvas.drawPath(_path, paint);
L.endSection('StrokeContent#applyTrimPath');
return;
}
var pathMetrics = _path.computeMetrics().toList();
var totalLength = pathMetrics.fold<double>(0.0, (a, b) => a + b.length);
var trimPath = pathGroup.trimPath!;
var offsetLength = totalLength * trimPath.offset.value / 360.0;
var startLength = totalLength * trimPath.start.value / 100.0 + offsetLength;
var endLength = totalLength * trimPath.end.value / 100.0 + offsetLength;
var offsetLength = totalLength * animOffsetValue;
var startLength = totalLength * animStartValue + offsetLength;
var endLength = min(totalLength * animEndValue + offsetLength,
startLength + totalLength - 1);
var currentLength = 0.0;
for (var j = pathGroup.paths.length - 1; j >= 0; j--) {

View File

@ -134,6 +134,9 @@ class PolystarContent implements PathContent, KeyPathElementContent {
currentAngle = radians(currentAngle);
// adjust current angle for partial points
var anglePerPoint = 2 * pi / points;
if (_polystarShape.isReversed) {
anglePerPoint *= -1;
}
var halfAnglePerPoint = anglePerPoint / 2.0;
var partialPointAmount = points - points.toInt();
if (partialPointAmount != 0) {

View File

@ -15,6 +15,7 @@ import 'compound_trim_path_content.dart';
import 'content.dart';
import 'key_path_element_content.dart';
import 'path_content.dart';
import 'rounded_corners_content.dart';
import 'trim_path_content.dart';
class RectangleContent implements KeyPathElementContent, PathContent {
@ -29,6 +30,9 @@ class RectangleContent implements KeyPathElementContent, PathContent {
final BaseKeyframeAnimation<Object, double> _cornerRadiusAnimation;
final CompoundTrimPathContent _trimPaths = CompoundTrimPathContent();
/// This corner radius is from a layer item. The first one is from the roundedness on this specific rect.
BaseKeyframeAnimation<double, double>? _roundedCornersAnimation;
bool _isPathValid = false;
RectangleContent(
@ -61,6 +65,8 @@ class RectangleContent implements KeyPathElementContent, PathContent {
var trimPath = content;
_trimPaths.addTrimPath(trimPath);
trimPath.addListener(invalidate);
} else if (content is RoundedCornersContent) {
_roundedCornersAnimation = content.roundedCorners;
}
}
}
@ -82,6 +88,10 @@ class RectangleContent implements KeyPathElementContent, PathContent {
var halfWidth = size.dx / 2.0;
var halfHeight = size.dy / 2.0;
var radius = _cornerRadiusAnimation.value;
var roundedCornersAnimation = _roundedCornersAnimation;
if (radius == 0 && roundedCornersAnimation != null) {
radius = min(roundedCornersAnimation.value, min(halfWidth, halfHeight));
}
var maxRadius = min(halfWidth, halfHeight);
if (radius > maxRadius) {
radius = maxRadius;

View File

@ -0,0 +1,236 @@
import 'dart:math' as math;
import 'dart:ui';
import '../../lottie_drawable.dart';
import '../../model/content/rounded_corners.dart';
import '../../model/content/shape_data.dart';
import '../../model/cubic_curve_data.dart';
import '../../model/layer/base_layer.dart';
import '../../utils.dart';
import '../keyframe/base_keyframe_animation.dart';
import 'content.dart';
import 'shape_modifier_content.dart';
class RoundedCornersContent implements ShapeModifierContent {
/// Copied from:
/// https://github.com/airbnb/lottie-web/blob/bb71072a26e03f1ca993da60915860f39aae890b/player/js/utils/common.js#L47
static const _roundedCornerMagicNumber = 0.5519;
final LottieDrawable lottieDrawable;
@override
final String name;
final BaseKeyframeAnimation<double, double> roundedCorners;
ShapeData? shapeData;
RoundedCornersContent(
this.lottieDrawable, BaseLayer layer, RoundedCorners roundedCorners)
: name = roundedCorners.name,
roundedCorners = roundedCorners.cornerRadius.createAnimation() {
layer.addAnimation(this.roundedCorners);
this.roundedCorners.addUpdateListener(_onValueChanged);
}
void _onValueChanged() {
lottieDrawable.invalidateSelf();
}
@override
void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
// Do nothing.
}
/// Rounded corner algorithm:
/// Iterate through each vertex.
/// If a vertex is a sharp corner, it rounds it.
/// If a vertex has control points, it is already rounded, so it does nothing.
/// <p>
/// To round a vertex:
/// Split the vertex into two.
/// Move vertex 1 directly towards the previous vertex.
/// Set vertex 1's in control point to itself so it is not rounded on that side.
/// Extend vertex 1's out control point towards the original vertex.
/// <p>
/// Repeat for vertex 2:
/// Move vertex 2 directly towards the next vertex.
/// Set vertex 2's out point to itself so it is not rounded on that side.
/// Extend vertex 2's in control point towards the original vertex.
/// <p>
/// The distance that the vertices and control points are moved are relative to the
/// shape's vertex distances and the roundedness set in the animation.
@override
ShapeData modifyShape(ShapeData startingShapeData) {
var startingCurves = startingShapeData.curves;
if (startingCurves.length <= 2) {
return startingShapeData;
}
var roundedness = roundedCorners.value;
if (roundedness == 0) {
return startingShapeData;
}
var modifiedShapeData = _getShapeData(startingShapeData);
modifiedShapeData.setInitialPoint(
startingShapeData.initialPoint.dx, startingShapeData.initialPoint.dy);
var modifiedCurves = modifiedShapeData.curves;
var modifiedCurvesIndex = 0;
var isClosed = startingShapeData.isClosed;
// i represents which vertex we are currently on. Refer to the docs of CubicCurveData prior to working with
// this code.
// When i == 0
// vertex=ShapeData.initialPoint
// inCp=if closed vertex else curves[size - 1].cp2
// outCp=curves[0].cp1
// When i == 1
// vertex=curves[0].vertex
// inCp=curves[0].cp2
// outCp=curves[1].cp1.
// When i == size - 1
// vertex=curves[size - 1].vertex
// inCp=curves[size - 1].cp2
// outCp=if closed vertex else curves[0].cp1
for (var i = 0; i < startingCurves.length; i++) {
var startingCurve = startingCurves[i];
var previousCurve =
startingCurves[floorMod(i - 1, startingCurves.length)];
var previousPreviousCurve =
startingCurves[floorMod(i - 2, startingCurves.length)];
var vertex = (i == 0 && !isClosed)
? startingShapeData.initialPoint
: previousCurve.vertex;
var inPoint =
(i == 0 && !isClosed) ? vertex : previousCurve.controlPoint2;
var outPoint = startingCurve.controlPoint1;
var previousVertex = previousPreviousCurve.vertex;
var nextVertex = startingCurve.vertex;
// We can't round the corner of the end of a non-closed curve.
var isEndOfCurve = !startingShapeData.isClosed &&
(i == 0 && i == startingCurves.length - 1);
if (inPoint == vertex && outPoint == vertex && !isEndOfCurve) {
// This vertex is a point. Round its corners
var dxToPreviousVertex = vertex.dx - previousVertex.dx;
var dyToPreviousVertex = vertex.dy - previousVertex.dy;
var dxToNextVertex = nextVertex.dx - vertex.dx;
var dyToNextVertex = nextVertex.dy - vertex.dy;
var dToPreviousVertex = hypot(dxToPreviousVertex, dyToPreviousVertex);
var dToNextVertex = hypot(dxToNextVertex, dyToNextVertex);
double previousVertexPercent =
math.min(roundedness / dToPreviousVertex, 0.5);
double nextVertexPercent = math.min(roundedness / dToNextVertex, 0.5);
// Split the vertex into two and move each vertex towards the previous/next vertex.
var newVertex1X =
vertex.dx + (previousVertex.dx - vertex.dx) * previousVertexPercent;
var newVertex1Y =
vertex.dy + (previousVertex.dy - vertex.dy) * previousVertexPercent;
var newVertex2X =
vertex.dx + (nextVertex.dx - vertex.dx) * nextVertexPercent;
var newVertex2Y =
vertex.dy + (nextVertex.dy - vertex.dy) * nextVertexPercent;
// Extend the new vertex control point towards the original vertex.
var newVertex1OutPointX =
newVertex1X - (newVertex1X - vertex.dx) * _roundedCornerMagicNumber;
var newVertex1OutPointY =
newVertex1Y - (newVertex1Y - vertex.dy) * _roundedCornerMagicNumber;
var newVertex2InPointX =
newVertex2X - (newVertex2X - vertex.dx) * _roundedCornerMagicNumber;
var newVertex2InPointY =
newVertex2Y - (newVertex2Y - vertex.dy) * _roundedCornerMagicNumber;
// Remap vertex/in/out point to CubicCurveData.
// Refer to the docs for CubicCurveData for more info on the difference.
var previousCurveData = modifiedCurves[
floorMod(modifiedCurvesIndex - 1, modifiedCurves.length)];
var currentCurveData = modifiedCurves[modifiedCurvesIndex];
previousCurveData.controlPoint2 = Offset(newVertex1X, newVertex1Y);
previousCurveData.vertex = Offset(newVertex1X, newVertex1Y);
if (i == 0) {
modifiedShapeData.setInitialPoint(newVertex1X, newVertex1Y);
}
currentCurveData.controlPoint1 =
Offset(newVertex1OutPointX, newVertex1OutPointY);
modifiedCurvesIndex++;
previousCurveData = currentCurveData;
currentCurveData = modifiedCurves[modifiedCurvesIndex];
previousCurveData.controlPoint2 =
Offset(newVertex2InPointX, newVertex2InPointY);
previousCurveData.vertex = Offset(newVertex2X, newVertex2Y);
currentCurveData.controlPoint1 = Offset(newVertex2X, newVertex2Y);
modifiedCurvesIndex++;
} else {
// This vertex is not a point. Don't modify it. Refer to the documentation above and for CubicCurveData for mapping a vertex
// oriented point to CubicCurveData (path segments).
var previousCurveData = modifiedCurves[
floorMod(modifiedCurvesIndex - 1, modifiedCurves.length)];
var currentCurveData = modifiedCurves[modifiedCurvesIndex];
previousCurveData.controlPoint2 =
Offset(previousCurve.vertex.dx, previousCurve.vertex.dy);
previousCurveData.vertex =
Offset(previousCurve.vertex.dx, previousCurve.vertex.dy);
currentCurveData.controlPoint1 =
Offset(startingCurve.vertex.dx, startingCurve.vertex.dy);
modifiedCurvesIndex++;
}
}
return modifiedShapeData;
}
/// Returns a shape data with the correct number of vertices for the rounded corners shape.
/// This just returns the object. It does not update any values within the shape.
ShapeData _getShapeData(ShapeData startingShapeData) {
var startingCurves = startingShapeData.curves;
var isClosed = startingShapeData.isClosed;
var vertices = 0;
for (var i = startingCurves.length - 1; i >= 0; i--) {
var startingCurve = startingCurves[i];
var previousCurve =
startingCurves[floorMod(i - 1, startingCurves.length)];
var vertex = (i == 0 && !isClosed)
? startingShapeData.initialPoint
: previousCurve.vertex;
var inPoint =
(i == 0 && !isClosed) ? vertex : previousCurve.controlPoint2;
var outPoint = startingCurve.controlPoint1;
var isEndOfCurve = !startingShapeData.isClosed &&
(i == 0 && i == startingCurves.length - 1);
if (inPoint == vertex && outPoint == vertex && !isEndOfCurve) {
vertices += 2;
} else {
vertices += 1;
}
}
var shapeData = this.shapeData;
if (shapeData == null || shapeData.curves.length != vertices) {
var newCurves = <CubicCurveData>[];
for (var i = 0; i < vertices; i++) {
newCurves.add(CubicCurveData());
}
this.shapeData = shapeData =
ShapeData(newCurves, initialPoint: Offset.zero, closed: false);
}
shapeData.setClosed(isClosed);
return shapeData;
}
/// Copied from the API 24+ AOSP source.
static int floorMod(int x, int y) {
return x - floorDiv(x, y) * y;
}
/// Copied from the API 24+ AOSP source.
static int floorDiv(int x, int y) {
var r = x ~/ y;
// if the signs are different and modulo not zero, round down
if ((x ^ y) < 0 && (r * y != x)) {
r--;
}
return r;
}
}

View File

@ -5,10 +5,11 @@ import '../../model/content/shape_trim_path.dart';
import '../../model/layer/base_layer.dart';
import '../../utils.dart';
import '../../utils/path_factory.dart';
import '../keyframe/base_keyframe_animation.dart';
import '../keyframe/shape_keyframe_animation.dart';
import 'compound_trim_path_content.dart';
import 'content.dart';
import 'path_content.dart';
import 'shape_modifier_content.dart';
import 'trim_path_content.dart';
class ShapeContent implements PathContent {
@ -17,7 +18,7 @@ class ShapeContent implements PathContent {
final ShapePath _shape;
final LottieDrawable lottieDrawable;
final BaseKeyframeAnimation<Object, Path> _shapeAnimation;
final ShapeKeyframeAnimation _shapeAnimation;
bool _isPathValid = false;
final _trimPaths = CompoundTrimPathContent();
@ -35,6 +36,7 @@ class ShapeContent implements PathContent {
@override
void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
List<ShapeModifierContent>? shapeModifierContents;
for (var i = 0; i < contentsBefore.length; i++) {
var content = contentsBefore[i];
if (content is TrimPathContent &&
@ -43,8 +45,12 @@ class ShapeContent implements PathContent {
var trimPath = content;
_trimPaths.addTrimPath(trimPath);
trimPath.addListener(_invalidate);
} else if (content is ShapeModifierContent) {
shapeModifierContents ??= [];
shapeModifierContents.add(content);
}
}
_shapeAnimation.setShapeModifiers(shapeModifierContents);
}
@override

View File

@ -0,0 +1,6 @@
import '../../model/content/shape_data.dart';
import 'content.dart';
abstract class ShapeModifierContent extends Content {
ShapeData modifyShape(ShapeData shapeData);
}

View File

@ -10,7 +10,7 @@ import 'base_keyframe_animation.dart';
import 'color_keyframe_animation.dart';
class DropShadowKeyframeAnimation {
static final double _degToRad = math.pi / 180.0;
static const double _degToRad = math.pi / 180.0;
final void Function() listener;
late final ColorKeyframeAnimation _color;

View File

@ -3,11 +3,13 @@ import '../../model/content/shape_data.dart';
import '../../utils/misc.dart';
import '../../utils/path_factory.dart';
import '../../value/keyframe.dart';
import '../content/shape_modifier_content.dart';
import 'base_keyframe_animation.dart';
class ShapeKeyframeAnimation extends BaseKeyframeAnimation<ShapeData, Path> {
final ShapeData _tempShapeData = ShapeData.empty();
final Path _tempPath = PathFactory.create();
List<ShapeModifierContent>? _shapeModifiers;
ShapeKeyframeAnimation(List<Keyframe<ShapeData>> keyframes)
: super(keyframes);
@ -19,7 +21,18 @@ class ShapeKeyframeAnimation extends BaseKeyframeAnimation<ShapeData, Path> {
_tempShapeData.interpolateBetween(
startShapeData, endShapeData, keyframeProgress);
MiscUtils.getPathFromData(_tempShapeData, _tempPath);
var modifiedShapeData = _tempShapeData;
var shapeModifiers = _shapeModifiers;
if (shapeModifiers != null) {
for (var i = shapeModifiers.length - 1; i >= 0; i--) {
modifiedShapeData = shapeModifiers[i].modifyShape(modifiedShapeData);
}
}
MiscUtils.getPathFromData(modifiedShapeData, _tempPath);
return _tempPath;
}
void setShapeModifiers(List<ShapeModifierContent>? shapeModifiers) {
_shapeModifiers = shapeModifiers;
}
}

View File

@ -1,5 +1,7 @@
import '../../model/document_data.dart';
import '../../value/keyframe.dart';
import '../../value/lottie_frame_info.dart';
import '../../value/lottie_value_callback.dart';
import 'keyframe_animation.dart';
class TextKeyframeAnimation extends KeyframeAnimation<DocumentData> {
@ -9,10 +11,58 @@ class TextKeyframeAnimation extends KeyframeAnimation<DocumentData> {
@override
DocumentData getValue(
Keyframe<DocumentData> keyframe, double keyframeProgress) {
if (keyframeProgress != 1.0 || keyframe.endValue == null) {
var valueCallback = this.valueCallback;
if (valueCallback != null) {
return valueCallback.getValueInternal(
keyframe.startFrame,
keyframe.endFrame ?? double.maxFinite,
keyframe.startValue,
keyframe.endValue ?? keyframe.startValue,
keyframeProgress,
getInterpolatedCurrentKeyframeProgress(),
progress)!;
} else if (keyframeProgress != 1.0 || keyframe.endValue == null) {
return keyframe.startValue!;
} else {
return keyframe.endValue!;
}
}
void setStringValueCallback(LottieValueCallback<String> valueCallback) {
super.setValueCallback(_DocumentDataValueCallback(valueCallback));
}
}
class _DocumentDataValueCallback extends LottieValueCallback<DocumentData> {
final LottieValueCallback<String> valueCallback;
_DocumentDataValueCallback(this.valueCallback) : super(null);
@override
DocumentData getValue(LottieFrameInfo<DocumentData> frameInfo) {
var stringFrameInfo = LottieFrameInfo<String>(
frameInfo.startFrame,
frameInfo.endFrame,
frameInfo.startValue!.text,
frameInfo.endValue!.text,
frameInfo.linearKeyframeProgress,
frameInfo.interpolatedKeyframeProgress,
frameInfo.overallProgress);
var text = valueCallback.getValue(stringFrameInfo)!;
var baseDocumentData = frameInfo.interpolatedKeyframeProgress == 1
? frameInfo.endValue!
: frameInfo.startValue!;
return DocumentData(
text: text,
fontName: baseDocumentData.fontName,
size: baseDocumentData.size,
justification: baseDocumentData.justification,
tracking: baseDocumentData.tracking,
lineHeight: baseDocumentData.lineHeight,
baselineShift: baseDocumentData.baselineShift,
color: baseDocumentData.color,
strokeColor: baseDocumentData.strokeColor,
strokeWidth: baseDocumentData.strokeWidth,
strokeOverFill: baseDocumentData.strokeOverFill);
}
}

View File

@ -1,13 +1,9 @@
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'l.dart';
import 'lottie_builder.dart';
import 'options.dart';
import 'providers/load_image.dart';
/// A widget to display a loaded [LottieComposition].
/// The [controller] property allows to specify a custom AnimationController that

View File

@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie.dart';
import 'lottie_delegates.dart';
import 'options.dart';
import 'providers/asset_provider.dart';
import 'providers/file_provider.dart';
import 'providers/load_image.dart';

View File

@ -1,7 +1,8 @@
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'composition.dart';
import 'lottie_drawable.dart';
import 'lottie_image_asset.dart';
import 'value_delegate.dart';
// TODO(xha): recognize style Bold, Medium, Regular, SemiBold, etc...

View File

@ -1,6 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie_delegates.dart';

View File

@ -129,8 +129,12 @@ abstract class LottieProperty {
static const dropShadow = DropShadow(
color: Color(0x00000000), direction: 0, distance: 0, radius: 0);
/// Set the color filter for an entire drawable content. Can be applied to fills, strokes, images, and solids.
static const ColorFilter colorFilter =
ColorFilter.mode(Color(0xFF000000), BlendMode.dst);
static final List<Color> gradientColor = [];
/// Replace the text for a text layer.
static const text = 'dynamic_text';
}

View File

@ -1,5 +1,4 @@
import 'dart:ui';
import '../../animation/keyframe/base_keyframe_animation.dart';
import '../../animation/keyframe/shape_keyframe_animation.dart';
import '../../value/keyframe.dart';
import '../content/shape_data.dart';
@ -10,7 +9,7 @@ class AnimatableShapeValue extends BaseAnimatableValue<ShapeData, Path> {
: super.fromKeyframes(keyframes);
@override
BaseKeyframeAnimation<ShapeData, Path> createAnimation() {
ShapeKeyframeAnimation createAnimation() {
return ShapeKeyframeAnimation(keyframes);
}
}

View File

@ -37,6 +37,7 @@ class PolystarShape implements ContentModel {
final AnimatableDoubleValue? innerRoundedness;
final AnimatableDoubleValue outerRoundedness;
final bool hidden;
final bool isReversed;
PolystarShape({
this.name,
@ -49,6 +50,7 @@ class PolystarShape implements ContentModel {
this.innerRoundedness,
required this.outerRoundedness,
required this.hidden,
required this.isReversed,
});
@override

View File

@ -0,0 +1,18 @@
import '../../animation/content/content.dart';
import '../../animation/content/rounded_corners_content.dart';
import '../../lottie_drawable.dart';
import '../animatable/animatable_value.dart';
import '../layer/base_layer.dart';
import 'content_model.dart';
class RoundedCorners implements ContentModel {
final String name;
final AnimatableValue<double, double> cornerRadius;
RoundedCorners(this.name, this.cornerRadius);
@override
Content toContent(LottieDrawable drawable, BaseLayer layer) {
return RoundedCornersContent(drawable, layer, this);
}
}

View File

@ -22,6 +22,10 @@ class ShapeData {
return _initialPoint;
}
void setClosed(bool closed) {
_closed = closed;
}
bool get isClosed {
return _closed;
}

View File

@ -1,7 +1,44 @@
import 'dart:ui';
/// One cubic path operation. CubicCurveData is structured such that it is easy to iterate through
/// it and build a path. However, it is modeled differently than most path operations.
///
/// CubicCurveData
/// | - vertex
/// | /
/// | cp1 cp2
/// | /
/// | |
/// | /
/// --------------------------
///
/// When incrementally building a path, it will already have a "current point" so that is
/// not captured in this data structure.
/// The control points here represent {@link android.graphics.Path#cubicTo(float, float, float, float, float, float)}.
///
/// Most path operations are centered around a vertex and its in control point and out control point like this:
/// | outCp
/// | /
/// | |
/// | v
/// | /
/// | inCp
/// --------------------------
class CubicCurveData {
Offset controlPoint1 = Offset.zero;
Offset controlPoint2 = Offset.zero;
Offset vertex = Offset.zero;
void setFrom(CubicCurveData curveData) {
vertex = Offset(curveData.vertex.dx, curveData.vertex.dy);
controlPoint1 =
Offset(curveData.controlPoint1.dx, curveData.controlPoint1.dy);
controlPoint2 =
Offset(curveData.controlPoint2.dx, curveData.controlPoint2.dy);
}
@override
String toString() {
return 'v=$vertex cp1$controlPoint1 cp2=$controlPoint2';
}
}

View File

@ -2,7 +2,6 @@ import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart' hide Layer;
import 'package:vector_math/vector_math_64.dart';
import '../../animation/content/content.dart';
import '../../animation/content/drawing_content.dart';
import '../../animation/keyframe/base_keyframe_animation.dart';

View File

@ -1,7 +1,5 @@
import 'dart:ui';
import 'package:characters/characters.dart';
import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import '../../animation/content/content_group.dart';
import '../../animation/keyframe/base_keyframe_animation.dart';
import '../../animation/keyframe/text_keyframe_animation.dart';
@ -486,6 +484,11 @@ class TextLayer extends BaseLayer {
..addUpdateListener(invalidateSelf);
addAnimation(_textSizeCallbackAnimation);
}
} else if (property == LottieProperty.text) {
if (callback != null) {
_textAnimation
.setStringValueCallback(callback as LottieValueCallback<String>);
}
}
}
}

View File

@ -9,6 +9,7 @@ import 'moshi/json_reader.dart';
import 'polysar_shape_parser.dart';
import 'rectangle_shape_parser.dart';
import 'repeat_parser.dart';
import 'rounded_corners_parser.dart';
import 'shape_fill_parser.dart';
import 'shape_group_parser.dart';
import 'shape_path_parser.dart';
@ -80,7 +81,7 @@ class ContentModelParser {
model = ShapeTrimPathParser.parse(reader, composition);
break;
case 'sr':
model = PolystarShapeParser.parse(reader, composition);
model = PolystarShapeParser.parse(reader, composition, d: d);
break;
case 'mm':
model = MergePathsParser.parse(reader);
@ -88,6 +89,9 @@ class ContentModelParser {
case 'rp':
model = RepeaterParser.parse(reader, composition);
break;
case 'rd':
model = RoundedCornersParser.parse(reader, composition);
break;
default:
composition.addWarning('Unknown shape type $type');
}

View File

@ -3,8 +3,19 @@ import '../model/document_data.dart';
import 'json_utils.dart';
import 'moshi/json_reader.dart';
final JsonReaderOptions _names = JsonReaderOptions.of(
['t', 'f', 's', 'j', 'tr', 'lh', 'ls', 'fc', 'sc', 'sw', 'of']);
final JsonReaderOptions _names = JsonReaderOptions.of([
't', // 0
'f', // 1
's', // 2
'j', // 3
'tr', // 4
'lh', // 5
'ls', // 6
'fc', // 7
'sc', // 8
'sw', // 9
'of', // 10
]);
DocumentData documentDataParser(JsonReader reader, {required double scale}) {
String? text;

View File

@ -37,6 +37,17 @@ class GradientColorParser {
while (reader.hasNext()) {
array.add(reader.nextDouble());
}
if (array.length == 4 && array[0] == 1) {
// If a gradient color only contains one color at position 1, add a second stop with the same
// color at position 0. Android's LinearGradient shader requires at least two colors.
// https://github.com/airbnb/lottie-android/issues/1967
array[0] = 0;
array.add(1);
array.add(array[1]);
array.add(array[2]);
array.add(array[3]);
_colorPoints = 2;
}
if (isArray) {
reader.endArray();
}

View File

@ -13,7 +13,7 @@ class KeyframeParser {
/// PathInterpolator fails to create the interpolator in those cases and hangs.
/// Clamping the cp helps prevent that.
static const _maxCpValue = 100.0;
static final _linearInterpolator = Curves.linear;
static const _linearInterpolator = Curves.linear;
static final _pathInterpolatorCache = <int, Curve>{};
static final JsonReaderOptions _names =

View File

@ -9,11 +9,12 @@ import 'moshi/json_reader.dart';
class PolystarShapeParser {
static final JsonReaderOptions _names = JsonReaderOptions.of(
['nm', 'sy', 'pt', 'p', 'r', 'or', 'os', 'ir', 'is', 'hd']);
['nm', 'sy', 'pt', 'p', 'r', 'or', 'os', 'ir', 'is', 'hd', 'd']);
PolystarShapeParser._();
static PolystarShape parse(JsonReader reader, LottieComposition composition) {
static PolystarShape parse(JsonReader reader, LottieComposition composition,
{required int d}) {
String? name;
PolystarShapeType? type;
late AnimatableDoubleValue points;
@ -24,6 +25,7 @@ class PolystarShapeParser {
AnimatableDoubleValue? innerRadius;
AnimatableDoubleValue? innerRoundedness;
var hidden = false;
var reversed = d == 3;
while (reader.hasNext()) {
switch (reader.selectName(_names)) {
@ -64,6 +66,10 @@ class PolystarShapeParser {
case 9:
hidden = reader.nextBoolean();
break;
case 10:
// "d" is 2 for normal and 3 for reversed.
reversed = reader.nextInt() == 3;
break;
default:
reader.skipName();
reader.skipValue();
@ -81,6 +87,7 @@ class PolystarShapeParser {
innerRoundedness: innerRoundedness,
outerRoundedness: outerRoundedness,
hidden: hidden,
isReversed: reversed,
);
}
}

View File

@ -0,0 +1,39 @@
import '../composition.dart';
import '../model/animatable/animatable_value.dart';
import '../model/content/rounded_corners.dart';
import 'animatable_value_parser.dart';
import 'moshi/json_reader.dart';
class RoundedCornersParser {
static final _names = JsonReaderOptions.of([
'nm', // 0
'r', // 1
'hd' // 1
]);
static RoundedCorners? parse(
JsonReader reader, LottieComposition composition) {
String? name;
AnimatableValue<double, double>? cornerRadius;
var hidden = false;
while (reader.hasNext()) {
switch (reader.selectName(_names)) {
case 0: //nm
name = reader.nextString();
break;
case 1: // r
cornerRadius =
AnimatableValueParser.parseFloat(reader, composition, isDp: true);
break;
case 2: // hd
hidden = reader.nextBoolean();
break;
default:
reader.skipValue();
}
}
return hidden ? null : RoundedCorners(name!, cornerRadius!);
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie_delegates.dart';
import 'lottie_drawable.dart';
import 'options.dart';
import 'render_lottie.dart';
/// A widget that displays a [LottieDrawable] directly.

View File

@ -1,6 +1,7 @@
import 'package:flutter/rendering.dart';
import '../lottie.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie_delegates.dart';
import 'lottie_drawable.dart';
/// A Lottie animation in the render tree.

View File

@ -1,5 +1,4 @@
import 'dart:math';
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart';

View File

@ -254,6 +254,11 @@ class ValueDelegate<T> {
double? relative}) =>
_double(keyPath, LottieProperty.textSize, value, callback, relative);
static ValueDelegate<String> text(List<String> keyPath,
{String? value,
String Function(LottieFrameInfo<String>)? callback}) =>
ValueDelegate._(keyPath, LottieProperty.text, value, callback);
static ValueDelegate<ColorFilter> colorFilter(List<String> keyPath,
{ColorFilter? value,
ColorFilter Function(LottieFrameInfo<ColorFilter>)? callback}) =>

View File

@ -7,21 +7,21 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "26.0.0"
version: "38.0.0"
analyzer:
dependency: "direct dev"
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "3.4.1"
archive:
dependency: "direct main"
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
version: "3.3.0"
args:
dependency: transitive
description:
@ -35,7 +35,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.1"
version: "2.8.2"
boolean_selector:
dependency: transitive
description:
@ -49,7 +49,7 @@ packages:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.2"
build_config:
dependency: transitive
description:
@ -63,28 +63,28 @@ packages:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.7"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.8"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.0"
version: "7.2.3"
built_collection:
dependency: transitive
description:
@ -98,14 +98,14 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.2"
version: "8.1.4"
characters:
dependency: "direct main"
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
charcode:
dependency: transitive
description:
@ -120,13 +120,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
clock:
dependency: transitive
description:
@ -168,7 +161,7 @@ packages:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.2"
fake_async:
dependency: transitive
description:
@ -220,7 +213,7 @@ packages:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.2"
graphs:
dependency: transitive
description:
@ -234,7 +227,7 @@ packages:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.2.0"
http_parser:
dependency: transitive
description:
@ -255,14 +248,14 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
version: "0.6.4"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
version: "4.4.0"
lints:
dependency: transitive
description:
@ -283,7 +276,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
meta:
dependency: transitive
description:
@ -297,7 +297,7 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
package_config:
dependency: transitive
description:
@ -312,13 +312,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
pool:
dependency: transitive
description:
@ -332,21 +325,21 @@ packages:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.2.0"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
shelf_web_socket:
dependency: transitive
description:
@ -407,7 +400,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
version: "0.4.8"
timing:
dependency: transitive
description:
@ -428,14 +421,14 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
@ -451,4 +444,5 @@ packages:
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"
dart: ">=2.16.0 <3.0.0"
flutter: ">=2.10.0"

View File

@ -1,14 +1,14 @@
name: lottie
description: Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.
version: 1.2.1
version: 1.3.0
homepage: https://github.com/xvrh/lottie-flutter
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: '>=2.10.0'
dependencies:
archive: ^3.0.0
characters: ^1.1.0
flutter:
sdk: flutter
path: ^1.8.0

View File

@ -1,7 +1,6 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';

View File

@ -2,27 +2,83 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';
void main() {
testWidgets('Dynamic test', (tester) async {
var composition = await LottieComposition.fromBytes(
late LottieComposition composition;
setUpAll(() async {
composition = await LottieComposition.fromBytes(
File('example/assets/Tests/DynamicText.json').readAsBytesSync());
await tester.pumpWidget(
MaterialApp(
home: Lottie(
composition: composition,
animate: false,
delegates: LottieDelegates(
text: (input) => '🔥c🔥👮🏿🔥',
textStyle: (font) => const TextStyle(
fontFamily: 'Roboto', fontFamilyFallback: ['Noto Emoji']),
values: const []),
),
),
);
await expectLater(
find.byType(Lottie), matchesGoldenFile('goldens/dynamic_text.png'));
});
void testGolden(String description, LottieDelegates delegates) async {
var screenshotName = description
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9 ]'), '')
.replaceAll(' ', '_');
var size = const Size(500, 400);
testWidgets(description, (tester) async {
await tester.pumpWidget(
MaterialApp(
home: FilmStrip(
composition,
delegates: delegates,
size: size,
),
),
);
await tester.pump();
await expectLater(find.byType(FilmStrip),
matchesGoldenFile('goldens/dynamic_text/$screenshotName.png'));
});
}
testGolden(
'Dynamic text delegate',
LottieDelegates(
text: (input) => '🔥c🔥👮🏿🔥',
textStyle: (font) => const TextStyle(
fontFamily: 'Roboto', fontFamilyFallback: ['Noto Emoji']),
values: const []));
testGolden(
'Dynamic Text ValueDelegate',
LottieDelegates(values: [
ValueDelegate.text(['NAME'], value: 'Text with ValueDelegate')
]),
);
testGolden(
'Dynamic Text ValueDelegate overallProgress',
LottieDelegates(values: [
ValueDelegate.text(['NAME'],
callback: (frame) => '${frame.overallProgress}')
]),
);
testGolden(
'Dynamic Text ValueDelegate startValue',
LottieDelegates(values: [
ValueDelegate.text(['NAME'], callback: (frame) => '${frame.startValue}!!')
]),
);
testGolden(
'Dynamic Text ValueDelegate endValue',
LottieDelegates(values: [
ValueDelegate.text(['NAME'], callback: (frame) => '${frame.endValue}!!')
]),
);
testGolden(
'Dynamic Text Emoji',
LottieDelegates(
textStyle: (font) => const TextStyle(
fontFamily: 'Roboto', fontFamilyFallback: ['Noto Emoji']),
values: [
ValueDelegate.text(['NAME'], value: '🔥💪💯'),
]),
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';