1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|

2 | // Use of this source code is governed by a BSD-style license that can be |

3 | // found in the LICENSE file. |

4 | |

5 | import 'dart:math' as math; |

6 | |

7 | import 'package:flutter/animation.dart'; |

8 | import 'package:flutter/foundation.dart'; |

9 | import 'package:vector_math/vector_math_64.dart' show Matrix4; |

10 | |

11 | import 'box.dart'; |

12 | import 'layer.dart'; |

13 | import 'object.dart'; |

14 | import 'proxy_box.dart'; |

15 | import 'viewport.dart'; |

16 | import 'viewport_offset.dart'; |

17 | |

18 | typedef _ChildSizingFunction = double Function(RenderBox child); |

19 | |

20 | /// A delegate used by [RenderListWheelViewport] to manage its children. |

21 | /// |

22 | /// [RenderListWheelViewport] during layout will ask the delegate to create |

23 | /// children that are visible in the viewport and remove those that are not. |

24 | abstract class ListWheelChildManager { |

25 | /// The maximum number of children that can be provided to |

26 | /// [RenderListWheelViewport]. |

27 | /// |

28 | /// If non-null, the children will have index in the range |

29 | /// `[0, childCount - 1]`. |

30 | /// |

31 | /// If null, then there's no explicit limits to the range of the children |

32 | /// except that it has to be contiguous. If [childExistsAt] for a certain |

33 | /// index returns false, that index is already past the limit. |

34 | int? get childCount; |

35 | |

36 | /// Checks whether the delegate is able to provide a child widget at the given |

37 | /// index. |

38 | /// |

39 | /// This function is not about whether the child at the given index is |

40 | /// attached to the [RenderListWheelViewport] or not. |

41 | bool childExistsAt(int index); |

42 | |

43 | /// Creates a new child at the given index and updates it to the child list |

44 | /// of [RenderListWheelViewport]. If no child corresponds to `index`, then do |

45 | /// nothing. |

46 | /// |

47 | /// It is possible to create children with negative indices. |

48 | void createChild(int index, { required RenderBox? after }); |

49 | |

50 | /// Removes the child element corresponding with the given RenderBox. |

51 | void removeChild(RenderBox child); |

52 | } |

53 | |

54 | /// [ParentData] for use with [RenderListWheelViewport]. |

55 | class ListWheelParentData extends ContainerBoxParentData<RenderBox> { |

56 | /// Index of this child in its parent's child list. |

57 | /// |

58 | /// This must be maintained by the [ListWheelChildManager]. |

59 | int? index; |

60 | |

61 | /// Transform applied to this child during painting. |

62 | /// |

63 | /// Can be used to find the local bounds of this child in the viewport, |

64 | /// and then use it, for example, in hit testing. |

65 | /// |

66 | /// May be null if child was laid out, but not painted |

67 | /// by the parent, but normally this shouldn't happen, |

68 | /// because [RenderListWheelViewport] paints all of the |

69 | /// children it has laid out. |

70 | Matrix4? transform; |

71 | } |

72 | |

73 | /// Render, onto a wheel, a bigger sequential set of objects inside this viewport. |

74 | /// |

75 | /// Takes a scrollable set of fixed sized [RenderBox]es and renders them |

76 | /// sequentially from top down on a vertical scrolling axis. |

77 | /// |

78 | /// It starts with the first scrollable item in the center of the main axis |

79 | /// and ends with the last scrollable item in the center of the main axis. This |

80 | /// is in contrast to typical lists that start with the first scrollable item |

81 | /// at the start of the main axis and ends with the last scrollable item at the |

82 | /// end of the main axis. |

83 | /// |

84 | /// Instead of rendering its children on a flat plane, it renders them |

85 | /// as if each child is broken into its own plane and that plane is |

86 | /// perpendicularly fixed onto a cylinder which rotates along the scrolling |

87 | /// axis. |

88 | /// |

89 | /// This class works in 3 coordinate systems: |

90 | /// |

91 | /// 1. The **scrollable layout coordinates**. This coordinate system is used to |

92 | /// communicate with [ViewportOffset] and describes its children's abstract |

93 | /// offset from the beginning of the scrollable list at (0.0, 0.0). |

94 | /// |

95 | /// The list is scrollable from the start of the first child item to the |

96 | /// start of the last child item. |

97 | /// |

98 | /// Children's layout coordinates don't change as the viewport scrolls. |

99 | /// |

100 | /// 2. The **untransformed plane's viewport painting coordinates**. Children are |

101 | /// not painted in this coordinate system. It's an abstract intermediary used |

102 | /// before transforming into the next cylindrical coordinate system. |

103 | /// |

104 | /// This system is the **scrollable layout coordinates** translated by the |

105 | /// scroll offset such that (0.0, 0.0) is the top left corner of the |

106 | /// viewport. |

107 | /// |

108 | /// Because the viewport is centered at the scrollable list's scroll offset |

109 | /// instead of starting at the scroll offset, there are paintable children |

110 | /// ~1/2 viewport length before and after the scroll offset instead of ~1 |

111 | /// viewport length after the scroll offset. |

112 | /// |

113 | /// Children's visibility inclusion in the viewport is determined in this |

114 | /// system regardless of the cylinder's properties such as [diameterRatio] |

115 | /// or [perspective]. In other words, a 100px long viewport will always |

116 | /// paint 10-11 visible 10px children if there are enough children in the |

117 | /// viewport. |

118 | /// |

119 | /// 3. The **transformed cylindrical space viewport painting coordinates**. |

120 | /// Children from system 2 get their positions transformed into a cylindrical |

121 | /// projection matrix instead of its Cartesian offset with respect to the |

122 | /// scroll offset. |

123 | /// |

124 | /// Children in this coordinate system are painted. |

125 | /// |

126 | /// The wheel's size and the maximum and minimum visible angles are both |

127 | /// controlled by [diameterRatio]. Children visible in the **untransformed |

128 | /// plane's viewport painting coordinates**'s viewport will be radially |

129 | /// evenly laid out between the maximum and minimum angles determined by |

130 | /// intersecting the viewport's main axis length with a cylinder whose |

131 | /// diameter is [diameterRatio] times longer, as long as those angles are |

132 | /// between -pi/2 and pi/2. |

133 | /// |

134 | /// For example, if [diameterRatio] is 2.0 and this [RenderListWheelViewport] |

135 | /// is 100.0px in the main axis, then the diameter is 200.0. And children |

136 | /// will be evenly laid out between that cylinder's -arcsin(1/2) and |

137 | /// arcsin(1/2) angles. |

138 | /// |

139 | /// The cylinder's 0 degree side is always centered in the |

140 | /// [RenderListWheelViewport]. The transformation from **untransformed |

141 | /// plane's viewport painting coordinates** is also done such that the child |

142 | /// in the center of that plane will be mostly untransformed with children |

143 | /// above and below it being transformed more as the angle increases. |

144 | class RenderListWheelViewport |

145 | extends RenderBox |

146 | with ContainerRenderObjectMixin<RenderBox, ListWheelParentData> |

147 | implements RenderAbstractViewport { |

148 | /// Creates a [RenderListWheelViewport] which renders children on a wheel. |

149 | /// |

150 | /// Optional arguments have reasonable defaults. |

151 | RenderListWheelViewport({ |

152 | required this.childManager, |

153 | required ViewportOffset offset, |

154 | double diameterRatio = defaultDiameterRatio, |

155 | double perspective = defaultPerspective, |

156 | double offAxisFraction = 0, |

157 | bool useMagnifier = false, |

158 | double magnification = 1, |

159 | double overAndUnderCenterOpacity = 1, |

160 | required double itemExtent, |

161 | double squeeze = 1, |

162 | bool renderChildrenOutsideViewport = false, |

163 | Clip clipBehavior = Clip.none, |

164 | List<RenderBox>? children, |

165 | }) : assert(diameterRatio > 0, diameterRatioZeroMessage), |

166 | assert(perspective > 0), |

167 | assert(perspective <= 0.01, perspectiveTooHighMessage), |

168 | assert(magnification > 0), |

169 | assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1), |

170 | assert(squeeze > 0), |

171 | assert(itemExtent > 0), |

172 | assert( |

173 | !renderChildrenOutsideViewport || clipBehavior == Clip.none, |

174 | clipBehaviorAndRenderChildrenOutsideViewportConflict, |

175 | ), |

176 | _offset = offset, |

177 | _diameterRatio = diameterRatio, |

178 | _perspective = perspective, |

179 | _offAxisFraction = offAxisFraction, |

180 | _useMagnifier = useMagnifier, |

181 | _magnification = magnification, |

182 | _overAndUnderCenterOpacity = overAndUnderCenterOpacity, |

183 | _itemExtent = itemExtent, |

184 | _squeeze = squeeze, |

185 | _renderChildrenOutsideViewport = renderChildrenOutsideViewport, |

186 | _clipBehavior = clipBehavior { |

187 | addAll(children); |

188 | } |

189 | |

190 | /// An arbitrary but aesthetically reasonable default value for [diameterRatio]. |

191 | static const double defaultDiameterRatio = 2.0; |

192 | |

193 | /// An arbitrary but aesthetically reasonable default value for [perspective]. |

194 | static const double defaultPerspective = 0.003; |

195 | |

196 | /// An error message to show when the provided [diameterRatio] is zero. |

197 | static const String diameterRatioZeroMessage = "You can't set a diameterRatio " |

198 | 'of 0 or of a negative number. It would imply a cylinder of 0 in diameter ' |

199 | 'in which case nothing will be drawn.'; |

200 | |

201 | /// An error message to show when the [perspective] value is too high. |

202 | static const String perspectiveTooHighMessage = 'A perspective too high will ' |

203 | 'be clipped in the z-axis and therefore not renderable. Value must be ' |

204 | 'between 0 and 0.01.'; |

205 | |

206 | /// An error message to show when [clipBehavior] and [renderChildrenOutsideViewport] |

207 | /// are set to conflicting values. |

208 | static const String clipBehaviorAndRenderChildrenOutsideViewportConflict = |

209 | 'Cannot renderChildrenOutsideViewport and clip since children ' |

210 | 'rendered outside will be clipped anyway.'; |

211 | |

212 | /// The delegate that manages the children of this object. |

213 | /// |

214 | /// This delegate must maintain the [ListWheelParentData.index] value. |

215 | final ListWheelChildManager childManager; |

216 | |

217 | /// The associated ViewportOffset object for the viewport describing the part |

218 | /// of the content inside that's visible. |

219 | /// |

220 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |

221 | /// viewport uses to select which part of its content to display. As the user |

222 | /// scrolls the viewport, this value changes, which changes the content that |

223 | /// is displayed. |

224 | ViewportOffset get offset => _offset; |

225 | ViewportOffset _offset; |

226 | set offset(ViewportOffset value) { |

227 | if (value == _offset) { |

228 | return; |

229 | } |

230 | if (attached) { |

231 | _offset.removeListener(_hasScrolled); |

232 | } |

233 | _offset = value; |

234 | if (attached) { |

235 | _offset.addListener(_hasScrolled); |

236 | } |

237 | markNeedsLayout(); |

238 | } |

239 | |

240 | /// {@template flutter.rendering.RenderListWheelViewport.diameterRatio} |

241 | /// A ratio between the diameter of the cylinder and the viewport's size |

242 | /// in the main axis. |

243 | /// |

244 | /// A value of 1 means the cylinder has the same diameter as the viewport's |

245 | /// size. |

246 | /// |

247 | /// A value smaller than 1 means items at the edges of the cylinder are |

248 | /// entirely contained inside the viewport. |

249 | /// |

250 | /// A value larger than 1 means angles less than ±[pi] / 2 from the |

251 | /// center of the cylinder are visible. |

252 | /// |

253 | /// The same number of children will be visible in the viewport regardless of |

254 | /// the [diameterRatio]. The number of children visible is based on the |

255 | /// viewport's length along the main axis divided by the children's |

256 | /// [itemExtent]. Then the children are evenly distributed along the visible |

257 | /// angles up to ±[pi] / 2. |

258 | /// |

259 | /// Just as it's impossible to stretch a paper to cover the an entire |

260 | /// half of a cylinder's surface where the cylinder has the same diameter |

261 | /// as the paper's length, choosing a [diameterRatio] smaller than [pi] |

262 | /// will leave same gaps between the children. |

263 | /// |

264 | /// Defaults to an arbitrary but aesthetically reasonable number of 2.0. |

265 | /// |

266 | /// Must be a positive number. |

267 | /// {@endtemplate} |

268 | double get diameterRatio => _diameterRatio; |

