主題風格、屏幕適配
主題
樣式統一管理
全局樣式
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: <Widget>[
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<MediaQuery>()!.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
工具封裝
工具類方式
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方法不需要寫()