opt: легонько наоптимизибурмалдил в rlottie стикерах/эмодзи
This commit is contained in:
@@ -29,7 +29,7 @@ class RlottieDiskCache {
|
||||
static final RlottieDiskCache instance = RlottieDiskCache._();
|
||||
|
||||
static const _magic = 0x4b524c46;
|
||||
static const _version = 1;
|
||||
static const _version = 2;
|
||||
static const int _maxBytes = 256 * 1024 * 1024;
|
||||
|
||||
Directory? _dir;
|
||||
@@ -103,6 +103,13 @@ class RlottieDiskCache {
|
||||
for (var i = 0; i < frameCount; i++) {
|
||||
payload.setRange(i * frameBytes, (i + 1) * frameBytes, frames[i]);
|
||||
}
|
||||
for (var i = frameCount - 1; i >= 1; i--) {
|
||||
final cur = i * frameBytes;
|
||||
final prev = (i - 1) * frameBytes;
|
||||
for (var b = 0; b < frameBytes; b++) {
|
||||
payload[cur + b] ^= payload[prev + b];
|
||||
}
|
||||
}
|
||||
final compressed = gzip.encode(payload);
|
||||
final header = ByteData(28);
|
||||
header.setUint32(0, _magic);
|
||||
@@ -131,6 +138,13 @@ class RlottieDiskCache {
|
||||
final frameBytes = px * px * 4;
|
||||
final payload = Uint8List.fromList(gzip.decode(bytes.sublist(28)));
|
||||
if (payload.length != frameBytes * frameCount) return null;
|
||||
for (var i = 1; i < frameCount; i++) {
|
||||
final cur = i * frameBytes;
|
||||
final prev = (i - 1) * frameBytes;
|
||||
for (var b = 0; b < frameBytes; b++) {
|
||||
payload[cur + b] ^= payload[prev + b];
|
||||
}
|
||||
}
|
||||
final frames = <Uint8List>[];
|
||||
for (var i = 0; i < frameCount; i++) {
|
||||
frames.add(Uint8List.sublistView(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
@@ -81,7 +82,7 @@ class RlottieEngine {
|
||||
|
||||
static String? debugLibraryPath;
|
||||
|
||||
static const int _maxBytes = 384 * 1024 * 1024;
|
||||
static const int _maxBytes = 192 * 1024 * 1024;
|
||||
static const int _modelCacheBytes = 20 * 1024 * 1024;
|
||||
|
||||
final Map<String, RlottieClip> _clips = {};
|
||||
@@ -91,9 +92,10 @@ class RlottieEngine {
|
||||
int _totalBytes = 0;
|
||||
int _clock = 0;
|
||||
int _nextJobId = 1;
|
||||
int _rrIndex = 0;
|
||||
|
||||
bool? _available;
|
||||
Future<SendPort>? _workerFuture;
|
||||
Future<List<SendPort>>? _poolFuture;
|
||||
|
||||
int _tick() => ++_clock;
|
||||
String _keyFor(String url, int px) => '$url@$px';
|
||||
@@ -107,15 +109,23 @@ class RlottieEngine {
|
||||
}();
|
||||
}
|
||||
|
||||
Future<SendPort> _worker() {
|
||||
return _workerFuture ??= () async {
|
||||
Future<SendPort> _worker() async {
|
||||
final pool = await (_poolFuture ??= _spawnPool());
|
||||
return pool[_rrIndex++ % pool.length];
|
||||
}
|
||||
|
||||
Future<List<SendPort>> _spawnPool() async {
|
||||
final count = (Platform.numberOfProcessors - 1).clamp(1, 3);
|
||||
final ports = <SendPort>[];
|
||||
for (var i = 0; i < count; i++) {
|
||||
final receive = ReceivePort();
|
||||
await Isolate.spawn(rlottieWorkerMain, receive.sendPort);
|
||||
final broadcast = receive.asBroadcastStream();
|
||||
final port = await broadcast.first as SendPort;
|
||||
broadcast.listen(_onWorkerMessage);
|
||||
return port;
|
||||
}();
|
||||
ports.add(port);
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
void _onWorkerMessage(dynamic message) {
|
||||
@@ -155,7 +165,7 @@ class RlottieEngine {
|
||||
bytes,
|
||||
frame.px,
|
||||
frame.px,
|
||||
ui.PixelFormat.rgba8888,
|
||||
ui.PixelFormat.bgra8888,
|
||||
(image) {
|
||||
job.clip._setFrame(frame.index, image);
|
||||
_totalBytes += frame.px * frame.px * 4;
|
||||
@@ -229,8 +239,13 @@ class RlottieEngine {
|
||||
if (inlineJson == null) {
|
||||
final disk = await RlottieDiskCache.instance.load(url, px);
|
||||
if (disk != null) {
|
||||
final clip = await _clipFromDisk(key, px, disk);
|
||||
final clip = RlottieClip(key: key, px: px)
|
||||
..frameCount = disk.frameCount
|
||||
..frameRate = disk.frameRate
|
||||
..durationMs = disk.durationMs;
|
||||
clip._allocate(disk.frameCount);
|
||||
_clips[key] = clip;
|
||||
unawaited(_decodeDiskProgressive(clip, disk));
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
@@ -255,26 +270,25 @@ class RlottieEngine {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<RlottieClip> _clipFromDisk(String key, int px, DiskClip disk) async {
|
||||
final clip = RlottieClip(key: key, px: px)
|
||||
..frameCount = disk.frameCount
|
||||
..frameRate = disk.frameRate
|
||||
..durationMs = disk.durationMs
|
||||
..complete = true;
|
||||
clip._allocate(disk.frameCount);
|
||||
Future<void> _decodeDiskProgressive(RlottieClip clip, DiskClip disk) async {
|
||||
for (var i = 0; i < disk.frameCount; i++) {
|
||||
final image = await _decode(disk.frames[i], px);
|
||||
if (!identical(_clips[clip.key], clip)) return;
|
||||
final image = await _decode(disk.frames[i], clip.px);
|
||||
if (!identical(_clips[clip.key], clip)) {
|
||||
image.dispose();
|
||||
return;
|
||||
}
|
||||
clip._setFrame(i, image);
|
||||
_totalBytes += px * px * 4;
|
||||
_totalBytes += clip.px * clip.px * 4;
|
||||
}
|
||||
clip.complete = true;
|
||||
_evictIfNeeded();
|
||||
return clip;
|
||||
}
|
||||
|
||||
Future<ui.Image> _decode(Uint8List rgba, int px) {
|
||||
Future<ui.Image> _decode(Uint8List bgra, int px) {
|
||||
final completer = Completer<ui.Image>();
|
||||
ui.decodeImageFromPixels(
|
||||
rgba, px, px, ui.PixelFormat.rgba8888, completer.complete);
|
||||
bgra, px, px, ui.PixelFormat.bgra8888, completer.complete);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ class CancelJob {
|
||||
final int jobId;
|
||||
}
|
||||
|
||||
const double _maxCacheFps = 30.0;
|
||||
|
||||
void rlottieWorkerMain(SendPort toMain) {
|
||||
final port = ReceivePort();
|
||||
toMain.send(port.sendPort);
|
||||
@@ -104,24 +106,33 @@ void rlottieWorkerMain(SendPort toMain) {
|
||||
final total = rl.totalFrame(anim);
|
||||
final fps = rl.frameRate(anim);
|
||||
final durationMs = fps <= 0 ? 1000 : (total / fps * 1000).round();
|
||||
|
||||
var outCount = total;
|
||||
if (fps > _maxCacheFps && total > 1) {
|
||||
outCount = (durationMs / 1000.0 * _maxCacheFps).round().clamp(2, total);
|
||||
}
|
||||
final outFps = durationMs <= 0 ? fps : outCount * 1000.0 / durationMs;
|
||||
toMain.send(ClipMeta(
|
||||
jobId: job.jobId,
|
||||
totalFrame: total,
|
||||
frameRate: fps,
|
||||
totalFrame: outCount,
|
||||
frameRate: outFps,
|
||||
durationMs: durationMs,
|
||||
));
|
||||
|
||||
final px = job.px;
|
||||
final buffer = calloc<Uint32>(px * px);
|
||||
final byteView = buffer.cast<Uint8>().asTypedList(px * px * 4);
|
||||
try {
|
||||
for (var i = 0; i < total; i++) {
|
||||
for (var i = 0; i < outCount; i++) {
|
||||
if (cancelled.contains(job.jobId)) break;
|
||||
rl.render(anim, i, buffer, px);
|
||||
final rgba = _bgraToRgba(buffer, px * px);
|
||||
final src = outCount == total
|
||||
? i
|
||||
: (i * (total - 1) / (outCount - 1)).round().clamp(0, total - 1);
|
||||
rl.render(anim, src, buffer, px);
|
||||
toMain.send(RenderedFrame(
|
||||
jobId: job.jobId,
|
||||
index: i,
|
||||
data: TransferableTypedData.fromList([rgba]),
|
||||
data: TransferableTypedData.fromList([Uint8List.fromList(byteView)]),
|
||||
px: px,
|
||||
));
|
||||
}
|
||||
@@ -138,16 +149,3 @@ void rlottieWorkerMain(SendPort toMain) {
|
||||
});
|
||||
}
|
||||
|
||||
Uint8List _bgraToRgba(Pointer<Uint32> buffer, int pixels) {
|
||||
final src = buffer.asTypedList(pixels);
|
||||
final out = Uint8List(pixels * 4);
|
||||
for (var i = 0; i < pixels; i++) {
|
||||
final v = src[i];
|
||||
final o = i * 4;
|
||||
out[o] = (v >> 16) & 0xff;
|
||||
out[o + 1] = (v >> 8) & 0xff;
|
||||
out[o + 2] = v & 0xff;
|
||||
out[o + 3] = (v >> 24) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ class LottiePlayer extends StatefulWidget {
|
||||
class _LottiePlayerState extends State<LottiePlayer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const int _leadFrames = 6;
|
||||
static const double _slowSpeed = 0.5;
|
||||
static const double _rampMs = 200.0;
|
||||
|
||||
final ValueNotifier<int> _frameIndex = ValueNotifier(0);
|
||||
late final Ticker _ticker;
|
||||
@@ -94,6 +96,11 @@ class _LottiePlayerState extends State<LottiePlayer>
|
||||
bool _started = false;
|
||||
bool _showedFrames = false;
|
||||
|
||||
double _speed = 1.0;
|
||||
double _targetSpeed = 1.0;
|
||||
double _playheadMs = 0.0;
|
||||
double? _lastElapsedMs;
|
||||
|
||||
bool get _isScrolling => _scrollState?.value ?? false;
|
||||
bool get _canLoad =>
|
||||
!_isScrolling && !LottieLoadGovernor.instance.throttled.value;
|
||||
@@ -125,6 +132,10 @@ class _LottiePlayerState extends State<LottiePlayer>
|
||||
_releaseClip();
|
||||
_started = false;
|
||||
_showedFrames = false;
|
||||
_playheadMs = 0.0;
|
||||
_speed = 1.0;
|
||||
_targetSpeed = _isScrolling ? _slowSpeed : 1.0;
|
||||
_lastElapsedMs = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,20 +163,31 @@ class _LottiePlayerState extends State<LottiePlayer>
|
||||
if (clip == null || clip.frameCount <= 1) return;
|
||||
final periodMs = clip.durationMs;
|
||||
if (periodMs <= 0) return;
|
||||
final t = (elapsed.inMilliseconds % periodMs) / periodMs;
|
||||
final index = (t * (clip.frameCount - 1)).round().clamp(
|
||||
0,
|
||||
clip.frameCount - 1,
|
||||
);
|
||||
|
||||
final nowMs = elapsed.inMicroseconds / 1000.0;
|
||||
final last = _lastElapsedMs;
|
||||
_lastElapsedMs = nowMs;
|
||||
if (last == null) return;
|
||||
var dt = nowMs - last;
|
||||
if (dt < 0) dt = 0;
|
||||
if (dt > 64) dt = 64;
|
||||
|
||||
if (_speed != _targetSpeed) {
|
||||
final step = dt / _rampMs * (1.0 - _slowSpeed);
|
||||
final diff = _targetSpeed - _speed;
|
||||
_speed = diff.abs() <= step ? _targetSpeed : _speed + step * diff.sign;
|
||||
}
|
||||
|
||||
_playheadMs = (_playheadMs + dt * _speed) % periodMs;
|
||||
final t = _playheadMs / periodMs;
|
||||
final index =
|
||||
(t * (clip.frameCount - 1)).round().clamp(0, clip.frameCount - 1);
|
||||
if (index != _frameIndex.value) _frameIndex.value = index;
|
||||
}
|
||||
|
||||
void _onGateChanged() {
|
||||
if (!mounted) return;
|
||||
if (_isScrolling) {
|
||||
if (_ticker.isActive) _ticker.stop();
|
||||
return;
|
||||
}
|
||||
_targetSpeed = _isScrolling ? _slowSpeed : 1.0;
|
||||
final clip = _clip;
|
||||
if (clip != null) {
|
||||
_maybeStartTicker(clip);
|
||||
@@ -182,15 +204,18 @@ class _LottiePlayerState extends State<LottiePlayer>
|
||||
}
|
||||
|
||||
void _maybeStartTicker(RlottieClip clip) {
|
||||
if (_isScrolling || clip.frameCount <= 1) return;
|
||||
if (clip.frameCount <= 1) return;
|
||||
final lead = clip.frameCount < _leadFrames ? clip.frameCount : _leadFrames;
|
||||
if (clip.ready.value >= lead && !_ticker.isActive) _ticker.start();
|
||||
if (clip.ready.value >= lead && !_ticker.isActive) {
|
||||
_lastElapsedMs = null;
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
void _ensure(double box) {
|
||||
if (_clip != null) return;
|
||||
final dpr = MediaQuery.devicePixelRatioOf(context);
|
||||
final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 512.0);
|
||||
final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 384.0);
|
||||
_px = (raw / 32).ceil() * 32;
|
||||
if (_started || !_canLoad) return;
|
||||
_startLoad();
|
||||
|
||||
Reference in New Issue
Block a user