Flutter--分页器(思路和过程)

523 阅读5分钟

先放成品图

image.png

先对这个产品图分析,实现这个效果需要有哪些问题或需求去解决

  • 根据当前不同的页数显示不同样式
  • 点击跳转到指定页面

对这个效果图分析
1 2 3...10
1 2 3 4...10
1...3 4 5...10
1...7 8 9 10
1...8 9 10
一共五种形式 这里假定一共10页 当页数小于3时是第一种 当页数等于3时是第二种 当页数小于3大于8时是第三种 当页数等于8时是第四种 当页数大于8时是第五种

暂时用all存放这些

List all = [  
'1 2 3...10',  //当页数小于3时
'1 2 3 4...10',  //当页数等于3时
'1...3 4 5...10',  //当页数小于3大于8时
'1...7 8 9 10',  //当页数等于8时
'1...8 9 10'  //当页数大于8时
];

对于五种以上的情况可以使用switch分支语句来判断所处情况

switch (indexPage) {  
case 1:  
case 2:  
all[0];  //当页数小于3时
break;  
case 3:  
all[1];  //当页数等于3时
break;  
case 8:  
all[3];  //当页数等于8时
break;  
case 9:  
case 10:  
all[4];  //当页数大于8时
break;  
default:  
all[2];  //当页数小于3大于8时
}

image.png 现在还原这个样式
每个子项分布均匀可以用Row的mainAxisAlignment: MainAxisAlignment.spaceEvenly,来实现 Row需要传入children 这里定义一个List<Widget> children = [];用来存储当前页面的分页器 关于内容的填充这里初步先用死数据来生成

children.addAll(List.generate(5, (index) {  
if (index == 3) return const Text('...');  
if (index == 4) {  
return Text('10');  
}  
return Text('{$index + 1}');  
}));

结果为

Text('1') Text('2') Text('3') Text('...') Text('10')
DefaultTextStyle(  
style: const TextStyle(  
color: Colors.black,  
fontSize: 28,  
fontWeight: FontWeight.bold),  
child: Row(  
mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
children: children),  
),

效果为

image.png
为了更具体实现这个案例,先虚拟使用场景,所以先创建最基本的页面\

Column(  
    children: [  
     Expanded(  
     child: PageView(  
  
     },  
     children: [  
         buildPhotoView(size, '1'),  
         buildPhotoView(size, '2'),  
         buildPhotoView(size, '3'),  
         buildPhotoView(size, '4'),  
         buildPhotoView(size, '5'),  
         buildPhotoView(size, '6'),  
         buildPhotoView(size, '7'),  
         buildPhotoView(size, '8'),  
         buildPhotoView(size, '9'),  
         buildPhotoView(size, '10'),  
        ],  
      ),  
     ),  
     Builder(builder: (context) {  
      List<Widget> children = [];  
        return Container(  
          height: 80,  
          width: 300,  
          padding: const EdgeInsets.only(top: 10),  
          decoration: BoxDecoration(border: Border.all()),  
            child: DefaultTextStyle(  
            style: const TextStyle(  
            color: Colors.black,  
            fontSize: 28,  
            fontWeight: FontWeight.bold),  
            child: Row(  
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
             children: children),  
          ),  
        );  
       })  
    ],  
);
Wrap buildPhotoView(Size size, String index) {  
return Wrap(  
spacing: 30,  
runSpacing: 30,  
children: [  
photosItem(size, index),  
photosItem(size, index),  
photosItem(size, index),  
photosItem(size, index),  
photosItem(size, index),  
photosItem(size, index),  
],  
);  
}  
  
Container photosItem(Size size, String index) {  
return Container(  
width: size.width / 3 - 70,  
height: size.height / 2 - 110,  
decoration: const BoxDecoration(color: Colors.grey),  
child: Center(  
child: Text(  
'$index',  
style: const TextStyle(fontSize: 40),  
)),  
);  
}

这里把分页器和页面放到column里
现在已经有了最基本的模型
下一步思考怎样继续去实现

  • pageview应该和分页器产生关联
    这里初始化pageController,
late PageController pageController;  
int indexPage = 1;//当前界面
@override  
void initState() {  
// TODO: implement initState  
super.initState();  
pageController = PageController();  
 
}

PageView(
controller: pageController,
onPageChanged: (value) {  
   indexPage = value + 1;  
   setState(() {});  
},
//其他省略
)
  • 获取总页数并且对不同页数做对应的处理
