-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding Composition XAML Helpers #1567
Conversation
That's just beyond amazing. Will review ASAP. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of all the static helper classes. It feels like an odd place to put all the attached properties, rather on the classes that uses them, and I think there's several places here that runs huge risks for memory leaks. Lastly the static seems to prevent you from having multiple frames which isn't uncommon at all.
/// Identifies the <see cref="Target"/> property | ||
/// </summary> | ||
public static readonly DependencyProperty TargetProperty = | ||
DependencyProperty.Register("Target", typeof(string), typeof(AnimationBase), new PropertyMetadata(string.Empty, OnAnimationPropertyChanged)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the default string.Empty and not null? (like ImplicitTargetProperty
)
Target
should use nameof(Target)
(same for DPs below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
{ | ||
get | ||
{ | ||
var collection = (KeyFrameCollection)GetValue(KeyFramesProperty); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should never have code in get/setters of dependency properties (there's no guarantee this is called if you're using GetValue). Instead initialize in the constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or in the KeyFramesProperty
itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This removes the need to specify a KeyFrameCollection in XAML. For example, with this, I can add the keyframes like so:
<Animation>
<KeyFrame />
<KeyFrame />
</Animation>
Otherwise, I would have to specify the collection:
<Animation>
<KeyFrameCollection>
<KeyFrame />
<KeyFrame />
</KeyFrameCollection>
</Animation>
I could initialize a new KeyFrameCollection in the constructor, but why have an instance if it's not needed? Open to other suggestions?
/// Gets a value indicating whether Universal API Contract 4 is available on the current device | ||
/// where Implicit Show/Hide animations and Translation property on <see cref="Visual"/> are available | ||
/// </summary> | ||
public static bool IsCreatorsUpdateOrAbove => (bool)(_isCreatorsUpdateOrAbove ?? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like an odd place for this property. Also I think the property should convey whether a feature is supported rather than what version this is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree and Disagree. For performance reasons (which I realize is not much), it feels weird having to check each API in a separate property. Agree it doesn't fit in this class, I'll move it out to an internal helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to internal class
/// </summary> | ||
/// <param name="d">The animation where a property has changed</param> | ||
/// <param name="e">The details about the property change</param> | ||
protected static void OnAnimationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A protected static method seems odd. Why not have an instance method that's called that you would be overriding?Also you'd be overriding one method that's called for multiple different dependency properties. It would be cleaner to have one method per property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why one method per property if the action is not specific to a property and can all be done in a single method? It's not cleaner and seems more error prone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would agree with his first statement to create an instance method to follow proper event handling
protected virtual void OnAnimationChanged(EventArgs e)
{
AnimationChanged?.Invoke(this, EventArgs.Empty);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// <param name="e">The details about the property change</param> | ||
protected static void OnAnimationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | ||
{ | ||
(d as AnimationBase).AnimationChanged?.Invoke(d, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't specify null, but EventArgs.Empty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will Fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
{ | ||
animationCollection.Element = element; | ||
animationCollection.AnimationCollectionChanged -= ShowCollectionChanged; | ||
animationCollection.AnimationCollectionChanged += ShowCollectionChanged; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why - then + ? This seems like a "just in case" kind of thing.
Also these event handlers should be weak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, the same AnimationCollection can be added on multiple elements. But there is no need to do this in those cases, so I'll remove these.
{ | ||
animationCollection.Element = element; | ||
animationCollection.AnimationCollectionChanged -= HideCollectionChanged; | ||
animationCollection.AnimationCollectionChanged += HideCollectionChanged; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. Don't -/+ and should be weak
{ | ||
animationCollection.Element = element; | ||
animationCollection.AnimationCollectionChanged -= AnimationsCollectionChanged; | ||
animationCollection.AnimationCollectionChanged += AnimationsCollectionChanged; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Friends don't let friends do this :) And weak please
@@ -67,7 +67,7 @@ public partial class DropShadowPanel | |||
/// On platforms not supporting drop shadows, this control has no effect. | |||
/// </remarks> | |||
public static bool IsSupported => | |||
(!ControlHelpers.IsRunningInLegacyDesignerMode) && ApiInformation.IsTypePresent("Windows.UI.Composition.DropShadow"); // SDK >= 14393 | |||
(!DesignTimeHelpers.IsRunningInLegacyDesignerMode) && ApiInformation.IsTypePresent("Windows.UI.Composition.DropShadow"); // SDK >= 14393 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks unrelated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the design time helpers you did to the base UI package so I can use them outside of the controls package (makes no sense to implement the same thing multiple times). Had to update all references to it.
/// </summary> | ||
internal static partial class ControlHelpers | ||
public static class DesignTimeHelpers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change in visibility? Seems unrelated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unrelated, will change back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually now that I understand you moved it out to a different assembly for reuse, I guess you have to make it public
How is this better/different from the Expression classes added earlier? |
@skendrot, they are two different things:
|
{ | ||
get | ||
{ | ||
var collection = (KeyFrameCollection)GetValue(KeyFramesProperty); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or in the KeyFramesProperty
itself
/// </summary> | ||
/// <param name="d">The animation where a property has changed</param> | ||
/// <param name="e">The details about the property change</param> | ||
protected static void OnAnimationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would agree with his first statement to create an instance method to follow proper event handling
protected virtual void OnAnimationChanged(EventArgs e)
{
AnimationChanged?.Invoke(this, EventArgs.Empty);
}
/// </summary> | ||
/// <typeparam name="T">Type of <see cref="TypedKeyFrame{U}"/> to use</typeparam> | ||
/// <typeparam name="U">Type of value being animated.</typeparam> | ||
public abstract class TypedAnimationBase<T, U> : AnimationBase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of T and U maybe TKeyFrame and T since the first must the a TypedKeyFrame
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
public abstract class TypedAnimationBase<T, U> : AnimationBase | ||
where T : TypedKeyFrame<U>, new() | ||
{ | ||
private T fromKeyFrame; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private variables should have an underscore prefix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
_previousPageConnectedAnimationProps.Clear(); | ||
_coordinatedAnimationElements.Clear(); | ||
|
||
_navigationFrame.Navigating += NavigationFrame_Navigating; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing it's possible that the new value is null, should chec for that before subscribing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
}; | ||
|
||
var navigatedPage = NavigationFrame.Content as Page; | ||
navigatedPage.Loaded += handler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would the Page never load? It was just navigated to
/// Identifies the Implicit.HideAnimations XAML attached property | ||
/// </summary> | ||
public static readonly DependencyProperty HideAnimationsProperty = | ||
DependencyProperty.RegisterAttached("HideAnimations", typeof(AnimationCollection), typeof(Implicit), new PropertyMetadata(null, HideAnimationsChanged)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Value should default to a new instance of AnimationCollection. Same for below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. You should never have reference types defined as default dependency properties. Those should instead be initialized in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reference?
{ | ||
var collection = (AnimationCollection)obj.GetValue(ShowAnimationsProperty); | ||
|
||
if (collection == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking for null here, default the value and do not allow a null value to be set. Or, if the developer decides to set the value to null, so be it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't prevent an attached property from being set to null even if you throw, because the throw would happen after the set:
try {
myObject.SetValue(Implicit.AnimationsProperty, null);
} catch { }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I say if the developer decides to set it to null, so be it.
{ | ||
var collection = (AnimationCollection)obj.GetValue(HideAnimationsProperty); | ||
|
||
if (collection == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. default the value and do not check for null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. No :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, do not check for null and default the value, whether in the ctor or the DP
…initial value before delay
/// <summary> | ||
/// Gets a value indicating whether the collection contains an animation that targets the Translation property | ||
/// </summary> | ||
public bool ContainsTranslationAnimation => this.Where(anim => !string.IsNullOrWhiteSpace(anim.Target) && anim.Target.StartsWith("Translation")).Count() > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be public? Seems like it should be internal, or just put this method in the Implicit class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be helpful if running animations manually in order to enable translation animations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a proven use-case to support that? Or are you just theorizing? :-p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's based on my experience using it. :)
} | ||
|
||
private static void AddListViewBaseItemAnimationDetails(ConnectedAnimationHelper helper, Windows.UI.Xaml.Controls.ListViewBase listViewBase) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check for helper being null and at the top and early exit. No need to do all of this work if the helper is null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
**XAML** | ||
|
||
```xml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be xaml as per #1570
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
<Border Height="100" | ||
Width="100" | ||
Background="Purple" | ||
extensions:VisualEx.CenterPoint="50,50,0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts about adding a "RelativeCenterPoint" extension that behaves similar to RenderTransform Centre Point? - as an Expression Animation so that it automatically updates the absolute value whenever associated visual changes size
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a great idea, makes a lot more sense then KeepCenterPointCentered. I just added a new property called NormalizedCenterPoint that takes values between 0 and 1, and removed the KeepCenterPointCentered.
How does everyone feel about the Duration type on the animations being a TimeSpan? The more I use it, the more I hate having to type in the entire thing (ex: 0:0:0.3) vs just typing in the milliseconds (ex: 300). Any thoughts? |
element.SizeChanged -= KeepCenteredElementSizeChanged; | ||
element.SizeChanged += KeepCenteredElementSizeChanged; | ||
var vectorValue = normalizedValue.ToVector3(); | ||
if (vectorValue.X < 0 || vectorValue.X > 1 || vectorValue.Y < 0 || vectorValue.Y > 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say it would be fine not to enforce this - quite a few times I've found it useful to put centre points outside of 0 to 1 - for example, setting the centre point of a page header to match that of the actual entire page, or an element on the circumference of a circle having a centre point at the centre of the circle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, removed
{ | ||
element.SizeChanged -= KeepCenteredElementSizeChanged; | ||
element.SizeChanged += KeepCenteredElementSizeChanged; | ||
var vectorValue = normalizedValue.ToVector3(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can ToVector3 also support "float, float" in addition too "float" and "float, float, float"? Not sure where it's used elsewhere but for this case it'd be handy as most people don't ever use the Z component.
Or perhaps just a special case just for Normalized Centre point? (Seeing as the Z is always set to Zero anyway)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that but decided against it as it felt "dirty". Most properties are Vector3 and there is a possibility that could cause unforeseen issues.
@nmetulev In code behind I'm all for (fluent) extensions for Storyboards/CompositionAnimations that take doubles in for TimeSpans, but for XAML I'd hesitate a little bit if only because it'd make it "easier" for people to understand coming from storyboards if they have similar timing syntax (and it's not immediately obvious what units the duration would be if you still had it labelled as just 'duration', but that's very minor) (As an aside I was going to test this for memory leaks but hot damned this repo is sucking up all of my poor slow ADSL internet :'( ) |
@nmetulev for the TimeSpan question. Would you want your own TimeSpan wrapper class that could know how take values in either format from the timing input (0:0:0.3 or 300) and convert automatically to a TimeSpan value for the code? I do agree though that supporting the timing from storyboards would lead to better adoption for consistency and familiarity. |
|
||
namespace Microsoft.Toolkit.Uwp.UI.Animations | ||
{ | ||
internal class ApiInformationHelper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper would be useful beyond just the Animations namespace, should it be moved up in the project tree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't encourage checking for a specific version IMO, devs should still check for existence of a specific API. I know it's hypocritical, but the reason why I'm using this is to avoid calling the ApiInformation API multiple times
/// <summary> | ||
/// ScalarAnimation that animates the <see cref="Visual.RotationAngleInDegrees"/> property | ||
/// </summary> | ||
public class RotationInDegreesAnimation : ScalarAnimation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just instead have a Unit
dependency property on the RotationAnimation which specifies an enum for Radians
or Degrees
? It'd be nice to default to Degrees over Radians (like we default for the Rotate helper currently in the toolkit) and have a less verbose name/setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming comes from the property name on the visual that is animated. Visual has both Rotation and RotationInDegrees, so I thought it's better to keep it consistent
@michael-hawker I'm convinced now that keeping TimeSpan is the right thing to do :) |
string[] values = str.Split(','); | ||
|
||
var count = values.Count(); | ||
Vector2 vector = Vector2.Zero; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Struct. No init necessary.
string[] values = str.Split(','); | ||
|
||
var count = values.Count(); | ||
Vector3 vector = Vector3.Zero; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Struct. No init necessary.
string[] values = str.Split(','); | ||
|
||
var count = values.Count(); | ||
Vector4 vector = Vector4.Zero; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Struct. No init necessary.
{ | ||
if (e.NewValue is string str) | ||
{ | ||
SetCenterPoint(d, e.NewValue as string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use str, instead of a new redundant "as string".
} | ||
catch (Exception) | ||
{ | ||
throw; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty throw. For debugging purposes?
/// </summary> | ||
/// <param name="obj">The <see cref="Frame"/></param> | ||
/// <returns><see cref="ConnectedAnimationHelper"/> attached to the Frame</returns> | ||
private static ConnectedAnimationHelper GetConnectedAnimationHelper(DependencyObject obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you using a DependencyProperty here? They are all Frames, always. And never null. the DP is private. I think the DP should be removed.
if (e.NavigationMode == Windows.UI.Xaml.Navigation.NavigationMode.Back) | ||
{ | ||
var sourcePage = (sender as Frame).ForwardStack.LastOrDefault(); | ||
parameter = sourcePage?.Parameter ?? null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already using ?. "?? null" can be safely removed.
foreach (var previousProps in _previousPageConnectedAnimationProps) | ||
{ | ||
var connectedAnimation = cas.GetAnimation(previousProps.Key); | ||
if (connectedAnimation != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use null propagation "?."
} | ||
|
||
RoutedEventHandler handler = null; | ||
handler = (s, args) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not a local function?
|
||
private void Frame_Navigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e) | ||
{ | ||
var navigatedPage = (sender as Frame).Content as Page; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use pattern matching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pattern matching in these cases causes a vs warning
PR Type
What kind of change does this PR introduce?
PR Checklist
Please check if your PR fulfills the following requirements:
What is the new behavior?
This PR is adding UWP Composition XAML Helpers:
Does this PR introduce a breaking change?