This is a contest submission.
There are over 5 million mobile apps. Developers thrive with brevity; a place where programmers can bring ideas to life quickly. Widgets enticed me into working with Flutter because they're fun and expressive.
I created a stateful widget that parents a PageView. On this widget is a scrollbar that can be moved around to jump pages.
The scrollbar can be tapped to quickly jump to an associated page. Along the greater body, the user can (slowly) turn pages sequentially. The right bar is scrollable and flips pages quickly. There exists an enlarged hit-box for reaching the first and last page, but pages from (pages.length-2) are jumped to by tapping the appropriate pigeon-holed location. SlidingPageView is a stack with a PageView on the bottom, a Container (slider) that animates y-direction, and a GestureDetector that is (width of the screen / 8) wide and the height of the screen.
The implicit scrollbar eliminates cartesian confusion. SlidingPageView was built to be a commonly used interface for all platforms, devices, and orientations.
To create a SlidingPageView, provide a list of pages. No animation, or page controllers needed. Instead that stuff is handled by the widget.
List<Widget>[] pages = [Image.asset("images/${pageOne}.png"), Image.asset("images/${pageTwo}.png"), Image.asset("images/${pageThree}.png"), Image.asset("images/${pageFour}.png")];
Widget slidingPageView = SlidingPageView(children: pages);
The right side has the hidden gesture detector, the slider will show along the top when first entering the screen, but 1.5 secs in the slider disappears from the screen.
After a tap gesture, the slider will jump to the touch location, and the PageView will also jump to the mapped index of the tap location. After jumping, the slider animates off screen.
During a drag gesture, the slider will jump to the associated location, and the PageView will also jump to the mapped index of the tap location; however the slider does not animate off screen.
After a drag gesture ends, the slider animates off screen.
My goal was to make the first and last page easiest to turn to. But all other pages are still reachable by gesture. The size of the scroll container and gesture detector are calculated below.
itemCount = 47 // Number of Bart stations in json file (station.json).
sliderWidth = 6; // 6 pixels wide
sliderHeight = MediaQuery.of(context).size.height / 7; // Calculate size for slider height.
totalWidth = MediaQuery.of(context).size.width; // (context is rebuilt upon orientation change)
yViewInset = Scaffold.of(context).widget.appBar != null ? AppBar().preferredSize.height.toInt() + (Platform.isIOS ? MediaQuery.of(context).size.height > MediaQuery.of(context).size.width ? 21 : 0 : 25) : 0;
totalHeight = MediaQuery.of(context).size.height - (yViewInset); // The SlidingPageView will calculate if the appBar is active and deduct it's size, it also has logic for substracting system specific amounts.
tapScrollView = GestureDetector(x: totalWidth - (totalWidth/8), y: 0, width: (totalWidth/8), height: totalHeight); // A transparent view that is on top of our slider on the stack.
This model is visual representation of the gesture mappings to the SlidingPageView in this app. But it is meant to size to any PageView with more than 2 children. Upon orientation change, the cells may grow in the x-direction to counter system UI padding. For example the landscape left cells on an iphone X are a greater width because the SlidingPageView covers the notched area.
gestureDetectorWidth = (MediaQuery.of(context).size.width / 9)+MediaQuery.of(context).padding.right; // 1/9th of the screen size added to the safe area right padding inset.
pigeonHoleCellHeight = ((_totalHeight - (_sliderHeight*2)) / (_itemCount-2))).round(); // The pigeon cell height is the (total height - padding area) divided by the number of pigeoned cells (itemCount - 2). Where 2 is the number of padded cells (first and last).
Appbar or Top
__________________
| || page 1 |
| || page 1 |
| || page 1 ||
| || page 2 |
| || page 3 |
| ... |
| ... |
| ... |
| ... |
| || page 46 |
| || page 47 |
| || page 47 |
| || page 47 |
------------------
Nav Bar or Bottom
When a tap or scroll occurs, a local touch position is computed. This gets turned into a y-value that ranges from 0 to the height of the PageView.
If a tap with local y-value less than or equal to the slider's height, then page 1 was selected. (index 0)
If a scroll with local y-value greater than the total height subtracted by the slider's height; then the last page was selected. (index itemCount-1)
If a tap with local yPosition greater than the slider's height but less than the total height subtracted by the slider's height. Calculate a pigeon holed index.
int localY = (context.findRenderObject( ) as RenderBox).globalToLocal( position ).dy.round(); // Convert to local y postiion.
int pigeonHoleIndex = ((localY - _sliderHeight) / ((_totalHeight - (_sliderHeight*2)) / (_itemCount-2))).round(); // With 47 pages, there are 45 pigeon holed cells. A sliderHeight of 80 will make the totalPigeonHeight => totalHeight - (80*2);
int nextIndex = localY <= _sliderHeight ? 0 : localY >= _totalHeight - _sliderHeight ? _itemCount-1 : pigeonHoleIndex; // Check the top and bottom of the canvas against the touch location. If inside the pigeon, 1 < n < itemCount, use pigeon index.
This method is used by a Positioned widget that parents the slider container. The top parameter is calculated using offSet(currentIndex).
If at the first page, set the top value to 0. Align the slider along the top.
If we are at the last page, set the top value to total height subtracted by the slider's height. Align the slider along the bottom.
Otherwise we calculate a top value using the pigeon hole.
double pigeonHoleOffset = (index * (_totalHeight - _sliderHeight) / _itemCount);
double offsetFor(int index) => index <= 0 ? 0 : index >= _itemCount-1 ? _totalHeight-_sliderHeight : pigeonHoleOffset;
The SlidingPageView only supports vertical PageView direction, but it could easily support horizontal page flipping if I had the space. For example, a horizontal SlidingPageView may be a delightful way for users to develop a timeline or reading experience.
It was really difficult having less than 5120 bytes of Dart, I rewrote this project 6 times with different implementations and features.
The example app was comprised of wonderful pictures by these people.
Ashby - Franco Folini
West Oakland - Thomas Hawk
12th St. Oakland City Center - Brian Stokle
16th St. Mission - Adam Moss
19th St. Oakland - Victoria Hyde
24th St. Mission - Stephanie Vacher
Antioch - Jim Maurer
Balboa - Andrew Nash
Bay Fair - Adam Moss
Civic Center/UN Plaza - telmo32
Colma - cifraser1
Coliseum - Paul Sullivan
Concord - jacampos
Daly City - meligrosa
Downtown Berkeley - Tzuhsun Hsu
El Cerrito del Norte - ~dgies
Dublin/Plesanton - Eric Fischer
Embarcadero - tian2992
Fremont - Mike Linksvayer
Fruitvale - pengrin™
Glen Park - Travis Wise
Hayward - wilson
Lafayette - Franco Folini
MacArthur - Melinda * Young
Millbrae - Prayitno
Montgomery St. - Markus Spiering
North Berkeley - Romel Jacinto
North Concord/Martinez - rocor
Orinda - Mike Linksvayer
Pittsburg Center - Jim Maurer
Pleasant Hill/Contra Costa Centre - Brian Stokle
Pittsburg/Bay Point - Jim Maurer
El Cerrito Plaza - Paul Sullivan
Powell St - Jeromyu
Richmond - Don Barrett
Rockridge - Robert MacCloy
San Leandro - AC ServiceInfo
San Bruno - The West End
SF Airport - inmediahk
South Hayward - Ferrous Büller
South San Francisco - Denmark
Union City - Rafael Castillo
Warm Springs - Jim Maurer
Walnut Creek - Paolo Valdemarin
West Dublin/Pleasanton - Eric Fischer