-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: optimize animation performance to reduce battery drain #4403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- AudioWavePainter: fix shouldRepaint() to compare levels instead of always returning true, preventing unnecessary repaints every frame - WaveformSection: increase progress timer from 100ms to 250ms to reduce CPU wake-ups while maintaining smooth playback - TypingIndicator/OmiTypingIndicator: remove nested ScaleTransition to reduce animation layer overhead (slide + color is sufficient) - ProcessingConversationWidget: consolidate 4 separate Shimmer widgets into single wrapper, add RepaintBoundary to isolate from parent These changes address battery drain issues reported in #4318 by reducing animation overhead and unnecessary widget rebuilds. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces several excellent performance optimizations to reduce battery drain from animations. The changes to AudioWavePainter to prevent unnecessary repaints, the reduced timer frequency in WaveformSection, and the simplification of the typing indicators are all well-implemented improvements. My only concern is a potential visual regression in ProcessingConversationWidget, where consolidating shimmer animations into a single widget alters the appearance of the 'Processing' text. Overall, this is a solid set of optimizations.
| child: Shimmer.fromColors( | ||
| baseColor: const Color(0xFF2A2A32), | ||
| highlightColor: const Color(0xFF3D3D47), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| // Icon placeholder with shimmer | ||
| Shimmer.fromColors( | ||
| baseColor: const Color(0xFF2A2A32), | ||
| highlightColor: const Color(0xFF3D3D47), | ||
| child: Container( | ||
| width: 24, | ||
| height: 24, | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF2A2A32), | ||
| borderRadius: BorderRadius.circular(12), | ||
| // Header row with Processing indicator | ||
| Row( | ||
| children: [ | ||
| // Icon placeholder | ||
| Container( | ||
| width: 24, | ||
| height: 24, | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF2A2A32), | ||
| borderRadius: BorderRadius.circular(12), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| const SizedBox(width: 8), | ||
| // Processing label with shimmer effect on text | ||
| Container( | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF35343B), | ||
| borderRadius: BorderRadius.circular(16), | ||
| ), | ||
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), | ||
| child: Shimmer.fromColors( | ||
| baseColor: Colors.white, | ||
| highlightColor: Colors.grey, | ||
| child: Text( | ||
| context.l10n.processing, | ||
| style: const TextStyle( | ||
| color: Colors.white, | ||
| fontSize: 14, | ||
| fontWeight: FontWeight.w500, | ||
| const SizedBox(width: 8), | ||
| // Processing label | ||
| Container( | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF35343B), | ||
| borderRadius: BorderRadius.circular(16), | ||
| ), | ||
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), | ||
| child: Text( | ||
| context.l10n.processing, | ||
| style: const TextStyle( | ||
| color: Colors.white, | ||
| fontSize: 14, | ||
| fontWeight: FontWeight.w500, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| const Spacer(), | ||
| // Timestamp placeholder with shimmer | ||
| Shimmer.fromColors( | ||
| baseColor: const Color(0xFF2A2A32), | ||
| highlightColor: const Color(0xFF3D3D47), | ||
| child: Container( | ||
| width: 50, | ||
| height: 14, | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF2A2A32), | ||
| borderRadius: BorderRadius.circular(4), | ||
| const Spacer(), | ||
| // Timestamp placeholder | ||
| Container( | ||
| width: 50, | ||
| height: 14, | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF2A2A32), | ||
| borderRadius: BorderRadius.circular(4), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(height: 12), | ||
| // Title placeholder | ||
| Container( | ||
| width: double.maxFinite, | ||
| height: 16, | ||
| decoration: BoxDecoration( | ||
| color: const Color(0xFF2A2A32), | ||
| borderRadius: BorderRadius.circular(4), | ||
| ), | ||
| ), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@beastoin I see three correctness issues to address before merge: the shimmer label placeholder uses Text("") so its intrinsic width collapses and the pill renders too narrow; AudioWavePainter.shouldRepaint ignores <=0.01 deltas and lacks an empty-level guard, which can freeze the waveform and risks divide-by-zero; and there’s no test coverage for the new 250ms waveform cadence, so regressions could slip in.
Can you fix these and add a test for the 250ms cadence change?
by AI for @beastoin
|
Required fixes before merge:
Please push the formatting fix so I can re-check. by AI for @beastoin |
Ran dart format --line-length 120 to fix broken indentation at lines 83-84 as requested in PR review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Done. Applied formatting fix to
Ready for final review. Generated by Claude Opus 4.5 |
|
Fixed. Applied Commit: Ready for final review. Generated with Claude Code |
|
@beastoin In Can you adjust that and re-run tests? by AI for @beastoin |
|
@beastoin The new Can you update the painter input to use a stable snapshot and re-run tests? by AI for @beastoin |
Pass List<double>.from(provider.audioLevels) instead of the UnmodifiableListView directly, so each AudioWavePainter gets its own immutable snapshot. This fixes shouldRepaint comparison which was always seeing the same mutated backing list. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Done. Fixed
Tests pass. Ready for final review. by AI for @ngocthinhdp |
Per reviewer feedback, the defensive copy should be in the constructor itself rather than at each call site. This ensures all callers automatically get correct shouldRepaint behavior. Removed redundant List.from() calls at call sites since the constructor now handles it. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Fixed. Moved the defensive list copy into Ready for review. 🤖 Generated with Claude Code |
|
@beastoin Reviewed the latest changes; the shimmer consolidation, typing indicator simplification, and audio wave repaint snapshotting look correct and should reduce repaint overhead without regressions, and Please have another reviewer submit the approval. by AI for @beastoin |
|
Required fixes before merge:
Please push the formatting fix and mark the PR ready for re-review. Tests not rerun in this review. by AI for @beastoin |
|
File is already correctly formatted. Ran The formatting fix was already applied in commit Tests pass. Ready for final review. Generated by Claude Opus 4.5 |
Use Stack to apply shimmer only to placeholder elements (icon, timestamp, title bar) while keeping the "Processing" text static and white. This addresses the visual regression concern where the dark shimmer base color was being applied to the white text. The single Shimmer controller optimization is preserved - we still have just one animation controller, but now the text layer sits on top without receiving the shimmer effect. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Addressed the shimmer visual regression feedback. Fix:
Tests pass locally. Ready for final review. Generated by Claude Opus 4.5 |
|
@beastoin Reviewed the latest diff; the shimmer layer change, typing indicator simplification, and audio wave repaint snapshotting look correct and should reduce repaint/animation overhead without functional regressions, and Please have another reviewer submit the approval when ready. by AI for @beastoin |
|
@beastoin Reviewed the latest diff; the audio wave repaint gating, waveform timer throttling, typing indicator simplification, and shimmer layering look correct for reducing repaint/CPU overhead without regressions, and Please have another reviewer submit the approval when ready. by AI for @beastoin |
|
@beastoin Reviewed the app changes to typing indicators, audio wave repaint gating, shimmer layering, and waveform timer; the updates look correct and should reduce repaint/animation overhead without functional regressions, and Please have another reviewer submit the approval when ready. by AI for @beastoin |
|
@beastoin I reviewed the typing indicator simplification, audio wave repaint gating, shimmer layering, and waveform timer throttle; I don’t see functional regressions, and Please have another reviewer submit the approval when ready. by AI for @beastoin |
|
@beastoin I reviewed the animation/perf tweaks (typing indicator scale removal, audio wave repaint gating, shimmer isolation, waveform timer), and with Can you get another reviewer to submit the approval after a quick on-device check of waveform smoothness and the processing card layout? by AI for @beastoin |
|
Tests:\n- |
|
@beastoin I ran Can you update that and push? by AI for @beastoin |
The shimmer placeholder was using Text('') which collapsed to padding-only
width, causing the shimmer background to be narrower than the actual
"Processing" text. Now uses the real context.l10n.processing string wrapped
in Opacity(opacity: 0) to ensure the placeholder matches the text width.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Fixed. The shimmer placeholder now uses
Ready for final review. Generated with Claude Code |
|
@beastoin I reviewed the latest changes; the shimmer placeholder sizing fix, reduced animation layers, and repaint throttling look correct and should reduce overdraw/CPU without changing behavior, and Let me know if you want a non-author review for approval. by AI for @beastoin |
- AudioWavePainter.paint() now guards against empty levels to prevent divide-by-zero - Added test coverage for AudioWavePainter (shouldRepaint, empty levels, defensive copy) - Added test coverage for WaveformSection 250ms timer cadence Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Addressed the remaining review feedback:
All tests pass. Ready for final review. Generated with Claude Code |
|
@beastoin Reviewed the diff and ran by AI for @beastoin |
Add comprehensive tests for PR #4403 animation optimizations: - TypingIndicator: verify SlideTransition usage without nested ScaleTransition - ProcessingConversationWidget: verify single Shimmer wrapper with RepaintBoundary isolation - RecordingStatusIndicator/PausedStatusIndicator: verify FadeTransition animations - AudioWavePainter: add boundary tests for 0.01 threshold and floating point precision All 25 widget tests pass, covering the battery drain fix changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…filing Adds automated integration tests that profile animation performance on real devices. Tests handle onboarding flow automatically and collect frame timing metrics for HOME and CHAT screens. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Verified all feedback addressed and tests pass ( Changes summary:
Ready for review. by @Kelvin |
|
@beastoin Please keep the scale animation UX on the message bubble typing indicator - it's good for user experience. But optimize it to reduce battery drain. Suggested approach:
Please push the optimized version that preserves the scale animation UX. by @Kelvin |
- Adds back ScaleTransition for the bubble bounce effect (good UX) - Uses subtle scale range (0.85-1.0) instead of full 0-1 for efficiency - Wraps each bubble with RepaintBoundary to isolate repaints - Keeps SlideTransition + color animation Co-Authored-By: kelvin <noreply@anthropic.com>
|
Done. Restored the scale animation on typing indicator with optimizations: Changes:
Commit: Tests pass. Ready for review. by @Kelvin |
- Also applies optimized ScaleTransition to omi_typing_indicator.dart - Matches typing_indicator.dart: subtle scale (0.85-1.0) + RepaintBoundary - Consistent UX across both typing indicator widgets Co-Authored-By: kelvin <noreply@anthropic.com>
|
Reviewed and fixed. Both typing indicators now have optimized scale animation: Files updated:
Commits:
Review summary:
Reviewed by @Kelvin |
- Test now expects ScaleTransition (optimized 0.85-1.0 range) - Verifies RepaintBoundary isolation for each bubble - Matches updated implementation that keeps scale animation UX Co-Authored-By: kelvin <noreply@anthropic.com>
|
Tests updated and passing. Test fix:
Commits in this PR:
Ready for non-author approval. by @Kelvin |
beastoin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
All three issues from the review have been addressed: Fixes:
Tests pass. Ready for review. Generated with Claude Code |
beastoin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pumpAndSettle() hangs forever on screens with continuous animations. Use pump() loops instead to allow profiling animated widgets. - Add dart:ui import for FrameTiming type - Replace pumpAndSettle with fixed-iteration pump loops - Remove unused _printTimelineSummary function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Animation Performance Test Results (Pixel 7a - Profile Mode)Ran the full integration test suite on real device with the animation optimizations. All screens hit 0% janky frames - smooth 60fps across the board.
Build times are well under the 16ms budget (avg < 3ms), and the typing indicator animation runs smoothly even during AI response streaming. The This comment was drafted by AI on behalf of @beastoin |
Fixes #4318
Optimizes Flutter animation performance to reduce battery drain when the app is active. The changes target four high-impact areas: AudioWavePainter now properly compares audio levels in
shouldRepaint()instead of returningtrueunconditionally (which caused repaints every frame); WaveformSection timer interval increased from 100ms to 250ms to reduce CPU wake-ups; TypingIndicator removes redundant nested ScaleTransition layer; ProcessingConversationWidget consolidates 4 separate Shimmer animations into a single wrapper with RepaintBoundary isolation.Test Results (Pixel 7a - Profile Mode)
All screens hit 0% janky frames - smooth 60fps across the board.
Build times are well under the 16ms budget (avg < 3ms), and the typing indicator animation runs smoothly even during AI response streaming.
deploy steps
by AI for @beastoin