Skip to content

Commit

Permalink
fix(responsive): layout breakpoint calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoy312 committed Dec 15, 2023
1 parent 775b466 commit b5398d9
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 36 deletions.
48 changes: 44 additions & 4 deletions doc/controls/ResponsiveView.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
}
```

## Inheritance
Object → DependencyObject → UIElement → FrameworkElement → Control → ContentControl

## Properties
| Property | Type | Description |
| ----------------- | ---------------- | ------------------------------------------------------- |
Expand All @@ -42,7 +39,7 @@ Object → DependencyObject → UIElement → FrameworkElement &#859
### ResponsiveLayout
Provides the ability to override the breakpoint for each screen size: `Narrowest`, `Narrow`, `Normal`, `Wide`, and `Widest`.

### Properties
#### Properties
| Property | Type | Description |
| ---------- | ------ | ---------------------- |
| Narrowest | double | Default value is 150. |
Expand All @@ -51,6 +48,49 @@ Provides the ability to override the breakpoint for each screen size: `Narrowest
| Wide | double | Default value is 800. |
| Widest | double | Default value is 1080. |

#### Resolution Logics
The layouts whose value(ResponsiveExtension) or template(ResponsiveView) is not provided are first discarded. From the remaining layouts, we look for the first layout whose breakpoint at met by the current screen width. If none are found, the first layout is return regardless of its breakpoint.

Below are the selected layout at different screen width if all layouts are provided:

Width|Layout
-|-
149|Narrowest
150(Narrowest)|Narrowest
151|Narrowest
299|Narrowest
300(Narrow)|Narrow
301|Narrow
599|Narrow
600(Normal)|Normal
601|Normal
799|Normal
800(Wide)|Wide
801|Wide
1079|Wide
1080(Widest)|Widest
1081|Widest

Here are the selected layout at different screen width if only `Narrow` and `Wide` are provided:

Width|Layout
-|-
149|Narrow
150(~~Narrowest~~)|Narrow
151|Narrow
299|Narrow
300(Narrow)|Narrow
301|Narrow
599|Narrow
600(~~Normal~~)|Narrow
601|Narrow
799|Narrow
800(Wide)|Wide
801|Wide
1079|Wide
1080(~~Widest~~)|Wide
1081|Wide

## Usage

> [!TIP]
Expand Down
98 changes: 72 additions & 26 deletions doc/helpers/responsive-extension.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,12 @@
---
uid: Toolkit.Helpers.ResponsiveExtension
---

# ResponsiveExtension

The `ResponsiveExtension` class is a markup extension that enables the customization of `UIElement` properties based on screen size.
This functionality provides a dynamic and responsive user interface experience.

### Inheritance
Object → MarkupExtension → ResponsiveExtension

## Properties
| Property | Type | Description |
| ---------- | ---------------- | ---------------------------------------------------------- |
| Narrowest | object | Value to be used when the screen size is at its narrowest. |
| Narrow | object | Value to be used when the screen size is narrow. |
| Normal | object | Value to be used when the screen size is normal. |
| Wide | object | Value to be used when the screen size is wide. |
| Widest | object | Value to be used when the screen size is at its widest. |
| Layout | ResponsiveLayout | Overrides the screen size thresholds/breakpoints. |

### ResponsiveLayout
Provides the ability to override the default breakpoints (i.e., the window widths at which the value changes) for the screen sizes.
This is done using an instance of the `ResponsiveLayout` class.

#### Properties
| Property | Type | Description |
| ---------- | ---------------- | ---------------------- |
| Narrowest | double | Default value is 150. |
| Narrow | double | Default value is 300. |
| Normal | double | Default value is 600. |
| Wide | double | Default value is 800. |
| Widest | double | Default value is 1080. |

