media_query.dart 19.9 KB
Newer Older
1 2 3 4
// Copyright 2015 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:ui' as ui;

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

9 10 11
import 'basic.dart';
import 'framework.dart';

12 13 14 15
/// Whether in portrait or landscape.
enum Orientation {
  /// Taller than wide.
  portrait,
16

17 18 19
  /// Wider than tall.
  landscape
}
20

21 22 23 24 25 26 27 28
/// Information about a piece of media (e.g., a window).
///
/// For example, the [MediaQueryData.size] property contains the width and
/// height of the current window.
///
/// To obtain the current [MediaQueryData] for a given [BuildContext], use the
/// [MediaQuery.of] function. For example, to obtain the size of the current
/// window, use `MediaQuery.of(context).size`.
29 30 31 32
///
/// If no [MediaQuery] is in scope then the [MediaQuery.of] method will throw an
/// exception, unless the `nullOk` argument is set to true, in which case it
/// returns null.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
///
/// MediaQueryData includes two [EdgeInsets] values:
/// [padding] and [viewInsets]. These
/// values reflect the configuration of the device and are used by
/// many top level widgets, like [SafeArea] and the Cupertino and
/// Material scaffold widgets. The padding value defines areas that
/// might not be completely visible, like the display "notch" on the
/// iPhone X. The viewInsets value defines areas that aren't visible at
/// all, typically because they're obscured by the device's keyboard.
///
/// The viewInsets and padding values are independent, they're both
/// measured from the edges of the MediaQuery widget's bounds. The
/// bounds of the top level MediaQuery created by [WidgetsApp] are the
/// same as the window that contains the app.
///
/// Widgets whose layouts consume space defined by [viewInsets] or
/// [padding] shoud enclose their children in secondary MediaQuery
/// widgets that reduce those properties by the same amount.
/// The [removePadding] and [removeInsets] methods are useful for this.
52
@immutable
53
class MediaQueryData {
54 55 56
  /// Creates data for a media query with explicit values.
  ///
  /// Consider using [MediaQueryData.fromWindow] to create data based on a
57
  /// [Window].
58
  const MediaQueryData({
59 60 61 62 63 64
    this.size = Size.zero,
    this.devicePixelRatio = 1.0,
    this.textScaleFactor = 1.0,
    this.padding = EdgeInsets.zero,
    this.viewInsets = EdgeInsets.zero,
    this.alwaysUse24HourFormat = false,
65 66 67
    this.accessibleNavigation = false,
    this.invertColors = false,
    this.disableAnimations = false,
68
    this.boldText = false,
69
  });
70

71
  /// Creates data for a media query based on the given window.
72 73 74 75
  ///
  /// If you use this, you should ensure that you also register for
  /// notifications so that you can update your [MediaQueryData] when the
  /// window's metrics change. For example, see
76
  /// [WidgetsBindingObserver.didChangeMetrics] or [Window.onMetricsChanged].
77
  MediaQueryData.fromWindow(ui.Window window)
78
    : size = window.physicalSize / window.devicePixelRatio,
79
      devicePixelRatio = window.devicePixelRatio,
80
      textScaleFactor = window.textScaleFactor,
81 82
      padding = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio),
      viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio),
83
      accessibleNavigation = window.accessibilityFeatures.accessibleNavigation,
84
      invertColors = window.accessibilityFeatures.invertColors,
85
      disableAnimations = window.accessibilityFeatures.disableAnimations,
86
      boldText = window.accessibilityFeatures.boldText,
87
      alwaysUse24HourFormat = window.alwaysUse24HourFormat;
88

89
  /// The size of the media in logical pixels (e.g, the size of the screen).
90 91 92 93 94
  ///
  /// Logical pixels are roughly the same visual size across devices. Physical
  /// pixels are the size of the actual hardware pixels on the device. The
  /// number of physical pixels per logical pixel is described by the
  /// [devicePixelRatio].
95 96
  final Size size;

97 98 99 100 101
  /// The number of device pixels for each logical pixel. This number might not
  /// be a power of two. Indeed, it might not even be an integer. For example,
  /// the Nexus 6 has a device pixel ratio of 3.5.
  final double devicePixelRatio;

102 103 104 105
  /// The number of font pixels for each logical pixel.
  ///
  /// For example, if the text scale factor is 1.5, text will be 50% larger than
  /// the specified font size.
