Look at the renderings first

Implementation approach

In flutter, if you want to achieve the above page switching effect, you must think of pageView. The controller of pageView can listen to the scrolling event of pageView and obtain the scrolling position of pageView, so we can change the corresponding icon color according to the position in the scrolling event.

Change the color of the icon

ICONS are extracted from wechat and are all webP format pictures. To change the color of an image, use the ImageIcon component.

ImageIcon turns an image into a monochrome image, so you can use this component as long as the image doesn’t have a multicolor requirement.

Now that we can change the color, we also need to know what color to change when pageView scrolls. To capture the Color of a flutter as it scrolls from one page to another, use the lerp method provided by the Color class of the flutter. This method obtains the linear difference between the two colors

That is, in the scroll event, calculate t and change the color of the icon according to T to achieve the above effect.

pageController.addListener(() { int currentPage = pageController.page.toInt(); // Double t = pagecontroller.pagecontroller.pagecontroller.pagecontroller.currentPage; // Get the orientation based on the previous page positionif(lastPage < = pageController. Page) {/ / right currentPage is the current page / / transition from the current page to the next page streamController. The sink. The add (StreamModel (timeline:  t, index: currentPage, gotoIndex: currentPage + 1)); }else{/ / scroll left currentPage is back. / / transition from the current page to the previous page streamController sink. The add (StreamModel (timeline: t, index: currentPage + 1, gotoIndex: currentPage)); } lastPage = pageController.page; });Copy the code

CurrentPage = 1.11; currentPage = 1.11; 1.21… All the way up to 2, so the currentPage is the currentPage in this process. If the current page is 4, it will have a value of 3.99 when sliding to 3… 3.81… All the way up to 3, and in that process the currentPage is the previous page.

T calculation is simpler, 1.11-1=0.11, 3.99-3=0.99…..

Managing icon colors

Because I am using the BottomNavigationBar, it is too troublesome to change the icon color in the scrolling event of the pageController, so I use Stream to manage the icon state. Create a multi-subscription pipe with Stream, have all ICONS subscribe to it, and then send all the required data to all ICONS in a slide event.

Required data:

class StreamModel {
  const StreamModel({this.timeline, this.index, this.gotoIndex});
  
  final double timeline;
  final int index;
  final int gotoIndex;
}
Copy the code

Icon component

The constructor sets an index to determine which icon it is.

Use the StreamBuilder to wrap the component to change color, and bind the StreamController set from the constructor.

StreamBuilder controls icon colors based on parameters passed in by pageView scroll events.

class BottomNavIcon extends StatelessWidget {
    final StreamController<StreamModel> streamController;
    final int index;
    final String img;
     final String title;
     final double fontSize;
     Color _color;
     Color _activeColor;
     final bool isActive;
   BottomNavIcon(this.title, this.img, this.index,
      {@required this.streamController,
     this.isActive = false, this.fontSize = 18.0, Color Color = color.grey, Color activeColor = color.blue}) {_color = isActive? activeColor : color; _activeColor = isActive ? color : activeColor; } @override Widget build(BuildContext context) {return StreamBuilder(
        stream: streamController.stream,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          final StreamModel data = snapshot.data;
          double t = 0.0;
          if(data ! = null) {// Start indexif(data.index == index) { t = data.index > data.gotoIndex ? Data. timeline: 1.0-data. timeline;print("this${data.index}:${t}"); } // End indexif(data.gotoIndex == index) { t = data.index > data.gotoIndex ? 1.0 - data.timeline // The index at the beginning is larger than the index at the end. When the interpolation is greater than 0.6, I feel that the current color differs too much from the end color, so when the interpolation is greater than 0.6, I restore the default color t = t >= 0.6. 1 : t;print("goto${data.gotoIndex}:${t}"); }}if(t > 0.0&&t < 1.0) {//color.lerp gets linear interpolation between two colorsreturn Column(
              children: <Widget>[
                ImageIcon(AssetImage(this.img),
                    color: Color.lerp(_color, _activeColor, t)),
                Text(title,
                    style: TextStyle(
                        fontSize: fontSize,
                        color: Color.lerp(_color, _activeColor, t))),
              ],
            );
          }
          returnColumn( children: <Widget>[ ImageIcon(AssetImage(this.img), color: Color.fromRGBO(_color.red, _color.green, _color.blue, 1)), Text(title, style: TextStyle( fontSize: fontSize, color: Color.fromRGBO( _color.red, _color.green, _color.blue, 1))), ], ); }); }}Copy the code

The current (index== data.index) icon color is gradually lightened, scroll to (index==data.gotoIndex) icon color is gradually darker

Create streams with multiple subscriptions

final StreamController<StreamModel> streamController =
    StreamController.broadcast();
Copy the code

Loading icon

for (int i = 0; i < pages.length; i++) {
      TabBarModel model = pages[i];
      bars.add(
        BottomNavigationBarItem(
          icon: BottomNavIcon(
            model.title,
            'assets/images/tabbar_' + model.icon + '_c.webp',
            i,
            streamController: streamController,
          ),
          activeIcon: BottomNavIcon(
            model.title,
            'assets/images/tabbar_' + model.icon + '_s.webp',
            i,
            streamController: streamController,
            isActive: true,
          ),
          title: Center(),
        ),
      );
    }
Copy the code

The reason the above code has title Center is that you have created a component in the icon component that displays the title, so that you can set the color together. It’s not needed here, but its title is not allowed to be null, so just give it a component with zero height and width

conclusion

In fact, this effect and wechat is not exactly the same, wechat should be selected icon superimposed on the default icon. The default icon color gradient is linear, select the icon transparency gradient. This is not possible with the built-in BottomNavigationBar and may require a custom bottom navigation.

Writing a technical article for the first time, I feel a bit messy, so post the full code address:

  • Gist: gist.github.com/327100395/9…
  • DartPad: dartPad. Dev / 9 dee2497a99… (image read local, wrong path in dartPad, so image not displayed)