Search code examples
c#xamluwpwin-universal-app

UWP Scroll Text from end to start


I am implementing a scrolling text that when pointer enters it, it starts scrolling its content.

I am able to get it scrolling using the code below:

private DispatcherTimer ScrollingTextTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(16) };
ScrollingTextTimer.Tick += (sender, e) =>
{
    MainTitleScrollViewer.ChangeView(MainTitleScrollViewer.HorizontalOffset + 3, null, null);
    if (MainTitleScrollViewer.HorizontalOffset == MainTitleScrollViewer.ScrollableWidth)
    {
        MainTitleScrollViewer.ChangeView(0, null, null);
        ScrollingTextTimer.Stop();
    }
};

XAML:

<ScrollViewer
    x:Name="MainTitleScrollViewer"
    Grid.Row="0"
    Grid.Column="1"
    Margin="10,5"
    HorizontalScrollBarVisibility="Hidden"
    VerticalScrollBarVisibility="Disabled">
    <TextBlock
        x:Name="MainTitleTextBlock"
        VerticalAlignment="Bottom"
        FontSize="24"
        Foreground="White" />
</ScrollViewer>

However, there is an additional feature that I want to implement. When the text scrolls to its end, I don't want it to scroll back to the start. I want it to keep scrolling to the start. You can see what I mean from the screenshots I posted below. The screenshots are from Groove Music. You may need to check it out if I didn't explain my question well.

Normal

Scroll To the End

A possible solution might be doubling the text and putting some space between them. But I don't know when to stop scrolling if so.


Solution

  • This is my way of doing it and source code is here(xaml) and here(csharp):

    I created a UserControl called ScrollingTextBlock.

    This is XAML content of the UserControl.

    <Grid>
        <ScrollViewer x:Name="TextScrollViewer">
            <TextBlock x:Name="NormalTextBlock" />
        </ScrollViewer>
        <ScrollViewer x:Name="RealScrollViewer">
            <TextBlock x:Name="ScrollTextBlock" Visibility="Collapsed" />
        </ScrollViewer>
    </Grid>
    

    Basically, you need two ScrollViewers that overlaps.

    The first ScrollViewer is for detecting if the text is scrollable. And the TextBlock in it is for putting the text.

    The second ScrollViewer is the real ScrollViewer. You will be scrolling this one not the first one. And the TextBlock in it will have its Text equal to

    ScrollTextBlock.Text = NormalTextBlock.Text + new string(' ', 10) + NormalTextBlock.Text
    

    The new string(' ', 10) is just some blank space to make your text not look concatenated tightly, which you can see from the image in the question. You can change it into whatever you want.

    Then in the csharp code you need (explanations are in the comments):

        // Using 16ms because 60Hz is already good for human eyes.
        private readonly DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(16) };
    
        public ScrollingTextBlock()
        {
            this.InitializeComponent();
            timer.Tick += (sender, e) =>
            {
                // Calculate the total offset to scroll. It is fixed after your text is set.
                // Since we need to scroll to the "start" of the text,
                // the offset is equal the length of your text plus the length of the space,
                // which is the difference of the ActualWidth of the two TextBlocks.
                double offset = ScrollTextBlock.ActualWidth - NormalTextBlock.ActualWidth;
                // Scroll it horizontally.
                // Notice the Math.Min here. You cannot scroll more than offset.
                // " + 2" is just the distance it advances,
                // meaning that it also controls the speed of the animation.
                RealScrollViewer.ChangeView(Math.Min(RealScrollViewer.HorizontalOffset + 2, offset), null, null);
                // If scroll to the offset
                if (RealScrollViewer.HorizontalOffset == offset)
                {
                    // Re-display the NormalTextBlock first so that the text won't blink because they overlap.
                    NormalTextBlock.Visibility = Visibility.Visible;
                    // Hide the ScrollTextBlock.
                    // Hiding it will also set the HorizontalOffset of RealScrollViewer to 0,
                    // so that RealScrollViewer will be scrolling from the beginning of ScrollTextBlock next time.
                    ScrollTextBlock.Visibility = Visibility.Collapsed;
                    // Stop the animation/ticking.
                    timer.Stop();
                }
            };
        }
    
        public void StartScrolling()
        {
            // Checking timer.IsEnabled is to avoid restarting the animation when the text is already scrolling.
            // IsEnabled is true if timer has started, false if timer is stopped.
            // Checking TextScrollViewer.ScrollableWidth is for making sure the text is scrollable.
            if (timer.IsEnabled || TextScrollViewer.ScrollableWidth == 0) return;
            // Display this first so that user won't feel NormalTextBlock will be hidden.
            ScrollTextBlock.Visibility = Visibility.Visible;
            // Hide the NormalTextBlock so that it won't overlap with ScrollTextBlock when scrolling.
            NormalTextBlock.Visibility = Visibility.Collapsed;
            // Start the animation/ticking.
            timer.Start();
        }