Skip to content

Commit

Permalink
feat: BringIntoViewRequested handling on ScrollContentPresenter
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinZikmund committed Apr 4, 2022
1 parent 51a8de8 commit 3d09430
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
xmlns:local="using:UITests.Windows_UI_Xaml.UIElementTests"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

Expand All @@ -15,8 +16,8 @@
</Grid.RowDefinitions>
<Button Click="BringItemIntoView_Click">Bring into view</Button>
<ScrollViewer Height="300" Background="Yellow" Padding="20" Grid.Row="1">
<ScrollViewer Height="150" Background="Blue" Margin="0,500,0,0" Padding="20">
<Border x:Name="Item" Background="Red" Width="100" Height="100" Margin="0,500,0,0" />
<ScrollViewer Height="200" Background="Blue" Margin="0,400,0,0" Padding="20">
<Border x:Name="Item" Background="Red" Width="100" Height="100" Margin="0,600,0,0" />
</ScrollViewer>
</ScrollViewer>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI.DataBinding;
using Windows.UI.Xaml.Data;
using Uno.UI.DataBinding;
using System;
using System.Collections;
using System.Collections.Generic;
using Uno.Disposables;
using System.Runtime.CompilerServices;
using System.Text;
using Windows.Foundation;
using Uno.UI;
using Uno.UI.Helpers.WinUI;
using Uno.Extensions;
using Windows.UI.Xaml.Media;
#if XAMARIN_ANDROID
using View = Android.Views.View;
using Font = Android.Graphics.Typeface;
Expand All @@ -27,6 +22,8 @@
using View = Windows.UI.Xaml.UIElement;
#endif

using static Microsoft.UI.Xaml.Controls._Tracing;

namespace Windows.UI.Xaml.Controls
{
public partial class ScrollContentPresenter : ContentPresenter, ILayoutConstraints
Expand All @@ -38,6 +35,7 @@ public ScrollContentPresenter()

InitializeScrollContentPresenter();
}

partial void InitializePartial();

#region ScrollOwner
Expand All @@ -60,6 +58,307 @@ public object ScrollOwner

private ScrollViewer Scroller => ScrollOwner as ScrollViewer;