106 107 108 109 110
  ///
  /// See also:
  ///
  ///  * [MediaQuery.textScaleFactorOf], a convenience method which returns the
  ///    textScaleFactor defined for a [BuildContext].
111 112
  final double textScaleFactor;

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
  /// The parts of the display that are completely obscured by system UI,
  /// typically by the device's keyboard.
  ///
  /// When a mobile device's keyboard is visible `viewInsets.bottom`
  /// corresponds to the top of the keyboard.
  ///
  /// This value is independent of the [padding]: both values are
  /// measured from the edges of the [MediaQuery] widget's bounds. The
  /// bounds of the top level MediaQuery created by [WidgetsApp] are the
  /// same as the window (often the mobile device screen) that contains the app.
  ///
  /// See also:
  ///
  /// * [MediaQueryData], which provides some additional detail about this
  ///   property and how it differs from [padding].
128 129
  final EdgeInsets viewInsets;

130 131
  /// The parts of the display that are partially obscured by system UI,
  /// typically by the hardware display "notches" or the system status bar.
132 133 134 135
  ///
  /// If you consumed this padding (e.g. by building a widget that envelops or
  /// accounts for this padding in its layout in such a way that children are
  /// no longer exposed to this padding), you should remove this padding
136
  /// for subsequent descendants in the widget tree by inserting a new
137 138 139 140
  /// [MediaQuery] widget using the [MediaQuery.removePadding] factory.
  ///
  /// See also:
  ///
141 142
  ///  * [MediaQueryData], which provides some additional detail about this
  ///    property and how it differs from [viewInsets].
143 144
  ///  * [SafeArea], a widget that consumes this padding with a [Padding] widget
  ///    and automatically removes it from the [MediaQuery] for its child.
145
  final EdgeInsets padding;
146

147 148 149 150 151 152 153 154 155 156 157 158 159
  /// Whether to use 24-hour format when formatting time.
  ///
  /// The behavior of this flag is different across platforms:
  ///
  /// - On Android this flag is reported directly from the user settings called
  ///   "Use 24-hour format". It applies to any locale used by the application,
  ///   whether it is the system-wide locale, or the custom locale set by the
  ///   application.
  /// - On iOS this flag is set to true when the user setting called "24-Hour
  ///   Time" is set or the system-wide locale's default uses 24-hour
  ///   formatting.
  final bool alwaysUse24HourFormat;

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
  /// Whether the user is using an accessibility service like TalkBack or
  /// VoiceOver to interact with the application.
  ///
  /// When this setting is true, features such as timeouts should be disabled or
  /// have minimum durations increased.
  ///
  /// See also:
  ///
  ///  * [Window.AccessibilityFeatures], where the setting originates.
  final bool accessibleNavigation;

  /// Whether the device is inverting the colors of the platform.
  ///
  /// This flag is currently only updated on iOS devices.
  ///
  /// See also:
  ///
  ///  * [Window.AccessibilityFeatures], where the setting originates.
  final bool invertColors;

  /// Whether the platform is requesting that animations be disabled or reduced
  /// as much as possible.
  ///
183 184
  /// See also:
  ///
185 186 187
  ///  * [Window.AccessibilityFeatures], where the setting originates.
  final bool disableAnimations;

188 189 190 191 192 193 194 195
  /// Whether the platform is requesting that text be drawn with a bold font
  /// weight.
  ///
  /// See also:
  ///
  ///  * [Window.AccessibilityFeatures], where the setting originates.
  final bool boldText;

196
  /// The orientation of the media (e.g., whether the device is in landscape or portrait mode).
197 198 199 200
  Orientation get orientation {
    return size.width > size.height ? Orientation.landscape : Orientation.portrait;
  }

201 202
  /// Creates a copy of this media query data but with the given fields replaced
  /// with the new values.