269 | double _diameterRatio; |

270 | set diameterRatio(double value) { |

271 | assert( |

272 | value > 0, |

273 | diameterRatioZeroMessage, |

274 | ); |

275 | if (value == _diameterRatio) { |

276 | return; |

277 | } |

278 | _diameterRatio = value; |

279 | markNeedsPaint(); |

280 | markNeedsSemanticsUpdate(); |

281 | } |

282 | |

283 | /// {@template flutter.rendering.RenderListWheelViewport.perspective} |

284 | /// Perspective of the cylindrical projection. |

285 | /// |

286 | /// A number between 0 and 0.01 where 0 means looking at the cylinder from |

287 | /// infinitely far with an infinitely small field of view and 1 means looking |

288 | /// at the cylinder from infinitely close with an infinitely large field of |

289 | /// view (which cannot be rendered). |

290 | /// |

291 | /// Defaults to an arbitrary but aesthetically reasonable number of 0.003. |

292 | /// A larger number brings the vanishing point closer and a smaller number |

293 | /// pushes the vanishing point further. |

294 | /// |

295 | /// Must be a positive number. |

296 | /// {@endtemplate} |

297 | double get perspective => _perspective; |

298 | double _perspective; |

299 | set perspective(double value) { |

300 | assert(value > 0); |

301 | assert( |

302 | value <= 0.01, |

303 | perspectiveTooHighMessage, |

304 | ); |

305 | if (value == _perspective) { |

306 | return; |

307 | } |

308 | _perspective = value; |

309 | markNeedsPaint(); |

310 | markNeedsSemanticsUpdate(); |

311 | } |

312 | |

313 | /// {@template flutter.rendering.RenderListWheelViewport.offAxisFraction} |

314 | /// How much the wheel is horizontally off-center, as a fraction of its width. |

315 | |

316 | /// This property creates the visual effect of looking at a vertical wheel from |

317 | /// its side where its vanishing points at the edge curves to one side instead |

318 | /// of looking at the wheel head-on. |

319 | /// |

320 | /// The value is horizontal distance between the wheel's center and the vertical |

321 | /// vanishing line at the edges of the wheel, represented as a fraction of the |

322 | /// wheel's width. |

323 | /// |

324 | /// The value `0.0` means the wheel is looked at head-on and its vanishing |

325 | /// line runs through the center of the wheel. Negative values means moving |

326 | /// the wheel to the left of the observer, thus the edges curve to the right. |

327 | /// Positive values means moving the wheel to the right of the observer, |

328 | /// thus the edges curve to the left. |

329 | /// |

330 | /// The visual effect causes the wheel's edges to curve rather than moving |

331 | /// the center. So a value of `0.5` means the edges' vanishing line will touch |

332 | /// the wheel's size's left edge. |

333 | /// |

334 | /// Defaults to `0.0`, which means looking at the wheel head-on. |

335 | /// The visual effect can be unaesthetic if this value is too far from the |

336 | /// range `[-0.5, 0.5]`. |

337 | /// {@endtemplate} |

338 | double get offAxisFraction => _offAxisFraction; |

339 | double _offAxisFraction = 0.0; |

340 | set offAxisFraction(double value) { |

341 | if (value == _offAxisFraction) { |

342 | return; |

343 | } |

344 | _offAxisFraction = value; |

345 | markNeedsPaint(); |

346 | } |

347 | |

348 | /// {@template flutter.rendering.RenderListWheelViewport.useMagnifier} |

349 | /// Whether to use the magnifier for the center item of the wheel. |

350 | /// {@endtemplate} |

351 | bool get useMagnifier => _useMagnifier; |

352 | bool _useMagnifier = false; |

353 | set useMagnifier(bool value) { |

354 | if (value == _useMagnifier) { |

355 | return; |

356 | } |

357 | _useMagnifier = value; |

358 | markNeedsPaint(); |

359 | } |

360 | /// {@template flutter.rendering.RenderListWheelViewport.magnification} |

361 | /// The zoomed-in rate of the magnifier, if it is used. |

362 | /// |

363 | /// The default value is 1.0, which will not change anything. |

364 | /// If the value is > 1.0, the center item will be zoomed in by that rate, and |

365 | /// it will also be rendered as flat, not cylindrical like the rest of the list. |

366 | /// The item will be zoomed out if magnification < 1.0. |

367 | /// |

368 | /// Must be positive. |

369 | /// {@endtemplate} |

370 | double get magnification => _magnification; |

371 | double _magnification = 1.0; |

372 | set magnification(double value) { |

373 | assert(value > 0); |

374 | if (value == _magnification) { |

375 | return; |

376 | } |

377 | _magnification = value; |

378 | markNeedsPaint(); |

379 | } |

380 | |

381 | /// {@template flutter.rendering.RenderListWheelViewport.overAndUnderCenterOpacity} |

382 | /// The opacity value that will be applied to the wheel that appears below and |

383 | /// above the magnifier. |

384 | /// |

385 | /// The default value is 1.0, which will not change anything. |

386 | /// |

387 | /// Must be greater than or equal to 0, and less than or equal to 1. |

388 | /// {@endtemplate} |

389 | double get overAndUnderCenterOpacity => _overAndUnderCenterOpacity; |

390 | double _overAndUnderCenterOpacity = 1.0; |

391 | set overAndUnderCenterOpacity(double value) { |

392 | assert(value >= 0 && value <= 1); |

393 | if (value == _overAndUnderCenterOpacity) { |

394 | return; |

395 | } |

396 | _overAndUnderCenterOpacity = value; |

397 | markNeedsPaint(); |

398 | } |

399 | |

400 | /// {@template flutter.rendering.RenderListWheelViewport.itemExtent} |

401 | /// The size of the children along the main axis. Children [RenderBox]es will |

402 | /// be given the [BoxConstraints] of this exact size. |

403 | /// |

404 | /// Must be a positive number. |

405 | /// {@endtemplate} |

406 | double get itemExtent => _itemExtent; |

407 | double _itemExtent; |

408 | set itemExtent(double value) { |

409 | assert(value > 0); |

410 | if (value == _itemExtent) { |

411 | return; |

412 | } |

413 | _itemExtent = value; |

414 | markNeedsLayout(); |

415 | } |

416 | |

417 | |

418 | /// {@template flutter.rendering.RenderListWheelViewport.squeeze} |

419 | /// The angular compactness of the children on the wheel. |

