Skip to content
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

Merged
merged 35 commits into from
Nov 10, 2017
Merged

Adding Composition XAML Helpers #1567

merged 35 commits into from
Nov 10, 2017

Conversation

nmetulev
Copy link
Contributor

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[X] Feature
[ ] Code style update (formatting)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build or CI related changes
[ ] Documentation content changes
[ ] Sample app changes
[ ] Other... Please describe:

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Docs have been added / updated (for bug fixes / features)
  • Sample in sample app has been added / updated (for bug fixes / features)

What is the new behavior?

This PR is adding UWP Composition XAML Helpers:

  1. Visual Attached Properties - allows object visual properties to be modified through XAML
  2. Implicit Animations Attached Properties - implicit animations (including show and hide) can now be defined directly on the element in XAML, or as resources to be applied on multiple elements
  3. Connected Animations Attached Properties - enables the ConnectedAnimationService to be used in XAML by simply connecting two elements across pages by registering them with the same key. Supports coordinated and list/grid animations.

Does this PR introduce a breaking change?

[ ] Yes
[X] No

@azchohfi
Copy link
Contributor

That's just beyond amazing. Will review ASAP.

Copy link
Contributor

@dotMorten dotMorten left a 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));
Copy link
Contributor

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)

Copy link
Contributor Author

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);
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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 ??
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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);
}

Copy link
Contributor Author

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);
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will Fix

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks unrelated?

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor

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

@skendrot
Copy link
Contributor

How is this better/different from the Expression classes added earlier?

@nmetulev
Copy link
Contributor Author

@skendrot, they are two different things:

  • ExpressionBuilder enables composition expressions to be built with type safety
  • These classes enable implicit and connected animations through XAML

{
get
{
var collection = (KeyFrameCollection)GetValue(KeyFramesProperty);
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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));
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor

@dotMorten dotMorten Oct 25, 2017

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 { }

Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. No :-)

Copy link
Contributor

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

/// <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;
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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)
{
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


**XAML**

```xml
Copy link
Contributor

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

Copy link
Contributor Author

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"
Copy link
Contributor

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

Copy link
Contributor Author

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.

@nmetulev
Copy link
Contributor Author

nmetulev commented Nov 5, 2017

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)
Copy link
Contributor

@JohnnyWestlake JohnnyWestlake Nov 5, 2017

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.

Copy link
Contributor Author

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();
Copy link
Contributor

@JohnnyWestlake JohnnyWestlake Nov 5, 2017

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)

Copy link
Contributor Author

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.

@JohnnyWestlake
Copy link
Contributor

JohnnyWestlake commented Nov 5, 2017

@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 :'( )

@azchohfi azchohfi self-requested a review November 8, 2017 00:51
@azchohfi
Copy link
Contributor

azchohfi commented Nov 8, 2017

For some reason, the documentation files are double showing up with an extra empty line between lines. image.

@michael-hawker
Copy link
Member

@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
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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

@nmetulev
Copy link
Contributor Author

nmetulev commented Nov 8, 2017

@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;
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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;
Copy link
Contributor

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)
Copy link
Contributor

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;
Copy link
Contributor

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)
Copy link
Contributor

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) =>
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use pattern matching.

Copy link
Contributor Author

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

@shenchauhan shenchauhan merged commit 24794b9 into master Nov 10, 2017
@nmetulev nmetulev deleted the nmetulev/composition branch November 10, 2017 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants