Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active November 26, 2019 10:01
Show Gist options
  • Save PlugFox/c683eed018a3a72c55255e4e58447e0d to your computer and use it in GitHub Desktop.
Save PlugFox/c683eed018a3a72c55255e4e58447e0d to your computer and use it in GitHub Desktop.
// https://syncview.firebaseapp.com
// https://gist.github.com/PlugFox/c683eed018a3a72c55255e4e58447e0d
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
//import 'package:url_launcher/url_launcher.dart'; // io
import 'dart:html' show window; // html
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) =>
MaterialApp(
title: 'Sync ScrollControllers',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
),
home: Scaffold(
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600,
maxHeight: 800,
),
child: Card(
child: Padding(
padding: EdgeInsets.all(4),
child: HomePage(),
),
),
),
),
),
),
);
}
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final ScrollSync scrollSync = ScrollSync();
static void _launchURL() async {
const String url = 'https://gist.github.com/PlugFox/c683eed018a3a72c55255e4e58447e0d';
/*
// io
if (await canLaunch(url)) {
await launch(url);
}
*/
// html
window.open(url, '#sourceCode');
}
@override
void initState() {
super.initState();
this.scrollSync.init();
}
@override
void dispose() {
this.scrollSync.close();
super.dispose();
}
@override
Widget build(BuildContext context) =>
Provider<ScrollSync>(
builder: (BuildContext context) => this.scrollSync,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
flex: 1,
child: ListViewWidget(),
),
const SizedBox(
height: 4,
),
Expanded(
flex: 1,
child: PageViewWidget(),
),
const SizedBox(
height: 24,
child: const Padding(
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 2,),
child: const FlatButton(
onPressed: _HomePageState._launchURL,
child: const Text('Show source code', style: TextStyle(color: const Color(0xFF0000FF), decoration: TextDecoration.underline,),),
),
),
),
],
),
);
}
@immutable
class ListViewWidget extends StatelessWidget {
final List<Widget> _data = List<Widget>.generate(50, _map);
static Widget _map(int idx) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 10,),
child: Container(
height: 100,
color: const Color(0xFF7F7FFF),
child: Center(
child: Text('ListTile #$idx', style: TextStyle(fontSize: 24,),),
),
),
);
@override
Widget build(BuildContext context) =>
ListView(
scrollDirection: Axis.vertical,
physics: const AlwaysScrollableScrollPhysics(),
controller: Provider.of<ScrollSync>(context).listController,
children: this._data,
);
}
@immutable
class PageViewWidget extends StatelessWidget {
final List<Widget> _data = List<Widget>.generate(25, _map);
static Widget _map(int idx) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 0,),
child: Container(
color: const Color(0xFF7FFF7F),
child: Center(
child: Text('Page #$idx', style: TextStyle(fontSize: 24,),),
),
),
);
@override
Widget build(BuildContext context) =>
PageView(
scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(),
controller: Provider.of<ScrollSync>(context).pageController,
children: this._data,
);
}
class ScrollSync {
// ignore: close_sinks
BehaviorSubject<ScrollPosition> _behaviorSubject;
ScrollController listController;
PageController pageController;
void init() {
this._behaviorSubject = BehaviorSubject<ScrollPosition>()
..debounceTime(const Duration(milliseconds: 500))
.forEach(_onScroll);
this.listController = ScrollController()
..addListener(_listListener);
this.pageController = PageController()
..addListener(_pageListener);
}
void close() =>
this
..listController.dispose()
..pageController.dispose()
.._behaviorSubject.close();
void _listListener() =>
_behaviorSubject.add(
ScrollPosition.list(
current: this.listController.position.pixels,
max: this.listController.position.maxScrollExtent,
),
);
void _pageListener() =>
_behaviorSubject.add(
ScrollPosition.page(
current: this.pageController.position.pixels,
max: this.pageController.position.maxScrollExtent,
),
);
void _onScroll(ScrollPosition scrollPosition) async {
if (scrollPosition.isList) {
print(' * list -> page');
this.pageController.removeListener(_pageListener);
await this.pageController.animateTo(scrollPosition.pixelOffset(
this.pageController.position.maxScrollExtent),
duration: const Duration(milliseconds: 400),
curve: Curves.ease,
);
this.pageController.addListener(_pageListener);
} else {
print(' * page -> list');
this.listController.removeListener(_listListener);
await this.listController.animateTo(
scrollPosition.pixelOffset(this.listController.position.maxScrollExtent),
duration: const Duration(milliseconds: 400),
curve: Curves.ease,
);
this.listController.addListener(_listListener);
}
}
}
class ScrollPosition {
final bool isList;
bool get isPage => !this.isList;
double get percent => current*100 / max;
final double current;
final double max;
double pixelOffset(double maxPixels) => this.current*maxPixels/this.max;
ScrollPosition.list({@required this.current, @required this.max})
: this.isList = true;
ScrollPosition.page({@required this.current, @required this.max})
: this.isList = false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment