Thursday, May 3, 2018

Used async/await to make an animation in WPF. Am I evil?

I'm making a simple media player control. I want the play/pause buttons to only be visible when the user's mouse is over the video. I also wanted there to be a little animation that plays as they appear and disappear. I came up with a solution that I think is pretty clever, but I'm worried it might be bad practice.

Basically, I have an async function that runs an infinite while-loop. Every iteration, I update the control's height and then await Task.Delay() to pause until the next frame.
My justification:

  • It avoids cluttering up unrelated code. All of the animation-related variables can be kept inside the async function, instead of cluttering up the private fields.

  • The logic for the animation reads linearly. Instead of having it spread out throughout the file, I can keep it all in one function.

  • It's simple to comprehend, which is good for maintainability

  • It runs on the UI thread but doesn't block it because it's async. This allows the rest of the application to remain snappy and responsive.

The code itself looks something like this:

 public partial class VideoPlayer : UserControl { private const double VIDEO_CONTROL_ANIMATION_DURATION = 0.1; private SemaphoreSlim waiter = new SemaphoreSlim(0); private bool hideVideoControls = true; public VideoPlayer() { InitializeComponent(); UpdateControls(); // Start animating AnimateAsync(); } private void UpdateControls() { // Only enable the video controls if the loaded file is a video. // We don't want a "play" button to show up if the user loads a picture ;-) videoControls.IsEnabled = IsVideo; // Start animating if hideVideoControls changed bool oldHideVideoControls = hideVideoControls; hideVideoControls = !(IsVideo && IsMouseOver); if (oldHideVideoControls != hideVideoControls) waiter.Release(); // Other logic for enabling/disabling controls can be found here, too. } private async void AnimateAsync() { Stopwatch stopwatch = new Stopwatch(); // Used to find the delta time when animating double videoControlsHeight = videoControls.Height; // The height of the controls when fully expanded while (true) { // Get the delta time double deltaTime = stopwatch.Elapsed.TotalSeconds; stopwatch.Reset(); stopwatch.Start(); // Calculate the speed to change the height by double speed = (videoControlsHeight / VIDEO_CONTROL_ANIMATION_DURATION); // Hide or shrink it double height = videoControls.Height; if (hideVideoControls) { height -= speed * deltaTime; if (height < 0) height = 0; } else { height += speed * deltaTime; if (height > videoControlsHeight) height = videoControlsHeight; } // Apply the change in height videoControls.Height = height; // Stop updating if the animation is finished. double targetHeight = hideVideoControls ? 0 : videoControlsHeight; if (height == targetHeight) { stopwatch.Reset(); await waiter.WaitAsync(); } // Wait for the next frame // Tries to run at 120 FPS await Task.Delay(TimeSpan.FromSeconds(1.0 / 120)); } } 

I removed the irrelevant parts of the class. Whenever I want to hide or show the controls, I simply change hideVideoControls and then release the semaphore. You can see this happening in UpdateControls(). UpdateControls() gets called whenever something happens that might change the state of the UI happens, such as opening a new video, changing the volume, or moving your mouse over the video.

Now, I know the standard way to do something like this is to use Storyboards. I tried that, and it made the animation look WAY smoother on my 144hz monitor. Unfortunately, it made the code really messy. I also tried using a DispatcherTimer to power the animaiton, but that had SERIOUS performance problems for some reason.

I really like the pattern I chose, but I can't shake the feeling that this is a "no-no". Is there some hidden performance reason not to do this? Thanks for the feedback!

Used async/await to make an animation in WPF. Am I evil? Click here
  • Blogger Comment
  • Facebook Comment

0 comments:

Post a Comment

The webdev Team