สแกน QR Code และ Barcode ด้วย mobile scanner ใน Flutter

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
url launcher flutter ringtone player mobile scanner flutter

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ url_launcher flutter_ringtone_player mobile_scanner flutter

ดูแล้ว 436 ครั้ง


บทความนี้ต่อยอดจากตอนที่แล้ว ที่เราได้รู้จักกับการสร้าง qrcode
เพื่อใช้งาน แชร์ หรือส่งต่อไปยังแอปอื่นหรือบันทึกลงไว้ในเครื่องไปแล้ว
เนื้อหาตอนนี้เราจะมาสร้างส่วนที่เราใช้สแกนหรือการตรวจจับ qrcode
เพื่ออ่านค่า และนำค่าที่ได้ไปใช้งาน โดยเราจะใช้ plugin ที่ชื่อว่า 
mobile_scanner ในการจัดการ
 
ทบทวนบทความตอนที่แล้วได้ที่
สร้างคิวอาร์โค้ดในแอป Flutter ด้วย qr flutter อย่างง่าย http://niik.in/1120
 
 

ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง

    แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
 
  mobile_scanner: ^5.2.3
  flutter_ringtone_player: ^4.0.0+3
  url_launcher: ^6.3.0
 
    สำหรับ flutter_ringtone_player และ url_launcher ที่เพิ่มเข้ามา ก็เพื่อให้เราสามารถจัดการ
บางอย่างได้มากขึ้น โดย flutter_ringtone_player เราจะใช้สำหรับเล่นไฟล์เสียงจากเครื่อง เพื่อให้
รู้ว่ามีการสแกนและส่งผลลัพธ์กลับมาแล้ว ทำให้เรารับรู้และสังเกตได้ง่าย ส่วน url_launcher เราจะ
ใช้สำหรับจัดการกับรูปแบบข้อความที่สแกนได้เพื่อใช้งานต่อ เช่น ถ้าเป็นลิ้งค์ หรือข้อความ sms หรือ
ค่าอื่นๆ ตามรูปแบบข้อมูล ก็สามารถส่งต่อไปใช้งานได้เลย แทนที่จะแสดงข้อความธรรมดา โดยตัว 
url_launcher จะจัดการกับลิ้งค์หรือข้อความนั้นๆ แล้วส่งต่อไปจัดการได้
 

การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล

    จริงๆ เนื้อหานี้เป็นบทความต่อเนื่อง อย่างไรก็ดี เมื่อมีการใช้กล้อง เราควรเพิ่มการขอสิทธิ์การ
ใช้งานกล้องเข้าไปด้วย ด้านล่างเป็นสิทธิ์ทั้งหมดในโค้ดตัวอย่าง ดาวน์โหลดท้ายบทความ
ไฟล์ android > app > src > main > AndroidManifest.xml
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"/>
    <!-- For Android 13+ -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <application ....
....
</manifest>
 
 

การสแกน Qr Code และ Barcode ด้วย mobile_scanner

    ความสามารถของ mobile_scanner นอกจากจะสแกนคิวอาร์โค้ดแล้ว ยังรองรับการสแกน 
Barcode ได้อีกด้วย อย่างไรก็ตามการใช้งาน plugin ตัวนี้จะมีทั้งรูปแบบที่รวม MLKit 
Barcode-scanning for Android และแบบไม่รวม ซึ่งหากไม่ต้องการตั้งค่าอะไร จะเป็นแบบ
รวมเข้ามาแล้ว แต่ก็จะมีส่วนให้ขนาดไฟล์ของแอปเราเพิ่มขึ้น 3-10 MB โดยประมาณ ขึ้นกับการ
ใช้งาน ในที่นี้เราจะใช้แบบรวมมาแล้ว เพื่อให้รองรับกับ android หรือมือถือที่อาจจะไม่มี Google
Play ได้ และด้วยความสามารถที่น่าพอใจก็ดีพอที่จะใช้งานในรูปแบบนี้
 

ไฟล์ scanner.dart

 
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';


class Scanners extends StatefulWidget {
  static const routeName = '/scanner';

  const Scanners({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _ScannersState();
  }
}

class _ScannersState extends State<Scanners> with WidgetsBindingObserver {
  // กำหนดส่วนควบคุม scanner ปรับค่าต่างๆ
  final MobileScannerController controller = MobileScannerController(
    autoStart: false, // ไม่เริ่มอัตโนมัติ
    torchEnabled: false, // ไม่เปิดแฟลชทันที
    useNewCameraSelector: true, // ใช้การเรียกใช้กล้องแบบ API ใหม่ 
  );

  // กำหนดค่าเก็บผลลัพธ์ ในที่นี้เราจะมีอยู่ 2 ค่า
  // _barcode ค่าที่ได้จากการสแกน และ _result ค่าไว้เก็บชั่วคราวไปส่งต่อ หรือปรับแต่ง
  Barcode? _result;
  Barcode? _barcode;
  // ตัวแปรตรวจจับข้อมูลที่มีการส่งมาอยางต่อเนื่อง เช่น  ข้อมูลเซ็นเซอร์ หรือข้อมูลจากกล้อง
  StreamSubscription<Object?>? _subscription;
  // ตัวจัดการควบคุมข้อมูล String ที่ได้จากการสแกน
  TextEditingController _textController = TextEditingController();

  // ฟังก์ชั่นจัดการข้อมูล barcode ถ้าไม่มีให้มีค่าเป็น null
  void _handleBarcode(BarcodeCapture barcodes) {
    if (mounted) {
      setState(() {
        _barcode = barcodes.barcodes.firstOrNull;
      });
    }
  }

  @override
  void initState() {
    super.initState();
    // ตรวจจับสถานะของแอป เมื่อใช้ร่วมกับ  mixin WidgetsBindingObserver
    WidgetsBinding.instance.addObserver(this);
    // ตรวจจับค่า stream การเปลี่ยนแปลงข้อมูลที่ต่อเนื่อง
    _subscription = controller.barcodes.listen(_handleBarcode);
    // เรียกใช้ controller.start() โดยไม่ต้องรอให้ทำงานเสร็จ
    unawaited(controller.start());
  }

  // ตรวจจับการเปลี่ยนแปลงของสถานะแอปต่างๆ แล้วกำหนดการทำงาน
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (!controller.value.isInitialized) {
      return;
    }

    switch (state) {
      case AppLifecycleState.detached:
      case AppLifecycleState.hidden:
      case AppLifecycleState.paused:
        return;
      // แอปกลับมาทำงานต่อ  
      case AppLifecycleState.resumed:
        // ตรวจจับค่าข้อมูล
        _subscription = controller.barcodes.listen(_handleBarcode);
        unawaited(controller.start());
      case AppLifecycleState.inactive:
        // เมื่อไม่ active หรือเปิดแอปอื่นขึ้นมาใช้งานอยู่
        unawaited(_subscription?.cancel());
        _subscription = null;
        unawaited(controller.stop());
    }
  }

  // ฟังก์ชั่นสำหรับเปิดหรือเรียกใช้ค่าจาก รูปแบบข้อมูล
  Future<void> _launchURL(String url) async {
    try {
      // ตรวจสอบว่ามันเป็น URL หรือไม่
      final Uri? parsedUrl = Uri.tryParse(url);
      
      if (parsedUrl != null && (parsedUrl.isAbsolute || url.startsWith('geo:'))) {
        // ถ้าเป็น URL ที่ถูกต้องหรือ geo link
        if (await canLaunchUrl(parsedUrl)) {
          await launchUrl(parsedUrl);
        } else {
          throw 'xdebug: Could not launch $url';
        }
      } else {
        String finalUrl = url; 
          // ถ้าไม่ใช่ URL ให้ใช้ canLaunchUrlString
          if (await canLaunchUrlString(finalUrl)) {
            await launchUrlString(finalUrl);
          } else {
            throw 'xdebug: Could not launch $url';
          }
      }
    } catch (e) {
      print('Error launching URL: $e');
      // สามารถแสดงข้อความแสดงข้อผิดพลาดให้ผู้ใช้ได้ที่นี่ เช่น:
      // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error launching URL: $e')));
    }
  }

