Skip to content

Commit

Permalink
Add basic docs for ScrollView implementations (#19766)
Browse files Browse the repository at this point in the history
* Add basic docs for ScrollView implementations

* Add more detail about platform differences

* Update spellcheck workflow

* Fix typo

---------

Co-authored-by: Rui Marinho <me@ruimarinho.net>
  • Loading branch information
hartez and rmarinho authored Jan 16, 2024
1 parent c20f10d commit 3890e04
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/spellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
steps:
- uses: actions/checkout@v2
name: Check out the code
- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
name: Setup node
with:
node-version: "16"
node-version: "18"
- run: npm install -g cspell
name: Install cSpell
- run: cspell --config ./cSpell.json "docs/**/*.md" --no-progress
Expand Down
81 changes: 81 additions & 0 deletions docs/design/scrollview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ScrollView Implementation Notes

This document explains the implementation of ScrollView on each platform.

# The Problem with ScrollView

ScrollView is a challenging control to implement in a cross-platform way because the three primary target platforms (Windows, iOS, and Android) all have different rules for their native scrolling content controls. To make things even more challenging, we are also trying to make the .NET MAUI ScrollView work as closely as possible to the Xamarin.Forms ScrollView (for ease of migration).

# Interface

Cross-platform implementations of a ScrollView need to implement the `Microsoft.Maui.IScrollView` interface. In the Maui.Controls library, this implementation is `Microsoft.Maui.Controls.ScrollView`.

`IScrollView` derives from `IContentView` - a ScrollView in MAUI contains a single piece of content. The ScrollView will attempt to expand to be large enough to contain that content unless otherwise constrained; if constrained, the ScrollView will show a subsection of the content in its viewport, and allow scrolling to show the content in directions specified by the `Orientation` property (and depending on the settings for invidual scroll bar visibility).


## Scroll Bar Visibility

`IScrollView` has two scroll bar visibility properties: `HorizontalScrollBarVisibility` and `VerticalScrollBarVisibility`. Both support 3 possible values: `Default`, `Always`, and `Never`.

If the value is set to `Never`, the scroll bar in that direction will not be visible. The content will still be scrollable if it exceeds the size of the viewport.

If the value is set to `Always`, the scroll bar in that direction will be visible even if there isn't sufficient content to require scrolling.

If the value is set to `Default`, the scroll bar visibility will follow the rules of the target platform. Usually this means that the scroll bar will become visible if there is sufficient content to required scrolling in that direction, and the scroll bar will not be visible otherwise. Other behaviors may also apply (such as scroll bar fading) as dictated by the platform.

## Orientation

The `Orientation` property of `IScrollView` can be one of four values (in the `ScrollOrientation` enum):

- `Vertical` - the content scrolls vertically if it's taller than the viewport
- `Horizontal` - the content scrolls horizontally if it's wider than the viewport
- `Both` - the content scrolls horizontally if it's wider than the viewport, and vertically if it's taller than the viewport
- `Neither` - scrolling is disabled in both directions

The `Orientation` value affects how the ScrollView's `Content` is measured and laid out. If the value is `Vertical`, the measurement height is unconstrained (i.e., `Double.Infinity`). If the value is `Horizontal`, the measurement width is unconstrained. `Both` results in measurement being unconstrained in all directions, and `Neither` constrains the measurement to the width and height of the viewport.

## ContentSize

This is a read-only value determined by the actual size of the ScrollView's `Content`, which may (and usually does) exceed the size of the ScrollView itself.

## Offsets

`IScrollView` has two `double` values, `HorizontalOffset` and `VerticalOffset` which specify the offsets of the viewport relative to the content. This can also be thought of as the scroll position of the ScrollView in each direction.

## Scroll methods

`IScrollView` defines two methods related to scrolling. `RequestScrollTo()` is used by the virtual view to request that the native view scroll to the specified horizontal and vertical offsets. It includes a `bool` parameter to specify whether the scrolling operation should be animated or instant.

The other method, `ScrollFinished()`, is called by the native platform to indicate that a scrolling operation has finished. This is used to signal that scrolling is finished for various `async` operations.

# Platform Implementations

The behavior of the `Padding` property on ScrollView (inherited from Forms) requires that the padding is applied _inside_ the scrollable portion of the ScrollView. Also, the content of a ScrollView may have its own `Margin`. The inset of content in a ScrollView is effectively the sum of the ScrollView's `Padding` and the content's `Margin`. However, the various platforms all treat these properties differently within their native ScrollView equivalents. Much of the complexity of the ScrollViewHandler for each platform is addressing these differences.

## Windows

We'll start with Windows, because it's the most confusing. First off, it's important to note that as of this writing, the backing control for the .NET MAUI ScrollView is [`Microsoft.UI.Xaml.Controls.ScrollViewer`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.scrollviewer?view=winrt-22621). It is _not_ [`Microsoft.UI.Xaml.Controls.ScrollView`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.scrollview?view=windows-app-sdk-1.4), as this control was not available when .NET MAUI was first ported from Forms. _This may change in the future._ But for now, the native control is a ScrollView_er_.

The ScrollViewer control behavior differs from the .NET MAUI behavior (and Forms) in a couple of ways:

- The native `Padding` property creates space _around_ the scrollable area, rather than _inside_ of it.
- The ScrollViewer forces the content to start at location (0, 0) in the ScrollViewer, which defeats our cross-platform layout's `Margin` property.

To compound our problems, ScrollViewer is `sealed`. So we cannot override the measure/arrange behavior.

So, to make the Windows implementation of ScrollView work the way we want, we insert an extra layer - a `ContentPanel`. The native `Content` property of the ScrollViewer is set our extra `ContentPanel`, which hosts the content of the virtual ScrollView. This intermediate `ContentPanel` provides our virtual `Padding` and `Margin` property behaviors, and is responsible for invoking the `CrossPlatformMeasure()` and `CrossPlatformArrange()` methods.

## Android

Our Android implementation of ScrollView is backed by MauiScrollView, which is a subclass of NestedScrollView. Again, we have some issues because the fundamentals of ScrollView on Android differ from our .NET MAUI target behaviors:

- Android treats `Padding` as space around the scrollable area, rather than inside of it.
- Android's native measurements will not account for our virtual `Margin` when measuring ScrollView content.

So again, we insert an intermediate `ContentViewGroup` to handle these problems. The `ContentViewGroup` is laid out at (0, 0) in the MauiScrollView; it handles the `Padding` and `Margin` behaviors for us, and initiates `CrossPlatformMeasure()` and `CrossPlatformArrange()` for its `Content`.

Another note: the content of an Android ScrollView does not stretch to fill the viewport by default. That is, if you have a ScrollView which fills the screen and the content of the ScrollView is smaller than the screen, by default that content will not expand to take up the entire viewport (the behavior we expect for .NET MAUI). On Android, we can achieve the behavior we expect by setting the `FillViewport` property to `true` for the native ScrollView. This is all handled automatically by the Android ScrollViewHandler; I note it here because this causes an extra measure pass when the content is smaller than the ScrollView's viewport and the virtual ScrollView has layout alignment set to `Fill`. This is all explained in the comments for the ScrollViewHandler's `GetDesiredSize()` override, but I'm calling it out here as well in case anyone is investigating the number of measure calls being made.

## iOS

The default iOS ScrollView behavior is actually pretty close to what we want for .NET MAUI, but we still use an intermediate ContentView because it gives us a way to invoke the `CrossPlatformMeasure()` and `CrossPlatformArrange()` methods of the ScrollView `Content`.
26 changes: 26 additions & 0 deletions src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,30 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.MainPage"
xmlns:local="clr-namespace:Maui.Controls.Sample">

<Grid RowDefinitions="Auto,*,Auto"
Margin="12">
<Label Text="THE BLACK CAT by Edgar Allan Poe"
FontSize="Medium"
FontAttributes="Bold"
HorizontalOptions="Center" />
<ScrollView x:Name="scrollView" VerticalScrollBarVisibility="Default"
Grid.Row="1">
<StackLayout>
<Label Text="FOR the most wild, yet most homely narrative which I am about to pen, I neither expect nor solicit belief. Mad indeed would I be to expect it, in a case where my very senses reject their own evidence. Yet, mad am I not -- and very surely do I not dream. But to-morrow I die, and to-day I would unburthen my soul. My immediate purpose is to place before the world, plainly, succinctly, and without comment, a series of mere household events. In their consequences, these events have terrified -- have tortured -- have destroyed me. Yet I will not attempt to expound them. To me, they have presented little but Horror -- to many they will seem less terrible than barroques. Hereafter, perhaps, some intellect may be found which will reduce my phantasm to the common-place -- some intellect more calm, more logical, and far less excitable than my own, which will perceive, in the circumstances I detail with awe, nothing more than an ordinary succession of very natural causes and effects." />
<Label Text="From my infancy I was noted for the docility and humanity of my disposition. My tenderness of heart was even so conspicuous as to make me the jest of my companions. I was especially fond of animals, and was indulged by my parents with a great variety of pets. With these I spent most of my time, and never was so happy as when feeding and caressing them. This peculiarity of character grew with my growth, and, in my manhood, I derived from it one of my principal sources of pleasure. To those who have cherished an affection for a faithful and sagacious dog, I need hardly be at the trouble of explaining the nature or the intensity of the gratification thus derivable. There is something in the unselfish and self-sacrificing love of a brute, which goes directly to the heart of him who has had frequent occasion to test the paltry friendship and gossamer fidelity of mere Man." />
<Label Text="I married early, and was happy to find in my wife a disposition not uncongenial with my own. Observing my partiality for domestic pets, she lost no opportunity of procuring those of the most agreeable kind. We had birds, gold-fish, a fine dog, rabbits, a small monkey, and a cat." />
<Label Text="This latter was a remarkably large and beautiful animal, entirely black, and sagacious to an astonishing degree. In speaking of his intelligence, my wife, who at heart was not a little tinctured with superstition, made frequent allusion to the ancient popular notion, which regarded all black cats as witches in disguise. Not that she was ever serious upon this point -- and I mention the matter at all for no better reason than that it happens, just now, to be remembered." />
<Label Text="Pluto -- this was the cat's name -- was my favorite pet and playmate. I alone fed him, and he attended me wherever I went about the house. It was even with difficulty that I could prevent him from following me through the streets." />
<Label Text="Our friendship lasted, in this manner, for several years, during which my general temperament and character -- through the instrumentality of the Fiend Intemperance -- had (I blush to confess it) experienced a radical alteration for the worse. I grew, day by day, more moody, more irritable, more regardless of the feelings of others. I suffered myself to use intemperate language to my wife. At length, I even offered her personal violence. My pets, of course, were made to feel the change in my disposition. I not only neglected, but ill-used them. For Pluto, however, I still retained sufficient regard to restrain me from maltreating him, as I made no scruple of maltreating the rabbits, the monkey, or even the dog, when by accident, or through affection, they came in my way. But my disease grew upon me -- for what disease is like Alcohol ! -- and at length even Pluto, who was now becoming old, and consequently somewhat peevish -- even Pluto began to experience the effects of my ill temper." />
<Label Text="One night, returning home, much intoxicated, from one of my haunts about town, I fancied that the cat avoided my presence. I seized him; when, in his fright at my violence, he inflicted a slight wound upon my hand with his teeth. The fury of a demon instantly possessed me. I knew myself no longer. My original soul seemed, at once, to take its flight from my body; and a more than fiendish malevolence, gin-nurtured, thrilled every fibre of my frame. I took from my waistcoat-pocket a pen-knife, opened it, grasped the poor beast by the throat, and deliberately cut one of its eyes from the socket ! I blush, I burn, I shudder, while I pen the damnable atrocity." />
<Label Text="When reason returned with the morning -- when I had slept off the fumes of the night's debauch -- I experienced a sentiment half of horror, half of remorse, for the crime of which I had been guilty; but it was, at best, a feeble and equivocal feeling, and the soul remained untouched. I again plunged into excess, and soon drowned in wine all memory of the deed." />
<Label x:Name="finalLabel"
Text="In the meantime the cat slowly recovered. The socket of the lost eye presented, it is true, a frightful appearance, but he no longer appeared to suffer any pain. He went about the house as usual, but, as might be expected, fled in extreme terror at my approach. I had so much of my old heart left, as to be at first grieved by this evident dislike on the part of a creature which had once so loved me. But this feeling soon gave place to irritation. And then came, as if to my final and irrevocable overthrow, the spirit of PERVERSENESS. Of this spirit philosophy takes no account. Yet I am not more sure that my soul lives, than I am that perverseness is one of the primitive impulses of the human heart -- one of the indivisible primary faculties, or sentiments, which give direction to the character of Man. Who has not, a hundred times, found himself committing a vile or a silly action, for no other reason than because he knows he should not? Have we not a perpetual inclination, in the teeth of our best judgment, to violate that which is Law, merely because we understand it to be such? This spirit of perverseness, I say, came to my final overthrow. It was this unfathomable longing of the soul to vex itself -- to offer violence to its own nature -- to do wrong for the wrong's sake only -- that urged me to continue and finally to consummate the injury I had inflicted upon the unoffending brute. One morning, in cool blood, I slipped a noose about its neck and hung it to the limb of a tree; -- hung it with the tears streaming from my eyes, and with the bitterest remorse at my heart; -- hung it because I knew that it had loved me, and because I felt it had given me no reason of offence; -- hung it because I knew that in so doing I was committing a sin -- a deadly sin that would so jeopardize my immortal soul as to place it -- if such a thing were possible -- even beyond the reach of the infinite mercy of the Most Merciful and Most Terrible God." />
</StackLayout>
</ScrollView>
<Button Text="Scroll to end"
Grid.Row="2" />
</Grid>

</ContentPage>

0 comments on commit 3890e04

Please sign in to comment.