Search code examples
c#xamlanimationwinui-3

C# From random letters displaying to a text reveal effect for WPF/WinUI XAML TextBlock control


I'm trying to create a custom control in WinUI 3 based on a TextBlock control, that has to show the contained text using a random letter rolling effect (letter by letter) till the full text is revealed. Let's say something like the Airport Departures billboard...

To better understand the desired effect, this is a video where the desired result is made using Adobe After Effects.

How can I implement the (closest possible) effect in C# code?


Solution

  • In my solution, the animation is triggered by setting the Text DependencyProperty. That allows to easily restart the effect if the Text property is overridden during the current animation. But you could also trigger it by some public method. But keep in mind that the TextBlock control can get slow with high amounts of text and fast text changes. If you encounter performance issues you should rewrite this for a Win2D CanvasControl.

    LetterRevealTextBlock.xaml:

    <UserControl
        x:Class="Test.WinUI3.LetterRevealTextBlock"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:Test.WinUI3"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
    
        <TextBlock x:Name="LRTB" FontFamily="Consolas"/>
        
    </UserControl>
    

    LetterRevealTextBlock.xaml.cs:

    namespace Test.WinUI3;
    
    public sealed partial class LetterRevealTextBlock : UserControl
    {
        private static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text", typeof(string), typeof(LetterRevealTextBlock), new PropertyMetadata("", (d, e) => ((LetterRevealTextBlock)d).TextChanged(d, e)));
        public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
    
        private static readonly DependencyProperty ScrollIntervalProperty = DependencyProperty.Register(
        "ScrollInterval", typeof(int), typeof(LetterRevealTextBlock), new PropertyMetadata(40, (d, e) => ((LetterRevealTextBlock)d).ScrollIntervalChanged(d, e)));
        public int ScrollInterval { get => (int)GetValue(ScrollIntervalProperty); set => SetValue(ScrollIntervalProperty, value); }
    
        private static readonly DependencyProperty RevealIntervalProperty = DependencyProperty.Register(
        "RevealInterval", typeof(int), typeof(LetterRevealTextBlock), new PropertyMetadata(200, (d, e) => ((LetterRevealTextBlock)d).RevealIntervalChanged(d, e)));
        public int RevealInterval { get => (int)GetValue(RevealIntervalProperty); set => SetValue(RevealIntervalProperty, value); }
    
        const string Chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
        private Random Random = new Random();
    
        private char[] TextArray {get; set; }
    
        private int CurrentChar = 0;
     
        private DispatcherTimer RevealTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(200) };
        private DispatcherTimer ScrollTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(40) };
    
        private void ScrollIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ScrollTimer.Interval = TimeSpan.FromMilliseconds((int)e.NewValue);
        }
        private void RevealIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            RevealTimer.Interval = TimeSpan.FromMilliseconds((int)e.NewValue);
        }
    
        private void TextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TextArray = (e.NewValue as string).ToCharArray();
            CurrentChar = 0;
            ScrollTimer.Start();
            RevealTimer.Start();
        }
    
        private void ScrollTimer_Tick(object sender, object e)
        {
            for (int i = 0; i < Text.Length; i++)
            {
                if (i <= CurrentChar)
                {
                    TextArray[i] = Text[i];
                }
                else
                {
                    TextArray[i] = Text[i] == ' ' ? ' ' : Chars[Random.Next(Chars.Length - 1)];
                }
            }
            LRTB.Text = new string(TextArray);
            if (CurrentChar >= Text.Length - 1)
            {
                CurrentChar = 0;
                ScrollTimer.Stop();
                RevealTimer.Stop();
                return;
            }
        }
    
        private void RevealTimer_Tick(object sender, object e)
        {
            CurrentChar++;
        }
    
        public LetterRevealTextBlock()
        {
            this.InitializeComponent();
            ScrollTimer.Tick += ScrollTimer_Tick;
            RevealTimer.Tick += RevealTimer_Tick;
        }
    }