WIUI——自定义Slider

开发环境

VS2022

.net 6/8

需求

  1. 滑动条可以调节页码的变化。
  2. 滑动条在调用时可以实现对它绑定事件的控制(滑动条的值变化时响应的事件,可以取消绑定,需要时又可以再次绑定)。
  3. 滑动条数据变化时不需要在所有变化的时候都响应,只需要保证使用者能看到连续变化的数据,并且需要保证滑动条在最后停下时的数据一定要响应绑定事件。

先分析一下上述需求:

第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是可行的,其它控件可能就需要根据实际情况来实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值