420 | /// |

421 | /// This denotes a ratio of the number of children on the wheel vs the number |

422 | /// of children that would fit on a flat list of equivalent size, assuming |

423 | /// [diameterRatio] of 1. |

424 | /// |

425 | /// For instance, if this RenderListWheelViewport has a height of 100px and |

426 | /// [itemExtent] is 20px, 5 items would fit on an equivalent flat list. |

427 | /// With a [squeeze] of 1, 5 items would also be shown in the |

428 | /// RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown |

429 | /// in the RenderListWheelViewport. |

430 | /// |

431 | /// Changing this value will change the number of children built and shown |

432 | /// inside the wheel. |

433 | /// |

434 | /// Must be a positive number. |

435 | /// {@endtemplate} |

436 | /// |

437 | /// Defaults to 1. |

438 | double get squeeze => _squeeze; |

439 | double _squeeze; |

440 | set squeeze(double value) { |

441 | assert(value > 0); |

442 | if (value == _squeeze) { |

443 | return; |

444 | } |

445 | _squeeze = value; |

446 | markNeedsLayout(); |

447 | markNeedsSemanticsUpdate(); |

448 | } |

449 | |

450 | /// {@template flutter.rendering.RenderListWheelViewport.renderChildrenOutsideViewport} |

451 | /// Whether to paint children inside the viewport only. |

452 | /// |

453 | /// If false, every child will be painted. However the [Scrollable] is still |

454 | /// the size of the viewport and detects gestures inside only. |

455 | /// |

456 | /// Defaults to false. Cannot be true if [clipBehavior] is not [Clip.none] |

457 | /// since children outside the viewport will be clipped, and therefore cannot |

458 | /// render children outside the viewport. |

459 | /// {@endtemplate} |

460 | bool get renderChildrenOutsideViewport => _renderChildrenOutsideViewport; |

461 | bool _renderChildrenOutsideViewport; |

462 | set renderChildrenOutsideViewport(bool value) { |

463 | assert( |

464 | !renderChildrenOutsideViewport || clipBehavior == Clip.none, |

465 | clipBehaviorAndRenderChildrenOutsideViewportConflict, |

466 | ); |

467 | if (value == _renderChildrenOutsideViewport) { |

468 | return; |

469 | } |

470 | _renderChildrenOutsideViewport = value; |

471 | markNeedsLayout(); |

472 | markNeedsSemanticsUpdate(); |

473 | } |

474 | |

475 | /// {@macro flutter.material.Material.clipBehavior} |

476 | /// |

477 | /// Defaults to [Clip.hardEdge]. |

478 | Clip get clipBehavior => _clipBehavior; |

479 | Clip _clipBehavior = Clip.hardEdge; |

480 | set clipBehavior(Clip value) { |

481 | if (value != _clipBehavior) { |

482 | _clipBehavior = value; |

483 | markNeedsPaint(); |

484 | markNeedsSemanticsUpdate(); |

485 | } |

486 | } |

487 | |

488 | void _hasScrolled() { |

489 | markNeedsLayout(); |

490 | markNeedsSemanticsUpdate(); |

491 | } |

492 | |

493 | @override |

494 | void setupParentData(RenderObject child) { |

495 | if (child.parentData is! ListWheelParentData) { |

496 | child.parentData = ListWheelParentData(); |

497 | } |

498 | } |

499 | |

500 | @override |

501 | void attach(PipelineOwner owner) { |

502 | super.attach(owner); |

503 | _offset.addListener(_hasScrolled); |

504 | } |

505 | |

506 | @override |

507 | void detach() { |

508 | _offset.removeListener(_hasScrolled); |

509 | super.detach(); |

510 | } |

511 | |

512 | @override |

513 | bool get isRepaintBoundary => true; |

514 | |

515 | /// Main axis length in the untransformed plane. |

516 | double get _viewportExtent { |

517 | assert(hasSize); |

518 | return size.height; |

519 | } |

520 | |

521 | /// Main axis scroll extent in the **scrollable layout coordinates** that puts |

522 | /// the first item in the center. |

523 | double get _minEstimatedScrollExtent { |

524 | assert(hasSize); |

525 | if (childManager.childCount == null) { |

526 | return double.negativeInfinity; |

527 | } |

528 | return 0.0; |

529 | } |

530 | |

531 | /// Main axis scroll extent in the **scrollable layout coordinates** that puts |

532 | /// the last item in the center. |

533 | double get _maxEstimatedScrollExtent { |

534 | assert(hasSize); |

535 | if (childManager.childCount == null) { |

536 | return double.infinity; |

537 | } |

538 | |

539 | return math.max(0.0, (childManager.childCount! - 1) * _itemExtent); |

540 | } |

541 | |

542 | /// Scroll extent distance in the untransformed plane between the center |

543 | /// position in the viewport and the top position in the viewport. |

544 | /// |

545 | /// It's also the distance in the untransformed plane that children's painting |

546 | /// is offset by with respect to those children's [BoxParentData.offset]. |

547 | double get _topScrollMarginExtent { |

548 | assert(hasSize); |

549 | // Consider adding alignment options other than center. |

550 | return -size.height / 2.0 + _itemExtent / 2.0; |

551 | } |

552 | |

553 | /// Transforms a **scrollable layout coordinates**' y position to the |

554 | /// **untransformed plane's viewport painting coordinates**' y position given |

555 | /// the current scroll offset. |

556 | double _getUntransformedPaintingCoordinateY(double layoutCoordinateY) { |

557 | return layoutCoordinateY - _topScrollMarginExtent - offset.pixels; |

558 | } |

559 | |

560 | /// Given the _diameterRatio, return the largest absolute angle of the item |

561 | /// at the edge of the portion of the visible cylinder. |

562 | /// |

563 | /// For a _diameterRatio of 1 or less than 1 (i.e. the viewport is bigger |

564 | /// than the cylinder diameter), this value reaches and clips at pi / 2. |

565 | /// |

566 | /// When the center of children passes this angle, they are no longer painted |

567 | /// if [renderChildrenOutsideViewport] is false. |

568 | double get _maxVisibleRadian { |

569 | if (_diameterRatio < 1.0) { |

570 | return math.pi / 2.0; |

571 | } |

572 | return math.asin(1.0 / _diameterRatio); |

573 | } |

574 | |

