Flutter中根据不同屏幕尺寸有效缩放此UI的最佳实践

问题描述 投票:0回答:5

我正在 flutter 中做一个 UI,现在它在我的模拟器上看起来很棒,但我担心如果屏幕尺寸不同它会崩溃。防止这种情况的最佳实践是什么,尤其是使用 gridview。

这是我正在尝试做的用户界面(目前只有左侧部分):

UI

我现在拥有的代码正在运行。每个项目都在 Container 中,其中 2 个是 Gridview :

          Expanded(
            child: Container(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SizedBox(height: 100),
                  Container( // Top text
                    margin: const EdgeInsets.only(left: 20.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text("Hey,",
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 25)),
                        Text("what's up ?", style: TextStyle(fontSize: 25)),
                        SizedBox(height: 10),
                      ],
                    ),
                  ),
                  Container( // First gridview
                      height: MediaQuery.of(context).size.height/2,
                      child: GridView.count(
                          crossAxisCount: 3,
                          scrollDirection: Axis.horizontal,
                          crossAxisSpacing: 10,
                          mainAxisSpacing: 10,
                          padding: const EdgeInsets.all(10),
                          children: List.generate(9, (index) {
                            return Center(
                                child: ButtonTheme(
                                    minWidth: 100.0,
                                    height: 125.0,
                                    child: RaisedButton(
                                      splashColor: Color.fromRGBO(230, 203, 51, 1),
                                        color: (index!=0)?Colors.white:Color.fromRGBO(201, 22, 25, 1),
                                        child: Column(
                                            mainAxisAlignment:
                                                MainAxisAlignment.center,
                                            children: <Widget>[
                                              Image.asset(
                                                'assets/in.png',
                                                fit: BoxFit.cover,
                                              ),
                                              Text("Eat In",
                                                  style: TextStyle(
                                                      fontWeight:
                                                          FontWeight.bold))
                                            ]),
                                        onPressed: () {
                                        },
                                        shape: RoundedRectangleBorder(
                                            borderRadius:
                                                new BorderRadius.circular(
                                                    20.0)))));
                          }))),
                  Container( // Bottom Text
                    margin: const EdgeInsets.only(left: 20.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        SizedBox(height: 10),
                        Text("Popular",
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 25)),
                        SizedBox(height: 10),
                      ],
                    ),
                  ),
                  Container( // Second Gridview
                      height: MediaQuery.of(context).size.height/5,
                      child: GridView.count(
                          crossAxisCount: 2,
                          scrollDirection: Axis.horizontal,
                          children: List.generate(9, (index) {
                            return Center(
                                child: ButtonTheme(
                                    minWidth: 100.0,
                                    height: 125.0,
                                    child: FlatButton(
                                        color: Colors.white,
                                        child: Column(
                                            mainAxisAlignment:
                                                MainAxisAlignment.center,
                                            children: <Widget>[
                                              Image.asset(
                                                'assets/logo.png',
                                                fit: BoxFit.cover,
                                              ),
                                              Text("Name")
                                            ]),
                                        onPressed: () {},
                                        shape: RoundedRectangleBorder(
                                            borderRadius:
                                                new BorderRadius.circular(
                                                    20.0)))));
                          })))
                ],
              ),
            ),
            flex: 3,
          )

此代码的最佳实践是什么,以确保如果屏幕高度较小,所有内容仍然适合?

flutter dart flutter-layout
5个回答
6
投票

比率缩放解决方案 [ Flutter 移动应用程序 ]

因此,我相信您正在寻找一种缩放解决方案,可以在放大和缩小以适应不同屏幕密度的同时保持 UI 的比例(即比例)不变。实现这一目标的方法是将比率缩放解决方案应用于您的项目。 [点击下图查看更清晰]


比率缩放过程概述

第 1 步:定义固定缩放比例 [高度:宽度 => 2:1 比例](以像素为单位)。
第 2 步:指定您的应用程序是否是全屏应用程序(即定义状态栏是否在高度缩放中发挥作用)。
第 3 步:使用以下流程 [代码] 根据百分比缩放整个 UI(从应用程序栏到最小的文本)。


重要代码单位
=> McGyver [“MacGyver”的一个游戏] - 执行重要比例缩放的类。

// Imports: Third-Party.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// Imports: Local [internal] packages.
import 'package:pixel_perfect/utils/stringr.dart';
import 'package:pixel_perfect/utils/enums_all.dart';

