WPF 控件提供了 ScrollViewer 来实现滚动视图。ScrollViewer 的 PageDown、PageUp、PageLeft、PageRight 方法可以滚动一个视图大小的尺寸,但是 ScrollViewer 滚动时比较僵硬,没有动画效果。
平滑滚动控件命名为 RWrapPanel,继承 WrapPanel类和 IScrollInfo 接口。将WrapPanel 放入 ScrollViewer 并将 ScrollViewer 的 CanContentScroll 设置为 True(即按逻辑单元滚动),RWrapPanel 控件内部自己管理滚动逻辑。
平滑滚动的WrapPanel
public class RWrapPanel : WrapPanel, IScrollInfo {
const double lineOffset = 30;
const double wheelOffset = 90;
const double duration = 0.2;
TranslateTransform transForm;
public RWrapPanel() {
transForm = new TranslateTransform();
this.RenderTransform = transForm;
}
#region Layout
Size screenSize;
Size totalSize;
protected override Size MeasureOverride(Size constraint) {
this.screenSize = constraint;
if (this.Orientation == Orientation.Horizontal) {
constraint = new Size(double.PositiveInfinity, constraint.Height);
} else {
constraint = new Size(constraint.Width, double.PositiveInfinity);
}
totalSize = base.MeasureOverride(constraint);
return totalSize;
}
protected override Size ArrangeOverride(Size finalSize) {
var size = base.ArrangeOverride(finalSize);
if (ScrollOwner != null) {
if (this.Orientation == Orientation.Horizontal) {
var xOffsetAnimation = new DoubleAnimation() {
To = -HorizontalOffset,
Duration = TimeSpan.FromSeconds(duration),
};
transForm.BeginAnimation(TranslateTransform.XProperty, xOffsetAnimation);
} else {
var yOffsetAnimation = new DoubleAnimation() {
To = -VerticalOffset,
Duration = TimeSpan.FromSeconds(duration),
};
transForm.BeginAnimation(TranslateTransform.YProperty, yOffsetAnimation);
}
ScrollOwner.InvalidateScrollInfo();
}
return screenSize;
}
#endregion
///
/// 修正 value 的值, 如果 value 超出 value1~value2 范围,则去范围的端点值
///
///
///
///
///
double range(double value, double value1, double value2) {
var min = Math.Min(value1, value2);
var max = Math.Max(value1, value2);
value = Math.Max(value, min);
value = Math.Min(value, max);
return value;
}
void appendOffset(double x, double y) {
var offset = new Vector(HorizontalOffset + x, VerticalOffset + y);
offset.Y = range(offset.Y, 0, totalSize.Height - screenSize.Height);
offset.X = range(offset.X, 0, totalSize.Width - screenSize.Width);
HorizontalOffset = offset.X;
VerticalOffset = offset.Y;
InvalidateArrange();
}
#region IScrollInfo
public bool CanVerticallyScroll { get; set; }
public bool CanHorizontallyScroll { get; set; }
public double ExtentWidth => totalSize.Width;
public double ExtentHeight => totalSize.Height;
public double ViewportWidth => screenSize.Width;
public double ViewportHeight => screenSize.Height;
public double HorizontalOffset { get; private set; }
public double VerticalOffset { get; private set; }
public ScrollViewer ScrollOwner { get; set; }
public void LineDown() {
appendOffset(0, lineOffset);
}
public void LineLeft() {
appendOffset(-lineOffset, 0);
}
public void LineRight() {
appendOffset(lineOffset, 0);
}
public void LineUp() {
appendOffset(0, -lineOffset);
}
public Rect MakeVisible(Visual visual, Rect rectangle) {
throw new NotImplementedException();
}
public void MouseWheelDown() {
appendOffset(0, wheelOffset);
}
public void MouseWheelLeft() {
appendOffset(-wheelOffset, 0);
}
public void MouseWheelRight() {
appendOffset(wheelOffset, 0);
}
public void MouseWheelUp() {
appendOffset(0, -wheelOffset);
}
public void PageDown() {
appendOffset(0, screenSize.Height);
}
public void PageLeft() {
appendOffset(-screenSize.Width, 0);
}
public void PageRight() {
appendOffset(screenSize.Width, 0);
}
public void PageUp() {
appendOffset(0, -screenSize.Height);
}
public void SetHorizontalOffset(double offset) {
appendOffset(offset - HorizontalOffset, VerticalOffset);
}
public void SetVerticalOffset(double offset) {
appendOffset(HorizontalOffset, offset - VerticalOffset);
}
#endregion
}
后台代码
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Left_Click(object sender, RoutedEventArgs e) {
this.panel.PageLeft();
}
private void Right_Click(object sender, RoutedEventArgs e) {
this.panel.PageRight();
}
}