@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Reactive ;
using Avalonia.Controls.Primitives ;
using Avalonia.Input ;
using Avalonia.Input.GestureRecognizers ;
using Avalonia.Utilities ;
using Avalonia.VisualTree ;
@ -14,6 +15,7 @@ namespace Avalonia.Controls.Presenters
public class ScrollContentPresenter : ContentPresenter , IPresenter , IScrollable , IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1 ;
private const int ProximityPoints = 1 0 ;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -57,6 +59,30 @@ namespace Avalonia.Controls.Presenters
o = > o . Viewport ,
( o , v ) = > o . Viewport = v ) ;
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty < SnapPointsType > HorizontalSnapPointsTypeProperty =
ScrollViewer . HorizontalSnapPointsTypeProperty . AddOwner < ScrollContentPresenter > ( ) ;
/// <summary>
/// Defines the <see cref="VerticalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty < SnapPointsType > VerticalSnapPointsTypeProperty =
ScrollViewer . VerticalSnapPointsTypeProperty . AddOwner < ScrollContentPresenter > ( ) ;
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty < SnapPointsAlignment > HorizontalSnapPointsAlignmentProperty =
ScrollViewer . HorizontalSnapPointsAlignmentProperty . AddOwner < ScrollContentPresenter > ( ) ;
/// <summary>
/// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty < SnapPointsAlignment > VerticalSnapPointsAlignmentProperty =
ScrollViewer . VerticalSnapPointsAlignmentProperty . AddOwner < ScrollContentPresenter > ( ) ;
/// <summary>
/// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary>
@ -71,10 +97,19 @@ namespace Avalonia.Controls.Presenters
private IDisposable ? _l ogicalScrollSubscription ;
private Size _ viewport ;
private Dictionary < int , Vector > ? _ activeLogicalGestureScrolls ;
private Dictionary < int , Vector > ? _ scrollGestureSnapPoints ;
private List < Control > ? _ anchorCandidates ;
private Control ? _ anchorElement ;
private Rect _ anchorElementBounds ;
private bool _ isAnchorElementDirty ;
private bool _ areVerticalSnapPointsRegular ;
private bool _ areHorizontalSnapPointsRegular ;
private IReadOnlyList < double > ? _ horizontalSnapPoints ;
private double _ horizontalSnapPoint ;
private IReadOnlyList < double > ? _ verticalSnapPoints ;
private double _ verticalSnapPoint ;
private double _ verticalSnapPointOffset ;
private double _ horizontalSnapPointOffset ;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -93,6 +128,7 @@ namespace Avalonia.Controls.Presenters
AddHandler ( RequestBringIntoViewEvent , BringIntoViewRequested ) ;
AddHandler ( Gestures . ScrollGestureEvent , OnScrollGesture ) ;
AddHandler ( Gestures . ScrollGestureEndedEvent , OnScrollGestureEnded ) ;
AddHandler ( Gestures . ScrollGestureInertiaStartingEvent , OnScrollGestureInertiaStartingEnded ) ;
this . GetObservable ( ChildProperty ) . Subscribe ( UpdateScrollableSubscription ) ;
}
@ -142,6 +178,42 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise ( ViewportProperty , ref _ viewport , value ) ; }
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
/// </summary>
public SnapPointsType HorizontalSnapPointsType
{
get = > GetValue ( HorizontalSnapPointsTypeProperty ) ;
set = > SetValue ( HorizontalSnapPointsTypeProperty , value ) ;
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
/// </summary>
public SnapPointsType VerticalSnapPointsType
{
get = > GetValue ( VerticalSnapPointsTypeProperty ) ;
set = > SetValue ( VerticalSnapPointsTypeProperty , value ) ;
}
/// <summary>
/// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment HorizontalSnapPointsAlignment
{
get = > GetValue ( HorizontalSnapPointsAlignmentProperty ) ;
set = > SetValue ( HorizontalSnapPointsAlignmentProperty , value ) ;
}
/// <summary>
/// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment VerticalSnapPointsAlignment
{
get = > GetValue ( VerticalSnapPointsAlignmentProperty ) ;
set = > SetValue ( VerticalSnapPointsAlignmentProperty , value ) ;
}
/// <summary>
/// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary>
@ -424,6 +496,25 @@ namespace Avalonia.Controls.Presenters
}
Vector newOffset = new Vector ( x , y ) ;
if ( _ scrollGestureSnapPoints ? . TryGetValue ( e . Id , out var snapPoint ) = = true )
{
double xOffset = x ;
double yOffset = y ;
if ( HorizontalSnapPointsType ! = SnapPointsType . None )
{
xOffset = delta . X < 0 ? Math . Max ( snapPoint . X , newOffset . X ) : Math . Min ( snapPoint . X , newOffset . X ) ;
}
if ( VerticalSnapPointsType ! = SnapPointsType . None )
{
yOffset = delta . Y < 0 ? Math . Max ( snapPoint . Y , newOffset . Y ) : Math . Min ( snapPoint . Y , newOffset . Y ) ;
}
newOffset = new Vector ( xOffset , yOffset ) ;
}
bool offsetChanged = newOffset ! = Offset ;
Offset = newOffset ;
@ -434,7 +525,65 @@ namespace Avalonia.Controls.Presenters
}
private void OnScrollGestureEnded ( object? sender , ScrollGestureEndedEventArgs e )
= > _ activeLogicalGestureScrolls ? . Remove ( e . Id ) ;
{
_ activeLogicalGestureScrolls ? . Remove ( e . Id ) ;
_ scrollGestureSnapPoints ? . Remove ( e . Id ) ;
Offset = SnapOffset ( Offset ) ;
}
private void OnScrollGestureInertiaStartingEnded ( object? sender , ScrollGestureInertiaStartingEventArgs e )
{
if ( Content is not IScrollSnapPointsInfo )
return ;
if ( _ scrollGestureSnapPoints = = null )
_ scrollGestureSnapPoints = new Dictionary < int , Vector > ( ) ;
var offset = Offset ;
if ( HorizontalSnapPointsType ! = SnapPointsType . None & & VerticalSnapPointsType ! = SnapPointsType . None )
{
return ;
}
double xDistance = 0 ;
double yDistance = 0 ;
if ( HorizontalSnapPointsType ! = SnapPointsType . None )
{
xDistance = HorizontalSnapPointsType = = SnapPointsType . Mandatory ? GetDistance ( e . Inertia . X ) : 0 ;
}
if ( VerticalSnapPointsType ! = SnapPointsType . None )
{
yDistance = VerticalSnapPointsType = = SnapPointsType . Mandatory ? GetDistance ( e . Inertia . Y ) : 0 ;
}
offset = new Vector ( offset . X + xDistance , offset . Y + yDistance ) ;
System . Diagnostics . Debug . WriteLine ( $"{offset}" ) ;
_ scrollGestureSnapPoints . Add ( e . Id , SnapOffset ( offset ) ) ;
double GetDistance ( double speed )
{
var time = Math . Log ( ScrollGestureRecognizer . InertialScrollSpeedEnd / Math . Abs ( speed ) ) / Math . Log ( ScrollGestureRecognizer . InertialResistance ) ;
double timeElapsed = 0 , distance = 0 , step = 0 ;
while ( timeElapsed < = time )
{
double s = speed * Math . Pow ( ScrollGestureRecognizer . InertialResistance , timeElapsed ) ;
distance + = ( s * step ) ;
timeElapsed + = 0.016f ;
step = 0.016f ;
}
return distance ;
}
}
/// <inheritdoc/>
protected override void OnPointerWheelChanged ( PointerWheelEventArgs e )
@ -458,6 +607,30 @@ namespace Avalonia.Controls.Presenters
if ( Extent . Height > Viewport . Height )
{
double height = isLogical ? scrollable ! . ScrollSize . Height : 5 0 ;
if ( VerticalSnapPointsType = = SnapPointsType . MandatorySingle & & Content is IScrollSnapPointsInfo )
{
if ( _ areVerticalSnapPointsRegular )
{
height = _ verticalSnapPoint ;
}
else if ( _ verticalSnapPoints ! = null )
{
double yOffset = Offset . Y ;
switch ( VerticalSnapPointsAlignment )
{
case SnapPointsAlignment . Center :
yOffset + = Viewport . Height / 2 ;
break ;
case SnapPointsAlignment . Far :
yOffset + = Viewport . Height ;
break ;
}
var snapPoint = FindNearestSnapPoint ( _ verticalSnapPoints , yOffset , out var lowerSnapPoint ) ;
height = snapPoint - lowerSnapPoint ;
}
}
y + = - delta . Y * height ;
y = Math . Max ( y , 0 ) ;
y = Math . Min ( y , Extent . Height - Viewport . Height ) ;
@ -466,12 +639,37 @@ namespace Avalonia.Controls.Presenters
if ( Extent . Width > Viewport . Width )
{
double width = isLogical ? scrollable ! . ScrollSize . Width : 5 0 ;
if ( HorizontalSnapPointsType = = SnapPointsType . MandatorySingle & & Content is IScrollSnapPointsInfo )
{
if ( _ areHorizontalSnapPointsRegular )
{
width = _ horizontalSnapPoint ;
}
else if ( _ horizontalSnapPoints ! = null )
{
double xOffset = Offset . X ;
switch ( VerticalSnapPointsAlignment )
{
case SnapPointsAlignment . Center :
xOffset + = Viewport . Width / 2 ;
break ;
case SnapPointsAlignment . Far :
xOffset + = Viewport . Width ;
break ;
}
var snapPoint = FindNearestSnapPoint ( _ horizontalSnapPoints , xOffset , out var lowerSnapPoint ) ;
width = snapPoint - lowerSnapPoint ;
}
}
x + = - delta . X * width ;
x = Math . Max ( x , 0 ) ;
x = Math . Min ( x , Extent . Width - Viewport . Width ) ;
}
Vector newOffset = new Vector ( x , y ) ;
Vector newOffset = SnapOffset ( new Vector ( x , y ) ) ;
bool offsetChanged = newOffset ! = Offset ;
Offset = newOffset ;
@ -485,10 +683,36 @@ namespace Avalonia.Controls.Presenters
{
InvalidateArrange ( ) ;
}
else if ( change . Property = = ContentProperty )
{
if ( change . OldValue is IScrollSnapPointsInfo oldSnapPointsInfo )
{
oldSnapPointsInfo . VerticalSnapPointsChanged - = ScrollSnapPointsInfoSnapPointsChanged ;
oldSnapPointsInfo . HorizontalSnapPointsChanged + = ScrollSnapPointsInfoSnapPointsChanged ;
}
if ( Content is IScrollSnapPointsInfo scrollSnapPointsInfo )
{
scrollSnapPointsInfo . VerticalSnapPointsChanged + = ScrollSnapPointsInfoSnapPointsChanged ;
scrollSnapPointsInfo . HorizontalSnapPointsChanged + = ScrollSnapPointsInfoSnapPointsChanged ;
}
UpdateSnapPoints ( ) ;
}
else if ( change . Property = = HorizontalSnapPointsAlignmentProperty | |
change . Property = = VerticalSnapPointsAlignmentProperty )
{
UpdateSnapPoints ( ) ;
}
base . OnPropertyChanged ( change ) ;
}
private void ScrollSnapPointsInfoSnapPointsChanged ( object? sender , Interactivity . RoutedEventArgs e )
{
UpdateSnapPoints ( ) ;
}
private void BringIntoViewRequested ( object? sender , RequestBringIntoViewEventArgs e )
{
if ( e . TargetObject is not null )
@ -635,5 +859,145 @@ namespace Avalonia.Controls.Presenters
bounds = p . HasValue ? new Rect ( p . Value , control . Bounds . Size ) : default ;
return p . HasValue ;
}
private void UpdateSnapPoints ( )
{
if ( Content is IScrollSnapPointsInfo scrollSnapPointsInfo )
{
_ areVerticalSnapPointsRegular = scrollSnapPointsInfo . AreVerticalSnapPointsRegular ;
_ areHorizontalSnapPointsRegular = scrollSnapPointsInfo . AreHorizontalSnapPointsRegular ;
if ( ! _ areVerticalSnapPointsRegular )
{
_ verticalSnapPoints = scrollSnapPointsInfo . GetIrregularSnapPoints ( Layout . Orientation . Vertical , VerticalSnapPointsAlignment ) ;
}
else
{
_ verticalSnapPoints = new List < double > ( ) ;
_ verticalSnapPoint = scrollSnapPointsInfo . GetRegularSnapPoints ( Layout . Orientation . Vertical , VerticalSnapPointsAlignment , out _ verticalSnapPointOffset ) ;
}
if ( ! _ areHorizontalSnapPointsRegular )
{
_ horizontalSnapPoints = scrollSnapPointsInfo . GetIrregularSnapPoints ( Layout . Orientation . Horizontal , HorizontalSnapPointsAlignment ) ;
}
else
{
_ horizontalSnapPoints = new List < double > ( ) ;
_ horizontalSnapPoint = scrollSnapPointsInfo . GetRegularSnapPoints ( Layout . Orientation . Vertical , VerticalSnapPointsAlignment , out _ horizontalSnapPointOffset ) ;
}
}
else
{
_ horizontalSnapPoints = new List < double > ( ) ;
_ verticalSnapPoints = new List < double > ( ) ;
}
}
private Vector SnapOffset ( Vector offset )
{
if ( Content is not IScrollSnapPointsInfo )
return offset ;
var diff = GetAlignedDiff ( ) ;
if ( VerticalSnapPointsType ! = SnapPointsType . None )
{
offset = new Vector ( offset . X , offset . Y + diff . Y ) ;
double nearestSnapPoint = offset . Y ;
if ( _ areVerticalSnapPointsRegular )
{
var minSnapPoint = ( int ) ( offset . Y / _ verticalSnapPoint ) * _ verticalSnapPoint + _ verticalSnapPointOffset ;
var maxSnapPoint = minSnapPoint + _ verticalSnapPoint ;
var midPoint = ( minSnapPoint + maxSnapPoint ) / 2 ;
nearestSnapPoint = offset . Y < midPoint ? minSnapPoint : maxSnapPoint ;
}
else if ( _ verticalSnapPoints ! = null & & _ verticalSnapPoints . Count > 0 )
{
var higherSnapPoint = FindNearestSnapPoint ( _ verticalSnapPoints , offset . Y , out var lowerSnapPoint ) ;
var midPoint = ( lowerSnapPoint + higherSnapPoint ) / 2 ;
nearestSnapPoint = offset . Y < midPoint ? lowerSnapPoint : higherSnapPoint ;
}
offset = new Vector ( offset . X , nearestSnapPoint - diff . Y ) ;
}
if ( HorizontalSnapPointsType ! = SnapPointsType . None )
{
offset = new Vector ( offset . X + diff . X , offset . Y ) ;
double nearestSnapPoint = offset . X ;
if ( _ areHorizontalSnapPointsRegular )
{
var minSnapPoint = ( int ) ( offset . X / _ horizontalSnapPoint ) * _ horizontalSnapPoint + _ horizontalSnapPointOffset ;
var maxSnapPoint = minSnapPoint + _ horizontalSnapPoint ;
var midPoint = ( minSnapPoint + maxSnapPoint ) / 2 ;
nearestSnapPoint = offset . X < midPoint ? minSnapPoint : maxSnapPoint ;
}
else if ( _ horizontalSnapPoints ! = null & & _ horizontalSnapPoints . Count > 0 )
{
var higherSnapPoint = FindNearestSnapPoint ( _ horizontalSnapPoints , offset . X , out var lowerSnapPoint ) ;
var midPoint = ( lowerSnapPoint + higherSnapPoint ) / 2 ;
nearestSnapPoint = offset . X < midPoint ? lowerSnapPoint : higherSnapPoint ;
}
offset = new Vector ( nearestSnapPoint - diff . X , offset . Y ) ;
}
Vector GetAlignedDiff ( )
{
var vector = offset ;
switch ( VerticalSnapPointsAlignment )
{
case SnapPointsAlignment . Center :
vector + = new Vector ( 0 , Viewport . Height / 2 ) ;
break ;
case SnapPointsAlignment . Far :
vector + = new Vector ( 0 , Viewport . Height ) ;
break ;
}
switch ( HorizontalSnapPointsAlignment )
{
case SnapPointsAlignment . Center :
vector + = new Vector ( Viewport . Width / 2 , 0 ) ;
break ;
case SnapPointsAlignment . Far :
vector + = new Vector ( Viewport . Width , 0 ) ;
break ;
}
return vector - offset ;
}
return offset ;
}
private static double FindNearestSnapPoint ( IReadOnlyList < double > snapPoints , double value , out double lowerSnapPoint )
{
var point = snapPoints . BinarySearch ( value , Comparer < double > . Default ) ;
if ( point < 0 )
{
point = ~ point ;
lowerSnapPoint = snapPoints [ Math . Max ( 0 , point - 1 ) ] ;
}
else
{
lowerSnapPoint = snapPoints [ point ] ;
point + = 1 ;
}
return snapPoints [ Math . Min ( point , snapPoints . Count - 1 ) ] ;
}
}
}