這個工程和上一篇 (2)中介紹的排序大同小異,只是比上一篇交換復雜一點,不是通過單擊進行交換,
而是拖動一個 Tile 到另一個 Tile 上時,判斷兩個 Tile 的中心距離是否符合條件來判斷是否進行交換兩個 Tile。
歸根結底還是利用 FluidMoveBehavior 行為來使 Silverlight 的元素在重新定位時,產生動畫效果。畢竟在
實際開發中,用戶體驗還是很重要的,生動的交互比生硬的交互會更讓用戶感到親切。
當然項目中也用到了視覺狀態管理相關的技術,因為不是重點,這里不會過多的介紹。
效果交互圖:
第一步:首先定義一個 UserControl 類,作為一個 Tile 控件,並且在 CodeBehind 頁面中注冊一個
依賴屬性 TileBackgroundProperty,用來設置 Tile 不同的背景,效果:
給該 Tile 控件添加 3 種不同的視覺狀態:NormalVisualState、CheckedVisualState、FloatVisualState,即
當長按選中時,為 CheckedVisualState 狀態,並顯示右上角的圖片 “圖釘”;其它 tile 狀態為 FloatVisualState,
為了讓不同的 tile 漂浮的方向和方式不同,提供 4 個 FloatVisualState 。
相應的 xaml :

<Grid x:Name="LayoutRoot" Margin="10" Width="150" Height="150"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="VisualStateGroup"> <VisualState x:Name="NormalVisualState"/> <VisualState x:Name="CheckedVisualState"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="Assets_thumb_png"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames></Storyboard> </VisualState> <VisualState x:Name="FloatVisualState1"> <Storyboard RepeatBehavior="Forever" AutoReverse="True"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="-5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.6"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="FloatVisualState2"> <Storyboard RepeatBehavior="Forever" AutoReverse="True"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.6"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="FloatVisualState3"> <Storyboard RepeatBehavior="Forever" AutoReverse="True"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="-5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="-5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.6"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="FloatVisualState4"> <Storyboard RepeatBehavior="Forever" AutoReverse="True"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.9"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0.9"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="-5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="5"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="-5"/> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="5"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="grid"> <EasingDoubleKeyFrame KeyTime="0" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0.6"/> <EasingDoubleKeyFrame KeyTime="0:0:4.5" Value="0.6"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid x:Name="grid" Background="{StaticResource PhoneAccentBrush}" RenderTransformOrigin="0.5,0.5"> <Grid.RenderTransform> <CompositeTransform/> </Grid.RenderTransform> <Image Source="{Binding TileBackground}" Stretch="Fill"/> <Image x:Name="Assets_thumb_png" Visibility="Collapsed" Margin="124,-16,-15,124" Source="Assets/thumb.png" Stretch="Fill"/> </Grid> </Grid>
該自定義控件相應的 C#:

