上一回实现了一个宽度不均匀的Panel,这次我们编写一个简单的BigbangView主体。
首先创建一个模板化控件,删掉Themes/Generic.xaml中的<Style TargetType="BigbangView">...</Style>段。
然后打开C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\(SDK版本)\Generic\generic.xaml,在里面找到
<Style TargetType="ListViewItem" x:Key="ListViewItemExpanded"> ... </Style> <Style TargetType="ListView"> ... </Style>
这两段,复制到项目中Themes/Generic.xaml中,将TargetType="ListView"修改为TargetType="BigbangView",添加Setter:


<Setter Property="SelectionMode" Value="Multiple"></Setter> <Setter Property="HorizontalAlignment" Value="Stretch"></Setter> <Setter Property="VerticalAlignment" Value="Center"></Setter> <Setter Property="IsTabStop" Value="False" /> <Setter Property="TabNavigation" Value="Once" /> <Setter Property="IsSwipeEnabled" Value="True" /> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" /> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" /> <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" /> <Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" /> <Setter Property="ScrollViewer.VerticalScrollMode" Value="Enabled" /> <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="True" /> <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" /> <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" /> <Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True" /> <Setter Property="UseSystemFocusVisuals" Value="True" /> <Setter Property="ItemContainerTransitions"> <Setter.Value> <TransitionCollection> <AddDeleteThemeTransition /> <ContentThemeTransition /> <ReorderThemeTransition /> <EntranceThemeTransition IsStaggeringEnabled="False" /> </TransitionCollection> </Setter.Value> </Setter> <Setter Property="ItemContainerStyle"> <Setter.Value> <Style TargetType="ListViewItem"> 前面复制的ListViewItemExpanded的内容剪贴到这里 </Style> </Setter.Value> </Setter> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <control:BigbangPanel > <control:BigbangPanel.ChildrenTransitions> <TransitionCollection> <AddDeleteThemeTransition /> </TransitionCollection> </control:BigbangPanel.ChildrenTransitions> </control:BigbangPanel> </ItemsPanelTemplate> </Setter.Value> </Setter>
其中BigbangPanel是咱们上回书写的面板。
然后打开BigbangView.cs,修改基类:
public sealed class BigbangView : ListView
{
public BigbangView()
{
this.DefaultStyleKey = typeof(BigbangView);
}
}
接下来就是整个过程中最复杂,最枯燥的部分,编写按下滑动选中。
先打开安卓版的大爆炸(不是锤子的可以拿个安卓手机下载IT之家客户端看效果),对整个过程进行分析发现,有以下几种状态。
1、点击选中;
2、Panel高度小于控件高度,也就是ScrollViewer不启用时,按下向四周滑动可以更改选中状态;
3、Panel高度大于控件高度,上下滑动可以滚动ScrollViewer,左右滑动禁用ScrollViewer的滚动并且更改选中状态 。
这篇文章先实现鼠标的操作。由于鼠标在页面上下滑动并不会触发ScrollViewer的滚动,所以在此不考虑第三项。
首先修改Style中的Template段,


<Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="control:BigbangView"> <Border x:Name="RootBorder" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}"> <ScrollViewer x:Name="ScrollViewer" TabNavigation="{TemplateBinding TabNavigation}" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}" IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}" IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}" IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}" BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}" AutomationProperties.AccessibilityView="Raw"> <Grid x:Name="ItemsGrid" Background="Transparent" ManipulationMode="System"> <ItemsPresenter Header="{TemplateBinding Header}" HeaderTemplate="{TemplateBinding HeaderTemplate}" HeaderTransitions="{TemplateBinding HeaderTransitions}" Footer="{TemplateBinding Footer}" FooterTemplate="{TemplateBinding FooterTemplate}" FooterTransitions="{TemplateBinding FooterTransitions}" Padding="{TemplateBinding Padding}"/> </Grid> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter>
在后台代码中重载OnApplyTemplate()(实际应该做空判断,我偷懒了)
public BigbangView()
{
this.DefaultStyleKey = typeof(BigbangView);
this.Loaded += BigbangView_Loaded;
this.Unloaded += BigbangView_Unloaded;
this.SelectionChanged += BigbangView_SelectionChanged;
PointerPressedHandler = new PointerEventHandler(_PointerPressed);
PointerReleasedHandler = new PointerEventHandler(_PointerReleased);
PointerMovedHandler = new PointerEventHandler(_PointerMoved);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
RootBorder = GetTemplateChild("RootBorder") as Border;
ItemsGrid = GetTemplateChild("ItemsGrid") as Grid;
ScrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
ScrollViewer.AddHandler(UIElement.PointerPressedEvent, PointerPressedHandler, true);
ScrollViewer.AddHandler(UIElement.PointerReleasedEvent, PointerReleasedHandler, true);
ScrollViewer.AddHandler(UIElement.PointerCanceledEvent, PointerReleasedHandler, true);
ScrollViewer.AddHandler(UIElement.PointerExitedEvent, PointerReleasedHandler, true);
ScrollViewer.ViewChanging += _ScrollViewer_ViewChanging;
}
然后我们需要把每个子元素在Panel中的位置缓存下来,在Panel中添加属性
private Dictionary<UIElement, Rect> _ChildrenRects;
public Dictionary<UIElement, Rect> ChildrenRects
{
get
{
if (_ChildrenRects == null) _ChildrenRects = new Dictionary<UIElement, Rect>();
return _ChildrenRects;
}
set => _ChildrenRects = value;
}
将ArrangeOverride中Children[x].Arrange(new Rect...)修改为如下
var rect = new Rect(x, y, Children[i].DesiredSize.Width, Children[i].DesiredSize.Height);
Children[i].Arrange(rect);
ChildrenRects[Children[i]] = rect;
编写以下几个辅助的方法:
//从Item获取在Panel中的布局信息
private Rect? GetItemRect(object Item)
{
var itemContainer = base.ContainerFromItem(Item) as UIElement;
if (itemContainer != null && _Panel != null)
{
if (_Panel.ChildrenRects.ContainsKey(itemContainer))
{
return _Panel.ChildrenRects[itemContainer];
}
}
return null;
}
//从容器获取Item和Index
private int GetIndexFromContainer(UIElement Container, out object Item, IList<object> SourceList = null)
{
Item = ItemFromContainer(Container);
if (Item != null)
{
if (SourceList == null) SourceList = GetSourceList();
return SourceList.IndexOf(Item);
}
return -1;
}
//获取坐标位置的Item和Index
private int GetIndexFromPosition(Point Position, out object Item)
{
var sourceList = GetSourceList();
for (int i = 0; i < sourceList.Count; i++)
{
var rect = GetItemRect(sourceList[i]);
if (!rect.HasValue) break;
if (rect.Value.Contains(Position))
{
Item = sourceList[i];
return i;
}
}
Item = null;
return -1;
}
//获取源列表
private IList<object> GetSourceList()
{
if (ItemsSource == null) return Items.ToList();
else
{
var tmp = new List<object>();
foreach (var item in (IEnumerable)ItemsSource)
{
tmp.Add(item);
}
return tmp;
}
}
然后编写三个状态方法:OnSelectionStart初始化各个变量的状态,获取开始的点;OnSelecting更新被选中的项,OnSelectionComplate做最后的清理,还原状态:
int StartIndex = -1; //本次选择开始的位置
int EndIndex = -1; //本次选择结束的位置
bool? IsFirstItemHadSelected; //本次选择是选中后续还是取消选中后续
Point? StartPoint; //选中开始的坐标
UIElement StartContainer; //选择开始时的容器
private void OnSelectionStarted(Point Position)
{
var tmpIndex = GetIndexFromPosition(Position, out var item);
if (tmpIndex == -1)
{
return;
}
if (StartIndex < 0)
{
StartIndex = tmpIndex;
IsFirstItemHadSelected = SelectedItems.Contains(item);
StartContainer = ContainerFromItem(item) as UIElement;
}
}
private void OnSelecting(UIElement Container)
{
if (IsFirstItemHadSelected.HasValue)
{
var sourceList = GetSourceList();
var tmpIndex = GetIndexFromContainer(Container, out var item, sourceList);
if (tmpIndex == -1) return;
if (StartIndex < 0)
{
StartIndex = tmpIndex;
return;
}
else
{
EndIndex = tmpIndex;
if (EndIndex >= 0)
{
for (int i = Math.Min(StartIndex, EndIndex); i <= Math.Max(StartIndex, EndIndex); i++)
{
if (IsFirstItemHadSelected.Value)
{
if (SelectedItems.Contains(sourceList[i])) SelectedItems.Remove(sourceList[i]);
}
else
{
if (!SelectedItems.Contains(sourceList[i])) SelectedItems.Add(sourceList[i]);
}
}
}
}
}
}
private void OnSelectionComplated()
{
StartIndex = -1;
EndIndex = -1;
IsFirstItemHadSelected = null;
StartPoint = null;
StartContainer = null;
}
然后编写事件:
private void Container_PointerEntered(object sender, PointerRoutedEventArgs e)
{
if (IsFirstItemHadSelected.HasValue)
{
if (sender is UIElement ele && ele != StartContainer)
{
if (StartContainer == null)
{
StartContainer = ele;
StartIndex = GetIndexFromContainer(StartContainer, out var item);
}
else
{
OnSelecting(ele);
}
}
}
}
private void _PointerPressed(object sender, PointerRoutedEventArgs e)
{
StartPoint = e.GetCurrentPoint(ItemsGrid).Position;
OnSelectionStarted(StartPoint.Value);
IsSwipeEnable = true;
}
private void _PointerReleased(object sender, PointerRoutedEventArgs e)
{
if (IsSwipeEnable.HasValue)
{
OnSelectionComplated();
}
}
至此,BigbangView已经可以响应鼠标的滑动选择。
下回预告:BigbangView响应触摸。