protected override void OnBringIntoViewRequested(BringIntoViewRequestedEventArgs args)
{
base.OnBringIntoViewRequested(args);

UIElement content = Content as UIElement;

if (args.Handled ||
args.TargetElement == this ||
(args.TargetElement == content && content?.Visibility == Visibility.Collapsed) ||
!SharedHelpers.IsAncestor(args.TargetElement, this, true /*checkVisibility*/))
{
// Ignore the request when:
// - There is no InteractionTracker to fulfill it.
// - It was handled already.
// - The target element is this ScrollPresenter itself. A parent scrollPresenter may fulfill the request instead then.
// - The target element is effectively collapsed within the ScrollPresenter.
return;
}

Rect targetRect = new Rect();
//int offsetsChangeCorrelationId = 0;
double targetZoomedHorizontalOffset = 0.0;
double targetZoomedVerticalOffset = 0.0;
double appliedOffsetX = 0.0;
double appliedOffsetY = 0.0;
var viewportWidth = ViewportWidth;
var viewportHeight = ViewportHeight;
var zoomFactor = Scroller.ZoomFactor;

// Compute the target offsets based on the provided BringIntoViewRequestedEventArgs.
ComputeBringIntoViewTargetOffsets(
content,
args,
out targetZoomedHorizontalOffset,
out targetZoomedVerticalOffset,
out appliedOffsetX,
out appliedOffsetY,
out targetRect);

// Do not include the applied offsets so that potential parent bring-into-view contributors ignore that shift.
Rect nextTargetRect = new Rect(
targetRect.X * zoomFactor - targetZoomedHorizontalOffset - appliedOffsetX,
targetRect.Y * zoomFactor - targetZoomedVerticalOffset - appliedOffsetY,
Math.Min(targetRect.Width * zoomFactor, viewportWidth),
Math.Min(targetRect.Height * zoomFactor, viewportHeight));

Rect viewportRect = new Rect(
0.0f,
0.0f,
(float)viewportWidth,
(float)viewportHeight);

var verticalOffset = Scroller.VerticalOffset;
var horizontalOffset = Scroller.HorizontalOffset;
var zoomedVerticalOffset = verticalOffset;
var zoomedHorizontalOffset = horizontalOffset;

if (targetZoomedHorizontalOffset != zoomedHorizontalOffset ||
targetZoomedVerticalOffset != zoomedVerticalOffset)
{
//com_ptr<ScrollingScrollOptions> options =
// make_self<ScrollingScrollOptions>(
// args.AnimationDesired() ? ScrollingAnimationMode::Auto : ScrollingAnimationMode::Disabled,
// snapPointsMode);

Scroller.ChangeView(targetZoomedHorizontalOffset, targetZoomedVerticalOffset, zoomFactor, !args.AnimationDesired);
//ChangeOffsetsPrivate(
// targetZoomedHorizontalOffset /*zoomedHorizontalOffset*/,
// targetZoomedVerticalOffset /*zoomedVerticalOffset*/,
// ScrollPresenterViewKind::Absolute,
// *options,
// InteractionTrackerAsyncOperationTrigger::DirectViewChange,
// offsetsChangeCorrelationId /*existingViewChangeCorrelationId*/,
// nullptr /*viewChangeCorrelationId*/);
}
else
{
// No offset change was triggered because the target offsets are the same as the current ones. Mark the operation as completed immediately.
//RaiseViewChangeCompleted(true /*isForScroll*/, ScrollPresenterViewChangeResult::Completed, offsetsChangeCorrelationId);
}

if (SharedHelpers.DoRectsIntersect(nextTargetRect, viewportRect))
{
// Next bring a portion of this ScrollPresenter into view.
args.TargetRect = nextTargetRect;
args.TargetElement = this;
args.HorizontalOffset = args.HorizontalOffset - appliedOffsetX;
args.VerticalOffset = args.VerticalOffset - appliedOffsetY;
}
else
{
// This ScrollPresenter did not even partially bring the TargetRect into its viewport.
// Mark the operation as handled since no portion of this ScrollPresenter needs to be brought into view.
args.Handled = true;
}
}

