使用flutter實現一個關於物流進度效果
demo下載地址 https://github.com/qqcc1388/line_step_demo
實現思路也很簡單 將每個item拆開分成 leftWidget和rightWiget
leftWidget用來顯示豎線和⭕️,可以控制上豎線和下豎線都可以單獨隱藏和顯示,方便處理第一行和最后一行的豎線顯示隱藏問題,進度區域高度跟隨rightWidget高度,⭕️位置固定
rightWidget用來控制顯示內容區 高度部分 rightWidget內容區 利用column的 mainAxisSize: MainAxisSize.min,來根據內容自適應高度
思路是這個思路,但是在操作的時候發現 左邊進度區域和高度無法確定,因為內容區域使用column來自適應高度,如果整個item的高度是不確定的,那么進入區域的高度就不確定,這樣就沒法實現左邊鋪滿,右邊自適應了,那么解決問題的方法只有計算右邊內容區的高度,但是計算高度會有一定的誤差,這樣容易出現左邊無法鋪滿的情況,所有計算文本高度的方案是不可行的
最后發現flutter中提供了IntrinsicHeight這個控件,可以完美解決我的問題
根據內部子控件高度來調整高度,它將其子widget的高度調整其本身實際的高度:
將其子控件調整為該子控件的固有高度,舉個例子來說,Row中有3個子控件,其中只有一個有高度,默認情況下剩余2個控件將會充滿父組件,而使用IntrinsicHeight控件,則3個子控件的高度一致。
此類非常有用,例如,當可以使用無限制的高度並且您希望孩子嘗試以其他方式無限擴展以將其自身調整為更合理的高度時,該類非常有用。
但是此類相對昂貴,因為它在最終布局階段之前添加了一個推測性布局遍歷。 避免在可能的地方使用它。 在最壞的情況下,此小部件可能會導致樹的深度的布局為O(N²)。所以不推薦使用。
使用IntrinsicHeight將左邊和右邊包裹起來,這樣一旦右邊內容區自適應有,那么左邊容器的高度和整個item的高度一致了,這樣一旦確定了高度,進入部分就可以鋪滿整個item了
具體代碼 如下:
line_step.dart
import 'package:flutter/material.dart';
class LineStep extends StatefulWidget {
final Widget child;
final isTop;
final isBottom;
LineStep({
key,
@required this.child,
this.isTop: false,
this.isBottom: false,
}) : super(key: key);
@override
_LineStepState createState() => _LineStepState();
}
class _LineStepState extends State<LineStep> {
@override
Widget build(BuildContext context) {
return Material(
child: IntrinsicHeight(
child: Row(
children: <Widget>[
leftWidget(),
Expanded(
child: widget.child,
),
],
),
),
);
}
Widget leftWidget() {
return Container(
width: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 5,
color: widget.isTop ? Colors.transparent : Colors.blue,
height: 20,
),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(8)),
),
Expanded(
child: Container(
width: 5,
color: widget.isBottom ? Colors.transparent : Colors.blue,
)),
],
),
);
}
}
hom.dart
import 'package:flutter/material.dart';
import 'dart:ui' as ui show window;
import 'package:flutter/services.dart';
import 'package:flutter_alpha_appbar/line_step.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
ScrollController scrollController = ScrollController();
double navAlpha = 0;
@override
void initState() {
super.initState();
scrollController.addListener(() {
var offset = scrollController.offset;
if (offset < 0) {
if (navAlpha != 0) {
setState(() {
navAlpha = 0;
});
}
} else if (offset < 50) {
setState(() {
navAlpha = 1 - (50 - offset) / 50;
});
} else if (navAlpha != 1) {
setState(() {
navAlpha = 1;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnnotatedRegion(
value: navAlpha > 0.5
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Expanded(
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController,
padding: EdgeInsets.only(top: 0),
children: <Widget>[
_headerView(),
_contentList(),
],
),
),
],
),
_buildNavWidget(),
],
),
),
);
}
void back() {
Navigator.pop(context);
}
Widget _buildNavWidget() {
return Stack(
children: <Widget>[
Opacity(
opacity: 1 - navAlpha,
child: Container(
width: 44,
height: kToolbarHeight +
MediaQueryData.fromWindow(ui.window).padding.top,
padding: EdgeInsets.fromLTRB(
5, MediaQueryData.fromWindow(ui.window).padding.top, 0, 0),
child: GestureDetector(
onTap: back,
child: Container(
color: Colors.orange,
width: 20,
height: 30,
),
),
)),
Opacity(
opacity: navAlpha,
child: Container(
padding: EdgeInsets.fromLTRB(
5, MediaQueryData.fromWindow(ui.window).padding.top, 0, 0),
height: kToolbarHeight +
MediaQueryData.fromWindow(ui.window).padding.top,
color: Colors.white,
child: Row(
children: <Widget>[
Container(
width: 44,
),
Expanded(
child: Text(
'novel.name',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
Container(
width: 44,
),
],
),
),
)
],
);
}
Widget _headerView() {
return Container(
height: 200,
color: Colors.cyan,
);
}
List list = [
{
'title': '這種情況下Container的寬高鋪滿,我們給他套上IntrinsicWidth,不設置寬/高步長',
'content': '可以講子控件的高度調整至實'
},
{
'title': 'IntrinsicHeight',
'content': '可以講子控件的高度調整至實際高度。下面這個例子如果不使用IntrinsicHeight的情況下,'
},
{
'title': 'IntrinsicHeight',
'content':
'可以講子控件的高度調整至實際高度。下面這個例子如果不使用IntrinsicHeight的情況下,第一個Container將會撐滿整個body的高度,但使用了IntrinsicHeight高度會約束在50。這里Row的高度時需要有子內容的最大高度來決定的,但是第一個Container本身沒有高度,有沒有子控件,那么他就會去撐滿父控件,然后發現父控件Row也是不具有自己的高度的,就撐滿了body的高度。IntrinsicHeight就起到了約束Row實際高度的作用'
},
{
'title': '可以發現Container寬度被壓縮到50,但是高度沒有變化。我們再設置寬度步長為11',
'content': '這里設置步長會影響到子控件最后大小'
},
{
'title': '可以發現Container寬度被壓縮到50,但是高度沒有變化。我們再設置寬度步長為11',
'content': '這里設置步長會影響到子控件最后大小'
}
];
Widget _contentList() {
return Container(
child: ListView(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: list.map((e) {
final index = list.indexOf(e);
return _lineItems(e, index);
}).toList(),
),
);
}
Widget _lineItems(res, index) {
return Container(
decoration: BoxDecoration(
// color: Colors.cyan,
border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
padding: EdgeInsets.only(left: 15),
margin: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: LineStep(
key: Key('step$index'),
isTop: index == 0,
isBottom: index == list.length - 1,
child: rightWidget(res),
),
);
}
Widget leftWidget() {
return Container(
width: 20,
// height: 200,
// color: Colors.orange,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 5,
color: Colors.blue,
height: 20,
),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(8)),
),
Expanded(
child: Container(
width: 5,
color: Colors.blue,
)),
],
),
);
}
Widget rightWidget(res) {
return Container(
// color: Colors.blue,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
SizedBox(height: 15),
Text(
res['title'],
style: TextStyle(
color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold),
),
Text(
res['content'],
style: TextStyle(color: Colors.orange, fontSize: 15),
),
SizedBox(height: 15),
],
),
);
}
}