开发环境
VS2022
.net 6/8
需求
- 滑动条可以调节页码的变化。
- 滑动条在调用时可以实现对它绑定事件的控制(滑动条的值变化时响应的事件,可以取消绑定,需要时又可以再次绑定)。
- 滑动条数据变化时不需要在所有变化的时候都响应,只需要保证使用者能看到连续变化的数据,并且需要保证滑动条在最后停下时的数据一定要响应绑定事件。
先分析一下上述需求:
第1条是肯定能满足的,这是最基本的需要求;
第2条实现起来也简单,也就是在UI中调用这个Slider的时候能订阅事件与解除订阅事件;
第3条复杂点,“需要保证使用者看到连续变化的数据”这个也就是说要让使用者不能感到滑动条卡顿,也不能让他感到由此引发的图像变化卡顿(开发此的使用场景是用于图片的翻页);至于第3条中“保证滑动条在最后停下时的数据一定要响应绑定事件”即是要保证最后显示图片是滑动条最后显示的那张图片。
不卡顿,这个于人的直观感受而言就是要保证每秒24帧图片切换,就能保证图片在连续翻页时不会有卡顿感,那么也就是说连续切换时每张图片的显示时间不能超过1000ms/24=41.67ms,取一个整数,那40ms,也就是说保证每秒25帧的图片切换就能让人看到连续不卡顿的图片切换。
实现
需求中第2条好弄;第3条为了实现,愚采用的队列的方式,若slider的数据有连续的变化,只要调用事件有订阅,就将当前页码与时间添加到记录当前页码和时间的队列中;然后以40ms的间隔取出队列中的数据,并同时调用事件。
详细的实现见下述代码:
public sealed class PageSlider : Slider
{
const int freshTime = 40;
public PageSlider()
{
this.ManipulationStarted += PageSlider_ManipulationStarted;
this.ManipulationCompleted += PageSlider_ManipulationCompleted;
this.PointerWheelChanged += PageSlider_PointerWheelChanged;
this.ValueChanged += PageSlider_ValueChanged;
this.Unloaded += PageSlider_Unloaded;
}
/// <summary>
/// 移除ValueChanged事件(用于UI中其它地方调用)
/// </summary>
internal void RemoveValueChangedEvent()
{
this.ValueChanged -= PageSlider_ValueChanged;
}
/// <summary>
/// 添加ValueChanged事件(用于UI中其它地方调用)
/// </summary>
internal void AddValueChangedEvent()
{
this.ValueChanged += PageSlider_ValueChanged;
}
/// <summary>
/// 卸载时将timer停止
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageSlider_Unloaded(object sender, RoutedEventArgs e)
{
if (scrollTimer != null)
{
scrollTimer.Stop();
scrollTimer.Tick -= ScrollTimer_Tick;
scrollTimer = null;
}
pageSliderTimer.Stop();
}
private void PageSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
//slide没有操作手势(按住移动)且当前时间头之前时间的间隔大于刷新时间
if (!pageSliderManipulate && DateTime.Now.Subtract(PreTime).TotalMilliseconds > freshTime)
{
if (!IsInvokeEvent)//当前没有调用事件
{
InvokeEvent?.Invoke(this, null);//直接调用
}
}
//事件有订阅
if (InvokeEvent?.GetInvocationList().Length > 0)
{
//将当前页码、时间加入队列
pageNODatetime.Enqueue(new PageNOTime(e.NewValue, DateTime.Now));
}
}
private readonly DispatcherTimer pageSliderTimer = new();
/// <summary>
/// 用于存储翻页时当前页码、当前时间
/// </summary>
Queue<PageNOTime> pageNODatetime = new();
DateTime queueTime = DateTime.Now;
/// <summary>
/// 是否在进行操作手势
/// </summary>
bool pageSliderManipulate = false;
/// <summary>
/// 滑动条开始滑动
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageSlider_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
pageSliderManipulate = true;
pageSliderTimer.Interval = TimeSpan.FromMilliseconds(freshTime);//定时器40ms触发tick事件
pageSliderTimer.Tick += PageSliderTimer_Tick;
pageSliderTimer.Start();
}
/// <summary>
/// 翻页滑动条计时器事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageSliderTimer_Tick(object sender, object e)
{
Debug.WriteLine("PageSliderTimer_Tick");
//当前时间与之前时间间隔小于40ms时,直接将队列中出口第一个数据dequeue
while (pageNODatetime.Count > 1 && pageNODatetime.Peek().Time.Subtract(queueTime).TotalMilliseconds < freshTime)
{
var first = pageNODatetime.Dequeue();
queueTime = first.Time;
}
//若当前时间-preTime>=40ms
if (pageNODatetime.Count > 1 && pageNODatetime.Peek().Time.Subtract(queueTime).TotalMilliseconds >= freshTime)
{
var first = pageNODatetime.Dequeue();
queueTime = first.Time;
InvokeEvent?.Invoke(this, null);
return;
}
//queue中仅一个时直接调用,并清空queue
if (pageNODatetime.Count == 1)
{
var first = pageNODatetime.Dequeue();
queueTime = first.Time;
InvokeEvent?.Invoke(this, null);
return;
}
}
/// <summary>
/// 滑动条滚动完成
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void PageSlider_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
pageSliderTimer.Tick -= PageSliderTimer_Tick;
pageSliderManipulate = false;
pageSliderTimer.Stop();
if (IsInvokeEvent)
{
await Task.Delay((int)freshTime);//等待前次的图像更新完毕
}
pageNODatetime.Clear();
InvokeEvent?.Invoke(this, null);
}
/// <summary>
/// 需要调用的事件
/// </summary>
public event EventHandler InvokeEvent;
/// <summary>
/// 是否调用事件
/// </summary>
public bool IsInvokeEvent
{
get { return (bool)GetValue(IsInvokeEventProperty); }
set { SetValue(IsInvokeEventProperty, value); }
}
public static readonly DependencyProperty IsInvokeEventProperty =
DependencyProperty.Register("IsInvokeEvent", typeof(bool), typeof(PageSlider), new PropertyMetadata(false));
/// <summary>
/// 之前执行时刻
/// </summary>
public DateTime PreTime
{
get { return (DateTime)GetValue(PreTimeProperty); }
set { SetValue(PreTimeProperty, value); }
}
public static readonly DependencyProperty PreTimeProperty =
DependencyProperty.Register("PreTime", typeof(DateTime), typeof(PageSlider), new PropertyMetadata(DateTime.Now));
private DispatcherTimer scrollTimer;
//鼠标滚轮事件,方便测试
private void PageSlider_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var delta = e.GetCurrentPoint(this).Properties.MouseWheelDelta;
if (delta > 0)
{
this.Value--;
}
else if (delta < 0)
{
this.Value++;
}
e.Handled = true;
//避免滚动结束时未调用最后一次需要响应的事件
if (scrollTimer == null)
{
scrollTimer = new()
{
Interval = TimeSpan.FromMilliseconds(80) // 设置一个适当的时间间隔
};
scrollTimer.Tick += ScrollTimer_Tick;//初始化完成绑定事件
}
//scrollTimer.Stop();
scrollTimer.Start();
}
/// <summary>
/// ScrollTmer tick事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ScrollTimer_Tick(object sender, object e)
{
// 鼠标滚动结束
scrollTimer.Stop();
InvokeEvent?.Invoke(this, null);
}
/// <summary>
/// 翻页时记录页码与在前页的当时时间,以便后续进行显示;页码主要为后续可能的扩展做准备
/// </summary>
class PageNOTime
{
public PageNOTime(double pagetNO, DateTime time)
{
PageNO = pagetNO;
Time = time;
}
/// <summary>
/// 页码编号
/// </summary>
public double PageNO { get; set; }
/// <summary>
/// 时间
/// </summary>
public DateTime Time { get; set; }
}
}
注意
timer是很可能导致垃圾回收不及时的,若在运行,垃圾回收失败就会导致当前引用它的Page也不能回收,此page若每次调用都会实例化的话,会导致内存中出现多个此page的实例,此可能导致内存泄漏;一定在使用完timer后将其及时的释放资源(一般是调用stop或dispose)。
其实以上用队列的方式来实现搞复杂了,只需要每次在index或叫页码改变时比较与前一次是否调用过事件的时间比较就行,只要时间间隔大于40ms就再次调用需要的事件或方法。而当slider完成时再用操作完成事件来发最后一次结果即可。不过这样处理的话就只针对slider是可行的,其它控件可能就需要根据实际情况来实现了。