  // การล้างค่าต่างๆ เมื่อไม่มีการใช้งาน
  @override
  Future<void> dispose() async {
    _textController.dispose();
    WidgetsBinding.instance.removeObserver(this);
    unawaited(_subscription?.cancel());
    _subscription = null;
    super.dispose();
    await controller.dispose();
  }

  // ฟังก์ชั่นสำหรับคัดลองข้อความที่สแกน
  Future<void> _copyToClipboard() async {
    await Clipboard.setData(ClipboardData(text: _textController.text));

  // แสดงข้อความเมื่อคัดลอกข้อความแล้ว
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('คัดลอกข้อมูลเรียบร้อยแล้ว')),
    );    
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('สแกน บาร์โค้ด / คิวอาร์โค้ด'),
      ),
      body: SingleChildScrollView(
        child: Column(
          // mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Container(
              height: MediaQuery.of(context).size.height * 0.60, // กำหนดความสูงที่ต้องการ
              child: _buildScanner(context),
            ),            
          //  Expanded(flex: 3, child: _buildScanner(context)),
            Container(
              child: Column(
                // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  // ส่วนของการแสดงข้อมูล เมื่อสแกนแล้ว ไม่เป็นค่า null
                  if (_barcode != null)
                    Builder(builder: (context) {
              //        print("xdebug: ${_barcode!.rawValue!}");
                      // ถ้าผลลลัพธ์ชั่วคราวเป้น null หรือ ค่าเดิมไม่เหมือนค่าใหม่ที่สแกน
                      if (_result == null ||
                          (_result!.displayValue! != _barcode!.displayValue!)) {
                        // เก็บค่าชั่วคราว จากค่าที่เพิ่งสแกนมา    
                        _result = _barcode;
                        // เล่นไฟล์เสียงแจ้งเตือนใช้ค่าเริ่มต้นของระบบ
                        FlutterRingtonePlayer().playNotification();
                      }
                      // กำหนดค่า string ข้อความที่สแกนมาได้
                    /*  _textController.value =
                          TextEditingValue(text: _barcode!.displayValue!); */
                      // กรณีเป็นค่าที่ไม่ใช่ข้อความ เช่น พวก vCard WIFI Event ใช้ค่า rawValue 
                      _textController.value =
                          TextEditingValue(text: _barcode!.rawValue!);       

                      // กำหนดรูปแบบข้อมูลที่รองรับการเปิด้วย url_launcher
                      // ในที่นี้จะไม่รองรับการเปิด WIFI VCard และ Event Calendar
                      final canLaunchList = ['http:','https:','tel:','sms:', 'mailto:', 'geo:'];
                      bool showLaunch = canLaunchList.any((field) => _textController.text.toString().contains(field));

                      return Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Container(
                          width: 300.0,
                          child: TextFormField(
                            controller: _textController,
                            decoration: InputDecoration(
                              fillColor: Color(0xFFfeeeee), // สีพื้นหลัง
                              filled: true, // กำหนดถ้ามีใส่สีพื้นหลัง fillColor
                              prefixIcon: IconButton(
                                  onPressed: _copyToClipboard, // คัดลอกข้อความ
                                  icon: const FaIcon(FontAwesomeIcons.copy)),
                              // ตรวจสอบรูปแบบข้อความ เพื่อกำหนดปุ่มด้านหลัง สามารถเรียกใช้งานหรือไม่                      
                              suffixIcon: showLaunch
                                  ? IconButton(
                                      onPressed: () {
                                        // ถ้าเป็นลิ้งค์ เรียกใช้งานฟังก์ชั่น _launchURL
                                        _launchURL(_textController.text);
                                      },
                                      icon: Builder(builder: (context) {
                                        // ตรวจสอบว่าเป็นลิ้งค์ประเภทไหน เช่น ไลน์ อีเมล sms เบอร์โทร หรืออื่นๆ
                                        // แล้วกำหนดรูปแบบไอคอนให้สอดคล้อง
                                        if (_textController.text
                                                .toString()
                                                .indexOf(':\/\/line') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.line);
                                        } else if (_textController.text
                                                .toString()
                                                .indexOf('mailto:') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.envelope);
                                        } else if (_textController.text
                                                .toString()
                                                .indexOf('sms:') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.commentSms);
                                        } else if (_textController.text
                                                .toString()
                                                .indexOf('tel:') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.mobileScreenButton);
                                        } else if (_textController.text
                                                .toString()
                                                .indexOf('geo:') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.map);
                                        } else if (_textController.text
                                                    .toString()
                                                    .indexOf('youtu.be') >
                                                -1 ||
                                            _textController.text
                                                    .toString()
                                                    .indexOf('youtube.com') >
                                                -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.youtube);
                                        } else if (_textController.text
                                                    .toString()
                                                    .indexOf('facebook.com') >
                                                -1 ||
                                            _textController.text
                                                    .toString()
                                                    .indexOf('fb:\/\/') >
                                                -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.facebook);
                                        } else if (_textController.text
                                                .toString()
                                                .indexOf('tiktok.com') >
                                            -1) {
                                          return const FaIcon(
                                              FontAwesomeIcons.tiktok);
                                        } else {
                                          return const FaIcon(FontAwesomeIcons
                                              .upRightFromSquare);
                                        }
                                      }),
                                    )
                                  : null,
                            ),
                            maxLines: null, // ทำให้ TextField ขยายได้ไม่จำกัด
                            minLines: 1, // จำนวนแถวขั้นต่ำ
                            readOnly: true, // ทำให้เป็นแบบอ่านอย่างเดียว
                          ),
                        ),
                      );
                    }),
                    // แสดงข้อความแนะนำ หรือคำอธิบายการใช้งาน
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Center(
                        child: const Text(
                          'สแกนบาร์โค้ด หรือคิวอาร์โค้ด',
                          style: TextStyle(
                            fontSize: 20.0,
                            color: Colors.black54,
                          ),
                        ),
                      ),
                    ],
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      // ส่วนของการกำหนดการใช้งานแฟลช
                      ValueListenableBuilder(
                        valueListenable: controller,
                        builder: (context, state, child) {
                          // ถ้ากล้องไม่พร้อมใช้งานหรือยังไม่ทำงน แสดง
                          // สร้าง widget กล่องที่มีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้ ขนาด 0
                          // หรือ widget ที่ "ว่างเปล่า"                          
                          if (!state.isInitialized || !state.isRunning) {
                            return const SizedBox.shrink();
                          }
                          // เงื่อนไขของการแสดงปุ่มเปิดแฟลช หรือปิดแฟลช หรือแบบ auto
                          switch (state.torchState) {
                            case TorchState.auto:
                              return ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                    //        backgroundColor: Color(0xFFf8a8ab),
                                    ),
                                onPressed: () async {
                                  await controller.toggleTorch();
                                  setState(() {});
                                },
                                child: const Icon(
                                  Icons.flash_auto,
                                  color: Color(0xFF404040),
                                ),
                              );
                            case TorchState.off:
                              return ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                    //        backgroundColor: Color(0xFFf8a8ab),
                                    ),
                                onPressed: () async {
                                  await controller.toggleTorch();
                                  setState(() {});
                                },
                                child: const Icon(
                                  Icons.flash_off,
                                  color: Color(0xFF404040),
                                ),
                              );
                            case TorchState.on:
                              return ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                    //       backgroundColor: Color(0xFFf8a8ab),
                                    ),
                                onPressed: () async {
                                  await controller.toggleTorch();
                                  setState(() {});
                                },
                                child: const Icon(
                                  Icons.flash_on,
                                  color: Color(0xFF404040),
                                ),
                              );
                            case TorchState.unavailable:
                              return ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                    //            backgroundColor: ui.Color.fromARGB(255, 226, 221, 222),
                                    ),
                                onPressed: () async {
                                  setState(() {});
                                },
                                child: const Icon(
                                  Icons.no_flash,
                                  color: Colors.grey,
                                ),
                              );
                          }
                        },
                      ),
                      SizedBox(
                        width: 10.0,
                      ),
                      // ส่วนของการกำหนดกล้องหน้า กล้องหลัง ถ้ามี ตรวจสอบจาก controller
                      ValueListenableBuilder(
                        valueListenable: controller,
                        builder: (context, state, child) {
                          // ถ้ากล้องไม่พร้อมใช้งานหรือยังไม่ทำงน แสดง
                          // สร้าง widget กล่องที่มีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้ ขนาด 0
                          // หรือ widget ที่ "ว่างเปล่า"
                          if (!state.isInitialized || !state.isRunning) {
                            return const SizedBox.shrink();
                          }
                          
                          // ตรวจสอบค่าสถานะจากกล้อง
                          final int? availableCameras = state.availableCameras;
                     //     print("xdebug: ${availableCameras}");
                            // ตรวจสอบค่าสถานะจำนวนกล้องที่รองรับ 
                            // ถ้ามีแค่กล้องเดียว ไม่ต้องแสดงอะไร โขว์ widget ที่ "ว่างเปล่า" แทน
                            // เพราะไม่จำเป็นต้องสลับกล้อง
                          if (availableCameras != null &&
                              availableCameras < 2) {
                            return const SizedBox.shrink();
                          }
                          // กำหนด widget สำหรับไอคอนแสดงกล้อง
                          final Widget icon;
                          // กรณีมีมากกว่า 1 กล้อง
                          // ใช้กล้องไหนอยู่แสดงไอคอนตามกล้องนั้น
                          switch (state.cameraDirection) {
                            case CameraFacing.front:
                              icon = const Icon(Icons.camera_front,
                                  color: Color(0xFF404040));
                            case CameraFacing.back:
                              icon = const Icon(Icons.camera_rear,
                                  color: Color(0xFF404040));
                          }
                          // แสดงปุ่มสลับกล้อง
                          return ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              // backgroundColor: Color(0xFFf8a8ab),
                              // elevation: 0.0,
                            ),
                            onPressed: () async {
                              // เรียกใช้คำสั่งสลับกล้อง
                              await controller.switchCamera();
                              setState(() {});
                            },
                            child: icon,
                          );
                        },
                      ),
                      SizedBox(
                        width: 10.0,
                      ),
                      // ส่วนปุ่มใช้งาน และหยุดชั่วคราว ให้แสดงขึ้นกับค่า controller
                      ValueListenableBuilder(
                        valueListenable: controller,
                        builder: (context, state, child) {
                          // ถ้ากล้องหยุดชั่วคราวอยู่ หรือยังไม่ทำงาน แสดงปุ่มใช้งานต่อ
                          if (!state.isInitialized || !state.isRunning) {
                            return ElevatedButton(
                              style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.white,
                              ),
                              onPressed: () async {
                                // เรียรกใช้คำสั่งใช้งานกล้องต่อ
                                await controller.start();
                                setState(() {});
                              },
                              child: const Text('ใช้งานต่อ',
                                  style: TextStyle(
                                      fontSize: 20,
                                      color:
                                          Color(0xFFf92D050))),
                            );
                          }
                          // ถ้ากล้องใช้งานอยู่ แสดงปุม หยุดชั่วคราว
                          return ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.white,
                            ),
                            onPressed: () async {
                              // เรียกใช้คำส่งหยุดกล้องชั่วคราว
                              await controller.stop();
                              setState(() {});
                            },
                            child: const Text('หยุดชั่วคราว',
                                style: TextStyle(
                                    fontSize: 20,
                                    color: Color(0xFFf92D050))),
                          );
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  // ฟังก์ชั่นสร้างส่วนของช่องสแกน
  Widget _buildScanner(BuildContext context) {
    // กำหนดความกว้าง ความสูง ตามต้องการ
    final double overlayHeight = 250.0; // Set custom height
    final double overlayWidth = 300.0; // Set custom width
    return Stack(
      fit: StackFit.expand,
      children: [
        Positioned.fill(
          child: Container(
            color: Colors.black, // Background overlay
          ),
        ),
        // วางตำแหน่งของสแกนเนอร์
        Center(
          child: Container(
            width: overlayWidth,
            height: overlayHeight,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(
                  16.0), // กำหนดความมนของกรอบ
              child: MobileScanner(
                fit: BoxFit.cover,
                controller: controller,
              ),
            ),
          ),
        ),
        // ปรับแต่งพื้นที่สแกนเพิ่มเติม เช่นใส่เส้นขอบ
        Center(
          child: Container(
            width: overlayWidth,
            height: overlayHeight,
            decoration: BoxDecoration(
              border: Border.all(
                color: Color(0xFFf92D050),
                width: 3.0,
              ),
              borderRadius: BorderRadius.circular(16.0),
            ),
          ),
        ),
      ],
    );
  }
}
 

ผลลัพธ์ที่ได้

 


 
 
    เราสามารถนำไปสแกนรูปแบบ qrcode จากเนื้อหาตอนที่แล้วทั้ง 10 รูปแบบได้ โดยในโค้ดตัวอย่าง
จะรองรับ ข้อความทั่วไป ลิ้งค์เว็บไซต์ทั่วไป sms เบอร์โทร อีเมล พิกัดในแผนที่ โดยจะแสดงทั้งข้อมูล
และสามารถกดลิ้งค์ด้านหลังเพื่อเปิดไปยังแอปที่เกี่ยวข้องได้ เช่น เบอร์โทร กดเข้าไปก็จะไปหน้าโทรให้
อีเมล ก็จะไปหน้าส่งอีเมลให้ แบบนี้เป็นต้น ส่วนอีก 3 รูปแบบที่เหลือ คือ WIFI VCard และ Calendar 
Event จะไม่รองรับการเรียกใช้งานด้วย url_launcher ดังนั้น เราจึงแสดงแค่ปุ่มด้านหน้า ให้สามารถ
คัดลอกข้อความได้เท่านั้น ตัวสแกนเนอร์นี้ รองรับการสแกน Barcode ด้วย
 

เพิ่มเติมส่วนของ url_launcher

    ในตัวอย่างจะเห็นว่าเรามีการเปิดไปยังแอปต่างๆ ที่เกี่ยวข้องกับรูปแบบข้อมูลของ qrcode ซึ่งในส่วนนี้
