summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs')
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs531
1 files changed, 531 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs b/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs
new file mode 100644
index 00000000..64e859d7
--- /dev/null
+++ b/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs
@@ -0,0 +1,531 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Android.Content;
+using Android.Graphics;
+using Android.Support.V7.Widget;
+using Android.Views;
+
+namespace Xamarin.Forms.Platform.Android
+{
+ internal class PhysicalLayoutManager : RecyclerView.LayoutManager
+ {
+ // ObservableCollection is our public entryway to this method and it only supports single item removal
+ internal const int MaxItemsRemoved = 1;
+
+ static readonly int s_samplesCount = 5;
+ static Func<int, int> s_fixPosition = o => o;
+
+ readonly Context _context;
+ readonly Queue<Action<RecyclerView.Recycler, RecyclerView.State>> _deferredLayout;
+ readonly List<IntVector> _samples;
+ readonly SeekAndSnapScroller _scroller;
+ readonly Dictionary<int, global::Android.Views.View> _viewByAdaptorPosition;
+ readonly VirtualLayoutManager _virtualLayout;
+ readonly HashSet<int> _visibleAdapterPosition;
+ AdapterChangeType _adapterChangeType;
+ IntVector _locationOffset; // upper left corner of screen is positionOrigin + locationOffset
+ int _positionOrigin; // coordinates are relative to the upper left corner of this element
+
+ public PhysicalLayoutManager(Context context, VirtualLayoutManager virtualLayout, int positionOrigin)
+ {
+ _positionOrigin = positionOrigin;
+ _context = context;
+ _virtualLayout = virtualLayout;
+ _viewByAdaptorPosition = new Dictionary<int, global::Android.Views.View>();
+ _visibleAdapterPosition = new HashSet<int>();
+ _samples = Enumerable.Repeat(IntVector.Origin, s_samplesCount).ToList();
+ _deferredLayout = new Queue<Action<RecyclerView.Recycler, RecyclerView.State>>();
+ _scroller = new SeekAndSnapScroller(context, adapterPosition =>
+ {
+ IntVector end = virtualLayout.LayoutItem(positionOrigin, adapterPosition).Center();
+ IntVector begin = Viewport.Center();
+ return end - begin;
+ });
+
+ _scroller.OnBeginScroll += adapterPosition => OnBeginScroll?.Invoke(adapterPosition);
+ _scroller.OnEndScroll += adapterPosition => OnEndScroll?.Invoke(adapterPosition);
+ }
+
+ public IntVector Velocity => _samples.Aggregate((o, a) => o + a) / _samples.Count;
+
+ public System.Drawing.Rectangle Viewport => Rectangle + _locationOffset;
+
+ // helpers to deal with locations as IntRectangles and IntVectors
+ System.Drawing.Rectangle Rectangle => new System.Drawing.Rectangle(0, 0, Width, Height);
+
+ public override bool CanScrollHorizontally() => _virtualLayout.CanScrollHorizontally;
+
+ public override bool CanScrollVertically() => _virtualLayout.CanScrollVertically;
+
+ public override global::Android.Views.View FindViewByPosition(int adapterPosition)
+ {
+ // Used by SmoothScrollToPosition to know when the view
+ // for the targeted adapterPosition has been attached.
+
+ global::Android.Views.View view;
+ if (!_viewByAdaptorPosition.TryGetValue(adapterPosition, out view))
+ return null;
+ return view;
+ }
+
+ public override RecyclerView.LayoutParams GenerateDefaultLayoutParams()
+ {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
+ }
+
+ public void Layout(int width, int height)
+ {
+ // e.g. when rotated the width and height are updated the virtual layout will
+ // need to resize and provide a new viewport offset given the current one.
+ _virtualLayout.Layout(_positionOrigin, new System.Drawing.Size(width, height), ref _locationOffset);
+ }
+
+ public override void OnAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)
+ {
+ RemoveAllViews();
+ }
+
+ public event Action<int> OnAppearing;
+
+ public event Action<int> OnBeginScroll;
+
+ public event Action<int> OnDisappearing;
+
+ public event Action<int> OnEndScroll;
+
+ public override void OnItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)
+ {
+ _adapterChangeType = AdapterChangeType.Added;
+
+ _deferredLayout.Enqueue((recycler, state) =>
+ {
+ KeyValuePair<int, global::Android.Views.View>[] viewByAdaptorPositionCopy = _viewByAdaptorPosition.ToArray();
+ _viewByAdaptorPosition.Clear();
+ foreach (KeyValuePair<int, global::Android.Views.View> pair in viewByAdaptorPositionCopy)
+ {
+ global::Android.Views.View view = pair.Value;
+ int position = pair.Key;
+
+ // position unchanged
+ if (position < positionStart)
+ _viewByAdaptorPosition[position] = view;
+
+ // position changed
+ else
+ _viewByAdaptorPosition[position + itemCount] = view;
+ }
+
+ if (_positionOrigin >= positionStart)
+ _positionOrigin += itemCount;
+ });
+ base.OnItemsAdded(recyclerView, positionStart, itemCount);
+ }
+
+ public override void OnItemsChanged(RecyclerView recyclerView)
+ {
+ _adapterChangeType = AdapterChangeType.Changed;
+
+ // low-fidelity change event; assume everything has changed. If adapter reports it has "stable IDs" then
+ // RecyclerView will attempt to synthesize high-fidelity change events: added, removed, moved, updated.
+ base.OnItemsChanged(recyclerView);
+ }
+
+ public override void OnItemsMoved(RecyclerView recyclerView, int from, int toValue, int itemCount)
+ {
+ _adapterChangeType = AdapterChangeType.Moved;
+ base.OnItemsMoved(recyclerView, from, toValue, itemCount);
+ }
+
+ public override void OnItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)
+ {
+ Debug.Assert(itemCount == MaxItemsRemoved);
+ _adapterChangeType = AdapterChangeType.Removed;
+
+ int positionEnd = positionStart + itemCount;
+
+ _deferredLayout.Enqueue((recycler, state) =>
+ {
+ if (state.ItemCount == 0)
+ throw new InvalidOperationException("Cannot delete all items.");
+
+ // re-map views to their new positions
+ KeyValuePair<int, global::Android.Views.View>[] viewByAdaptorPositionCopy = _viewByAdaptorPosition.ToArray();
+ _viewByAdaptorPosition.Clear();
+ foreach (KeyValuePair<int, global::Android.Views.View> pair in viewByAdaptorPositionCopy)
+ {
+ global::Android.Views.View view = pair.Value;
+ int position = pair.Key;
+
+ // position unchanged
+ if (position < positionStart)
+ _viewByAdaptorPosition[position] = view;
+
+ // position changed
+ else if (position >= positionEnd)
+ _viewByAdaptorPosition[position - itemCount] = view;
+
+ // removed
+ else
+ {
+ _viewByAdaptorPosition[-1] = view;
+ if (_visibleAdapterPosition.Contains(position))
+ _visibleAdapterPosition.Remove(position);
+ }
+ }
+
+ // if removed origin then shift origin to first removed position
+ if (_positionOrigin >= positionStart && _positionOrigin < positionEnd)
+ {
+ _positionOrigin = positionStart;
+
+ // if no items to right of removed origin then set origin to item prior to removed set
+ if (_positionOrigin >= state.ItemCount)
+ {
+ _positionOrigin = state.ItemCount - 1;
+
+ if (!_viewByAdaptorPosition.ContainsKey(_positionOrigin))
+ throw new InvalidOperationException("VirtualLayoutManager must add items to the left and right of the origin");
+ }
+ }
+
+ // if removed before origin then shift origin left
+ else if (_positionOrigin >= positionEnd)
+ _positionOrigin -= itemCount;
+ });
+
+ base.OnItemsRemoved(recyclerView, positionStart, itemCount);
+ }
+
+ public override void OnItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount)
+ {
+ _adapterChangeType = AdapterChangeType.Updated;
+
+ // rebind rendered updated elements
+ _deferredLayout.Enqueue((recycler, state) =>
+ {
+ for (var i = 0; i < itemCount; i++)
+ {
+ int position = positionStart + i;
+
+ global::Android.Views.View view;
+ if (!_viewByAdaptorPosition.TryGetValue(position, out view))
+ continue;
+
+ recycler.BindViewToPosition(view, position);
+ }
+ });
+
+ base.OnItemsUpdated(recyclerView, positionStart, itemCount);
+ }
+
+ public override void OnLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
+ {
+ AdapterChangeType adapterChangeType = _adapterChangeType;
+ if (state.IsPreLayout)
+ adapterChangeType = default(AdapterChangeType);
+
+ // adapter updates
+ if (!state.IsPreLayout)
+ {
+ while (_deferredLayout.Count > 0)
+ _deferredLayout.Dequeue()(recycler, state);
+ }
+
+ // get visible items
+ int[] positions = _virtualLayout.GetPositions(_positionOrigin, state.ItemCount, Viewport,
+ // IsPreLayout => some type of data update of yet unknown type. Must assume update
+ // could be remove so virtualLayout must +1 off-screen left in case origin is
+ // removed and +n off-screen right to slide onscreen if a big item is removed
+ state.IsPreLayout || adapterChangeType == AdapterChangeType.Removed).ToRange();
+
+ // disappearing
+ List<int> disappearing = _viewByAdaptorPosition.Keys.Except(positions).ToList();
+
+ // defer cleanup of displaced items and lay them out off-screen so they animate off-screen
+ if (adapterChangeType == AdapterChangeType.Added)
+ {
+ positions = positions.Concat(disappearing).OrderBy(o => o).ToArray();
+ disappearing.Clear();
+ }
+
+ // recycle
+ foreach (int position in disappearing)
+ {
+ global::Android.Views.View view = _viewByAdaptorPosition[position];
+
+ // remove
+ _viewByAdaptorPosition.Remove(position);
+ OnAppearingOrDisappearing(position, false);
+
+ // scrap
+ new DecoratedView(this, view).DetachAndScrap(recycler);
+ }
+
+ // TODO: Generalize
+ if (adapterChangeType == AdapterChangeType.Removed && _positionOrigin == state.ItemCount - 1)
+ {
+ System.Drawing.Rectangle vlayout = _virtualLayout.LayoutItem(_positionOrigin, _positionOrigin);
+ _locationOffset = new IntVector(vlayout.Width - Width, _locationOffset.Y);
+ }
+
+ var nextLocationOffset = new System.Drawing.Point(int.MaxValue, int.MaxValue);
+ int nextPositionOrigin = int.MaxValue;
+ foreach (int position in positions)
+ {
+ // attach
+ global::Android.Views.View view;
+ if (!_viewByAdaptorPosition.TryGetValue(position, out view))
+ AddView(_viewByAdaptorPosition[position] = view = recycler.GetViewForPosition(position));
+
+ // layout
+ var decoratedView = new DecoratedView(this, view);
+ System.Drawing.Rectangle layout = _virtualLayout.LayoutItem(_positionOrigin, position);
+ System.Drawing.Rectangle physicalLayout = layout - _locationOffset;
+ decoratedView.Layout(physicalLayout);
+
+ bool isVisible = Viewport.IntersectsWith(layout);
+ if (isVisible)
+ OnAppearingOrDisappearing(position, true);
+
+ // update offsets
+ if (isVisible && position < nextPositionOrigin)
+ {
+ nextLocationOffset = layout.Location;
+ nextPositionOrigin = position;
+ }
+ }
+
+ // update origin
+ if (nextPositionOrigin != int.MaxValue)
+ {
+ _positionOrigin = nextPositionOrigin;
+ _locationOffset -= (IntVector)nextLocationOffset;
+ }
+
+ // scrapped views not re-attached must be recycled (why isn't this done by Android, I dunno)
+ foreach (RecyclerView.ViewHolder viewHolder in recycler.ScrapList.ToArray())
+ recycler.RecycleView(viewHolder.ItemView);
+ }
+
+ public override int ScrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)
+ {
+ var delta = new IntVector(dx, 0);
+ ScrollBy(ref delta, recycler, state);
+ return delta.X;
+ }
+
+ public override void ScrollToPosition(int adapterPosition)
+ {
+ if (adapterPosition < 0 || adapterPosition >= ItemCount)
+ throw new ArgumentException(nameof(adapterPosition));
+
+ _scroller.TargetPosition = adapterPosition;
+ StartSmoothScroll(_scroller);
+ }
+
+ public override int ScrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
+ {
+ var delta = new IntVector(0, dy);
+ ScrollBy(ref delta, recycler, state);
+ return delta.Y;
+ }
+
+ public override void SmoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int adapterPosition)
+ {
+ ScrollToPosition(adapterPosition);
+ }
+
+ // entry points
+ public override bool SupportsPredictiveItemAnimations() => true;
+
+ public override string ToString()
+ {
+ return $"offset={_locationOffset}";
+ }
+
+ public IEnumerable<global::Android.Views.View> Views()
+ {
+ return _viewByAdaptorPosition.Values;
+ }
+
+ public IEnumerable<int> VisiblePositions()
+ {
+ return _visibleAdapterPosition;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ }
+
+ void OffsetChildren(IntVector delta)
+ {
+ OffsetChildrenHorizontal(-delta.X);
+ OffsetChildrenVertical(-delta.Y);
+ }
+
+ void OnAppearingOrDisappearing(int position, bool isAppearing)
+ {
+ if (isAppearing)
+ {
+ if (!_visibleAdapterPosition.Contains(position))
+ {
+ _visibleAdapterPosition.Add(position);
+ OnAppearing?.Invoke(position);
+ }
+ }
+ else
+ {
+ if (_visibleAdapterPosition.Contains(position))
+ {
+ _visibleAdapterPosition.Remove(position);
+ OnDisappearing?.Invoke(position);
+ }
+ }
+ }
+
+ void ScrollBy(ref IntVector delta, RecyclerView.Recycler recycler, RecyclerView.State state)
+ {
+ _adapterChangeType = default(AdapterChangeType);
+
+ delta = Viewport.BoundTranslation(delta, _virtualLayout.GetBounds(_positionOrigin, state));
+
+ _locationOffset += delta;
+ _samples.Insert(0, delta);
+ _samples.RemoveAt(_samples.Count - 1);
+
+ OffsetChildren(delta);
+ OnLayoutChildren(recycler, state);
+ }
+
+ enum AdapterChangeType
+ {
+ Removed = 1,
+ Added,
+ Moved,
+ Updated,
+ Changed
+ }
+
+ internal struct DecoratedView
+ {
+ public static implicit operator global::Android.Views.View(DecoratedView view)
+ {
+ return view._view;
+ }
+
+ readonly PhysicalLayoutManager _layout;
+ readonly global::Android.Views.View _view;
+
+ internal DecoratedView(PhysicalLayoutManager layout, global::Android.Views.View view)
+ {
+ _layout = layout;
+ _view = view;
+ }
+
+ internal int Left => _layout.GetDecoratedLeft(_view);
+
+ internal int Top => _layout.GetDecoratedTop(_view);
+
+ internal int Bottom => _layout.GetDecoratedBottom(_view);
+
+ internal int Right => _layout.GetDecoratedRight(_view);
+
+ internal int Width => Right - Left;
+
+ internal int Height => Bottom - Top;
+
+ internal System.Drawing.Rectangle Rectangle => new System.Drawing.Rectangle(Left, Top, Width, Height);
+
+ internal void Measure(int widthUsed, int heightUsed)
+ {
+ _layout.MeasureChild(_view, widthUsed, heightUsed);
+ }
+
+ internal void MeasureWithMargins(int widthUsed, int heightUsed)
+ {
+ _layout.MeasureChildWithMargins(_view, widthUsed, heightUsed);
+ }
+
+ internal void Layout(System.Drawing.Rectangle position)
+ {
+ var renderer = _view as IVisualElementRenderer;
+ renderer.Element.Layout(position.ToFormsRectangle(_layout._context));
+
+ _layout.LayoutDecorated(_view, position.Left, position.Top, position.Right, position.Bottom);
+ }
+
+ internal void Add()
+ {
+ _layout.AddView(_view);
+ }
+
+ internal void DetachAndScrap(RecyclerView.Recycler recycler)
+ {
+ _layout.DetachAndScrapView(_view, recycler);
+ }
+ }
+
+ internal abstract class VirtualLayoutManager
+ {
+ internal abstract bool CanScrollHorizontally { get; }
+
+ internal abstract bool CanScrollVertically { get; }
+
+ internal abstract System.Drawing.Rectangle GetBounds(int positionOrigin, RecyclerView.State state);
+
+ internal abstract Tuple<int, int> GetPositions(int positionOrigin, int itemCount, System.Drawing.Rectangle viewport, bool isPreLayout);
+
+ internal abstract void Layout(int positionOrigin, System.Drawing.Size viewportSize, ref IntVector offset);
+
+ internal abstract System.Drawing.Rectangle LayoutItem(int positionOrigin, int position);
+ }
+
+ enum SnapPreference
+ {
+ None = 0,
+ Begin = 1,
+ End = -1
+ }
+
+ sealed class SeekAndSnapScroller : LinearSmoothScroller
+ {
+ readonly SnapPreference _snapPreference;
+ readonly Func<int, IntVector> _vectorToPosition;
+
+ internal SeekAndSnapScroller(Context context, Func<int, IntVector> vectorToPosition, SnapPreference snapPreference = SnapPreference.None) : base(context)
+ {
+ _vectorToPosition = vectorToPosition;
+ _snapPreference = snapPreference;
+ }
+
+ protected override int HorizontalSnapPreference => (int)_snapPreference;
+
+ public override PointF ComputeScrollVectorForPosition(int targetPosition)
+ {
+ IntVector vector = _vectorToPosition(targetPosition);
+ return new PointF(vector.X, vector.Y);
+ }
+
+ public event Action<int> OnBeginScroll;
+
+ public event Action<int> OnEndScroll;
+
+ protected override void OnStart()
+ {
+ OnBeginScroll?.Invoke(TargetPosition);
+ base.OnStart();
+ }
+
+ protected override void OnStop()
+ {
+ // expected this to be triggered with the animation stops but it
+ // actually seems to be triggered when the target is found
+ OnEndScroll?.Invoke(TargetPosition);
+ base.OnStop();
+ }
+ }
+ }
+} \ No newline at end of file