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

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';