前言
ListView應該算是在WP開發中最常用的一個顯示控件了,在我們的項目中,也大量的使用了ListView。很多WP上的開發者肯定也是如此。但是ListView有很多你可能沒用到的功能。這篇博客主要是結合項目中遇到的問題,從9個細節之處來介紹下ListView的全面使用。
基本
首先定義好我們准備使用的實體類,之后的代碼中將一直用到這些。
下面是MVVM中常用到的簡單基類,用於讓UI響應model的變化(雖然這個例子沒用到,但是如果有興趣的話,可以自己動手看看效果)
public abstract class Base : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // 使用CallerMemberNameAttribute可以獲得調用這個方法的成員名稱,對於屬性的set來說,就是屬性名 public void NotifyChange([CallerMemberName]string property = null) { if (this.PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } } }
用來在ListView中顯示的Item,我們假設Time屬性在應用中是不會改變的。
public class Item : Base { private string _title = string.Empty; public string Title { get { return _title; } set { _title = value; NotifyChange(); } } private string _content = string.Empty; public string Content { get { return _content; } set { _content = value; NotifyChange(); } } public string Time { get; set; } }
再來個Helper用來生成測試數據。
public static class DataHelper { public static ObservableCollection<Item> CreateItems() { var collection = new ObservableCollection<Item>(); for (var i = 0; i < 10; i++) { collection.Add(new Item { Title = "Title " + i.ToString(), Content = "Content " + i.ToString(), Time = DateTime.Now.ToString() }); } return collection; } }
這里為了簡單,直接把數據賦值給了頁面的上下文(DataContext),這樣在XAML中直接使用{Binding}即可綁定到當前的上下文。
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); this.NavigationCacheMode = NavigationCacheMode.Required; this.DataContext = DataHelper.CreateItems(); } }
<ListView x:Name="items_listview" ItemsSource="{Binding}"> <ListView.ItemTemplate> <!--一個簡單的ListView項目模板,綁定到Item.Title/Content--> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}"></TextBlock> <TextBlock Text="{Binding Path=Content}"></TextBlock> <TextBlock Text="{Binding Path=Time}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
這樣一個簡單ListView完成了,但是可以看到,默認的項目樣式有點丑,這個可以根據需要美化下ItemTemplate即可。。
對齊
我們的ListView現在所有的項目內容都是默認左對齊的,那么如果想要像博客園UAP那樣,把一部分內容放在右邊怎么辦呢?
在XAML中,控件有兩種對齊方式,HorizontalAlignment(水平對齊)和VerticalAlignment(垂直對齊),顯然我們現在需要的是水平對齊。然后利用Grid,將之分成2行,第2行用來顯示時間,並且設置TextBlock的HorizontalAlignment為Right就可以了,這樣我們的項目模板就變成了:
<DataTemplate> <!--每個項目都用顯示邊框,更好的區分開--> <Border BorderBrush="Blue" BorderThickness="1" Margin="0, 10, 0, 0"> <!--這里簡單的把每個項目的寬度都拉長,以便效果明顯--> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="20"></RowDefinition> </Grid.RowDefinitions> <StackPanel> <TextBlock Text="{Binding Path=Title}"></TextBlock> <TextBlock Text="{Binding Path=Content}"></TextBlock> </StackPanel> <TextBlock Grid.Row="1" HorizontalAlignment="Right" Text="{Binding Path=Time}"></TextBlock> </Grid> </Border> </DataTemplate>
但是運行的效果和想象的不一樣啊,時間也沒有靠右側顯示啊(我給每個項目都加了邊框,看起來明顯些)。
這是因為ListView的項目(也就是ListViewItem)的寬度和內容是一樣的,所以看不來效果。我們把項目的寬度設置成和ListView一樣的就可以了。
在Page.Resources內部修改ListViewItem的樣式:
<Page.Resources> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> </Style> </Page.Resources>
這次就對了。
多選
ListView默認是單選的,需要修改SelectionMode屬性來啟用多選的支持,這樣每個項目左側都會出現一個復選框。
現在ListView就變成下面這個樣子了:
ScrollViewer的獲得及應用
如果你用過我們的應用的話,你在某個作者的文章列表頁面會發現:隨着你用手向上滑動,頁面的標題會變成作者的頭像和昵稱,方便用戶識別當前在看誰的博客列表,那么這個功能是怎么實現的呢?
滾動前:
滾動后:
其實這個功能是通過判斷ListView的ScrollViewer的滾動方向和距離來實現的。
首先我們需要找到ListView上的ScrollViewer控件,這個控件不是顯式的在XAML中定義的,我們需要在VirtualTree上來查找。
下面這是個通用的查找方法。
public static ScrollViewer GetScrollViewer(Windows.UI.Xaml.DependencyObject depObj) { if (depObj is ScrollViewer) { return depObj as ScrollViewer; } for (int i = 0; i < Windows.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(depObj); i++) { var child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(depObj, i); var result = GetScrollViewer(child); if (result != null) return result; } return null; }
然后我們在ListView加載完成之后(這里一定要是加載完成之后,否則你得到可能是個null),查找這個ScrollViewer,然后添加ViewChanged事件(當滑動時觸發),並在事件內對滑動的距離和方向進行判斷
private void ListView_Loaded(object sender, RoutedEventArgs e) { this.scrollViewer = GetScrollViewer(this.ListView); this.scrollViewer.ViewChanged += scrollViewer_ViewChanged; } void scrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) { //VerticalOffset大於0表示向上滑動 if (this.scrollViewer.VerticalOffset > 50) { if (!isAuthorShowOnTitle) { //執行動畫 this.sb_AuthorMoveUp.Begin(); } } else { if (isAuthorShowOnTitle) { // 執行動畫 this.sb_AuthorMoveDown.Begin(); } } }
顯示多列
現在我們的ListView一直都是只顯示一列,那么怎么樣實現下圖的效果呢?可以不用GridView么?
使用ListView實現這個功能,需要自定義首先定義ItemsPanel模板(ItemsPanelTemplate),通過WrapGrid來實現的,WrapGrid是按從左到右或從上到下的順序對子元素進行定位,而這個布局的功能實際上就是GridView的,再修改MaximumRowsOrColumns來定義能夠顯示的最多列/行,這樣我們的ListView就實現了最多兩列的效果。
<ListView x:Name="items_listview" ItemsSource="{Binding}"> <ListView.ItemsPanel> <ItemsPanelTemplate> <WrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2"></WrapGrid> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemTemplate> <!--一個簡單的ListView項目模板,綁定到Item.Title/Content--> <DataTemplate> <!--每個項目都用顯示邊框,更好的區分開--> <Border BorderBrush="Blue" BorderThickness="1" Margin="0, 10, 0, 0"> <!--這里簡單的把每個項目的寬度都拉長,以便效果明顯--> <Grid Width="150"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="20"></RowDefinition> </Grid.RowDefinitions> <StackPanel> <TextBlock Text="{Binding Path=Title}"></TextBlock> <TextBlock Text="{Binding Path=Content}"></TextBlock> </StackPanel> <TextBlock VerticalAlignment="Bottom" Grid.Row="1" HorizontalAlignment="Right" Text="{Binding Path=Time}"></TextBlock> </Grid> </Border> </DataTemplate> </ListView.ItemTemplate> </ListView>
效果如下:
Header和Footer
很多使用ListView的人可能沒有注意到,其實ListView還有Header和Footer,同時也都支持自定義模板。
在博客園UAP中,我們使用Header來作為頁面的副標題,Footer則可以用來作為增量加載時的提示,比如”加載中。。“,”沒有更多了。。“。
Header和Footer的使用很簡單,和ItemTemplate一樣,只要定義好對應的模板就可以了。
<ListView.HeaderTemplate> <DataTemplate> <Grid Background="Red"> <TextBlock FontSize="25" Text="這是一個副標題"></TextBlock> </Grid> </DataTemplate> </ListView.HeaderTemplate> <ListView.FooterTemplate> <DataTemplate> <Grid Background="Green"> <TextBlock FontSize="25" Text="沒有更多內容啦。。。"></TextBlock> </Grid> </DataTemplate> </ListView.FooterTemplate>
效果如下圖。
分組
當我第一次使用Windows phone時,覺得應用列表頁面向上滑動時的效果很cool(如下圖,當沒有更多以b開頭的應用之后,手指再向上滑動,c會慢慢把b頂上去),要在Windows phone上實現這一效果,只需要使用ListView顯示分組數據就可以了,動畫效果是自帶的。
先定義一個簡單的分組類:
public class Group : Base { private string _name = string.Empty; public string Name { get { return _name; } set { _name = value; this.NotifyChange(); } } public ObservableCollection<Item> Items { get; private set; } public Group() { this.Items = new ObservableCollection<Item>(); } }
然后再DataHelper中添加一個生成分組數據的方法(請無視循環中可能存在的性能問題-_-)。
public static ObservableCollection<Group> CreateGroups() { var groups = new ObservableCollection<Group>(); for (var i = 0; i < 13; i++) { var group = new Group { Name = "Group " + i.ToString() }; for (var j = 0; j < 10; j++) { var item = new Item { Time = DateTime.Now.ToString(), Title = "Title " + j.ToString(), Content = "Content" + j.ToString() }; group.Items.Add(item); } groups.Add(group); } return groups; }
在頁面上,ListView的ItemsSource和之前的有點不一樣了,我們需要告訴ListView該怎么顯示數據,每個分組中項目列表是哪個屬性。這時候我們需要定義一個數據集視圖(CollectionView),具體請看下面代碼里的注釋。
<Page x:Class="ListView_Group_Sample.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:ListView_Group_Sample" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Page.Resources> <!--通過XAML創建數據集視圖,並通過視圖定義分組(IsSourceGrouped) ,每個分組中項目對應的路徑(ItemsPath,對應的就是我們Group.Items) ,Source表示當前視圖的源,{Binding}表示綁定的是當前的上下文(DataContext)--> <CollectionViewSource x:Name="cv_items" IsSourceGrouped="True" ItemsPath="Items" Source="{Binding}"></CollectionViewSource> </Page.Resources> <Grid> <!--這里ItemsSource和普通的有點不一樣了,要用到在Page.Resources中定義的視圖來顯示--> <ListView x:Name="items_listview" ItemsSource="{Binding Source={StaticResource cv_items}}"> <ListView.GroupStyle> <GroupStyle > <!--分組的頭部顯示的模板,這里我們用背景色來高亮,文字綁定到Group.Name--> <GroupStyle.HeaderTemplate> <DataTemplate> <Grid Background="BlueViolet"> <TextBlock Text="{Binding Path=Name}" FontSize="20"></TextBlock> </Grid> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </ListView.GroupStyle> <ListView.ItemTemplate> <!--一個簡單的ListView項目模板,綁定到Item.Title/Content--> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}"></TextBlock> <TextBlock Text="{Binding Path=Content}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Page>
這樣一個簡單的分組功能就實現了
開始的時候:
被推上去了:
但是遺憾的是,ListView分組顯示之后,就不能通過ISupportIncrementalLoading來實現增量加載了(增量加載組?)。
SemanticZoom控件
前面提到了WP的應用列表界面的分組顯示,那么這個頁面的另一個更cool的效果就是點擊任意分組的Header之后,會顯示一個縮小的索引視圖,這個就是SemanticZoom控件的效果。
這個控件實際上是通過控制內部的兩個ListView/GridView來實現這種效果的,一個顯示縮小的索引視圖(ZoomOutView),另一個顯示具體的分組列表(ZoomInView)。前面我們已經實現了分組列表,這樣我們只需要再用GridView實現個縮小的視圖,然后放在SemanticZoom空間內部就完成了。
實現一個只顯示Group.Name的Grid很簡單。這里需要注意的是,我們直接把分組集合綁定到了GridView.ItemsSource,這樣對於每個GridViewItem而言,其上下文就變成了Group,而不是Item,所以我們在TextBlock中綁定的是Group.Name。
<GridView ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <Border BorderBrush="Blue" BorderThickness="2" Margin="10, 10, 0, 0"> <Grid Height="150" Width="150" Background="Black"> <TextBlock Text="{Binding Path=Name}"></TextBlock> </Grid> </Border> </DataTemplate> </GridView.ItemTemplate> </GridView>
顯示如下,效果差不多-_-..
現在兩個視圖都有了,是時候放在SemanticZoom控件里了。現在把ListView放在ZoomInView用來顯示詳細信息,把GridView放在ZoomOutView顯示縮略信息。
<SemanticZoom> <SemanticZoom.ZoomedInView> <!--這里ItemsSource和普通的有點不一樣了,要用到在Page.Resources中定義的視圖來顯示--> <ListView x:Name="items_listview" ItemsSource="{Binding Source={StaticResource cv_items}}"> <ListView.GroupStyle> <GroupStyle > <!--分組的頭部顯示的模板,這里我們用背景色來高亮,文字綁定到Group.Name--> <GroupStyle.HeaderTemplate> <DataTemplate> <Grid Background="BlueViolet"> <TextBlock Text="{Binding Path=Name}" FontSize="20"></TextBlock> </Grid> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </ListView.GroupStyle> <ListView.ItemTemplate> <!--一個簡單的ListView項目模板,綁定到Item.Title/Content--> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}"></TextBlock> <TextBlock Text="{Binding Path=Content}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </SemanticZoom.ZoomedInView> <SemanticZoom.ZoomedOutView> <GridView ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <Border BorderBrush="Blue" BorderThickness="2" Margin="10, 10, 0, 0"> <Grid Height="150" Width="150" Background="Black"> <TextBlock Text="{Binding Path=Name}"></TextBlock> </Grid> </Border> </DataTemplate> </GridView.ItemTemplate> </GridView> </SemanticZoom.ZoomedOutView> </SemanticZoom>
這時候你如果運行程序,默認顯示的ListView,當你點擊分組的Header后,GridView會自動彈出來了,這樣一個簡單SemanticZoom就是實現了,切換工作都是系統幫忙實現的。
更新項目
在博客園UAP這個應用中,在博客列表頁面上,如果點擊文章標題的話,會運行自定義動畫把博客的summary隱藏起來,並顯示“朕無視”來表示忽略此文章。雖然我們可以使用綁定狀態來隱藏/顯示控件,但是這樣卻不能執行自定義動畫,所以我們是在ListView每個項目的DataContextChanged事件和OnApplyTemplate事件中進行狀態判斷的,其中每個項目都是一個自定義控件,在控件中判斷當前綁定數據的狀態來執行對應的邏輯。
下面這個PostControl自定義控件在兩個事件中通過GetTemplateChild得到子控件,然后對子控件進行對應的設置,如動畫,是否顯示等。
public sealed class PostControl : Control { public PostControl() { this.DefaultStyleKey = typeof(PostControl); this.DataContextChanged += PostControl_DataContextChanged; } void PostControl_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { this.UpdateUI(false); } protected override void OnApplyTemplate() { this.UpdateUI(false); } private void UpdateUI(bool showAnimation = true) { //更新邏輯 var tbSummary = this.GetTemplateChild("tb_Summary") as TextBlock; } }
這里需要注意,一定在這兩個事件中都要進行更新,因為有的時候,其中某個事件還得不到子控件,這個暫時還不知道原因,可能和調用的順序有關吧。
分享代碼,改變世界!
Windows Phone Store App link:
http://www.windowsphone.com/zh-cn/store/app/博客園-uap/500f08f0-5be8-4723-aff9-a397beee52fc
Windows Store App link:
http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059
GitHub open source link:
https://github.com/MS-UAP/cnblogs-UAP
MSDN Sample Code:
https://code.msdn.microsoft.com/CNBlogs-Client-Universal-477943ab