// Exports: Local [internal] packages.
export 'package:pixel_perfect/utils/enums_all.dart';



// 'McGyver' - the ultimate cool guy (the best helper class any app can ask for).
class McGyver {

  static final TAG_CLASS_ID = "McGyver";

  static double _fixedWidth;    // Defined in pixels !!
  static double _fixedHeight;   // Defined in pixels !!
  static bool _isFullScreenApp = false;   // Define whether app is a fullscreen app [true] or not [false] !!

  static void hideSoftKeyboard() {
    SystemChannels.textInput.invokeMethod("TextInput.hide");
  }

  static double roundToDecimals(double numToRound, int deciPlaces) {

    double modPlus1 = pow(10.0, deciPlaces + 1);
    String strMP1 = ((numToRound * modPlus1).roundToDouble() / modPlus1).toStringAsFixed(deciPlaces + 1);
    int lastDigitStrMP1 = int.parse(strMP1.substring(strMP1.length - 1));

    double mod = pow(10.0, deciPlaces);
    String strDblValRound = ((numToRound * mod).roundToDouble() / mod).toStringAsFixed(deciPlaces);
    int lastDigitStrDVR = int.parse(strDblValRound.substring(strDblValRound.length - 1));

    return (lastDigitStrMP1 == 5 && lastDigitStrDVR % 2 != 0) ? ((numToRound * mod).truncateToDouble() / mod) : double.parse(strDblValRound);
  }

  static Orientation setScaleRatioBasedOnDeviceOrientation(BuildContext ctx) {
    Orientation scaleAxis;
    if(MediaQuery.of(ctx).orientation == Orientation.portrait) {
      _fixedWidth = 420;                  // Ration: 1 [width]
      _fixedHeight = 840;                 // Ration: 2 [height]
      scaleAxis = Orientation.portrait;   // Shortest axis == width !!
    } else {
      _fixedWidth = 840;                   // Ration: 2 [width]
      _fixedHeight = 420;                  // Ration: 1 [height]
      scaleAxis = Orientation.landscape;   // Shortest axis == height !!
    }
    return scaleAxis;
  }

  static int rsIntW(BuildContext ctx, double scaleValue) {

    // -------------------------------------------------------------- //
    // INFO: Ratio-Scaled integer - Scaling based on device's width.  //
    // -------------------------------------------------------------- //

    final double _origVal = McGyver.rsDoubleW(ctx, scaleValue);
    return McGyver.roundToDecimals(_origVal, 0).toInt();
  }

  static int rsIntH(BuildContext ctx, double scaleValue) {

    // -------------------------------------------------------------- //
    // INFO: Ratio-Scaled integer - Scaling based on device's height. //
    // -------------------------------------------------------------- //

    final double _origVal = McGyver.rsDoubleH(ctx, scaleValue);
    return McGyver.roundToDecimals(_origVal, 0).toInt();
  }

  static double rsDoubleW(BuildContext ctx, double wPerc) {

    // ------------------------------------------------------------------------------------------------------- //
    // INFO: Ratio-Scaled double - scaling based on device's screen width in relation to fixed width ration.   //
    // INPUTS: - 'ctx'     [context] -> BuildContext                                                           //
    //         - 'wPerc'   [double]  -> Value (as a percentage) to be ratio-scaled in terms of width.          //
    // OUTPUT: - 'rsWidth' [double]  -> Ratio-scaled value.                                                    //
    // ------------------------------------------------------------------------------------------------------- //

    final int decimalPlaces = 14;   //* NB: Don't change this value -> has big effect on output result accuracy !!

    Size screenSize = MediaQuery.of(ctx).size;                  // Device Screen Properties (dimensions etc.).
    double scrnWidth = screenSize.width.floorToDouble();        // Device Screen maximum Width (in pixels).

    McGyver.setScaleRatioBasedOnDeviceOrientation(ctx);   //* Set Scale-Ratio based on device orientation.

    double rsWidth = 0;   //* OUTPUT: 'rsWidth' == Ratio-Scaled Width (in pixels)
    if (scrnWidth == _fixedWidth) {

      //* Do normal 1:1 ratio-scaling for matching screen width (i.e. '_fixedWidth' vs. 'scrnWidth') dimensions.
      rsWidth = McGyver.roundToDecimals(scrnWidth * (wPerc / 100), decimalPlaces);

    } else {

      //* Step 1: Calculate width difference based on width scale ration (i.e. pixel delta: '_fixedWidth' vs. 'scrnWidth').
      double wPercRatioDelta = McGyver.roundToDecimals(100 - ((scrnWidth / _fixedWidth) * 100), decimalPlaces);   // 'wPercRatioDelta' == Width Percentage Ratio Delta !!

      //* Step 2: Calculate primary ratio-scale adjustor (in pixels) based on input percentage value.
      double wPxlsInpVal = (wPerc / 100) * _fixedWidth;   // 'wPxlsInpVal' == Width in Pixels of Input Value.

      //* Step 3: Calculate secondary ratio-scale adjustor (in pixels) based on primary ratio-scale adjustor.
      double wPxlsRatDelta = (wPercRatioDelta / 100) * wPxlsInpVal;   // 'wPxlsRatDelta' == Width in Pixels of Ratio Delta (i.e. '_fixedWidth' vs. 'scrnWidth').

      //* Step 4: Finally -> Apply ratio-scales and return value to calling function / instance.
      rsWidth = McGyver.roundToDecimals((wPxlsInpVal - wPxlsRatDelta), decimalPlaces);

    }
    return rsWidth;
  }