## Remarks
**Initialization**: The `ResponsiveHelper` needs to be hooked up to the window's `SizeChanged` event in order for this markup to receive updates when the window size changes.
This is typically done in the `OnLaunched` method in the `App` class, where you can get the current `Window` instance for `ResponsiveHelper.HookupEvent`:
Expand All @@ -49,6 +24,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
helper.HookupEvent(MainWindow);
}
```

## Platform limitation (UWP-desktop)
`ResponsiveExtension` relies on `MarkupExtension.ProvideValue(IXamlServiceProvider)` to find the target control and property for continuous value updates, and to obtain the property type to apply automatic type conversion, as its value properties are parsed as string by the XAML engine. Since this overload is a recent addition exclusive to WinUI, UWP projects targeting Windows won't have access to these features. Uno UWP projects targeting non-Windows platforms do not face this limitation. However, the Windows app may crash or present unexpected behavior if you attempt to use this markup on a non-string property.
```xml
Expand All @@ -65,6 +41,74 @@ You can workaround this by declaring the values as resources and using {StaticRe
<Border Background="{utu:Responsive Narrow={StaticResource RedBrush},
Wide={StaticResource BlueBrush}}" />
```

## Properties
| Property | Type | Description |
| ---------- | ---------------- | ---------------------------------------------------------- |
| Narrowest | object | Value to be used when the screen size is at its narrowest. |
| Narrow | object | Value to be used when the screen size is narrow. |
| Normal | object | Value to be used when the screen size is normal. |
| Wide | object | Value to be used when the screen size is wide. |
| Widest | object | Value to be used when the screen size is at its widest. |
| Layout | ResponsiveLayout | Overrides the screen size thresholds/breakpoints. |

### ResponsiveLayout
Provides the ability to override the default breakpoints (i.e., the window widths at which the value changes) for the screen sizes.
This is done using an instance of the `ResponsiveLayout` class.

#### Properties
| Property | Type | Description |
| ---------- | ---------------- | ---------------------- |
| Narrowest | double | Default value is 150. |
| Narrow | double | Default value is 300. |
| Normal | double | Default value is 600. |
| Wide | double | Default value is 800. |
| Widest | double | Default value is 1080. |

#### Resolution Logics
The layouts whose value(ResponsiveExtension) or template(ResponsiveView) is not provided are first discarded. From the remaining layouts, we look for the first layout whose breakpoint at met by the current screen width. If none are found, the first layout is return regardless of its breakpoint.

Below are the selected layout at different screen width if all layouts are provided:

Width|Layout
-|-
149|Narrowest
150(Narrowest)|Narrowest
151|Narrowest
299|Narrowest
300(Narrow)|Narrow
301|Narrow
599|Narrow
600(Normal)|Normal
601|Normal
799|Normal
800(Wide)|Wide
801|Wide
1079|Wide
1080(Widest)|Widest
1081|Widest

Here are the selected layout at different screen width if only `Narrow` and `Wide` are provided:

Width|Layout
-|-
149|Narrow
150(~~Narrowest~~)|Narrow
151|Narrow
299|Narrow
300(Narrow)|Narrow
301|Narrow
599|Narrow
600(~~Normal~~)|Narrow
601|Narrow
799|Narrow
800(Wide)|Wide
801|Wide
1079|Wide
1080(~~Widest~~)|Wide
1081|Wide


## Usage

> [!TIP]
Expand All @@ -77,6 +121,8 @@ xmlns:utu="using:Uno.Toolkit.UI"
<TextBlock Background="{utu:Responsive Narrow=Red, Wide=Blue}" Text="Asd" />
```



