web analytics

การใช้ Raw Keyboard ใน Flutter

สวัสดีผู้อ่านครับ บล็อกนี้เป็นเรื่องเบาๆเกี่ยวกับการใช้งาน Keyboard ใน Flutter ครับ (Physical Keyboard) ตอนนี้ Flutter รองรับ Desktop แล้ว ใช้งานได้ทั้ง Windows , Linux , MacOS นั่นทำให้การทำแอปหรือโปรแกรมสำหรับ Desktop ไม่ใช่เรื่องยาก ซึ่ง Flutter ได้เตรียม Widget รองรับการทำ Interact / Action ต่างๆใน Desktop หนึ่งในนั้นคือ Input อย่าง Keyboard ที่ใน Desktop มักจะถูกใช้เป็น Shortcut ต่างๆ

จะเป็นอย่างไร มาลองเล่นกัน

เริ่มต้น

เริ่มจาก ผมได้สร้างแอปเปล่าๆขึ้นมาแบบนี้ก่อน

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: const Center(
        child: Text(
          'Keyboard !',
          style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }

FocusNode

การจับ event ของ keyboard จะใช้งานคู่กับ FocusNode
FocusNode เป็น object อันหนึ่งที่เก็บค่า state เกี่ยวกับการ focus Widget หมายความว่า ถ้าเรากำหนด FocusNode ให้กับ Widget ตัว Widget นั้นจะมีพฤติกรรมที่หลากหลายขึ้น เราจะสามารถจัดการกับพฤติกรรมของ user ได้มากขึ้น

class _MyHomePageState extends State<MyHomePage> {
  FocusNode mainFocusNode = FocusNode();

RawKeyboardListener

Widget ตัวเอกของเรื่อง Keyboard คือ widget ตัวนี้ ชื่อว่า RawKeyboardListener เพียงเพิ่มมันเป็น parent ของ widget ที่ต้องการ และกำหนด FocusNode ที่เราเตรียมไว้ให้มัน และเมื่อถูก focus แล้ว หากเรากดที่ปุ่มคีบอร์ด เราจะได้ข้อมูล Key จาก method ที่ชื่อว่า onKey(RawKeyEvent)


class _MyHomePageState extends State<MyHomePage> {
  FocusNode mainFocusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: RawKeyboardListener(
        focusNode: mainFocusNode,
        autofocus:true,
        onKey: (RawKeyEvent event) {
          //
        },
        child: const Center(
          child: Text(
            'Keyboard !',
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}

onKey จะมาพร้อมกับ object ที่ชื่อว่า RawKeyEvent โดยจะมี 2 event คือ
RawKeyDownEvent มาตอนเรากดปุ่มลง
RawKeyUpEvent มาตอนเรายกนิ้วขึ้น
ทั้งสืบทอดมาจาก RawKeyEvent และเราสามารถดูค่าปุ่มที่กดจาก logicalKey

RawKeyEvent

    onKey: (RawKeyEvent event) {
        print(event.runtimeType.toString());
        print(event.logicalKey.keyLabel);
    }

ดังนั้นเมื่อเรา กดปุ่ม onKey จะถูกเรียก 2 ครั้งนั่นเอง ดังนั้นเราต้องเช็คเงื่อนดีๆ

// Console
flutter: RawKeyDownEvent
flutter: RawKeyUpEvent
flutter: A
flutter: A

Request Focus

อีกจุดที่สำคัญคือ field ที่ชื่อว่า autofocus ของ RawKeyboardListener ซึ่ง default มันจะเป็น false
โดยหากเป็น true มันจะกำหนดค่าเริ่มต้นเป็นสถานะถูกโฟกัส

หากเราต้องการ focus แบบ manual เราจะใช้คำสั่ง requestFocus()

  @override
  Widget build(BuildContext context) {

    // Add request focus.
    return GestureDetector(
      onTap: (){
        mainFocusNode.requestFocus();
      },
      child: Scaffold(
        ...

ลองใช้งาน RawKeyboardListener

ลองมาใช้งาน RawKeyboardListener กัน โดยผมจะทำแอปง่ายๆ คือกดที่ keyboard แล้ว จะแสดง list ของปุ่มที่เรากด
ก่อนอื่นประกาศ List ของ LogicalKeyboardKey สำหรับเก็บข้อมูล

class _MyHomePageState extends State<MyHomePage> {
  ...
  List<LogicalKeyboardKey> listKeys = [];

จากนั้นเพิ่มให้กดปุ่มคีบอร์ดแล้วเพิ่มใน list พร้อมกับแสดง ListView

@override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        mainFocusNode.requestFocus();
      },
      child: Scaffold(
        body: RawKeyboardListener(
          focusNode: mainFocusNode,
          autofocus: true,
          onKey: (RawKeyEvent event) {
            if (event is RawKeyDownEvent) {
              setState(() {
                listKeys.add(event.logicalKey);
              });
            }
          },
          child: Container(padding: EdgeInsets.all(32),
            child: Column(
              children: [
                const Text(
                  'Keyboard !',
                  style: TextStyle(fontSize: 20),
                ),
                Expanded(
                  child: ListView(
                    children: [
                      for (LogicalKeyboardKey key in listKeys)
                        Text(
                          key.keyLabel,
                          style: TextStyle(fontSize: 16),
                        )
                    ],
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }

เมื่อกดคีบอร์ด ก็จะแสดงชื่อปุ่มดังนี้

Multi-RawKeyboardListener

ทีนี้ลองใช้งาน RawKeyboardListener ในกรณีที่มีหลายอัน ว่าจะได้ไหม เพราะโปรแกรมเราอาจจะรองรับ keyboard ต่างกันในแต่ละ widget ก็ได้ โดยตัวอย่างนี้ผมจะทำแอปที่แสดง ListView 2 อัน สามารถกดเลือกที่ ListView ที่ต้องการ เมื่อพิมพ์คีบอร์ด ค่าของปุ่มนั้นก็จะแสดงที่ ListView นั้น

ก่อนอื่น ประกาศ FocusNode และ List<LogicalKeyboardKey> อย่างละ 2 อัน ผมขอเรียกว่า Zone A , Zone B

class _MyHomePageState extends State<MyHomePage> {
  FocusNode zoneAFocusNode = FocusScopeNode();
  FocusNode zoneBFocusNode = FocusScopeNode();
  List<LogicalKeyboardKey> listKeyZoneA = [];
  List<LogicalKeyboardKey> listKeyZoneB = [];

จากนั้นก็ใช้หลักการเดิม เพียงแต่ใช้ RawKeyboardListener อีกตัวเพิ่มเข้ามา และเรียกคำสั่ง requestFocus() , unfocus() ของ NodeFocus ให้ถูกต้อง เขียนโค้ดประมาณนี้

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        padding: EdgeInsets.all(32),
        child: Row(
          children: [
            Expanded(
              child: RawKeyboardListener(
                focusNode: zoneAFocusNode,
                onKey: (RawKeyEvent event) {
                  if (event is RawKeyDownEvent) {
                    setState(() {
                      listKeyZoneA.add(event.logicalKey);
                    });
                  }
                },
                child: GestureDetector(
                  onTap: () {
                    setState(() {
                      zoneAFocusNode.requestFocus();
                      zoneBFocusNode.unfocus();
                    });
                  },
                  child: Container(
                    decoration: BoxDecoration(
                        color: Colors.blue[100],
                        border: zoneAFocusNode.hasFocus ? Border.all(
                            width: 4, color: Colors.blue[300]!) : null),
                    padding: EdgeInsets.all(16),
                    child: Column(
                      children: [
                        const Text(
                          'Zone A',
                          style: TextStyle(fontSize: 20),
                        ),
                        Expanded(
                          child: ListView(
                            children: [
                              for (LogicalKeyboardKey key in listKeyZoneA)
                                Text(
                                  key.keyLabel,
                                  style: TextStyle(fontSize: 16),
                                )
                            ],
                          ),
                        )
                      ],
                    ),
                  ),
                ),
              ),
            ),
            SizedBox(
              width: 16,
            ),
            Expanded(
              child: RawKeyboardListener(
                  focusNode: zoneBFocusNode,
                  onKey: (RawKeyEvent event) {
                    if (event is RawKeyDownEvent) {
                      setState(() {
                        listKeyZoneB.add(event.logicalKey);
                      });
                    }
                  },
                  child: GestureDetector(
                    onTap: () {
                      setState(() {
                        zoneBFocusNode.requestFocus();
                        zoneAFocusNode.unfocus();
                      });
                    },
                    child: Container(
                      decoration: BoxDecoration(
                          color: Colors.red[100],
                          border: zoneBFocusNode.hasFocus ? Border.all(
                              width: 4, color: Colors.red[300]!) : null),
                      padding: EdgeInsets.all(16),
                      child: Column(
                        children: [
                          const Text(
                            'Zone B',
                            style: TextStyle(fontSize: 20),
                          ),
                          Expanded(
                            child: ListView(
                              children: [
                                for (LogicalKeyboardKey key in listKeyZoneB)
                                  Text(
                                    key.keyLabel,
                                    style: TextStyle(fontSize: 16),
                                  )
                              ],
                            ),
                          )
                        ],
                      ),
                    ),
                  )),
            ),
          ],
        ),
      ),
    );
  }

จะได้โปรแกรมง่ายๆ แบบนี้แล้ว

Logical Key และ Physical Key

ที่น่าสนใจ คือ RawKeyEvent นั้น จะมี field 2 ตัวที่เกี่ยวกับ Key คือ Logical Key และ Physical Key
ซึ่งข้อแตกต่างของมันคือ Logical Key จะเป็นค่าของปุ่มตามระบบ ในขณะที่ Physical Key เป็นค่าที่ปุ่มที่มากับ Hardware ของจริงๆ เช่น เราสามารถกำหนดใน OS ให้ปุ่ม A กดแล้วเป็นค่า Z ก็ได้ ซึ่งมักจะถูกใช้ในพวกเกมที่ต้องใช้จอยสติ๊ก ที่สามารถแก้ให้ปุ่ม A B ทำอย่างอื่นได้

Multi-Key pressed

แน่นอนว่าโปรแกรมที่รองรับ Shrtcut จาก keyboard มักจะไม่ได้กดเพียงปุ่มเดียว มักจะต้องกด Ctrl หรือ Shift ด้วย และมักมี 2-3 ปุ่มที่ต้องกด เช่น Ctrl + Shift + C แบบนี้เราจะเขียนโปรแกรมอย่างไร

วิธีการง่ายมาก เพราะ RawKeyEvent นั้นมี method get ต่างๆมาให้อยู่แล้ว เราสามารใช้ isControlPressed ได้เลย เพื่อเช็คว่าปุ่ม Ctrl ถูกกดอยู่หรือไม่ ซึ่งจะเห็นว่ามีทั้ง Shift , Control , Alt , Meta (Meta คือ ปุ่ม Windows หรือ ปุ่ม Command)

ดังนั้นเราสามารถเขียน การกดปุ่ม Ctrl + Shift + C ได้ดังนี้

    RawKeyboardListener(
                focusNode: zoneAFocusNode,
                onKey: (RawKeyEvent event) {
                  if (event is RawKeyDownEvent) {
                    if (event.isControlPressed &&
                        event.isShiftPressed &&
                        event.logicalKey == LogicalKeyboardKey.keyC) {
                      print("This is Ctrl + Shift + C");
                    }
                  }
                },
                child: ...

RawKeyEventData

อ่านมาถึงตรงนี้ จะเห็นว่า Flutter ช่วยจัดการ Key Event ของ Platform ต่างๆให้มาอยู่ใน RawKeyEvent ของ Flutter ได้เป็นอย่างดี แต่ในมุมหนึ่งมันทำให้ข้อมูลจาก Native Platform มันถูกเปลี่ยนแปลงไป ดังนั้นหากเราต้องการจัดการกับ ค่าของ Key Event จาก Native จริงๆ Flutter ก็เพิ่ม RawKeyEventData มาให้ใน RawKeyEvent เลย โดยจะอยู่ใน field ที่ชื่อว่า data ซึ่งเราสามารถ cast มาเป็น RawKeyEventData ของ Platform ที่เรากำลังใช้อยู่ได้

จบแล้ว

สำหรับบทความนี้เกี่ยวกับการเขียน Flutter จัดการกับ Keyboard event ซึ่ง Flutter ได้เตรียม widget มาให้จัดการเรียบร้อยแล้ว ใช้งานไม่ยากเลย รองรับทั้ง Web และ Desktop หวังว่าบทความนี้จะมีประโยชน์กับผู้อ่านนะครับ

ในบทความถัดไป เป็นเรื่องต่อเนื่องกันเกี่ยวกับการทำ Shortcut โดยใช้กลไก Actions และ Shortcut Widget