  static double rsDoubleH(BuildContext ctx, double hPerc) {

    // ------------------------------------------------------------------------------------------------------- //
    // INFO: Ratio-Scaled double - scaling based on device's screen height in relation to fixed height ration. //
    // INPUTS: - 'ctx'      [context] -> BuildContext                                                          //
    //         - 'hPerc'    [double]  -> Value (as a percentage) to be ratio-scaled in terms of height.        //
    // OUTPUT: - 'rsHeight' [double]  -> Ratio-scaled value.                                                   //
    // ------------------------------------------------------------------------------------------------------- //

    final int decimalPlaces = 14;   //* NB: Don't change this value -> has big effect on output result accuracy !!

    Size scrnSize = MediaQuery.of(ctx).size;                  // Device Screen Properties (dimensions etc.).
    double scrnHeight = scrnSize.height.floorToDouble();      // Device Screen maximum Height (in pixels).
    double statsBarHeight = MediaQuery.of(ctx).padding.top;   // Status Bar Height (in pixels).

    McGyver.setScaleRatioBasedOnDeviceOrientation(ctx);   //* Set Scale-Ratio based on device orientation.

    double rsHeight = 0;   //* OUTPUT: 'rsHeight' == Ratio-Scaled Height (in pixels)
    if (scrnHeight == _fixedHeight) {

      //* Do normal 1:1 ratio-scaling for matching screen height (i.e. '_fixedHeight' vs. 'scrnHeight') dimensions.
      rsHeight = McGyver.roundToDecimals(scrnHeight * (hPerc / 100), decimalPlaces);

    } else {

      //* Step 1: Calculate height difference based on height scale ration (i.e. pixel delta: '_fixedHeight' vs. 'scrnHeight').
      double hPercRatioDelta = McGyver.roundToDecimals(100 - ((scrnHeight / _fixedHeight) * 100), decimalPlaces);   // 'hPercRatioDelta' == Height Percentage Ratio Delta !!

      //* Step 2: Calculate height of Status Bar as a percentage of the height scale ration (i.e. 'statsBarHeight' vs. '_fixedHeight').
      double hPercStatsBar = McGyver.roundToDecimals((statsBarHeight / _fixedHeight) * 100, decimalPlaces);   // 'hPercStatsBar' == Height Percentage of Status Bar !!

      //* Step 3: Calculate primary ratio-scale adjustor (in pixels) based on input percentage value.
      double hPxlsInpVal = (hPerc / 100) * _fixedHeight;   // 'hPxlsInpVal' == Height in Pixels of Input Value.

      //* Step 4: Calculate secondary ratio-scale adjustors (in pixels) based on primary ratio-scale adjustor.
      double hPxlsStatsBar = (hPercStatsBar / 100) * hPxlsInpVal;     // 'hPxlsStatsBar' == Height in Pixels of Status Bar.
      double hPxlsRatDelta = (hPercRatioDelta / 100) * hPxlsInpVal;   // 'hPxlsRatDelta' == Height in Pixels of Ratio Delat (i.e. '_fixedHeight' vs. 'scrnHeight').

      //* Step 5: Check if '_isFullScreenApp' is true and adjust 'Status Bar' scalar accordingly.
      double hAdjStatsBarPxls = _isFullScreenApp ? 0 : hPxlsStatsBar;   // Set to 'zero' if FULL SCREEN APP !!

      //* Step 6: Finally -> Apply ratio-scales and return value to calling function / instance.
      rsHeight = McGyver.roundToDecimals(hPxlsInpVal - (hPxlsRatDelta + hAdjStatsBarPxls), decimalPlaces);

    }
    return rsHeight;
  }

