Khi làm việc với MatrixTransform trên WPF để tạo hiệu ứng biến đổi hình học động, việc triển khai hoạt ảnh dựa trực tiếp trên cấu trúc Matrix thường gặp giới hạn — đặc biệt là thiếu khả năng tích hợp các hàm làm mượt (easing functions) chuẩn. Giải pháp dưới đây cung cấp một lớp hoạt ảnh mở rộng, kế thừa từ AnimationTimeline, cho phép điều khiển từng thành phần của ma trận (M11, M12, M21, M22, OffsetX, OffsetY) theo tiến trình thời gian có thể tùy chỉnh độ cong.
1. Lớp hoạt ảnh chính: SmoothMatrixAnimation
Lớp này thay thế cách tiếp cận tuyến tính thuần túy bằng cơ chế nội suy từng phần tử ma trận, đồng thời hỗ trợ đầy đủ IEasingFunction và các kiểu hoạt ảnh phổ biến như From, To, FromTo.
public class SmoothMatrixAnimation : MatrixAnimationBase
{
public static readonly DependencyProperty FromProperty =
DependencyProperty.Register(nameof(From), typeof(Matrix?), typeof(SmoothMatrixAnimation),
new PropertyMetadata(null, OnAnimationParameterChanged));
public static readonly DependencyProperty ToProperty =
DependencyProperty.Register(nameof(To), typeof(Matrix?), typeof(SmoothMatrixAnimation),
new PropertyMetadata(null, OnAnimationParameterChanged));
public static readonly DependencyProperty EasingFunctionProperty =
DependencyProperty.Register(nameof(EasingFunction), typeof(IEasingFunction), typeof(SmoothMatrixAnimation));
public Matrix? From
{
get => (Matrix?)GetValue(FromProperty);
set => SetValue(FromProperty, value);
}
public Matrix? To
{
get => (Matrix?)GetValue(ToProperty);
set => SetValue(ToProperty, value);
}
public IEasingFunction EasingFunction
{
get => (IEasingFunction)GetValue(EasingFunctionProperty);
set => SetValue(EasingFunctionProperty, value);
}
private AnimationMode _mode = AnimationMode.Automatic;
private Matrix[] _keyFrames = Array.Empty<Matrix>();
protected override Freezable CreateInstanceCore() => new SmoothMatrixAnimation();
protected override Matrix GetCurrentValueCore(
Matrix defaultOrigin,
Matrix defaultDestination,
AnimationClock clock)
{
if (_mode == AnimationMode.Automatic)
ValidateParameters(defaultOrigin, defaultDestination);
var progress = clock.CurrentProgress.Value;
if (EasingFunction != null)
progress = EasingFunction.Ease(progress);
var start = ResolveStartMatrix(defaultOrigin);
var end = ResolveEndMatrix(defaultDestination);
// Nội suy từng thành phần độc lập
var m11 = Lerp(start.M11, end.M11, progress);
var m12 = Lerp(start.M12, end.M12, progress);
var m21 = Lerp(start.M21, end.M21, progress);
var m22 = Lerp(start.M22, end.M22, progress);
var offsetX = Lerp(start.OffsetX, end.OffsetX, progress);
var offsetY = Lerp(start.OffsetY, end.OffsetY, progress);
return new Matrix(m11, m12, m21, m22, offsetX, offsetY);
}
private void ValidateParameters(Matrix origin, Matrix dest)
{
if (From.HasValue && To.HasValue)
{
_mode = AnimationMode.FromTo;
_keyFrames = new[] { From.Value, To.Value };
}
else if (From.HasValue)
{
_mode = AnimationMode.From;
_keyFrames = new[] { From.Value };
}
else if (To.HasValue)
{
_mode = AnimationMode.To;
_keyFrames = new[] { To.Value };
}
else
{
_mode = AnimationMode.Automatic;
_keyFrames = Array.Empty<Matrix>();
}
}
private Matrix ResolveStartMatrix(Matrix fallback) =>
_mode switch
{
AnimationMode.From => _keyFrames[0],
AnimationMode.FromTo => _keyFrames[0],
_ => fallback
};
private Matrix ResolveEndMatrix(Matrix fallback) =>
_mode switch
{
AnimationMode.To => _keyFrames[0],
AnimationMode.FromTo => _keyFrames[1],
_ => fallback
};
private static double Lerp(double a, double b, double t) => a + (b - a) * t;
private static void OnAnimationParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((SmoothMatrixAnimation)d)._mode = AnimationMode.Automatic;
}
2. Kiểu hoạt ảnh được hỗ trợ
internal enum AnimationMode : byte
{
Automatic,
From,
To,
FromTo
}
3. Lớp cơ sở trừu tượng: MatrixAnimationBase
public abstract class MatrixAnimationBase : AnimationTimeline
{
protected MatrixAnimationBase() : base() { }
public sealed override Type TargetPropertyType => typeof(Matrix);
public sealed override object GetCurrentValue(
object defaultOrigin,
object defaultDestination,
AnimationClock clock)
{
if (defaultOrigin is not Matrix origin)
throw new ArgumentException("defaultOrigin must be of type Matrix");
if (defaultDestination is not Matrix destination)
throw new ArgumentException("defaultDestination must be of type Matrix");
return GetCurrentValueCore(origin, destination, clock);
}
protected abstract Matrix GetCurrentValueCore(
Matrix defaultOrigin,
Matrix defaultDestination,
AnimationClock clock);
}
4. Hàm tiện ích nội suy ma trận
Không cần mở rộng lớp Matrix — chỉ sử dụng toán tử nội suy đơn giản trên từng thuộc tính:
public static class MatrixInterpolator
{
public static Matrix Interpolate(Matrix a, Matrix b, double t) =>
new Matrix(
Lerp(a.M11, b.M11, t),
Lerp(a.M12, b.M12, t),
Lerp(a.M21, b.M21, t),
Lerp(a.M22, b.M22, t),
Lerp(a.OffsetX, b.OffsetX, t),
Lerp(a.OffsetY, b.OffsetY, t)
);
private static double Lerp(double start, double end, double t) => start + (end - start) * t;
}
5. Cách sử dụng trong XAML hoặc code-behind
var animation = new SmoothMatrixAnimation
{
Duration = TimeSpan.FromMilliseconds(300),
FillBehavior = FillBehavior.Stop,
EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 2 }
};
animation.From = currentTransform.Matrix;
animation.To = targetTransform.Matrix;
matrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, animation);
Cơ chế này đảm bảo hoạt ảnh ma trận giữ nguyên tính tương thích với hệ thống timing của WPF, đồng thời cho phép áp dụng bất kỳ hàm làm mượt nào từ không gian tên System.Windows.Media.Animation, bao gồm cả các hàm tùy chỉnh do người dùng định nghĩa.