Hello everyone, 10-10-6 in the first half of the year, so it has not been updated, take the time of the May Day holiday to update a warm up, let's get straight to the subject.Copy the code

The effect

implementation

Note: code directly written in the framework project demo, so used some of the code inside, but does not affect the reading. And, as usual, I'll put it in the notes so it's easy to readCopy the code

Bedrock MVVM+Provider develops scaffolding

The page layout

The root layout

  @override
  Widget build(BuildContext context) {
    return switchStatusBar2Dark(
        child: ProviderWidget<CrossListVM>(
            builder: (ctx,model,child){
              return _buildPage();
            },
            model: vm,onModelReady: (model) {},));
  }
Copy the code
Widget _buildPage() { return Container( width: getWidthPx(750),height: getHeightPx(1334), child: Column( children: [ getSizeBox(height: GetWidthPx (40) + screenUtil.getStatusBarh (context)), _buildTabs(), // Horizontal TAB listView getSizeBox(height: GetWidthPx (40)), Expanded(Child: _buildPageBody(),],),); }Copy the code

Horizontal TAB code

Widget _buildTabs() { return Container( width: getWidthPx(750),height: getWidthPx(100), child: ListView.builder( cacheExtent: 1500, padding: EdgeInsets.symmetric(horizontal: GetWidthPx (32)) // Scroll Controller controller: vm. TabController, scrollDirection: Axis.horizontal, itemCount: vm.tabsTitle.length, itemBuilder: (ctx,e) { return GestureDetector( onTap: () { vm.tapTab(e); }, child: TabItemWidget(e).generateWidget(), ); },),); }Copy the code

The longitudinal listview

Widget _buildPageBody() { return Container( width: getWidthPx(750),color: Colors.white, child: Listview. builder(// Add scroll Controller Controller: vm. BodyController, itemCount: vm.tabsTitle.length, padding: EdgeInsets.symmetric(horizontal: getWidthPx(32)), itemBuilder: (ctx,e) => BodyItemWidget(e).generateWidget(), ), ); }Copy the code

Now that’s the basic layout code for the page, let’s look at the widget code for the Item.

Layout of the Item Widget

Layout code for TAB item:

@override Widget build(BuildContext) {// Save the TAB context. SaveTabsChildCtx (tabIndex, context); Return selector <CrossListVM,int>(selector: (CTX, model) {return model.selecttabIndex; }, builder: (ctx,value,child) { return Container( width: getWidthPx(150), padding: EdgeInsets.symmetric(horizontal: getWidthPx(20)), margin: EdgeInsets.only(right: getWidthPx(32)), decoration: BoxDecoration( color: value == tabIndex ? Colors.green : Colors.white, borderRadius: BorderRadius.all(Radius.circular(getHeightPx(16))), border: Border. All (color: color.black,width: getWidthPx(2)), align.center, child: Text(' title $tabIndex',style: TextStyle(color: Colors.black,fontSize: getSp(20)),), ); }); }Copy the code

Vertical list item layout code:

late double height; @override void initState() { super.initState(); vm = Provider.of(context,listen: false); Clamp (5, 19) * 50; // Generate a Random height = (Random().nextint (20)).clamp(5, 19) * 50; } override Widget build(BuildContext context) {// Save the context as vm.saveBodyItemCtx(index, context); return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(getHeightPx(16))), border: Border.all(color: Colors.lightBlueAccent,width: GetWidthPx (2)),margin: EdgeInsets. Only (bottom: getWidthPx(32)), child: Column(children: [Text(' 查 看 $index',style: TextStyle(color: Colors.black,fontSize: getSp(30))), Container( width: getWidthPx(750), height: getWidthPx(height), margin: EdgeInsets.symmetric(horizontal: getWidthPx(16),vertical: getWidthPx(42)), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(getHeightPx(16))), color: Color.grey),child: Text(' content $index',style: TextStyle(color: color.white,fontSize: getSp(50)),),); }Copy the code

Here the overall layout of the page code is complete, very simple, now to achieve the concrete effect.

Effect implementation – scroll to switch TAB

First we need to set a listener for the controller of the vertical list.

In the page layout:

@override void initState() { super.initState(); vm.initListeners(); } void initListeners() { bodyController.addListener(() { if (isTapAnimateList) return; // There are always three surviving updateUnMountedList() without changing the cache; updateTabs(); }); }Copy the code

Then we set a variable, isTapAnimateList

Bool isTapAnimateList = false;Copy the code

We then collect/update the index of the surviving item using the updateUnMountedList method.

It can also be interpreted as an item that is not recycledCopy the code
/// Select * from list <int>. unMountedList; Void updateUnMountedList() {unMountedList = bodyChildctx.entries. where((CTX) {StatefulElement ele = ctx.value as StatefulElement; return (ele.state.mounted); }) .map<int>((e) => e.key) .toList(); }Copy the code

You can see here that there’s a new variable bodyChildCtx

  ///body item context
  final SplayTreeMap<int, BuildContext> bodyChildCtx = SplayTreeMap<int, BuildContext>();
Copy the code

This is actually the context we use to store the item in the save method in the Build of the Item Widget:

And, of course, there's a save container for tabsCopy the code
///tab item context final SplayTreeMap<int, BuildContext> tabsChildCtx = SplayTreeMap<int, BuildContext>(); ///body item context final SplayTreeMap<int, BuildContext> bodyChildCtx = SplayTreeMap<int, BuildContext>(); Final SplayTreeMap<int, double> itemOffsetY = SplayTreeMap<int, double>(); void saveBodyItemCtx(int index , BuildContext ctx) { bodyChildCtx.update(index, (value) => ctx , ifAbsent: ()=>ctx); } void saveTabsChildCtx(int index, BuildContext ctx) { tabsChildCtx.update(index, (value) => ctx,ifAbsent: () => ctx); }Copy the code

Let’s go to the controller callback and look at the third method:

Int currentItemIndex = 0; // select TAB int selectTabIndex = 0; Bool isScrollAnimateTabs = false; bool isScrollAnimateTabs = false; Tabs void updateTabs() {if (isScrollAnimateTabs) return; CurrentItemIndex = currentIndexOnScreen(); if (selectTabIndex == currentItemIndex) return; selectTabIndex = currentItemIndex; final ctx = tabsChildCtx[currentItemIndex]; if (ctx ! = null) { StatefulElement ele = ctx as StatefulElement; if(! ele.state.mounted) return; isScrollAnimateTabs = true; EnsureVisible (CTX, duration: duration (milliseconds: tabsScrollDuration)); } isScrollAnimateTabs = false; // Refresh TAB check status notifyListeners(refreshSelector: true); }Copy the code

Let’s look at the currentIndexOnScreen method involved above:

/ / / returns the current list of perpendicular to the screen (top) index int currentIndexOnScreen () {if (unMountedList = = null | | (unMountedList? .isEmpty ?? true)) return 0; / / here make a head, tail handle the if (bodyController. Position. The pixels = = bodyController. Position. MinScrollExtent) {/ / head pos return unMountedList! .first; } else if (bodyController.position.pixels == bodyController.position.maxScrollExtent) { //tail pos return unMountedList! .last; } / / if not the first, we need to calculate the offset of the item final double scrollPos = bodyController. Position. The pixels. / / collect calculate the position of the item relative viewport collectVerticalItemHeight (); for (int i = 0; i < unMountedList! .length - 1; i++) { final int ctxIndex = unMountedList! [i]; If ((itemOffsetY[ctxIndex]! - scrollPos).abs() < 20) { return unMountedList! [i]; }} // Dr Return currentItemIndex; }Copy the code

Method of relative viewport item location collectVerticalItemHeight internal implementation as follows:

// * The height of the item applied here does not change, So did the collection to avoid double-counting void collectVerticalItemHeight () {final minScrollExtent = bodyController. Position. MinScrollExtent; final maxScrollExtent = bodyController.position.maxScrollExtent; unMountedList! .forEach((live) { if (itemOffsetY.containsKey(live)) return; final BuildContext ctx = bodyChildCtx[live]! ; final RenderObject renderObject = ctx.findRenderObject()! ; / / by item CTX access to its viweport final RenderAbstractViewport viewport. = RenderAbstractViewport of (renderObject)! ; / / through the viewport. GetOffsetToReveal obtained it offsets the final double target = viewport. GetOffsetToReveal (renderObject, 0.0). Offset. Clamp (minScrollExtent maxScrollExtent); itemOffsetY[live] = target; }); }Copy the code

Now that we have implemented the function of scrolling through the vertical list to switch TAB, we need to implement the effect of clicking TAB to scroll to the corresponding vertical list item.

Effect implementation – Click TAB to switch vertical List items

First add a click event, which is sure to drop:

Widget _buildTabs() {return Container(width: getWidthPx(750),height: getWidthPx(100), child: ListView.builder( cacheExtent: 1500, padding: EdgeInsets.symmetric(horizontal: getWidthPx(32)), controller: vm.tabController, scrollDirection: Axis.horizontal, itemCount: vm.tabsTitle.length, itemBuilder: (CTX,e) {return GestureDetector(onTap: () {// Here is the click event! vm.tapTab(e); }, child: TabItemWidget(e).generateWidget(), ); },),); } void tapTab(int index) async {// Scroll vertical list item jumpToItem(index); selectTabIndex = index; // Refresh the TAB selection effect notifyListeners(refreshSelector: true); // Here scroll the TAB clicked to the middle of the screen await Scrollable. EnsureVisible (tabsChildCtx[index]! And duration: the duration (milliseconds: 500), alignment: 0.5); }Copy the code

The jumpToItem method is used to scroll the corresponding item to the top of the screen as follows:

final List<int> tabsTitle = List.generate(25, (index) => index); Final int standardSingleTime = 128; final int standardSingleTime = 128; Final int onCardScrollDuration = 16*8; Void jumpToItem(int index) async {// Count the distance to the target final int dis = (index-selecttabIndex).abs(); if (dis == 0) return; // Mark vertical list scrolling for TAB drive isTapAnimateList = true; If (dis == 1) {// If (dis == 1) {final StatefulElement Element = bodyChildCtx[index] as StatefulElement; if(! element.state.mounted)return; scrollDuration = 500; await Scrollable.ensureVisible(element, duration: Duration(milliseconds: scrollDuration)); } else {// If it is not adjacent, then it means that the target item may not have been created // So the method adopted here is to roll over one item by one XD // Calculate the roll time of a single item according to the proportion of distance to the total length x time slice // but not too small, ScrollDuration = math.max((dis/tabstitle.length * standardSingleTime).ceil(), onCardScrollDuration); If (index > selectTabIndex) {// if (index > selectTabIndex) {// if (index > selectTabIndex); while (i <= index) { final StatefulElement element = bodyChildCtx[i] as StatefulElement; if(! element.state.mounted) { i--; continue; } // while loop, we scroll to await scrollable. ensureVisible(element, duration: duration (milliseconds: scrollDuration)); i++; Int I = selectTabIndex - 1; int I = selectTabIndex - 1; while (i >= index) { final StatefulElement element = bodyChildCtx[i] as StatefulElement; if(! element.state.mounted) { i--; continue; } await Scrollable.ensureVisible(element, duration: Duration(milliseconds: scrollDuration)); i--; } } } isTapAnimateList = false; }Copy the code

At this point, the vertical linkage effect of the cross list is basically over. There are still many areas that can be optimized on the whole, and we will continue to improve them later. If there are any shortcomings, please point out them.

conclusion

Demo

The demo code

Other articles

Flutter mimics netease Cloud music App

Flutter&Android startup page (splash screen page) loading process and optimization scheme

Flutter version of imitation. Parallax effect of Zhihu list

Flutter — Realize progressive card switching for netease Cloud Music

Flutter imitates flush list of self-selected stocks