У меня есть элемент управления, который отображает кривую с контрольными точками, которыми может управлять пользователь. Если пользователь перетаскивает контрольную точку за пределы элемента управления, она становится недоступной, поэтому я создаю небольшую анимацию, которая масштабирует кривую вниз до тех пор, пока все контрольные точки снова не станут видимыми. Я использую RectAnimation
на DependencyProperty
в суррогатном типе объекта, чтобы захватить значения, создаваемые анимацией. В основном это работает, за исключением того, что случайным образом, возможно, в одном случае из десяти, анимация внезапно останавливается на полпути, без исключения, без события Completed
, и кривая не была полностью переведена в целевые границы.
Немного упрощенный код:
Rect elementBounds = GetBoundsForControlPoints();
if ((elementBounds.Left < 0) || (elementBounds.Right > ActualWidth)
|| (elementBounds.Top < 0) || (elementBounds.Bottom > ActualHeight))
{
var fitBounds = FitRectangleToVisibleArea(elementBounds); // same aspect but entirely visible
var anim = new RectAnimation();
anim.From = elementBounds;
anim.To = fitBounds;
anim.Duration = TimeSpan.FromSeconds(0.4);
var animTarget = new AnimationTarget();
animTarget.StartRect = elementBounds;
animTarget.EndRect = fitBounds;
animTarget.RectChanged +=
(_, _) =>
{
TranslateControlPoints(animTarget.StartRect, animTarget.Rect);
};
animTarget.BeginAnimation(AnimationTarget.RectProperty, anim);
}
Это прокси-класс:
class AnimationTarget : UIElement
{
public static DependencyProperty RectProperty = DependencyProperty.Register(nameof(Rect), typeof(Rect), typeof(AnimationTarget), new UIPropertyMetadata(RectChangedCallback));
public Rect StartRect;
public Rect EndRect;
public Rect Rect
{
get => (Rect)GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
public event EventHandler? RectChanged;
static void RectChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((AnimationTarget)d).RectChanged?.Invoke(d, EventArgs.Empty);
}
}
Большую часть времени анимация выполняется идеально, но время от времени она просто... останавливается. Кривая находится на полпути своего перемещения и просто остается там. Я могу запустить новую анимацию, и она начнется с того места, где остановилась предыдущая.
Что может быть причиной того, что анимация иногда просто останавливается на полпути?
Хм, я не вижу «остановленного» события в объектах анимации...
Я не сказал «событие». Альтернативой доведению анимации до конца является ее «остановка»; который требует собственной «последовательности обработки», чтобы «синхронизировать» визуальный элемент с реальными данными. Пользователи могут «ускорять» или замедлять мою анимацию; что требует мышления за пределами «коробки раскадровки». Я также «связываю» их.
Ах хорошо. Таким образом, у вас никогда не было анимации, которая внезапно останавливалась на полпути без указания вашего кода.
Если вы имеете в виду «сбой», я могу сломать свои истории, если не «остановлю» их перед изменением. Я не могу позволить себе остановку анимации без причины, потому что это «является» основной частью приложения (поэтому шаблон тестируется без конца со всеми «проверками» на месте: ограничения; есть/не анимируется; есть вращение масштабируется и т. д.) Серьезное дело, эти анимации :0)
Нет, не рушится. Это просто... останавливается. Я могу внести изменение, которое вызовет новую анимацию, и она обычно выполняется до завершения. Иногда тоже останавливается.
Когда дети лягут спать, я посмотрю, смогу ли я снять это на видео.
Это видео демонстрирует поведение: youtube.com/watch?v=NT1Y1WGoygU Подробности смотрите в описании видео.
Анимации работают независимо от кода. Вы можете нажать точку останова в своем коде, которая остановит «код», в то время как анимация продолжит работать на «визуальном слое». Чтобы «видеть», что делает и собирается делать анимация, у меня есть «монитор», который работает параллельно; таймер, который запускается каждую секунду и вычисляет текущую позицию каждой анимации с помощью преобразований рендеринга и сравнивает ее с ожидаемыми результатами. Цель состоит в том, чтобы обнаружить «столкновения» и другие вещи.
Итак, я избегаю проблем в первую очередь за счет «прозрачности». Это была моя первоначальная мысль: нужно больше прозрачности при работе с анимацией, которая работает более или менее независимо от кода, который ее запускает.
А при использовании для анимации свойств отличных от «преобразований» (вращение, масштабирование, перемещение, наклон) «могут» возникнуть проблемы. Например. "Предпочтительно" (?) использовать преобразование масштабирования вместо анимации "ширины и/или высоты"... и синхронизировать с фактическими данными (только), если и когда это необходимо. (Независимые и зависимые анимации; ваша выглядит как «зависимая»)
«Зависимые» анимации вызывают изменения макета; независимые - нет.
И последнее замечание: я прибегнул к «Раскадровкам» только после того, как исчерпал свои собственные методы «анимации». Если бы мне нужно было, я бы перемещал вещи «в коде», используя таймер. За несколько мс можно сделать многое.
Моя анимация не ориентирована на перемещение объекта в визуальном дереве. Как вы можете видеть на видео, я анимирую свойство суррогатного объекта, и когда значение изменяется, я устанавливаю новые местоположения для точек кривой, которые затем отдельным образом передаются в пользовательский интерфейс. Анимируемое свойство имеет тип Rect
и является единственным свойством объекта, поэтому масштаб и перевод не имеют отношения к моей ситуации. Поскольку этот суррогатный/прокси-объект представляет собой просто отдельное свойство Rect
, его нет в визуальном дереве, и поэтому любые манипуляции с макетами не имеют отношения к анимации.
Думаю, мне просто придется написать свой собственный код анимации. Но это определенно похоже на слепой обходной путь. Не помогает понять, почему анимация иногда внезапно останавливается.
Что такое «Рект»? Это слева, сверху, ширина и высота. Это «зависимые» свойства «перевода» и «масштабирования». Я «анимирую» свои пользовательские элементы управления, используя эти свойства, но посредством «преобразований». Как я уже сказал, зависимые анимации вызывают обновления макета. «Визуальный слой» предназначен для работы со скоростью 60 кадров в секунду. Если ваша анимация «не может», она, вероятно, попадет в поток пользовательского интерфейса; который сейчас завален обновлениями. (Просто предположение).
Это могло бы объяснить плохую производительность (чего на практике я не наблюдаю), но я не понимаю, как это объясняет отказ WPF от анимации на полпути. :-П
Ваши манипуляции с «контрольными точками» «случайны». Если в результате что-то станет «отрицательным» (например, ширина), это объяснит это. Это одно из объяснений. Я уверен, что есть и другие.
В ходе тестирования я перетаскиваю контрольные точки во всех направлениях. Негативность вообще не должна иметь никакого влияния, а также, как анимация узнает об этом? Мое назначение обновленных значений находится в try
/catch
. Даже если бы он выдавал исключения, он должен продолжать запускать тики анимации.
Исходя из вашего подхода, вы не можете подтвердить значения раскадровки в момент остановки анимации, поэтому вы просто размышляете. И, как я уже говорил, анимация не «запускает ваш код» (кроме завершения), поэтому «ловить» нечего. Что касается «негативного осознания», если вы прикажете анимации изменять ширину, скажем, на 100, на -110 (используя шаблон «По» «с течением времени»), что, по вашему мнению, может произойти?
Это невозможно. Анимация передается от одного Rect к другому Rect. Пока начальный и конечный Rect правильно сформированы, каждый промежуточный Rect также будет правильно сформирован. В любом случае, я готов оставить этот вопрос позади. Насколько я понимаю, у анимации WPF есть проблемы с надежностью. Я не знаю, что это такое, но код анимации, который я опубликовал в своем ответе на этот вопрос, не имеет подобной проблемы.
Ну, у меня нет ответа в смысле объяснения основного поведения. Но у меня есть ответ в смысле устранения проблемы: анимацию я реализовал сам.
Я создал свой собственный класс RectAnimation
, в котором есть члены From
, To
, Duration
, Completed
и который объединяется с классом AnimationTarget
из моей предыдущей реализации, предоставляя события Rect
DependencyProperty
и RectChanged
.
Когда вызывается BeginAnimation
, он запускает Thread
(с IsBackground
, установленным на true
), который фиксирует время начала, время окончания (время начала + продолжительность), начальный и конечный прямоугольник (чтобы гарантировать, что не может быть внешних изменений), а затем входит в цикл . Каждый раз при прохождении цикла он получает текущее значение DateTime
и вычисляет значение progress
(двойное, от 0,0 до 1,0), выходя из цикла, если progress
выходит за пределы диапазона (например, если достигнуто время окончания), а затем передает это значение. progress
к методу AnimationTick
. Короткая задержка ограничивает теоретическую максимальную частоту кадров до 100 кадров в секунду.
AnimationTick
вычисляет промежуточное Rect
значение для предоставленного progress
, а затем начинает процесс присвоения его свойству зависимости. Это процесс, поскольку, будучи свойством зависимости, назначение может быть выполнено только в правильном потоке. Dispatcher.BeginInvoke
используется для постановки изменений в очередь, и код гарантирует, что в любой момент времени существует только одно невыполненное назначение, на случай, если очередь пользовательского интерфейса будет резервной копией. По этой причине, даже если он вычисляет 100 обновлений в секунду, если он обновляет только пользовательский интерфейс, например. 30 раз в секунду, то свойство Rect
будет обновляться 30 раз в секунду.
Использование класса очень похоже на использование системы RectAnimation
, с основным отличием в том, что он анимирует свойство непосредственно в моем классе RectAnimation
, а не выполняется для указанного свойства какого-либо другого объекта.
class RectAnimation : UIElement
{
public static DependencyProperty RectProperty = DependencyProperty.Register(nameof(Rect), typeof(Rect), typeof(RectAnimation), new UIPropertyMetadata(RectChangedCallback));
public Rect StartRect;
public Rect EndRect;
public TimeSpan Duration;
public event EventHandler? Completed;
public Rect Rect
{
get => (Rect)GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
public void BeginAnimation()
{
var thread = new Thread(AnimationThreadProc);
thread.IsBackground = true;
thread.Start();
}
void AnimationThreadProc()
{
try
{
DateTime startTime = DateTime.UtcNow;
DateTime endTime = startTime + Duration;
var startRect = StartRect;
var endRect = EndRect;
while (true)
{
DateTime now = DateTime.UtcNow;
double progress = (now - startTime) / Duration;
if ((progress < 0.0) || (progress > 1.0))
break;
AnimationTick(startRect, endRect, progress);
Thread.Sleep(10);
}
Rect = endRect;
Completed?.Invoke(this, EventArgs.Empty);
}
catch { }
}
bool _tickOutstanding = false;
RectReference? _tickRect;
// Allow for atomic updates.
record RectReference(Rect Value);
void AnimationTick(Rect startRect, Rect endRect, double progress)
{
_tickRect = new RectReference(new Rect(
startRect.X + (endRect.X - startRect.X) * progress,
startRect.Y + (endRect.Y - startRect.Y) * progress,
startRect.Width + (endRect.Width - startRect.Width) * progress,
startRect.Height + (endRect.Height - startRect.Height) * progress));
if (!_tickOutstanding)
{
_tickOutstanding = true;
Dispatcher.BeginInvoke(
DispatcherPriority.Send,
() =>
{
if (_tickRect != null)
Rect = _tickRect.Value;
_tickOutstanding = false;
});
}
}
public Point TransformPoint(Point pt)
{
var currentRect = Rect;
var relativePosition = pt - StartRect.TopLeft;
relativePosition.X *= currentRect.Width / StartRect.Width;
relativePosition.Y *= currentRect.Height / StartRect.Height;
return (Point)(relativePosition + currentRect.TopLeft);
}
public event EventHandler? RectChanged;
static void RectChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((RectAnimation)d).RectChanged?.Invoke(d, EventArgs.Empty);
}
}
Хотя мои собственные методы анимации были более мощными, в конечном итоге раскадровки были более плавными. Итак, вы придумали гибрид.
Проблем с плавностью я не наблюдал, но в данном случае надежность в любом случае на первом месте, поэтому даже если бы она была чуть менее плавной, я бы все равно выбрал тот, которому можно доверять. :-)
У меня есть оболочка вокруг моих раскадровок с «ожидаемыми результатами», которые проверяются на «завершено» или «остановлено», что дает мне то, с чем мне нужно работать при отладке. Хотя ваша продолжительность кажется постоянной, невозможно узнать, верны ли ваши граничные предположения.