tts autoPlay

    Future<void> _autoPlay() async {
        _isAuto = true;
        _autoSpeak();

        await showDialog (
            context: context,
            barrierDismissible: false,
            builder: (BuildContext context) {
                return SimpleDialog(
                    contentPadding: EdgeInsets.only(bottom: 16),
                    title: Text('${_title} ${_cn}:${_pIndex+1}', textAlign: TextAlign.center,),
                    children: <Widget>[
                        _verse_ck(_verses[_cn-1].vs[_pIndex], 'kk'),
                        GestureDetector(
                            child: Container(
                                padding: EdgeInsets.all(16.0),
                                decoration: BoxDecoration(
                                    shape: BoxShape.circle,
                                    color: Colors.grey[200],
                                ),
                                child: Icon(Icons.pause, color: Colors.grey,),
                            ),
                            onTap: () {
                                _isAuto = false;
                                _stop();
                                Navigator.pop(context);
                            }
                        )
                    ],
                );
            }
        );
    }

preloader

    void _loadVerses(int bn) async {
        var dialog = showDialog(
            context: context,
            barrierDismissible: false,
            builder: (BuildContext context) {
                return AlertDialog(
                    titlePadding: EdgeInsets.all(32),
                    title:  Text(_books[bn - 1].kr, textAlign: TextAlign.center),
                    contentPadding: EdgeInsets.only(bottom: 32),
                    content: Text('로딩중', textAlign: TextAlign.center),
                );
            }
        );
        String jsonVerses = await rootBundle.loadString('assets/verses/${bn}.json');
        Iterable list = json.decode(jsonVerses);
        setState(() {
            _bn = bn;
            _cn = 1;
            _title = _books[bn-1].kr;
            _verses = list.map((model) => VersesList.fromJson(model)).toList();
            _chapters = List<String>.generate(_books[bn-1].cc, (int index) => '제${index+1}장');
            _controller = TabController(length: _chapters.length, vsync: this)..addListener(_controllerHandler);
            sharedPreferences.setInt("bn", bn);
            _isLoading = false;
        });
        Navigator.of(context, rootNavigator: true).pop('dialog');
    }

_scroller.animateTo  좌표지정

scroll은 어차피 rendering 된 만큼 상태에서 상대 좌표가 된다.
결국 절대 좌표가 아니라는 것!

따라서 key 값으로 position을 구하게 되면 보이지 않는 Container의 position은 구할 수 없게된다.
생성 될 때 좌표 값을 저장해 두는 방법으로 처리해야 한다.

AudioPlayer

    GestureDetector _play(int id, String lang) {

        return GestureDetector(
            child: Container(
                padding: EdgeInsets.fromLTRB(16, _size*.8 , 16, 16 ),
                child: !_isPlay ? Icon(Icons.volume_up, color: Colors.grey[300]) : Icon(Icons.volume_off, color: Colors.grey[300])
            ),
            onTap: () {
                if( _isPlay ) {
                    setState(() {
                        _isPlay = false;
                    });
                    audioPlayer.stop();
                } else {
                    setState(() {
                        _isPlay = true;
                    });
                    audioPlayer.play('<URL>');
                }

            }
        );
    }
    @override
    void initState() {
        super.initState();
        audioPlayer = new AudioPlayer();
        audioPlayer.onPlayerCompletion.listen((event) {
            setState(() {
                _isPlay = false;
            });
        });
    }
    play(String url) async {
        await audioPlayer.play(url);
    }

