本篇之所以起这样一个名字,是因为重点并非如何自定义控件,不涉及创建CustomControl和UserControl使用的Template和XAML概念。而是通过继承的方法来扩展一个现有的类,在继承的子类中增加属性和扩展行为。
我们在《UWP开发入门(七)——下拉刷新》中提到过嵌套ScrollViewer的实现思路,本篇我们对ListView的第一个扩展行为,即是摒弃嵌套的做法,而是通过访问ListView内部的ScrollViewer控件,来监听ViewChanged事件。
访问ListView内部的ScrollViewer,必定离不开VisualTreeHelper类中的以下两个方法:
public static DependencyObject GetChild(DependencyObject reference, System.Int32 childIndex); public static System.Int32 GetChildrenCount(DependencyObject reference);
可以将这两个方法进一步组合得到:
static T FindFirstChild<T>(FrameworkElement element) where T : FrameworkElement { int childrenCount = VisualTreeHelper.GetChildrenCount(element); var children = new FrameworkElement[childrenCount]; for (int i = 0; i < childrenCount; i++) { var child = VisualTreeHelper.GetChild(element, i) as FrameworkElement; children[i] = child; if (child is T) return (T)child; } for (int i = 0; i < childrenCount; i++) if (children[i] != null) { var subChild = FindFirstChild<T>(children[i]); if (subChild != null) return subChild; } return null; }
该方法通过递归来遍历FrameworkElement内部的元素,并返回第一个符合类型的元素。ListViewEx的第一个扩展如下:
public class ListViewEx : ListView, INotifyPropertyChanged { private ScrollViewer _scrollViewer; public event EventHandler LoadHistoryEvent; public event PropertyChangedEventHandler PropertyChanged; protected void OnProperyChanged([CallerMemberName] string name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } public ListViewEx() { this.Loaded += ListViewEx_Loaded; this.Unloaded += ListViewEx_Unloaded; } private void ListViewEx_Unloaded(object sender, RoutedEventArgs e) { this.Unloaded -= ListViewEx_Unloaded; if (_scrollViewer != null) { _scrollViewer.ViewChanged -= Sv_ViewChanged; } } private void ListViewEx_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { this.Loaded -= ListViewEx_Loaded; _scrollViewer = FindFirstChild<ScrollViewer>(this); if (_scrollViewer != null) { _scrollViewer.ViewChanged += Sv_ViewChanged; } } private async void Sv_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) { if (e.IsIntermediate == false && _scrollViewer.VerticalOffset < 1) { _scrollViewer.ChangeView(null, 50, null); await Task.Delay(10); LoadHistoryEvent?.Invoke(this, EventArgs.Empty); } }
嗯嗯,可以看到优雅的 -= event和恰到好处的null check,啊啊!忍不住想点个赞!在ViewChanged事件监测到滚动条到达顶部后,果断触发ListViewEx内定义的LoadHistoryEvent来通知更新数据。
本篇如果仅增加一个LoadHistoryEvent,又会被人非议是在补完前篇,一篇拆成两篇写……那好吧,我们再给ListViewEx增加第二个扩展GoBottomVisiblity属性,顾名思义即是ListViewEx向上滚动几屏以后,显示一个GoBottom的按钮。
在ListViewEx的类中,首先定义属性GoBottomVisibility,然后同样是在ViewChanged事件中,计算是否显示GoBottom按钮。
public Visibility GoBottomVisiblity { get { return _goBottomVisiblity; } set { _goBottomVisiblity = value; this.OnProperyChanged(); } } private void Sv_ViewChanged2(object sender, ScrollViewerViewChangedEventArgs e) { if (e.IsIntermediate == false) { CheckGoBottomVisibility(); } } private void CheckGoBottomVisibility() { if (_scrollViewer.VerticalOffset + _scrollViewer.ViewportHeight < _scrollViewer.ScrollableHeight) { GoBottomVisiblity = Visibility.Visible; } else { GoBottomVisiblity = Visibility.Collapsed; } }
代码没法全部贴上来,一个是太长了显得啰嗦,二是会被管理员说没内涵从首页删掉……
大体上本篇就是给ListView扩展了LoadHistoryEvent事件和GoBottomVisibility属性。最后说说怎么用,XAML里使用ListViewEx代替默认的ListView,会发现多出一个LoadHistoryEvent,挂上加载数据的事件就OK了。然后在列表的下部画一个三角箭头,Visibility绑定到ListViewEx的GoBottomVisibility属性上就收工了。
<Grid> <local:ListViewEx x:Name="listViewEx" ItemsSource="{x:Bind Items}" LoadHistoryEvent="ListViewEx_LoadHistoryEvent"></local:ListViewEx> <Grid Margin="10" VerticalAlignment="Bottom" HorizontalAlignment="Center" Tapped="Grid_Tapped" Visibility="{x:Bind listViewEx.GoBottomVisiblity,Mode=OneWay}"> <Ellipse Fill="LightGray" Width="30" Height="30"></Ellipse> <Polyline Stroke="White" Points="10,10 15,20 20,10"></Polyline> </Grid> </Grid>
完整代码放在GayHub上,地址:https://github.com/manupstairs/UWPSamples
非常感谢各位捧场,能够点开页面看到这里,拜谢了!Orz