原地址:https://www.cnblogs.com/yk250/p/10043694.html 无图无真相:
1,重写panel类(模拟实现一个竖直方向排列的panel,相当于默认的StackPanel实现的效果):
其中 availableSize 是xaml中传入的大小。调用child的Measure方法测量后得到DesiredSize用于计算实际返回的大小。
public class VStackPanel: Panel { protected override Size MeasureOverride(Size availableSize) { Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity); Size measureSize = new Size() { Width = availableSize .Width}; foreach (UIElement item in InternalChildren) { item.Measure(size); measureSize.Height += item.DesiredSize.Height; } return measureSize; } protected override Size ArrangeOverride(Size finalSize) { double height = 0; foreach (UIElement item in InternalChildren) { Point Point = new Point(0, height); item.Arrange(new Rect(Point, item.DesiredSize)); height += item.RenderSize.Height; } return finalSize; } }
Xaml中我们使用一个简单的ItemsControl做测试(重写ItemsPanelTemplate,使用自定义的 VStackPanel):
<ItemsControl.ItemsPanel> <ItemsPanelTemplate> <local:VStackPanel></local:VStackPanel> </ItemsPanelTemplate> </ItemsControl.ItemsPanel>
添加数据项后显示和普通的StackPanel并无区别。
2,给 Add,Insert,Move统一制作布局动画。(Remove操作需要单独处理。)
首先,写一个AnimateBasePanel 的抽象类(只能被继承不能被实例化)基于Panel,用户布局更改的操作:
public abstract class AnimateBasePanel : Panel { }
然后,定义一个附加属性对象 AnimatePanelItemData,用于记录child布局前后位置的变化:
public class AnimatePanelItemData
{
public Point Target;
public Point Current;
}
public static AnimatePanelItemData GetData(DependencyObject obj) { return (AnimatePanelItemData)obj.GetValue(DataProperty); } public static void SetData(DependencyObject obj, AnimatePanelItemData value) { obj.SetValue(DataProperty, value); } // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... public static readonly DependencyProperty DataProperty = DependencyProperty.RegisterAttached("Data", typeof(AnimatePanelItemData), typeof(AnimateBasePanel), new PropertyMetadata(null));
当新增child时,定义一个可被重载的虚方法:(新增时,将AnimatePanelItemData附加到child上并且设置child的RenderTransform作为动画起始位置,最后使用 child.Arrange(bounds)定位child在panel中的位置和大小。)
protected virtual void ArrangeNewChid(UIElement child, Rect bounds)
{
var data = new AnimatePanelItemData();
child.SetValue(DataProperty, data);
child.RenderTransformOrigin = new Point(0.5, 0.5);
TransformGroup group = new TransformGroup();
group.Children.Add(new TranslateTransform() { X = -child.DesiredSize.Width, Y = child.DesiredSize.Height });
child.RenderTransform = group;
data.Current = new Point();
data.Target = bounds.Location;
child.Arrange(bounds);
CreateTransition(child, new Point(), TimeSpan.FromMilliseconds(200), null).Begin();
}
对已经存在的child当布局发生改变时,即Insert,Move时定义一个同样的虚方法。(获取附加属性值,然后计算布局前后的TranslateTransform的X,Y的偏移量,最后同样使用child.Arrange(bounds)定位child在panel中的位置和大小。)
protected virtual void ArrangeExistsChid(UIElement child, Rect bounds) { AnimatePanelItemData data = (AnimatePanelItemData)child.GetValue(DataProperty); data.Current = new Point(data.Target.X, data.Target.Y); data.Target = bounds.Location; child.RenderTransformOrigin = new Point(0.5, 0.5); TransformGroup group = new TransformGroup(); child.RenderTransform = group; group.Children.Add(new TranslateTransform() { X = -(data.Target.X - data.Current.X), Y = -(data.Target.Y - data.Current.Y) }); child.Arrange(bounds); CreateTransition(child, new Point(), TimeSpan.FromMilliseconds(200), null).Begin(); }
其中 CreateTransition 为最后布局更新时执行的动画:
internal static Storyboard CreateTransition(UIElement element, Point newLocation, TimeSpan _duration, EasingFunctionBase easing) { var duration = new Duration(_duration); var sb = new Storyboard { Duration = duration }; var translateAnimationX = new DoubleAnimation { To = newLocation.X, Duration = duration }; var translateAnimationY = new DoubleAnimation { To = newLocation.Y, Duration = duration }; if (easing != null) { translateAnimationX.EasingFunction = easing; translateAnimationY.EasingFunction = easing; } Storyboard.SetTarget(translateAnimationX, element); Storyboard.SetTarget(translateAnimationY, element); Storyboard.SetTargetProperty(translateAnimationX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)")); Storyboard.SetTargetProperty(translateAnimationY, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)")); sb.Children.Add(translateAnimationX); sb.Children.Add(translateAnimationY); return sb; }
最后统一处理(用于子类调用):
protected virtual void ArrangeChild(UIElement child, Rect bounds) { AnimatePanelItemData data = (AnimatePanelItemData)child.GetValue(DataProperty); if (data == null) { ArrangeNewChid(child, bounds); } else { ArrangeExistsChid(child, bounds); } }
3,我们使用AnimateBasePanel代替VStackPanel的基类(使用AnimateBasePanel的方法代替ArrangeChild代替item.Arrange方法。):
public class VStackPanel: AnimateBasePanel { protected override Size MeasureOverride(Size availableSize) { Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity); Size measureSize = new Size() { Width = availableSize .Width}; foreach (UIElement item in InternalChildren) { item.Measure(size); measureSize.Height += item.DesiredSize.Height; } return measureSize; } protected override Size ArrangeOverride(Size finalSize) { double height = 0; foreach (UIElement item in InternalChildren) { Point Point = new Point(0, height); ArrangeChild(item, new Rect(Point,item.DesiredSize)); height += item.RenderSize.Height; } return finalSize; } }
也可以重载AnimateBasePanel的 ArrangeNewChid或ArrangeExistsChid方法:
protected override void ArrangeNewChid(UIElement child, Rect bounds)
{
var data = new AnimatePanelItemData();
child.SetValue(DataProperty, data);
child.RenderTransformOrigin = new Point(0.5, 0.5);
TransformGroup group = new TransformGroup();
child.RenderTransform = group;
group.Children.Add(new TranslateTransform() { X = -child.DesiredSize.Width, Y = 0 });
data.Current = new Point();
data.Target = bounds.Location;
child.Arrange(bounds);
CreateTransition(child, new Point(), TimeSpan.FromMilliseconds(200), null).Begin();
}
到此为止,除了remove的布局没有完全实现外,其他布局已经能正常表现了。
4,使用blend库进行删除动画的制作:
首先使用mvvm模式定义好ModelBase(CallerMemberName属性需要使用C#新特性。)
public class ModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
然后定义每一项item的数据模型:(只使用一个用于显示的Name属性和用于删除的IsDelete属性和一个Delete方法。)
public class TestModel : ModelBase { public virtual void Delete() { IsDelete = true; } public string Name { get; set; } private bool _isDelete; /// <summary> /// _isDelete /// </summary> public bool IsDelete { get { return _isDelete; } set { _isDelete = value; RaisePropertyChanged(); } } }
接着,定义一个简单的ViewModel:
public class MainViewModel : ModelBase { public MainViewModel() { TestModels = new ObservableCollection<TestModel>() { }; } private ObservableCollection<TestModel> _TestModels; /// <summary> /// _TestModels /// </summary> public ObservableCollection<TestModel> TestModels { get { return _TestModels; } set { _TestModels = value; RaisePropertyChanged(); } } }
前台Xaml 随意写一下:(实现当remove删除某一项的时候,先调用Delete把动画执行完成以后再使用RemoveItemInListBoxAction删除该项。
)
<ItemsControl ItemsSource="{Binding TestModels}" BorderBrush="Red" BorderThickness="1"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <local:VStackPanel></local:VStackPanel> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Border BorderBrush="Red" BorderThickness="1" Height="30" x:Name="br" RenderTransformOrigin=".5,.5" Loaded="br_Loaded"> <Border.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Border.RenderTransform> <i:Interaction.Triggers> <ei:DataTrigger Binding="{Binding IsDelete}" Value="true"> <ei:ControlStoryboardAction x:Name="StoryboardAction"> <ei:ControlStoryboardAction.Storyboard> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)" Storyboard.TargetName="br"> <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="{Binding ElementName=br,Path=ActualWidth}"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="br"> <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </ei:ControlStoryboardAction.Storyboard> </ei:ControlStoryboardAction> </ei:DataTrigger> <ei:StoryboardCompletedTrigger Storyboard="{Binding ElementName=StoryboardAction,Path=Storyboard}"> <pi:RemoveItemInListBoxAction></pi:RemoveItemInListBoxAction> </ei:StoryboardCompletedTrigger> </i:Interaction.Triggers> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="{Binding Name}"></TextBlock> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
其中 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:pi="http://schemas.microsoft.com/prototyping/2010/interactivity" 引用一下。
代码 :https://files.cnblogs.com/files/yk250/CustomPanelTest.rar