public partial class TileControl : UserControl { public TileControl() { InitializeComponent(); this.DataContext = this; } #region TileBackground (依賴屬性) /// <summary> /// Tile 的背景圖片 /// </summary> public string TileBackground { get { return (string)GetValue(TileBackgroundProperty); } set { SetValue(TileBackgroundProperty, value); } } public static readonly DependencyProperty TileBackgroundProperty = DependencyProperty.Register("TileBackground", typeof(string), typeof(TileControl), new PropertyMetadata("/Assets/Tiles/FlipCycleTileMedium.png")); #endregion // 漂浮 static Random rand = new Random(); public void Float() { // 使 tile 進入不同的漂浮狀態 switch (rand.Next(5)) { case 1: VisualStateManager.GoToState(this, "FloatVisualState1", false); break; case 2: VisualStateManager.GoToState(this, "FloatVisualState2", false); break; case 3: VisualStateManager.GoToState(this, "FloatVisualState3", false); break; case 4: VisualStateManager.GoToState(this, "FloatVisualState4", false); break; default: VisualStateManager.GoToState(this, "FloatVisualState3", false); break; } } // 選中 public void Checked() { VisualStateManager.GoToState(this, "CheckedVisualState", false); } // 恢復 public void Reset() { VisualStateManager.GoToState(this, "NormalVisualState", false); } }
第二步:定義不同的 Tile 在重疊時,判斷中心點的距離,是否小於 40px,如果是,則交換兩個 Tile。
如果 Tile 控件的 寬和 高設置為 150px,則各個控件的坐標為:
當拖動一個 Tile 到其它 Tile 上時,判斷兩個 Tile 的中心點坐標的距離:
第三步:在 MainPage 頁面中,添加一個 Grid,分別放置各個自定義控件,並且每個自定義控件放在一個 Boder 里面,
並且分別注冊 Border 的長按事件 Hold 統一為 Border_Hold:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.Resources> <Style TargetType="Border"> <Setter Property="Background" Value="Transparent"/> </Style> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <i:Interaction.Behaviors> <el:FluidMoveBehavior x:Name="fluidBehavior" AppliesTo="Children" Duration="00:00:01"> <el:FluidMoveBehavior.EaseY> <QuinticEase EasingMode="EaseOut"/> </el:FluidMoveBehavior.EaseY> <el:FluidMoveBehavior.EaseX> <QuinticEase EasingMode="EaseOut"/> </el:FluidMoveBehavior.EaseX> </el:FluidMoveBehavior> </i:Interaction.Behaviors> <Border Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/01.png"/> </Border> <Border Grid.Column="1" Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/02.png"/> </Border> <Border Grid.Row="1" Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/03.png"/> </Border> <Border Grid.Row="1" Grid.Column="1" Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/04.png"/> </Border> <Border Grid.Row="2" Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/05.png"/> </Border> <Border Grid.Row="2" Grid.Column="1" Hold="Border_Hold"> <local:TileControl TileBackground="/Assets/Tiles/06.png"/> </Border> </Grid>
第四步:在 MainPage 頁面的 CodeBehind 中,定義一個類型為 Dictionary<Point, Border> 的泛型字典 dic,
當頁面加載完成時,初始化該字典:
public MainPage() { InitializeComponent(); this.Loaded += MainPage_Loaded; } void MainPage_Loaded(object sender, RoutedEventArgs e) { InitDic(); } // Size : 資源素距離父元素左上角的距離 Dictionary<Point, Border> dic = new Dictionary<Point, Border>(); int childWidth; // 初始化一個字典集合,Key:border 元素的中心點坐標,value:border 自己 void InitDic() { dic.Clear(); foreach (var item in ContentPanel.Children) { childWidth = (int)item.DesiredSize.Width; Border border = item as Border; int row = Grid.GetRow(border); int col = Grid.GetColumn(border); Point position = new Point(col * childWidth + childWidth / 2, row * childWidth + childWidth / 2); border.Tag = position; dic.Add(position, border); Debug.WriteLine("point- x:" + (col * childWidth + childWidth / 2) + " y:" + (row * childWidth + childWidth / 2)); } }
在 Border 的 Border_Hold 方法中,添加邏輯,用來處理各個 Tile 的視圖狀態,並且注冊 ManipulationDelta 和 MouseLeftButtonUp 事件:
Border borderTemp; CompositeTransform transformTemp; private void Border_Hold(object sender, System.Windows.Input.GestureEventArgs e) { e.Handled = true; borderTemp = sender as Border; // 注冊 tile 右上角“圖釘”圖片的單擊事件 (borderTemp.Child as TileControl).Assets_thumb_png.Tap += Assets_thumb_png_Tap; transformTemp = new CompositeTransform(); transformTemp.TranslateX = x; transformTemp.TranslateY = y; borderTemp.RenderTransform = transformTemp; // 拖動 border 時,改變它的坐標 borderTemp.ManipulationDelta += borderTemp_ManipulationDelta; // 當手指離開 border 時,判斷各個 tile 中心點的距離 borderTemp.MouseLeftButtonUp += borderTemp_MouseLeftButtonUp; if (borderTemp.Child != null) { foreach (var item in ContentPanel.Children) { Border border = item as Border; if (border.Child != null) { TileControl tile = border.Child as TileControl; // 被選中的 tile 進入 CheckedVisualState 視圖狀態,其余的進入 FloatVisualState 視圖狀態 if (tile == (borderTemp.Child as TileControl)) { tile.Checked(); border.IsHitTestVisible = true; } else { tile.Float(); // 當處於漂浮狀態時,不再接收“單擊”等屏幕事件 border.IsHitTestVisible = false; } } } } }
當手指離開選擇的 Tile 時,判斷各個 Tile 的中心點的距離,從而判斷是否具備交換兩個 Tile 的條件:
void borderTemp_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { e.Handled = true; DateTime timeStart = DateTime.Now; Debug.WriteLine("Translation.X: " + transformTemp.TranslateX); Debug.WriteLine("Translation.Y: " + transformTemp.TranslateY); Point pointSource = (Point)borderTemp.Tag; Point point = new Point(transformTemp.TranslateX + pointSource.X, transformTemp.TranslateY + pointSource.Y); bool IsSwaped = false; // 當手指離開屏幕時,判斷選中的 tile 的中心點和其它中心點的直線距離, // 如果距離小於 40px,則兩個 tile 互換 //換 foreach (Point position in dic.Keys) { Border border2 = dic[position]; if (borderTemp != border2) { x = (int)(point.X - position.X); y = (int)(point.Y - position.Y); // 計算兩個 Tile 中心點的直線距離
int distance = (int)Math.Sqrt(x * x + y * y); if (distance < 40) { // 交換兩個 tile 的位置 SwapBorder(borderTemp, border2); IsSwaped = true; break; } } } Debug.WriteLine(DateTime.Now - timeStart); if (!IsSwaped) { transformTemp.TranslateX = 0; transformTemp.TranslateY = 0; } }
重要代碼粘貼到上面了,具體代碼可以下載工程。至此有關 FluidMoveBehavior 行為的三篇文章暫時寫完了。
這個demo 運行的交互很簡單,但是算法上還是頗費周折,想了幾種實現方式,最終采用了上面的方式,重點時間
都花費在了數學計算上了。不過 demo 的代碼還是比較粗糙,性能也沒有優化,只是實現了大概的交互。