OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 // This file contains View framework classes. | |
6 // As it grows, it may need to be split into multiple files. | |
7 | |
8 /** A factory that creates a view from a data model. */ | |
9 interface ViewFactory<D> { | |
10 View newView(D item); | |
11 | |
12 /** The width of the created view or null if the width is not fixed. */ | |
13 int get width(); | |
14 | |
15 /** The height of the created view or null if the height is not fixed. */ | |
16 int get height(); | |
17 } | |
18 | |
19 interface VariableSizeViewFactory<D> { | |
20 View newView(D item); | |
21 | |
22 /** The width of the created view for a specific data model. */ | |
23 int getWidth(D item); | |
24 | |
25 /** The height of the created view for a specific data model. */ | |
26 int getHeight(D item); | |
27 } | |
28 | |
29 /** A collection of event listeners. */ | |
30 class EventListeners { | |
31 var listeners; | |
32 EventListeners() { | |
33 listeners = new List(); | |
34 } | |
35 | |
36 void addListener(listener) { | |
37 listeners.add(listener); | |
38 } | |
39 | |
40 void fire(var event) { | |
41 for (final listener in listeners) { | |
42 listener(event); | |
43 } | |
44 } | |
45 } | |
46 | |
47 | |
48 /** | |
49 * Private view class used to store placeholder views for detatched ListView | |
50 * elements. | |
51 */ | |
52 class _PlaceholderView extends View { | |
53 _PlaceholderView() : super() {} | |
54 | |
55 Element render() => new Element.tag('div'); | |
56 } | |
57 | |
58 /** | |
59 * Class providing all metrics required to layout a data driven list view. | |
60 */ | |
61 interface ListViewLayout<D> { | |
62 void onDataChange(); | |
63 | |
64 // TODO(jacobr): placing the newView member function on this class seems like | |
65 // the wrong design. | |
66 View newView(int index); | |
67 /** Get the height of the view. Possibly expensive to compute. */ | |
68 int getHeight(int viewLength); | |
69 /** Get the width of the view. Possibly expensive to compute. */ | |
70 int getWidth(int viewLength); | |
71 /** Get the length of the view. Possible expensive to compute. */ | |
72 int getLength(int viewLength); | |
73 /** Estimated height of the view. Guaranteed to be fast to compute. */ | |
74 int getEstimatedHeight(int viewLength); | |
75 /** Estimated with of the view. Guaranteed to be fast to compute. */ | |
76 int getEstimatedWidth(int viewLength); | |
77 | |
78 /** | |
79 * Returns the offset in px that the ith item in the view should be placed | |
80 * at. | |
81 */ | |
82 int getOffset(int index); | |
83 | |
84 /** | |
85 * The page the ith item in the view should be placed in. | |
86 */ | |
87 int getPage(int index, int viewLength); | |
88 int getPageStartIndex(int index, int viewLength); | |
89 | |
90 int getEstimatedLength(int viewLength); | |
91 /** | |
92 * Snap a specified index to the nearest visible view given the [viewLength]. | |
93 */ | |
94 int getSnapIndex(num offset, num viewLength); | |
95 /** | |
96 * Returns an interval specifying what views are currently visible given a | |
97 * particular [:offset:]. | |
98 */ | |
99 Interval computeVisibleInterval(num offset, num viewLength, | |
100 num bufferLength); | |
101 } | |
102 | |
103 /** | |
104 * Base class used for the simple fixed size item [:ListView:] classes and more | |
105 * complex list view classes such as [:VariableSizeListView:] using a | |
106 * [:ListViewLayout:] class to drive the actual layout. | |
107 */ | |
108 class GenericListView<D> extends View { | |
109 /** Minimum throw distance in pixels to trigger snapping to the next item. */ | |
110 static final SNAP_TO_NEXT_THROW_THRESHOLD = 15; | |
111 | |
112 static final INDEX_DATA_ATTRIBUTE = 'data-index'; | |
113 | |
114 final bool _scrollable; | |
115 final bool _showScrollbar; | |
116 final bool _snapToItems; | |
117 Scroller scroller; | |
118 Scrollbar _scrollbar; | |
119 List<D> _data; | |
120 ObservableValue<D> _selectedItem; | |
121 Map<int, View> _itemViews; | |
122 Element _containerElem; | |
123 bool _vertical; | |
124 /** Length of the scrollable dimension of the view in px. */ | |
125 int _viewLength = 0; | |
126 Interval _activeInterval; | |
127 bool _paginate; | |
128 bool _removeClippedViews; | |
129 ListViewLayout<D> _layout; | |
130 D _lastSelectedItem; | |
131 PageState _pages; | |
132 | |
133 /** | |
134 * Creates a new GenericListView with the given layout and data. If [:_data:] | |
135 * is an [:ObservableList<T>:] then it will listen to changes to the list | |
136 * and update the view appropriately. | |
137 */ | |
138 GenericListView( | |
139 this._layout, | |
140 this._data, | |
141 this._scrollable, | |
142 this._vertical, | |
143 this._selectedItem, | |
144 this._snapToItems, | |
145 this._paginate, | |
146 this._removeClippedViews, | |
147 this._showScrollbar, | |
148 this._pages) | |
149 : super(), | |
150 _activeInterval = new Interval(0, 0), | |
151 _itemViews = new Map<int, View>() { | |
152 // TODO(rnystrom): Move this into enterDocument once we have an exitDocument | |
153 // that we can use to unregister it. | |
154 if (_scrollable) { | |
155 window.on.resize.add((Event event) { | |
156 if (isInDocument) { | |
157 onResize(); | |
158 } | |
159 }); | |
160 } | |
161 } | |
162 | |
163 void onSelectedItemChange() { | |
164 // TODO(rnystrom): use Observable to track the last value of _selectedItem | |
165 // rather than tracking it ourselves. | |
166 _select(findIndex(_lastSelectedItem), false); | |
167 _select(findIndex(_selectedItem.value), true); | |
168 _lastSelectedItem = _selectedItem.value; | |
169 } | |
170 | |
171 Collection<View> get childViews() { | |
172 return _itemViews.getValues(); | |
173 } | |
174 | |
175 void _onClick(MouseEvent e) { | |
176 int index = _findAssociatedIndex(e.target); | |
177 if (index != null) { | |
178 _selectedItem.value = _data[index]; | |
179 } | |
180 } | |
181 | |
182 int _findAssociatedIndex(Node leafNode) { | |
183 Node node = leafNode; | |
184 while (node != null && node != _containerElem) { | |
185 if (node.parent == _containerElem) { | |
186 return _nodeToIndex(node); | |
187 } | |
188 node = node.parent; | |
189 } | |
190 return null; | |
191 } | |
192 | |
193 int _nodeToIndex(Element node) { | |
194 // TODO(jacobr): use data attributes when available. | |
195 String index = node.attributes[INDEX_DATA_ATTRIBUTE]; | |
196 if (index != null && index.length > 0) { | |
197 return Math.parseInt(index); | |
198 } | |
199 return null; | |
200 } | |
201 | |
202 Element render() { | |
203 final node = new Element.tag('div'); | |
204 if (_scrollable) { | |
205 _containerElem = new Element.tag('div'); | |
206 _containerElem.tabIndex = -1; | |
207 node.nodes.add(_containerElem); | |
208 } else { | |
209 _containerElem = node; | |
210 } | |
211 | |
212 if (_scrollable) { | |
213 scroller = new Scroller( | |
214 _containerElem, | |
215 _vertical /* verticalScrollEnabled */, | |
216 !_vertical /* horizontalScrollEnabled */, | |
217 true /* momentumEnabled */, | |
218 () { | |
219 num width = _layout.getWidth(_viewLength); | |
220 num height = _layout.getHeight(_viewLength); | |
221 width = width != null ? width : 0; | |
222 height = height != null ? height : 0; | |
223 final completer = new Completer<Size>(); | |
224 completer.complete(new Size(width, height)); | |
225 return completer.future; | |
226 }, | |
227 _paginate && _snapToItems ? | |
228 Scroller.FAST_SNAP_DECELERATION_FACTOR : 1); | |
229 scroller.onContentMoved.add((e) => renderVisibleItems(false)); | |
230 if (_pages != null) { | |
231 watch(_pages.target, (s) => _onPageSelected()); | |
232 } | |
233 | |
234 if (_snapToItems) { | |
235 scroller.onDecelStart.add((e) => _decelStart()); | |
236 scroller.onScrollerDragEnd.add((e) => _decelStart()); | |
237 } | |
238 if (_showScrollbar) { | |
239 _scrollbar = new Scrollbar(scroller, true); | |
240 } | |
241 } else { | |
242 _reserveArea(); | |
243 renderVisibleItems(true); | |
244 } | |
245 | |
246 return node; | |
247 } | |
248 | |
249 void afterRender(Element node) { | |
250 // If our data source is observable, observe it. | |
251 if (_data is ObservableList<D>) { | |
252 ObservableList<D> observable = _data; | |
253 attachWatch(observable, (EventSummary e) { | |
254 if (e.target == observable) { | |
255 onDataChange(); | |
256 } | |
257 }); | |
258 } | |
259 | |
260 if (_selectedItem != null) { | |
261 addOnClick(function(Event e) { _onClick(e); }); | |
262 } | |
263 | |
264 if (_selectedItem != null) { | |
265 watch(_selectedItem, (EventSummary summary) => onSelectedItemChange()); | |
266 } | |
267 } | |
268 | |
269 void onDataChange() { | |
270 _layout.onDataChange(); | |
271 _renderItems(); | |
272 } | |
273 | |
274 void _reserveArea() { | |
275 final style = _containerElem.style; | |
276 int width = _layout.getWidth(_viewLength); | |
277 int height = _layout.getHeight(_viewLength); | |
278 if (width != null) { | |
279 style.width = '${width}px'; | |
280 } | |
281 if (height != null) { | |
282 style.height = '${height}px'; | |
283 } | |
284 // TODO(jacobr): this should be specified by the default CSS for a | |
285 // GenericListView. | |
286 style.overflow = 'hidden'; | |
287 } | |
288 | |
289 | |
290 void onResize() { | |
291 int lastViewLength = _viewLength; | |
292 node.rect.then((ElementRect rect) { | |
293 _viewLength = _vertical ? rect.offset.height : rect.offset.width; | |
294 if (_viewLength != lastViewLength) { | |
295 if (_scrollbar != null) { | |
296 _scrollbar.refresh(); | |
297 } | |
298 renderVisibleItems(true); | |
299 } | |
300 }); | |
301 } | |
302 | |
303 void enterDocument() { | |
304 if (scroller != null) { | |
305 onResize(); | |
306 | |
307 if (_scrollbar != null) { | |
308 _scrollbar.initialize(); | |
309 } | |
310 } | |
311 } | |
312 | |
313 int getNextIndex(int index, bool forward) { | |
314 int delta = forward ? 1 : -1; | |
315 if (_paginate) { | |
316 int newPage = Math.max(0, _layout.getPage(index, _viewLength) + delta); | |
317 index = _layout.getPageStartIndex(newPage, _viewLength); | |
318 } else { | |
319 index += delta; | |
320 } | |
321 return GoogleMath.clamp(index, 0, _data.length - 1); | |
322 } | |
323 | |
324 void _decelStart() { | |
325 num currentTarget = scroller.verticalEnabled ? | |
326 scroller.currentTarget.y : scroller.currentTarget.x; | |
327 num current = scroller.verticalEnabled ? | |
328 scroller.contentOffset.y : scroller.contentOffset.x; | |
329 num targetIndex = _layout.getSnapIndex(currentTarget, _viewLength); | |
330 if (current != currentTarget) { | |
331 // The user is throwing rather than statically releasing. | |
332 // For this case, we want to move them to the next snap interval | |
333 // as long as they made at least a minimal throw gesture. | |
334 num currentIndex = _layout.getSnapIndex(current, _viewLength); | |
335 if (currentIndex == targetIndex && | |
336 (currentTarget - current).abs() > SNAP_TO_NEXT_THROW_THRESHOLD && | |
337 -_layout.getOffset(targetIndex) != currentTarget) { | |
338 num snappedCurrentPosition = -_layout.getOffset(targetIndex); | |
339 targetIndex = getNextIndex(targetIndex, currentTarget < current); | |
340 } | |
341 } | |
342 num targetPosition = -_layout.getOffset(targetIndex); | |
343 if (currentTarget != targetPosition) { | |
344 if (scroller.verticalEnabled) { | |
345 scroller.throwTo(scroller.contentOffset.x, targetPosition); | |
346 } else { | |
347 scroller.throwTo(targetPosition, scroller.contentOffset.y); | |
348 } | |
349 } else { | |
350 // Update the target page only after we are all done animating. | |
351 if (_pages != null) { | |
352 _pages.target.value =_layout.getPage(targetIndex, _viewLength); | |
353 } | |
354 } | |
355 } | |
356 | |
357 void _renderItems() { | |
358 for (int i = _activeInterval.start; i < _activeInterval.end; i++) { | |
359 _removeView(i); | |
360 } | |
361 _itemViews.clear(); | |
362 _activeInterval = new Interval(0, 0); | |
363 if (scroller == null) { | |
364 _reserveArea(); | |
365 } | |
366 renderVisibleItems(false); | |
367 } | |
368 | |
369 void _onPageSelected() { | |
370 if (_pages.target != | |
371 _layout.getPage(_activeInterval.start, _viewLength)) { | |
372 _throwTo(_layout.getOffset( | |
373 _layout.getPageStartIndex(_pages.target.value, _viewLength))); | |
374 } | |
375 } | |
376 | |
377 num get _offset() { | |
378 return scroller.verticalEnabled ? | |
379 scroller.getVerticalOffset() : scroller.getHorizontalOffset(); | |
380 } | |
381 | |
382 /** | |
383 * Calculates visible interval, based on the scroller position. | |
384 */ | |
385 Interval getVisibleInterval() { | |
386 return _layout.computeVisibleInterval(_offset, _viewLength, 0); | |
387 } | |
388 | |
389 void renderVisibleItems(bool lengthChanged) { | |
390 Interval targetInterval; | |
391 if (scroller != null) { | |
392 targetInterval = getVisibleInterval(); | |
393 } else { | |
394 // If the view is not scrollable, render all elements. | |
395 targetInterval = new Interval(0, _data.length); | |
396 } | |
397 | |
398 if (_pages != null) { | |
399 _pages.current.value = | |
400 _layout.getPage(targetInterval.start, _viewLength); | |
401 } | |
402 if (_pages != null) { | |
403 _pages.length.value = _data.length > 0 ? | |
404 _layout.getPage(_data.length - 1, _viewLength) + 1 : 0; | |
405 } | |
406 | |
407 if (!_removeClippedViews) { | |
408 // Avoid removing clipped views by extending the target interval to | |
409 // include the existing interval of rendered views. | |
410 targetInterval = targetInterval.union(_activeInterval); | |
411 } | |
412 | |
413 if (lengthChanged == false && targetInterval == _activeInterval) { | |
414 return; | |
415 } | |
416 | |
417 // TODO(jacobr): add unittests that this code behaves correctly. | |
418 | |
419 // Remove views that are not needed anymore | |
420 for (int i = _activeInterval.start, | |
421 end = Math.min(targetInterval.start, _activeInterval.end); | |
422 i < end; i++) { | |
423 _removeView(i); | |
424 } | |
425 for (int i = Math.max(targetInterval.end, _activeInterval.start); | |
426 i < _activeInterval.end; i++) { | |
427 _removeView(i); | |
428 } | |
429 | |
430 // Add new views | |
431 for (int i = targetInterval.start, | |
432 end = Math.min(_activeInterval.start, targetInterval.end); | |
433 i < end; i++) { | |
434 _addView(i); | |
435 } | |
436 for (int i = Math.max(_activeInterval.end, targetInterval.start); | |
437 i < targetInterval.end; i++) { | |
438 _addView(i); | |
439 } | |
440 | |
441 _activeInterval = targetInterval; | |
442 } | |
443 | |
444 void _removeView(int index) { | |
445 // Do not remove placeholder views as they need to stay present in case | |
446 // they scroll out of view and then back into view. | |
447 if (!(_itemViews[index] is _PlaceholderView)) { | |
448 // Remove from the active DOM but don't destroy. | |
449 _itemViews[index].node.remove(); | |
450 childViewRemoved(_itemViews[index]); | |
451 } | |
452 } | |
453 | |
454 View _newView(int index) { | |
455 final view = _layout.newView(index); | |
456 view.node.attributes[INDEX_DATA_ATTRIBUTE] = index.toString(); | |
457 return view; | |
458 } | |
459 | |
460 View _addView(int index) { | |
461 if (_itemViews.containsKey(index)) { | |
462 final view = _itemViews[index]; | |
463 _addViewHelper(view, index); | |
464 childViewAdded(view); | |
465 return view; | |
466 } | |
467 | |
468 final view = _newView(index); | |
469 _itemViews[index] = view; | |
470 // TODO(jacobr): its ugly to put this here... but its needed | |
471 // as typical even-odd css queries won't work as we only display some | |
472 // children at a time. | |
473 if (index == 0) { | |
474 view.addClass('first-child'); | |
475 } | |
476 _selectHelper(view, _data[index] == _lastSelectedItem); | |
477 // The order of the child elements doesn't matter as we use absolute | |
478 // positioning. | |
479 _addViewHelper(view, index); | |
480 childViewAdded(view); | |
481 return view; | |
482 } | |
483 | |
484 void _addViewHelper(View view, int index) { | |
485 _positionSubview(view.node, index); | |
486 // The view might already be attached. | |
487 if (view.node.parent != _containerElem) { | |
488 _containerElem.nodes.add(view.node); | |
489 } | |
490 } | |
491 | |
492 /** | |
493 * Detach a subview from the view replacing it with an empty placeholder view. | |
494 * The detatched subview can be safely reparented. | |
495 */ | |
496 View detachSubview(D itemData) { | |
497 int index = findIndex(itemData); | |
498 View view = _itemViews[index]; | |
499 if (view == null) { | |
500 // Edge case: add the view so we can detatch as the view is currently | |
501 // outside but might soon be inside the visible area. | |
502 assert(!_activeInterval.contains(index)); | |
503 _addView(index); | |
504 view = _itemViews[index]; | |
505 } | |
506 final placeholder = new _PlaceholderView(); | |
507 view.node.replaceWith(placeholder.node); | |
508 _itemViews[index] = placeholder; | |
509 return view; | |
510 } | |
511 | |
512 /** | |
513 * Reattach a subview from the view that was detached from the view | |
514 * by calling detachSubview. [callback] is called once the subview is | |
515 * reattached and done animating into position. | |
516 */ | |
517 void reattachSubview(D data, View view, bool animate) { | |
518 int index = findIndex(data); | |
519 // TODO(jacobr): perform some validation that the view is | |
520 // really detached. | |
521 var currentPosition; | |
522 if (animate) { | |
523 currentPosition = | |
524 FxUtil.computeRelativePosition(view.node, _containerElem); | |
525 } | |
526 assert (_itemViews[index] is _PlaceholderView); | |
527 view.enterDocument(); | |
528 _itemViews[index].node.replaceWith(view.node); | |
529 _itemViews[index] = view; | |
530 if (animate) { | |
531 FxUtil.setTranslate(view.node, currentPosition.x, currentPosition.y, 0); | |
532 // The view's position is unchanged except now re-parented to | |
533 // the list view. | |
534 window.setTimeout(() { _positionSubview(view.node, index); }, 0); | |
535 } else { | |
536 _positionSubview(view.node, index); | |
537 } | |
538 } | |
539 | |
540 int findIndex(D targetItem) { | |
541 // TODO(jacobr): move this to a util library or modify this class so that | |
542 // the data is an List not a Collection. | |
543 int i = 0; | |
544 for (D item in _data) { | |
545 if (item == targetItem) { | |
546 return i; | |
547 } | |
548 i++; | |
549 } | |
550 return null; | |
551 } | |
552 | |
553 void _positionSubview(Element node, int index) { | |
554 if (_vertical) { | |
555 FxUtil.setTranslate(node, 0, _layout.getOffset(index), 0); | |
556 } else { | |
557 FxUtil.setTranslate(node, _layout.getOffset(index), 0, 0); | |
558 } | |
559 node.style.zIndex = index.toString(); | |
560 } | |
561 | |
562 void _select(int index, bool selected) { | |
563 if (index != null) { | |
564 final subview = getSubview(index); | |
565 if (subview != null) { | |
566 _selectHelper(subview, selected); | |
567 } | |
568 } | |
569 } | |
570 | |
571 void _selectHelper(View view, bool selected) { | |
572 if (selected) { | |
573 view.addClass('sel'); | |
574 } else { | |
575 view.removeClass('sel'); | |
576 } | |
577 } | |
578 | |
579 View getSubview(int index) { | |
580 return _itemViews[index]; | |
581 } | |
582 | |
583 void showView(D targetItem) { | |
584 int index = findIndex(targetItem); | |
585 if (index != null) { | |
586 if (_layout.getOffset(index) < -_offset) { | |
587 _throwTo(_layout.getOffset(index)); | |
588 } else if (_layout.getOffset(index + 1) > (-_offset + _viewLength)) { | |
589 // TODO(jacobr): for completeness we should check whether | |
590 // the current view is longer than _viewLength in which case | |
591 // there are some nasty edge cases. | |
592 _throwTo(_layout.getOffset(index + 1) - _viewLength); | |
593 } | |
594 } | |
595 } | |
596 | |
597 void _throwTo(num offset) { | |
598 if (_vertical) { | |
599 scroller.throwTo(0, -offset); | |
600 } else { | |
601 scroller.throwTo(-offset, 0); | |
602 } | |
603 } | |
604 } | |
605 | |
606 class FixedSizeListViewLayout<D> implements ListViewLayout<D> { | |
607 final ViewFactory<D> itemViewFactory; | |
608 final bool _vertical; | |
609 List<D> _data; | |
610 bool _paginate; | |
611 | |
612 FixedSizeListViewLayout(this.itemViewFactory, this._data, this._vertical, | |
613 this._paginate); | |
614 | |
615 void onDataChange() {} | |
616 | |
617 View newView(int index) { | |
618 return itemViewFactory.newView(_data[index]); | |
619 } | |
620 | |
621 int get _itemLength() { | |
622 return _vertical ? itemViewFactory.height : itemViewFactory.width; | |
623 } | |
624 | |
625 | |
626 int getWidth(int viewLength) { | |
627 return _vertical ? itemViewFactory.width : getLength(viewLength); | |
628 } | |
629 | |
630 int getHeight(int viewLength) { | |
631 return _vertical ? getLength(viewLength) : itemViewFactory.height; | |
632 } | |
633 | |
634 int getEstimatedHeight(int viewLength) { | |
635 // Returns the exact height as it is trivial to compute for this layout. | |
636 return getHeight(viewLength); | |
637 } | |
638 | |
639 int getEstimatedWidth(int viewLength) { | |
640 // Returns the exact height as it is trivial to compute for this layout. | |
641 return getWidth(viewLength); | |
642 } | |
643 | |
644 int getEstimatedLength(int viewLength) { | |
645 // Returns the exact length as it is trivial to compute for this layout. | |
646 return getLength(viewLength); | |
647 } | |
648 | |
649 int getLength(int viewLength) { | |
650 int itemLength = | |
651 _vertical ? itemViewFactory.height : itemViewFactory.width; | |
652 if (viewLength == null || viewLength == 0) { | |
653 return itemLength * _data.length; | |
654 } else if (_paginate) { | |
655 if (_data.length > 0) { | |
656 final pageLength = getPageLength(viewLength); | |
657 return getPage(_data.length - 1, viewLength) | |
658 * pageLength + Math.max(viewLength, pageLength); | |
659 } else { | |
660 return 0; | |
661 } | |
662 } else { | |
663 return itemLength * (_data.length - 1) + Math.max(viewLength, itemLength); | |
664 } | |
665 } | |
666 | |
667 int getOffset(int index) { | |
668 return index * _itemLength; | |
669 } | |
670 | |
671 int getPageLength(int viewLength) { | |
672 final itemsPerPage = (viewLength / _itemLength).floor(); | |
673 return (Math.max(1, itemsPerPage) * _itemLength).toInt(); | |
674 } | |
675 | |
676 int getPage(int index, int viewLength) { | |
677 return (getOffset(index) / getPageLength(viewLength)).floor().toInt(); | |
678 } | |
679 | |
680 int getPageStartIndex(int page, int viewLength) { | |
681 return (getPageLength(viewLength) / _itemLength).toInt() * page; | |
682 } | |
683 | |
684 int getSnapIndex(num offset, int viewLength) { | |
685 int index = (-offset / _itemLength).round().toInt(); | |
686 if (_paginate) { | |
687 index = getPageStartIndex(getPage(index, viewLength), viewLength); | |
688 } | |
689 return GoogleMath.clamp(index, 0, _data.length - 1); | |
690 } | |
691 | |
692 Interval computeVisibleInterval( | |
693 num offset, num viewLength, num bufferLength) { | |
694 num targetIntervalStart = | |
695 Math.max(0,((-offset - bufferLength) / _itemLength).floor()); | |
696 num targetIntervalEnd = GoogleMath.clamp( | |
697 ((-offset + viewLength + bufferLength) / _itemLength).ceil(), | |
698 targetIntervalStart, | |
699 _data.length); | |
700 return new Interval(targetIntervalStart.toInt(), | |
701 targetIntervalEnd.toInt()); | |
702 } | |
703 } | |
704 | |
705 /** | |
706 * Simple list view class where each item has fixed width and height. | |
707 */ | |
708 class ListView<D> extends GenericListView<D> { | |
709 | |
710 /** | |
711 * Creates a new ListView for the given data. If [:_data:] is an | |
712 * [:ObservableList<T>:] then it will listen to changes to the list and | |
713 * update the view appropriately. | |
714 */ | |
715 ListView(List<D> data, ViewFactory<D> itemViewFactory, bool scrollable, | |
716 bool vertical, ObservableValue<D> selectedItem, | |
717 [bool snapToItems = false, | |
718 bool paginate = false, | |
719 bool removeClippedViews = false, | |
720 bool showScrollbar = false, | |
721 PageState pages = null]) | |
722 : super(new FixedSizeListViewLayout<D>(itemViewFactory, data, vertical, | |
723 paginate), | |
724 data, scrollable, vertical, selectedItem, snapToItems, paginate, | |
725 removeClippedViews, showScrollbar, pages); | |
726 } | |
727 | |
728 /** | |
729 * Layout where each item may have variable size along the axis the list view | |
730 * extends. | |
731 */ | |
732 class VariableSizeListViewLayout<D> implements ListViewLayout<D> { | |
733 List<D> _data; | |
734 List<int> _itemOffsets; | |
735 List<int> _lengths; | |
736 int _lastOffset = 0; | |
737 bool _vertical; | |
738 bool _paginate; | |
739 VariableSizeViewFactory<D> itemViewFactory; | |
740 Interval _lastVisibleInterval; | |
741 | |
742 VariableSizeListViewLayout(this.itemViewFactory, data, this._vertical, | |
743 this._paginate) : | |
744 _data = data, | |
745 _lastVisibleInterval = new Interval(0, 0) { | |
746 _itemOffsets = <int>[]; | |
747 _lengths = <int>[]; | |
748 _itemOffsets.add(0); | |
749 } | |
750 | |
751 void onDataChange() { | |
752 _itemOffsets.clear(); | |
753 _itemOffsets.add(0); | |
754 _lengths.clear(); | |
755 } | |
756 | |
757 View newView(int index) => itemViewFactory.newView(_data[index]); | |
758 | |
759 int getWidth(int viewLength) { | |
760 if (_vertical) { | |
761 return itemViewFactory.getWidth(null); | |
762 } else { | |
763 return getLength(viewLength); | |
764 } | |
765 } | |
766 | |
767 int getHeight(int viewLength) { | |
768 if (_vertical) { | |
769 return getLength(viewLength); | |
770 } else { | |
771 return itemViewFactory.getHeight(null); | |
772 } | |
773 } | |
774 | |
775 int getEstimatedHeight(int viewLength) { | |
776 if (_vertical) { | |
777 return getEstimatedLength(viewLength); | |
778 } else { | |
779 return itemViewFactory.getHeight(null); | |
780 } | |
781 } | |
782 | |
783 int getEstimatedWidth(int viewLength) { | |
784 if (_vertical) { | |
785 return itemViewFactory.getWidth(null); | |
786 } else { | |
787 return getEstimatedLength(viewLength); | |
788 } | |
789 } | |
790 | |
791 // TODO(jacobr): this logic is overly complicated. Replace with something | |
792 // simpler. | |
793 int getEstimatedLength(int viewLength) { | |
794 if (_lengths.length == _data.length) { | |
795 // No need to estimate... we have all the data already. | |
796 return getLength(viewLength); | |
797 } | |
798 if (_itemOffsets.length > 1 && _lengths.length > 0) { | |
799 // Estimate length by taking the average of the lengths | |
800 // of the known views. | |
801 num lengthFromAllButLastElement = 0; | |
802 if (_itemOffsets.length > 2) { | |
803 lengthFromAllButLastElement = | |
804 (getOffset(_itemOffsets.length - 2) - | |
805 getOffset(0)) * | |
806 (_data.length / (_itemOffsets.length - 2)); | |
807 } | |
808 return (lengthFromAllButLastElement + | |
809 Math.max(viewLength, _lengths[_lengths.length - 1])).toInt(); | |
810 } else { | |
811 if (_lengths.length == 1) { | |
812 return Math.max(viewLength, _lengths[0]); | |
813 } else { | |
814 return viewLength; | |
815 } | |
816 } | |
817 } | |
818 | |
819 int getLength(int viewLength) { | |
820 if (_data.length == 0) { | |
821 return viewLength; | |
822 } else { | |
823 // Hack so that _lengths[length - 1] is available. | |
824 getOffset(_data.length); | |
825 return (getOffset(_data.length - 1) - getOffset(0)) + | |
826 Math.max(_lengths[_lengths.length - 1], viewLength); | |
827 } | |
828 } | |
829 | |
830 int getOffset(int index) { | |
831 if (index >= _itemOffsets.length) { | |
832 int offset = _itemOffsets[_itemOffsets.length - 1]; | |
833 for (int i = _itemOffsets.length; i <= index; i++) { | |
834 int length = _vertical ? itemViewFactory.getHeight(_data[i - 1]) | |
835 : itemViewFactory.getWidth(_data[i - 1]); | |
836 offset += length; | |
837 _itemOffsets.add(offset); | |
838 _lengths.add(length); | |
839 } | |
840 } | |
841 return _itemOffsets[index]; | |
842 } | |
843 | |
844 int getPage(int index, int viewLength) { | |
845 // TODO(jacobr): implement. | |
846 throw 'Not implemented'; | |
847 } | |
848 | |
849 int getPageStartIndex(int page, int viewLength) { | |
850 // TODO(jacobr): implement. | |
851 throw 'Not implemented'; | |
852 } | |
853 | |
854 int getSnapIndex(num offset, int viewLength) { | |
855 for (int i = 1; i < _data.length; i++) { | |
856 if (getOffset(i) + getOffset(i - 1) > -offset * 2) { | |
857 return i - 1; | |
858 } | |
859 } | |
860 return _data.length - 1; | |
861 } | |
862 | |
863 Interval computeVisibleInterval( | |
864 num offset, num viewLength, num bufferLength) { | |
865 offset = offset.toInt(); | |
866 int start = _findFirstItemBefore( | |
867 -offset - bufferLength, | |
868 _lastVisibleInterval != null ? _lastVisibleInterval.start : 0); | |
869 int end = _findFirstItemAfter( | |
870 -offset + viewLength + bufferLength, | |
871 _lastVisibleInterval != null ? _lastVisibleInterval.end : 0); | |
872 _lastVisibleInterval = new Interval(start, Math.max(start, end)); | |
873 _lastOffset = offset; | |
874 return _lastVisibleInterval; | |
875 } | |
876 | |
877 int _findFirstItemAfter(num target, int hint) { | |
878 for (int i = 0; i < _data.length; i++) { | |
879 if (getOffset(i) > target) { | |
880 return i; | |
881 } | |
882 } | |
883 return _data.length; | |
884 } | |
885 | |
886 // TODO(jacobr): use hint. | |
887 int _findFirstItemBefore(num target, int hint) { | |
888 // We go search this direction delaying computing the actual view size | |
889 // as long as possible. | |
890 for (int i = 1; i < _data.length; i++) { | |
891 if (getOffset(i) >= target) { | |
892 return i - 1; | |
893 } | |
894 } | |
895 return Math.max(_data.length - 1, 0); | |
896 } | |
897 } | |
898 | |
899 class VariableSizeListView<D> extends GenericListView<D> { | |
900 | |
901 VariableSizeListView(List<D> data, | |
902 VariableSizeViewFactory<D> itemViewFactory, | |
903 bool scrollable, | |
904 bool vertical, | |
905 ObservableValue<D> selectedItem, | |
906 [bool snapToItems = false, | |
907 bool paginate = false, | |
908 bool removeClippedViews = false, | |
909 bool showScrollbar = false, | |
910 PageState pages = null]) | |
911 : super(new VariableSizeListViewLayout(itemViewFactory, data, vertical, | |
912 paginate), | |
913 data, scrollable, vertical, selectedItem, snapToItems, | |
914 paginate, removeClippedViews, showScrollbar, pages); | |
915 } | |
916 | |
917 /** A back button that is equivalent to clicking "back" in the browser. */ | |
918 class BackButton extends View { | |
919 BackButton() : super(); | |
920 | |
921 Element render() => new Element.html('<div class="back-arrow button"></div>'); | |
922 | |
923 void afterRender(Element node) { | |
924 addOnClick((e) => window.history.back()); | |
925 } | |
926 } | |
927 | |
928 | |
929 // TODO(terry): Maybe should be part of ButtonView class in appstack/view? | |
930 /** OS button. */ | |
931 class PushButtonView extends View { | |
932 final String _text; | |
933 final String _cssClass; | |
934 final _clickHandler; | |
935 | |
936 PushButtonView(this._text, this._cssClass, this._clickHandler) : super(); | |
937 | |
938 Element render() { | |
939 return new Element.html('<button class="${_cssClass}">${_text}</button>'); | |
940 } | |
941 | |
942 void afterRender(Element node) { | |
943 addOnClick(_clickHandler); | |
944 } | |
945 } | |
946 | |
947 | |
948 // TODO(terry): Add a drop shadow around edge and corners need to be rounded. | |
949 // Need to support conveyor for contents of dialog so it's not | |
950 // larger than the parent window. | |
951 /** A generic dialog view supports title, done button and dialog content. */ | |
952 class DialogView extends View { | |
953 final String _title; | |
954 final String _cssName; | |
955 final View _content; | |
956 Element container; | |
957 PushButtonView _done; | |
958 | |
959 DialogView(this._title, this._cssName, this._content) : super() {} | |
960 | |
961 Element render() { | |
962 final node = new Element.html(''' | |
963 <div class="dialog-modal"> | |
964 <div class="dialog $_cssName"> | |
965 <div class="dialog-title-area"> | |
966 <span class="dialog-title">$_title</span> | |
967 </div> | |
968 <div class="dialog-body"></div> | |
969 </div> | |
970 </div>'''); | |
971 | |
972 _done = new PushButtonView('Done', 'done-button', | |
973 EventBatch.wrap((e) => onDone())); | |
974 final titleArea = node.query('.dialog-title-area'); | |
975 titleArea.nodes.add(_done.node); | |
976 | |
977 container = node.query('.dialog-body'); | |
978 container.nodes.add(_content.node); | |
979 | |
980 return node; | |
981 } | |
982 | |
983 /** Override to handle dialog done. */ | |
984 void onDone() { } | |
985 } | |
OLD | NEW |