I've been knocking other people's (@datayeah & Vithani Ravi) solutions a bit hard here, so I thought I'd share my own attempt[s] at solving this variable screen density scaling problem or shut up. So I approach this problem from a solid/fixed foundation: I base all my scaling off a fixed (immutable) ratio of 2:1 (height:width). I have a helper class "McGyver" that does all the heavy lifting (and useful code finessing) across my app. This "McGyver" class contains only static methods and static constant class members.
RATIO SCALING METHOD: I scale both width & height independently based on the 2:1 Aspect Ratio. I take width & height input values and divide each by the width & height constants and finally compute an adjustment factor by which to scale the respective width & height input values. The actual code looks as follows:
import 'dart:math';
import 'package:flutter/material.dart';
class McGyver {
static const double _fixedWidth = 410; // Set to an Aspect Ratio of 2:1 (height:width)
static const double _fixedHeight = 820; // Set to an Aspect Ratio of 2:1 (height:width)
// Useful rounding method (@andyw solution -> https://stackoverflow.com/questions/28419255/how-do-you-round-a-double-in-dart-to-a-given-degree-of-precision-after-the-decim/53500405#53500405)
static double roundToDecimals(double val, int decimalPlaces){
double mod = pow(10.0, decimalPlaces);
return ((val * mod).round().toDouble() / mod);
}
// The 'Ratio-Scaled' Widget method (takes any generic widget and returns a "Ratio-Scaled Widget" - "rsWidget")
static Widget rsWidget(BuildContext ctx, Widget inWidget, double percWidth, double percHeight) {
// ---------------------------------------------------------------------------------------------- //
// INFO: Ratio-Scaled "SizedBox" Widget - Scaling based on device's height & width at 2:1 ratio. //
// ---------------------------------------------------------------------------------------------- //
final int _decPlaces = 5;
final double _fixedWidth = McGyver._fixedWidth;
final double _fixedHeight = McGyver._fixedHeight;
Size _scrnSize = MediaQuery.of(ctx).size; // Extracts Device Screen Parameters.
double _scrnWidth = _scrnSize.width.floorToDouble(); // Extracts Device Screen maximum width.
double _scrnHeight = _scrnSize.height.floorToDouble(); // Extracts Device Screen maximum height.
double _rsWidth = 0;
if (_scrnWidth == _fixedWidth) { // If input width matches fixedWidth then do normal scaling.
_rsWidth = McGyver.roundToDecimals((_scrnWidth * (percWidth / 100)), _decPlaces);
} else { // If input width !match fixedWidth then do adjustment factor scaling.
double _scaleRatioWidth = McGyver.roundToDecimals((_scrnWidth / _fixedWidth), _decPlaces);
double _scalerWidth = ((percWidth + log(percWidth + 1)) * pow(1, _scaleRatioWidth)) / 100;
_rsWidth = McGyver.roundToDecimals((_scrnWidth * _scalerWidth), _decPlaces);
}
double _rsHeight = 0;
if (_scrnHeight == _fixedHeight) { // If input height matches fixedHeight then do normal scaling.
_rsHeight = McGyver.roundToDecimals((_scrnHeight * (percHeight / 100)), _decPlaces);
} else { // If input height !match fixedHeight then do adjustment factor scaling.
double _scaleRatioHeight = McGyver.roundToDecimals((_scrnHeight / _fixedHeight), _decPlaces);
double _scalerHeight = ((percHeight + log(percHeight + 1)) * pow(1, _scaleRatioHeight)) / 100;
_rsHeight = McGyver.roundToDecimals((_scrnHeight * _scalerHeight), _decPlaces);
}
// Finally, hand over Ratio-Scaled "SizedBox" widget to method call.
return SizedBox(
width: _rsWidth,
height: _rsHeight,
child: inWidget,
);
}
}
... ... ...
Then you would individually scale your widgets (which for my perfectionist disease is ALL of my UI) with a simple static call to the "rsWidget()" method as follows:
// Step 1: Define your widget however you like (this widget will be supplied as the "inWidget" arg to the "rsWidget" method in Step 2)...
Widget _btnLogin = RaisedButton(color: Colors.blue, elevation: 9.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(McGyver.rsDouble(context, ScaleType.width, 2.5))),
child: McGyver.rsText(context, "LOGIN", percFontSize: EzdFonts.button2_5, textColor: Colors.white, fWeight: FontWeight.bold),
onPressed: () { _onTapBtnLogin(_tecUsrId.text, _tecUsrPass.text); }, );
// Step 2: Scale your widget by calling the static "rsWidget" method...
McGyver.rsWidget(context, _btnLogin, 34.5, 10.0) // ...and Bob's your uncle!!
The cool thing is that the "rsWidget()" method returns a widget!! So you can either assign the scaled widget to another variable like _rsBtnLogin
for use all over the place - or you could simply use the full McGyver.rsWidget()
method call in-place inside your build()
method (exactly how you need it to be positioned in the widget tree) and it will work perfectly as it should.
For those more astute coders: you will have noticed that I used two additional ratio-scaled methods McGyver.rsText()
and McGyver.rsDouble()
(not defined in the code above) in my RaisedButton()
- so I basically go crazy with this scaling stuff...because I demand my apps to be absolutely pixel perfect at any scale or screen density!! I ratio-scale my ints, doubles, padding, text (everything that requires UI consistency across devices). I scale my texts based on width only, but specify which axis to use for all other scaling (as was done with the ScaleType.width
enum used for the McGyver.rsDouble()
call in the code example above).
I know this is crazy - and is a lot of work to do on the main thread - but I am hoping somebody will see my attempt here and help me find a better (more light-weight) solution to my screen density 1:1 scaling nightmares.