我正在 flutter 中做一个 UI,现在它在我的模拟器上看起来很棒,但我担心如果屏幕尺寸不同它会崩溃。防止这种情况的最佳实践是什么,尤其是使用 gridview。
这是我正在尝试做的用户界面(目前只有左侧部分):
我现在拥有的代码正在运行。每个项目都在 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,
)
此代码的最佳实践是什么,以确保如果屏幕高度较小,所有内容仍然适合?
因此,我相信您正在寻找一种缩放解决方案,可以在放大和缩小以适应不同屏幕密度的同时保持 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 个模拟器] - 但它显然存在一些问题:
no SP for text
功能时 McGyver.rsText()
。您希望您的 UI 在任何比例或屏幕密度下都具有精确的比例。 除了这 3 个问题之外,这种比例缩放方法的效果非常好,我可以将其用作我所有 flutter 项目中唯一的缩放解决方案。我希望它能帮助其他和我有同样追求的程序员。对此方法/代码的任何改进总是受欢迎的。
使用 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,
),
],
),
你可以对字体大小做同样的事情
最好使用 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));
}
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