可以鼠标滑动的Banner控件!

效果预览

压缩后动画.gif

技术需求

要做一个Banner控件,图片可以自动循环轮播,可以鼠标滑屏滚动,点击单张图片可以预览

技术框架

基于.net 4.6 的技术框架
基于wpf的客户端开发框架

技术实现

1.创建一个自定义控件,例如RollBox

在Grid内需要一个Canvas容器来承载Banner内所有的图片,另外,因为图片可能较多,所以要进行异步加载,需要一个Grid控件(MsgBox)作为加载时的遮罩层

<UserControl x:Class="RollWall.RollBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:RollWall" Loaded="UserControl_Loaded"
             mc:Ignorable="d" 
             d:DesignHeight="800" d:DesignWidth="1800" x:Name="UC">
    <Grid>
        <Canvas x:Name="box" ClipToBounds="True" 
        Height="{Binding ElementName=UC,Path=Height}" 
        Width="{Binding ElementName=UC,Path=Width}" 
        Background="OldLace">
        </Canvas>
        <Grid x:Name="MsgBox" Visibility="Collapsed">
            <Border Opacity="0.8" Background="Black" />
            <TextBlock Text="正在加载资源..." Foreground="White" FontSize="45" VerticalAlignment="Center" HorizontalAlignment="Center" />
        </Grid>
    </Grid>

</UserControl>
2.定义相关依赖属性

根据需求只定义了轮播速度和图片源两个依赖属性

#region 封装自定义依赖属性

        public static readonly DependencyProperty RollSouceProperty = DependencyProperty.Register("RollSouce", typeof(string),typeof(RollBox));

        public static readonly DependencyProperty RollSpeedProperty = DependencyProperty.Register("RollSpeed", typeof(RollSpeedEnum), typeof(RollBox),new PropertyMetadata(RollSpeedEnum.Medium));

        /// <summary>
        /// 图片源路径(文件夹完整路径)
        /// </summary>
        public string RollSouce
        {
            get
            {
                return (string)GetValue(RollSouceProperty);
            }
            set
            {
                SetValue(RollSouceProperty,value);
            }
        }



        /// <summary>
        /// 轮播速度
        /// </summary>
        public RollSpeedEnum RollSpeed
        {
            get
            {
                return (RollSpeedEnum)GetValue(RollSpeedProperty);
            }
            set
            {
                SetValue(RollSouceProperty, value);
            }
        }


        /// <summary>
        /// 轮播速度
        /// </summary>
        public enum RollSpeedEnum
        {
            /// <summary>
            /// 快速
            /// </summary>
            FAST,
            /// <summary>
            /// 中速
            /// </summary>
            Medium,
            /// <summary>
            /// 慢速
            /// </summary>
            SLOW
        }

        #endregion
3.加载图片集

这里通过异步的方式加载图片,图片通过Rectangle控件进行呈现,并通过设置DecodePixelHeight对图片Dpi进行限制,保证性能

private double speed = 2;//自动滚动速度值
private List<Rectangle> Rects;
        /// <summary>
        /// 载入图片集合
        /// </summary>
        private async void LoadPictures()
        {

            await Task.Run(()=> {

                this.Dispatcher.Invoke(new Action(()=> {
                    if (!string.IsNullOrEmpty(RollSouce) && Directory.Exists(RollSouce))
                    {
                        string[] files = Directory.GetFiles(RollSouce);
                        if (files.Length > 0)
                        {
                            Rects = new List<Rectangle>();
                            foreach (var item in files)
                            {
                                Rectangle rect = new Rectangle();
                                rect.Stroke = new SolidColorBrush(Colors.White);
                                rect.StrokeThickness = 3;
                                rect.RadiusX = 5;
                                rect.RadiusY = 5;
                                BitmapImage bit = new BitmapImage(new Uri(item));
                                bit.DecodePixelHeight = (int)this.Height;
                                rect.Height = this.Height;
                                double d = bit.PixelWidth * (this.Height / bit.PixelHeight);
                                rect.Width = Math.Round(d, 2);
                                rect.Fill = new ImageBrush(bit);
                                #region 拖拽事件绑定
                                rect.MouseLeftButtonDown += Rect_MouseLeftButtonDown;
                                rect.MouseLeftButtonUp += Rect_MouseLeftButtonUp;
                                rect.MouseMove += Rect_MouseMove;
                                
                                
                                #endregion

                                Rects.Add(rect);
                            }
                        }
                    }

                    
                    //根据容器的长度初始化元素
                    ShowItems();
                    //设置轮播速度
                    switch (RollSpeed)
                    {
                        case RollSpeedEnum.FAST:
                            speed = 3;
                            break;
                        case RollSpeedEnum.Medium:
                            speed = 1;
                            break;
                        case RollSpeedEnum.SLOW:
                            speed = 0.5;
                            break;
                        default:
                            speed = 2;
                            break;
                    }
                    //自动轮播动画处理,每秒60帧事件
                    CompositionTarget.Rendering += CompositionTarget_Rendering;



                }));
                

            });


            MsgBox.Visibility = Visibility.Collapsed;
        }

