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 class PageState { | |
6 final ObservableValue<int> current; | |
7 final ObservableValue<int> target; | |
8 final ObservableValue<int> length; | |
9 PageState() : | |
10 current = new ObservableValue<int>(0), | |
11 target = new ObservableValue<int>(0), | |
12 length = new ObservableValue<int>(1); | |
13 } | |
14 | |
15 /** Simplifies using a PageNumberView and PagedColumnView together. */ | |
16 class PagedContentView extends CompositeView { | |
17 final View content; | |
18 final PageState pages; | |
19 | |
20 PagedContentView(this.content) | |
21 : super('paged-content'), | |
22 pages = new PageState() { | |
23 addChild(new PagedColumnView(pages, content)); | |
24 addChild(new PageNumberView(pages)); | |
25 } | |
26 } | |
27 | |
28 /** Displays current page and a left/right arrow. Used with [PagedColumnView] */ | |
29 class PageNumberView extends View { | |
30 final PageState pages; | |
31 Element _label; | |
32 Element _left, _right; | |
33 | |
34 PageNumberView(this.pages) : super(); | |
35 | |
36 Element render() { | |
37 // TODO(jmesserly): this was supposed to use the somewhat flatter unicode | |
38 // glyphs that Chrome uses on the new tab page, but the text is getting | |
39 // corrupted. | |
40 final node = new Element.html(''' | |
41 <div class="page-number"> | |
42 <div class="page-number-left">‹</div> | |
43 <div class="page-number-label"></div> | |
44 <div class="page-number-right">›</div> | |
45 </div> | |
46 '''); | |
47 _left = node.query('.page-number-left'); | |
48 _label = node.query('.page-number-label'); | |
49 _right = node.query('.page-number-right'); | |
50 return node; | |
51 } | |
52 | |
53 void enterDocument() { | |
54 watch(pages.current, (s) => _update()); | |
55 watch(pages.length, (s) => _update()); | |
56 | |
57 _left.on.click.add((e) { | |
58 if (pages.current.value > 0) { | |
59 pages.target.value = pages.current.value - 1; | |
60 } | |
61 }); | |
62 | |
63 _right.on.click.add((e) { | |
64 if (pages.current.value + 1 < pages.length.value) { | |
65 pages.target.value = pages.current.value + 1; | |
66 } | |
67 }); | |
68 } | |
69 | |
70 void _update() { | |
71 _label.text = '${pages.current.value + 1} of ${pages.length.value}'; | |
72 } | |
73 } | |
74 | |
75 /** | |
76 * A horizontal scrolling view that snaps to items like [ConveyorView], but only | |
77 * has one child. Instead of scrolling between views, it scrolls between content | |
78 * that flows horizontally in columns. Supports left/right swipe to switch | |
79 * between pages. Can also be used with [PageNumberView]. | |
80 * | |
81 * This control assumes that it is styled with fixed or percent width and | |
82 * height, so the content will flow out horizontally. This allows it to compute | |
83 * the number of pages using [:scrollWidth:] and [:offsetWidth:]. | |
84 */ | |
85 class PagedColumnView extends View { | |
86 | |
87 static final MIN_THROW_PAGE_FRACTION = 0.01; | |
88 final View contentView; | |
89 | |
90 final PageState pages; | |
91 | |
92 Element _container; | |
93 int _columnGap, _columnWidth; | |
94 int _viewportSize; | |
95 Scroller scroller; | |
96 | |
97 PagedColumnView(this.pages, this.contentView) : super(); | |
98 | |
99 Element render() { | |
100 final node = new Element.html(''' | |
101 <div class="paged-column"> | |
102 <div class="paged-column-container"></div> | |
103 </div>'''); | |
104 _container = node.query('.paged-column-container'); | |
105 _container.nodes.add(contentView.node); | |
106 | |
107 // TODO(jmesserly): if we end up with empty columns on the last page, | |
108 // this causes the last page to end up right justified. But it seems to | |
109 // work reasonably well for both clicking and throwing. So for now, leave | |
110 // the scroller configured the default way. | |
111 | |
112 // TODO(jacobr): use named arguments when available. | |
113 scroller = new Scroller( | |
114 _container, | |
115 false /* verticalScrollEnabled */, | |
116 true /* horizontalScrollEnabled */, | |
117 true /* momementumEnabled */, | |
118 () { | |
119 final completer = new Completer<Size>(); | |
120 _container.rect.then((ElementRect rect) { | |
121 // Only view width matters. | |
122 completer.complete(new Size(_getViewLength(rect), 1)); | |
123 }); | |
124 return completer.future; | |
125 }, | |
126 Scroller.FAST_SNAP_DECELERATION_FACTOR); | |
127 | |
128 scroller.onDecelStart.add(_snapToPage); | |
129 scroller.onScrollerDragEnd.add(_snapToPage); | |
130 scroller.onContentMoved.add(_onContentMoved); | |
131 return node; | |
132 } | |
133 | |
134 int _getViewLength(ElementRect rect) { | |
135 return _computePageSize(rect) * pages.length.value; | |
136 } | |
137 | |
138 // TODO(jmesserly): would be better to not have this code in enterDocument. | |
139 // But we need computedStyle to read our CSS properties. | |
140 void enterDocument() { | |
141 contentView.node.computedStyle.then((CSSStyleDeclaration style) { | |
142 _computeColumnGap(style); | |
143 | |
144 // Trigger a fake resize event so we measure our height. | |
145 windowResized(); | |
146 | |
147 // Hook img onload events, so we find out about changes in content size | |
148 for (ImageElement img in contentView.node.queryAll("img")) { | |
149 if (!img.complete) { | |
150 img.on.load.add((e) { | |
151 _updatePageCount(null); | |
152 }); | |
153 } | |
154 } | |
155 | |
156 // If the selected page changes, animate to it. | |
157 watch(pages.target, (s) => _onPageSelected()); | |
158 watch(pages.length, (s) => _onPageSelected()); | |
159 }); | |
160 } | |
161 | |
162 /** Read the column-gap setting so we know how far to translate the child. */ | |
163 void _computeColumnGap(CSSStyleDeclaration style) { | |
164 String gap = style.columnGap; | |
165 if (gap == 'normal') { | |
166 gap = style.fontSize; | |
167 } | |
168 _columnGap = _toPixels(gap, 'column-gap or font-size'); | |
169 _columnWidth = _toPixels(style.columnWidth, 'column-width'); | |
170 } | |
171 | |
172 static int _toPixels(String value, String message) { | |
173 // TODO(jmesserly): Safari 4 has a bug where this property does not end | |
174 // in "px" like it should, but the value is correct. Handle that gracefully. | |
175 if (value.endsWith('px')) { | |
176 value = value.substring(0, value.length - 2); | |
177 } | |
178 return Math.parseDouble(value).round().toInt(); | |
179 } | |
180 | |
181 /** Watch for resize and update page count. */ | |
182 void windowResized() { | |
183 // TODO(jmesserly): verify we aren't triggering unnecessary layouts. | |
184 | |
185 // The content needs to have its height explicitly set, or columns don't | |
186 // flow to the right correctly. So we copy our own height and set the height | |
187 // of the content. | |
188 node.rect.then((ElementRect rect) { | |
189 contentView.node.style.height = '${rect.offset.height}px'; | |
190 }); | |
191 _updatePageCount(null); | |
192 } | |
193 | |
194 bool _updatePageCount(Callback callback) { | |
195 int pageLength = 1; | |
196 _container.rect.then((ElementRect rect) { | |
197 if (rect.scroll.width > rect.offset.width) { | |
198 pageLength = (rect.scroll.width / _computePageSize(rect)) | |
199 .ceil().toInt(); | |
200 } | |
201 pageLength = Math.max(pageLength, 1); | |
202 | |
203 int oldPage = pages.target.value; | |
204 int newPage = Math.min(oldPage, pageLength - 1); | |
205 | |
206 // Hacky: make sure a change event always fires. | |
207 // This is so we adjust the 3d transform after resize. | |
208 if (oldPage == newPage) { | |
209 pages.target.value = 0; | |
210 } | |
211 assert(newPage < pageLength); | |
212 pages.target.value = newPage; | |
213 pages.length.value = pageLength; | |
214 if (callback != null) { | |
215 callback(); | |
216 } | |
217 }); | |
218 } | |
219 | |
220 void _onContentMoved(Event e) { | |
221 _container.rect.then((ElementRect rect) { | |
222 num current = scroller.contentOffset.x; | |
223 int pageSize = _computePageSize(rect); | |
224 pages.current.value = -(current / pageSize).round().toInt(); | |
225 }); | |
226 } | |
227 | |
228 void _snapToPage(Event e) { | |
229 num current = scroller.contentOffset.x; | |
230 num currentTarget = scroller.currentTarget.x; | |
231 _container.rect.then((ElementRect rect) { | |
232 int pageSize = _computePageSize(rect); | |
233 int destination; | |
234 num currentPageNumber = -(current / pageSize).round(); | |
235 num pageNumber = -currentTarget / pageSize; | |
236 if (current == currentTarget) { | |
237 // User was just static dragging so round to the nearest page. | |
238 pageNumber = pageNumber.round(); | |
239 } else { | |
240 if (currentPageNumber == pageNumber.round() && | |
241 (pageNumber - currentPageNumber).abs() > MIN_THROW_PAGE_FRACTION && | |
242 -current + _viewportSize < _getViewLength(rect) && current < 0) { | |
243 // The user is trying to throw so we want to round up to the | |
244 // nearest page in the direction they are throwing. | |
245 pageNumber = currentTarget < current | |
246 ? currentPageNumber + 1 : currentPageNumber - 1; | |
247 } else { | |
248 pageNumber = pageNumber.round(); | |
249 } | |
250 } | |
251 pageNumber = pageNumber.toInt(); | |
252 num translate = -pageNumber * pageSize; | |
253 pages.current.value = pageNumber; | |
254 if (currentTarget != translate) { | |
255 scroller.throwTo(translate, 0); | |
256 } else { | |
257 // Update the target page number when we are done animating. | |
258 pages.target.value = pageNumber; | |
259 } | |
260 }); | |
261 } | |
262 | |
263 int _computePageSize(ElementRect rect) { | |
264 // Hacky: we need to duplicate the way the columns are being computed, | |
265 // including rounding, to figure out how far to translate the div. | |
266 // See http://www.w3.org/TR/css3-multicol/#column-width | |
267 _viewportSize = rect.offset.width; | |
268 | |
269 // Figure out how many columns we're rendering. | |
270 // The algorithm ensures we're bigger than the specified min size. | |
271 int perPage = Math.max(1, | |
272 (_viewportSize + _columnGap) ~/ (_columnWidth + _columnGap)); | |
273 | |
274 // Divide up the viewport between the columns. | |
275 int columnSize = (_viewportSize - (perPage - 1) * _columnGap) ~/ perPage; | |
276 | |
277 // Finally, compute how big each page is, and how far to translate. | |
278 return perPage * (columnSize + _columnGap); | |
279 } | |
280 | |
281 void _onPageSelected() { | |
282 _container.rect.then((ElementRect rect) { | |
283 int translate = -pages.target.value * _computePageSize(rect); | |
284 scroller.throwTo(translate, 0); | |
285 }); | |
286 } | |
287 } | |
OLD | NEW |