double? maxScrollExtent;
@override  
void initState() {   
   super.initState();  
   pageController = PageController();  
   WidgetsBinding.instance?.addPostFrameCallback((_) {  
   if (pageController.hasClients) {  
   maxScrollExtent = pageController.position.maxScrollExtent;  
   print(maxScrollExtent);  
   setState(() {}); // 触发重新构建以更新 UI  
   }  
   });  
}

WidgetsBinding.instance?.addPostFrameCallback 方法用于在当前帧绘制完成后执行一个回调函数。在这个回调函数中,可以执行一些需要在 widget 渲染完成后才能执行的操作,例如获取 widget 的尺寸、位置信息等。

这里用来确保PageView绘制完成后通过pageController.position.maxScrollExtent;来获取Pageview的控制的滚动视图的最大滚动偏移量。滚动内容在当前视图大小下可以滚动的最大距离。 再通过LayoutBuilder来获取Pageview的宽度maxwidth

LayoutBuilder(  
  builder: (BuildContext context, BoxConstraints constraints) {  
     double maxwidth = constraints.maxWidth;
     )

maxScrollExtent! ~/ maxwidth + 1即为PageView的总页数

因为涉及点击跳转和背景阴影这里选择定义一个方法来解决

Widget buildPhotosPage(int page, int select) {  //page是这个Widget代表的页码,select代表是选中的页面数值
   return GestureDetector(  
   onTap: () {  
   //实现页面跳转
      setState(() {});  
      pageController.animateToPage(page - 1,  
      duration: Duration(milliseconds: 300), curve: Curves.easeInSine);  
   },  
   child: Container(  
     decoration: BoxDecoration(  
     color: page == select ? Colors.grey : Colors.transparent),  /如果当前页面和选择页面相同显示阴影
     child: Text('$page'),  
     ),  
   );  
}

至于生成逻辑则放在分页器的build里,因为switch语句必须放在函数内部(方法内部),build可以提供一个构造函数,便于用switch生成Row的children 下面提供了if语句和switch语句的2种思路

     Builder(builder: (context) {
                List<Widget> children = [];
                if (maxScrollExtent != null) {
                  int max = maxScrollExtent! ~/ maxwidth + 1;
                  print(max);
                  if (max < 6) {
                    children.clear();
                    children.addAll(List.generate(5, (index) {
                      return buildPhotosPage(index + 1, indexPage);
                    }));
                  } else if (indexPage < 3) {
                    children.clear();
                    children.addAll(List.generate(5, (index) {
                      if (index == 3) return const Text('...');
                      if (index == 4) {
                        return buildPhotosPage(max, indexPage);
                      }
                      return buildPhotosPage(index + 1, indexPage);
                    }));
                  } else if (indexPage == 3) {
                    children.clear();
                    children.addAll(List.generate(6, (index) {
                      if (index == 4) return const Text('...');
                      if (index == 5) {
                        return Text('${max}');
                      }
                      return buildPhotosPage(index + 1, indexPage);
                    }));
                  } else if (indexPage > 3 && indexPage < max - 2) {
                    children.clear();
                    children.addAll(List.generate(7, (index) {
                      if (index == 0) {
                        return buildPhotosPage(1, indexPage);
                      }
                      if (index == 1) return const Text('...');
                      if (index == 5) return const Text('...');
                      if (index == 6) {
                        return buildPhotosPage(max, indexPage);
                      }
                      return buildPhotosPage(indexPage + index - 3, indexPage);
                    }));
                  } else if (indexPage == max - 2) {
                    children.clear();
                    children.addAll(List.generate(6, (index) {
                      if (index == 0) return buildPhotosPage(1, indexPage);
                      if (index == 1) return const Text('...');
                      return buildPhotosPage(indexPage + index - 3, indexPage);
                    }));
                  } else if (indexPage == max - 1 || indexPage == max) {
                    children.clear();
                    children.addAll(List.generate(5, (index) {
                      if (index == 0) return buildPhotosPage(1, indexPage);
                      if (index == 1) return const Text('...');

                      return buildPhotosPage(max + index - 4, indexPage);
                    }));
                  }
这是用swtich的思路 swtich不如if方便因为case的数值必须是常量,所以涉及总页数的判断需要用到进一步的if来进行判断  default里的if用来判断最后两页,不采用这种方法,swtich里只能判断已知的常量如case 8,没有if里的indexPage == max - 2灵活
                  //   switch (indexPage) {
                  //     case 1:
                  //     case 2:
                  //       children.clear();
                  //       children.addAll(List.generate(5, (index) {
                  //         if (index == 3) return const Text('...');
                  //         if (index == 4) {
                  //           return buildPhotosPage(max + 1, indexPage);
                  //         }
                  //         return buildPhotosPage(index + 1, indexPage);
                  //       }));
                  //
                  //       break;
                  //     case 3:
                  //       children.clear();
                  //       children.addAll(List.generate(6, (index) {
                  //         if (index == 4) return const Text('...');
                  //         if (index == 5) {
                  //           return Text('${max + 1}');
                  //         }
                  //         return buildPhotosPage(index + 1, indexPage);
                  //       }));
                  //       break;
                  //     case 8:
                  //       children.clear();
                  //       children.addAll(List.generate(6, (index) {
                  //         if (index == 0) return buildPhotosPage(1, indexPage);
                  //         if (index == 1) return const Text('...');
                  //         return buildPhotosPage(
                  //             indexPage + index - 3, indexPage);
                  //       }));
                  //       break;
                  //     default:
                  //       children.clear();
                  //       if (indexPage == max || indexPage == max + 1) {
                  //         children.addAll(List.generate(5, (index) {
                  //           if (index == 0) return buildPhotosPage(1, indexPage);
                  //           if (index == 1) return const Text('...');
                  //           if (index == 2) {
                  //             return buildPhotosPage(max - 1, indexPage);
                  //           }
                  //           if (index == 3) {
                  //             return buildPhotosPage(max, indexPage);
                  //           }
                  //           if (index == 4) {
                  //             return buildPhotosPage(max + 1, indexPage);
                  //           }
                  //           return Container();
                  //         }));
                  //         break;
                  //       }
                  //       children.clear();
                  //       children.addAll(List.generate(7, (index) {
                  //         if (index == 0) {
                  //           return buildPhotosPage(1, indexPage);
                  //         }
                  //         if (index == 1) return const Text('...');
                  //         if (index == 5) return const Text('...');
                  //         if (index == 6) {
                  //           return buildPhotosPage(max + 1, indexPage);
                  //         }
                  //         return buildPhotosPage(
                  //             indexPage + index - 3, indexPage);
                  //       }));
                  //       break;
                  //     // 添加默认情况的组件,或者不添加任何内容
                  //   }
                }

                return Container(
                  height: 80,
                  width: 300,
                  padding: const EdgeInsets.only(top: 10),
                  decoration: BoxDecoration(border: Border.all()),
                  child: DefaultTextStyle(
                    style: const TextStyle(
                        color: Colors.black,
                        fontSize: 28,
                        fontWeight: FontWeight.bold),
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: children),
                  ),
                );
              })