ruby rt

    Container _rt(String word) {
        List<String> s = word.split( RegExp("<rt>|<\/rt>") );
        return Container(
            child: Stack(
                alignment:  Alignment.topCenter,
                children: <Widget>[
                    Container(
                        child: Text( s[1], textAlign: TextAlign.center,  style: TextStyle(fontSize: _size*.6, color: Colors.grey ) ),
                    ),
                    Container(
                        padding: EdgeInsets.only(top: _size*.6),
                        child: Text( s[0],  style: TextStyle(fontSize: _size, ) )
                    ),
                ]
            )
        );
    }
    Widget _verse_ck(String text, bool center, bool play) {
        text = text.replaceAll( "<" , "<");
        text = text.replaceAll( ">" , ">");

        List<String> split = text.split( RegExp("<ruby>|<\/ruby>") );
        List<Widget> wrap = [];

        for (var s in split) {
            if (s == '')
                continue;
            if ( s.indexOf( "<rt>" ) != -1) {
                if (s.indexOf( "<rp>" ) != -1)
                    wrap.add( _rp(s) );
                else if ( s.indexOf( "<u>" ) != -1)
                    wrap.add( _rtu(s) );
                else
                    wrap.add( _rt(s) );
            } else {
                if ( s.indexOf( " " ) != -1) {
                    List<String> space = s.split( RegExp(" ") );
                    for (var p in space) {
                        if (p == '')
                            continue;
                        wrap.add( _word(p) );
                        wrap.add( _space() );
                    }
                } else {
                    wrap.add( _word(s) );
                }
            }
        }

        if (play)
            wrap.add( _play() );

        if (center)
            return Container (
                padding: EdgeInsets.fromLTRB(16, 32, 16, 32),
                child: Center(
                    child: Wrap(
                        children: wrap
                    )
                )
            );
        else
            return Container (
                padding: EdgeInsets.fromLTRB(16, 32, 16, 32),
                child: Wrap(
                    children: wrap
                )
            );

    }
    Widget _buildVerse(Verse vs ) {
        List<Widget> widgets = [];
        List<String> vss = ['kr'];

        widgets.add( _verse_vn(vs.vn) );

        for( var v in vss) {
            if (v == 'tc') {
                if (vs.ts != '')
                    widgets.add( _verse_ck(vs.ts, true, false) );
                widgets.add( _verse_ck(vs.tc, false, false) );
            }
            if (v == 'kr') {
                if (vs.ks != '')
                    widgets.add( _verse_ck(vs.ks, true, false) );
                widgets.add( _verse_ck(vs.kr, false, true) );
            }
            if (v == 'sc')
                widgets.add( _verse_ck(vs.sc, false, true) );

            if (v == 'en') {
                if (vs.es != '')
                    widgets.add( _verse_es(vs.es) );
                widgets.add( _verse_en(vs.en) );
            }
        }

        return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: widgets
        );
    }

_verse_kr

    Widget _buildVerse(Verse vs ) {
        return Wrap(
            children: <Widget>[
                _verse_kr( vs.kr ),
            ],
        );
    }

    Widget _verse_kr(String kr ) {
        List<Widget> words = [];
        List<String> split = kr.split( RegExp("<ruby>|<\/ruby>") );
        for (var s in split) {
            if ( s.indexOf( "<rt>" ) != -1) {
                List<String> rt = s.split( RegExp("<rt>|<\/rt>") );
                words.add( _makeWord(rt[1], rt[0]) );
            } else {
                if ( s.indexOf( " " ) != -1 ) {
                    List<String> space = s.split( " " );
                    for (var w in space) {
                        words.add( _makeWord('', w ) );
                        words.add( Text(' ') );
                    }
                } else {
                    words.add( _makeWord('', s ) );
                }
            }
        }
        words.add( Container(
            padding: EdgeInsets.fromLTRB(8, _size*.6, 0, 0),
            child: Icon(Icons.volume_up, color: Colors.grey[300])
            )
        );
        return Container (
            padding: EdgeInsets.all(16),
            child: Wrap(
                children: words,
            )
        );
    }

    Widget _makeWord(String sup, String text ){
        return Column(
            children: <Widget>[
                Container(
                    height: _size * .6,
                    child: Text(sup, textAlign: TextAlign.center, style: TextStyle( fontSize: _size*.6)),
                ),
                Container(
                    child: Text(text, textAlign: TextAlign.center, style: TextStyle( fontSize: _size) ),
                ),
            ],
        );
    }

