summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs
blob: f63b6fd9a7ce6f90b434d4568c25195f511d2a34 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Android.Support.V7.Widget;
using AndroidListView = Android.Widget.ListView;
using static System.Diagnostics.Debug;
using Observer = Android.Support.V7.Widget.RecyclerView.AdapterDataObserver;
using BclDebug = System.Diagnostics.Debug;
using IntRectangle = System.Drawing.Rectangle;
using IntSize = System.Drawing.Size;

namespace Xamarin.Forms.Platform.Android
{
	public class CarouselViewRenderer : ViewRenderer<CarouselView, RecyclerView>
	{
		PhysicalLayoutManager _physicalLayout;
		int _position;

		public CarouselViewRenderer()
		{
			AutoPackage = false;
		}

		ItemViewAdapter Adapter
		{
			get { return (ItemViewAdapter)Control.GetAdapter(); }
		}

		new RecyclerView Control
		{
			get
			{
				Initialize();
				return base.Control;
			}
		}

		ICarouselViewController Controller => Element;

		PhysicalLayoutManager LayoutManager
		{
			get { return (PhysicalLayoutManager)Control.GetLayoutManager(); }
		}

		protected override Size MinimumSize()
		{
			return new Size(40, 40);
		}

		protected override void OnElementChanged(ElementChangedEventArgs<CarouselView> e)
		{
			CarouselView oldElement = e.OldElement;
			if (oldElement != null)
				e.OldElement.CollectionChanged -= OnCollectionChanged;

			base.OnElementChanged(e);
			Initialize();
		}

		protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == "Position" && _position != Element.Position)
				_physicalLayout.ScrollToPosition(Element.Position);