575 | double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) { |

576 | double extent = 0.0; |

577 | RenderBox? child = firstChild; |

578 | while (child != null) { |

579 | extent = math.max(extent, childSize(child)); |

580 | child = childAfter(child); |

581 | } |

582 | return extent; |

583 | } |

584 | |

585 | @override |

586 | double computeMinIntrinsicWidth(double height) { |

587 | return _getIntrinsicCrossAxis( |

588 | (RenderBox child) => child.getMinIntrinsicWidth(height), |

589 | ); |

590 | } |

591 | |

592 | @override |

593 | double computeMaxIntrinsicWidth(double height) { |

594 | return _getIntrinsicCrossAxis( |

595 | (RenderBox child) => child.getMaxIntrinsicWidth(height), |

596 | ); |

597 | } |

598 | |

599 | @override |

600 | double computeMinIntrinsicHeight(double width) { |

601 | if (childManager.childCount == null) { |

602 | return 0.0; |

603 | } |

604 | return childManager.childCount! * _itemExtent; |

605 | } |

606 | |

607 | @override |

608 | double computeMaxIntrinsicHeight(double width) { |

609 | if (childManager.childCount == null) { |

610 | return 0.0; |

611 | } |

612 | return childManager.childCount! * _itemExtent; |

613 | } |

614 | |

615 | @override |

616 | bool get sizedByParent => true; |

617 | |

618 | @override |

619 | @protected |

620 | Size computeDryLayout(covariant BoxConstraints constraints) { |

621 | return constraints.biggest; |

622 | } |

623 | |

624 | /// Gets the index of a child by looking at its [parentData]. |

625 | /// |

626 | /// This relies on the [childManager] maintaining [ListWheelParentData.index]. |

627 | int indexOf(RenderBox child) { |

628 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |

629 | assert(childParentData.index != null); |

630 | return childParentData.index!; |

631 | } |

632 | |

633 | /// Returns the index of the child at the given offset. |

634 | int scrollOffsetToIndex(double scrollOffset) => (scrollOffset / itemExtent).floor(); |

635 | |

636 | /// Returns the scroll offset of the child with the given index. |

637 | double indexToScrollOffset(int index) => index * itemExtent; |

638 | |

639 | void _createChild(int index, { RenderBox? after }) { |

640 | invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) { |

641 | assert(constraints == this.constraints); |

642 | childManager.createChild(index, after: after); |

643 | }); |

644 | } |

645 | |

646 | void _destroyChild(RenderBox child) { |

647 | invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) { |

648 | assert(constraints == this.constraints); |

649 | childManager.removeChild(child); |

650 | }); |

651 | } |

652 | |

653 | void _layoutChild(RenderBox child, BoxConstraints constraints, int index) { |

654 | child.layout(constraints, parentUsesSize: true); |

655 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |

656 | // Centers the child horizontally. |

657 | final double crossPosition = size.width / 2.0 - child.size.width / 2.0; |

658 | childParentData.offset = Offset(crossPosition, indexToScrollOffset(index)); |

659 | } |

660 | |

661 | /// Performs layout based on how [childManager] provides children. |

662 | /// |

663 | /// From the current scroll offset, the minimum index and maximum index that |

664 | /// is visible in the viewport can be calculated. The index range of the |

665 | /// currently active children can also be acquired by looking directly at |

666 | /// the current child list. This function has to modify the current index |

667 | /// range to match the target index range by removing children that are no |

668 | /// longer visible and creating those that are visible but not yet provided |

669 | /// by [childManager]. |

670 | @override |

671 | void performLayout() { |

672 | offset.applyViewportDimension(_viewportExtent); |

673 | // Apply the content dimensions first if it has exact dimensions in case it |

674 | // changes the scroll offset which determines what should be shown. Such as |

675 | // if the child count decrease, we should correct the pixels first, otherwise, |

676 | // it may be shown blank null children. |

677 | if (childManager.childCount != null) { |

678 | offset.applyContentDimensions(_minEstimatedScrollExtent, _maxEstimatedScrollExtent); |

679 | } |

680 | |

681 | // The height, in pixel, that children will be visible and might be laid out |

682 | // and painted. |

683 | double visibleHeight = size.height * _squeeze; |

684 | // If renderChildrenOutsideViewport is true, we spawn extra children by |

685 | // doubling the visibility range, those that are in the backside of the |

686 | // cylinder won't be painted anyway. |

687 | if (renderChildrenOutsideViewport) { |

688 | visibleHeight *= 2; |

689 | } |

690 | |

691 | final double firstVisibleOffset = |

692 | offset.pixels + _itemExtent / 2 - visibleHeight / 2; |

693 | final double lastVisibleOffset = firstVisibleOffset + visibleHeight; |

694 | |

695 | // The index range that we want to spawn children. We find indexes that |

696 | // are in the interval [firstVisibleOffset, lastVisibleOffset). |

697 | int targetFirstIndex = scrollOffsetToIndex(firstVisibleOffset); |

698 | int targetLastIndex = scrollOffsetToIndex(lastVisibleOffset); |

699 | // Because we exclude lastVisibleOffset, if there's a new child starting at |

700 | // that offset, it is removed. |

701 | if (targetLastIndex * _itemExtent == lastVisibleOffset) { |

702 | targetLastIndex--; |

703 | } |

704 | |

705 | // Validates the target index range. |

706 | while (!childManager.childExistsAt(targetFirstIndex) && targetFirstIndex <= targetLastIndex) { |

707 | targetFirstIndex++; |

708 | } |

709 | while (!childManager.childExistsAt(targetLastIndex) && targetFirstIndex <= targetLastIndex) { |

710 | targetLastIndex--; |

711 | } |

712 | |

713 | // If it turns out there's no children to layout, we remove old children and |

714 | // return. |

715 | if (targetFirstIndex > targetLastIndex) { |

716 | while (firstChild != null) { |

717 | _destroyChild(firstChild!); |

718 | } |

719 | return; |

720 | } |

721 | |

722 | // Now there are 2 cases: |

723 | // - The target index range and our current index range have intersection: |

724 | // We shorten and extend our current child list so that the two lists |

725 | // match. Most of the time we are in this case. |

726 | // - The target list and our current child list have no intersection: |

727 | // We first remove all children and then add one child from the target |

728 | // list => this case becomes the other case. |

729 | |

730 | // Case when there is no intersection. |

731 | if (childCount > 0 && |

732 | (indexOf(firstChild!) > targetLastIndex || indexOf(lastChild!) < targetFirstIndex)) { |

733 | while (firstChild != null) { |

734 | _destroyChild(firstChild!); |

735 | } |

736 | } |

737 | |

738 | final BoxConstraints childConstraints = constraints.copyWith( |

739 | minHeight: _itemExtent, |

740 | maxHeight: _itemExtent, |

741 | minWidth: 0.0, |

742 | ); |

743 | // If there is no child at this stage, we add the first one that is in |

744 | // target range. |

745 | if (childCount == 0) { |

746 | _createChild(targetFirstIndex); |

747 | _layoutChild(firstChild!, childConstraints, targetFirstIndex); |

748 | } |

749 | |

750 | int currentFirstIndex = indexOf(firstChild!); |

751 | int currentLastIndex = indexOf(lastChild!); |

752 | |

753 | // Remove all unnecessary children by shortening the current child list, in |

754 | // both directions. |

755 | while (currentFirstIndex < targetFirstIndex) { |

756 | _destroyChild(firstChild!); |

757 | currentFirstIndex++; |

758 | } |

759 | while (currentLastIndex > targetLastIndex) { |

760 | _destroyChild(lastChild!); |

761 | currentLastIndex--; |

762 | } |

763 | |

764 | // Relayout all active children. |

765 | RenderBox? child = firstChild; |

766 | int index = currentFirstIndex; |

767 | while (child != null) { |

768 | _layoutChild(child, childConstraints, index++); |

769 | child = childAfter(child); |

770 | } |

771 | |

772 | // Spawning new children that are actually visible but not in child list yet. |

773 | while (currentFirstIndex > targetFirstIndex) { |

774 | _createChild(currentFirstIndex - 1); |

775 | _layoutChild(firstChild!, childConstraints, --currentFirstIndex); |

776 | } |

777 | while (currentLastIndex < targetLastIndex) { |

778 | _createChild(currentLastIndex + 1, after: lastChild); |

779 | _layoutChild(lastChild!, childConstraints, ++currentLastIndex); |

780 | } |

781 | |

782 | // Applying content dimensions bases on how the childManager builds widgets: |

783 | // if it is available to provide a child just out of target range, then |

784 | // we don't know whether there's a limit yet, and set the dimension to the |

785 | // estimated value. Otherwise, we set the dimension limited to our target |

786 | // range. |

787 | final double minScrollExtent = childManager.childExistsAt(targetFirstIndex - 1) |

788 | ? _minEstimatedScrollExtent |

789 | : indexToScrollOffset(targetFirstIndex); |

790 | final double maxScrollExtent = childManager.childExistsAt(targetLastIndex + 1) |

791 | ? _maxEstimatedScrollExtent |

792 | : indexToScrollOffset(targetLastIndex); |

793 | offset.applyContentDimensions(minScrollExtent, maxScrollExtent); |

794 | } |

