主题风格、屏幕适配
主题
样式统一管理
全局样式
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// 1.亮度: light-dark
brightness: Brightness.light,
// 2.primarySwatch: primaryColor/accentColor的结合体
primarySwatch: Colors.red,
// 3.主要颜色: 导航/底部TabBar
primaryColor: Colors.pink,
// 4.次要颜色: FloatingActionButton/按钮颜色
accentColor: Colors.orange,
// 5.卡片主题
cardTheme: CardTheme(
color: Colors.greenAccent,
elevation: 10,
shape: Border.all(width: 3, color: Colors.red),
margin: EdgeInsets.all(10)
),
// 6.按钮主题
buttonTheme: ButtonThemeData(
minWidth: 0,
height: 25
),
// 7.文本主题
textTheme: TextTheme(
title: TextStyle(fontSize: 30, color: Colors.blue),
display1: TextStyle(fontSize: 10),
)
),
home: XXTHomePage(),
);
}
}
Text组件使用全局主题:
body: Center(
child: Column(
children: [
Text("Hello World"),
Text("Hello World", style: TextStyle(fontSize: 14),),
Text("Hello World", style: TextStyle(fontSize: 20),),
Text("Hello World", style: Theme.of(context).textTheme.body2,),
Text("Hello World", style: Theme.of(context).textTheme.display3,),
Switch(value: true, onChanged: (value) {},),
CupertinoSwitch(value: true, onChanged: (value) {}, activeColor: Colors.red,),
RaisedButton(child: Text("R"), onPressed: () {},),
Card(child: Text("你好啊,李银河", style: TextStyle(fontSize: 50),),)
],
),
),
局部Theme
如果某个具体的Widget不希望直接使用全局的Theme,而希望自己来定义,应该如何做呢?
非常简单,只需要在Widget的父节点包裹一下Theme即可
创建另外一个新的页面,页面中使用新的主题:
在新的页面的Scaffold外,包裹了一个Theme,并且设置data为一个新的ThemeData
class XXTSecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(),
child: Scaffold(
),
);
}
}
新的主题
但是,我们很多时候并不是想完全使用一个新的主题,而且在之前的主题基础之上进行修改:
class XXTSecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(
primaryColor: Colors.greenAccent
),
child: Scaffold(
),
);
}
}
特殊问题: accentColor在这里并不会被覆盖。
//页面内局部theme,无法改变accentColor,widget内设置accentColor也不生效
Theme(
data: Theme.of(context).copyWith(
accentColor: Colors.greenAccent
),
child: FloatingActionButton(
),
);
//需要通过以下方式设置
floatingActionButton: Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
secondary: Colors.pink
)
),
child: FloatingActionButton(
child: Icon(Icons.pets),
onPressed: () {
},
),
)
官方回答
页面背景色
项目一般都是有自己的颜色风格,设置统一的主题,页面背景色等。使用canvasColor,然后某页面如果有特殊的背景色,可以在Scafold中设置backgroundColor
暗黑模式适配
MaterialApp中有theme和dartTheme两个参数:
按照下面的写法,我们已经默认适配了暗黑主题
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.light(),
darkTheme: ThemeData(
primaryColor: Colors.grey,
primaryTextTheme: TextTheme(
title: TextStyle(
color: Colors.white,
fontSize: _titleFontSize
)
),
textTheme: TextTheme(
title: TextStyle(color: Colors.white),
body1: TextStyle(color: Colors.white70)
)
);
home: XXTHomePage(),
);
}
}
屏幕适配
Flutter中的单位
在进行Flutter开发时,我们通常不需要传入尺寸的单位,那么Flutter使用的是什么单位呢?
- Flutter使用的是类似于iOS中的点pt,也就是point。
- 所以我们经常说iPhone6的尺寸是375x667,但是它的分辨率其实是750x1334。
- 因为iPhone6的dpr(devicePixelRatio)是2.0,iPhone6plus的dpr是3.0
iPhone设备参数
在Flutter开发中,我们使用的是对应的逻辑分辨率
Flutter设备信息
// 1.媒体查询信息
final mediaQueryData = MediaQuery.of(context);
// 2.获取宽度和高度
final screenWidth = mediaQueryData.size.width;
final screenHeight = mediaQueryData.size.height;
final physicalWidth = window.physicalSize.width;
final physicalHeight = window.physicalSize.height;
final dpr = window.devicePixelRatio;
print("屏幕width:$screenWidth height:$screenHeight");
print("分辨率: $physicalWidth - $physicalHeight");
print("dpr: $dpr");
// 3.状态栏的高度
// 有刘海的屏幕:44 没有刘海的屏幕为20
final statusBarHeight = mediaQueryData.padding.top;
// 有刘海的屏幕:34 没有刘海的屏幕0
final bottomHeight = mediaQueryData.padding.bottom;
print("状态栏height: $statusBarHeight 底部高度:$bottomHeight");
注意一个知识点:
获取屏幕宽高的时候,如果是在MyApp的build函数中执行的,会报错,大致意思是MediaQuery还没有初始化完,这个可以看下MediaQuery的源码
源码流程:
//1、获取屏幕宽度的代码:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
return MaterialApp(
);
}
}
//2、width是从size获取,size是从MediaQuery.of(context)得来,那么看MediaQuery.of(context)的逻辑
static MediaQueryData of(BuildContext context) {
assert(context != null);
assert(debugCheckHasMediaQuery(context));
return context.dependOnInheritedWidgetOfExactType()!.data;
}
of函数获取是的MediaQueryData,并且是基于MediaQuery获取的,然后获取data,,这能感觉到是先生成的data,然后通过of(context)获取的
//3、那么就找data初始化的地方。
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
...
...
}
//3.1、看MediaQueryData初始化的逻辑,,代码就略了,,内部看到也是基于window获取,然后计算的,,所以,可以有一些启发,,外部我们使用的时候即使不用MediaQuery,自己也可以通过window计算
//分析
以上是MediaQuery.of(context)的代码逻辑,重点是MediaQuery.of(context)函数中的断言,debugCheckHasMediaQuery(context),,这里给出了原因:
No MediaQuery ancestor could be found starting from the context that was passed to MediaQuery.of(). This can happen because you have not added a WidgetsApp, CupertinoApp, or MaterialApp widget (those widgets introduce a MediaQuery), or it can happen if the context you use comes from a widget above those widgets.
大致意思就是用到MediaQueryData的时候,但是WidgetsApp, CupertinoApp, or MaterialApp还不存在,或者说还没创建完,,,
总结:所以,如果要用MediaQuery.of(context)获取屏幕参数,不要在程序入口的build内获取,,,或者,通过window自己去获取计算
获取一些设备相关的信息,可以使用官方提供的一个库:
dependencies:
device_info: ^0.4.2+1
适配方案
小程序中以iPhone6作为设计稿,宽度375,分辨率750
rpx适配:小程序中rpx的原理是什么呢?
不管是什么屏幕,统一分成750份
在iPhone5上:1rpx = 320/750 = 0.4266 ≈ 0.42px
在iPhone6上:1rpx = 375/750 = 0.5px
在iPhone6plus上:1rpx = 414/750 = 0.552px
。。。
- 屏幕适配也可以使用第三方库:flutter_screenutil
flutter_screenutil
工具封装
工具类方式
class XXTSizeFit {
// 1.基本信息
static double physicalWidth;
static double physicalHeight;
static double screenWidth;
static double screenHeight;
static double dpr;
static double statusHeight;
static double rpx;
static double px;
static void initialize({double standardSize = 750}) {
// 1.手机的物理分辨率
physicalWidth = window.physicalSize.width;
physicalHeight = window.physicalSize.height;
// 2.获取dpr
dpr = window.devicePixelRatio;
// 3.宽度和高度
screenWidth = physicalWidth / dpr;
screenHeight = physicalHeight / dpr;
// 4.状态栏高度
statusHeight = window.padding.top / dpr;
// 5.计算rpx的大小
rpx = screenWidth / standardSize;
px = screenWidth / standardSize * 2;
}
static double setRpx(double size) {
return rpx * size;
}
static double setPx(double size) {
return px * size;
}
}
使用
body: Center(
child: Container(
width: XXTSizeFit.setPx(200),
height: XXTSizeFit. setRpx(400),
color: Colors.red,
alignment: Alignment.center
),
)
extension
extension DoubleFit on double {
double px() {
return XXTSizeFit.setPx(this);
}
double rpx() {
return XXTSizeFit.setRpx(this);
}
}
使用
body: Center(
child: Container(
width: 200.px(),
height: 400.rpx(),
color: Colors.red,
alignment: Alignment.center
),
)
这种方式是不是比单纯的工具类方式方便多了,extension是dart语法,,跟ios的category差不多,category是种类/分类的意思
flutter中extension的使用也大致有两个好处:
- 增加类自定义的方法
- 扩展可以更好的分类方法集
但是200.px()
这种方式使用还是不像android中200px那样简洁,,给extension扩展get方法
extension - get方法
extension DoubleFit on double {
double get px {
return XXTSizeFit.setPx(this);
}
double get rpx {
return XXTSizeFit.setRpx(this);
}
}
使用
body: Center(
child: Container(
width: 200.px,
height: 400.rpx,
color: Colors.red,
alignment: Alignment.center
)
)
注意,扩展的get方法不需要写()