การเขียนแอปแบบกราฟฟิกอินเทอร์เฟส สิ่งที่ขาดไม่ได้เลยสำหรับการทำงานกับงาน 2D คือ หน้าจอที่จะใช้แสดงผลว่ามีโครงสร้างเป็นอย่างไร เช่น ขนาดกว้าง ยาว ขอบภายใน แนวนอน แนวตั้ง สิ่งเหล่านี้จำเป็นต้องทราบเพื่อจะได้ออกแบบหน้าจอส่วนติดต่อกับผู้ใช้งานได้มีประสิทธิภาพและใช้งานได้ง่าย การเข้าถึงข้อมูลเหล่านี้สามารถทำได้ผ่าน MediaQuery class
ตัว MediaQuery class เป็น InheritedWidget ที่มีข้อมูลของหน้าจอที่เป็นพื้นที่สำหรับแสดงผล การเข้าถึงข้อมูลสามารถใช้คำสั่ง .of()
หาก widget ที่เรียกใช้คำสั่งนี้มี MediaQuery ใน widget tree สายเดียวกัน ก็จะคืนค่าเป็น MediaQueryData
กลับมา แต่หากไม่เจอจะเกิด Exception error แทน ในกรณีที่ไม่แน่ใจสามารถใช้คำสั่ง .maybeOf()
เพื่อคืนค่ากลับมาเป็น null
หากไม่เจอ MediaQuery ใน widget tree เดียวกัน
import 'package:flutter/material.dart';
import 'dart:developer';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
MediaQueryData m = MediaQuery.of(context); // get MediaQuery from BuildContext
log(m.padding.toString()); // output → [log] EdgeInsets.zero
log(m.size.toString()); // output → [log] Size(1920.0, 1017.0)
return const MaterialApp(home: Scaffold(body: Center(child: Text('hello'))));
}
}
ข้อมูลจาก MediaQuery จะมาจาก parent ของ MainApp อีกที
ถ้าลองย่อขนาดหน้าจอ จะพบว่าตัว framework จะทำการ rebuild ตัว widget ใหม่ โดยค่าที่ได้จาก MediaQuery จะเปลี่ยนแปลงตามค่าปัจจุบัน
ลองย่อหน้าต่างของแอปดู จะเห็นว่าข้อมูลจาก log จะเปลี่ยน
ตัว MediaQuery ไม่ว่าจะเรียกจาก widget ตัวไหนก็ตามใน widget tree ก็จะได้ค่าเดียวกัน ตัวอย่าง แบ่งหน้าจอเป็นสองฝั่ง แล้วลองเรียก MediaQuery จาก widget MyText
ที่อยู่ข้างใน ผลที่ได้จะเป็นผลเดียวกันตัว MainApp
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
MediaQueryData m = MediaQuery.of(context);
log(m.padding.toString());
log(m.size.toString());
return const MaterialApp(
home:
Scaffold(
body: Row(children: [
Expanded(flex: 1, child: MyText('left')),
Expanded(flex: 2, child: MyText('right'))
])));
}
}
class MyText extends StatelessWidget {
final String displayText;
const MyText(this.displayText, {super.key});
@override
Widget build(BuildContext context) {
MediaQueryData m = MediaQuery.of(context);
log('$displayText: ${m.padding}');
log('$displayText: ${m.size}');
return Text(displayText);
}
}
ผลที่ได้จาก MediaQuery ใน MyText ก็เท่ากับ MainApp
การอ่านค่าหน้าจอของแอปจาก MediaQuery ด้วยคำสั่ง .of()
ขนาดหน้าจอที่แสดงผลของแอปในหน่วย logical pixels ซึ่งจะไม่เท่ากับ physical pixels เสมอไป ตัว framework จะคำนวณตามค่า devicePixelRatio เช่น จอ Smart phone มีความละเอียดสูง ค่านี้ก็จะสูงตาม
MediaQueryData m = MediaQuery.of(context);
log("Screen width: ${m.size.width}px.");
log("Screen height: ${m.size.height}px.");
หรือจะใช้คำสั่ง .sizeOf()
เพื่อดึงมาเฉพาะข้อมูล Size ก็ได้
Size s = MediaQuery.sizeOf(context);
log("Screen width: ${s.width}px.");
log("Screen height: ${s.height}px.");
หากต้องการทราบถึงจำนวนจุด pixels จริง ๆ บนหน้าจอ สามารถใช้คำสั่งตามตัวอย่าง หากทดสอบบนหน้าจอ PC ที่กำหนดค่า dpi ตามปกติ จะได้ค่าเท่ากับ devicePixelRatio = 1.0 แต่ถ้าหากอ่านค่าจากพวกอุปกรณ์พกพาจะได้ค่าอื่น เช่น Nexus 6 จะอ่านค่า devicePixelRatio = 3.5 หรือ Apple iPhone 14 จะอ่านค่า devicePixelRatio = 3 เป็นต้น จากนั้นเอามาคูณกับค่า pixel ที่อ่านได้จาก .size
MediaQueryData m = MediaQuery.of(context);
log("devicePixelRatio: ${m.devicePixelRatio}"); // output → [log] devicePixelRatio: 1.0
log("Screen physical width: ${m.size.width * m.devicePixelRatio}px.");
log("Screen physical height: ${m.size.height * m.devicePixelRatio}px.");
// read only devicePixelRatio
double d = MediaQuery.devicePixelRatioOf(context);
log("devicePixelRatio: $d"); // output → [log] devicePixelRatio: 1.0
ตั้งค่า Scaling ใน Setting ของ Windows จะมีผลต่อค่า devicePixelRatio
เนื่องจากในอุปกรณ์ smart phone หรือ tablet มักจะมี System UI ที่เข้ามาบดบังพื้นที่การแสดงผลของแอป เช่น
จึงทำให้มีส่วนที่ตัว framework ไม่สามารถวาด UI หรือตรวจสอบการแตะหรือวาดนิ้วได้ โดยค่าเหล่านี้จะเก็บไว้ใน EdgeInsets จะอ่านได้จากคำสั่ง
ตำแหน่งพื้นที่ที่ไม่สามารถทำงานได้
หากมีความจำเป็นต้องการเขียนแอปบน smart phone หรือ tablet สามารถใช้ SafeArea class เพื่อป้องกันการแสดง widget ไปยังพื้นที่ที่ผู้ใช้ไม่สามารถแตะหรือวาดนิ้วได้
เปรียบเทียบระหว่างการไม่ใช้และใช้งาน SafeArea
การตรวจสอบแนวหน้าจอหรือ Screen orientation สามารถอ่านได้จากคำสั่ง .orientation
หรือจากคำสั่ง MediaQuery.orientationOf()
Orientation.portrait
จอวางในแนวตั้งOrientation.landscape
จอวางในแนวนอนจากการทดสอบหากจอเป็นสี่เหลี่ยมจัตุรัส ผลที่ได้จะเป็น Orientation.portrait
ทดสอบปรับขนาดแอปเพื่อดูค่า orientation
.textScalerOf()
→ TextScaler
.boldTextOf()
→ true
ถ้า platform ระบุว่าต้องการให้แสดงผลให้มีความหนา/เข้มเป็นพิเศษ.highContrastOf()
→ true
ถ้า platform ระบุว่าต้องการให้แสดงผลสีแบบ high contrast (มีเฉพาะ iOS).invertColorsOf()
→ true
ถ้า platform ระบุว่าต้องการให้แสดงผลสีแบบ invert (มีเฉพาะ iOS).platformBrightnessOf()
→ Brightness
.alwaysUse24HourFormatOf()
→ true
ถ้า platform ระบุว่าต้องการให้แสดงผลแบบ 24H.disableAnimationsOf()
→ true
ถ้า platform ระบุว่าต้องการลด/ไม่แสดงเอฟเฟกเคลื่อนไหว.displayFeaturesOf()
→ DisplayFeature
เช่น อุปกรณ์ที่มีสองจอ จอพับได้.onOffSwitchLabelsOf()
→ true
ถ้า platform ระบุว่าต้องการให้แสดงข้อความ on/off ข้างในสวิตช์ด้วย (มีเฉพาะ iOS).navigationModeOf()
→ NavigationMode
หากเขียนแอปบน platform ที่มีการใช้อุปกรณ์ในการเปลี่ยนเส้นทางของแอป เช่น การใช้รีโมทบนแอปที่ทำงานบนทีวีเนื่องจากการอ่านข้อมูลจาก MediaQuery จะเหมือนกับการอ่านข้อมูลจาก InheritedWidget ที่ใช้คำสั่ง dependOnInheritedWidgetOfExactType<T>()
widget ตัวไหนที่อ่านด้วยวิธีดังกล่าวจะถูกผูกกับ MediaQuery หาก MediaQuery เกิดการเปลี่ยนแปลง จะทำให้ widget ตัวดังกล่าวถูก rebuilt
.of()
เพื่ออ่านข้อมูล.alwaysUse24HourFormatOf()
เพราะถ้าข้อมูลส่วนอื่นของ MediaQuery เปลี่ยนแปลงแต่ส่วนนี้ไม่ได้เปลี่ยน ก็จะไม่กระทบกับ widget และไม่ต้องถูก rebuilt ใหม่.getInheritedWidgetOfExactType()
MediaQuery? mediaQuery = context.getInheritedWidgetOfExactType<MediaQuery>();
assert(mediaQuery != null);
MediaQueryData m = mediaQuery!.data;
log("light:${m.platformBrightness} ");
log("Viewport width: ${m.size.width}px.");
log("Viewport height: ${m.size.height}px.");
log("Orientation: ${m.orientation}");