795 | |

796 | bool _shouldClipAtCurrentOffset() { |

797 | final double highestUntransformedPaintY = |

798 | _getUntransformedPaintingCoordinateY(0.0); |

799 | return highestUntransformedPaintY < 0.0 |

800 | || size.height < highestUntransformedPaintY + _maxEstimatedScrollExtent + _itemExtent; |

801 | } |

802 | |

803 | @override |

804 | void paint(PaintingContext context, Offset offset) { |

805 | if (childCount > 0) { |

806 | if (_shouldClipAtCurrentOffset() && clipBehavior != Clip.none) { |

807 | _clipRectLayer.layer = context.pushClipRect( |

808 | needsCompositing, |

809 | offset, |

810 | Offset.zero & size, |

811 | _paintVisibleChildren, |

812 | clipBehavior: clipBehavior, |

813 | oldLayer: _clipRectLayer.layer, |

814 | ); |

815 | } else { |

816 | _clipRectLayer.layer = null; |

817 | _paintVisibleChildren(context, offset); |

818 | } |

819 | } |

820 | } |

821 | |

822 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |

823 | |

824 | @override |

825 | void dispose() { |

826 | _clipRectLayer.layer = null; |

827 | _childOpacityLayerHandler.layer = null; |

828 | super.dispose(); |

829 | } |

830 | final LayerHandle<OpacityLayer> _childOpacityLayerHandler = LayerHandle<OpacityLayer>(); |

831 | /// Paints all children visible in the current viewport. |

832 | void _paintVisibleChildren(PaintingContext context, Offset offset) { |

833 | // The magnifier cannot be turned off if the opacity is less than 1.0. |

834 | if (overAndUnderCenterOpacity >= 1) { |

835 | _paintAllChildren(context, offset); |

836 | return; |

837 | } |

838 | |

839 | // In order to reduce the number of opacity layers, we first paint all |

840 | // partially opaque children, then finally paint the fully opaque children. |

841 | _childOpacityLayerHandler.layer = context.pushOpacity(offset, (overAndUnderCenterOpacity * 255).round(), (PaintingContext context, Offset offset) { |

842 | _paintAllChildren(context, offset, center: false); |

843 | }); |

844 | _paintAllChildren(context, offset, center: true); |

845 | } |

846 | |

847 | void _paintAllChildren(PaintingContext context, Offset offset, { bool? center }) { |

848 | RenderBox? childToPaint = firstChild; |

849 | while (childToPaint != null) { |

850 | final ListWheelParentData childParentData = childToPaint.parentData! as ListWheelParentData; |

851 | _paintTransformedChild(childToPaint, context, offset, childParentData.offset, center: center); |

852 | childToPaint = childAfter(childToPaint); |

853 | } |

854 | } |

855 | |

856 | // Takes in a child with a **scrollable layout offset** and paints it in the |

857 | // **transformed cylindrical space viewport painting coordinates**. |

858 | // |

859 | // The value of `center` is passed through to _paintChildWithMagnifier only |

860 | // if the magnifier is enabled and/or opacity is < 1.0. |

