Skip to content

Latest commit

 

History

History
495 lines (294 loc) · 38.1 KB

layout.md

File metadata and controls

495 lines (294 loc) · 38.1 KB

Cross-Platform Layout

This document explains the workings of .NET MAUI's cross-platform layout system. It primarily includes details of the MAUI.Core layer (where most of the layout is implemented), plus some exceptions/customizations in MAUI.Controls which are included for backward compatibility with Xamarin.Forms.

This document is a work in progress.

The Basics

LayoutAlignment

Layout alignment controls how a View positions itself in a container. The possible options are Fill, Start, Center, and End. Fill is the default for all of the Views in the Controls SDK.

LayoutAlignment in Core is mostly handled by the ComputeFrame() extension method. When a View is being arranged in a Rect, it calls ComputeFrame() to determine what its Frame should be. Frames are always in relation to the container.

For ease of migration, Controls still uses the LayoutOptions structure, which also includes the (deprecated) "AndExpand" flags. The Core layout system ignores the "AndExpand" flags and translates the Fill/Start/Center/End values from LayoutOptions to the LayoutAlignment enumeration.

  • Note: Controls still supports "AndExpand" on StackLayout (but not VerticalStackLayout or HorizontalStackLayout). It does this by converting the StackLayout at runtime to a single column or single row Grid. Because that's all "AndExpand" was ever really doing in Forms.

Center

The View is centered within the container along the axis.

Start/End

The View is aligned with the starting or ending edge of the container along the axis. Vertically, Start is the top edge, End is the bottom edge. Horizontally, when the FlowDirection of the container is LTR, Start is the left edge and End is the right edge. When the container's FlowDirection is RTL, these are reversed.

However, the Core layout code doesn't actually worry about this - it always computes the frames as if the layout were LTR. The underlying platforms do all the work to flip the layouts in the LayoutHandlers and the platform layout views (e.g., LayoutViewGroup and LayoutPanel). So the Core layout engine doesn't have to worry about effective flow direction.

Fill

If the View does not have an explicit size along the axis, then Fill will make the Frame be the size of the container in that direction (minus any margins). The DesiredSize of the View doesn't matter - Fill will stretch/shrink the View to the container size.

If the View does have an explicit size along the axis (i.e, if the Width or Height is set to something other than Dimension.Unset), then ComputeFrame() now has a conflict - the user has requested that the View fill a space, but they've also requested an explicit size. In this case, the explicit size wins. The Frame will have the explicit size, and if that size is less than the available space the Frame will be aligned to the center of the space.

For example, given this layout...

<VerticalStackLayout WidthRequest="400">
    <Label Text="Hello" WidthRequest="100" HorizontalOptions="Fill"/>
</VerticalStackLayout>

...the label will be 100 units wide and centered horizontally. This also applies to Maximum/Minimum sizes along the axis; e.g.,

<VerticalStackLayout WidthRequest="400">
    <Label Text="Hello" MaximumWidthRequest="50" HorizontalOptions="Fill"/>
</VerticalStackLayout>

...will also center the Label horizontally, and will limit it to 50 units wide.

Dimension

The layout code in Core relies on several constants set in Microsoft.Maui.Primitives.Dimension to avoid any confusion from magic numbers when dealing with the height/width of views.

By default, the height/width of a view in Core's layout is Dimension.Unset; this indicates that the view should be whatever size the underlying platform wants given other constraints (container size, etc.). To indicate an explicit, user-set height/width for an IView, the IView.Width or IView.Height need to return an actual number (between Dimension.Minimum and Dimension.Maximum). The Dimension.IsExplicitSet() helper can be used to check for that.