			base.OnElementPropertyChanged(sender, e);
		}

		protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
		{
			int width = right - left;
			int height = bottom - top;

			LayoutManager.Layout(width, height);

			base.OnLayout(changed, left, top, right, bottom);

			Control.Measure(new MeasureSpecification(width, MeasureSpecificationType.Exactly), new MeasureSpecification(height, MeasureSpecificationType.Exactly));

			Control.Layout(0, 0, width, height);
		}

		void Initialize()
		{
			// cache hit? Check if the view page is already created
			RecyclerView recyclerView = base.Control;
			if (recyclerView != null)
				return;

			// cache miss
			recyclerView = new RecyclerView(Context);
			SetNativeControl(recyclerView);

			// layoutManager
			recyclerView.SetLayoutManager(_physicalLayout = new PhysicalLayoutManager(Context, new VirtualLayoutManager(), Element.Position));

			// swiping
			var dragging = false;
			recyclerView.AddOnScrollListener(new OnScrollListener(onDragStart: () => dragging = true, onDragEnd: () =>
			{
				dragging = false;
				IntVector velocity = _physicalLayout.Velocity;

				int target = velocity.X > 0 ? _physicalLayout.VisiblePositions().Max() : _physicalLayout.VisiblePositions().Min();
				_physicalLayout.ScrollToPosition(target);
			}));

			// scrolling
			var scrolling = false;
			_physicalLayout.OnBeginScroll += position => scrolling = true;
			_physicalLayout.OnEndScroll += position => scrolling = false;

			// appearing
			_physicalLayout.OnAppearing += appearingPosition => { Controller.SendPositionAppearing(appearingPosition); };

			// disappearing
			_physicalLayout.OnDisappearing += disappearingPosition =>
			{
				Controller.SendPositionDisappearing(disappearingPosition);

				// animation completed
				if (!scrolling && !dragging)
				{
					_position = _physicalLayout.VisiblePositions().Single();

					OnPositionChanged();
					OnItemChanged();
				}
			};

			// adapter
			var adapter = new ItemViewAdapter(this);
			adapter.RegisterAdapterDataObserver(new PositionUpdater(this));
			recyclerView.SetAdapter(adapter);

			// initialize properties
			Element.Position = 0;

			// initialize events
			Element.CollectionChanged += OnCollectionChanged;
		}

		void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					Adapter.NotifyItemRangeInserted(e.NewStartingIndex, e.NewItems.Count);
					break;

				case NotifyCollectionChangedAction.Move:
					for (var i = 0; i < e.NewItems.Count; i++)
						Adapter.NotifyItemMoved(e.OldStartingIndex + i, e.NewStartingIndex + i);
					break;

				case NotifyCollectionChangedAction.Remove:
					if (Element.Count == 0)
						throw new InvalidOperationException("CarouselView must retain a least one item.");

					Adapter.NotifyItemRangeRemoved(e.OldStartingIndex, e.OldItems.Count);
					break;

				case NotifyCollectionChangedAction.Replace:
					Adapter.NotifyItemRangeChanged(e.OldStartingIndex, e.OldItems.Count);
					break;

				case NotifyCollectionChangedAction.Reset:
					Adapter.NotifyDataSetChanged();
					break;

				default:
					throw new Exception($"Enum value '{(int)e.Action}' is not a member of NotifyCollectionChangedAction enumeration.");
			}
		}

		void OnItemChanged()
		{
			object item = ((IItemViewController)Element).GetItem(_position);
			Controller.SendSelectedItemChanged(item);
		}

		void OnPositionChanged()
		{
			Element.Position = _position;
			Controller.SendSelectedPositionChanged(_position);
		}

		// http://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
		// http://developer.android.com/training/material/lists-cards.html
		// http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/

		class OnScrollListener : RecyclerView.OnScrollListener
		{
			readonly Action _onDragEnd;
			readonly Action _onDragStart;
			ScrollState _lastScrollState;

			internal OnScrollListener(Action onDragEnd, Action onDragStart)
			{
				_onDragEnd = onDragEnd;
				_onDragStart = onDragStart;
			}

			public override void OnScrollStateChanged(RecyclerView recyclerView, int newState)
			{
				var state = (ScrollState)newState;
				if (_lastScrollState != ScrollState.Dragging && state == ScrollState.Dragging)
					_onDragStart();

				if (_lastScrollState == ScrollState.Dragging && state != ScrollState.Dragging)
					_onDragEnd();

				_lastScrollState = state;
				base.OnScrollStateChanged(recyclerView, newState);
			}

			enum ScrollState
			{
				Idle,
				Dragging,
				Settling
			}
		}

		class PositionUpdater : Observer
		{
			readonly CarouselViewRenderer _carouselView;

			internal PositionUpdater(CarouselViewRenderer carouselView)
			{
				_carouselView = carouselView;
			}

			public override void OnItemRangeInserted(int positionStart, int itemCount)
			{
				// removal after the current position won't change current position
				if (positionStart > _carouselView._position)
					;

				// raise position changed
				else
				{
					_carouselView._position += itemCount;
					_carouselView.OnPositionChanged();
				}

				base.OnItemRangeInserted(positionStart, itemCount);
			}

			public override void OnItemRangeMoved(int fromPosition, int toPosition, int itemCount)
			{
				base.OnItemRangeMoved(fromPosition, toPosition, itemCount);
			}

			public override void OnItemRangeRemoved(int positionStart, int itemCount)
			{
				Assert(itemCount == 1);

				// removal after the current position won't change current position
				if (positionStart > _carouselView._position)
					;

				// raise item changed
				else if (positionStart == _carouselView._position && positionStart != _carouselView.Adapter.ItemCount)
				{
					_carouselView.OnItemChanged();
					return;
				}

				// raise position changed
				else
				{
					_carouselView._position -= itemCount;
					_carouselView.OnPositionChanged();
				}

				base.OnItemRangeRemoved(positionStart, itemCount);
			}
		}

		internal class VirtualLayoutManager : PhysicalLayoutManager.VirtualLayoutManager
		{
			const int Columns = 1;

			IntSize _itemSize;

			internal override bool CanScrollHorizontally => true;

			internal override bool CanScrollVertically => false;

			public override string ToString()
			{
				return $"itemSize={_itemSize}";
			}

			internal override IntRectangle GetBounds(int originPosition, RecyclerView.State state)
				=> new IntRectangle(LayoutItem(originPosition, 0).Location, new IntSize(_itemSize.Width * state.ItemCount, _itemSize.Height));

			internal override Tuple<int, int> GetPositions(int positionOrigin, int itemCount, IntRectangle viewport, bool includeBuffer)
			{
				// returns one item off-screen in either direction. 
				int buffer = includeBuffer ? 1 : 0;
				int left = GetPosition(itemCount, positionOrigin - buffer, viewport.Left);
				int right = GetPosition(itemCount, positionOrigin + buffer, viewport.Right, true);

				int start = left;
				int count = right - left + 1;
				return new Tuple<int, int>(start, count);
			}

			internal override void Layout(int positionOffset, IntSize viewportSize, ref IntVector offset)
			{
				int width = viewportSize.Width / Columns;
				int height = viewportSize.Height;

				if (_itemSize.Width != 0)
					offset *= (double)width / _itemSize.Width;

				_itemSize = new IntSize(width, height);
			}

			internal override IntRectangle LayoutItem(int positionOffset, int position)
			{
				// measure
				IntSize size = _itemSize;

				// layout
				var location = new IntVector((position - positionOffset) * size.Width, 0);

				// allocate
				return new IntRectangle(location, size);
			}

			int GetPosition(int itemCount, int positionOrigin, int x, bool exclusive = false)
			{
				int position = x / _itemSize.Width + positionOrigin;
				bool hasRemainder = x % _itemSize.Width != 0;

				if (hasRemainder && x < 0)
					position--;

				if (!hasRemainder && exclusive)
					position--;

				position = position.Clamp(0, itemCount - 1);
				return position;
			}
		}
	}

	// RecyclerView virtualizes indexes (adapter position <-> viewGroup child index) 
	// PhysicalLayoutManager virtualizes location (regular layout <-> screen)
}