drawer with header & Expanded

            drawer: Drawer(
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                        Container(
                            height: 88,
                            width: double.infinity,
                            child: DrawerHeader(
                                margin: EdgeInsets.all(0),
                                child: Text(_appName, style: TextStyle(fontSize: 20.0)),
                                decoration: BoxDecoration(
                                    color: Colors.white,
                                ),
                            ),
                        ),
                        Expanded(
                            child: Container(
                                margin: EdgeInsets.all(0),
                                color: Colors.white,
                                child: ListView.separated(
                                    separatorBuilder: (context, index) => Divider(
                                        color: Colors.grey[400],
                                    ),
                                    itemCount: _books.length,
                                    itemBuilder: (context, index) {
                                        return _buildBook(_books[index]);
                                    }
                                )
                            ),

                        ),
                        Container(
                            height: 8,
                            color: Colors.white,
                        ),
                    ]
                )
            ),
    Widget _buildBook(Book book) {
        return InkWell(
            child: Container(
                padding: EdgeInsets.fromLTRB(24, 8, 0, 8),
                child: Text(
                    book.kr, style: TextStyle(fontSize: 16.0)
                ),
            ),
            onTap: (){
                _setTabs(book);
                Navigator.pop(context);
            },
        );
    }

assets/book.json 가져오기

    _loadBook() async {
        String jsonBooks = await rootBundle.loadString('assets/book.json');
        Iterable list = json.decode(jsonBooks);
        setState(() {
            _books = list.map((model) => Book.fromJson(model)).toList();
            for (int i=0; i<_books[0].cc; i++)
                _chapters.add( '제'+(i+1).toString()+'장' );
            _controller = TabController(length: _chapters.length, vsync: this);
        });
    }

    Widget _buildBook(Book book) {
        return ListTile(
            title: Text(
                book.kr,
            ),
            onTap: (){
                setState(() {
                    _chapters = [];
                    for (int i=0; i<book.cc; i++)
                        _chapters.add( '제'+(i+1).toString()+'장' );
                    _controller.animateTo(0);
                    _controller = TabController(length: _chapters.length, vsync: this);
                });
                Navigator.pop(context);
            },
        );
    }
class Book {
    final int bn;
    final int cc;
    final String kr;

    Book({
        this.bn,
        this.cc,
        this.kr,
    }) ;

    factory Book.fromJson(Map<String, dynamic> json){
        return Book(
            bn: json['bn'],
            cc: json['cc'],
            kr: json['kr'],
        );
    }
}

Dynamic children for TabView in flutter

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {

    TabController controller;
    List<String> categories = ["a", "b", "c", "d", "e", "f", "g", "h"];

    @override
    void initState() {
        super.initState();
        controller = TabController(length: categories.length, vsync: this);
    }

    @override
    void dispose() {
        controller.dispose();
        super.dispose();
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(widget.title),
                elevation: 0.2,
                bottom: TabBar(
                    isScrollable: true,
                    tabs: List<Widget>.generate(categories.length, (int index){
                        return Tab(text: categories[index] );
                    }),
                    controller: controller,
                )
            ),
            backgroundColor: Colors.white,

            body: TabBarView(
                children: List<Widget>.generate(categories.length, (int index){
                    return new Text(categories[index]);
                }),
                controller: controller,
            ),

            floatingActionButton: FloatingActionButton(
                onPressed: _incrementCounter,
                tooltip: 'Increment',
                child: Icon(Icons.add),
            ),

        );
    }

    void _incrementCounter() {
        setState(() {
            categories.add('i');
            controller = TabController(length: categories.length, vsync: this);
        });
    }
}
    void _incrementCounter() {
        setState(() {
            controller.animateTo(0);
            categories = ["a", "b", "c"];
            controller = TabController(length: categories.length, vsync: this);
        });
    }