เราจำเป็นต้องกำหนดเพิ่มเติมในไฟล์ AndroidManifest.xml ส่วนของค่าการ <queries> หรือการ
บอกให้แอปนั้นสามารถรองรับการทำงานบางสิ่งบางอย่าง หรือการใช้งานร่วมกับแอปที่เกี่ยวข้องได้
 
ไฟล์ android > app > src > main > AndroidManifest.xml
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"/>
    <!-- For Android 13+ -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

    <application
        android:label="Demo App"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
     ..........
    </application>

    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="sms" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="tel" />
        </intent>
        <intent>
            <action android:name="android.support.customtabs.action.CustomTabsService" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="geo" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="http" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <intent>
            <action android:name="android.intent.action.SENDTO" />
            <data android:scheme="mailto" />
        </intent>
    </queries>
</manifest>
 
    การกำหนดค่าภายในแท็ก <queries> ในไฟล์ AndroidManifest.xml เป็นการแจ้งว่าแอป
ของเราต้องการค้นหาและสามารถทำงานร่วมกับแอปพลิเคชันอื่นๆ ที่สามารถตอบสนองต่อ intent 
ที่กำหนดไว้ได้ โดยเฉพาะการทำงานร่วมกับข้อมูลหรือบริการต่างๆ ที่ระบุโดย scheme เช่น ข้อความ
(SMS), การโทร (tel), แผนที่ (geo), เว็บไซต์ (http, https), อีเมล (mailto) เป็นต้น
 
    หวังว่าเนื้อหาตอนนี้จะเป็นประโยชน์ไม่มากก็น้อยในการนำปรับไปประยุกต์ใช้งานต่อไป สำหรับ
