| 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 /** | |
| 6 * Implements a grid-based layout system based on: | |
| 7 * [http://dev.w3.org/csswg/css3-grid-align/] | |
| 8 * | |
| 9 * This layout is designed to support animations and work on browsers that | |
| 10 * don't support grid natively. As such, we implement it on top of absolute | |
| 11 * positioning. | |
| 12 */ | |
| 13 // TODO(jmesserly): the DOM integration still needs work: | |
| 14 // - The grid assumes it is absolutely positioned in its container. | |
| 15 // Becasue of that, the grid doesn't work right unless it has at least one | |
| 16 // fractional size in each dimension. In other words, only "top down" grids | |
| 17 // work at the moment, because the grid can't determine its own size. | |
| 18 // The core algorithm supports computing min breadth; the issue is about how | |
| 19 // to integrate it into our View layer. | |
| 20 // - Unless a child element is "display: inline-block" we can't get its | |
| 21 // horizontal content size. | |
| 22 // - Once we set an element's size to "position: absolute", we lose the | |
| 23 // ability to get its original content size. If the width or height gets | |
| 24 // set to something other than the content size, we can't recover the | |
| 25 // original content size. | |
| 26 // - There's some rounding to ints when we want to set the positions of our | |
| 27 // tracks. I don't think we necessarily need to do that. | |
| 28 // | |
| 29 // TODO(jmesserly): Some features of the spec are unimplemented: | |
| 30 // - grid-flow & items that have row and column set to 'auto'. | |
| 31 // - Grid writing modes (right to left languages, etc) | |
| 32 // - We don't do a second calculation pass if min content size of a grid-item | |
| 33 // changes due to column width. | |
| 34 // - The CSS parsing is not 100% complete, see the parser TODOs. | |
| 35 // - We don't implement error recovery for invalid combinations of CSS | |
| 36 // properties, or invalid CSS property values. Instead we throw an error. | |
| 37 // | |
| 38 // TODO(jmesserly): high level performance optimizations we could do: | |
| 39 // - Optimize for the common case of spanCount = 1 | |
| 40 // - Optimize for the vbox/hbox case (1 row or 1 column) | |
| 41 // - Optimize for the case of no content sized tracks | |
| 42 // - Optimize for the "incremental update" cases | |
| 43 class GridLayout extends ViewLayout { | |
| 44 | |
| 45 /** Configuration parameters defined in CSS. */ | |
| 46 final GridTrackList rows; | |
| 47 final GridTrackList columns; | |
| 48 final GridTemplate template; | |
| 49 | |
| 50 /** The default sizing for new rows. */ | |
| 51 final TrackSizing rowSizing; | |
| 52 | |
| 53 /** The default sizing for new columns. */ | |
| 54 final TrackSizing columnSizing; | |
| 55 | |
| 56 /** | |
| 57 * This stores the grid's size during a layout. | |
| 58 * Used for rows/columns with % or fr units. | |
| 59 */ | |
| 60 int _gridWidth, _gridHeight; | |
| 61 | |
| 62 /** | |
| 63 * During a layout, this stores all row/column size information. | |
| 64 * Because grid-items can implicitly specify their own rows/columns, we can't | |
| 65 * compute this until we know the set of items. | |
| 66 */ | |
| 67 List<GridTrack> _rowTracks, _columnTracks; | |
| 68 | |
| 69 /** During a layout, tracks which dimension we're processing. */ | |
| 70 Dimension _dimension; | |
| 71 | |
| 72 GridLayout(Positionable view) | |
| 73 : super(view), | |
| 74 rows = _GridTrackParser.parse(view.customStyle['grid-rows']), | |
| 75 columns = _GridTrackParser.parse(view.customStyle['grid-columns']), | |
| 76 template = _GridTemplateParser.parse(view.customStyle['grid-template']), | |
| 77 | |
| 78 rowSizing = _GridTrackParser.parseTrackSizing( | |
| 79 view.customStyle['grid-row-sizing']), | |
| 80 | |
| 81 columnSizing = _GridTrackParser.parseTrackSizing( | |
| 82 view.customStyle['grid-column-sizing']) { | |
| 83 | |
| 84 _rowTracks = rows != null ? rows.tracks : new List<GridTrack>(); | |
| 85 _columnTracks = columns != null ? columns.tracks : new List<GridTrack>(); | |
| 86 } | |
| 87 | |
| 88 | |
| 89 int get currentWidth() => _gridWidth; | |
| 90 int get currentHeight() => _gridHeight; | |
| 91 | |
| 92 void cacheExistingBrowserLayout() { | |
| 93 // We don't need to do anything as we don't rely on the _cachedViewRect | |
| 94 // when the grid layout is used. | |
| 95 } | |
| 96 | |
| 97 // TODO(jacobr): cleanup this method so that it returns a Future | |
| 98 // rather than taking a Completer as an argument. | |
| 99 /** The main entry point for layout computation. */ | |
| 100 void measureLayout(Future<Size> size, Completer<bool> changed) { | |
| 101 _ensureAllTracks(); | |
| 102 window.requestLayoutFrame(() { | |
| 103 _gridWidth = size.value.width; | |
| 104 _gridHeight = size.value.height; | |
| 105 | |
| 106 if (_rowTracks.length > 0 && _columnTracks.length > 0) { | |
| 107 _measureTracks(); | |
| 108 _setBoundsOfChildren(); | |
| 109 if (changed != null) { | |
| 110 changed.complete(true); | |
| 111 } | |
| 112 } | |
| 113 }); | |
| 114 } | |
| 115 | |
| 116 /** | |
| 117 * The top level measurement function. | |
| 118 * [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks] | |
| 119 */ | |
| 120 void _measureTracks() { | |
| 121 // Resolve logical width, then height. Width comes first so we can use | |
| 122 // the width when determining the content-sized height. | |
| 123 try { | |
| 124 _dimension = Dimension.WIDTH; | |
| 125 _computeUsedBreadthOfTracks(_columnTracks); | |
| 126 _dimension = Dimension.HEIGHT; | |
| 127 _computeUsedBreadthOfTracks(_rowTracks); | |
| 128 } finally { | |
| 129 _dimension = null; | |
| 130 } | |
| 131 | |
| 132 // TODO(jmesserly): we're supposed to detect a min-content size change | |
| 133 // due to our computed width and trigger a new layout. | |
| 134 // How do we implement that? | |
| 135 } | |
| 136 | |
| 137 num _getRemainingSpace(List<GridTrack> tracks) { | |
| 138 num remaining = _getGridContentSize(); | |
| 139 remaining -= CollectionUtils.sum(tracks, (t) => t.usedBreadth); | |
| 140 return Math.max(0, remaining); | |
| 141 } | |
| 142 | |
| 143 /** | |
| 144 * This is the core Grid Track sizing algorithm. It is run for Grid columns | |
| 145 * and Grid rows. The goal of the function is to ensure: | |
| 146 * 1. That each Grid Track satisfies its minSizing | |
| 147 * 2. That each Grid Track grows from the breadth which satisfied its | |
| 148 * minSizing to a breadth which satifies its | |
| 149 * maxSizing, subject to RemainingSpace. | |
| 150 */ | |
| 151 // Note: spec does not correctly doc all the parameters to this function. | |
| 152 void _computeUsedBreadthOfTracks(List<GridTrack> tracks) { | |
| 153 | |
| 154 // TODO(jmesserly): as a performance optimization we could cache this | |
| 155 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
| 156 CollectionUtils.sortBy(items, (item) => _getSpanCount(item)); | |
| 157 | |
| 158 // 1. Initialize per Grid Track variables | |
| 159 for (final t in tracks) { | |
| 160 // percentage or length sizing functions will return a value | |
| 161 // min-content, max-content, or a fraction will be set to 0 | |
| 162 t.usedBreadth = t.minSizing.resolveLength(_getGridContentSize()); | |
| 163 t.maxBreadth = t.maxSizing.resolveLength(_getGridContentSize()); | |
| 164 t.updatedBreadth = 0; | |
| 165 } | |
| 166 | |
| 167 // 2. Resolve content-based MinTrackSizingFunctions | |
| 168 final USED_BREADTH = const _UsedBreadthAccumulator(); | |
| 169 final MAX_BREADTH = const _MaxBreadthAccumulator(); | |
| 170 | |
| 171 _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH); | |
| 172 | |
| 173 _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH); | |
| 174 | |
| 175 // 3. Ensure that maxBreadth is as big as usedBreadth for each track | |
| 176 for (final t in tracks) { | |
| 177 if (t.maxBreadth < t.usedBreadth) { | |
| 178 t.maxBreadth = t.usedBreadth; | |
| 179 } | |
| 180 } | |
| 181 | |
| 182 // 4. Resolve content-based MaxTrackSizingFunctions | |
| 183 _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH); | |
| 184 | |
| 185 _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH); | |
| 186 | |
| 187 // 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their | |
| 188 // maxBreadth value until RemainingSpace is exhausted. | |
| 189 // Note: it's not spec'd what to pass as the accumulator, but usedBreadth | |
| 190 // seems right. | |
| 191 _distributeSpaceToTracks(tracks, _getRemainingSpace(tracks), | |
| 192 USED_BREADTH, false); | |
| 193 | |
| 194 // Spec wording is confusing about which direction this assignment happens, | |
| 195 // but this is the way that makes sense. | |
| 196 for (final t in tracks) { | |
| 197 t.usedBreadth = t.updatedBreadth; | |
| 198 } | |
| 199 | |
| 200 // 6. Grow all Grid Tracks having a fraction as their maxSizing | |
| 201 final tempBreadth = _calcNormalizedFractionBreadth(tracks); | |
| 202 for (final t in tracks) { | |
| 203 t.usedBreadth = Math.max(t.usedBreadth, | |
| 204 tempBreadth * t.maxSizing.fractionValue); | |
| 205 } | |
| 206 | |
| 207 _computeTrackPositions(tracks); | |
| 208 } | |
| 209 | |
| 210 /** | |
| 211 * Final steps to finish positioning tracks. Takes the track size and uses | |
| 212 * it to get start and end positions. Also rounds the positions to integers. | |
| 213 */ | |
| 214 void _computeTrackPositions(List<GridTrack> tracks) { | |
| 215 // Compute start positions of tracks, as well as the final position | |
| 216 | |
| 217 num position = 0; | |
| 218 for (final t in tracks) { | |
| 219 t.start = position; | |
| 220 position += t.usedBreadth; | |
| 221 } | |
| 222 | |
| 223 // Now, go through and round each position to an integer. Then | |
| 224 // compute the sizes based on those integers. | |
| 225 num finalPosition = position; | |
| 226 | |
| 227 for (int i = 0; i < tracks.length; i++) { | |
| 228 int startEdge = tracks[i].start; | |
| 229 int endEdge; | |
| 230 if (i < tracks.length - 1) { | |
| 231 endEdge = tracks[i + 1].start.round().toInt(); | |
| 232 tracks[i + 1].start = endEdge; | |
| 233 } else { | |
| 234 endEdge = finalPosition.round().toInt(); | |
| 235 } | |
| 236 int breadth = endEdge - startEdge; | |
| 237 | |
| 238 // check that we're not off by >= 1px. | |
| 239 assert((endEdge - startEdge - tracks[i].usedBreadth).abs() < 1); | |
| 240 | |
| 241 tracks[i].usedBreadth = breadth; | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 /** | |
| 246 * This method computes a '1fr' value, referred to as the | |
| 247 * tempBreadth, for a set of Grid Tracks. The value computed | |
| 248 * will ensure that when the tempBreadth is multiplied by the | |
| 249 * fractions associated with tracks, that the UsedBreadths of tracks | |
| 250 * will increase by an amount equal to the maximum of zero and the specified | |
| 251 * freeSpace less the sum of the current UsedBreadths. | |
| 252 */ | |
| 253 num _calcNormalizedFractionBreadth(List<GridTrack> tracks) { | |
| 254 | |
| 255 final fractionTracks = tracks.filter((t) => t.maxSizing.isFraction); | |
| 256 | |
| 257 // Note: the spec has various bugs in this function, such as mismatched | |
| 258 // identifiers and names that aren't defined. For the most part it's | |
| 259 // possible to figure out the meaning. It's also a bit confused about | |
| 260 // how to compute spaceNeededFromFractionTracks, but that should just be the | |
| 261 // set to the remaining free space after usedBreadth is accounted for. | |
| 262 | |
| 263 // We use the tempBreadth field to store the normalized fraction breadth | |
| 264 for (final t in fractionTracks) { | |
| 265 t.tempBreadth = t.usedBreadth / t.maxSizing.fractionValue; | |
| 266 } | |
| 267 | |
| 268 CollectionUtils.sortBy(fractionTracks, (t) => t.tempBreadth); | |
| 269 | |
| 270 num spaceNeededFromFractionTracks = _getRemainingSpace(tracks); | |
| 271 num currentBandFractionBreadth = 0; | |
| 272 num accumulatedFractions = 0; | |
| 273 for (final t in fractionTracks) { | |
| 274 if (t.tempBreadth != currentBandFractionBreadth) { | |
| 275 if (t.tempBreadth * accumulatedFractions > | |
| 276 spaceNeededFromFractionTracks) { | |
| 277 break; | |
| 278 } | |
| 279 currentBandFractionBreadth = t.tempBreadth; | |
| 280 } | |
| 281 accumulatedFractions += t.maxSizing.fractionValue; | |
| 282 spaceNeededFromFractionTracks += t.usedBreadth; | |
| 283 } | |
| 284 return spaceNeededFromFractionTracks / accumulatedFractions; | |
| 285 } | |
| 286 | |
| 287 /** | |
| 288 * Ensures that for each Grid Track in tracks, a value will be | |
| 289 * computed, updatedBreadth, that represents the Grid Track's share of | |
| 290 * freeSpace. | |
| 291 */ | |
| 292 void _distributeSpaceToTracks(List<GridTrack> tracks, num freeSpace, | |
| 293 _BreadthAccumulator breadth, bool ignoreMaxBreadth) { | |
| 294 | |
| 295 // TODO(jmesserly): in some cases it would be safe to sort the passed in | |
| 296 // list in place. Not always though. | |
| 297 tracks = CollectionUtils.orderBy(tracks, | |
| 298 (t) => t.maxBreadth - breadth.getSize(t)); | |
| 299 | |
| 300 // Give each Grid Track an equal share of the space, but without exceeding | |
| 301 // their maxBreadth values. Because there are different MaxBreadths | |
| 302 // assigned to the different Grid Tracks, this can result in uneven growth. | |
| 303 for (int i = 0; i < tracks.length; i++) { | |
| 304 num share = freeSpace / (tracks.length - i); | |
| 305 share = Math.min(share, tracks[i].maxBreadth); | |
| 306 tracks[i].tempBreadth = share; | |
| 307 freeSpace -= share; | |
| 308 } | |
| 309 | |
| 310 // If the first loop completed having grown every Grid Track to its | |
| 311 // maxBreadth, and there is still freeSpace, then divide that space | |
| 312 // evenly and assign it to each Grid Track without regard for its | |
| 313 // maxBreadth. This phase of growth will always be even, but only occurs | |
| 314 // when the ignoreMaxBreadth flag is true. | |
| 315 if (freeSpace > 0 && ignoreMaxBreadth) { | |
| 316 for (int i = 0; i < tracks.length; i++) { | |
| 317 final share = freeSpace / (tracks.length - i); | |
| 318 tracks[i].tempBreadth += share; | |
| 319 freeSpace -= share; | |
| 320 } | |
| 321 } | |
| 322 | |
| 323 // Note: the spec has us updating all grid tracks, not just the passed in | |
| 324 // tracks, but I think that's a spec bug. | |
| 325 for (final t in tracks) { | |
| 326 t.updatedBreadth = Math.max(t.updatedBreadth, t.tempBreadth); | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 /** | |
| 331 * This function prioritizes the distribution of space driven by Grid Items | |
| 332 * in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid | |
| 333 * Items having a lower spanCount have an opportunity to increase the size of | |
| 334 * the Grid Tracks they cover before those with larger SpanCounts. | |
| 335 * | |
| 336 * Note: items are assumed to be already sorted in increasing span count | |
| 337 */ | |
| 338 void _distributeSpaceBySpanCount(List<ViewLayout> items, | |
| 339 ContentSizeMode sizeMode, _BreadthAccumulator breadth) { | |
| 340 | |
| 341 items = items.filter((item) => | |
| 342 _hasContentSizedTracks(_getTracks(item), sizeMode, breadth)); | |
| 343 | |
| 344 var tracks = []; | |
| 345 | |
| 346 for (int i = 0; i < items.length; i++) { | |
| 347 final item = items[i]; | |
| 348 | |
| 349 final itemTargetSize = item.measureContent(this, _dimension, sizeMode); | |
| 350 | |
| 351 final spannedTracks = _getTracks(item); | |
| 352 _distributeSpaceToTracks(spannedTracks, itemTargetSize, breadth, true); | |
| 353 | |
| 354 // Remember that we need to update the sizes on these tracks | |
| 355 tracks.addAll(spannedTracks); | |
| 356 | |
| 357 // Each time we transition to a new spanCount, update any modified tracks | |
| 358 bool spanCountFinished = false; | |
| 359 if (i + 1 == items.length) { | |
| 360 spanCountFinished = true; | |
| 361 } else if (_getSpanCount(item) != _getSpanCount(items[i + 1])) { | |
| 362 spanCountFinished = true; | |
| 363 } | |
| 364 | |
| 365 if (spanCountFinished) { | |
| 366 for (final t in tracks) { | |
| 367 breadth.setSize(t, | |
| 368 Math.max(breadth.getSize(t), t.updatedBreadth)); | |
| 369 } | |
| 370 tracks = []; | |
| 371 } | |
| 372 } | |
| 373 } | |
| 374 | |
| 375 /** | |
| 376 * Returns true if we have an appropriate content sized dimension, and don't | |
| 377 * cross a fractional track. | |
| 378 */ | |
| 379 static bool _hasContentSizedTracks(Collection<GridTrack> tracks, | |
| 380 ContentSizeMode sizeMode, _BreadthAccumulator breadth) { | |
| 381 | |
| 382 for (final t in tracks) { | |
| 383 final fn = breadth.getSizingFunction(t); | |
| 384 if (sizeMode == ContentSizeMode.MAX && fn.isMaxContentSized || | |
| 385 sizeMode == ContentSizeMode.MIN && fn.isContentSized) { | |
| 386 | |
| 387 // Make sure we don't cross a fractional track | |
| 388 return tracks.length == 1 || !tracks.some((t_) => t_.isFractional); | |
| 389 } | |
| 390 } | |
| 391 return false; | |
| 392 } | |
| 393 | |
| 394 /** Ensures that the numbered track exists. */ | |
| 395 void _ensureTrack(List<GridTrack> tracks, TrackSizing sizing, | |
| 396 int start, int span) { | |
| 397 // Start is 1-based. Make it 0-based. | |
| 398 start -= 1; | |
| 399 | |
| 400 // Grow the list if needed | |
| 401 int length = start + span; | |
| 402 int first = Math.min(start, tracks.length); | |
| 403 tracks.length = Math.max(tracks.length, length); | |
| 404 | |
| 405 // Fill in tracks | |
| 406 for (int i = first; i < length; i++) { | |
| 407 if (tracks[i] == null) { | |
| 408 tracks[i] = new GridTrack(sizing); | |
| 409 } | |
| 410 } | |
| 411 } | |
| 412 | |
| 413 /** | |
| 414 * Scans children creating GridLayoutParams as needed, and creates all of the | |
| 415 * rows and columns that we will need. | |
| 416 * | |
| 417 * Note: this can potentially create new rows/columns, so this needs to be | |
| 418 * run before the track sizing algorithm. | |
| 419 */ | |
| 420 void _ensureAllTracks() { | |
| 421 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
| 422 | |
| 423 for (final child in items) { | |
| 424 if (child.layoutParams == null) { | |
| 425 final p = new GridLayoutParams(child.view, this); | |
| 426 _ensureTrack(_rowTracks, rowSizing, p.row, p.rowSpan); | |
| 427 _ensureTrack(_columnTracks, columnSizing, p.column, p.columnSpan); | |
| 428 child.layoutParams = p; | |
| 429 } | |
| 430 child.cacheExistingBrowserLayout(); | |
| 431 } | |
| 432 } | |
| 433 | |
| 434 /** | |
| 435 * Given the track sizes that were computed, position children in the grid. | |
| 436 */ | |
| 437 void _setBoundsOfChildren() { | |
| 438 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
| 439 | |
| 440 for (final item in items) { | |
| 441 GridLayoutParams childLayout = item.layoutParams; | |
| 442 var xPos = _getTrackLocationX(childLayout); | |
| 443 var yPos = _getTrackLocationY(childLayout); | |
| 444 | |
| 445 int left = xPos.start, width = xPos.length; | |
| 446 int top = yPos.start, height = yPos.length; | |
| 447 | |
| 448 // Somewhat counterintuitively (at least to me): | |
| 449 // grid-col-align is the horizontal alignment | |
| 450 // grid-row-align is the vertical alignment | |
| 451 xPos = childLayout.columnAlign.align(xPos, item.currentWidth); | |
| 452 yPos = childLayout.rowAlign.align(yPos, item.currentHeight); | |
| 453 | |
| 454 item.setBounds(xPos.start, yPos.start, xPos.length, yPos.length); | |
| 455 } | |
| 456 } | |
| 457 | |
| 458 num _getGridContentSize() { | |
| 459 switch (_dimension) { | |
| 460 case Dimension.WIDTH: | |
| 461 return _gridWidth; | |
| 462 case Dimension.HEIGHT: | |
| 463 return _gridHeight; | |
| 464 } | |
| 465 } | |
| 466 | |
| 467 _GridLocation _getTrackLocationX(GridLayoutParams childLayout) { | |
| 468 int start = childLayout.column - 1; | |
| 469 int end = start + childLayout.columnSpan - 1; | |
| 470 | |
| 471 start = _columnTracks[start].start; | |
| 472 end = _columnTracks[end].end; | |
| 473 | |
| 474 return new _GridLocation(start, end - start); | |
| 475 } | |
| 476 | |
| 477 _GridLocation _getTrackLocationY(GridLayoutParams childLayout) { | |
| 478 int start = childLayout.row - 1; | |
| 479 int end = start + childLayout.rowSpan - 1; | |
| 480 | |
| 481 start = _rowTracks[start].start; | |
| 482 end = _rowTracks[end].end; | |
| 483 | |
| 484 return new _GridLocation(start, end - start); | |
| 485 } | |
| 486 | |
| 487 /** Gets the tracks that this item crosses. */ | |
| 488 // TODO(jmesserly): might be better to return an iterable | |
| 489 List<GridTrack> _getTracks(ViewLayout item) { | |
| 490 GridLayoutParams childLayout = item.layoutParams; | |
| 491 | |
| 492 int start, span; | |
| 493 List<GridTrack> tracks; | |
| 494 switch (_dimension) { | |
| 495 case Dimension.WIDTH: | |
| 496 start = childLayout.column - 1; | |
| 497 span = childLayout.columnSpan; | |
| 498 tracks = _columnTracks; | |
| 499 break; | |
| 500 case Dimension.HEIGHT: | |
| 501 start = childLayout.row - 1; | |
| 502 span = childLayout.rowSpan; | |
| 503 tracks = _rowTracks; | |
| 504 } | |
| 505 | |
| 506 assert(start >= 0 && span >= 1); | |
| 507 | |
| 508 final result = new List<GridTrack>(span); | |
| 509 for (int i = 0; i < span; i++) { | |
| 510 result[i] = tracks[start + i]; | |
| 511 } | |
| 512 return result; | |
| 513 } | |
| 514 | |
| 515 int _getSpanCount(ViewLayout item) { | |
| 516 GridLayoutParams childLayout = item.layoutParams; | |
| 517 return (_dimension == Dimension.WIDTH ? | |
| 518 childLayout.columnSpan : childLayout.rowSpan); | |
| 519 } | |
| 520 } | |
| OLD | NEW |