mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-06-06 00:23:00 +08:00
[ColorPicker] Change zoom animation behavior (#11057)
* [ColorPicker] Change zoom animation behavior Makes the main window large enough to accommodate all zoom levels. * [ColorPicker] Change zoom window position logic Use PointFromScreen to calculate mouse position relative to window This requires a "visible" window, so use opacity to fake-hide window Window is still fully hidden when color picker closes * [ColorPicker] Extract and modify resize behavior Allows easier editing of animation easing/duration * Update expect.txt IAnimatable IEasing Co-authored-by: Clint Rutkas <clint@rutkas.com>
This commit is contained in:
parent
21247c0bb0
commit
9461909321
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@ -752,6 +752,7 @@ hxx
|
||||
Hyperlink
|
||||
IAction
|
||||
IActivated
|
||||
IAnimatable
|
||||
IApp
|
||||
IApplication
|
||||
IAppx
|
||||
@ -796,6 +797,7 @@ IDrop
|
||||
idx
|
||||
IDXGI
|
||||
IDYES
|
||||
IEasing
|
||||
IEnum
|
||||
IEnumerable
|
||||
IEnumerator
|
||||
|
@ -1,60 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Animation;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
|
||||
namespace ColorPicker.Behaviors
|
||||
{
|
||||
public class MoveWindowBehavior : Behavior<Window>
|
||||
{
|
||||
public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(double), typeof(MoveWindowBehavior), new PropertyMetadata(new PropertyChangedCallback(LeftPropertyChanged)));
|
||||
|
||||
private static void LeftPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var sender = ((MoveWindowBehavior)d).AssociatedObject;
|
||||
var move = new DoubleAnimation(sender.Left, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
|
||||
move.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
|
||||
sender.BeginAnimation(Window.LeftProperty, move, HandoffBehavior.Compose);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TopProperty = DependencyProperty.Register("Top", typeof(double), typeof(MoveWindowBehavior), new PropertyMetadata(new PropertyChangedCallback(TopPropertyChanged)));
|
||||
|
||||
private static void TopPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var sender = ((MoveWindowBehavior)d).AssociatedObject;
|
||||
var move = new DoubleAnimation(sender.Top, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
|
||||
move.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
|
||||
sender.BeginAnimation(Window.TopProperty, move, HandoffBehavior.Compose);
|
||||
}
|
||||
|
||||
public double Left
|
||||
{
|
||||
get
|
||||
{
|
||||
return (double)GetValue(LeftProperty);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
SetValue(LeftProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public double Top
|
||||
{
|
||||
get
|
||||
{
|
||||
return (double)GetValue(TopProperty);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
SetValue(TopProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,20 +11,55 @@ namespace ColorPicker.Behaviors
|
||||
{
|
||||
public class ResizeBehavior : Behavior<FrameworkElement>
|
||||
{
|
||||
// animation behavior variables
|
||||
// used when size is getting bigger
|
||||
private static readonly TimeSpan _animationTime = TimeSpan.FromMilliseconds(200);
|
||||
private static readonly IEasingFunction _easeFunction = new SineEase() { EasingMode = EasingMode.EaseOut };
|
||||
|
||||
// used when size is getting smaller
|
||||
private static readonly TimeSpan _animationTimeSmaller = _animationTime;
|
||||
private static readonly IEasingFunction _easeFunctionSmaller = new QuadraticEase() { EasingMode = EasingMode.EaseIn };
|
||||
|
||||
private static void CustomAnimation(DependencyProperty prop, IAnimatable sender, double fromValue, double toValue)
|
||||
{
|
||||
// if the animation is to/from a value of 0, it will cancel the current animation
|
||||
DoubleAnimation move = null;
|
||||
if (toValue > 0 && fromValue > 0)
|
||||
{
|
||||
// if getting bigger
|
||||
if (fromValue < toValue)
|
||||
{
|
||||
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTime), FillBehavior.Stop)
|
||||
{
|
||||
EasingFunction = _easeFunction,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTimeSmaller), FillBehavior.Stop)
|
||||
{
|
||||
EasingFunction = _easeFunctionSmaller,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// HandoffBehavior must be SnapshotAndReplace
|
||||
// Compose does not allow cancellation
|
||||
sender.BeginAnimation(prop, move, HandoffBehavior.SnapshotAndReplace);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty WidthProperty = DependencyProperty.Register("Width", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(WidthPropertyChanged)));
|
||||
|
||||
private static void WidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var sender = ((ResizeBehavior)d).AssociatedObject;
|
||||
var move = new DoubleAnimation(sender.Width, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
|
||||
move.Completed += (s, e1) =>
|
||||
{
|
||||
sender.BeginAnimation(FrameworkElement.WidthProperty, null);
|
||||
sender.Width = (double)e.NewValue;
|
||||
};
|
||||
|
||||
move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut };
|
||||
sender.BeginAnimation(FrameworkElement.WidthProperty, move, HandoffBehavior.Compose);
|
||||
var fromValue = sender.Width;
|
||||
var toValue = (double)e.NewValue;
|
||||
|
||||
// setting Width before animation prevents jumping
|
||||
sender.Width = toValue;
|
||||
CustomAnimation(FrameworkElement.WidthProperty, sender, fromValue, toValue);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(HeightPropertyChanged)));
|
||||
@ -32,15 +67,13 @@ namespace ColorPicker.Behaviors
|
||||
private static void HeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var sender = ((ResizeBehavior)d).AssociatedObject;
|
||||
var move = new DoubleAnimation(sender.Height, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
|
||||
move.Completed += (s, e1) =>
|
||||
{
|
||||
sender.BeginAnimation(FrameworkElement.HeightProperty, null);
|
||||
sender.Height = (double)e.NewValue;
|
||||
};
|
||||
|
||||
move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut };
|
||||
sender.BeginAnimation(FrameworkElement.HeightProperty, move, HandoffBehavior.Compose);
|
||||
var fromValue = sender.Height;
|
||||
var toValue = (double)e.NewValue;
|
||||
|
||||
// setting Height before animation prevents jumping
|
||||
sender.Height = toValue;
|
||||
CustomAnimation(FrameworkElement.HeightProperty, sender, fromValue, toValue);
|
||||
}
|
||||
|
||||
public double Width
|
||||
|
@ -18,33 +18,27 @@ namespace ColorPicker.Helpers
|
||||
[Export(typeof(ZoomWindowHelper))]
|
||||
public class ZoomWindowHelper
|
||||
{
|
||||
private const int ZoomWindowChangeDelayInMS = 50;
|
||||
private const int ZoomFactor = 2;
|
||||
private const int BaseZoomImageSize = 50;
|
||||
private const int MaxZoomLevel = 4;
|
||||
private const int MinZoomLevel = 0;
|
||||
|
||||
private static readonly Bitmap _bmp = new Bitmap(BaseZoomImageSize, BaseZoomImageSize, PixelFormat.Format32bppArgb);
|
||||
private static readonly Graphics _graphics = Graphics.FromImage(_bmp);
|
||||
|
||||
private readonly IZoomViewModel _zoomViewModel;
|
||||
private readonly AppStateHandler _appStateHandler;
|
||||
private readonly IThrottledActionInvoker _throttledActionInvoker;
|
||||
|
||||
private int _currentZoomLevel;
|
||||
private int _previousZoomLevel;
|
||||
|
||||
private ZoomWindow _zoomWindow;
|
||||
|
||||
private double _lastLeft;
|
||||
private double _lastTop;
|
||||
|
||||
private double _previousScaledX;
|
||||
private double _previousScaledY;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler, IThrottledActionInvoker throttledActionInvoker)
|
||||
public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler)
|
||||
{
|
||||
_zoomViewModel = zoomViewModel;
|
||||
_appStateHandler = appStateHandler;
|
||||
_throttledActionInvoker = throttledActionInvoker;
|
||||
_appStateHandler.AppClosed += AppStateHandler_AppClosed;
|
||||
_appStateHandler.AppHidden += AppStateHandler_AppClosed;
|
||||
}
|
||||
@ -73,7 +67,7 @@ namespace ColorPicker.Helpers
|
||||
{
|
||||
_currentZoomLevel = 0;
|
||||
_previousZoomLevel = 0;
|
||||
HideZoomWindow();
|
||||
HideZoomWindow(true);
|
||||
}
|
||||
|
||||
private void SetZoomImage(System.Windows.Point point)
|
||||
@ -89,28 +83,17 @@ namespace ColorPicker.Helpers
|
||||
{
|
||||
var x = (int)point.X - (BaseZoomImageSize / 2);
|
||||
var y = (int)point.Y - (BaseZoomImageSize / 2);
|
||||
var rect = new Rectangle(x, y, BaseZoomImageSize, BaseZoomImageSize);
|
||||
|
||||
using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb))
|
||||
{
|
||||
var g = Graphics.FromImage(bmp);
|
||||
g.CopyFromScreen(rect.Left, rect.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
|
||||
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
|
||||
|
||||
var bitmapImage = BitmapToImageSource(bmp);
|
||||
|
||||
_zoomViewModel.ZoomArea = bitmapImage;
|
||||
_zoomViewModel.ZoomFactor = 1;
|
||||
}
|
||||
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
|
||||
}
|
||||
else
|
||||
{
|
||||
var enlarge = (_currentZoomLevel - _previousZoomLevel) > 0 ? true : false;
|
||||
var currentZoomFactor = enlarge ? ZoomFactor : 1.0 / ZoomFactor;
|
||||
|
||||
_zoomViewModel.ZoomFactor *= currentZoomFactor;
|
||||
_zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1);
|
||||
}
|
||||
|
||||
ShowZoomWindow((int)point.X, (int)point.Y);
|
||||
ShowZoomWindow(point);
|
||||
}
|
||||
|
||||
private static BitmapSource BitmapToImageSource(Bitmap bitmap)
|
||||
@ -130,84 +113,67 @@ namespace ColorPicker.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private void HideZoomWindow()
|
||||
private void HideZoomWindow(bool fully = false)
|
||||
{
|
||||
if (_zoomWindow != null)
|
||||
{
|
||||
_zoomWindow.Hide();
|
||||
_zoomWindow.Opacity = 0;
|
||||
_zoomViewModel.DesiredWidth = 0;
|
||||
_zoomViewModel.DesiredHeight = 0;
|
||||
|
||||
if (fully)
|
||||
{
|
||||
_zoomWindow.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowZoomWindow(int x, int y)
|
||||
private void ShowZoomWindow(System.Windows.Point point)
|
||||
{
|
||||
if (_zoomWindow == null)
|
||||
_zoomWindow ??= new ZoomWindow
|
||||
{
|
||||
_zoomWindow = new ZoomWindow();
|
||||
_zoomWindow.Content = _zoomViewModel;
|
||||
_zoomWindow.Loaded += ZoomWindow_Loaded;
|
||||
_zoomWindow.IsVisibleChanged += ZoomWindow_IsVisibleChanged;
|
||||
}
|
||||
Content = _zoomViewModel,
|
||||
Opacity = 0,
|
||||
};
|
||||
|
||||
// we just started zooming, remember where we opened zoom window
|
||||
if (_currentZoomLevel == 1 && _previousZoomLevel == 0)
|
||||
{
|
||||
var dpi = MonitorResolutionHelper.GetCurrentMonitorDpi();
|
||||
_previousScaledX = x / dpi.DpiScaleX;
|
||||
_previousScaledY = y / dpi.DpiScaleY;
|
||||
}
|
||||
|
||||
_lastLeft = Math.Floor(_previousScaledX - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2));
|
||||
_lastTop = Math.Floor(_previousScaledY - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2));
|
||||
|
||||
var justShown = false;
|
||||
if (!_zoomWindow.IsVisible)
|
||||
{
|
||||
_zoomWindow.Left = _lastLeft;
|
||||
_zoomWindow.Top = _lastTop;
|
||||
_zoomViewModel.Height = BaseZoomImageSize;
|
||||
_zoomViewModel.Width = BaseZoomImageSize;
|
||||
_zoomWindow.Show();
|
||||
justShown = true;
|
||||
}
|
||||
|
||||
if (_zoomWindow.Opacity < 0.5)
|
||||
{
|
||||
var halfWidth = _zoomWindow.Width / 2;
|
||||
var halfHeight = _zoomWindow.Height / 2;
|
||||
|
||||
// usually takes 1-3 iterations to converge
|
||||
// 5 is just an arbitrary limit to prevent infinite loops
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
// mouse position relative to top left of _zoomWindow
|
||||
var scaledPoint = _zoomWindow.PointFromScreen(point);
|
||||
|
||||
var diffX = scaledPoint.X - halfWidth;
|
||||
var diffY = scaledPoint.Y - halfHeight;
|
||||
|
||||
// minimum difference that is considered important
|
||||
const double minDiff = 0.05;
|
||||
if (Math.Abs(diffX) < minDiff && Math.Abs(diffY) < minDiff)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_zoomWindow.Left += diffX;
|
||||
_zoomWindow.Top += diffY;
|
||||
}
|
||||
|
||||
// make sure color picker window is on top of just opened zoom window
|
||||
AppStateHandler.SetTopMost();
|
||||
_zoomWindow.Opacity = 1;
|
||||
}
|
||||
|
||||
// dirty hack - sometimes when we just show a window on a second monitor with different DPI,
|
||||
// window position is not set correctly on a first time, we need to "ping" it again to make it appear on the proper location
|
||||
if (justShown)
|
||||
{
|
||||
_zoomWindow.Left = _lastLeft + 1;
|
||||
_zoomWindow.Top = _lastTop + 1;
|
||||
SessionEventHelper.Event.ZoomUsed = true;
|
||||
}
|
||||
|
||||
_throttledActionInvoker.ScheduleAction(
|
||||
() =>
|
||||
{
|
||||
_zoomWindow.DesiredLeft = _lastLeft;
|
||||
_zoomWindow.DesiredTop = _lastTop;
|
||||
_zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
|
||||
_zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
|
||||
},
|
||||
ZoomWindowChangeDelayInMS);
|
||||
}
|
||||
|
||||
private void ZoomWindow_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
// need to set at this point again, to avoid issues moving between screens with different scaling
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
_zoomWindow.Left = _lastLeft;
|
||||
_zoomWindow.Top = _lastTop;
|
||||
}
|
||||
}
|
||||
|
||||
private void ZoomWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// need to call it again at load time, because it does was not dpi aware at the first time of Show() call
|
||||
_zoomWindow.Left = _lastLeft;
|
||||
_zoomWindow.Top = _lastTop;
|
||||
_zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
|
||||
_zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
|
||||
}
|
||||
|
||||
private void AppStateHandler_AppClosed(object sender, EventArgs e)
|
||||
|
@ -11,6 +11,8 @@
|
||||
Focusable="False">
|
||||
|
||||
<Border x:Name="WindowBorder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{DynamicResource WindowBorderBrush}"
|
||||
Margin="12"
|
||||
BorderThickness="1"
|
||||
|
@ -8,7 +8,6 @@
|
||||
mc:Ignorable="d"
|
||||
Title="Zoom window"
|
||||
WindowStyle="None"
|
||||
SizeToContent="WidthAndHeight"
|
||||
Topmost="True"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
@ -17,6 +16,5 @@
|
||||
Focusable="False">
|
||||
<e:Interaction.Behaviors>
|
||||
<behaviors:CloseZoomWindowBehavior/>
|
||||
<behaviors:MoveWindowBehavior Left="{Binding DesiredLeft, Mode=TwoWay}" Top="{Binding DesiredTop}"/>
|
||||
</e:Interaction.Behaviors>
|
||||
</Window>
|
||||
|
@ -10,50 +10,16 @@ namespace ColorPicker
|
||||
/// <summary>
|
||||
/// Interaction logic for ZoomWindow.xaml
|
||||
/// </summary>
|
||||
public partial class ZoomWindow : Window, INotifyPropertyChanged
|
||||
public partial class ZoomWindow : Window
|
||||
{
|
||||
private double _left;
|
||||
private double _top;
|
||||
|
||||
public ZoomWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
public double DesiredLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
return _left;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_left = value;
|
||||
NotifyPropertyChanged(nameof(DesiredLeft));
|
||||
}
|
||||
}
|
||||
|
||||
public double DesiredTop
|
||||
{
|
||||
get
|
||||
{
|
||||
return _top;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_top = value;
|
||||
NotifyPropertyChanged(nameof(DesiredTop));
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private void NotifyPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
// must be large enough to fit max zoom
|
||||
Width = 500;
|
||||
Height = 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user