### Custom thresholds
```xml
xmlns:utu="using:Uno.Toolkit.UI"
Expand Down
66 changes: 66 additions & 0 deletions src/Uno.Toolkit.RuntimeTests/Tests/ResponsiveHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.Toolkit.UI;

namespace Uno.Toolkit.RuntimeTests.Tests;

[TestClass]
public class ResponsiveHelperTests
{
private readonly static ResponsiveLayout DefaultLayout = ResponsiveLayout.Create(150, 300, 600, 800, 1080);

// note: not to scale; '[' = inclusive to the right
// 0 150 300 600 800 1080 ...
// Narrowest(also) - Narrowest [ Narrow [ Normal [ Wide [ Widest - // full layout
// Normal - -

[TestMethod]
public void When_Resolving_AllLayout()
{
var layout = DefaultLayout;
var options = Enum.GetValues<Layout>();

Assert.AreEqual(Layout.Narrowest, ResponsiveHelper.ResolveLayoutCore(layout, 149, options), "149");
Assert.AreEqual(Layout.Narrowest, ResponsiveHelper.ResolveLayoutCore(layout, 150, options), "150"); // breakpoint=Narrowest
Assert.AreEqual(Layout.Narrowest, ResponsiveHelper.ResolveLayoutCore(layout, 151, options), "151");
Assert.AreEqual(Layout.Narrowest, ResponsiveHelper.ResolveLayoutCore(layout, 299, options), "299");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 300, options), "300"); // breakpoint=Narrow
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 301, options), "301");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 599, options), "599");
Assert.AreEqual(Layout.Normal, ResponsiveHelper.ResolveLayoutCore(layout, 600, options), "600"); // breakpoint=Normal
Assert.AreEqual(Layout.Normal, ResponsiveHelper.ResolveLayoutCore(layout, 601, options), "601");
Assert.AreEqual(Layout.Normal, ResponsiveHelper.ResolveLayoutCore(layout, 799, options), "799");
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 800, options), "800"); // breakpoint=Wide
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 801, options), "801");
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 1079, options), "1079");
Assert.AreEqual(Layout.Widest, ResponsiveHelper.ResolveLayoutCore(layout, 1080, options), "1080"); // breakpoint=Widest
Assert.AreEqual(Layout.Widest, ResponsiveHelper.ResolveLayoutCore(layout, 1081, options), "1081");
}

[TestMethod]
public void When_Resolving_PartialLayout()
{
var layout = DefaultLayout;
var options = new[] { Layout.Narrow, Layout.Wide, Layout.Widest };

Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 149, options), "149");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 150, options), "150"); // breakpoint=Narrowest (unavailable)
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 151, options), "151");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 299, options), "299");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 300, options), "300"); // breakpoint=Narrow
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 301, options), "301");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 599, options), "599");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 600, options), "600"); // breakpoint=Normal (unavailable)
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 601, options), "601");
Assert.AreEqual(Layout.Narrow, ResponsiveHelper.ResolveLayoutCore(layout, 799, options), "799");
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 800, options), "800"); // breakpoint=Wide
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 801, options), "801");
Assert.AreEqual(Layout.Wide, ResponsiveHelper.ResolveLayoutCore(layout, 1079, options), "1079");
Assert.AreEqual(Layout.Widest, ResponsiveHelper.ResolveLayoutCore(layout, 1080, options), "1080"); // breakpoint=Widest
Assert.AreEqual(Layout.Widest, ResponsiveHelper.ResolveLayoutCore(layout, 1081, options), "1081");
}
}
29 changes: 23 additions & 6 deletions src/Uno.Toolkit.UI/Helpers/ResponsiveHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#endif

using System;
using System.Linq;
using System.Collections.Generic;
using Windows.Foundation;
using Uno.Disposables;
Expand Down Expand Up @@ -109,6 +110,8 @@ public static ResponsiveLayout Create(double narrowest, double narrow, double no
Wide = wide,
Widest = widest,
};

public IEnumerable<double> GetBreakpoints() => new[] { Narrowest, Narrow, Normal, Wide, Widest };

public override string ToString() => "[" + string.Join(",", Narrowest, Narrow, Normal, Wide, Widest) + "]";
}
Expand Down Expand Up @@ -191,21 +194,35 @@ internal void Register(IResponsiveCallback host)
internal (ResponsiveLayout Layout, Size Size, Layout? Result) ResolveLayout(ResponsiveLayout? layout, IEnumerable<Layout> options)
{
layout ??= Layout;
var result =
options.FirstOrNull(SatisfyLayoutThreshold) ??
options.LastOrNull();
var result = ResolveLayoutCore(layout, WindowSize.Width, options);

return (layout, WindowSize, result);
}

bool SatisfyLayoutThreshold(Layout x) => x switch
internal static Layout? ResolveLayoutCore(ResponsiveLayout layout, double width, IEnumerable<Layout> options)
{
return options
.Concat(new Layout[] { (Layout)int.MaxValue }) // used to get the +inf for the last one's upper-boundary
.ZipSkipOne()
.Select(x => new
{
Layout = x.Previous,
InclusiveLBound = GetThreshold(x.Previous),
ExclusiveUBound = GetThreshold(x.Current),
})
.FirstOrDefault(x => x.InclusiveLBound <= width && width < x.ExclusiveUBound)
?.Layout ?? options.FirstOrNull();

double GetThreshold(Layout x) => x switch
{
UI.Layout.Narrowest => layout.Narrowest,
UI.Layout.Narrow => layout.Narrow,
UI.Layout.Normal => layout.Normal,
UI.Layout.Wide => layout.Wide,
UI.Layout.Widest => layout.Widest,
_ => double.NaN,
} >= WindowSize.Width;

_ => double.PositiveInfinity,
};
}

internal static IDisposable UsingDebuggableInstance()
Expand Down

0 comments on commit b5398d9

Please sign in to comment.