In SwiftUI, is there a simple way to size the children with non-fixed size as a factor of remaining space available? (Flex Factor) - layout

Here's what I need to do: I have a parent view with a fixed width and 4 children. 2 of the children (1st and 3rd) have fixed width. The others (2nd and 4th) have flexible width. The default interpretation for flexible is that they would share equally the space available, but I don't want this, instead, I want to tell each flexible View how much of the remaining space it should take, for instance, the first should take 2/3 and the second 1/3.
For people familiar with the Flex Layout, this is equivalent to say I want my 2nd View to have flex: 2 and my 4th view to have flex: 1. See the example below:
In the example above, the parent view has the fixed width of 600px. The first child has 100px width, the third has 140px. The remaining space is 360px, so the second child gets 240px (2/3) and the fourth child gets 120px(1/3). If I change the width of the parent view, the size of the second and fourth children will adapt accordingly.
It is extremely simple to do this for the Web, see the code below:
<div style="width: 600; display: flex; flex-direction: row;">
<div style="width: 100; height: 100; background-color: red;"></div>
<div style="flex: 2; height: 100; background-color: green;"></div>
<div style="width: 140; height: 100; background-color: blue;"></div>
<div style="flex: 1; height: 100; background-color: purple;"></div>
</div>
It is also super-simple to do it in Flutter:
Row(
children: [
Container(height: 50, width: 50, color: Colors.red),
Flexible(flex: 2, child: Container(height: 50, color: Colors.green)),
Container(height: 50, width: 80, color: Colors.blue),
Flexible(flex: 1, child: Container(height: 50, color: Colors.purple)),
]
)
It's very similar in Jetpack Compose (Android):
Row {
Row(modifier = Modifier.height(50.dp).width(50.dp).background(color = Color.Red)) {}
Row(modifier = Modifier.height(50.dp).weight(2F).background(color = Color.Green)) {}
Row(modifier = Modifier.height(50.dp).width(80.dp).background(color = Color.Blue)) {}
Row(modifier = Modifier.height(50.dp).weight(1F).background(color = Color.Black)) {}
}
I tried a lot of things, but I couldn't find any simple way of achieving this behavior in SwiftUI. From my understanding, to size the children, SwiftUI first calculates the size of every child that can have its size calculated, i.e. every child that has a fixed size, after that it calculates the remaining space and divide it by the number of children with an undefined size telling each of them this is the maximum size they can occupy (this is the size we read when using a GeometryReader). Is there a way to tell SwiftUI to not divide the space available equally among the children with undefined size, but instead divide it according to a factor specified by each one of them?
Is there a different approach to this in a way I can obtain the same result as the other platforms I mentioned here?
For now, my strategy is much more complex than I'd like it to be. I render the first child with width = 50, the second child with width = 0 (should be flexible), the third child with width = 80, the fourth child with width = 0 (should be flexible) and an additional 5th child with width = .infinity. After the first render, I measure the size of the 5th child and update a state with this value, which represents the remaining space after the children with fixed size have been placed. After the state is updated, I can calculate the sizes of the flexible children and render everything a second time, now with the correct sizes. See the code below:
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
struct ContentView: View {
private let totalFlexFactor = 3
#State private var freeSpace: CGSize? = nil
func getFlexibleWidth(parentWidth: CGFloat, factor: Int) -> CGFloat {
return (CGFloat(factor) / CGFloat(totalFlexFactor)) * (freeSpace?.width ?? 0)
}
var body: some View {
GeometryReader { parentGeometry in
HStack(spacing: 0) {
HStack() {}
.frame(maxWidth: 50, maxHeight: .infinity)
.background(Color.red)
HStack() {}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 2), maxHeight: .infinity)
.background(Color.green)
HStack() {}
.frame(maxWidth: 80, maxHeight: .infinity)
.background(Color.blue)
HStack() {}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 1), maxHeight: .infinity)
.background(Color.purple)
if (freeSpace == nil) {
HStack() {}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
}
}
.frame(maxWidth: .infinity, maxHeight: 50)
.background(Color.gray)
.onPreferenceChange(SizePreferenceKey.self) { newSize in
if (freeSpace == nil) {
freeSpace = newSize
}
}
}
}
}
Credits to Federico Zanetello for the code that measures the size of the children.
As I said before, I don't see this as a very elegant solution, mainly if we compare it to the other platforms. I'd appreciate any suggestion! Just keep in mind the example I gave: fixed sized views intercalated with flexibly sized views.
Edit: I'm trying to build a component called Flexbox. In my example, I set the widths of each view, but it's just for simplicity. In fact, inside the Flexbox component, I don't know the size of any of the children, that's why, in my solution, I had to measure them.
Example: I might call:
Flexbox {
ViewWith100Width()
Flexible(flex: 2) { ViewWithFlexibleSize() }
ViewWith80Width()
Flexible(flex: 1) { AnotherViewWithFlexibleSize() }
}
But I could also call:
Flexbox {
ViewWith40Width()
Flexible(flex: 2) { ViewWithFlexibleSize() }
ViewWith60Width()
Flexible(flex: 1) { AnotherViewWithFlexibleSize() }
}
The component Flexbox must be agnostic of the size of whichever child is passed to it. That's why I had to measure each non-flexible child.
I haven't mentioned this in the original formulation of the question because I didn't want to overcomplicate it, since I just want to know if there's an easier way to simulate the Flexbox behavior of web/flutter/compose in swiftui.