861 | void _paintTransformedChild( |

862 | RenderBox child, |

863 | PaintingContext context, |

864 | Offset offset, |

865 | Offset layoutOffset, { |

866 | required bool? center, |

867 | }) { |

868 | final Offset untransformedPaintingCoordinates = offset |

869 | + Offset( |

870 | layoutOffset.dx, |

871 | _getUntransformedPaintingCoordinateY(layoutOffset.dy), |

872 | ); |

873 | |

874 | // Get child's center as a fraction of the viewport's height. |

875 | final double fractionalY = |

876 | (untransformedPaintingCoordinates.dy + _itemExtent / 2.0) / size.height; |

877 | final double angle = -(fractionalY - 0.5) * 2.0 * _maxVisibleRadian / squeeze; |

878 | // Don't paint the backside of the cylinder when |

879 | // renderChildrenOutsideViewport is true. Otherwise, only children within |

880 | // suitable angles (via _first/lastVisibleLayoutOffset) reach the paint |

881 | // phase. |

882 | if (angle > math.pi / 2.0 || angle < -math.pi / 2.0) { |

883 | return; |

884 | } |

885 | |

886 | final Matrix4 transform = MatrixUtils.createCylindricalProjectionTransform( |

887 | radius: size.height * _diameterRatio / 2.0, |

888 | angle: angle, |

889 | perspective: _perspective, |

890 | ); |

891 | |

892 | // Offset that helps painting everything in the center (e.g. angle = 0). |

893 | final Offset offsetToCenter = Offset( |

894 | untransformedPaintingCoordinates.dx, |

895 | -_topScrollMarginExtent, |

896 | ); |

897 | |

898 | final bool shouldApplyOffCenterDim = overAndUnderCenterOpacity < 1; |

899 | if (useMagnifier || shouldApplyOffCenterDim) { |

900 | _paintChildWithMagnifier(context, offset, child, transform, offsetToCenter, untransformedPaintingCoordinates, center: center); |

901 | } else { |

902 | assert(center == null); |

903 | _paintChildCylindrically(context, offset, child, transform, offsetToCenter); |

904 | } |

905 | } |

906 | |

907 | // Paint child with the magnifier active - the child will be rendered |

908 | // differently if it intersects with the magnifier. |

909 | // |

910 | // `center` controls how items that partially intersect the center magnifier |

911 | // are rendered. If `center` is false, items are only painted cynlindrically. |

912 | // If `center` is true, only the clipped magnifier items are painted. |

913 | // If `center` is null, partially intersecting items are painted both as the |

914 | // magnifier and cynlidrical item, while non-intersecting items are painted |

915 | // only cylindrically. |

916 | // |

917 | // This property is used to lift the opacity that would be applied to each |

918 | // cylindrical item into a single layer, reducing the rendering cost of the |

919 | // pickers which use this viewport. |

920 | void _paintChildWithMagnifier( |

921 | PaintingContext context, |

922 | Offset offset, |

923 | RenderBox child, |

924 | Matrix4 cylindricalTransform, |

925 | Offset offsetToCenter, |

926 | Offset untransformedPaintingCoordinates, { |

927 | required bool? center, |

928 | }) { |

929 | final double magnifierTopLinePosition = |

930 | size.height / 2 - _itemExtent * _magnification / 2; |

931 | final double magnifierBottomLinePosition = |

932 | size.height / 2 + _itemExtent * _magnification / 2; |

933 | |

934 | final bool isAfterMagnifierTopLine = untransformedPaintingCoordinates.dy |

935 | >= magnifierTopLinePosition - _itemExtent * _magnification; |

936 | final bool isBeforeMagnifierBottomLine = untransformedPaintingCoordinates.dy |

937 | <= magnifierBottomLinePosition; |

938 | |

939 | final Rect centerRect = Rect.fromLTWH( |

940 | 0.0, |

941 | magnifierTopLinePosition, |

942 | size.width, |

943 | _itemExtent * _magnification, |

944 | ); |

945 | final Rect topHalfRect = Rect.fromLTWH( |

946 | 0.0, |

947 | 0.0, |

948 | size.width, |

949 | magnifierTopLinePosition, |

950 | ); |

951 | final Rect bottomHalfRect = Rect.fromLTWH( |

952 | 0.0, |

953 | magnifierBottomLinePosition, |

954 | size.width, |

955 | magnifierTopLinePosition, |

956 | ); |

957 | // Some part of the child is in the center magnifier. |

958 | final bool inCenter = isAfterMagnifierTopLine && isBeforeMagnifierBottomLine; |

959 | |

960 | if ((center == null || center) && inCenter) { |

961 | // Clipping the part in the center. |

962 | context.pushClipRect( |

963 | needsCompositing, |

964 | offset, |

965 | centerRect, |

966 | (PaintingContext context, Offset offset) { |

967 | context.pushTransform( |

968 | needsCompositing, |

969 | offset, |

970 | _magnifyTransform(), |

971 | (PaintingContext context, Offset offset) { |

972 | context.paintChild(child, offset + untransformedPaintingCoordinates); |

973 | }, |

974 | ); |

975 | }, |

976 | ); |

977 | } |

978 | |

979 | // Clipping the part in either the top-half or bottom-half of the wheel. |

980 | if ((center == null || !center) && inCenter) { |

981 | context.pushClipRect( |

982 | needsCompositing, |

983 | offset, |

984 | untransformedPaintingCoordinates.dy <= magnifierTopLinePosition |

985 | ? topHalfRect |

986 | : bottomHalfRect, |

987 | (PaintingContext context, Offset offset) { |

988 | _paintChildCylindrically( |

989 | context, |

990 | offset, |

991 | child, |

992 | cylindricalTransform, |

993 | offsetToCenter, |

994 | ); |

995 | }, |

996 | ); |

997 | } |

998 | |

999 | if ((center == null || !center) && !inCenter) { |

1000 | _paintChildCylindrically( |

1001 | context, |

1002 | offset, |

1003 | child, |

1004 | cylindricalTransform, |

1005 | offsetToCenter, |

1006 | ); |

1007 | } |

1008 | } |

1009 | |

1010 | // / Paint the child cylindrically at given offset. |

1011 | void _paintChildCylindrically( |

1012 | PaintingContext context, |

1013 | Offset offset, |

1014 | RenderBox child, |

1015 | Matrix4 cylindricalTransform, |

1016 | Offset offsetToCenter, |

1017 | ) { |

1018 | final Offset paintOriginOffset = offset + offsetToCenter; |

1019 | |

1020 | // Paint child cylindrically, without [overAndUnderCenterOpacity]. |

1021 | void painter(PaintingContext context, Offset offset) { |

1022 | context.paintChild( |

1023 | child, |

1024 | // Paint everything in the center (e.g. angle = 0), then transform. |

1025 | paintOriginOffset, |

1026 | ); |

1027 | } |

1028 | |

1029 | context.pushTransform( |

1030 | needsCompositing, |

1031 | offset, |

1032 | _centerOriginTransform(cylindricalTransform), |

1033 | // Pre-transform painting function. |

1034 | painter, |

1035 | ); |

1036 | |

1037 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |

1038 | // Save the final transform that accounts both for the offset and cylindrical transform. |

1039 | final Matrix4 transform = _centerOriginTransform(cylindricalTransform) |

1040 | ..translate(paintOriginOffset.dx, paintOriginOffset.dy); |

1041 | childParentData.transform = transform; |

1042 | } |