By definition, any IView in the Core layout system has a minimum width/height set (if not otherwise specified, it's zero). Whether a maximum width/height has been set can be checked with Dimension.IsMaximumSet().

In the Controls layer there are some disconnects between these values in order to provide backward compatibility with magic numbers from Forms. So a View can set a WidthRequest of -1, and this will ultimately map to Dimension.Unset.

Z-Index

Z-Index as a Core layout concept does not explicitly map to any platform z-index properties. Instead, it relies on the ordering of the platform controls within the layout backing controls on each platform.

For example, in an Android ViewGroup, absent any other Elevation settings, the children of the ViewGroup will be drawn in order; the first child will be the "lowest", with subsequent overlapping children being drawn atop one another. Windows and iOS work similarly.

So in the Core LayoutHandler implementations, the views in the ILayout are sorted by their ZIndex (preserving their collection order) when they are added to the platform layout control. This ordering is preserved as children are added, removed, or have their ZIndex modified. Because controls later in the collection are drawn over controls earlier in the collection, this effectively gives us a z-index without explicitly mapping any properties.

The underlying platforms do have concepts akin to z-index, but they behave differently enough that simple mapping causes side effects that are undesirable for our cross-platform layouts. (Specifically, Android's closest property, Elevation, also affects the shadows of elevated views, and causes problems when elevating controls like Button, which utilizes Elevation for state effects like "pressed".)

Example 1

A Grid has 3 children:

Label1 { ZIndex = 0 }
Label2 { ZIndex = 0 }
Label3 { ZIndex = 0 }

The LayoutHandler will create a LayoutViewGroup with the children in the order {Label1, Label2, Label3}.

Example 2

A Grid has 3 children:

Label1 { ZIndex = 0 }
Label2 { ZIndex = 10 }
Label3 { ZIndex = 0 }

The LayoutHandler will create a LayoutViewGroup with the children in the order {Label1, Label3, Label2}. Label2 is pushed to the top because it has the highest z-index. The order of Label1 and Label3 with respect to one another is preserved.

Padding

Padding is space inside of a view around the content of that view. Anything that implements IPadding can have padding. In some cases, the padding is provided by the platform (by mapping the IPadding thickness values to platform padding properties). For cross-platform layouts, the padding is provided by the layout system.

If padding is requested in the layout system it will be provided to the best of the system's ability. This means that the content of a control will be shrunk enough to provide the padding even if that means shrinking the control to its minimum size (possibly zero).

Margin

Margin is space outside of a view. Margins are handled entirely by the layout system. Like padding, if a margin is requested it will be granted, even if that shrinks the platform view itself to nothing.

Margins are included in the DesiredSize that a view reports after measurement.

Visibility

In MAUI.Core, Visibility has 3 possible values:

  • Visible: the item is visible
  • Hidden: the item occupies space, but cannot be seen
  • Collapsed: the item does not occupy space, and cannot be seen

The MAUI.Core built-in layouts understand and honor all three values.

In MAUI.Controls, for backward-compatibility, visibility is limited to two values via the IsVisible property. IsVisble == true maps to Visibility.Visible; IsVisible == false maps to Visibility.Collapsed.

The mapped implementations of visibility for individual controls in MAUI.Core should all handle Visibility.Hidden properly. So while MAUI.Controls does not have the concept of Visibility.Hidden, other SDKs can use it if they like. And custom handlers/controls in MAUI.Controls can also make use of it if they need to.

Layout vs ContentView

A Layout is a list of views with rules and properties defining how to arrange those views within a frame. Examples of Layouts include Grid, AbsoluteLayout, StackLayout, VerticalStackLayout, and HorizontalStackLayout.

A ContentView is responsible for displaying a single view. It takes an input view (the Content property) and displays an output view (the PresentedContent property). The input view and output view may be the same, or there may be some transformation applied (e.g., using a template). Examples of ContentViews include Page and ScrollView.

Note: Because of some backward-compatibility constraints, in some cases the base classes for the implementations in MAUI.Controls are confusing. So while ScrollView is not a Layout, it does inherit from the Layout class.

Measure/Arrange

The cross-platform measure/arrange process in MAUI.Core piggybacks on each platform's layout process. Whenever the platform requests the measure or arrangement of a backing control (e.g., a ContentViewGroup, LayoutViewGroup, LayoutPanel, etc.), the backing control calls its CrossPlatformMeasure() or CrossPlatformArrange() method.

CrossPlatformMeasure

In a Layout, CrossPlatformMeasure() is responsible for calling IView.Measure() on each of the Layout's children. In a ContentView, CrossPlatformMeasure() is responsible for calling IView.Measure() on the ContentView's PresentedContent view.

CrossPlatformArrange

In a Layout, CrossPlatformArrange() is responsible for calling IView.Arrange() on each of the Layout's children. In a ContentView, CrossPlatformArrange() is responsible for calling IView.Arrange() on the ContentView's PresentedContent view.

Platform Backing Layouts

All layouts are backed by a single control per platform:

PlatformControl
iOSLayoutView
AndroidLayoutViewGroup
WinUILayoutPanel

(If you're familiar with how layout worked in Xamarin.Forms, these backing controls are similar to the DefaultRenderer implementations on each platform.)

Layouts are mapped to these controls via a LayoutHandler. The LayoutHandler is responsible for adding the platform controls for each child of the layout to the backing control, keeping them in an appropriate Z-Index order, and mapping the CrossPlatformMeasure() and CrossPlatformArrange() methods.

When a backing control (e.g., LayoutViewGroup) is being measured (in response to the platform's measure/layout pass), it calls CrossPlatformMeasure() on its cross-platform IView. When the backing control is being arranged, it calls CrossPlatformArrange(). This is one of the points where control is passed between the platform's layout system and MAUI.Core's layout system.

The backing controls also handle bounds clipping for their layouts, and are capable of handling safe area on platforms which support it.

  • Note: at the moment, we only support Safe Area on iOS. We should support it on Android, but we haven't done that yet.

Platform Backing ContentViews

All content views are backed by a single control per platform:

PlatformControl
iOSContentView
AndroidContentViewGroup
WinUIContentPanel

ContentViews are mapped to these controls via a ContentViewHandler. The ContentViewHandler is responsible for setting up the backing control, updating the platform content, and mapping the CrossPlatformArrange() and CrossPlatformMeasure() methods.

As with layouts, the ContentView backing controls handle bounds clipping and safe area.

Layout Implementation

ILayout/ILayoutManager

The ILayout interface is composed of a few other interfaces - it's an IView, an IContainer (that is, it's a list of other IViews), an IPadding (it provides padding around its content), and an ISafeAreaView (it's aware of the safe area of the host platform, and is able to either confine itself to the safe area, or ignore the boundaries).

It also provides the ClipsToBounds property, which determines whether its child views can be displayed outside of its boundaries, or are clipped at the edges.

📝 In MAUI.Controls, the value of ClipsToBounds is provided by the IsClippedToBounds property of Layout. For migration convenience from Xamarin.Forms, the IsClippedToBounds property defaults to false, so layouts in MAUI.Controls do not clip by default.

The ILayout interface also provides the CrossPlatformMeasure() and CrossPlatfromArrange() methods; these methods are utilized by the platform backing controls (e.g. LayoutViewGroup) to handle the cross-platform layout work.

Technically speaking, it's possible to implement all of the cross-platform layout logic directly in an implementation of ILayout, and it's possible that some SDKs may choose to do so. But by convention, that work is instead delegated to an implementation of ILayoutManager.

ILayoutManager defines two methods - Measure() and ArrangeChildren(). By convention, the ILayout implementation can call ILayoutManager.Measure() to do the work of CrossPlatformMeasure() and ILayoutManager.ArrangeChildren() to do the work of CrossPlatformArrange(). All of the actual logic to handle cross-platform layout is contained in the ILayoutManager implementation; this allows us to mix/match/reuse implementations, and it allows other SDK implementors to reuse that logic in other layout types if they desire.

In MAUI.Controls, the abstract type Layout handles delegating the cross-platform measure and arrange calls to a layout manager. It also contains a predefined overridable method CreateLayoutManager() that derived layouts can use to specify the layout manager they want. And it contains logic which can look for an ILayoutManagerFactory service implementation. Layout handles most of the work of adding/removing child views, and provides several virtual methods for doing addtional bookkeeping when the collection changes. It also contains logic for notifying the ILayoutHandler implementation of those changes, so the LayoutHandler can do things like adding/removing platform controls and sorting by z-index.

Customization

The built-in layouts have predefined layout managers to handle their layout. However, it's possible that users may want to define custom layout logic for layouts. There are two ways to do this.

The first is to create a custom layout type (usually a subclass of an existing layout type, or of Layout) and override CreateLayoutManager().

The alternative is to implement ILayoutManagerFactory, register the implementation with the application's service provider, and use that to define which layout manager implementation is to be used for each layout type. In this second scenario, it's possible to define a new layout manager for an existing layout type; e.g., a user can provide a custom layout manager for Grid, with different layout rules or other customizations. This is especially useful for situations where users would like to patch in a previously deprecated behavior for a layout. It's also great when you would like a new behavior but don't want to update the type of an existing widely-used layout.

Creating custom layouts and layout managers is relatively easy; we keep a repo of examples to help folks get started.

The Layouts

The following descriptions apply to the default layout manager implementations provided in MAUI.Core and MAUI.Controls.

VerticalStackLayout/HorizontalStackLayout

VerticalStackLayout and HorizontalStackLayout are very straightforward - they stack their child views next to each other.

In the unconstrained direction (vertically for VSL, horizontally for HSL), they stack their child views one after the other until all of them have been stacked. They are unconstrained in this direction; they will keep stacking even if it takes them past the width or height constraint parameters given to the Measure and Arrange methods. In other words, the effective height constraint of a VerticalStackLayout is always infinity; the effective width constraint of a HorizontalStackLayout is always infinity.

In the other direction, the layout is constrained. For example, a single VerticalStackLayout on a Page will have its width constrained to the Page's width.

Putting a VSL or HSL into a ScrollView with the appropriate scrolling direction will allow the user to view content which exceeds the size of the container.

Both layouts provide a Spacing property; this spacing is applied between all visible items in the layout. Views which have Visibility.Collapsed are not displayed and do not count for spacing purposes.

StackLayout

StackLayout works the same as VerticalStackLayout/HorizontalStackLayout, but it also includes the Orientation property. The StackLayout uses this property to determine whether it will render a VerticalStackLayout or a HorizontalStackLayout. Under the hood, the Orientation property simply switches the LayoutManager being used.

StackLayout also checks its child views for the presence of any "AndExpand" layout options; if any exist in the relevant direction (e.g., if there are vertical "AndExpand" options and the Orientation is set to Vertical), then an AndExpandLayoutManager is swapped in. This LayoutManager converts the StackLayout at runtime to a single row/column Grid, with the appropriate row/column definitions to expand the views which require it.

Grid

Grids subdivide a given area into rows and columns for arranging their child views.

RowDefinitions and ColumnDefinitions

The set of rows and columns in a Grid is defined by its RowDefinitions and ColumnDefinitions. Rows are defined in order from the top of the Grid downward. A Grid with no explicit RowDefinitions defaults to a single row with height GridLength.Star. Columns are defined in order from the left to the right. A Grid with no explicit ColumnDefinitions defaults to a single column with width GridLength.Star.

Spacing

A Grid can specify ColumnSpacing and RowSpacing. These are double values which specify how much empty space the Grid should leave between its rows and columns. Spacing on both axes defaults to zero. Spacing is applied between rows/columns; it is not applied to the outside edges of the Grid. A Grid with a single column or row will not have any spacing applied on that axis.

Empty rows/columns still count for the purposes of spacing - if the row/column does not have any child views in it, the spacing is still applied. For example, the following Grid:

<Grid RowSpacing="10" ColumnSpacing="10" RowDefintions="Auto, Auto" ColumnDefinitions="Auto,Auto" />

will measure 10x10, even though it's empty.

GridLength

Every row and column definition specifies a GridLength, which can be of three types:

Explicit

A double value which specifies the size of the row/column. When specified, the row/column will get this size in the final layout regardless of its content.

Auto

The Grid will measure all of the child Views in the row/column and set the height/width large enough to accommodate the largest View. If the row/column has no child Views, its size will be set to zero. The row/column still counts for the purposes of spacing, however.

Star

"Star" values indicate a weighted portion of the remaining space available. Star values are specified in the format "[weight]*", where [weight] is a double and defaults to 1.0 if not specified.

As an example, take a Grid with HeightRequest="100" and RowDefinitions="*,2*,6*,0.5*,0.5*". The available space is 100 units. The sum of the weights is 10 (1 + 2 + 6 + 0.5 + 0.5). So the size of each "*" is 100 / 10 = 10. Therefore, the rows would have heights of 10, 20, 60, 5, and 5 units.

GridLength.Star only has meaning when its axis is constrained; the concept of "remaining space" stops making sense when the space is infinite. When a row or column with GridLength.Star is measured with a constraint of double.PositiveInfinity, it is treated as if it were GridLength.Auto.

AbsoluteLayout

AbsoluteLayout allows child views to be positioned and sized using explicit values and/or proportionally to the size of the layout.

The layout of a child view in AbsoluteLayout can be specified with a combination of two values:

LayoutBounds

The layout bounds of a child view is a Rect which specifies the position and size of the view. If not specified, the default layout bounds for a view are a position of (0, 0) with the Width and Height properties set to AbsoluteLayout.AutoSize (a constant value of -1).

LayoutFlags

The layout flags of a child view specify how the values in the layout bounds are used. By default, no flags are specified (AbsoluteLayoutFlags.None). The XProportional and YProportional values can be independently set, or both values can be set at once by setting PositionProportional. Likewise, HeightProportional and WidthProportional can be set independently, or both can be set at once with SizeProportional. All flags can be set at once using AbsoluteLayoutFlags.All.

Layout Bounds/Flags Interaction

The layout rules for a child view are determined by a combination of its layout bounds and its layout flags:

Position

If the XProportional/YProportional flags are set, then the X/Y values of the layout bounds are multiplied by the width/height of the AbsoluteLayout to determine the final X/Y coordinate values. For instance, imagine an AbsoluteLayout which is 100 x 100. It has a child with layout bounds of (0.4, 0.6, 20, 20), and the child's XProportional and YProportional flags are set. The position of that child will be X = (100 * 0.4) = 40 and Y = (100 * 0.6) = 60.

If XProportional/YProportional flags are not set, then the X and Y values in the layout bounds are explicit values. For instance, imagine an AbsoluteLayout which is 100 x 100. It has a child with layout bounds of (45, 67, 20, 20), and the child's XProportional and YProportional flags are not set. The position of that child will be X = 45 and Y = 67.

Both explicit and proportional position values can exceed the boundaries of the AbsoluteLayout.

Size

If the WidthProportional/HeightProportional flags are set, then the Width/Height values of the layout bounds are multiplied by the width/height of the AbsoluteLayout to determine the final Width/Height values. For instance, imagine an AbsoluteLayout which is 100 x 100. It has a child with layout bounds of (0, 0, 0.3, 0.47), and the child's WidthProportional and HeightProportional flags are set. The size of that child will be Width = (100 * 0.3) = 30 and Height = (100 * 0.47) = 47.

If WidthProportional/HeightProportional flags are not set, then the Width and Height values in the layout bounds are explicit values. For instance, imagine an AbsoluteLayout which is 100 x 100. It has a child with layout bounds of (0, 0, 45, 20), and the child's WidthProportional and HeightProportional flags are not set. The size of that child will be Width = 45 and Height = 20.

Both explicit and proportional size values can exceed the boundaries of the AbsoluteLayout.

Unbounded Dimensions

If the AbsoluteLayout is unbounded along a dimension (e.g., if it's a child of a VerticalStackLayout, and is not provided with an explicit height), then the concept of "proportional" along that dimension has no meaning. Proportional flags along those dimensions are ignored; if the layout bounds specifies a non-Auto value along that dimension, it will be used. Otherwise, the automatic size of the view will be used along that dimension.

For example, say an AbsoluteLayout is the child of a VerticalStackLayout with a width of 200. The only child of the AbsoluteLayout has LayoutBounds of (0, 0, 100, 0.5) and has AbsoluteLayoutFlags.HeightProportional set. Because the AbsoluteLayout has no vertical constraints, the AbsoluteLayoutFlags.HeightProportional flag is ignored, and the child's height is set to the value of 0.5 from its layout bounds.

The Layout Process

The cross-platform layout process in Maui.Core piggy-backs off of the native layout process on each platform. Generally speaking, all layout work is initiated by the native layout system; the cross-platform layout process kicks in when a layout or content backing control initiates it as a result of being measured/arranged by the native layout system.

The following diagram shows the process when the native layout system wants to measure a backing view:

sequenceDiagram
    participant P as Platform
    participant BV as Backing View
    participant XV as Cross-Platform View
    P->>BV: Measure
    BV->>XV: Cross-Platform Measure
    Note over XV: Update DesiredSize
    XV->>BV: DesiredSize
    Note over BV: Internal bookkeeping (e.g. SetMeasuredDimension())
    BV->>P: Size
Loading

For the purposes of walking through this, let's assume that the cross-platform View being measured is a ContentView which contains a Label. The native platform (e.g., Android) needs to know the size of the ContentView, given some constraints. Let's say 100 units for the width, 200 units for the height.

The platform calls Measure() on the ContentView's backing view (e.g., on Android this would be a ContentViewGroup) with the constraints. The backing view converts those constraints to cross-platform units (if necessary), and then calls its CrossPlatformMeasure() method with those constraints to determine how large the content (the Label in this case) would like to be.

The CrossPlatformMeasure() method is responsible for calling the Label's Measure() method. The Label measures its native counterpart (more on that later), and updates its DesiredSize property based on that measurement. That value is returned to to the backing view as the result of CrossPlatformMeasure(). The backing view does whatever internal bookkeeping it needs to, then returns its measured size to the platform.

Measuring a Layout

The process for Layout measurement is basically the same as described above, except that multiple child views need to be measured:

sequenceDiagram
    participant P as Platform
    participant BV as Layout Backing View
    participant XV as Cross-Platform Layout
    participant C as Child View
    P->>BV: Measure
    BV->>XV: Cross-Platform Measure
    loop Each Child
        XV->>C: Measure
        C->>XV: DesiredSize
    end
    Note over XV: Update DesiredSize
    XV->>BV: DesiredSize
    Note over BV: Internal bookkeeping (e.g. SetMeasuredDimension())
    BV->>P: Size
Loading

The process of iterating over the child views and measuring each one is generally handled by the LayoutManager for each type of Layout, inside of the LayoutManager's Measure() method.

Measuring a View

Any time a cross-platform View is measured, it hands off the actual measurement to its native counterpart.

For instance, in the example above, the Label's Measure() method takes the constraints it is given by CrossPlatformMeasure() and makes any appropriate adjustments (for example, subtracting its margins). It then hands off the updated constraints to its Handler's GetDesiredSize() method. The Handler is aware of the native control (a TextView, on Android); the Handler takes care of converting the constraints to appropriate values for the platform and calling the native control's version of Measure(). The Handler takes the return value from the native measurement and converts it back to cross-platform values (if necessary) and returns it to the Label.

The Label adjusts the result if necessary (for example, by adding back in the size of its margins), and then tracks that result in its DesiredSize property. It then returns that value as the result of Measure().

sequenceDiagram
	participant V as View
	participant H as Handler
	participant NV as Native View
	Note over V: Adjust for margins
	V->>H: GetDesiredSize
	Note over H: Convert to native 
	H->>NV: Measure
	NV->>H: Size
	Note over H: Convert to x-plat
	H->>V: Size
	Note over V: Adjust for margins, record DesiredSize
Loading

Other Notes

Each platform handles layout slightly differently; one of the goals of the cross-platform layout code is to be as platform-agnostic as possible. All special-case scenarios should be dealt with in the handler platform code for each platform. #if in cross-platform layout code is a code smell; if it's ever necessary (and it probably shouldn't be), it needs to have a lot of explanation.

As a general rule, any layout pass should call Measure() before calling Arrange(). It's perfectly legal to call Measure() multiple times before calling Arrange(); a platform may need to do some speculative measurements before arranging views.

It is also legal to call Arrange() multiple times at different sizes/locations as long as Measure() has been called at least once. For example, a desktop application may determine that a window resizing operation requires arranging a view at a different location, but that the changes to the window size could not have affected the measurements of the view - there's no reason to re-measure the view.

Each platform generally handles its own optimization of measurement operations; the platform code is much better at making such decisions than the cross-platform layout code ever could be. The goal of the cross-platform code should be to get out of the way and allow the platform to do its own optimization. For example, if the cross-platform code calls Measure() on an Android view twice in a row with the same measureSpec values, the native Android code will simply return the cached value unless it determines that there's a good underlying reason for the native view to be remeasured. Attempting to make that decision (or cache the measurement value) at the cross-platform level defeats the native decision making (which has far superior local knowledge).

Platform Notes

Measurement on Android

MeasureSpec

When you see native Android measurement methods, usually they take parameters called widthMeasureSpec and heightMeasureSpec. You may notice that these tend are integer values. It is very important to note that these are not equivalent to the double widthConstraint and double heightConstraint values that we use for cross-platform measurement in MAUI. Rather, these are an Android construct - a packed integer value that includes both the size of the constraint and the type of constraint.

The mode of the measureSpec (that is, the type of constraint), is packed into the upper bits of the integer. The possible options are UNSPECIFIED, EXACTLY, and AT_MOST. See https://developer.android.com/reference/android/view/View.MeasureSpec for more info on what those values mean.

The size of the measureSpec is in the rest of the bits. We have convenience methods in the MeasureSpec class for extracting both the size of the spec and the mode.

So even though a measureSpec is an integer, you cannot simply add or subtract values from it. If you want to increase or decrease the measurement constraints, you need to unpack the size, make your changes, and pack that into a new measureSpec using MeasureSpec.MakeMeasureSpec(). If you try to simply add or subtract values from a measureSpec, the nature of integers and integer overflow will result in nonsense values.

It is also important to remember that measureSpec size values should always be in native pixels. This means that when constructing a measureSpec from cross-platform sizes, you need to convert the sizes using Context.ToPixels().

Legacy Notes

OnSizeAllocated

The VisualElement.OnSizeAllocated(double width, double height) method is still available for overriding to support controls being ported from Xamarin.Forms to MAUI.Controls. In Forms, this method was typically used to react to size changes; occasionally it was a way to ensure that a control was part of the control hierarchy and ready to show up on screen.

This method does not exist at all in MAUI.Core; in MAUI.Controls, it's available for backward compatibility and is called when the Frame (also aliased as Bounds for backward compatibility) is updated during the Arrange portion of the layout process. Specifically, it's called when the Frame is set; for a typical VisualElement this is during the ArrangeOverride() method, right before the native platform's arrange method is called.

When creating a custom component for MAUI.Controls, the suggested customization point is an override of the ArrangeOverride(Rect bounds) method, rather than OnSizeAllocated(). This provides more flexibility.

Xamarin.Forms -> MAUI.Controls Layout Differences

StackLayout

There are a few differences between the stack layouts in MAUI (StackLayout, VerticalStackLayout, and HorizontalStackLayout) and the StackLayout in Xamarin.Forms. The first is that the MAUI stack layouts are very simple; they stack their child views in a single direction until all of them have been stacked. They will keep going until the last child has been stacked, even if that takes them beyond the available space in the stacking direction. MAUI stack layouts simply arrange controls in a particular direction; they do not subdivide a space.

This is in contrast to the Xamarin.Forms StackLayout, which changes its behavior based on circumstances and the presence of any "AndExpand" layout options (e.g., FillAndExpand or CenterAndExpand). Sometimes the Forms StackLayout subdivides the space, expanding to (or stopping at) the edge of its container; in other cases, it expands beyond its container. All of these special cases impact layout performance and make the behavior of the StackLayout more difficult to reason about.

This brings us to the second main difference: the MAUI VerticalStackLayout and HorizontalStackLayout do not recognize the "AndExpand" layout options. If they see a child with layout options that include "AndExpand", they simply treat it as if the "AndExpand" weren't there - e.g., FillAndExpand becomes Fill.

For simplicity of migration from Forms to MAUI, the MAUI.Controls StackLayout does honor "AndExpand", at least for the time being. All of the "AndExpand" options have been marked Obsolete. If you want to avoid the warning about the obsolete properties, you should convert your layouts which use "AndExpand" options to the appropriate layout type. The suggested process is as follows:

  1. If your layout is anything other than a StackLayout, remove all uses of "AndExpand". Just as in Xamarin.Forms, the "AndExpand" options have no effect on any layout other than StackLayout. If your layout wasn't a StackLayout, "AndExpand" was never doing anything.

  2. Remove any "AndExpand" properties which are orthogonal to the stacking direction. For example, if you have a StackLayout with an Orientation of Vertical, and it has a child with a HorizontalAligment="CenterAndExpand" - that "AndExpand" does nothing. You can just remove it.

  3. If you have any remaining "AndExpand" properties on a StackLayout, you should convert that StackLayout to a Grid; the Grid is designed to subdivide a space, and will provide the layout that "AndExpand" provided in Xamarin.Forms. For example,

<StackLayout>
	<Label Text="howdy"/>
	<Image VerticalOptions="FillAndExpand" src="dotnetbot.png"/>
</StackLayout>

can be converted to

<Grid RowDefinitions="Auto, *">
	<Label Text="howdy"/>
	<Image Grid.Row="1" src="dotnetbot.png"/>
</StackLayout>

Anything that was marked "AndExpand" should go in its own row or column with a size of "*".

ScrollView Changes

The other main difference is that the Xamarin.Forms ScrollView does not behave consistently when stacking. It has some arbitrary limits on minimum size which depend partially on its content, and it will compress to allow other items to fit on the screen inside a StackLayout in ways that are inconsistent and sometimes surprising.

MAUI, on the other hand, simply allows the ScrollView to expand its viewport to the size of its content unless otherwise constrained. This means that inside of a VerticalStackLayout, which can expand infinitely, a ScrollView will simply set the height of its viewport to the height of the contnet; it will not scroll. This can be a little surprising for Forms users. Remember, StackLayouts simply continue in their stacking direction until they run out of content; they do not subdivide their container along that axis. If you want to limit your content to a constrained space in a direction, you should use another control, like a Grid.

So instead of this:

<StackLayout> 
	<ScrollView>...</ScrollView>
</StackLayout>

you probably want this:

<Grid>
	<ScrollView>...</ScrollView>
</Grid>

This also applies when putting the ScrollView in a Grid row/column marked as Auto. In Forms, this situation would treat the ScrollView's size as Auto right up until the Grid would no longer fit on a screen - then it would limit the size of the ScrollView to the available space (effectively changing the Auto value to *). This special treatment is confusing and more costly to compute, so in MAUI the ScrollView simply follows the same rules as everything else - be as big as you want to be.

Grid

The biggest change in Grid behavior between Xamarin.Forms and MAUI.Controls is that Grids don't automagically add missing rows/columns for you anymore. For example, in Forms you could do this:

<Grid>
	<Label Text="Hello"/>
	<Label Grid.Row="1" Text="World"/>
</Grid>

And even though you didn't declare that there were two rows in the Grid, Forms would guess that's what you wanted and add the second row for you. MAUI.Controls doesn't do that; you have to explicitly say you want RowDefinitions="Auto,Auto". This was changed for performance reasons.

However, MAUI.Controls does still assume the zeroth row/column for you. That is, if you don't declare any RowDefinitions or ColumnDefinitions, then the default is RowDefinitions="*" and ColumnDefinitions="*"

General Changes

MAUI.Controls generally honors explicit size requests. If you ask for a control to be 200 points wide, then MAUI will oblige you and make that control 200 points wide, even if the container is only 100 points wide.