根据容器的长度初始化元素

        /// <summary>
        /// 初始化容器内的元素
        /// </summary>
        private void ShowItems()
        {
            if(Rects != null && Rects.Count > 0)
            {
                double currentLeft = 0;
                for(var i = 0; i < Rects.Count; i++)
                {
                    var item = Rects[i];
                    Canvas.SetLeft(item,currentLeft);
                    box.Children.Add(item);
                    currentLeft += item.Width;
                   
                }
            }
            
        }


4.设置自动轮播

通过CompositionTarget.Rendering的每秒60帧事件处理每一个Rectangle在Canvas中的位置来实现轮播

        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            if (isDown) return;

            //每秒60帧
            if (box.Children.Count > 0 && waitSecond == 3 && !isRuning)
            {
                //确保等待时间已到、惯性运动已结束、容器内元素存在
                foreach(Rectangle rect in box.Children)
                {
                    double currentLeft = Canvas.GetLeft(rect);
                    Canvas.SetLeft(rect, currentLeft - speed);
                }
                Check();
            }
                
            
        }

对于即将溢出容器视图和即将进入容器视图范围的元素要进行检查和处理

        /// <summary>
        /// 检查即将进入或溢出容器视图范围内的元素并处理
        /// </summary>
        /// <param name="isRightToLeft">是否从右往左滑动(鼠标动作)</param>
        private void Check(bool isRightToLeft = true)
        {
            double _left = 0d;

            var item = box.Children[0] as Rectangle;
            var _item = box.Children[box.Children.Count - 1] as Rectangle;
            if (isRightToLeft)
            {
                //从右往左滑动
                _left = Canvas.GetLeft(item);
                //当容器内第一个元素即将从容器左边界消失时处理
                if (_left <= -item.Width)
                {
                    //移除容器内首个元素并将其添加到容器末尾
                    box.Children.RemoveAt(0);
                    var lastItem = box.Children[box.Children.Count - 1] as Rectangle;
                    double lastLeft = Canvas.GetLeft(lastItem);
                    Canvas.SetLeft(item, lastLeft + lastItem.Width);
                    box.Children.Add(item);
                }
            }
            else
            {
                //从左往右滑动
                _left = Canvas.GetLeft(item);
                //当容器内第一个元素的位置在即将出现的最后一个元素的位置时处理
                if (_left >= -_item.Width)
                {
                    //移除容器内最后一个元素并将其添加到容器首位
                    box.Children.RemoveAt(box.Children.Count - 1);
                    var firstItem = box.Children[0] as Rectangle;
                    double firstLeft = Canvas.GetLeft(firstItem);
                    Canvas.SetLeft(_item, firstLeft - _item.Width);
                    box.Children.Insert(0, _item);
                }
            }

        }
5.鼠标拖拽滚动

