flutter 主題風格、屏幕適配 主題風格、屏幕適配

主題風格、屏幕適配

主題

樣式統一管理

全局樣式

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

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方法不需要寫()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章