到这里就大致完成了 但如果从第一页点击跳转到最后一页的话,下面会因为跳转发生持续变化

recording.gif 如果保留这个,可以忽略后面 如果不想保留 方法一是改成无动画的跳转,这种方法不推荐,

      onTap: () {
        setState(() {});
        pageController.jumpToPage(page - 1);
      },

方法二 添加监听器,跳转时暂停onPageChanged

  bool canTriggerPageChange = true; //用来进行跳转暂停pageChange
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    pageController = PageController();
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      if (pageController.hasClients) {
        maxScrollExtent = pageController.position.maxScrollExtent;
        print(maxScrollExtent);
        setState(() {}); // 触发重新构建以更新 UI
        pageController.addListener(() {
          if (pageController.page == pageController.page!.roundToDouble()) {
            // 页面停止滚动后更新 indexPage
            indexPage = pageController.page!.toInt() + 1;
            if (canTriggerPageChange) {
              setState(() {});
            }
          }
        });
      }
    });
  }
  
   PageView(
                  controller: pageController,
                  onPageChanged: (value) {
                    setState(() {});
                    if (canTriggerPageChange) {
                      indexPage = value + 1;
                      print(indexPage);
                      setState(() {});
                    }
                  },
  )
  
  
  
        onTap: () {
        canTriggerPageChange = false;
        setState(() {});
        pageController
            .animateToPage(page - 1,
                duration: Duration(milliseconds: 300), curve: Curves.easeInSine)
            .whenComplete(() {
          canTriggerPageChange = true;
          setState(() {});
        });
      },