203 204 205 206 207
  MediaQueryData copyWith({
    Size size,
    double devicePixelRatio,
    double textScaleFactor,
    EdgeInsets padding,
208
    EdgeInsets viewInsets,
209
    bool alwaysUse24HourFormat,
210 211 212
    bool disableAnimations,
    bool invertColors,
    bool accessibleNavigation,
213
    bool boldText,
214
  }) {
215
    return MediaQueryData(
216 217 218 219
      size: size ?? this.size,
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
      textScaleFactor: textScaleFactor ?? this.textScaleFactor,
      padding: padding ?? this.padding,
220
      viewInsets: viewInsets ?? this.viewInsets,
221
      alwaysUse24HourFormat: alwaysUse24HourFormat ?? this.alwaysUse24HourFormat,
222 223 224
      invertColors: invertColors ?? this.invertColors,
      disableAnimations: disableAnimations ?? this.disableAnimations,
      accessibleNavigation: accessibleNavigation ?? this.accessibleNavigation,
225
      boldText: boldText ?? this.boldText,
226 227 228
    );
  }

229
  /// Creates a copy of this media query data but with the given [padding]s
Ian Hickson's avatar
Ian Hickson committed
230 231 232 233 234 235 236 237 238 239 240 241
  /// replaced with zero.
  ///
  /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
  /// must not be null. If all four are false (the default) then this
  /// [MediaQueryData] is returned unmodified.
  ///
  /// See also:
  ///
  ///  * [new MediaQuery.removePadding], which uses this method to remove padding
  ///    from the ambient [MediaQuery].
  ///  * [SafeArea], which both removes the padding from the [MediaQuery] and
  ///    adds a [Padding] widget.
242
  ///  * [removeViewInsets], the same thing but for [viewInsets].
Ian Hickson's avatar
Ian Hickson committed
243
  MediaQueryData removePadding({
244 245 246 247
    bool removeLeft = false,
    bool removeTop = false,
    bool removeRight = false,
    bool removeBottom = false,
Ian Hickson's avatar
Ian Hickson committed
248 249 250
  }) {
    if (!(removeLeft || removeTop || removeRight || removeBottom))
      return this;
251
    return MediaQueryData(
Ian Hickson's avatar
Ian Hickson committed
252 253 254 255 256 257 258 259 260
      size: size,
      devicePixelRatio: devicePixelRatio,
      textScaleFactor: textScaleFactor,
      padding: padding.copyWith(
        left: removeLeft ? 0.0 : null,
        top: removeTop ? 0.0 : null,
        right: removeRight ? 0.0 : null,
        bottom: removeBottom ? 0.0 : null,
      ),
261
      viewInsets: viewInsets,
262
      alwaysUse24HourFormat: alwaysUse24HourFormat,
263 264 265
      disableAnimations: disableAnimations,
      invertColors: invertColors,
      accessibleNavigation: accessibleNavigation,
266
      boldText: boldText,
Ian Hickson's avatar
Ian Hickson committed
267 268 269
    );
  }

270 271 272 273 274 275 276 277 278 279 280 281 282
  /// Creates a copy of this media query data but with the given [viewInsets]
  /// replaced with zero.
  ///
  /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
  /// must not be null. If all four are false (the default) then this
  /// [MediaQueryData] is returned unmodified.
  ///
  /// See also:
  ///
  ///  * [new MediaQuery.removeViewInsets], which uses this method to remove
  ///    padding from the ambient [MediaQuery].
  ///  * [removePadding], the same thing but for [padding].
  MediaQueryData removeViewInsets({
283 284 285 286
    bool removeLeft = false,
    bool removeTop = false,
    bool removeRight = false,
    bool removeBottom = false,
287 288 289
  }) {
    if (!(removeLeft || removeTop || removeRight || removeBottom))
      return this;
290
    return MediaQueryData(
291 292 293 294 295 296 297 298 299 300 301
      size: size,
      devicePixelRatio: devicePixelRatio,
      textScaleFactor: textScaleFactor,
      padding: padding,
      viewInsets: viewInsets.copyWith(
        left: removeLeft ? 0.0 : null,
        top: removeTop ? 0.0 : null,
        right: removeRight ? 0.0 : null,
        bottom: removeBottom ? 0.0 : null,
      ),
      alwaysUse24HourFormat: alwaysUse24HourFormat,
302 303 304
      disableAnimations: disableAnimations,
      invertColors: invertColors,
      accessibleNavigation: accessibleNavigation,
305
      boldText: boldText,
306 307 308
    );
  }

309
  @override