  static Widget rsWidget(BuildContext ctx, Widget inWidget,
                    double percWidth, double percHeight, {String viewID}) {

    // ---------------------------------------------------------------------------------------------- //
    // INFO: Ratio-Scaled "SizedBox" Widget - Scaling based on device's width & height.         //
    // ---------------------------------------------------------------------------------------------- //

    return SizedBox(
      width: Scalar.rsDoubleW(ctx, percWidth),
      height: Scalar.rsDoubleH(ctx, percHeight),
      child: inWidget,
    );
  }

  //* SPECIAL 'rsWidget' that has both its height & width ratio-scaled based on 'width' alone !!
  static Widget rsWidgetW(BuildContext ctx, Widget inWidget,
                    double percWidth, double percHeight, {String viewID}) {

    // ---------------------------------------------------------------------------------------------- //
    // INFO: Ratio-Scaled "SizedBox" Widget - Scaling based on device's width ONLY !!          //
    // ---------------------------------------------------------------------------------------------- //

    return SizedBox(
      width: Scalar.rsDoubleW(ctx, percWidth),
      height: Scalar.rsDoubleW(ctx, percHeight),
      child: inWidget,
    );
  }

  static Widget rsText(BuildContext ctx, String text, {double fontSize,
                      Color textColor, Anchor txtLoc, FontWeight fontWeight}) {

    // ---------------------------------------------------------------------------------------- //
    // INFO: Ratio-Scaled Text Widget - Default Font Weight == NORMAL !!                        //
    // ---------------------------------------------------------------------------------------- //

    // Scale the Font Size (based on device's screen width).
    double txtScaleFactor = MediaQuery.of(ctx).textScaleFactor;
    double _rsFontSize = (fontSize != null) ? McGyver.rsDoubleW(ctx, fontSize) : McGyver.rsDoubleW(ctx, 2.5);

    TextAlign _txtLoc;
    if (txtLoc == Anchor.left) {
      _txtLoc = TextAlign.left;
    } else if (txtLoc == Anchor.middle) {
      _txtLoc = TextAlign.center;
    } else {
      _txtLoc = TextAlign.right;
    }

    return Text(
      text,
      textAlign: _txtLoc,
      style: TextStyle(
        fontFamily: Stringr.strAppFontFamily,
        fontSize: (_rsFontSize / txtScaleFactor) * 1.0,
        color: (textColor != null) ? textColor : Colors.black,
        fontWeight: (fontWeight != null) ? fontWeight : FontWeight.normal,
      ),
    );
  }

}

McGyver class covers
整个过程在
Steps 1 & 2 of
Ratio-Scaling Process
下概述。
then
剩下要做的就是在构建过程中
apply Step 3
如下...

AppBar 代码片段:[在图像中创建 AppBar 的代码 - 图 1 - 上方]

