一、目的:封装一个具有拖动效果和缓冲效果的ScrollView
二、实现
1、在ScrollView中通过鼠标拖动可以平移
2、在ScrollView中滚动时具有缓冲效果
3、封装在行为中直接附加
三、示例
四、实现过程
1、创建拖动行为,如下
/// <summary> ScrollViewer带有鼠标拖动和触摸拖动效果 </summary>
public class ScrollViewMouseDragBehavior : Behavior<ScrollViewer>
{
ScrollViewer scrollViewer;
protected override void OnAttached()
{
scrollViewer = AssociatedObject as ScrollViewer;
if (scrollViewer == null) return;
scrollViewer.PreviewMouseDown += AssociatedObject_PreviewMouseDown;
scrollViewer.PreviewMouseMove += AssociatedObject_PreviewMouseMove;
scrollViewer.TouchDown += AssociatedObject_TouchDown;
scrollViewer.PreviewTouchMove += AssociatedObject_PreviewTouchMove;
}
protected override void OnDetaching()
{
if (scrollViewer == null) return;
scrollViewer.PreviewMouseDown -= AssociatedObject_PreviewMouseDown;
scrollViewer.PreviewMouseMove -= AssociatedObject_PreviewMouseMove;
scrollViewer.TouchDown -= AssociatedObject_TouchDown;
scrollViewer.PreviewTouchMove -= AssociatedObject_PreviewTouchMove;
}
/// <summary> 是否到达顶部 </summary>
public bool IsTopped
{
get { return (bool)GetValue(IsToppedProperty); }
set { SetValue(IsToppedProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsToppedProperty =
DependencyProperty.Register("IsTopped", typeof(bool), typeof(ScrollViewMouseDragBehavior), new PropertyMetadata(default(bool), (d, e) =>
{
ScrollViewMouseDragBehavior control = d as ScrollViewMouseDragBehavior;
if (control == null) return;
//bool config = e.NewValue as bool;
}));
/// <summary> 是否到达底部 </summary>
public bool IsBottomed
{
get { return (bool)GetValue(IsBottomedProperty); }
set { SetValue(IsBottomedProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsBottomedProperty =
DependencyProperty.Register("IsBottomed", typeof(bool), typeof(ScrollViewMouseDragBehavior), new PropertyMetadata(default(bool), (d, e) =>
{
ScrollViewMouseDragBehavior control = d as ScrollViewMouseDragBehavior;
if (control == null) return;
//bool config = e.NewValue as bool;
}));
void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
this.ScrollMove(e.Source);
}
void AssociatedObject_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
this.GetLast(e.Source);
}
private void AssociatedObject_PreviewTouchMove(object sender, TouchEventArgs e)
{
this.ScrollMove(e.Source);
}
private void AssociatedObject_TouchDown(object sender, TouchEventArgs e)
{
this.GetLast(e.Source);
}
void GetLast(object source)
{
Point pp = Mouse.GetPosition(source as FrameworkElement);
Point temp = (source as FrameworkElement).PointToScreen(pp);
last = temp;
}
Point last;
void ScrollMove(object source)
{
Point pp = Mouse.GetPosition(source as FrameworkElement);
Point temp = (source as FrameworkElement).PointToScreen(pp);
double y = temp.Y - last.Y;
this.scrollViewer.ScrollToVerticalOffset(this.scrollViewer.VerticalOffset - y);
last = temp;
this.CheckPosition();
}
void CheckPosition()
{
if (this.scrollViewer.VerticalOffset == 0)
{
this.IsTopped = true;
}
else if (IsVerticalScrollBarAtBottom(this.scrollViewer))
{
this.IsBottomed = true;
}
else
{
this.IsTopped = false;
this.IsBottomed = false;
}
}
public bool IsVerticalScrollBarAtBottom(ScrollViewer s)
{
bool isAtButtom = false;
double dVer = s.VerticalOffset;
double dViewport = s.ViewportHeight;
double dExtent = s.ExtentHeight;
if (dVer != 0)
{
if (dVer + dViewport == dExtent)
{
isAtButtom = true;
}
else
{
isAtButtom = false;
}
}
else
{
isAtButtom = false;
}
return isAtButtom;
}
}
2、创建缓冲行为如下
/// <summary>
/// Behavior that watches an element (or a set of elements) for layout changes, and moves the element smoothly to the new position when needed.
/// This behavior does not animate the size or visibility of an element; it only animates the offset of that element within its parent container.
/// </summary>
public sealed class FluidMoveBehavior : FluidMoveBehaviorBase
{
/// <summary>
/// The duration of the move.
/// </summary>
public Duration Duration
{
get { return (Duration)this.GetValue(DurationProperty); }
set { this.SetValue(DurationProperty, value); }
}
/// <summary>
/// Dependency property for the duration of the move.
/// </summary>
public static readonly DependencyProperty DurationProperty = DependencyProperty.Register("Duration", typeof(Duration), typeof(FluidMoveBehavior), new PropertyMetadata(new Duration(TimeSpan.FromSeconds(1.0))));
/// <summary>
/// Spawning point for this item.
/// </summary>
public TagType InitialTag
{
get { return (TagType)this.GetValue(InitialTagProperty); }
set { this.SetValue(InitialTagProperty, value); }
}
/// <summary>
/// Dependency property for the tag type to use just before the object is loaded.
/// </summary>
public static readonly DependencyProperty InitialTagProperty = DependencyProperty.Register("InitialTag", typeof(TagType), typeof(FluidMoveBehavior), new PropertyMetadata(TagType.Element));
/// <summary>
/// Extra path to add to the binding when TagType is specified.
/// </summary>
public string InitialTagPath
{
get { return (string)this.GetValue(InitialTagPathProperty); }
set { this.SetValue(InitialTagPathProperty, value); }
}
/// <summary>
/// Dependency property for the extra path to add to the binding when UsaBindingAsTag is true.
/// </summary>
public static readonly DependencyProperty InitialTagPathProperty = DependencyProperty.Register("InitialTagPath", typeof(string), typeof(FluidMoveBehavior), new PropertyMetadata(String.Empty));
/// <summary>
/// Identity tag used to detect element motion between containers.
/// </summary>
private static readonly DependencyProperty initialIdentityTagProperty = DependencyProperty.RegisterAttached("InitialIdentityTag", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
private static object GetInitialIdentityTag(DependencyObject obj) { return obj.GetValue(initialIdentityTagProperty); }
private static void SetInitialIdentityTag(DependencyObject obj, object value) { obj.SetValue(initialIdentityTagProperty, value); }
/// <summary>
/// Flag that says whether elements are allowed to float above their containers (in a Popup or Adorner) when changing containers.
/// </summary>
public bool FloatAbove
{
get { return (bool)this.GetValue(FloatAboveProperty); }
set { this.SetValue(FloatAboveProperty, value); }
}
/// <summary>
/// Dependency property for the FloatAbove flag.
/// </summary>
public static readonly DependencyProperty FloatAboveProperty = DependencyProperty.Register("FloatAbove", typeof(bool), typeof(FluidMoveBehavior), new PropertyMetadata(true));
/// <summary>
/// EasingFunction to use for the horizontal component of the move.
/// </summary>
public IEasingFunction EaseX
{
get { return (IEasingFunction)this.GetValue(EaseXProperty); }
set { this.SetValue(EaseXProperty, value); }
}
/// <summary>
/// Dependency property for the EasingFunction to use for the horizontal component of the move.
/// </summary>
public static readonly DependencyProperty EaseXProperty = DependencyProperty.Register("EaseX", typeof(IEasingFunction), typeof(FluidMoveBehavior), new PropertyMetadata(null));
/// <summary>
/// EasingFunction to use for the vertical component of the move.
/// </summary>
public IEasingFunction EaseY
{
get { return (IEasingFunction)this.GetValue(EaseYProperty); }
set { this.SetValue(EaseYProperty, value); }
}
/// <summary>
/// Dependency property for the EasingFunction to use for the vertical component of the move.
/// </summary>
public static readonly DependencyProperty EaseYProperty = DependencyProperty.Register("EaseY", typeof(IEasingFunction), typeof(FluidMoveBehavior), new PropertyMetadata(null));
/// <summary>
/// Remember the popup/adorner being used, in case of element motion between containers when FloatAbove is true.
/// </summary>
private static readonly DependencyProperty overlayProperty = DependencyProperty.RegisterAttached("Overlay", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
private static object GetOverlay(DependencyObject obj) { return obj.GetValue(overlayProperty); }
private static void SetOverlay(DependencyObject obj, object value) { obj.SetValue(overlayProperty, value); }
/// <summary>
/// Opacity cache used when floating a Popup.
/// </summary>
private static readonly DependencyProperty cacheDuringOverlayProperty = DependencyProperty.RegisterAttached("CacheDuringOverlay", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
private static object GetCacheDuringOverlay(DependencyObject obj) { return obj.GetValue(cacheDuringOverlayProperty); }
private static void SetCacheDuringOverlay(DependencyObject obj, object value) { obj.SetValue(cacheDuringOverlayProperty, value); }
/// <summary>
/// Marks the animation transform.
/// </summary>
private static readonly DependencyProperty hasTransformWrapperProperty = DependencyProperty.RegisterAttached("HasTransformWrapper", typeof(bool), typeof(FluidMoveBehavior), new PropertyMetadata(false));
private static bool GetHasTransformWrapper(DependencyObject obj) { return (bool)obj.GetValue(hasTransformWrapperProperty); }
private static void SetHasTransformWrapper(DependencyObject obj, bool value) { obj.SetValue(hasTransformWrapperProperty, value); }
private static Dictionary<object, Storyboard> transitionStoryboardDictionary = new Dictionary<object, Storyboard>();
protected override bool ShouldSkipInitialLayout
{
get
{
return base.ShouldSkipInitialLayout || (this.InitialTag == TagType.DataContext);
}
}
protected override void EnsureTags(FrameworkElement child)
{
base.EnsureTags(child);
// If we are going to use a binding for the tag, make sure we have one set up.
if (this.InitialTag == TagType.DataContext)
{
object tagValue = child.ReadLocalValue(initialIdentityTagProperty);
if (!(tagValue is BindingExpression))
{
child.SetBinding(initialIdentityTagProperty, new Binding(this.InitialTagPath));
}
}
}
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Trying to keep the number of function parameters down to a minimum.")]
internal override void UpdateLayoutTransitionCore(FrameworkElement child, FrameworkElement root, object tag, TagData newTagData)
{
TagData tagData;
Rect previousRect;
bool parentChange = false;
bool usingBeforeLoaded = false;
object initialTag = GetInitialIdentityTag(child);
// Locate the previous tag, and the parent-relative previous rect. The previous rect is computed using the app-relative rect if switching parents.
// Note that we do not use the app-relative rect all time time, because when the parent itself moves, it accounts for all the motion and we do not have to.
bool gotData = TagDictionary.TryGetValue(tag, out tagData);
// if spawn point has changed then throw away the old one
if (gotData && tagData.InitialTag != initialTag)
{
gotData = false;
TagDictionary.Remove(tag);
}
if (!gotData)
{
TagData spawnData;
if (initialTag != null && TagDictionary.TryGetValue(initialTag, out spawnData))
{
previousRect = TranslateRect(spawnData.AppRect, root, newTagData.Parent);
parentChange = true;
usingBeforeLoaded = true;
}
else
{
previousRect = Rect.Empty;
}
tagData = new TagData() { ParentRect = Rect.Empty, AppRect = Rect.Empty, Parent = newTagData.Parent, Child = child, Timestamp = DateTime.Now, InitialTag = initialTag };
TagDictionary.Add(tag, tagData);
}
else if (tagData.Parent != VisualTreeHelper.GetParent(child))
{
previousRect = TranslateRect(tagData.AppRect, root, newTagData.Parent);
parentChange = true;
}
else
{
previousRect = tagData.ParentRect;
}
FrameworkElement originalChild = child;
if ((!FluidMoveBehavior.IsEmptyRect(previousRect) && !FluidMoveBehavior.IsEmptyRect(newTagData.ParentRect)) && (!IsClose(previousRect.Left, newTagData.ParentRect.Left) || !IsClose(previousRect.Top, newTagData.ParentRect.Top)) ||
(child != tagData.Child && transitionStoryboardDictionary.ContainsKey(tag)))
{
Rect currentRect = previousRect;
bool forceFloatAbove = false;
// If this element was animating before, append its current transform to the start position and kill the old animation.
// Note that in an overlay scenario, the animation is on the image in the overlay.
Storyboard oldTransitionStoryboard = null;
if (transitionStoryboardDictionary.TryGetValue(tag, out oldTransitionStoryboard))
{
object tagOverlay = GetOverlay(tagData.Child);
AdornerContainer adornerContainer = (AdornerContainer)tagOverlay;
forceFloatAbove = (tagOverlay != null); // if floating before, we need to keep floating
FrameworkElement elementWithTransform = tagData.Child;
if (tagOverlay != null)
{
Canvas overlayCanvas = adornerContainer.Child as Canvas;
if (overlayCanvas != null)
{
elementWithTransform = overlayCanvas.Children[0] as FrameworkElement;
}
}
// if we're picking a specific starting point, don't append this transform
if (!usingBeforeLoaded)
{
Transform transform = GetTransform(elementWithTransform);
currentRect = transform.TransformBounds(currentRect);
}
transitionStoryboardDictionary.Remove(tag);
oldTransitionStoryboard.Stop();
oldTransitionStoryboard = null;
RemoveTransform(elementWithTransform);
if (tagOverlay != null)
{
System.Windows.Documents.AdornerLayer.GetAdornerLayer(root).Remove(adornerContainer);
TransferLocalValue(tagData.Child, FluidMoveBehavior.cacheDuringOverlayProperty, FrameworkElement.RenderTransformProperty);
SetOverlay(tagData.Child, null);
}
}
object overlay = null;
// If we need to float this element, then we have to:
// 1. Take a picture of it
// 2. Put that picture in an Image in a popup
// 3. Hide the original element (opacity=0 so we do not disturb layout)
// 4. Animate the image
// 5. Keep track of all the info we need to unwind this later
if (forceFloatAbove || (parentChange && this.FloatAbove))
{
Canvas canvas = new Canvas() { Width = newTagData.ParentRect.Width, Height = newTagData.ParentRect.Height, IsHitTestVisible = false };
Rectangle rectangle = new Rectangle() { Width = newTagData.ParentRect.Width, Height = newTagData.ParentRect.Height, IsHitTestVisible = false };
rectangle.Fill = new VisualBrush(child);
canvas.Children.Add(rectangle);
AdornerContainer adornerContainer = new AdornerContainer(child) { Child = canvas };
overlay = adornerContainer;
// remember this overlay so we can get info from it
SetOverlay(originalChild, overlay);
System.Windows.Documents.AdornerLayer adorners = System.Windows.Documents.AdornerLayer.GetAdornerLayer(root);
adorners.Add(adornerContainer);
// Note: Not using this approach currently because the bitmap is not ready yet
// To remove use of VisualBrush, have to fill in bitmap after a render
//RenderTargetBitmap bitmap = new RenderTargetBitmap((int)child.ActualWidth, (int)child.ActualHeight, 96, 96, PixelFormats.Pbgra32);
//bitmap.Render(parent);
//image.Source = bitmap;
// can't animate this or it will flash, have to set the value outright
TransferLocalValue(child, FrameworkElement.RenderTransformProperty, FluidMoveBehavior.cacheDuringOverlayProperty);
child.RenderTransform = new TranslateTransform(-10000, -10000);
canvas.RenderTransform = new TranslateTransform(10000, 10000);
// change value here so that the animations will be applied to the image
child = rectangle;
}
// OK, now build the actual animation
Rect parentRect = newTagData.ParentRect;
Storyboard transitionStoryboard = CreateTransitionStoryboard(child, usingBeforeLoaded, ref parentRect, ref currentRect);
// Put this storyboard in the running dictionary so we can detect reentrancy
transitionStoryboardDictionary.Add(tag, transitionStoryboard);
transitionStoryboard.Completed += delegate (object sender, EventArgs e)
{
Storyboard currentlyRunningStoryboard;
if (transitionStoryboardDictionary.TryGetValue(tag, out currentlyRunningStoryboard) && currentlyRunningStoryboard == transitionStoryboard)
{
transitionStoryboardDictionary.Remove(tag);
transitionStoryboard.Stop();
RemoveTransform(child);
child.InvalidateMeasure();
if (overlay != null)
{
System.Windows.Documents.AdornerLayer.GetAdornerLayer(root).Remove((AdornerContainer)overlay);
TransferLocalValue(originalChild, FluidMoveBehavior.cacheDuringOverlayProperty, FrameworkElement.RenderTransformProperty);
SetOverlay(originalChild, null);
}
}
};
transitionStoryboard.Begin();
}
// Store current tag status
tagData.ParentRect = newTagData.ParentRect;
tagData.AppRect = newTagData.AppRect;
tagData.Parent = newTagData.Parent;
tagData.Child = newTagData.Child;
tagData.Timestamp = newTagData.Timestamp;
}
private Storyboard CreateTransitionStoryboard(FrameworkElement child, bool usingBeforeLoaded, ref Rect layoutRect, ref Rect currentRect)
{
Duration duration = this.Duration;
Storyboard transitionStoryboard = new Storyboard();
transitionStoryboard.Duration = duration;
double xScaleFrom = (!usingBeforeLoaded || layoutRect.Width == 0.0) ? 1.0 : (currentRect.Width / layoutRect.Width);
double yScaleFrom = (!usingBeforeLoaded || layoutRect.Height == 0.0) ? 1.0 : (currentRect.Height / layoutRect.Height);
double xFrom = currentRect.Left - layoutRect.Left;
double yFrom = currentRect.Top - layoutRect.Top;
TransformGroup transform = new TransformGroup();
transform.Children.Add(new ScaleTransform() { ScaleX = xScaleFrom, ScaleY = yScaleFrom });
transform.Children.Add(new TranslateTransform() { X = xFrom, Y = yFrom });
AddTransform(child, transform);
string prefix = "(FrameworkElement.RenderTransform).";
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup != null && GetHasTransformWrapper(child))
{
prefix += "(TransformGroup.Children)[" + (transformGroup.Children.Count - 1) + "].";
}
if (usingBeforeLoaded)
{
if (xScaleFrom != 1.0)
{
DoubleAnimation xScaleAnimation = new DoubleAnimation() { Duration = duration, From = xScaleFrom, To = 1.0 };
Storyboard.SetTarget(xScaleAnimation, child);
Storyboard.SetTargetProperty(xScaleAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[0].(ScaleTransform.ScaleX)", new object[0]));
xScaleAnimation.EasingFunction = this.EaseX;
transitionStoryboard.Children.Add(xScaleAnimation);
}
if (yScaleFrom != 1.0)
{
DoubleAnimation yScaleAnimation = new DoubleAnimation() { Duration = duration, From = yScaleFrom, To = 1.0 };
Storyboard.SetTarget(yScaleAnimation, child);
Storyboard.SetTargetProperty(yScaleAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[0].(ScaleTransform.ScaleY)", new object[0]));
yScaleAnimation.EasingFunction = this.EaseY;
transitionStoryboard.Children.Add(yScaleAnimation);
}
}
if (xFrom != 0.0)
{
DoubleAnimation xAnimation = new DoubleAnimation() { Duration = duration, From = xFrom, To = 0.0 };
Storyboard.SetTarget(xAnimation, child);
Storyboard.SetTargetProperty(xAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[1].(TranslateTransform.X)", new object[0]));
xAnimation.EasingFunction = this.EaseX;
transitionStoryboard.Children.Add(xAnimation);
}
if (yFrom != 0.0)
{
DoubleAnimation yAnimation = new DoubleAnimation() { Duration = duration, From = yFrom, To = 0.0 };
Storyboard.SetTarget(yAnimation, child);
Storyboard.SetTargetProperty(yAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[1].(TranslateTransform.Y)", new object[0]));
yAnimation.EasingFunction = this.EaseY;
transitionStoryboard.Children.Add(yAnimation);
}
return transitionStoryboard;
}
private static void AddTransform(FrameworkElement child, Transform transform)
{
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup == null)
{
transformGroup = new TransformGroup();
transformGroup.Children.Add(child.RenderTransform);
child.RenderTransform = transformGroup;
SetHasTransformWrapper(child, true);
}
transformGroup.Children.Add(transform);
}
private static Transform GetTransform(FrameworkElement child)
{
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup != null && transformGroup.Children.Count > 0)
{
return transformGroup.Children[transformGroup.Children.Count - 1];
}
else
{
return new TranslateTransform();
}
}
private static void RemoveTransform(FrameworkElement child)
{
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup != null)
{
if (GetHasTransformWrapper(child))
{
child.RenderTransform = transformGroup.Children[0];
SetHasTransformWrapper(child, false);
}
else
{
transformGroup.Children.RemoveAt(transformGroup.Children.Count - 1);
}
}
}
private static void TransferLocalValue(FrameworkElement element, DependencyProperty source, DependencyProperty dest)
{
object value = element.ReadLocalValue(source);
BindingExpressionBase bindingExpressionBase = value as BindingExpressionBase;
if (bindingExpressionBase != null)
{
element.SetBinding(dest, bindingExpressionBase.ParentBindingBase);
}
else if (value == DependencyProperty.UnsetValue)
{
element.ClearValue(dest);
}
else
{
element.SetValue(dest, element.GetAnimationBaseValue(source));
}
element.ClearValue(source);
}
private static bool IsClose(double a, double b)
{
return (Math.Abs((double)(a - b)) < 1E-07);
}
private static bool IsEmptyRect(Rect rect)
{
return ((rect.IsEmpty || double.IsNaN(rect.Left)) || double.IsNaN(rect.Top));
}
}
3、在XAML按如下加入行为
<ScrollViewer>
<h:Interaction.Behaviors>
<h:ScrollViewMouseDragBehavior/>
</h:Interaction.Behaviors>
<StackPanel>
<h:Interaction.Behaviors>
<h:FluidMoveBehavior AppliesTo="Self" Duration="00:00:01" FloatAbove="True"/>
</h:Interaction.Behaviors>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
由上面添加过程即可实现示例带有鼠标或触摸拖动效果的ScrollView控件