310
  bool operator ==(Object other) {
311 312
    if (other.runtimeType != runtimeType)
      return false;
313
    final MediaQueryData typedOther = other;
314
    return typedOther.size == size
315 316
        && typedOther.devicePixelRatio == devicePixelRatio
        && typedOther.textScaleFactor == textScaleFactor
317
        && typedOther.padding == padding
318
        && typedOther.viewInsets == viewInsets
319 320 321
        && typedOther.alwaysUse24HourFormat == alwaysUse24HourFormat
        && typedOther.disableAnimations == disableAnimations
        && typedOther.invertColors == invertColors
322 323
        && typedOther.accessibleNavigation == accessibleNavigation
        && typedOther.boldText == boldText;
324 325
  }

326
  @override
327 328 329 330 331 332 333 334 335 336 337
  int get hashCode {
    return hashValues(
      size,
      devicePixelRatio,
      textScaleFactor,
      padding,
      viewInsets,
      alwaysUse24HourFormat,
      disableAnimations,
      invertColors,
      accessibleNavigation,
338
      boldText,
339 340
    );
  }
341

342
  @override
343
  String toString() {
344 345
    return '$runtimeType('
             'size: $size, '
Ian Hickson's avatar
Ian Hickson committed
346 347
             'devicePixelRatio: ${devicePixelRatio.toStringAsFixed(1)}, '
             'textScaleFactor: ${textScaleFactor.toStringAsFixed(1)}, '
348 349
             'padding: $padding, '
             'viewInsets: $viewInsets, '
350 351 352 353
             'alwaysUse24HourFormat: $alwaysUse24HourFormat, '
             'accessibleNavigation: $accessibleNavigation'
             'disableAnimations: $disableAnimations'
             'invertColors: $invertColors'
354
             'boldText: $boldText'
355
           ')';
356
  }
357 358
}

359
/// Establishes a subtree in which media queries resolve to the given data.
360 361 362 363 364 365 366 367 368
///
/// For example, to learn the size of the current media (e.g., the window
/// containing your app), you can read the [MediaQueryData.size] property from
/// the [MediaQueryData] returned by [MediaQuery.of]:
/// `MediaQuery.of(context).size`.
///
/// Querying the current media using [MediaQuery.of] will cause your widget to
/// rebuild automatically whenever the [MediaQueryData] changes (e.g., if the
/// user rotates their device).
369 370 371 372 373 374 375 376 377 378
///
/// If no [MediaQuery] is in scope then the [MediaQuery.of] method will throw an
/// exception, unless the `nullOk` argument is set to true, in which case it
/// returns null.
///
/// See also:
///
///  * [WidgetsApp] and [MaterialApp], which introduce a [MediaQuery] and keep
///    it up to date with the current screen metrics as they change.
///  * [MediaQueryData], the data structure that represents the metrics.
379
class MediaQuery extends InheritedWidget {
380 381 382
  /// Creates a widget that provides [MediaQueryData] to its descendants.
  ///
  /// The [data] and [child] arguments must not be null.
383
  const MediaQuery({
384
    Key key,
385
    @required this.data,
386
    @required Widget child,
387 388 389
  }) : assert(child != null),
       assert(data != null),
       super(key: key, child: child);
390

Ian Hickson's avatar
Ian Hickson committed
391 392 393
  /// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
  /// the given context, but removes the specified paddings.
  ///
394
  /// This should be inserted into the widget tree when the [MediaQuery] padding
395
  /// is consumed by a widget in such a way that the padding is no longer
396
  /// exposed to the widget's descendants or siblings.
397
  ///
Ian Hickson's avatar
Ian Hickson committed
398 399 400 401 402 403 404 405 406 407 408 409 410 411
  /// The [context] argument is required, must not be null, and must have a
  /// [MediaQuery] in scope.
  ///
  /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
  /// must not be null. If all four are false (the default) then the returned
  /// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not
  /// particularly useful.
  ///
  /// The [child] argument is required and must not be null.
  ///
  /// See also:
  ///
  ///  * [SafeArea], which both removes the padding from the [MediaQuery] and
  ///    adds a [Padding] widget.
412 413
  ///  * [MediaQueryData.padding], the affected property of the [MediaQueryData].
  ///  * [new removeViewInsets], the same thing but for removing view insets.
Ian Hickson's avatar
Ian Hickson committed
414 415 416
  factory MediaQuery.removePadding({
    Key key,
    @required BuildContext context,
417 418 419 420
    bool removeLeft = false,
    bool removeTop = false,
    bool removeRight = false,
    bool removeBottom = false,
Ian Hickson's avatar
Ian Hickson committed
421 422
    @required Widget child,
  }) {
423
    return MediaQuery(
Ian Hickson's avatar
Ian Hickson committed
424 425 426 427 428 429 430
      key: key,
      data: MediaQuery.of(context).removePadding(
        removeLeft: removeLeft,
        removeTop: removeTop,
        removeRight: removeRight,
        removeBottom: removeBottom,
      ),
431 432 433 434 435 436 437 438 439
      child: child,
    );
  }

