← Back to Blog

Flutter ClipRect Localization: Rectangular Clipping for Multilingual Apps

fluttercliprectclippingreveallocalizationrtl

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

  1. Use CustomClipper for RTL: Create custom clippers that respect text direction
  2. Provide semantic labels: Describe the hidden/revealed state for accessibility
  3. Animate smoothly: Use curved animations for natural reveal effects
  4. Consider reading direction: Always use Directionality.of(context) for RTL support
  5. Combine with opacity: Fade content in as it's revealed for polish
  6. Handle edge cases: Ensure clipping works at 0% and 100%
  7. Test bidirectionally: Verify reveals work correctly in both LTR and RTL
  8. Use appropriate curves: easeOut curves feel natural for reveals
  9. Preserve aspect ratios: When clipping images, maintain proportions
  10. 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.