1043 | |

1044 | /// Return the Matrix4 transformation that would zoom in content in the |

1045 | /// magnified area. |

1046 | Matrix4 _magnifyTransform() { |

1047 | final Matrix4 magnify = Matrix4.identity(); |

1048 | magnify.translate(size.width * (-_offAxisFraction + 0.5), size.height / 2); |

1049 | magnify.scale(_magnification, _magnification, _magnification); |

1050 | magnify.translate(-size.width * (-_offAxisFraction + 0.5), -size.height / 2); |

1051 | return magnify; |

1052 | } |

1053 | |

1054 | /// Apply incoming transformation with the transformation's origin at the |

1055 | /// viewport's center or horizontally off to the side based on offAxisFraction. |

1056 | Matrix4 _centerOriginTransform(Matrix4 originalMatrix) { |

1057 | final Matrix4 result = Matrix4.identity(); |

1058 | final Offset centerOriginTranslation = Alignment.center.alongSize(size); |

1059 | result.translate( |

1060 | centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1), |

1061 | centerOriginTranslation.dy, |

1062 | ); |

1063 | result.multiply(originalMatrix); |

1064 | result.translate( |

1065 | -centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1), |

1066 | -centerOriginTranslation.dy, |

1067 | ); |

1068 | return result; |

1069 | } |

1070 | |

1071 | static bool _debugAssertValidHitTestOffsets(String context, Offset offset1, Offset offset2) { |

1072 | if (offset1 != offset2) { |

1073 | throw FlutterError("$context - hit test expected values didn't match:$offset1 !=$offset2 "); |

1074 | } |

1075 | return true; |

1076 | } |

1077 | |

1078 | @override |

1079 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |

1080 | final ListWheelParentData parentData = child.parentData! as ListWheelParentData; |

1081 | final Matrix4? paintTransform = parentData.transform; |

1082 | if (paintTransform != null) { |

1083 | transform.multiply(paintTransform); |

1084 | } |

1085 | } |

1086 | |

1087 | @override |

1088 | Rect? describeApproximatePaintClip(RenderObject child) { |

1089 | if (_shouldClipAtCurrentOffset()) { |

1090 | return Offset.zero & size; |

1091 | } |

1092 | return null; |

1093 | } |

1094 | |

1095 | @override |

1096 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |

1097 | RenderBox? child = lastChild; |

1098 | while (child != null) { |

1099 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |

1100 | final Matrix4? transform = childParentData.transform; |

1101 | // Skip not painted children |

1102 | if (transform != null) { |

1103 | final bool isHit = result.addWithPaintTransform( |

1104 | transform: transform, |

1105 | position: position, |

1106 | hitTest: (BoxHitTestResult result, Offset transformed) { |

1107 | assert(() { |

1108 | final Matrix4? inverted = Matrix4.tryInvert(PointerEvent.removePerspectiveTransform(transform)); |

1109 | if (inverted == null) { |

1110 | return _debugAssertValidHitTestOffsets('Null inverted transform', transformed, position); |

1111 | } |

1112 | return _debugAssertValidHitTestOffsets('MatrixUtils.transformPoint', transformed, MatrixUtils.transformPoint(inverted, position)); |

1113 | }()); |

1114 | return child!.hitTest(result, position: transformed); |

1115 | }, |

1116 | ); |

1117 | if (isHit) { |

1118 | return true; |

1119 | } |

1120 | } |

1121 | child = childParentData.previousSibling; |

1122 | } |

1123 | return false; |

1124 | } |

1125 | |

1126 | @override |

1127 | RevealedOffset getOffsetToReveal( |

1128 | RenderObject target, |

1129 | double alignment, { |

1130 | Rect? rect, |

1131 | Axis? axis, // Unused, only Axis.vertical supported by this viewport. |

1132 | }) { |

1133 | // `target` is only fully revealed when in the selected/center position. Therefore, |

1134 | // this method always returns the offset that shows `target` in the center position, |

1135 | // which is the same offset for all `alignment` values. |

1136 | rect ??= target.paintBounds; |

1137 | |

1138 | // `child` will be the last RenderObject before the viewport when walking up from `target`. |

1139 | RenderObject child = target; |

1140 | while (child.parent != this) { |

1141 | child = child.parent!; |

1142 | } |

1143 | |

1144 | final ListWheelParentData parentData = child.parentData! as ListWheelParentData; |

1145 | final double targetOffset = parentData.offset.dy; // the so-called "centerPosition" |

1146 | |

1147 | final Matrix4 transform = target.getTransformTo(child); |

1148 | final Rect bounds = MatrixUtils.transformRect(transform, rect); |

1149 | final Rect targetRect = bounds.translate(0.0, (size.height - itemExtent) / 2); |

1150 | |

1151 | return RevealedOffset(offset: targetOffset, rect: targetRect); |

1152 | } |

1153 | |

1154 | @override |

1155 | void showOnScreen({ |

1156 | RenderObject? descendant, |

1157 | Rect? rect, |

1158 | Duration duration = Duration.zero, |

1159 | Curve curve = Curves.ease, |

1160 | }) { |

1161 | if (descendant != null) { |

1162 | // Shows the descendant in the selected/center position. |

1163 | final RevealedOffset revealedOffset = getOffsetToReveal(descendant, 0.5, rect: rect); |

1164 | if (duration == Duration.zero) { |

1165 | offset.jumpTo(revealedOffset.offset); |

1166 | } else { |

1167 | offset.animateTo(revealedOffset.offset, duration: duration, curve: curve); |

1168 | } |

1169 | rect = revealedOffset.rect; |

1170 | } |

1171 | |

1172 | super.showOnScreen( |

1173 | rect: rect, |

1174 | duration: duration, |

1175 | curve: curve, |

1176 | ); |

1177 | } |

1178 | } |

1179 |