While I think PreferenceKeys are a great tool to have and use, this is not a case where they are needed. Your plan was overly complicated. This comes down to a simple math problem, but first there is a coding issue:
.frame(maxWidth: 50, maxHeight: .infinity)
gives you a flexible, not a fixed width. What you have told ViewBuilder to do is assign to the width of the frame any value up to 50, not a value of 50. I understand what you were doing, as you wanted to expand the height to the size of the view container. There are two alternatives:
.frame(minWidth: 50, maxWidth: 50, maxHeight: .infinity)
or
.frame(width: 50)
.frame(maxHeight: .infinity)
I chose the latter as it makes your intention very clear. Since they were fixed, I also assigned them to let constants to be used anywhere I needed them.
Also, while I did not test this, I suspect the PreferenceKey couldn't handle the disappearing/reappearing view. Since they were unnecessary, it didn't delve into it.
At this point, you can find your flexible space simply by parentGeometry less the two fixed widths. No PreferenceKeys needed. in the end, the code simplifies to:
struct ContentView: View {
private let totalFlexFactor: CGFloat = 3
#State private var freeSpace: CGSize? = nil
private let width1: CGFloat = 100
private let width2: CGFloat = 80
private func getFlexibleWidth(parentWidth: CGFloat, factor: CGFloat) -> CGFloat {
((parentWidth - (width1 + width2)) / totalFlexFactor) * factor
}
var body: some View {
GeometryReader { parentGeometry in
HStack(spacing: 0) {
HStack {
Text(width1.description)
}
.frame(width: width1)
.frame(maxHeight: .infinity)
.background(Color.red)
HStack {
Text(getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 2).description)
}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 2), maxHeight: .infinity)
.background(Color.green)
HStack {
Text(width2.description)
}
.frame(width: width2)
.frame(maxHeight: .infinity)
.background(Color.blue)
HStack {
Text(getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 1).description)
}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 1), maxHeight: .infinity)
.background(Color.purple)
}
.frame(maxWidth: .infinity, maxHeight: 50)
.background(Color.gray)
}
}
}
I also displayed the values for the widths of the various HStacks in the HStacks.
Also, with regard to SwiftUI syntax, it is not necessary to write HStack() unless you intend to use the optional values of alignment: and spacing:, otherwise we omit the ().

