Best practice for effectively scale this UI according to different screen sizes in Flutter

后端 未结 2 1961
伪装坚强ぢ
伪装坚强ぢ 2020-12-20 08:27

I\'m doing a UI in flutter and right now it look great on my emulator but I\'m afraid it will break if screen size is different. What is the best practice to prevent this, e

2条回答
  •  囚心锁ツ
    2020-12-20 08:54

    Ratio-Scaling Solution [ Flutter mobile apps ]

    So I believe you're looking for a scaling solution that maintains the proportions (i.e. ratios) of your UI intact while scaling up and down to fit different screen densities. The way to achieve this is to apply a Ratio-Scaling solution to your project.


    Outline of Ratio-Scaling Process:

    Step 1: Define a fixed scaling ratio [Height:Width => 2:1 ratio] in pixels.
    Step 2: Specify whether your app is a full screen app or not (i.e. define whether the Status Bar plays a role in your height scaling).
    Step 3: Scale your entire UI (from the App bar to the tiniest text) on the basis of percentages using the following process [code].


    IMPORTANT CODE unit:
    => McGyver [ a play on 'MacGyver' ] - the class that does the important ratio-scaling.

    // 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 val, int places) {
        double mod = pow(10.0, places);
        return ((val * mod).round().toDouble() / mod);
      }
    
      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,
          ),
        );
      }
    
    }
    


    The McGyver class covers the entire process outlined under Steps 1 & 2 of the Ratio-Scaling Process. All that is then left to do is to apply Step 3 in the build process as follows...

    AppBar Code Snippet: [ code that creates the AppBar in the image - Fig. 1 - above ]

    Container(
      color: Colors.blue[500],
      width: McGyver.rsDoubleW(con, 100.5),
      height: McGyver.rsDoubleH(con, 8.5),
      child: Row(
        children: [
          //* 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),),
            ),
          ),
        ],
      ),
    ),
    

    Limitations of Ratio-Scaling Code
    This ratio-scaling solution worked surprisingly well on ALL devices tested [7 physical devices & 1 emulator] - but it clearly has some issues with:

    1. Text
    2. Padding
    3. Extreme Aspect Ratios

    - Text scale factor is nullified (deactivated by this code) - so no SP for text when using the McGyver.rsText() feature. You want your UI to have the exact proportions at ANY scale or screen density.
    - There is some weird scaling [ going on behind the scenes ] with padding in Flutter (and Android in general).
    - Devices with extremely weird aspect ratios (i.e. width:height pixel ratios) also cause the proportions of the UI to distort somewhat.

    Aside from these three issues this ratio-scaling approach works well enough for me to use it as my only scaling solution in all my flutter projects. I hope it will help other programmers on the same quest as I am. Any improvement to this approach / code is always welcomed.


提交回复
热议问题