效果预览
技术需求
要做一个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.目前只实现了基本的功能,并没有实现效果的最优化,比如鼠标滑动操作时,惯性运动会有卡顿的现象,如果您有优化的方案欢迎指导!