private void ComputeBringIntoViewTargetOffsets(
UIElement content,
BringIntoViewRequestedEventArgs requestEventArgs,
out double targetZoomedHorizontalOffset,
out double targetZoomedVerticalOffset,
out double appliedOffsetX,
out double appliedOffsetY,
out Rect targetRect)
{

targetZoomedHorizontalOffset = 0.0;
targetZoomedVerticalOffset = 0.0;

appliedOffsetX = 0.0;
appliedOffsetY = 0.0;

targetRect = new Rect();

var target = requestEventArgs.TargetElement;

MUX_ASSERT(content != null);
MUX_ASSERT(target != null);

Rect transformedRect = GetDescendantBounds(content, target, requestEventArgs.TargetRect);

double targetX = transformedRect.X;
double targetWidth = transformedRect.Width;
double targetY = transformedRect.Y;
double targetHeight = transformedRect.Height;

var viewportWidth = ViewportWidth;
var viewportHeight = ViewportHeight;
var zoomFactor = Scroller.ZoomFactor;
if (!double.IsNaN(requestEventArgs.HorizontalAlignmentRatio))
{
// Account for the horizontal alignment ratio
MUX_ASSERT(requestEventArgs.HorizontalAlignmentRatio >= 0.0 && requestEventArgs.HorizontalAlignmentRatio <= 1.0);


targetX += (targetWidth - viewportWidth / zoomFactor) * requestEventArgs.HorizontalAlignmentRatio;
targetWidth = viewportWidth / zoomFactor;
}

if (!double.IsNaN(requestEventArgs.VerticalAlignmentRatio))
{
// Account for the vertical alignment ratio
MUX_ASSERT(requestEventArgs.VerticalAlignmentRatio >= 0.0 && requestEventArgs.VerticalAlignmentRatio <= 1.0);

targetY += (targetHeight - viewportHeight / zoomFactor) * requestEventArgs.VerticalAlignmentRatio;
targetHeight = viewportHeight / zoomFactor;
}

var verticalOffset = Scroller.VerticalOffset;
var horizontalOffset = Scroller.HorizontalOffset;
var zoomedVerticalOffset = verticalOffset;
var zoomedHorizontalOffset = horizontalOffset;

double targetZoomedHorizontalOffsetTmp = ComputeZoomedOffsetWithMinimalChange(
zoomedHorizontalOffset,
zoomedHorizontalOffset + viewportWidth,
targetX * zoomFactor,
(targetX + targetWidth) * zoomFactor);
double targetZoomedVerticalOffsetTmp = ComputeZoomedOffsetWithMinimalChange(
zoomedVerticalOffset,
zoomedVerticalOffset + viewportHeight,
targetY * zoomFactor,
(targetY + targetHeight) * zoomFactor);

double scrollableWidth = Scroller.ScrollableWidth;
double scrollableHeight = Scroller.ScrollableHeight;

targetZoomedHorizontalOffsetTmp = targetZoomedHorizontalOffsetTmp.Clamp(0.0, scrollableWidth);
targetZoomedVerticalOffsetTmp = targetZoomedVerticalOffsetTmp.Clamp(0.0, scrollableHeight);

double offsetX = requestEventArgs.HorizontalOffset;
double offsetY = requestEventArgs.VerticalOffset;
double appliedOffsetXTmp = 0.0;
double appliedOffsetYTmp = 0.0;

// If the target offset is within bounds and an offset was provided, apply as much of it as possible while remaining within bounds.
if (offsetX != 0.0 && targetZoomedHorizontalOffsetTmp >= 0.0)
{
if (targetZoomedHorizontalOffsetTmp <= scrollableWidth)
{
if (offsetX > 0.0)
{
appliedOffsetXTmp = Math.Min(targetZoomedHorizontalOffsetTmp, offsetX);
}
else
{
appliedOffsetXTmp = -Math.Min(scrollableWidth - targetZoomedHorizontalOffsetTmp, -offsetX);
}
targetZoomedHorizontalOffsetTmp -= appliedOffsetXTmp;
}
}

if (offsetY != 0.0 && targetZoomedVerticalOffsetTmp >= 0.0)
{
if (targetZoomedVerticalOffsetTmp <= scrollableHeight)
{
if (offsetY > 0.0)
{
appliedOffsetYTmp = Math.Min(targetZoomedVerticalOffsetTmp, offsetY);
}
else
{
appliedOffsetYTmp = -Math.Min(scrollableHeight - targetZoomedVerticalOffsetTmp, -offsetY);
}
targetZoomedVerticalOffsetTmp -= appliedOffsetYTmp;
}
}

MUX_ASSERT(targetZoomedHorizontalOffsetTmp >= 0.0);
MUX_ASSERT(targetZoomedVerticalOffsetTmp >= 0.0);
MUX_ASSERT(targetZoomedHorizontalOffsetTmp <= scrollableWidth);
MUX_ASSERT(targetZoomedVerticalOffsetTmp <= scrollableHeight);

//if (snapPointsMode == ScrollingSnapPointsMode::Default)
//{
// // Finally adjust the target offsets based on snap points
// targetZoomedHorizontalOffsetTmp = ComputeValueAfterSnapPoints<ScrollSnapPointBase>(
// targetZoomedHorizontalOffsetTmp, m_sortedConsolidatedHorizontalSnapPoints);
// targetZoomedVerticalOffsetTmp = ComputeValueAfterSnapPoints<ScrollSnapPointBase>(
// targetZoomedVerticalOffsetTmp, m_sortedConsolidatedVerticalSnapPoints);

// // Make sure the target offsets are within the scrollable boundaries
// targetZoomedHorizontalOffsetTmp = targetZoomedHorizontalOffsetTmp.Clamp(0.0, scrollableWidth);
// targetZoomedVerticalOffsetTmp = targetZoomedVerticalOffsetTmp.Clamp(0.0, scrollableHeight);

// MUX_ASSERT(targetZoomedHorizontalOffsetTmp >= 0.0);
// MUX_ASSERT(targetZoomedVerticalOffsetTmp >= 0.0);
// MUX_ASSERT(targetZoomedHorizontalOffsetTmp <= scrollableWidth);
// MUX_ASSERT(targetZoomedVerticalOffsetTmp <= scrollableHeight);
//}

targetZoomedHorizontalOffset = targetZoomedHorizontalOffsetTmp;
targetZoomedVerticalOffset = targetZoomedVerticalOffsetTmp;

appliedOffsetX = appliedOffsetXTmp;
appliedOffsetY = appliedOffsetYTmp;

targetRect = new Rect(
targetX,
targetY,
targetWidth,
targetHeight);
}

