自定义panel实现,并实现item更改和移除动画。

原地址:https://www.cnblogs.com/yk250/p/10043694.html  无图无真相:

自定义panel实现,并实现item更改和移除动画。

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