proxy_box_test.dart 11 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:typed_data';
import 'dart:ui' as ui show Image;
7 8

import 'package:flutter/animation.dart';
9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/gestures.dart';
11
import 'package:flutter/rendering.dart';
12
import 'package:flutter/src/scheduler/ticker.dart';
13
import '../flutter_test_alternative.dart';
14 15 16 17 18 19 20

import 'rendering_tester.dart';

void main() {
  test('RenderFittedBox paint', () {
    bool painted;
    RenderFittedBox makeFittedBox() {
21 22 23
      return RenderFittedBox(
        child: RenderCustomPaint(
          painter: TestCallbackPainter(onPaint: () {
24 25
            painted = true;
          }),
26 27 28 29 30 31 32 33 34 35
        ),
      );
    }

    painted = false;
    layout(makeFittedBox(), phase: EnginePhase.paint);
    expect(painted, equals(true));

    // The RenderFittedBox should not paint if it is empty.
    painted = false;
36
    layout(makeFittedBox(), constraints: BoxConstraints.tight(Size.zero), phase: EnginePhase.paint);
37 38
    expect(painted, equals(false));
  });
39 40 41 42

  test('RenderPhysicalModel compositing on Fuchsia', () {
    debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;

43
    final RenderPhysicalModel root = RenderPhysicalModel(color: const Color(0xffff00ff));
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
    layout(root, phase: EnginePhase.composite);
    expect(root.needsCompositing, isFalse);

    // On Fuchsia, the system compositor is responsible for drawing shadows
    // for physical model layers with non-zero elevation.
    root.elevation = 1.0;
    pumpFrame(phase: EnginePhase.composite);
    expect(root.needsCompositing, isTrue);

    root.elevation = 0.0;
    pumpFrame(phase: EnginePhase.composite);
    expect(root.needsCompositing, isFalse);

    debugDefaultTargetPlatformOverride = null;
  });

  test('RenderPhysicalModel compositing on non-Fuchsia', () {
    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;

63
    final RenderPhysicalModel root = RenderPhysicalModel(color: const Color(0xffff00ff));
64 65 66 67 68 69 70 71 72 73 74 75 76 77
    layout(root, phase: EnginePhase.composite);
    expect(root.needsCompositing, isFalse);

    // On non-Fuchsia platforms, Flutter draws its own shadows.
    root.elevation = 1.0;
    pumpFrame(phase: EnginePhase.composite);
    expect(root.needsCompositing, isFalse);

    root.elevation = 0.0;
    pumpFrame(phase: EnginePhase.composite);
    expect(root.needsCompositing, isFalse);

    debugDefaultTargetPlatformOverride = null;
  });
78 79

  test('RenderSemanticsGestureHandler adds/removes correct semantic actions', () {
80
    final RenderSemanticsGestureHandler renderObj = RenderSemanticsGestureHandler(
81 82 83 84
      onTap: () {},
      onHorizontalDragUpdate: (DragUpdateDetails details) {},
    );

85
    SemanticsConfiguration config = SemanticsConfiguration();
86 87 88 89
    renderObj.describeSemanticsConfiguration(config);
    expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
    expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
    expect(config.getActionHandler(SemanticsAction.scrollRight), isNotNull);
90

91
    config = SemanticsConfiguration();
92 93
    renderObj.validActions = <SemanticsAction>[SemanticsAction.tap, SemanticsAction.scrollLeft].toSet();

94 95 96 97
    renderObj.describeSemanticsConfiguration(config);
    expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
    expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
    expect(config.getActionHandler(SemanticsAction.scrollRight), isNull);
98
  });
99 100 101 102 103 104 105

  group('RenderPhysicalShape', () {
    setUp(() {
      debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
    });

    test('shape change triggers repaint', () {
106
      final RenderPhysicalShape root = RenderPhysicalShape(
107
        color: const Color(0xffff00ff),
108
        clipper: const ShapeBorderClipper(shape: CircleBorder()),
109 110 111 112 113
      );
      layout(root, phase: EnginePhase.composite);
      expect(root.debugNeedsPaint, isFalse);

      // Same shape, no repaint.
114
      root.clipper = const ShapeBorderClipper(shape: CircleBorder());
115 116 117
      expect(root.debugNeedsPaint, isFalse);

      // Different shape triggers repaint.
118
      root.clipper = const ShapeBorderClipper(shape: StadiumBorder());
119 120 121 122
      expect(root.debugNeedsPaint, isTrue);
    });

    test('compositing on non-Fuchsia', () {
123
      final RenderPhysicalShape root = RenderPhysicalShape(
124
        color: const Color(0xffff00ff),
125
        clipper: const ShapeBorderClipper(shape: CircleBorder()),
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
      );
      layout(root, phase: EnginePhase.composite);
      expect(root.needsCompositing, isFalse);

      // On non-Fuchsia platforms, Flutter draws its own shadows.
      root.elevation = 1.0;
      pumpFrame(phase: EnginePhase.composite);
      expect(root.needsCompositing, isFalse);

      root.elevation = 0.0;
      pumpFrame(phase: EnginePhase.composite);
      expect(root.needsCompositing, isFalse);

      debugDefaultTargetPlatformOverride = null;
    });
  });
142 143

  test('RenderRepaintBoundary can capture images of itself', () async {
144 145
    RenderRepaintBoundary boundary = RenderRepaintBoundary();
    layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
146 147 148 149 150 151
    pumpFrame(phase: EnginePhase.composite);
    ui.Image image = await boundary.toImage();
    expect(image.width, equals(100));
    expect(image.height, equals(200));

    // Now with pixel ratio set to something other than 1.0.
152 153
    boundary = RenderRepaintBoundary();
    layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
154 155 156 157 158 159
    pumpFrame(phase: EnginePhase.composite);
    image = await boundary.toImage(pixelRatio: 2.0);
    expect(image.width, equals(200));
    expect(image.height, equals(400));

    // Try building one with two child layers and make sure it renders them both.
160 161 162
    boundary = RenderRepaintBoundary();
    final RenderStack stack = RenderStack()..alignment = Alignment.topLeft;
    final RenderDecoratedBox blackBox = RenderDecoratedBox(
163
        decoration: const BoxDecoration(color: Color(0xff000000)),
164 165
        child: RenderConstrainedBox(
          additionalConstraints: BoxConstraints.tight(const Size.square(20.0)),
166
        ));
167
    stack.add(RenderOpacity()
168 169
      ..opacity = 0.5
      ..child = blackBox);
170
    final RenderDecoratedBox whiteBox = RenderDecoratedBox(
171
        decoration: const BoxDecoration(color: Color(0xffffffff)),
172 173
        child: RenderConstrainedBox(
          additionalConstraints: BoxConstraints.tight(const Size.square(10.0)),
174
        ));
175
    final RenderPositionedBox positioned = RenderPositionedBox(
176 177 178 179 180 181 182
      widthFactor: 2.0,
      heightFactor: 2.0,
      alignment: Alignment.topRight,
      child: whiteBox,
    );
    stack.add(positioned);
    boundary.child = stack;
183
    layout(boundary, constraints: BoxConstraints.tight(const Size(20.0, 20.0)));
184 185 186 187
    pumpFrame(phase: EnginePhase.composite);
    image = await boundary.toImage();
    expect(image.width, equals(20));
    expect(image.height, equals(20));
188 189 190 191
    ByteData data = await image.toByteData();

    int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);

192 193
    expect(data.lengthInBytes, equals(20 * 20 * 4));
    expect(data.elementSizeInBytes, equals(1));
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    expect(getPixel(0, 0), equals(0x00000080));
    expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));

    final OffsetLayer layer = boundary.layer;

    image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
    expect(image.width, equals(20));
    expect(image.height, equals(20));
    data = await image.toByteData();
    expect(getPixel(0, 0), equals(0x00000080));
    expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));

    // non-zero offsets.
    image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0));
    expect(image.width, equals(30));
    expect(image.height, equals(30));
    data = await image.toByteData();
    expect(getPixel(0, 0), equals(0x00000000));
    expect(getPixel(10, 10), equals(0x00000080));
    expect(getPixel(image.width - 1, 0), equals(0x00000000));
    expect(getPixel(image.width - 1, 10), equals(0xffffffff));

    // offset combined with a custom pixel ratio.
    image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0), pixelRatio: 2.0);
    expect(image.width, equals(60));
    expect(image.height, equals(60));
    data = await image.toByteData();
    expect(getPixel(0, 0), equals(0x00000000));
    expect(getPixel(20, 20), equals(0x00000080));
    expect(getPixel(image.width - 1, 0), equals(0x00000000));
    expect(getPixel(image.width - 1, 20), equals(0xffffffff));