The main idea in to layout is where to use needed containers. Here (fixed-flex-fixed-flex) GeometryReader can be used only for three.
Tested with Xcode 13.3 / iOS 15.4
Main idea part is:
GeometryReader { gp in
HStack (spacing: 0) {
Color.green
.frame(width: (gp.size.width - fixed2) * 2/3)
Color.blue
.frame(width: fixed2)
Completed findings and report is here

Try .layoutPriority(1/0) to have grow and shrink behaviour
setting priority 1, lets view grow and shrink

Related

SwiftUI - Text minimumScaleFactor not scaling only when needed

I have a Text that I want to modify with a .minimumScaleFactor(0.1) when the length of its string extends outside the view. However, the scaling applies every time, even when the original font size would be perfectly fine.
My view is structured thusly:
VStack(alignment: .center, spacing: 0) {
HStack {
Image("medal \(place)").resizable()
.foregroundColor(color)
.frame(width: 40, height: 40)
Spacer()
Text(username)
.font(.bold(16))
.lineLimit(1)
.minimumScaleFactor(0.1)
.frame(alignment: .trailing)
.foregroundColor(Color("mediumTextColor"))
}
Spacer()
Text(score)
.font(.extraBold(60))
.foregroundColor(color)
.lineLimit(1)
.minimumScaleFactor(0.7)
Spacer()
}
.frame(height: 96)
.padding(10)
.cornerRadius(16)
.overlay(RoundedRectangle(cornerRadius: 16)
.stroke(color, lineWidth: 2))
A continuation of answer above.
What is important is the order in which the modifiers are applied:
Text("This is a really long sentence.")
.minimumScaleFactor(0.5)
.font(.custom("OpenSans", size: 15))
.lineLimit(1)
.layoutPriority(1)
There isn't a need for the GeometryReader unless you need it for something else
Adding .lineLimit(1) to the Text will work well.
Xcode: 11.3.1
Important for that minimumScaleFactor() only does its job when it is needed, is the combination of .minimumScaleFactor AND .lineLimit(1); otherwise it will not work. layoutPriority(1) is not the key here.
Example - this works; Scale is only applied when needed:
Text("Something").minimumScaleFactor(0.5).lineLimit(1)
Adding a GeometryReader around the VStack that my two Text views were in solved this. As requested by #gohnjanotis:
GeometryReader { proxy in
VStack(alignment: .center, spacing: 0) {
HStack {
Image("medal \(self.place)").resizable()
.foregroundColor(self.color)
.frame(width: 40, height: 40)
Spacer()
Text(self.username)
.minimumScaleFactor(0.1)
.font(.bold(16))
.lineLimit(1)
.frame(alignment: .trailing)
.foregroundColor(Color("mediumTextColor"))
.layoutPriority(1)
}
.padding(.top, 10)
.padding(.horizontal, 10)
Spacer()
Text(self.score)
.font(.extraBold(60))
.foregroundColor(self.color)
.lineLimit(1)
.minimumScaleFactor(0.1)
.layoutPriority(1)
.padding(.horizontal, 10)
.padding(.bottom, 10)
.offset(y: -10)
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .top)
}
.frame(maxHeight: 130)
Add .layoutPriority(1) modifier to your Text to make sure it takes the space if there is any.
After trying to figuring this out for a long while... playing with minimumScaleFactor, LineLimit, layoutPriority... I found out that using .frame() helps solve my problem.
My problem is that minimumScaleFactor applies to Text even if it is not needed.
Applying .frame() ad the last line solve my problem.
Something like this...
GeometryReader { geometry in
VStack{
Text("Hello")
.foregroundColor(Color.Black)
.font(.system(size: 50))
.minimumScaleFactor(0.1)
.lineLimit(1)
.frame(height: geometry.size.height)
}
}

How to create responsive Text widget in Flutter?

I'm facing a problem with responsive texts. In my app are different text font sizes and I need to do them responsive for different screen sizes (only phones and device orientation portrait). I also added textScaleFactor: 1.0 to my MaterialApp like this:
builder: (context, widget) {
return MediaQuery(
child: widget,
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
);
},
but it does not help much.
I tried to also calculate the font size with MediaQuery.of(context).size.width, but I think it dangerous and wrong. I want it to be as close as possible to the design that was given to me, but I started to lose it on these steps.
How can I solve this?
you can use this plugin flutter_screenutil.
It is a flutter plugin for adapting screen and font size.Let your UI display a reasonable layout on different screen sizes!
Initialize and set the fit size and font size to scale according to the system's "font size" accessibility option #
Please set the width and height of the design draft before use, the width and height of the design draft (unit px). Be sure to set the page in the MaterialApp's home(ie the entry file, just set it once) to ensure that the fit size is set before each use:
//fill in the screen size of the device in the design
//default value : width : 1080px , height:1920px ,
allowFontScaling:false
ScreenUtil.instance = ScreenUtil.getInstance()..init(context);
//If the design is based on the size of the iPhone6 ​​(iPhone6 ​​750*1334)
ScreenUtil.instance = ScreenUtil(width: 750, height:
1334)..init(context);
//If you wang to set the font size is scaled according to the system's
"font size" assist option
ScreenUtil.instance = ScreenUtil(width: 750, height: 1334,
allowFontScaling: true)..init(context);
Use: #
Adapt screen size: #
Pass the px size of the design draft:
Adapted to screen width: ScreenUtil.getInstance().setWidth(540),
Adapted to screen height: ScreenUtil.getInstance().setHeight(200),
You can also use ScreenUtil() instead of ScreenUtil.getInstance(), for example:ScreenUtil().setHeight(200)
Note
Height is also adapted according to setWidth to ensure no deformation (when you want a square)
setHeight method is mainly adapted in height, you want to control the height and actuality of a screen on the UIUsed when the same is displayed.
//for example:
//rectangle
Container(
width: ScreenUtil.getInstance().setWidth(375),
height: ScreenUtil.getInstance().setHeight(200),
...
),
////If you want to display a square:
Container(
width: ScreenUtil.getInstance().setWidth(300),
height: ScreenUtil.getInstance().setWidth(300),
),
Adapter font:
//Incoming font size,the unit is pixel, fonts will not scale to
respect Text Size accessibility settings
//(AllowallowFontScaling when initializing ScreenUtil)
ScreenUtil.getInstance().setSp(28)
//Incoming font size,the unit is pixel,fonts will scale to respect Text
Size accessibility settings
//(If somewhere does not follow the global allowFontScaling setting)
ScreenUtil(allowFontScaling: true).setSp(28)
//for example:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'My font size is 24px on the design draft and will not change with the system.',
style: TextStyle(
color: Colors.black,
fontSize: ScreenUtil.getInstance().setSp(24),
)),
Text(
'My font size is 24px on the design draft and will change with the system.',
style: TextStyle(
color: Colors.black,
fontSize: ScreenUtil(allowFontScaling: true).setSp(24),
)),
],
)
Other related apis:
ScreenUtil.pixelRatio //Device pixel density
ScreenUtil.screenWidth //Device width
ScreenUtil.screenHeight //Device height
ScreenUtil.bottomBarHeight //Bottom safe zone distance, suitable for buttons with full screen
ScreenUtil.statusBarHeight //Status bar height , Notch will be higher Unit px
ScreenUtil.textScaleFactory //System font scaling factor
ScreenUtil.getInstance().scaleWidth //Ratio of actual width dp to design draft px
ScreenUtil.getInstance().scaleHeight //Ratio of actual height dp to design draft px
I faced the same problem while developing a responsive app for web and mobile, but textScaleFactor helped to solve my problem, default textScaleFactor is 1 so I wrote a method to change textScaleFactor according to the width of the screen
class ScaleSize {
static double textScaleFactor(BuildContext context, {double maxTextScaleFactor = 2}) {
final width = MediaQuery.of(context).size.width;
double val = (width / 1400) * maxTextScaleFactor;
return max(1, min(val, maxTextScaleFactor));
}
}
We can control the size version according to its scale,
Use in a Text widget
Text(
intro.subtitle,
style: Theme.of(context).textTheme.subtitle1,
textAlign: TextAlign.center,
textScaleFactor: ScaleSize.textScaleFactor(context),
),
class SizeConfig {
static MediaQueryData _mediaQueryData;
static double screenWidth;
static double screenHeight;
static double blockSizeHorizontal;
static double blockSizeVertical;
void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
blockSizeHorizontal = screenWidth / 100;
blockSizeVertical = screenHeight / 100;
}
}
SizeConfig().init(context); add this after widget build and use
style: TextStyle(fontSize: 2 * SizeConfig.blockSizeVertical,),
Multiply with your desired number, try at least one time. I have attached screen shots.
My solution:
Other Solutions:
try this: you get the adaptive text size according to different screen sizes
class AdaptiveTextSize {
const AdaptiveTextSize();
getadaptiveTextSize(BuildContext context, dynamic value) {
// 720 is medium screen height
return (value / 720) * MediaQuery.of(context).size.height;
}
}
use case :
Text("Paras Arora",style: TextStyle(fontSize:
AdaptiveTextSize().getadaptiveTextSize(context, 20)),
You can try this:
final size = MediaQuery.of(context).size;
You could apply the same concept, in this case for the container:
Container(
width: size.width * 0.85,
...
)
You can get the constraints from LayoutBuilder and pass that to the ScreenUtil.init() as shown in the following code.
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
ScreenUtil.init(
constraints,
designSize: orientation == Orientation.portrait
? (Platform.isAndroid || Platform.isIOS) ? Size(450.0, 870.0) :
Size(705.0, 1366.0)
: (Platform.isAndroid || Platform.isIOS) ? Size(870.0, 450.0) :
Size(1366.0, 705.0),
allowFontScaling: false,
);
return MaterialApp(
theme: ThemeData(
textTheme: Theme.of(context).textTheme.copyWith(
headline6: TextStyle(
fontSize: 24.sp,
),
),
),
home: HomeScreen(),
);
},
);
},
);
We can check orientation == Orientation.portrait to set width & height of the screen in which we are designing. To support both orientations just inverse the width & height values accordingly.
You can also check Platform.isAndroid || Platform.isIOS & give the width & height of mobile devices.
I would like to answer to my question. In past months I was using flutter_screenutil
But as I mentioned in the comments, I need to add responsive fonts sizes in the theme, so I customized the flutter_screenutil package to use it in there. I think it is working perfect. I already used this solution in couple of projects and have not any trouble.
import 'package:flutter/material.dart';
class CustomScreenUtil {
static CustomScreenUtil _instance;
static const int defaultWidth = 1080;
static const int defaultHeight = 1920;
/// Size of the phone in UI Design , px
num uiWidthPx;
num uiHeightPx;
/// allowFontScaling Specifies whether fonts should scale to respect Text Size accessibility settings. The default is false.
bool allowFontScaling;
static double _screenWidth;
static double _screenHeight;
static double _pixelRatio;
static double _statusBarHeight;
static double _bottomBarHeight;
static double _textScaleFactor;
CustomScreenUtil._();
factory CustomScreenUtil() {
return _instance;
}
static void init({num width = defaultWidth,
num height = defaultHeight,
bool allowFontScaling = false}) {
if (_instance == null) {
_instance = CustomScreenUtil._();
}
_instance.uiWidthPx = width;
_instance.uiHeightPx = height;
_instance.allowFontScaling = allowFontScaling;
_pixelRatio = WidgetsBinding.instance.window.devicePixelRatio;
_screenWidth = WidgetsBinding.instance.window.physicalSize.width;
_screenHeight = WidgetsBinding.instance.window.physicalSize.height;
_statusBarHeight = WidgetsBinding.instance.window.padding.top;
_bottomBarHeight = WidgetsBinding.instance.window.padding.bottom;
_textScaleFactor = WidgetsBinding.instance.window.textScaleFactor;
}
/// The number of font pixels for each logical pixel.
static double get textScaleFactor => _textScaleFactor;
/// The size of the media in logical pixels (e.g, the size of the screen).
static double get pixelRatio => _pixelRatio;
/// The horizontal extent of this size.
static double get screenWidthDp => _screenWidth;
///The vertical extent of this size. dp
static double get screenHeightDp => _screenHeight;
/// The vertical extent of this size. px
static double get screenWidth => _screenWidth * _pixelRatio;
/// The vertical extent of this size. px
static double get screenHeight => _screenHeight * _pixelRatio;
/// The offset from the top
static double get statusBarHeight => _statusBarHeight;
/// The offset from the bottom.
static double get bottomBarHeight => _bottomBarHeight;
/// The ratio of the actual dp to the design draft px
double get scaleWidth => _screenWidth / uiWidthPx;
double get scaleHeight => _screenHeight / uiHeightPx;
double get scaleText => scaleWidth;
/// Adapted to the device width of the UI Design.
/// Height can also be adapted according to this to ensure no deformation ,
/// if you want a square
num setWidth(num width) => width * scaleWidth;
/// Highly adaptable to the device according to UI Design
/// It is recommended to use this method to achieve a high degree of adaptation
/// when it is found that one screen in the UI design
/// does not match the current style effect, or if there is a difference in shape.
num setHeight(num height) => height * scaleHeight;
///Font size adaptation method
///#param [fontSize] The size of the font on the UI design, in px.
///#param [allowFontScaling]
num setSp(num fontSize, {bool allowFontScalingSelf}) =>
allowFontScalingSelf == null
? (allowFontScaling
? (fontSize * scaleText)
: ((fontSize * scaleText) / _textScaleFactor))
: (allowFontScalingSelf
? (fontSize * scaleText)
: ((fontSize * scaleText) / _textScaleFactor));
}
Now screen util using his sizes from WidgetsBinding.instance.window not from MediaQuery and now we can use it without context like this:
_screenUtil = CustomScreenUtil();
ThemeData(
primaryTextTheme: TextTheme(
bodyText1: TextStyle(
fontSize: _screenUtil.setSp(12),
)
))
I don't know if this is the most solution but I'm working like this
For the Font Size Similar to phone size of app :
MaterialApp( home: MediaQuery(data:MediaQuery.of(context).copyWith(textScaleFactor: 1.2), child: HomePage()) );
For ListTile_Subtitle :
Text('Your Text' , textScaleFactor:1.0)
NOTE : Here , 1.2 in normal text = 1.2 x (the_FontSize_of_device)
& for ListTile_Subtitle , we need smaller than normal font size , so , we control
it with Text Widget textScaleFactor argument , which indicates here :
1x(the_FontSize_of_device),
Similar if You need Big text than normal font size of device , than ,
Text('aaaa' , textScaleFactor:2)
1. First Solution
You can use auto_size_text package.
2. Second Solution
A work around without the package and if you prefer using a custom theme:
get the width
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Container(...
create your custom ThemeData i.e for light and dark in you theme_provider class
static ThemeData dark() {
return ThemeData(
textTheme: const TextTheme(
bodyText2:
TextStyle(color: Colors.white70, fontWeight: FontWeight.w300)...
Finally, edit your Text widget with different font sizes for different screens. You can use screen height as well...
Text(
widget.featuredModel.eventTitle,
style: Theme.of(context).textTheme.bodyText2!
.copyWith( fontSize: width >= 1201
? 22
: width >= 601
? 20
: width >= 400
? 16
: 14),
),
LayoutBuilder(
builder:(context,constraints){
return Text("this is responsive text",
style:TextStyle
(fontSize:constraints.maxWidth*the percentage of your text));
//("this is how to calculate // percentage of fontsize"
// e.g "fontSize/total Width*100" then for example i recived the percentage on //
// calculator"3.45" then multiply the maxWidth with 0.0345)
}
);
)
Do this!
const Expanded(
child: Text(
"Your text.",
overflow: TextOverflow.visible,
maxLines: 5,
softWrap: true,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.normal,
),
textAlign: TextAlign.start,
),
),

Finding the exact bounds of text

I need to know the exact bounds a piece of text -- the equivalent of getTextBounds for Android. I realize this goes somewhat counter to Flutter's design, but I am using text in a non-traditional way (as if the text were, say, embedded into an artistic picture at a precise location and size).
I've tried three methods:
TextPainter's minIntrinsicWidth and height. Source below. This produces a bounding box with space on all sides:
I need the sides of that rectangle to be right up against the black pixels of the '8'. (In this particular case, width and maxIntrinsicWidth give the same value as minIntrinsicWidth; also, preferredLineHeight gives the same value as height.)
Paragraph's getBoxesForRange -- basically the same result as with TextPainter.
FittedBox -- also leaves spaces, no matter the BoxFit enum value. I used this answer as a starting point.
TextPainter code follows. (I realize this is inefficient; I don't care until I get the needed bounding box. I used Scene/Picture/etc in hopes of finding appropriate functionality in the lower levels.)
import 'dart:ui';
import 'dart:typed_data';
import 'package:flutter/painting.dart';
const black = Color(0xff000000);
const white = Color(0xffffffff);
final identityTransform = new Float64List(16)
..[0] = 1.0
..[5] = 1.0
..[10] = 1.0
..[15] = 1.0;
TextPainter createTextPainter() {
return new TextPainter(
text: new TextSpan(
text: '8',
style: new TextStyle(
color: black,
fontSize: 200.0,
fontFamily: "Roboto",
),
),
textDirection: TextDirection.ltr,
)..layout();
}
void drawBackground(Canvas canvas, Rect screen) {
canvas.drawRect(
screen,
new Paint()
..color = white
..style = PaintingStyle.fill);
}
Picture createPicture() {
final recorder = new PictureRecorder();
final screen = Offset.zero & window.physicalSize;
final canvas = new Canvas(recorder, screen);
final offset = screen.center;
final painter = createTextPainter();
final bounds = offset & Size(painter.minIntrinsicWidth, painter.height);
drawBackground(canvas, screen);
painter.paint(canvas, offset);
canvas.drawRect(
bounds,
new Paint()
..color = black
..style = PaintingStyle.stroke
..strokeWidth = 3.0);
return recorder.endRecording();
}
Scene createScene() {
final builder = new SceneBuilder()
..pushTransform(identityTransform)
..addPicture(Offset.zero, createPicture())
..pop();
return builder.build();
}
void beginFrame(Duration timeStamp) {
window.render(createScene());
}
void main() {
window.onBeginFrame = beginFrame;
window.scheduleFrame();
}
To ignore that padding you have to make really low level calls as the Text and the space practically get painted together. I tried searching if there is any way other way out in on the Internet but couldn't find anything which could actually solve your issue.
I tried hunting through the source code of Flutter SDK (Widget_Library), but all I could see is that the final resultant widget is a RichText whose source code disappears into thin air. (I couldn't find that widget's definition as I just searched it on GitHub...In Android Studio you might get some reference to it by hovering over it and pressing Ctrl+LeftClick... But I am pretty sure that it will take you to another unknown world as fonts are painted based on their font family(which would be .ttf or .otf file...Either ways they'll be painted along with the padding your referring to).
All you can do for now is create our own character set widget which accepts a raw string and reads through each character and paints a number/character already defined by you using some custom paint function,
or if your ready to let go that padding to save some time(which might or might not be crucial for you right now), you either use some predefined function to get that along with the space or use the below solution to get the dimensions of any widget you want.
I need to know the exact bounds a piece of text...
The easiest way to know the width of the any widget is by using a GlobalKey
Declare and initialize a variable to store a GlobalKey,
GlobalKey textKey = GlobalKey();
Pass the GlobalKey to the key parameter of your Text widget,
Text(
"hello",
key: textKey,
)
Now I'll wrap the above widget with a GestureDetector to print the width of the above widget.
GestureDetector(
onTap: (){
print(textKey.currentContext.size.width); //This is your width
print(textKey.currentContext.size.height); //This is your height
},
child: Text(
"hello",
key: textKey,
),
)
As I mentioned earlier you can use it on any widget, so can you use it on TextSpan widget:
GestureDetector(
onTap: (){
print(textKey.currentContext.size.width); //This is your width
},
TextSpan(
key: textKey,
text: '8',
style: new TextStyle(
color: black,
fontSize: 200.0,
fontFamily: "Roboto",
),
),
)
Now with the help of GlobalKey() you can get all the information required about that piece of Text and paint your boundary/rectangle accordingly.
Note: GlobalKey() gives your widget a unique identity, so don't reuse it on multiple widgets.
==================
Edit:
If can somehow get the color of each and every pixel in your app/text, then there is a possibility of getting what your trying to achieve.
Note: You'll have to find the offset/padding of each side separately.
x1,y1 are the co-ordinates for the starting point of your container and x2,y2 are the ending points.
minL,minR,minT,minB are the offset(s) you were looking for.
Basic Logic: Scan each line vertically/horizontally based on the side taken under consideration, until you get a pixel whose color equals the text color and do this for all the imaginary lines until you get the minimum offset(distance of the text from the boundary) for all sides.
Imagine your drawing a line from the boundary of your Container until you meet one of the bounds of the text(/meet the text color) and you do that for all other set of (horizontal[Left/Right] or vertical[Top/Bottom])lines and you just keep the shortest distance in your mind.
// Left Side
for(i=y1+1;i<y2;i++)
for(j=x1+1;j<x2;j++)
if(colorof(j,i)==textcolor && minL>j) {minL = j; break;}
// Right Side
for(i=y1+1;i<y2;i++)
for(j=x2-1;j>x1;j--)
if(colorof(j,i)==textcolor && minR>j) {minR = j; break;}
// Top Side
for(i=x1+1;i<x2;i++)
for(j=y1;j<y2;j++)
if(colorof(i,j)==textcolor && minT>j) {minT = j; break;}
// Bottom Side
for(i=x1+1;i<x1;i++)
for(j=y2;j>y1;j--)
if(colorof(i,j)==textcolor && minB>j) {minB = j; break;}
x1 = x1 + minL;
y1 = y1 + minT;
x2 = x2 - minR;
y2 = y2 - minB;
While implementing the above logic ensure that there is no other overlapping/underlying widget(Scaffold/Stack Combo) whose color somehow matches the text color.
My way to find the smallest rectangle that completely encloses a single-line text:
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<ui.Rect>? _boundsRequest;
#override
Widget build(BuildContext context) {
const textSpan = TextSpan(
text: 'pixel',
style: TextStyle(
color: Colors.red,
fontSize: 300,
fontStyle: FontStyle.italic,
),
);
_boundsRequest ??= getTextBounds(text: textSpan, context: context);
return Padding(
padding: const EdgeInsets.only(left: 16),
child: FutureBuilder(
future: _boundsRequest,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final bounds = snapshot.data;
if (bounds != null) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: bounds.top,
left: bounds.left,
child: Container(
width: bounds.width,
height: bounds.height,
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
strokeAlign: StrokeAlign.outside),
),
),
),
const Text.rich(textSpan),
],
);
}
}
return Container();
},
),
);
}
}
Future<Rect> getTextBounds({
required TextSpan text,
required BuildContext context,
ui.TextDirection textDirection = ui.TextDirection.ltr,
}) async {
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
final textScaleFactor = MediaQuery.of(context).textScaleFactor;
final painter = TextPainter(
text: text,
textAlign: ui.TextAlign.center,
textScaleFactor: textScaleFactor,
textDirection: textDirection,
);
painter.layout();
final extraWidth = 2 * painter.height.toInt();
final width = painter.width.toInt() + extraWidth;
final height = painter.height.toInt();
final shift = extraWidth / 2; // Create extra space to the left
painter.paint(canvas, ui.Offset(shift, 0));
final ui.Image image = await recorder.endRecording().toImage(width, height);
final bounds = await _getImageBounds(image);
return ui.Rect.fromLTWH(
bounds.left - shift,
bounds.top,
bounds.width,
bounds.height,
);
}
Future<ui.Rect> _getImageBounds(ui.Image image) async {
final data = await image.toByteData();
if (data != null) {
final list = data.buffer.asUint32List();
return _getBufferBounds(list, image.width, image.height);
}
return Rect.zero;
}
// https://pub.dev/documentation/image/latest/image/findTrim.html
ui.Rect _getBufferBounds(
List<int> list,
int width,
int height,
) {
int getPixel(int x, int y) => list[y * width + x];
var left = width;
var right = 0;
int? top;
var bottom = 0;
for (int y = 0; y < height; ++y) {
var first = true;
for (int x = 0; x < width; ++x) {
// 8 bits alpha chanel threshold (0-254):
const threshold = 64;
if (getPixel(x, y) >>> 24 > threshold) {
if (x < left) {
left = x;
}
if (x > right) {
right = x;
}
top ??= y;
bottom = y;
if (first) {
first = false;
x = right;
}
}
}
}
if (top == null) {
return ui.Rect.fromLTWH(
0,
0,
width.toDouble(),
height.toDouble(),
);
}
return ui.Rect.fromLTRB(
left.toDouble(),
top.toDouble(),
right.toDouble() + 1,
bottom.toDouble() + 1,
);
}
I use TextPainter to draw the text into a ui.Image. Then I search for the transparent pixels to calculate the bounds.
The bounding rectangle can also have negative values.
(Caution) Documentation on TextPainter.width: The horizontal space required to paint this text. Not quite right: With many fonts and italics, the space is exceeded both to the left and to the right. Therefore, I reserve some extra space for the width of the temporary image.
(Caution) The rectangle is only correct if the text itself can determine how much space it takes up. But if there is too little space, the text may be scaled down or wrapped into multiple lines.
You can use the TextPainter class to determine the width of some text.
https://api.flutter.dev/flutter/painting/TextPainter-class.html Flutter uses that during layout to determine the size of a text widget.
Not sure if the result from TextPainter will include a small amount of padding or not, though if it does, the already proposed solution will likely do so too.