private double ComputeZoomedOffsetWithMinimalChange(
double viewportStart,
double viewportEnd,
double childStart,
double childEnd)
{
bool above = childStart < viewportStart && childEnd < viewportEnd;
bool below = childEnd > viewportEnd && childStart > viewportStart;
bool larger = (childEnd - childStart) > (viewportEnd - viewportStart);

// # CHILD POSITION CHILD SIZE SCROLL REMEDY
// 1 Above viewport <= viewport Down Align top edge of content & viewport
// 2 Above viewport > viewport Down Align bottom edge of content & viewport
// 3 Below viewport <= viewport Up Align bottom edge of content & viewport
// 4 Below viewport > viewport Up Align top edge of content & viewport
// 5 Entirely within viewport NA No change
// 6 Spanning viewport NA No change
if ((above && !larger) || (below && larger))
{
// Cases 1 & 4
return childStart;
}
else if (above || below)
{
// Cases 2 & 3
return childEnd - viewportEnd + viewportStart;
}

// cases 5 & 6
return viewportStart;
}

private Rect GetDescendantBounds(
UIElement content,
UIElement descendant,
Rect descendantRect)
{
MUX_ASSERT(content != null);

FrameworkElement contentAsFE = content as FrameworkElement;
GeneralTransform transform = descendant.TransformToVisual(content);
Thickness contentMargin = new Thickness();

if (contentAsFE != null)
{
contentMargin = contentAsFE.Margin;
}

return transform.TransformBounds(
new Rect(
contentMargin.Left + descendantRect.X,
contentMargin.Top + descendantRect.Y,
descendantRect.Width,
descendantRect.Height));
}

#if __WASM__
bool _forceChangeToCurrentView;

Expand Down Expand Up @@ -165,34 +464,6 @@ protected override Size ArrangeOverride(Size finalSize)
return finalSize;
}

#if !__ANDROID__ && !__IOS__
public Rect MakeVisible(UIElement visual, Rect rectangle)
{
if (visual is FrameworkElement fe && Scroller is not null)
{
var scrollRect = new Rect(
0,
0,
ActualWidth,
ActualHeight
);

var visualPoint = visual.TransformToVisual(this).TransformPoint(new Point());
var visualRect = new Rect(visualPoint, new Size(fe.ActualWidth, fe.ActualHeight));

var deltaX = Math.Min(visualRect.Left - scrollRect.Left, Math.Max(0, visualRect.Right - scrollRect.Right));
var deltaY = Math.Min(visualRect.Top - scrollRect.Top, Math.Max(0, visualRect.Bottom - scrollRect.Bottom));

var targetOffsetX = Scroller.HorizontalOffset + deltaX;
var targetOffsetY = Scroller.VerticalOffset + deltaY;
Scroller.ScrollToHorizontalOffset(targetOffsetX);
Scroller.ScrollToVerticalOffset(targetOffsetY);
}

return rectangle;
}
#endif

internal override bool IsViewHit()
=> true;
#elif __IOS__ // Note: No __ANDROID__, the ICustomScrollInfo support is made directly in the NativeScrollContentPresenter
Expand Down
Loading

0 comments on commit 3d09430

Please sign in to comment.