225
  });
226 227

  test('RenderOpacity does not composite if it is transparent', () {
228
    final RenderOpacity renderOpacity = RenderOpacity(
229
      opacity: 0.0,
230
      child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
231 232 233 234 235 236 237
    );

    layout(renderOpacity, phase: EnginePhase.composite);
    expect(renderOpacity.needsCompositing, false);
  });

  test('RenderOpacity does not composite if it is opaque', () {
238
    final RenderOpacity renderOpacity = RenderOpacity(
239
      opacity: 1.0,
240
      child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
241 242 243 244 245 246 247
    );

    layout(renderOpacity, phase: EnginePhase.composite);
    expect(renderOpacity.needsCompositing, false);
  });

  test('RenderAnimatedOpacity does not composite if it is transparent', () async {
248 249
    final Animation<double> opacityAnimation = AnimationController(
      vsync: _FakeTickerProvider(),
250 251
    )..value = 0.0;

252
    final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
253
      alwaysIncludeSemantics: false,
254
      opacity: opacityAnimation,
255
      child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
256 257 258 259 260 261 262
    );

    layout(renderAnimatedOpacity, phase: EnginePhase.composite);
    expect(renderAnimatedOpacity.needsCompositing, false);
  });

  test('RenderAnimatedOpacity does not composite if it is opaque', () {
263 264
    final Animation<double> opacityAnimation = AnimationController(
      vsync: _FakeTickerProvider(),
265 266
    )..value = 1.0;

267
    final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
268
      alwaysIncludeSemantics: false,
269
      opacity: opacityAnimation,
270
      child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
271 272 273 274 275 276 277 278 279
    );

    layout(renderAnimatedOpacity, phase: EnginePhase.composite);
    expect(renderAnimatedOpacity.needsCompositing, false);
  });
}

class _FakeTickerProvider implements TickerProvider {
  @override
280
  Ticker createTicker(TickerCallback onTick, [bool disableAnimations = false]) {
281
    return _FakeTicker();
282
  }
283
}
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322

class _FakeTicker implements Ticker {
  @override
  bool muted;

  @override
  void absorbTicker(Ticker originalTicker) {}

  @override
  String get debugLabel => null;

  @override
  bool get isActive => null;

  @override
  bool get isTicking => null;

  @override
  bool get scheduled => null;

  @override
  bool get shouldScheduleTick => null;

  @override
  void dispose() {}

  @override
  void scheduleTick({bool rescheduling = false}) {}

  @override
  TickerFuture start() {
    return null;
  }

  @override
  void stop({bool canceled = false}) {}

  @override
  void unscheduleTick() {}
323 324 325

  @override
  String toString({bool debugIncludeStack = false}) => super.toString();
326
}