鼠标模拟手指的滑屏操作,通过对MouseLeftButtonDown、MouseLeftButtonUp、MouseMove相关事件的处理对容器内元素的位置进行调整

        #region 拖拽事件处理
        Point start_point;//点击时坐标
        Point end_point;//离开时坐标

        private void Rect_MouseMove(object sender, MouseEventArgs e)
        {
            
            if (isDown)
            {
                end_point = e.GetPosition(box);
                w = end_point.X - start_point.X;
                fv = w;
                //移动处理
                Move(w);
                start_point = e.GetPosition(box);
            }


            
        }

        private void Rect_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Rectangle rect = sender as Rectangle;
            if (isDown)
            {
                isDown = false;
                
                var _fv = (int)fv;
                friction = ((_fv >> 31) * 2 + 1) * inertance;//根据力度套用公式计算出惯性大小,公式要记住
                num = Math.Abs(friction);

                timer.Start();

                rect.Cursor = Cursors.Arrow;
                //释放鼠标捕获
                rect.ReleaseMouseCapture();
                //开始计时,达到指定时间开始自动轮播
                waitSecond = 0;
                waitTimer.Start();
            }
            

        }

        private void Rect_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            timer.Stop();
            fv = 0;
            Rectangle rect = sender as Rectangle;
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                start_point = e.GetPosition(box);
                isDown = true;
                rect.Cursor = Cursors.Hand;
            }
                

        }

       
        #endregion

控件移动

       /// <summary>
        /// 控件移动
        /// </summary>
        /// <param name="distance">移动的距离</param>
        private void Move(double distance)
        {
            foreach (var rect in Rects)
            {
                double left = Canvas.GetLeft(rect) + distance;
                Canvas.SetLeft(rect, left);
            }
        }
6.为拖拽滚动加入惯性

参考了惯性运动的Js写法,相关链接:javascript 的惯性运动

        #region 惯性运动
        double inertance = 1.4; //惯性系数,越大,惯性越不明显,不能小于0
        double fv = 0d; //滑动的力度
        double friction = 0d;//惯性大小
        double num = 0;
        DispatcherTimer timer;//处理惯性运动的定时器
        bool isDown;//鼠标是否按下 true 按下 false 没按下
        double w = 0d;//移动的距离,有正负值,以横坐标为基准
        bool isRuning = false;//是否正在惯性运动 true 是 false 否

        private void Timer_Tick(object sender, EventArgs e)
        {
            isRuning = true;
            fv -= friction;//力度按 惯性的大小递减
            Move(fv);
            if (Math.Abs(fv) < num)
            {
                timer.Stop();
                isRuning = false;
            }

            if (w > 0)
            {
                Check(false);
            }
            else
            {
                Check();
            }


        }
        #endregion
7.两个定时器DispatcherTimer

到这里基本功能已经实现,程序中涉及到的两个定时器timer和waitTimer,一个是用来用来进行惯性运动处理的,一个是用来用来对切换轮播时等待的时长进行监听的。

private int waitSecond = 0;//由手动控制到自动轮播等待时长(超过3秒则开始继续滚动)
DispatcherTimer waitTimer;//等待时长监听的定时器


          
           //用来进行惯性运动的定时器
            timer = new DispatcherTimer();
            timer.Tick += Timer_Tick;
            timer.Interval = TimeSpan.FromMilliseconds(0.5);

            //用来进行等待时长监听的定时器
            waitTimer = new DispatcherTimer();
            waitTimer.Tick += WaitTimer_Tick;
            waitTimer.Interval = TimeSpan.FromSeconds(1);

        


       

惯性运动定时器事件处理

private void Timer_Tick(object sender, EventArgs e)
        {
            isRuning = true;
            fv -= friction;//力度按 惯性的大小递减
            Move(fv);
            if (Math.Abs(fv) < num)
            {
                timer.Stop();
                isRuning = false;
            }

            if (w > 0)
            {
                Check(false);
            }
            else
            {
                Check();
            }


        }

监听切换到自动轮播等待的时长定时器事件处理。为了手动操作和自动轮播正常切换,需要一个等待的间隔,这里设置了3秒的等待间隔

       private void WaitTimer_Tick(object sender, EventArgs e)
        {
            if (waitSecond == 3)
            {
                waitTimer.Stop();
                return;
            }
            waitSecond++;
        }
8.调用方式

封装好的控件可以通过如下方式直接引用,RollSouce设置图片源文件夹路径,RollSpeed设置自动轮播速度,容器的宽度和高度也是必须设置的

<local:RollBox RollSouce="D:\Roboat\素材\示例图片" Height="350" Width="800" RollSpeed="Medium" Margin="20">
</local:RollBox>
9.总结

1.点击单张图片预览文中没有做相关的实现,可以定义图片预览的委托通过外部的控件实现此功能
2.目前只实现了基本的功能,并没有实现效果的最优化,比如鼠标滑动操作时,惯性运动会有卡顿的现象,如果您有优化的方案欢迎指导!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容