maui-linux/Xamarin.Forms.Platform.Android/Renderers/ScrollViewRenderer.cs

514 строки
14 KiB
C#

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Android.Animation;
using Android.Content;
using Android.Graphics;
using Android.Views;
using Android.Widget;
using Xamarin.Forms.Internals;
using AScrollView = Android.Widget.ScrollView;
using AView = Android.Views.View;
namespace Xamarin.Forms.Platform.Android
{
public class ScrollViewRenderer : AScrollView, IVisualElementRenderer, IEffectControlProvider
{
ScrollViewContainer _container;
HorizontalScrollView _hScrollView;
ScrollBarVisibility _defaultHorizontalScrollVisibility = 0;
ScrollBarVisibility _defaultVerticalScrollVisibility = 0;
bool _isAttached;
internal bool ShouldSkipOnTouch;
bool _isBidirectional;
ScrollView _view;
int _previousBottom;
bool _isEnabled;
bool _disposed;
LayoutDirection _prevLayoutDirection = LayoutDirection.Ltr;
bool _checkedForRtlScroll = false;
public ScrollViewRenderer(Context context) : base(context)
{
}
[Obsolete("This constructor is obsolete as of version 2.5. Please use ScrollViewRenderer(Context) instead.")]
public ScrollViewRenderer() : base(Forms.Context)
{
}
protected IScrollViewController Controller
{
get { return (IScrollViewController)Element; }
}
internal float LastX { get; set; }
internal float LastY { get; set; }
public VisualElement Element
{
get { return _view; }
}
public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
event EventHandler<PropertyChangedEventArgs> ElementPropertyChanged;
event EventHandler<PropertyChangedEventArgs> IVisualElementRenderer.ElementPropertyChanged
{
add { ElementPropertyChanged += value; }
remove { ElementPropertyChanged -= value; }
}
public SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint)
{
Measure(widthConstraint, heightConstraint);
return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight), new Size(40, 40));
}
public void SetElement(VisualElement element)
{
ScrollView oldElement = _view;
_view = (ScrollView)element;
if (oldElement != null)
{
oldElement.PropertyChanged -= HandlePropertyChanged;
((IScrollViewController)oldElement).ScrollToRequested -= OnScrollToRequested;
}
if (element != null)
{
OnElementChanged(new VisualElementChangedEventArgs(oldElement, element));
if (_container == null)
{
Tracker = new VisualElementTracker(this);
_container = new ScrollViewContainer(_view, Context);
}
_view.PropertyChanged += HandlePropertyChanged;
Controller.ScrollToRequested += OnScrollToRequested;
LoadContent();
UpdateBackgroundColor();
UpdateOrientation();
UpdateIsEnabled();
UpdateHorizontalScrollBarVisibility();
UpdateVerticalScrollBarVisibility();
UpdateFlowDirection();
element.SendViewInitialized(this);
if (!string.IsNullOrEmpty(element.AutomationId))
ContentDescription = element.AutomationId;
}
EffectUtilities.RegisterEffectControlProvider(this, oldElement, element);
}
void UpdateFlowDirection()
{
if (Element is IVisualElementController controller)
{
var flowDirection = controller.EffectiveFlowDirection.IsLeftToRight()
? LayoutDirection.Ltr
: LayoutDirection.Rtl;
if (_prevLayoutDirection != flowDirection && _hScrollView != null)
{
_prevLayoutDirection = flowDirection;
_hScrollView.LayoutDirection = flowDirection;
}
}
}
public VisualElementTracker Tracker { get; private set; }
public void UpdateLayout()
{
if (Tracker != null)
Tracker.UpdateLayout();
}
public ViewGroup ViewGroup => this;
AView IVisualElementRenderer.View => this;
public override void Draw(Canvas canvas)
{
canvas.ClipRect(canvas.ClipBounds);
base.Draw(canvas);
}
public override bool OnInterceptTouchEvent(MotionEvent ev)
{
if (Element.InputTransparent)
return false;
// set the start point for the bidirectional scroll;
// Down is swallowed by other controls, so we'll just sneak this in here without actually preventing
// other controls from getting the event.
if (_isBidirectional && ev.Action == MotionEventActions.Down)
{
LastY = ev.RawY;
LastX = ev.RawX;
}
return base.OnInterceptTouchEvent(ev);
}
public override bool OnTouchEvent(MotionEvent ev)
{
if (!_isEnabled)
return false;
if (ShouldSkipOnTouch)
{
ShouldSkipOnTouch = false;
return false;
}
// The nested ScrollViews will allow us to scroll EITHER vertically OR horizontally in a single gesture.
// This will allow us to also scroll diagonally.
// We'll fall through to the base event so we still get the fling from the ScrollViews.
// We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending
// on the initial direction of movement (i.e., horizontal/vertical).
if (_isBidirectional && !Element.InputTransparent)
{
float dX = LastX - ev.RawX;
LastY = ev.RawY;
LastX = ev.RawX;
if (ev.Action == MotionEventActions.Move)
{
foreach (AHorizontalScrollView child in this.GetChildrenOfType<AHorizontalScrollView>())
{
child.ScrollBy((int)dX, 0);
break;
}
// Fall through to base.OnTouchEvent, it'll take care of the Y scrolling
}
}
return base.OnTouchEvent(ev);
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
SetElement(null);
Tracker?.Dispose();
Tracker = null;
RemoveAllViews();
_container?.Dispose();
_container = null;
}
base.Dispose(disposing);
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
_isAttached = true;
}
protected override void OnDetachedFromWindow()
{
base.OnDetachedFromWindow();
_isAttached = false;
}
protected virtual void OnElementChanged(VisualElementChangedEventArgs e)
{
EventHandler<VisualElementChangedEventArgs> changed = ElementChanged;
if (changed != null)
changed(this, e);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
// If the scroll view has changed size because of soft keyboard dismissal
// (while WindowSoftInputModeAdjust is set to Resize), then we may need to request a
// layout of the ScrollViewContainer
bool requestContainerLayout = bottom > _previousBottom;
_previousBottom = bottom;
base.OnLayout(changed, left, top, right, bottom);
if (_view.Content != null && _hScrollView != null)
_hScrollView.Layout(0, 0, right - left, Math.Max(bottom - top, (int)Context.ToPixels(_view.Content.Height)));
else if (_view.Content != null && requestContainerLayout)
_container?.RequestLayout();
// if the target sdk >= 17 then setting the LayoutDirection on the scroll view natively takes care of the scroll
if (!_checkedForRtlScroll && _hScrollView != null && Element is IVisualElementController controller && controller.EffectiveFlowDirection.IsRightToLeft())
{
if (Context.TargetSdkVersion() < 17)
_hScrollView.ScrollX = _container.MeasuredWidth - _hScrollView.MeasuredWidth - _hScrollView.ScrollX;
else
Device.BeginInvokeOnMainThread(() => UpdateScrollPosition(_hScrollView.ScrollX, ScrollY));
}
_checkedForRtlScroll = true;
}
protected override void OnScrollChanged(int l, int t, int oldl, int oldt)
{
_checkedForRtlScroll = true;
base.OnScrollChanged(l, t, oldl, oldt);
var context = Context;
UpdateScrollPosition(context.FromPixels(l), context.FromPixels(t));
}
internal void UpdateScrollPosition(double x, double y)
{
if (_view != null)
{
if (_view.Orientation == ScrollOrientation.Both)
{
var context = Context;
if (x == 0)
x = context.FromPixels(_hScrollView.ScrollX);
if (y == 0)
y = context.FromPixels(ScrollY);
}
Controller.SetScrolledPosition(x, y);
}
}
void IEffectControlProvider.RegisterEffect(Effect effect)
{
var platformEffect = effect as PlatformEffect;
if (platformEffect != null)
OnRegisterEffect(platformEffect);
}
void OnRegisterEffect(PlatformEffect effect)
{
effect.SetContainer(this);
effect.SetControl(this);
}
static int GetDistance(double start, double position, double v)
{
return (int)(start + (position - start) * v);
}
void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
ElementPropertyChanged?.Invoke(this, e);
if (e.PropertyName == "Content")
LoadContent();
else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName)
UpdateBackgroundColor();
else if (e.PropertyName == ScrollView.OrientationProperty.PropertyName)
UpdateOrientation();
else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName)
UpdateIsEnabled();
else if (e.PropertyName == ScrollView.HorizontalScrollBarVisibilityProperty.PropertyName)
UpdateHorizontalScrollBarVisibility();
else if (e.PropertyName == ScrollView.VerticalScrollBarVisibilityProperty.PropertyName)
UpdateVerticalScrollBarVisibility();
else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
UpdateFlowDirection();
}
void UpdateIsEnabled()
{
if (Element == null)
{
return;
}
_isEnabled = Element.IsEnabled;
}
void LoadContent()
{
_container.ChildView = _view.Content;
}
async void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
{
_checkedForRtlScroll = true;
if (!_isAttached)
{
return;
}
// 99.99% of the time simply queuing to the end of the execution queue should handle this case.
// However it is possible to end a layout cycle and STILL be layout requested. We want to
// back off until all are done, even if they trigger layout storms over and over. So we back off
// for 10ms tops then move on.
var cycle = 0;
while (IsLayoutRequested)
{
await Task.Delay(TimeSpan.FromMilliseconds(1));
cycle++;
if (cycle >= 10)
break;
}
var context = Context;
var x = (int)context.ToPixels(e.ScrollX);
var y = (int)context.ToPixels(e.ScrollY);
int currentX = _view.Orientation == ScrollOrientation.Horizontal || _view.Orientation == ScrollOrientation.Both ? _hScrollView.ScrollX : ScrollX;
int currentY = _view.Orientation == ScrollOrientation.Vertical || _view.Orientation == ScrollOrientation.Both ? ScrollY : _hScrollView.ScrollY;
if (e.Mode == ScrollToMode.Element)
{
Point itemPosition = Controller.GetScrollPositionForElement(e.Element as VisualElement, e.Position);
x = (int)context.ToPixels(itemPosition.X);
y = (int)context.ToPixels(itemPosition.Y);
}
if (e.ShouldAnimate)
{
ValueAnimator animator = ValueAnimator.OfFloat(0f, 1f);
animator.SetDuration(1000);
animator.Update += (o, animatorUpdateEventArgs) =>
{
var v = (double)animatorUpdateEventArgs.Animation.AnimatedValue;
int distX = GetDistance(currentX, x, v);
int distY = GetDistance(currentY, y, v);
if (_view == null)
{
// This is probably happening because the page with this Scroll View
// was popped off the stack during animation
animator.Cancel();
return;
}
switch (_view.Orientation)
{
case ScrollOrientation.Horizontal:
_hScrollView.ScrollTo(distX, distY);
break;
case ScrollOrientation.Vertical:
ScrollTo(distX, distY);
break;
default:
_hScrollView.ScrollTo(distX, distY);
ScrollTo(distX, distY);
break;
}
};
animator.AnimationEnd += delegate
{
if (Controller == null)
return;
Controller.SendScrollFinished();
};
animator.Start();
}
else
{
switch (_view.Orientation)
{
case ScrollOrientation.Horizontal:
_hScrollView.ScrollTo(x, y);
break;
case ScrollOrientation.Vertical:
ScrollTo(x, y);
break;
default:
_hScrollView.ScrollTo(x, y);
ScrollTo(x, y);
break;
}
Controller.SendScrollFinished();
}
}
void IVisualElementRenderer.SetLabelFor(int? id)
{
}
void UpdateBackgroundColor()
{
SetBackgroundColor(Element.BackgroundColor.ToAndroid(Color.Transparent));
}
void UpdateOrientation()
{
if (_view.Orientation == ScrollOrientation.Horizontal || _view.Orientation == ScrollOrientation.Both)
{
if (_hScrollView == null)
{
_hScrollView = new AHorizontalScrollView(Context, this);
UpdateFlowDirection();
}
((AHorizontalScrollView)_hScrollView).IsBidirectional = _isBidirectional = _view.Orientation == ScrollOrientation.Both;
if (_hScrollView.Parent != this)
{
_container.RemoveFromParent();
_hScrollView.AddView(_container);
AddView(_hScrollView);
}
}
else
{
if (_container.Parent != this)
{
_container.RemoveFromParent();
if (_hScrollView != null)
_hScrollView.RemoveFromParent();
AddView(_container);
}
}
}
void UpdateHorizontalScrollBarVisibility()
{
if (_hScrollView != null)
{
if (_defaultHorizontalScrollVisibility == 0)
{
_defaultHorizontalScrollVisibility = _hScrollView.HorizontalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never;
}
var newHorizontalScrollVisiblility = _view.HorizontalScrollBarVisibility;
if (newHorizontalScrollVisiblility == ScrollBarVisibility.Default)
{
newHorizontalScrollVisiblility = _defaultHorizontalScrollVisibility;
}
_hScrollView.HorizontalScrollBarEnabled = newHorizontalScrollVisiblility == ScrollBarVisibility.Always;
}
}
void UpdateVerticalScrollBarVisibility()
{
if (_defaultVerticalScrollVisibility == 0)
_defaultVerticalScrollVisibility = VerticalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never;
var newVerticalScrollVisibility = _view.VerticalScrollBarVisibility;
if (newVerticalScrollVisibility == ScrollBarVisibility.Default)
newVerticalScrollVisibility = _defaultVerticalScrollVisibility;
VerticalScrollBarEnabled = newVerticalScrollVisibility == ScrollBarVisibility.Always;
}
}
}