pub.dev score: license, dartdoc, analysis, tests

- Add MIT license
- Add dartdoc to all public APIs (UxKeyboard, BendBox, Bezier, Json)
- Add analysis_options.yaml with flutter_lints and public_member_api_docs
- Add repository, issue_tracker, topics to pubspec.yaml
- Expand package description
- Fix BendBox constructor (const, super.key)
- Remove unused import in keyboard.dart
- Replace boilerplate tests with UxKeyboard smoke tests
- Move ux dependency to example's dependencies (not dev_dependencies)
This commit is contained in:
agra
2026-04-16 18:50:33 +03:00
parent 0fff294caf
commit 785c6d3c31
15 changed files with 234 additions and 125 deletions

22
LICENSE
View File

@@ -1 +1,21 @@
TODO: Add your license here.
MIT License
Copyright (c) 2024 SWIPELAB LTD
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
analysis_options.yaml Normal file
View File

@@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
public_member_api_docs: true

View File

@@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"ux","path":"/Users/agra/projects/ux/","native_build":true,"dependencies":[],"dev_dependency":true}],"android":[{"name":"ux","path":"/Users/agra/projects/ux/","native_build":true,"dependencies":[],"dev_dependency":true}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"ux","dependencies":[]}],"date_created":"2026-04-15 13:13:36.363037","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"ux","path":"/Users/agra/projects/ux/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"ux","path":"/Users/agra/projects/ux/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"ux","dependencies":[]}],"date_created":"2026-04-16 18:49:44.385595","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}}

View File

@@ -62,6 +62,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -91,6 +99,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
@@ -177,12 +193,12 @@ packages:
source: hosted
version: "0.7.10"
ux:
dependency: "direct dev"
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.2.0"
version: "0.3.0"
vector_math:
dependency: transitive
description:

View File

@@ -8,56 +8,14 @@ environment:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: 1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
ux:
path: ../
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
dev_dependencies:
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@@ -1,26 +1,11 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ux_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) =>
widget is Text && (widget.data?.startsWith('Running on:') ?? false),
),
findsOneWidget,
);
testWidgets('ChatScreen renders', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: ChatScreen()));
expect(find.text('UxKeyboard Chat'), findsOneWidget);
expect(find.text('Type a message...'), findsOneWidget);
});
}

View File