Container(
  color: Colors.blue[500],
  width: McGyver.rsDoubleW(con, 100.5),
  height: McGyver.rsDoubleH(con, 8.5),
  child: Row(
    children: <Widget>[
      //* Hamburger Button => Button 1.
      Padding(
        padding: EdgeInsets.fromLTRB(_padLeft, _padTop, 0, _padBottom),
        child: Container(
          color: Colors.yellow,
          width: _appBarBtnsWidth,
          height: _appBarBtnsHeight,
          child: Center(child: McGyver.rsText(context, "1", fontSize: 5.5, fontWeight: FontWeight.bold, textColor: Colors.red),),
        ),
      ),
      //* AppBar Info Text (center text).
      Padding(
        padding: EdgeInsets.only(left: McGyver.rsDoubleW(con, 3.5), right: McGyver.rsDoubleW(con, 3.5)),
        child: Container(
          // color: Colors.pink,
          width: McGyver.rsDoubleW(context, 52.5),
          child: McGyver.rsText(con, "100% Ratio-Scaled UI", fontSize: 4.5, textColor: Colors.white, fontWeight: FontWeight.bold, txtLoc: Anchor.left),
        ),
      ),
      //* Right Button Group - LEFT Button => Button 2.
      Padding(
        padding: EdgeInsets.fromLTRB(McGyver.rsDoubleW(con, 0), _padTop, McGyver.rsDoubleH(con, 1.5), _padBottom),
        child: Container(
          color: Colors.black,
          width: _appBarBtnsWidth,
          height: _appBarBtnsHeight,
          child: Center(child: McGyver.rsText(context, "2", fontSize: 5.5, fontWeight: FontWeight.bold, textColor: Colors.white),),
        ),
      ),
      //* Right Button Group - RIGHT Button => Button 3.
      Padding(
        padding: EdgeInsets.fromLTRB(McGyver.rsDoubleW(con, 0), _padTop, 0, _padBottom),
        child: Container(
          color: Colors.pink,
          width: _appBarBtnsWidth,
          height: _appBarBtnsHeight,
          child: Center(child: McGyver.rsText(context, "3", fontSize: 5.5, fontWeight: FontWeight.bold, textColor: Colors.yellow),),
        ),
      ),
    ],
  ),
),

比率缩放代码的局限性
这种比例缩放解决方案在所有测试的设备上都表现得非常好 [7 个物理设备和 1 个模拟器] - 但它显然存在一些问题:

  1. 文字
  2. 填充
  3. 边缘纵横比

  • Text 比例因子无效(通过此代码停用) - 因此在使用
    no SP for text
    功能时
    McGyver.rsText()
    。您希望您的 UI 在任何比例或屏幕密度下都具有精确的比例。
  • Flutter(以及一般的 Android)中的 padding 存在一些奇怪的缩放[在幕后进行]。
  • 具有极其奇怪的纵横比(即宽度:高度像素密度)的设备也会导致 UI 的比例有些扭曲。

除了这 3 个问题之外,这种比例缩放方法的效果非常好,我可以将其用作我所有 flutter 项目中唯一的缩放解决方案。我希望它能帮助其他和我有同样追求的程序员。对此方法/代码的任何改进总是受欢迎的。



1
投票

使用 flutter widget

LayoutBuilder
每次使用它时,它都会给你一个
BoxConstraint
它能做的是,它会告诉你哪些空间(maxHeight、maxWidth 等)可用于 widget 树中的其他子项,你利用这个细节来划分孩子们内部的空间

例如

如果您想将可用宽度划分为 3

Containers
,请执行

Row(
          children: <Widget>[
            Container(
              width: constraints.maxWidth / 3,
            ),
            Container(
              width: constraints.maxWidth / 3,
            ),
            Container(
              width: constraints.maxWidth / 3,
            ),
          ],
        ),

你可以对字体大小做同样的事情


1
投票

最好使用 MediaQuery.of(context).size,因为在使用外部包时,您将无法在方向更改时保持小部件的大小,如果您的应用程序需要方向更改以获得更好的视觉效果,这可能会是一个很大的失败效果:

Widget build(BuildContext context) {
AppBar appBar = AppBar(title: const Text("Home"));
height = MediaQuery.of(context).size.height -
    appBar.preferredSize.height -
    MediaQuery.of(context).padding.top; // for responsive adjustment
width = MediaQuery.of(context).size.width; // for responsive adjustment
debugPrint("$height, width: ${MediaQuery.of(context).size.width}");
return Scaffold(appBar: appBar, body: ResponsivePage(height,width));
}

0
投票

看看这个包: https://pub.dev/packages/scaled_app

runApp
替换为
runAppScaled
,整个UI将自动缩放。

当你想快速适应不同的屏幕尺寸时非常有帮助。


0
投票

使用 sizedbox.expandFittedBox 小部件的完美解决方案。

   class ScalingBox extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        double logicWidth = 600;
        double logicHeight = 600;
        return SizedBox.expand(
            child: Container(
                color: Colors.blueGrey,
                child: FittedBox(
                    fit: BoxFit.contain,
                    alignment: Alignment.center,
                    child: SizedBox(
                      width: logicWidth,
                      height: logicHeight,
                      child: Contents(),// your content here
                    ))));
      }
    }

我从一篇文章中找到的。 https://stasheq.medium.com/scale-whole-app-or-widget-contents-to-a-screen-size-in-flutter-e3be161b5ab4

© www.soinside.com 2019 - 2024. All rights reserved.