บทความในตอนหน้าจะเป็นอะไร รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 01-10-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_050_02102024_source.rar


   เพิ่มเติมเนื้อหา ครั้งที่ 2 วันที่ 11-10-2024


การสแกนจากรูปที่มีรหัส Qrcode หรือ Barcode ในเครื่อง

 
นอกจจากจะสามารถสแกนผ่านกล้องโดยตรงแล้ว เรายังสามารถสแกนจากรูปภาพภายในเครื่องได้
โดยจะมีการประยุกต์เล็กน้อย โดยตัองติดตั้งตัวแพ็กเก็จที่ชื่อว่า image_picker ในไฟล์ 
pubspec.yaml เนื้อหาเกี่ยวกับการใช้งาน image_picker ดูได้ที่บทความ http://niik.in/1118
 
image_picker: ^1.1.2
 
จากนั้นก็กำหนดตัวแปร ที่เกี่ยวข้อง
 
    final ImagePicker _picker = ImagePicker();
    XFile? _imageFile; // ตัวแปรสำหรับเก็บไฟล์รูปภาพที่เลือก
 
ตามด้วยกำหนดฟังก์ชันที่ใช้งาน จะมีแค่ 2 ฟังก์ชั่น คือตัวใช้อ่านรหัสจากรูป และฟังก์ชั่นแสดง การ
แจ้งเตือนกรณีไม่มีหรือไม่พบ code ในภาพ
 
  // ฟังก์ชั่นสแกนจากรูปที่เลือก
  void _scanImageFromGallery() async {
    _imageFile = await _picker.pickImage(source: ImageSource.gallery);
    if (_imageFile != null) {
      final barcodes = await controller.analyzeImage(_imageFile!.path);
      if (barcodes != null) {
        setState(() {
          _barcode = barcodes.barcodes.firstOrNull;
        });        
      } else {
        _showAlert('ไม่พบ Qrcode หรือ Barcode');
      }
    }
  }

  // ฟังก์ชั่นใช้สำหรับการแจ้งเตือน
  void _showAlert(String message) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Scan Result'),
          content: Text(message),
          actions: <Widget>[
            TextButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.of(context).pop(); // Close the dialog
              },
            ),
          ],
        );
      },
    );
  }
 
และส่วนสุดท้าย เราก็แค่เพิ่มปุ่มตรง action มุมบนขวาเข้าไป เพื่อเรียกใช้งาน
 
      appBar: AppBar(
        title: Text('สแกน บาร์โค้ด / คิวอาร์โค้ด'),
        actions: [
          IconButton(
            onPressed: () {
              _scanImageFromGallery();
            }, //
            icon: FaIcon(FontAwesomeIcons.image),
          ),
        ],
      ),
 
เท่านี้เราก็สามารถสแกนรหัส qrcode และ barcode ได้ทั้งจากในไฟล์ที่เครื่อง และการสแกนจาก
กล้อง ทำให้สะดวกมากขึ้น ส่วนการจัดการอื่นๆ ก็เหมือนเดิม ไม่ต้องปรับอะไร
 

ผลลัพธ์ที่ได้


 


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