@@ -1,11 +1,19 @@
import 'package:flutter/material.dart';
/// A widget that paints a filled shape with curved (bent) edges.
///
/// Each edge bends inward by the amount specified in [inward].
class BendBox extends StatelessWidget {
/// How far each edge bends inward. Positive values bend toward the center.
final EdgeInsets inward;
/// The fill color of the shape.
final Color color;
BendBox({this.inward = const EdgeInsets.all(0), this.color = Colors.red});
/// Creates a [BendBox] with the given [inward] bend and [color].
const BendBox({super.key, this.inward = const EdgeInsets.all(0), this.color = Colors.red});
@override
Widget build(BuildContext context) {
return CustomPaint(painter: _BendBoxPainter(inward: inward, color: color));
}
@@ -20,6 +28,7 @@ class _BendBoxPainter extends CustomPainter {
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.fill
@@ -39,5 +48,6 @@ class _BendBoxPainter extends CustomPainter {
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'dart:ui';
/// Computes the approximate arc length of a [Bezier] curve by sampling.
double bezierLength(Bezier bezier, [double steps = 10]) {
assert(steps != 0);
final step = 1 / steps;
@@ -14,36 +15,56 @@ double bezierLength(Bezier bezier, [double steps = 10]) {
return length;
}
/// Base class for parametric bezier curves.
///
/// Evaluate a point on the curve with [point] at parameter `t` in [0, 1].
abstract class Bezier {
/// Returns the point on the curve at parameter [t] (0 = start, 1 = end).
Offset point(double t);
/// The approximate arc length of the curve.
double get length => bezierLength(this);
}
/// A straight line segment from [p0] to [p1].
class LinearBezier extends Bezier {
Offset p0, p1;
/// The start point.
Offset p0;
/// The end point.
Offset p1;
/// Creates a linear bezier from [p0] to [p1].
LinearBezier(this.p0, this.p1);
@override
Offset point(double t) {
return p0 + (p1 - p0) * t;
}
@override
double get length => (p1 - p0).distance;
}
/// A quadratic bezier curve with one control point.
class QuadraticBezier extends Bezier {
double _quadraticBezier(double t, double p0, double p1, double p2) {
//final lt = 1 - t;
//return lt * (lt * p0 + t * p1) + t * (lt * p1 + t * p2);
//return pow(1 - t, 2) * p0 + 2 * (1 - t) * t * p1 + pow(t, 2) * p2;
return p1 + pow(1 - t, 2) * (p0 - p1) + pow(t, 2) * (p2 - p1);
}
Offset p0, p1, p2;
/// The start point.
Offset p0;
/// The control point.
Offset p1;
/// The end point.
Offset p2;
/// Creates a quadratic bezier from [p0] to [p2] with control point [p1].
QuadraticBezier(this.p0, this.p1, this.p2);
@override
Offset point(double t) {
return Offset(
_quadraticBezier(t, p0.dx, p1.dx, p2.dx),
@@ -52,6 +73,7 @@ class QuadraticBezier extends Bezier {
}
}
/// A cubic bezier curve with two control points.
class CubicBezier extends Bezier {
double _cubicBezier(double t, double p0, double p1, double p2, double p3) {
return pow(1 - t, 3) * p0 +
@@ -60,35 +82,53 @@ class CubicBezier extends Bezier {
pow(t, 3) * p3;
}
Offset p0, p1, p2, p3;
/// The start point.
Offset p0;
/// The first control point.
Offset p1;
/// The second control point.
Offset p2;
/// The end point.
Offset p3;
/// Creates a cubic bezier from [p0] to [p3] with control points [p1] and [p2].
CubicBezier(this.p0, this.p1, this.p2, this.p3);
Offset point(double t) =>
Offset(
@override
Offset point(double t) => Offset(
_cubicBezier(t, p0.dx, p1.dx, p2.dx, p3.dx),
_cubicBezier(t, p0.dy, p1.dy, p2.dy, p3.dy),
);
}
/// A composite path of multiple bezier segments.
///
/// Build a path incrementally with [lineTo], [quadTo], and [cubeTo].
/// Evaluate any point along the total path with [point].
class PathBezier extends Bezier {
double _length = 0;
@override
double get length => _length;
List<Bezier> _curves = [];
List<double> _lens = [];
final List<Bezier> _curves = [];
final List<double> _lens = [];
/// The starting point of the path.
final Offset p0;
Offset _p0;
/// Creates a path starting at [p0].
PathBezier(this.p0) : _p0 = p0;
/// Creates a path tracing a rounded rectangle.
static PathBezier roundedRect(RRect rrect) {
return PathBezier(Offset(rrect.left + rrect.width / 2, rrect.top))
..lineTo(Offset(rrect.right - rrect.trRadiusX, rrect.top))
..quadTo(
Offset(rrect.right, rrect.top),
..quadTo(Offset(rrect.right, rrect.top),
Offset(rrect.right, rrect.top + rrect.trRadiusY))
..lineTo(Offset(rrect.right, rrect.bottom - rrect.brRadiusY))
..quadTo(Offset(rrect.right, rrect.bottom),
@@ -102,7 +142,7 @@ class PathBezier extends Bezier {
..lineTo(Offset(rrect.left + rrect.width / 2, rrect.top));
}
_add(Bezier bezier, Offset pn) {
void _add(Bezier bezier, Offset pn) {
final bl = bezierLength(bezier);
_curves.add(bezier);
_lens.add(bl);
@@ -110,32 +150,29 @@ class PathBezier extends Bezier {
_p0 = pn;
}
lineTo(Offset p1) => _add(LinearBezier(_p0, p1), p1);
/// Appends a straight line to [p1].
void lineTo(Offset p1) => _add(LinearBezier(_p0, p1), p1);
quadTo(Offset p1, Offset p2) => _add(QuadraticBezier(_p0, p1, p2), p2);
/// Appends a quadratic curve with control point [p1] to endpoint [p2].
void quadTo(Offset p1, Offset p2) =>
_add(QuadraticBezier(_p0, p1, p2), p2);
cubeTo(Offset p1, Offset p2, Offset p3) =>
/// Appends a cubic curve with control points [p1], [p2] to endpoint [p3].
void cubeTo(Offset p1, Offset p2, Offset p3) =>
_add(CubicBezier(_p0, p1, p2, p3), p3);
relativeLineTo(Offset p1) =>
lineTo(
p1 + _p0,
);
/// Appends a straight line to a point relative to the current position.
void relativeLineTo(Offset p1) => lineTo(p1 + _p0);
relativeQuadTo(Offset p1, Offset p2) =>
quadTo(
p1 + p0,
p1 + p2 + p0,
);
relativeCubeTo(Offset p1, Offset p2, Offset p3) =>
cubeTo(
p0 + p1,
p0 + p1 + p2,
p0 + p1 + p2 + p3,
);
/// Appends a quadratic curve with relative control and end points.
void relativeQuadTo(Offset p1, Offset p2) =>
quadTo(p1 + p0, p1 + p2 + p0);
/// Appends a cubic curve with relative control and end points.
void relativeCubeTo(Offset p1, Offset p2, Offset p3) =>
cubeTo(p0 + p1, p0 + p1 + p2, p0 + p1 + p2 + p3);
@override
Offset point(double t) {
if (t > 1) {
t = t - t.floor();

View File

@@ -1,15 +1,28 @@
typedef T _JsonConvert<T>(Map<String, dynamic> json);
/// Signature for a function that converts a JSON map to a typed object.
typedef JsonConvert<T> = T Function(Map<String, dynamic> json);
/// Utilities for converting JSON structures to typed Dart objects.
class Json {
static List<T> list<T>(List? json, _JsonConvert<T> fromJson) => json == null
/// Converts a JSON list of maps to a typed `List<T>`.
///
/// Returns an empty list if [json] is null.
static List<T> list<T>(List? json, JsonConvert<T> fromJson) => json == null
? []
: json.cast<Map<String, dynamic>>().map(fromJson).toList();
static Map<String, T> map<T>(Map? json, _JsonConvert<T> fromJson) => json == null
? {}
: Map.fromEntries(
json.entries.map((e) => MapEntry(e.key, fromJson(e.value))));
/// Converts a JSON map of maps to a typed `Map<String, T>`.
///
/// Returns an empty map if [json] is null.
static Map<String, T> map<T>(Map? json, JsonConvert<T> fromJson) =>
json == null
? {}
: Map.fromEntries(
json.entries.map((e) => MapEntry(e.key, fromJson(e.value))));
/// Reads a value at a dot-separated [path] from a nested JSON structure.
///
/// Supports both map keys and list indices (numeric segments).
/// Returns [defaultValue] if the path doesn't exist.
static dynamic path<T>(Map<String, dynamic> json, String path,
{dynamic defaultValue}) {
try {
@@ -24,7 +37,6 @@ class Json {
});
return current ?? defaultValue;
} catch (error) {
print(error);
return defaultValue;
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
@@ -126,19 +125,50 @@ double _inverseLerp(List<double> samples, double value) {
// ---------------------------------------------------------------------------
/// Frame-accurate keyboard height tracking for iOS and Android.
///
/// Reads the keyboard position directly from the native layer via FFI each
/// frame, bypassing Flutter's `MediaQuery.viewInsets` which lags behind.
///
/// Supports interactive dismiss (swipe-to-dismiss like iMessage) and provides
/// sampled native animation curves with adaptive learning.
///
/// Use the singleton [instance] and listen for changes via [addListener]:
///
/// ```dart
/// final keyboard = UxKeyboard.instance;
/// keyboard.enableInteractiveDismiss(trackingInset: 56);
/// ```
class UxKeyboard with ChangeNotifier {
UxKeyboard._() {
if (_lib == null) return;
SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
}
/// The singleton instance.
static final UxKeyboard instance = UxKeyboard._();
double _height = 0;
/// The current keyboard height in logical pixels.
///
/// Updated every frame while the keyboard is animating or open.
/// Returns 0 when the keyboard is fully closed.
double get height => _height;
/// The last system-reported keyboard height.
///
/// Unlike [height], this is not interpolated — it reflects the target
/// height from the most recent keyboard notification.
double get systemHeight => _uxSystemHeight?.call() ?? 0;
/// Whether the keyboard is currently visible.
bool get isOpen => _height > 0;
/// Whether an interactive dismiss pan gesture is active.
///
/// When true, the user is dragging the keyboard down. Use this to freeze
/// scroll views so they don't fight the pan gesture.
bool get isTracking => (_uxIsTracking?.call() ?? 0) > 0;
// Animation state — replays the keyboard's own animation inside Flutter.
@@ -308,6 +338,13 @@ class UxKeyboard with ChangeNotifier {
_obs.clear();
}
/// Enables swipe-to-dismiss on the keyboard.
///
/// [trackingInset] is the height of your input bar in logical pixels.
/// The dismiss gesture activates when the finger enters the keyboard zone
/// below this inset.
void enableInteractiveDismiss({double trackingInset = 0}) => _uxEnableInteractiveDismiss?.call(trackingInset);
/// Disables the swipe-to-dismiss gesture.
void disableInteractiveDismiss() => _uxDisableInteractiveDismiss?.call();
}

View File

@@ -1,3 +1,7 @@
int _idCounter = 0;
/// Returns a monotonically increasing integer ID.
///
/// Useful for generating unique keys within a single isolate session.
/// Resets to 0 on app restart.
int nextId() => _idCounter++;

View File

@@ -1,6 +1,10 @@
library ux;
/// Flutter toolkit for fluid, native-feeling UIs.
///
/// Includes [UxKeyboard] for frame-accurate keyboard height tracking,
/// [BendBox] for curved layout painting, and bezier curve utilities.
library;
export 'src/bend_box.dart';
export 'src/json_extension.dart';
export 'src/bezier.dart';
export 'src/keyboard.dart';
export 'src/keyboard.dart';

View File

@@ -54,6 +54,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -83,6 +91,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:

View File

@@ -1,7 +1,16 @@
name: ux
description: UX Kit
description: >-
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
and layout primitives.
version: 0.3.0
homepage: https://swipelab.co/ux.html
repository: https://github.com/swipelab/ux
issue_tracker: https://github.com/swipelab/ux/issues
topics:
- keyboard
- ui
- animation
environment:
sdk: ">=3.0.0"
@@ -11,6 +20,7 @@ dependencies:
sdk: flutter
dev_dependencies:
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
@@ -21,4 +31,4 @@ flutter:
pluginClass: KeyboardPlugin
android:
package: io.swipelab.ux
pluginClass: KeyboardPlugin
pluginClass: KeyboardPlugin

View File

@@ -1,21 +1,16 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ux/ux.dart';
void main() {
const MethodChannel channel = MethodChannel('ux');
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
test('UxKeyboard.instance is a singleton', () {
expect(UxKeyboard.instance, same(UxKeyboard.instance));
});
tearDown(() {
channel.setMockMethodCallHandler(null);
test('UxKeyboard.height starts at 0', () {
expect(UxKeyboard.instance.height, 0);
});
test('getPlatformVersion', () async {
expect(await UX.platformVersion, '42');
test('UxKeyboard.isOpen is false when height is 0', () {
expect(UxKeyboard.instance.isOpen, false);
});
}