先放成品图
先对这个产品图分析,实现这个效果需要有哪些问题或需求去解决
- 根据当前不同的页数显示不同样式
- 点击跳转到指定页面
对这个效果图分析
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时
}
现在还原这个样式
每个子项分布均匀可以用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),
),
效果为
为了更具体实现这个案例,先虚拟使用场景,所以先创建最基本的页面\
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),
),
);
})
到这里就大致完成了 但如果从第一页点击跳转到最后一页的话,下面会因为跳转发生持续变化
如果保留这个,可以忽略后面
如果不想保留
方法一是改成无动画的跳转,这种方法不推荐,
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(() {});
});
},