Resizing Text by Theme in Flutter

Issue

I’m trying to use a GestureDetector to allow the user to change the font size by pinching:

class _PinchToScaleFontState extends State<PinchToScaleFont> {
  double _baseFontScale = 1;
  double _fontScale = 1;
  ThemeData _themeData;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Theme(
        data: _themeData, 
        child: widget.child       // The desired outcome is that all Text is resized
      ),
      onScaleStart: (ScaleStartDetails scaleStartDetails) {
        _baseFontScale = _fontScale;
      },
      onScaleUpdate: (ScaleUpdateDetails scaleUpdateDetails) {
        // don't update the UI if the scale didn't change
        if (scaleUpdateDetails.scale == 1.0) {
          return;
        }
        setState(() {
          double fontScale = (_baseFontScale * scaleUpdateDetails.scale).clamp(0.5, 5.0);
          _updateFontScale(fontScale);
          SharedPreferences.getInstance().then((prefs) => prefs.setDouble('fontScale', fontScale));
        });
      },
    );
  }

I can get the following code to adjust the scale of a TextField, but it won’t resize any Text widgets.

  _updateFontScale(double fontScale) {
    setState(() {
      _fontScale = fontScale;
      ThemeData theme = Theme.of(context);

      /// This doesn't seem to work at all
      // _themeData = theme.copyWith(textTheme: theme.textTheme.merge(TextTheme(bodyText2: TextStyle(fontSize: 14 * fontScale))));

      /// This works for `TextField` but not `Text`
      _themeData = theme.copyWith(textTheme: theme.textTheme.apply(fontSizeFactor: fontScale)); // merge(TextTheme()));
    });
    // }
  }

It’s strange. In the code below I can use the saved fontScale to initialise the font size for the whole app the next time it’s loaded, but why won’t the code above, which seems to be accessing the same theme property give the same results?

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  double savedFontScale = (await SharedPreferences.getInstance()).getDouble('fontScale') ?? 1.0;

  runApp(MyApp(savedFontScale));
}

class MyApp extends StatelessWidget {
  final double fontScale;

  MyApp(this.fontScale);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: APP_NAME,
      theme: ThemeData(
        textTheme: TextTheme(
          /// This works for all `Text` widgets - but you've got to restart the app
          bodyText2: TextStyle(fontSize: 14 * fontScale),
      ...
      home: 
         ...
         PinchToScaleFont(
             ...
             TextField('This _will_ resize 😀'),
             Text('This will not resize, but it should 😀'),

Solution

You can copy paste run full code below
Because textScaleFactor of Text reference MediaQueryData.textScaleFactor
source code of Text.dart https://github.com/flutter/flutter/blob/97295dc9a885c995cda99ba9cee421d3ab1a8e2d/packages/flutter/lib/src/widgets/text.dart#L479

  /// The value given to the constructor as textScaleFactor. If null, will
  /// use the [MediaQueryData.textScaleFactor] obtained from the ambient
  /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
  final double? textScaleFactor;

You can wrap widget.child with MediaQuery
and set mediaQueryData.copyWith(textScaleFactor: fontScale)
code snippet

  MediaQueryData _mediaQueryData;

  _updateFontScale(double fontScale) {
    setState(() {
      _fontScale = fontScale;
      ThemeData theme = Theme.of(context);
      MediaQueryData mediaQueryData = MediaQuery.of(context);
      ...

      _mediaQueryData = mediaQueryData.copyWith(textScaleFactor: fontScale);
    });
  ...   

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Theme(
          data: _themeData,
          child: MediaQuery(data: _mediaQueryData, child: widget.child)          
          ),

working demo

enter image description here

full code

import 'package:flutter/material.dart';

class PinchToScaleFont extends StatefulWidget {
  final Widget child;

  const PinchToScaleFont({Key key, this.child}) : super(key: key);
  @override
  _PinchToScaleFontState createState() => _PinchToScaleFontState();
}

class _PinchToScaleFontState extends State<PinchToScaleFont> {
  double _baseFontScale = 1;
  double _fontScale = 1;
  ThemeData _themeData;
  MediaQueryData _mediaQueryData;

  _updateFontScale(double fontScale) {
    setState(() {
      _fontScale = fontScale;
      ThemeData theme = Theme.of(context);
      MediaQueryData mediaQueryData = MediaQuery.of(context);

      /// This doesn't seem to work at all
      // _themeData = theme.copyWith(textTheme: theme.textTheme.merge(TextTheme(bodyText2: TextStyle(fontSize: 14 * fontScale))));

      /// This works for `TextField` but not `Text`
      _themeData = theme.copyWith(
          textTheme: theme.textTheme
              .apply(fontSizeFactor: fontScale)); // merge(TextTheme()));

      _mediaQueryData = mediaQueryData.copyWith(textScaleFactor: fontScale);
    });
    // }
  }

  @override
  void didChangeDependencies() {
    _themeData = Theme.of(context);
    _mediaQueryData = MediaQuery.of(context);
    super.didChangeDependencies();
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Theme(
          data: _themeData,
          child: MediaQuery(data: _mediaQueryData, child: widget.child)
          // The desired outcome is that all Text is resized
          ),
      onScaleStart: (ScaleStartDetails scaleStartDetails) {
        _baseFontScale = _fontScale;
      },
      onScaleUpdate: (ScaleUpdateDetails scaleUpdateDetails) {
        // don't update the UI if the scale didn't change
        if (scaleUpdateDetails.scale == 1.0) {
          return;
        }
        setState(() {
          double fontScale =
              (_baseFontScale * scaleUpdateDetails.scale).clamp(0.5, 5.0);
          _updateFontScale(fontScale);
          //SharedPreferences.getInstance().then((prefs) => prefs.setDouble('fontScale', fontScale));
        });
      },
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            PinchToScaleFont(
                child: Column(
              children: [
                TextField(),
                Text('This will not resize, but it should 😀'),
              ],
            )),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Answered By – chunhunghan

Answer Checked By – Marie Seifert (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *