Search code examples
fluttergesturedetectorflutter-theme

Resizing Text by Theme in Flutter


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),
          ),
        );
      }
    }