Flutter —— 弹性盒子布局&状态管理&项目搭建
1. Stack
创建一个StackDemo。
class StackDemo extends StatelessWidget {
const StackDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color:Colors.yellow,
alignment:Alignment(0,0),
child: Stack(
),
);
}
}
为Stack添加三个子部件Container,然后里面添加Icon。
return Container(
color:Colors.yellow,
alignment:Alignment(0,0),
child: Stack(
children: [
Container(
color: Colors.white,
width: 100,
height: 200,
child: Icon(Icons.add),
),
Container(
color: Colors.white,
width: 100,
height: 200,
child: Icon(Icons.seven_k),
),
Container(
color: Colors.white,
width: 100,
height: 200,
child: Icon(Icons.qr_code),
),
],
),
);
运行后发现这里只显示了一个QRcode,这是因为stack是允许子组件堆叠。因为这里大小相同所以被最后一个子部件遮挡住了。
修改大小后,发现这里确实堆叠了起来。
2. Positioned
Positioned用于根据Stack的四个角来确定子组件的位置。将刚才的三个container用 Positioned包起来,然后设置位置属性。
Positioned(
child: Container(
color: Colors.white,
width: 200,
height: 200,
child: Icon(Icons.add),
)),
Positioned(
left: 20,
top: 20,
child: Container(
color: Colors.red,
width: 100,
height: 100,
child: Icon(Icons.seven_k),
)),
Positioned(
right: 0,
child: Container(
color: Colors.blue,
width: 50,
height: 50,
child: Icon(Icons.qr_code),
)),
发现位置按照想要的位置属性改变了。
位置的变化还可以使用margin。那么什么时候用margin什么时候用positioned呢?如果只是为了挪动位置,那么就用positioned,如果要和外界的部件保持边距,就用margin。如果用margin的话,如果外面是一个Expand,那么会因为margin的变化而增加大小。
3. AspectRatio
AspectRatio可以提供父部件一个宽高比。AspectRatio中aspectRatio是必传的属性。
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0),
child: Container(
color:Colors.red,
height: 150,
width: 100,
child: AspectRatio(
aspectRatio: 1/2
),
),
);
运行后发现没有效果。这是因为父部件同时给height和width赋值的情况下,AspectRatio就不起效果了。
把height或者width去掉后就会有效果了。
4. 状态管理
状态是一个概念。在iOS中,任何控件都有一些响应,响应后都会改变样式。在flutter中只要部件有变化,就需要改变状态。在flutter中是增量渲染,如果属性发生则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。Flutter中页面逻辑和数据逻辑是分开的,它会保留数据,替换页面。Flutter设置了一个保留数据逻辑的widget就是StatefulWidget,StatefulWidget里面封装了数据逻辑和页面逻辑。
创建一个StatelessWidget,在点击的时候进行count++。
class StateManagerDemo extends StatelessWidget {
//const StateManagerDemo({Key? key}) : super(key: key);
int count = 0;
@override
Widget build(BuildContext context) {
return
Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text("Layout Demo"),
),
body: Center(
child: Chip(label:Text("$count"),),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
count++;
print("$count");
},
child: const Icon(Icons.add),
),
);
}
}
运行后发现count发生改变,但是ui没有变化。
这个时候就需要使用StatefulWidget了。
class StateManagerDemo extends StatefulWidget {
const StateManagerDemo({Key? key}) : super(key: key);
@override
_StateManagerDemoState createState() => _StateManagerDemoState();
}
class _StateManagerDemoState extends State<StateManagerDemo> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text("Layout Demo"),
),
body: Center(
child: Chip(label:Text("$count"),),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
count++;
print("$count");
},
child: const Icon(Icons.add),
),
);
}
}
这个时候点击后还是没有变化,但是在热重载之后,中间的数字就变了,说明数据还是被保留了下来。因为这里还需要在走一遍build方法才能变化,所以需要把count++放在setState里面,这样页面就会跟着变化了,因为调用setState就会重新build一次,build里面还是增量渲染。
onPressed: () {
setState(() {
count++;
});
print("$count");
},
_StateManagerDemoState类是私有的类,外界无法访问状态管理者,_StateManagerDemoState不能直接接受数据。StateManagerDemo里面则是有对外暴露的所有接口。_StateManagerDemoState可以访问到StateManagerDemo里面的数据然后进行保留。也就是说,数据的管理在_StateManagerDemoState里面。
5. 项目搭建
5.1 底部导航栏
创建一个新工程。将MyHomePage改成下面的结构。
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
);
}
}
将MyHomePage返回的Widget改成Scaffold,并且添加bottomNavigationBar。
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
fixedColor: Colors.green,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.chat), label: "微信"),
BottomNavigationBarItem(icon: Icon(Icons.bookmark), label: "通讯录"),
BottomNavigationBarItem(icon: Icon(Icons.history), label: "发现"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "我"),
],
),
);
接下来要切换选中的item,那么就需要保存数据,那么MyHomePage就要变成StatefulWidget,并且在BottomNavigationBar里面的onTap调用setState修改_currentIndex。
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
fixedColor: Colors.green,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.chat), label: "微信"),
BottomNavigationBarItem(icon: Icon(Icons.bookmark), label: "通讯录"),
BottomNavigationBarItem(icon: Icon(Icons.history), label: "发现"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "我"),
],
onTap: (index){
setState(() {
_currentIndex = index;
});
},
),
);
}
}
接下来创建四个界面,这里先都设置为StatefulWidget。但是平时开发一般整个页面是StatelessWidget的,只有在需要改变的页面才设置为StatefulWidget。然后一家一个包含四个页面的List,在Scaffold中的body使用位置为currentIndex 的页面。
class _MyHomePageState extends State<MyHomePage> {
int _currentIndex = 0;
List<Widget> _pages = [ChatPage(),FriendsPage(),DiscoverPage(),MinePage()];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
fixedColor: Colors.green,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.chat), label: "微信"),
BottomNavigationBarItem(icon: Icon(Icons.bookmark), label: "通讯录"),
BottomNavigationBarItem(icon: Icon(Icons.history), label: "发现"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "我"),
],
onTap: (index){
setState(() {
_currentIndex = index;
});
},
),
);
}
}
这时候点击的话,就可以切换页面了。
看到上面点击切换的时候,会有动画,我们不需要这个动画,那么可以在MaterialApp把其去掉。在MaterialApp中的ThemeData修改highlightColor和splashColor,这样动画就消失了。可以修改primarySwatch来修改主题色。
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
highlightColor: const Color.fromRGBO(1, 0, 0, 0.0),
splashColor: const Color.fromRGBO(1, 0, 0, 0.0),
primarySwatch: Colors.green,
),
home: const MyHomePage(),
);
}
这里还有一个字体变大的动画,那么可以在BottomNavigationBar修改selectedFontSize。
selectedFontSize: 12,
5.2 程序的名字以及图标
来到android——app——src——main的AndroidManifest.xml。这里可以修改程序的名字以及图标。
android:label="哈喽哈喽"
要使用图标的话就要先导入图片。点开上面的res,然后看到这里有,hdpi(相当于iOS的1x),mdpi(相当于iOS的1.5x),xhdpi(相当于iOS的2x),xxhdpi(相当于iOS的3x),xxxhdpi(相当于iOS的4x)。将图片复制到对应的文件夹,命名不能是驼峰以及包含中文。
android:icon="@mipmap/app_icon">
启动图片在drawable以及drawable-v21里面配置。
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item>
5.3 Assets
Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。常见类型的 assets 包括静态数据(例如JSON文件)、配置文件、图标和图片等。
这里添加一个Images文件夹,然后在images文件夹里面放图片。
Flutter 使用pubspec.yaml 文件来管理应用程序所需的资源,包的管理也是在pubspec.yaml 文件中的。在文件中添加引用,这里的格式非常重要,多一个少一个空格都不行。
接下来就可以去 BottomNavigationBar将icon修改成自己的图片,这里需要设置activeIcon来区分选中和未选中状态。
BottomNavigationBarItem(icon: Image(image: AssetImage("images/tabbar_chat.png"),height: 20,width: 20,),activeIcon: Image(image: AssetImage("images/tabbar_chat_hl.png"),height: 20,width: 20,), label: "微信"),
BottomNavigationBarItem(icon: Image(image: AssetImage("images/tabbar_friends.png"),height: 20,width: 20,),activeIcon: Image(image: AssetImage("images/tabbar_friends_hl.png"),height: 20,width: 20,), label: "通讯录"),
BottomNavigationBarItem(icon: Image(image: AssetImage("images/tabbar_discover.png"),height: 20,width: 20,),activeIcon: Image(image: AssetImage("images/tabbar_discover_hl.png"),height: 20,width: 20,), label: "发现"),
BottomNavigationBarItem(icon: Image(image: AssetImage("images/tabbar_mine.png"),height: 20,width: 20,), activeIcon: Image(image: AssetImage("images/tabbar_mine_hl.png"),height: 20,width: 20,),label: "我"),