FabricJS iText Width And Height

Is there a way to set the width and height of an iText object? When creating the object if you set width and height it doesn't do anything. What I am after is the bounding box to be a fixed size on the canvas as an editable region, I have already extended the iText class to allow for individual character editing but I can't seem to work out how to size the box as a fixed size and allow content editing inside of it. Bare in mind that the text box can't be moved or scaled, it's static.
It's Too late for answer but other may be looking for answer.
First of all You can use TextBox class of fabricjs if you want to give fixed width & height to Text field. It is Subclass of Itext. But the problem with TextBox was it's text wrap.
So If you don't want to use TextBox but still want to have fixed width and height with Itext then following extended subclass can help you.
I have overridden the initDimensions method .
this.LimitedTextbox = fabric.util.createClass(fabric.IText, {
initDimensions: function() {
this.isEditing && this.initDelayedCursor();
this.clearContextTop();
if (this.__skipDimension) {
return;
}
this._splitText();
this._clearCache();
if (this.path) {
this.width = this.path.width;
this.height = this.path.height;
}
else {
let width = this.calcTextWidth();
if(width>this.maxWidth){
this.width = width;
}else{
this.width = this.maxWidth;
}
this.height = this.calcTextHeight();
}
if (this.textAlign.indexOf('justify') !== -1) {
this.enlargeSpaces();
}
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
onKeyDown:function(e){
if (e.keyCode == 13 && this._textLines.length>=this.maxLines) {
this.exitEditing();
}
this.callSuper('onKeyDown',e);
let maxLine = Math.max(...this.__lineWidths);
self.changeBorderWidth(this.toObject().groupId, this, { iWidth:
this.width, iHeight: this.height,maxLine:maxLine,id:this.toObject().id
},false,this.toObject().customType==='stamp_input');
if(this.dynamicMinWidth>this.maxWidth){
this.width = this.dynamicMinWidth;
}
},
});
Then you can use like below
const text = new this.LimitedTextbox(txt, {
left: 100,
top: 100,
fontFamily: "arial",
angle: 0,
fontSize: 24,
fontWeight: "bold",
hasControls: true,
hoverCursor: "text",
width:250,
maxWidth: 250,
maxLines: 3,
hasRotatingPoint: true,
});
I Hope it helps , you can modify initDimensions as per your requirement , you can also see other methods in fabric's documemtation.

PaintCode 2 vs 3 incompatible due to resizableImageWithCapInsets

I recently switched from using PaintCode 2 to PaintCode 3, I am using it together with xCode/Swift.
I noticed however, that all my image generating functions not behave differently. They seam to standard addopt cap insets.
As an example below you can find one canvas "ViewMissingImage", and how its configured in PaintCode (2 or 3 its identical).
Code generated via PaintCode 2
public class func imageOfViewMissingImage(frame frame: CGRect = CGRect(x: 6, y: 5, width: 109, height: 109)) -> UIImage {
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
PaintCode.drawViewMissingImage(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
let imageOfViewMissingImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageOfViewMissingImage
}
Code generated via PaintCode 3
public dynamic class func imageOfViewMissingImage(imageSize imageSize: CGSize = CGSize(width: 109, height: 109)) -> UIImage {
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
PaintCode.drawViewMissingImage(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
let imageOfViewMissingImage = UIGraphicsGetImageFromCurrentImageContext()!.resizableImageWithCapInsets(UIEdgeInsetsZero, resizingMode: .Tile)
UIGraphicsEndImageContext()
return imageOfViewMissingImage
}
I think that the PaintCode 2 never used the capp insets, maybe it was a bug?
I do not want these cap insets, how can I get rid of them?
The solution is straightforward:
Put the Cap Inset on "Stretch" instead of tile in PaintCode UI!

Resources