  /// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
  /// the given context, but removes the specified view insets.
  ///
  /// This should be inserted into the widget tree when the [MediaQuery] view
  /// insets are consumed by a widget in such a way that the view insets are no
440
  /// longer exposed to the widget's descendants or siblings.
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
  ///
  /// The [context] argument is required, must not be null, and must have a
  /// [MediaQuery] in scope.
  ///
  /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
  /// must not be null. If all four are false (the default) then the returned
  /// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not
  /// particularly useful.
  ///
  /// The [child] argument is required and must not be null.
  ///
  /// See also:
  ///
  ///  * [MediaQueryData.viewInsets], the affected property of the [MediaQueryData].
  ///  * [new removePadding], the same thing but for removing paddings.
  factory MediaQuery.removeViewInsets({
    Key key,
    @required BuildContext context,
459 460 461 462
    bool removeLeft = false,
    bool removeTop = false,
    bool removeRight = false,
    bool removeBottom = false,
463 464
    @required Widget child,
  }) {
465
    return MediaQuery(
466 467 468 469 470 471 472
      key: key,
      data: MediaQuery.of(context).removeViewInsets(
        removeLeft: removeLeft,
        removeTop: removeTop,
        removeRight: removeRight,
        removeBottom: removeBottom,
      ),
Ian Hickson's avatar
Ian Hickson committed
473 474 475 476
      child: child,
    );
  }

477 478 479 480
  /// Contains information about the current media.
  ///
  /// For example, the [MediaQueryData.size] property contains the width and
  /// height of the current window.
481 482
  final MediaQueryData data;

483 484
  /// The data from the closest instance of this class that encloses the given
  /// context.
485 486 487 488
  ///
  /// You can use this function to query the size an orientation of the screen.
  /// When that information changes, your widget will be scheduled to be rebuilt,
  /// keeping your widget up-to-date.
489 490 491 492 493 494
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MediaQueryData media = MediaQuery.of(context);
  /// ```
495 496 497 498 499 500
  ///
  /// If there is no [MediaQuery] in scope, then this will throw an exception.
  /// To return null if there is no [MediaQuery], then pass `nullOk: true`.
  ///
  /// If you use this from a widget (e.g. in its build function), consider
  /// calling [debugCheckHasMediaQuery].
501
  static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
Ian Hickson's avatar
Ian Hickson committed
502 503
    assert(context != null);
    assert(nullOk != null);
504
    final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery);
505 506 507 508
    if (query != null)
      return query.data;
    if (nullOk)
      return null;
509
    throw FlutterError(
510 511 512 513 514 515 516 517
      'MediaQuery.of() called with a context that does not contain a MediaQuery.\n'
      'No MediaQuery ancestor could be found starting from the context that was passed '
      'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
      'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
      'if the context you use comes from a widget above those widgets.\n'
      'The context used was:\n'
      '  $context'
    );
518 519
  }

520 521 522 523 524 525
  /// Returns textScaleFactor for the nearest MediaQuery ancestor or 1.0, if
  /// no such ancestor exists.
  static double textScaleFactorOf(BuildContext context) {
    return MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0;
  }

526 527 528 529 530 531
  /// Returns the boldText accessibility setting for the nearest MediaQuery
  /// ancestor, or false if no such ancestor exists.
  static bool boldTextOverride(BuildContext context) {
    return MediaQuery.of(context, nullOk: true)?.boldText ?? false;
  }

532
  @override
533
  bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
534

535
  @override
536 537
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
538
    properties.add(DiagnosticsProperty<MediaQueryData>('data', data, showName: false));
539 540
  }
}