Flutter ClipRect Localization: Rectangular Clipping for Multilingual Apps
ClipRect clips its child to a rectangular region, useful for creating reveal effects, image cropping, and progressive disclosure patterns. When combined with localization, ClipRect enables dynamic content reveals that respect text direction and cultural reading patterns. This guide covers comprehensive strategies for localizing ClipRect widgets in Flutter multilingual applications.
Understanding ClipRect Localization
ClipRect widgets require localization for:
- Progressive reveals: Text unveiling animations respecting RTL
- Image cropping: Direction-aware image viewing
- Reading progress: Highlight indicators for completed content
- Spoiler content: Hidden content reveals with localized warnings
- Comparison sliders: Before/after comparisons with localized labels
- Truncated previews: Content previews with expand functionality
Basic ClipRect with Localized Content
Start with a simple clipping example:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedClipRectDemo extends StatefulWidget {
const LocalizedClipRectDemo({super.key});
@override
State<LocalizedClipRectDemo> createState() => _LocalizedClipRectDemoState();
}
class _LocalizedClipRectDemoState extends State<LocalizedClipRectDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _revealAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_revealAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleReveal() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.clipRectTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.clipRectDescription,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
Semantics(
label: l10n.revealableContentAccessibility,
child: AnimatedBuilder(
animation: _revealAnimation,
builder: (context, child) {
return ClipRect(
clipper: _DirectionalClipper(
revealFraction: _revealAnimation.value,
isRtl: isRtl,
),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.hiddenContentText,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 18,
),
),
),
);
},
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _toggleReveal,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Text(
_controller.isCompleted ? l10n.hideContent : l10n.revealContent,
);
},
),
),
],
),
),
);
}
}
class _DirectionalClipper extends CustomClipper<Rect> {
final double revealFraction;
final bool isRtl;
_DirectionalClipper({
required this.revealFraction,
required this.isRtl,
});
@override
Rect getClip(Size size) {
if (isRtl) {
// RTL: reveal from right to left
final left = size.width * (1 - revealFraction);
return Rect.fromLTWH(left, 0, size.width * revealFraction, size.height);
} else {
// LTR: reveal from left to right
return Rect.fromLTWH(0, 0, size.width * revealFraction, size.height);
}
}
@override
bool shouldReclip(covariant _DirectionalClipper oldClipper) {
return revealFraction != oldClipper.revealFraction || isRtl != oldClipper.isRtl;
}
}
ARB File Structure for ClipRect
{
"clipRectTitle": "Content Reveal",
"@clipRectTitle": {
"description": "Title for clip rect demo"
},
"clipRectDescription": "Tap the button to reveal hidden content",
"revealableContentAccessibility": "Hidden content area, activate button to reveal",
"hiddenContentText": "This is the hidden content that gets revealed progressively from the reading direction start.",
"hideContent": "Hide Content",
"revealContent": "Reveal Content"
}
Reading Progress Indicator
Create a reading progress overlay with directional clipping:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedReadingProgress extends StatefulWidget {
const LocalizedReadingProgress({super.key});
@override
State<LocalizedReadingProgress> createState() => _LocalizedReadingProgressState();
}
class _LocalizedReadingProgressState extends State<LocalizedReadingProgress> {
final ScrollController _scrollController = ScrollController();
double _readProgress = 0.0;
@override
void initState() {
super.initState();
_scrollController.addListener(_updateProgress);
}
void _updateProgress() {
if (_scrollController.hasClients) {
final maxScroll = _scrollController.position.maxScrollExtent;
if (maxScroll > 0) {
setState(() {
_readProgress = (_scrollController.offset / maxScroll).clamp(0.0, 1.0);
});
}
}
}
@override
void dispose() {
_scrollController.removeListener(_updateProgress);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(
title: Text(l10n.articleTitle),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(4),
child: Semantics(
label: l10n.readingProgressAccessibility(
(_readProgress * 100).round(),
),
child: Stack(
children: [
Container(
height: 4,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
ClipRect(
clipper: _ProgressClipper(
progress: _readProgress,
isRtl: isRtl,
),
child: Container(
height: 4,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
),
body: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.articleHeading,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
...List.generate(10, (index) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
l10n.articleParagraph(index + 1),
style: Theme.of(context).textTheme.bodyLarge,
),
)),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.progressMessage((_readProgress * 100).round())),
),
);
},
icon: const Icon(Icons.analytics),
label: Text('${(_readProgress * 100).round()}%'),
),
);
}
}
class _ProgressClipper extends CustomClipper<Rect> {
final double progress;
final bool isRtl;
_ProgressClipper({required this.progress, required this.isRtl});
@override
Rect getClip(Size size) {
if (isRtl) {
final left = size.width * (1 - progress);
return Rect.fromLTWH(left, 0, size.width * progress, size.height);
}
return Rect.fromLTWH(0, 0, size.width * progress, size.height);
}
@override
bool shouldReclip(covariant _ProgressClipper oldClipper) {
return progress != oldClipper.progress || isRtl != oldClipper.isRtl;
}
}
Before/After Comparison Slider
Create an image comparison slider with localized labels:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedComparisonSlider extends StatefulWidget {
final String beforeImage;
final String afterImage;
const LocalizedComparisonSlider({
super.key,
required this.beforeImage,
required this.afterImage,
});
@override
State<LocalizedComparisonSlider> createState() => _LocalizedComparisonSliderState();
}
class _LocalizedComparisonSliderState extends State<LocalizedComparisonSlider> {
double _sliderPosition = 0.5;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.comparisonSliderTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
l10n.comparisonInstructions,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Expanded(
child: Semantics(
label: l10n.comparisonSliderAccessibility(
(_sliderPosition * 100).round(),
),
slider: true,
value: '${(_sliderPosition * 100).round()}%',
child: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onHorizontalDragUpdate: (details) {
setState(() {
_sliderPosition = (details.localPosition.dx / constraints.maxWidth)
.clamp(0.0, 1.0);
});
},
child: Stack(
children: [
// After image (full)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
widget.afterImage,
fit: BoxFit.cover,
),
),
),
// Before image (clipped)
Positioned.fill(
child: ClipRect(
clipper: _ComparisonClipper(
position: _sliderPosition,
isRtl: isRtl,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
widget.beforeImage,
fit: BoxFit.cover,
),
),
),
),
// Labels
Positioned(
left: isRtl ? null : 12,
right: isRtl ? 12 : null,
top: 12,
child: _buildLabel(context, l10n.beforeLabel),
),
Positioned(
left: isRtl ? 12 : null,
right: isRtl ? null : 12,
top: 12,
child: _buildLabel(context, l10n.afterLabel),
),
// Slider handle
Positioned(
left: constraints.maxWidth * _sliderPosition - 20,
top: 0,
bottom: 0,
child: Container(
width: 40,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 4,
height: constraints.maxHeight * 0.4,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
),
],
),
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
),
],
),
child: const Icon(
Icons.swap_horiz,
color: Colors.black87,
),
),
Container(
width: 4,
height: constraints.maxHeight * 0.4,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
),
],
),
),
],
),
),
),
],
),
);
},
),
),
),
],
),
),
);
}
Widget _buildLabel(BuildContext context, String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(16),
),
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
);
}
}
class _ComparisonClipper extends CustomClipper<Rect> {
final double position;
final bool isRtl;
_ComparisonClipper({required this.position, required this.isRtl});
@override
Rect getClip(Size size) {
if (isRtl) {
final left = size.width * (1 - position);
return Rect.fromLTWH(left, 0, size.width * position, size.height);
}
return Rect.fromLTWH(0, 0, size.width * position, size.height);
}
@override
bool shouldReclip(covariant _ComparisonClipper oldClipper) {
return position != oldClipper.position || isRtl != oldClipper.isRtl;
}
}
Spoiler Content Revealer
Create a spoiler system with localized warnings:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSpoilerContent extends StatefulWidget {
final String spoilerText;
final String? spoilerCategory;
const LocalizedSpoilerContent({
super.key,
required this.spoilerText,
this.spoilerCategory,
});
@override
State<LocalizedSpoilerContent> createState() => _LocalizedSpoilerContentState();
}
class _LocalizedSpoilerContentState extends State<LocalizedSpoilerContent>
with SingleTickerProviderStateMixin {
bool _isRevealed = false;
late AnimationController _controller;
late Animation<double> _revealAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_revealAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutQuart,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleSpoiler() {
setState(() {
_isRevealed = !_isRevealed;
if (_isRevealed) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Semantics(
label: _isRevealed
? l10n.spoilerRevealedAccessibility(widget.spoilerText)
: l10n.spoilerHiddenAccessibility(
widget.spoilerCategory ?? l10n.spoilerDefaultCategory,
),
button: true,
child: GestureDetector(
onTap: _toggleSpoiler,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_isRevealed ? Icons.visibility : Icons.visibility_off,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
widget.spoilerCategory ?? l10n.spoilerDefaultCategory,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
_isRevealed ? l10n.tapToHide : l10n.tapToReveal,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 12),
AnimatedBuilder(
animation: _revealAnimation,
builder: (context, child) {
return Stack(
children: [
// Blurred background when hidden
if (_revealAnimation.value < 1.0)
Opacity(
opacity: 1 - _revealAnimation.value,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n.spoilerWarning,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
),
),
// Revealed content
ClipRect(
clipper: _VerticalRevealClipper(
revealFraction: _revealAnimation.value,
),
child: Opacity(
opacity: _revealAnimation.value,
child: Text(
widget.spoilerText,
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
],
);
},
),
],
),
),
),
);
}
}
class _VerticalRevealClipper extends CustomClipper<Rect> {
final double revealFraction;
_VerticalRevealClipper({required this.revealFraction});
@override
Rect getClip(Size size) {
return Rect.fromLTWH(0, 0, size.width, size.height * revealFraction);
}
@override
bool shouldReclip(covariant _VerticalRevealClipper oldClipper) {
return revealFraction != oldClipper.revealFraction;
}
}
class SpoilerDemoPage extends StatelessWidget {
const SpoilerDemoPage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.spoilerDemoTitle)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
l10n.spoilerDemoDescription,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
LocalizedSpoilerContent(
spoilerCategory: l10n.movieSpoilerCategory,
spoilerText: l10n.movieSpoilerText,
),
const SizedBox(height: 16),
LocalizedSpoilerContent(
spoilerCategory: l10n.bookSpoilerCategory,
spoilerText: l10n.bookSpoilerText,
),
const SizedBox(height: 16),
LocalizedSpoilerContent(
spoilerCategory: l10n.gameSpoilerCategory,
spoilerText: l10n.gameSpoilerText,
),
],
),
);
}
}
Animated Text Reveal
Create a typewriter-style text reveal:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedTextReveal extends StatefulWidget {
const LocalizedTextReveal({super.key});
@override
State<LocalizedTextReveal> createState() => _LocalizedTextRevealState();
}
class _LocalizedTextRevealState extends State<LocalizedTextReveal>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
int _currentQuoteIndex = 0;
final List<String> _quoteKeys = [
'inspirationalQuote1',
'inspirationalQuote2',
'inspirationalQuote3',
];
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _nextQuote() {
setState(() {
_currentQuoteIndex = (_currentQuoteIndex + 1) % _quoteKeys.length;
});
_controller.reset();
_controller.forward();
}
String _getQuote(AppLocalizations l10n, int index) {
switch (index) {
case 0:
return l10n.inspirationalQuote1;
case 1:
return l10n.inspirationalQuote2;
case 2:
return l10n.inspirationalQuote3;
default:
return l10n.inspirationalQuote1;
}
}
String _getAuthor(AppLocalizations l10n, int index) {
switch (index) {
case 0:
return l10n.quoteAuthor1;
case 1:
return l10n.quoteAuthor2;
case 2:
return l10n.quoteAuthor3;
default:
return l10n.quoteAuthor1;
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final quote = _getQuote(l10n, _currentQuoteIndex);
final author = _getAuthor(l10n, _currentQuoteIndex);
return Scaffold(
appBar: AppBar(title: Text(l10n.textRevealTitle)),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.format_quote,
size: 48,
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
),
const SizedBox(height: 24),
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Semantics(
label: l10n.quoteAccessibility(quote, author),
child: ClipRect(
clipper: _TextRevealClipper(
revealFraction: _controller.value,
isRtl: isRtl,
),
child: Text(
quote,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontStyle: FontStyle.italic,
height: 1.5,
),
textAlign: TextAlign.center,
),
),
);
},
),
const SizedBox(height: 16),
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value >= 0.9 ? 1.0 : 0.0,
child: Text(
'— $author',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
);
},
),
const SizedBox(height: 48),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton.icon(
onPressed: () {
_controller.reset();
_controller.forward();
},
icon: const Icon(Icons.replay),
label: Text(l10n.replayAnimation),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: _nextQuote,
icon: const Icon(Icons.arrow_forward),
label: Text(l10n.nextQuote),
),
],
),
const SizedBox(height: 24),
// Quote indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_quoteKeys.length, (index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index == _currentQuoteIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
);
}),
),
],
),
),
);
}
}
class _TextRevealClipper extends CustomClipper<Rect> {
final double revealFraction;
final bool isRtl;
_TextRevealClipper({
required this.revealFraction,
required this.isRtl,
});
@override
Rect getClip(Size size) {
if (isRtl) {
final left = size.width * (1 - revealFraction);
return Rect.fromLTWH(left, 0, size.width * revealFraction, size.height);
}
return Rect.fromLTWH(0, 0, size.width * revealFraction, size.height);
}
@override
bool shouldReclip(covariant _TextRevealClipper oldClipper) {
return revealFraction != oldClipper.revealFraction || isRtl != oldClipper.isRtl;
}
}
Complete ARB File for ClipRect
{
"@@locale": "en",
"clipRectTitle": "Content Reveal",
"clipRectDescription": "Tap the button to reveal hidden content",
"revealableContentAccessibility": "Hidden content area, activate button to reveal",
"hiddenContentText": "This is the hidden content that gets revealed progressively from the reading direction start.",
"hideContent": "Hide Content",
"revealContent": "Reveal Content",
"articleTitle": "Article Reader",
"articleHeading": "Understanding Flutter Clipping Widgets",
"articleParagraph": "This is paragraph {number} of the article content. Flutter provides powerful clipping widgets that allow you to create sophisticated visual effects and content reveals.",
"@articleParagraph": {
"placeholders": {
"number": {"type": "int"}
}
},
"readingProgressAccessibility": "Reading progress: {percent} percent complete",
"@readingProgressAccessibility": {
"placeholders": {
"percent": {"type": "int"}
}
},
"progressMessage": "You have read {percent}% of this article",
"@progressMessage": {
"placeholders": {
"percent": {"type": "int"}
}
},
"comparisonSliderTitle": "Before & After",
"comparisonInstructions": "Drag the slider left or right to compare images",
"comparisonSliderAccessibility": "Image comparison at {percent} percent",
"@comparisonSliderAccessibility": {
"placeholders": {
"percent": {"type": "int"}
}
},
"beforeLabel": "Before",
"afterLabel": "After",
"spoilerDemoTitle": "Spoiler Content",
"spoilerDemoDescription": "Tap on any spoiler block to reveal its hidden content",
"spoilerDefaultCategory": "Spoiler",
"spoilerWarning": "Contains spoiler content",
"tapToReveal": "Tap to reveal",
"tapToHide": "Tap to hide",
"spoilerHiddenAccessibility": "{category} content hidden, tap to reveal",
"@spoilerHiddenAccessibility": {
"placeholders": {
"category": {"type": "String"}
}
},
"spoilerRevealedAccessibility": "Spoiler revealed: {content}",
"@spoilerRevealedAccessibility": {
"placeholders": {
"content": {"type": "String"}
}
},
"movieSpoilerCategory": "Movie Ending",
"movieSpoilerText": "The twist at the end reveals that the hero was actually the villain all along!",
"bookSpoilerCategory": "Book Plot",
"bookSpoilerText": "The mysterious character turns out to be the protagonist's long-lost sibling.",
"gameSpoilerCategory": "Game Secret",
"gameSpoilerText": "There's a hidden ending if you collect all the secret items.",
"textRevealTitle": "Daily Inspiration",
"inspirationalQuote1": "The only way to do great work is to love what you do.",
"quoteAuthor1": "Steve Jobs",
"inspirationalQuote2": "Innovation distinguishes between a leader and a follower.",
"quoteAuthor2": "Steve Jobs",
"inspirationalQuote3": "Stay hungry, stay foolish.",
"quoteAuthor3": "Steve Jobs",
"quoteAccessibility": "Quote: {quote}, by {author}",
"@quoteAccessibility": {
"placeholders": {
"quote": {"type": "String"},
"author": {"type": "String"}
}
},
"replayAnimation": "Replay",
"nextQuote": "Next Quote"
}
Best Practices Summary
- Use CustomClipper for RTL: Create custom clippers that respect text direction
- Provide semantic labels: Describe the hidden/revealed state for accessibility
- Animate smoothly: Use curved animations for natural reveal effects
- Consider reading direction: Always use Directionality.of(context) for RTL support
- Combine with opacity: Fade content in as it's revealed for polish
- Handle edge cases: Ensure clipping works at 0% and 100%
- Test bidirectionally: Verify reveals work correctly in both LTR and RTL
- Use appropriate curves: easeOut curves feel natural for reveals
- Preserve aspect ratios: When clipping images, maintain proportions
- Add progress indicators: Show users how much content is revealed
Conclusion
ClipRect provides powerful rectangular clipping capabilities for creating progressive reveals, reading progress indicators, comparison sliders, and spoiler content systems. By implementing direction-aware custom clippers and combining them with smooth animations, you can create polished reveal effects that work seamlessly in multilingual applications. The key is using CustomClipper with RTL awareness and always providing proper accessibility labels for the hidden and revealed states.
Remember to test your clipping implementations with both LTR and RTL text directions to ensure the reveal animations flow naturally with the reading direction of each language.