Flutter 输入处理:集成 GPIO/I2C 等硬件输入事件到手势系统

Flutter 输入处理:集成 GPIO/I2C 等硬件输入事件到手势系统

在现代人机交互领域,触摸屏、鼠标和键盘无疑占据主导地位。然而,在嵌入式系统、工业控制、物联网设备或定制硬件产品中,物理按键、旋钮、传感器等硬件输入仍然不可或缺。Flutter 作为一个出色的 UI 框架,在构建跨平台应用程序方面表现卓越,但其核心设计主要围绕着软输入(触摸、鼠标、键盘)。如何将低层级的硬件输入事件(如 GPIO 状态变化、I2C 传感器数据)无缝集成到 Flutter 丰富的手势系统,从而实现更直观、更可靠的用户体验,是许多开发者面临的挑战。

本讲座将深入探讨这一主题,从硬件接口的底层细节,到 Flutter 平台通道的桥接机制,再到 Flutter 手势系统的内部工作原理,最终提出并详细阐述多种集成策略,并通过具体代码示例加以说明。我们的目标是构建一个能够统一处理软硬件输入的、响应迅速且富有表现力的用户界面。

一、硬件输入的必要性与 Flutter 的挑战

1.1 硬件输入的独特价值

尽管触摸屏提供了极高的灵活性,但物理输入设备在特定场景下拥有不可替代的优势:

  • 可靠性与触觉反馈: 物理按键提供明确的触觉反馈,在恶劣环境(如戴手套操作、高震动环境)或需要盲操作时至关重要。
  • 精准控制: 旋钮、编码器等提供精确的数值调节,远胜于滑动条或虚拟按钮。
  • 特定功能绑定: 专用按键可以直接触发特定功能,简化操作流程。
  • 传感器驱动交互: 接近传感器、加速度计等可以实现基于环境或设备状态的隐式交互。
  • 功耗与唤醒: 硬件中断可以有效地唤醒系统,降低功耗。

1.2 Flutter 的挑战与机遇

Flutter 的 UI 渲染和事件处理机制高度优化,专为高性能的触摸驱动界面设计。其手势系统能够识别复杂的触摸序列,并将其转化为可理解的交互行为。然而,这种抽象层也意味着硬件事件并非天然地融入其输入管道:

  • 事件类型不匹配: 硬件事件通常是离散的(例如,按键按下/释放)或基于数值的(例如,编码器增量),而非 Flutter 期望的 PointerEvent(包含位置、压力、设备类型等信息)。
  • 平台差异性: GPIO 和 I2C 是操作系统和硬件平台相关的接口,需要通过原生代码进行访问。
  • 并发与性能: 硬件事件可能频率很高,处理不当可能导致 UI 卡顿。

本讲座将针对这些挑战,提供一套系统性的解决方案。

二、理解嵌入式 Linux/IoT 环境下的硬件输入

在嵌入式领域,尤其是在基于 Linux 的设备上,GPIO 和 I2C 是最常见的两种硬件接口。

2.1 GPIO (General Purpose Input/Output)

GPIO 引脚是最基本的数字 I/O 接口,可用于读取开关状态、控制 LED、连接按钮等。

2.1.1 工作模式

  • 输入模式: 读取引脚的数字电平(高电平/低电平)。
  • 输出模式: 设置引脚的数字电平。

2.1.2 状态检测方法

  • 轮询 (Polling): 定期读取 GPIO 引脚的状态。简单但效率低,如果事件不频繁,会浪费 CPU 周期;如果轮询间隔过长,可能错过短暂的事件。
  • 中断 (Interrupts): 当 GPIO 引脚的状态发生变化时,硬件会生成一个中断信号,通知 CPU。这是处理按键、开关等事件的首选方式,效率高、实时性好。

2.1.3 Linux 系统接口:libgpiod

在现代 Linux 系统中,libgpiod 是访问 GPIO 的标准和推荐库,它提供了比传统 sysfs 接口更健壮、更安全、功能更丰富的 API。

libgpiod 核心概念:

  • gpiod_chip 代表一个 GPIO 控制器(例如,一个 SoC 可能有多个 GPIO 控制器)。
  • gpiod_line 代表控制器上的一个独立 GPIO 引脚。
  • gpiod_line_request 请求一个或多个 GPIO line,指定其方向(输入/输出)、模式(活动高/低)以及是否监听事件。
  • gpiod_line_event 用于读取 GPIO line 上的事件(上升沿/下降沿)。

C/C++ libgpiod 示例 (监听 GPIO 按键事件):

假设我们有一个按钮连接到 GPIO chip0line 23,当按下时为低电平,释放时为高电平。

#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#ifndef CONSUMER
#define CONSUMER "flutter-gpio-button"
#endif

// Forward declaration for event callback (if needed for a more complex setup)
typedef void (*gpio_event_callback)(int line_offset, int event_type, struct timespec timestamp);

struct GpioMonitorContext {
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    int line_offset;
    char *chip_path;
    gpio_event_callback callback;
    volatile bool running; // Flag to control the event loop
};

// Function to initialize and monitor a GPIO line
int start_gpio_monitor(struct GpioMonitorContext *context) {
    context->chip = gpiod_chip_open_by_path(context->chip_path);
    if (!context->chip) {
        fprintf(stderr, "Failed to open GPIO chip %s: %sn", context->chip_path, strerror(errno));
        return -1;
    }

    context->line = gpiod_chip_get_line(context->chip, context->line_offset);
    if (!context->line) {
        fprintf(stderr, "Failed to get GPIO line %d from chip %s: %sn", context->line_offset, context->chip_path, strerror(errno));
        gpiod_chip_close(context->chip);
        return -1;
    }

    // Request the line as input, with bias pull-up (if button pulls to ground)
    // Or bias pull-down (if button pulls to VCC)
    // Here, assuming button pulls to ground, so we need pull-up.
    // GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP is ideal.
    // If not available or button is active-high, adjust flags.
    int ret = gpiod_line_request_both_edges_events(
        context->line,
        CONSUMER
    );
    if (ret < 0) {
        fprintf(stderr, "Failed to request GPIO line %d for events: %sn", context->line_offset, strerror(errno));
        gpiod_chip_close(context->chip);
        return -1;
    }

    printf("Monitoring GPIO line %d on chip %s for events...n", context->line_offset, context->chip_path);

    struct gpiod_line_event event;
    struct timespec timeout = { .tv_sec = 1, .tv_nsec = 0 }; // 1 second timeout for polling

    context->running = true;
    while (context->running) {
        // Wait for an event with a timeout
        ret = gpiod_line_event_read(context->line, &event);
        if (ret == 0) {
            // Timeout occurred, no event
            // printf("Timeout, still waiting...n");
            continue;
        } else if (ret < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // Non-blocking read would return this, but gpiod_line_event_read is blocking by default.
                // If using gpiod_line_event_read_fd and select/poll, this is normal.
                // With gpiod_line_event_read, a negative return typically indicates an error other than timeout.
                 fprintf(stderr, "Error reading GPIO event: %sn", strerror(errno));
                 if (errno == EBADF) { // Bad file descriptor, maybe line was closed externally
                     context->running = false;
                 }
                 continue; // Try again or break if fatal
            } else {
                fprintf(stderr, "Failed to read GPIO event: %sn", strerror(errno));
                context->running = false; // Exit loop on error
            }
        } else {
            // Event occurred
            const char* event_str = (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE) ? "RISING_EDGE" : "FALLING_EDGE";
            printf("GPIO line %d event: %s at %lld.%09ldn",
                   context->line_offset, event_str,
                   (long long)event.ts.tv_sec, (long)event.ts.tv_nsec);
            if (context->callback) {
                context->callback(context->line_offset, event.event_type, event.ts);
            }
        }
    }

    printf("Stopping GPIO monitor for line %d.n", context->line_offset);
    gpiod_line_release(context->line);
    gpiod_chip_close(context->chip);
    return 0;
}

// Function to stop the monitoring loop
void stop_gpio_monitor(struct GpioMonitorContext *context) {
    if (context) {
        context->running = false;
    }
}

// Dummy callback for demonstration
void my_event_callback(int line_offset, int event_type, struct timespec timestamp) {
    // In a real scenario, this would send data over a pipe/socket to the Dart side
    // or call a Dart FFI function.
    // For now, just print.
    const char* event_str = (event_type == GPIOD_LINE_EVENT_RISING_EDGE) ? "RISING_EDGE" : "FALLING_EDGE";
    printf("[Callback] Line %d, Type: %s, Time: %lld.%09ldn",
           line_offset, event_str, (long long)timestamp.tv_sec, (long)timestamp.tv_nsec);
}

// Example usage (main function for testing)
/*
int main() {
    struct GpioMonitorContext context;
    memset(&context, 0, sizeof(context));
    context.chip_path = "/dev/gpiochip0"; // Or "/dev/gpiochip1", etc.
    context.line_offset = 23; // Or your specific line number
    context.callback = my_event_callback;

    // Start monitoring in a separate thread in a real application
    // For this simple test, it will block main.
    start_gpio_monitor(&context);

    // After some time or an external signal, call stop_gpio_monitor(&context);
    // For example, if running in a separate thread:
    // pthread_t gpio_thread;
    // pthread_create(&gpio_thread, NULL, (void*(*)(void*))start_gpio_monitor, &context);
    // sleep(10); // Monitor for 10 seconds
    // stop_gpio_monitor(&context);
    // pthread_join(gpio_thread, NULL);

    return 0;
}
*/

2.2 I2C (Inter-Integrated Circuit)

I2C 是一种同步、半双工、多主控、多从控的串行通信总线,广泛用于连接传感器(如加速度计、陀螺仪、温度传感器)、EEPROM、实时时钟、LCD 驱动器等。

2.2.1 工作原理

  • 两线制: 仅需两条线:SDA (数据线) 和 SCL (时钟线)。
  • 主从模式: 主设备发起通信,从设备响应。
  • 地址寻址: 每个从设备都有一个唯一的 7 位或 10 位地址。

2.2.2 Linux 系统接口:i2c-dev

在 Linux 中,I2C 设备通过 /dev/i2c-N (N 是总线号,例如 /dev/i2c-1) 文件进行访问。可以使用标准的 open(), ioctl(), read(), write() 系统调用进行操作。

C/C++ i2c-dev 示例 (读取 I2C 传感器数据):

假设我们有一个 I2C 温度传感器,地址为 0x48,读取其温度寄存器 0x00

#include <linux/i2c-dev.h> // For I2C_SLAVE, I2C_RDWR etc.
#include <sys/ioctl.h>     // For ioctl
#include <fcntl.h>         // For open
#include <unistd.h>        // For close, read, write
#include <stdio.h>
#include <string.h>
#include <errno.h>

// Function to read a register from an I2C device
int i2c_read_register(const char* i2c_dev_path, int slave_address, unsigned char reg_address, unsigned char* data, int num_bytes) {
    int file;

    if ((file = open(i2c_dev_path, O_RDWR)) < 0) {
        fprintf(stderr, "Failed to open I2C bus %s: %sn", i2c_dev_path, strerror(errno));
        return -1;
    }

    // Set the slave address
    if (ioctl(file, I2C_SLAVE, slave_address) < 0) {
        fprintf(stderr, "Failed to acquire I2C bus access or talk to slave %02x: %sn", slave_address, strerror(errno));
        close(file);
        return -1;
    }

    // Write the register address to read from
    if (write(file, &reg_address, 1) != 1) {
        fprintf(stderr, "Failed to write register address %02x to slave %02x: %sn", reg_address, slave_address, strerror(errno));
        close(file);
        return -1;
    }

    // Read the data
    if (read(file, data, num_bytes) != num_bytes) {
        fprintf(stderr, "Failed to read %d bytes from slave %02x, register %02x: %sn", num_bytes, slave_address, reg_address, strerror(errno));
        close(file);
        return -1;
    }

    close(file);
    return 0; // Success
}

// Function to write to a register of an I2C device
int i2c_write_register(const char* i2c_dev_path, int slave_address, unsigned char reg_address, const unsigned char* data, int num_bytes) {
    int file;
    unsigned char buffer[num_bytes + 1];

    if ((file = open(i2c_dev_path, O_RDWR)) < 0) {
        fprintf(stderr, "Failed to open I2C bus %s: %sn", i2c_dev_path, strerror(errno));
        return -1;
    }

    if (ioctl(file, I2C_SLAVE, slave_address) < 0) {
        fprintf(stderr, "Failed to acquire I2C bus access or talk to slave %02x: %sn", slave_address, strerror(errno));
        close(file);
        return -1;
    }

    buffer[0] = reg_address;
    memcpy(&buffer[1], data, num_bytes);

    if (write(file, buffer, num_bytes + 1) != (num_bytes + 1)) {
        fprintf(stderr, "Failed to write %d bytes to slave %02x, register %02x: %sn", num_bytes, slave_address, reg_address, strerror(errno));
        close(file);
        return -1;
    }

    close(file);
    return 0; // Success
}

// Example: Read 2 bytes (e.g., a 16-bit temperature reading)
/*
int main() {
    const char* i2c_bus = "/dev/i2c-1"; // Your I2C bus
    int sensor_addr = 0x48;             // Your sensor's I2C address
    unsigned char temp_reg = 0x00;      // Temperature register address

    unsigned char read_data[2];
    if (i2c_read_register(i2c_bus, sensor_addr, temp_reg, read_data, 2) == 0) {
        // Assuming the sensor returns temperature as 16-bit signed integer (MSB first)
        short raw_temp = (read_data[0] << 8) | read_data[1];
        printf("Raw temperature data: 0x%02x 0x%02x -> %dn", read_data[0], read_data[1], raw_temp);
        // Further processing for actual temperature value (e.g., divide by 256 for some sensors)
        double temperature_c = raw_temp / 256.0;
        printf("Temperature: %.2f Cn", temperature_c);
    } else {
        fprintf(stderr, "Failed to read temperature.n");
    }

    return 0;
}
*/

重要提示: 在实际应用中,对 I2C 设备的读取通常需要在单独的线程中进行,以避免阻塞主线程,尤其是在需要频繁读取或等待设备响应时。

三、原生硬件事件到 Flutter 的桥接:平台通道

Flutter 与原生代码之间的通信主要通过平台通道 (Platform Channels) 实现。它们是 Flutter 框架中用于在 Dart 代码和特定平台原生代码之间传递消息的机制。

3.1 平台通道概述

平台通道基于异步消息传递,支持三种主要类型:

  1. MethodChannel 用于调用原生方法并接收返回结果。适用于一次性操作,如初始化硬件、设置参数或执行命令。
  2. EventChannel 用于从原生代码发送连续的事件流到 Dart 代码。这对于硬件输入事件(如按钮按下、传感器数据更新)至关重要。
  3. BasicMessageChannel 用于在 Dart 和原生代码之间发送结构化的、任意类型的消息。它提供了最高级的灵活性,但通常用于更复杂的场景。

对于硬件输入,我们将主要依赖 EventChannel 来传输实时事件。

3.2 EventChannel 的工作原理

EventChannel 允许原生代码作为事件源,将事件发送给 Dart 端的订阅者。

  1. Dart 端: 创建一个 EventChannel 实例,并通过 receiveBroadcastStream() 方法获取一个 Stream
  2. 原生端: 实现一个 StreamHandler 接口(Android)或 FlutterStreamHandler 协议(iOS/macOS/Linux),负责在 onListen 回调中启动事件监听,并在 onCancel 回调中停止监听。
  3. 事件传输: 当原生代码检测到硬件事件时,通过 EventSink 将事件数据发送到 Dart Stream

3.3 示例:从 C++ libgpiod 到 Dart EventChannel

我们将构建一个原生插件,用 C++ 监听 GPIO 事件,并通过 EventChannel 将其发送给 Flutter。

3.3.1 定义 Dart API

首先,在 Dart 层面定义我们期望的事件类型和通道名称。

// lib/hardware_input_plugin.dart
import 'dart:async';
import 'package:flutter/services.dart';

enum GpioEventType {
  /// Button pressed (active low, so falling edge)
  buttonDown,
  /// Button released (active low, so rising edge)
  buttonUp,
}

/// Represents a hardware button event
class GpioButtonEvent {
  final int lineOffset;
  final GpioEventType type;
  final DateTime timestamp;

  GpioButtonEvent(this.lineOffset, this.type, this.timestamp);

  factory GpioButtonEvent.fromJson(Map<String, dynamic> json) {
    return GpioButtonEvent(
      json['lineOffset'] as int,
      GpioEventType.values[json['type'] as int], // Assuming integer mapping
      DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
    );
  }

  Map<String, dynamic> toJson() => {
    'lineOffset': lineOffset,
    'type': type.index,
    'timestamp': timestamp.millisecondsSinceEpoch,
  };

  @override
  String toString() => 'GpioButtonEvent(lineOffset: $lineOffset, type: $type, timestamp: $timestamp)';
}

class HardwareInputPlugin {
  static const String _gpioEventChannelName = 'com.example.hardware_input/gpio_events';
  static const String _gpioMethodChannelName = 'com.example.hardware_input/gpio_methods';

  static const EventChannel _gpioEventChannel = EventChannel(_gpioEventChannelName);
  static const MethodChannel _gpioMethodChannel = MethodChannel(_gpioMethodChannelName);

  /// Initializes a GPIO line for event listening.
  /// Call this before subscribing to events.
  static Future<void> initGpioLine(String chipPath, int lineOffset) async {
    await _gpioMethodChannel.invokeMethod('initGpioLine', {
      'chipPath': chipPath,
      'lineOffset': lineOffset,
    });
  }

  /// Releases a GPIO line.
  static Future<void> releaseGpioLine(String chipPath, int lineOffset) async {
    await _gpioMethodChannel.invokeMethod('releaseGpioLine', {
      'chipPath': chipPath,
      'lineOffset': lineOffset,
    });
  }

  /// Returns a stream of GPIO button events.
  /// Make sure to call [initGpioLine] first.
  static Stream<GpioButtonEvent> get gpioButtonEvents {
    return _gpioEventChannel.receiveBroadcastStream().map((event) {
      if (event is Map) {
        return GpioButtonEvent.fromJson(Map<String, dynamic>.from(event));
      }
      throw FormatException('Unexpected event type: $event');
    });
  }
}

3.3.2 原生 (C++) 实现

linux/plugins/hardware_input/hardware_input_plugin.cc 中,我们将集成 libgpiod 代码。

hardware_input_plugin.cc (核心逻辑)

#include "hardware_input_plugin.h"

#include <flutter_linux/flutter_linux.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <pthread.h> // For threading
#include <map>

// Global context for GPIO monitoring, managed by the plugin
struct GpioMonitorContext {
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    int line_offset;
    char chip_path[256]; // Fixed size buffer for path
    pthread_t thread_id; // Thread ID for the monitor
    volatile bool running;
    FlEventSink event_sink; // The Flutter event sink to send events to
};

// Map to store active GPIO monitors (chipPath + lineOffset -> GpioMonitorContext)
static std::map<std::pair<std::string, int>, GpioMonitorContext*> active_gpio_monitors;
static pthread_mutex_t monitors_mutex = PTHREAD_MUTEX_INITIALIZER;

// Helper to find a monitor context
static GpioMonitorContext* find_monitor_context(const std::string& chip_path, int line_offset) {
    pthread_mutex_lock(&monitors_mutex);
    auto it = active_gpio_monitors.find({chip_path, line_offset});
    GpioMonitorContext* context = (it != active_gpio_monitors.end()) ? it->second : nullptr;
    pthread_mutex_unlock(&monitors_mutex);
    return context;
}

// Event callback for `libgpiod`
static void gpio_event_callback_internal(int line_offset, int event_type, struct timespec timestamp, FlEventSink event_sink) {
    if (!event_sink) return;

    // Create a Flutter value (map) to send
    FlValue* event_map = fl_value_new_map();
    fl_value_set_string(event_map, "lineOffset", fl_value_new_int(line_offset));
    fl_value_set_string(event_map, "type", fl_value_new_int(event_type == GPIOD_LINE_EVENT_FALLING_EDGE ? 0 : 1)); // 0 for buttonDown (falling), 1 for buttonUp (rising)
    fl_value_set_string(event_map, "timestamp", fl_value_new_int((gint64)timestamp.tv_sec * 1000 + timestamp.tv_nsec / 1000000)); // Milliseconds

    fl_event_sink_send(event_sink, event_map);
    fl_value_unref(event_map); // Release the value after sending
}

// Thread function for GPIO monitoring
static void* gpio_monitor_thread_func(void* arg) {
    GpioMonitorContext *context = static_cast<GpioMonitorContext*>(arg);
    if (!context) {
        fprintf(stderr, "GPIO monitor thread started with null context.n");
        return nullptr;
    }

    context->chip = gpiod_chip_open_by_path(context->chip_path);
    if (!context->chip) {
        fprintf(stderr, "Failed to open GPIO chip %s in thread: %sn", context->chip_path, strerror(errno));
        context->running = false;
        return nullptr;
    }

    context->line = gpiod_chip_get_line(context->chip, context->line_offset);
    if (!context->line) {
        fprintf(stderr, "Failed to get GPIO line %d from chip %s in thread: %sn", context->line_offset, context->chip_path, strerror(errno));
        gpiod_chip_close(context->chip);
        context->chip = nullptr;
        context->running = false;
        return nullptr;
    }

    // Request the line for both edge events
    int ret = gpiod_line_request_both_edges_events(
        context->line,
        "flutter-gpio-consumer" // Consumer name
    );
    if (ret < 0) {
        fprintf(stderr, "Failed to request GPIO line %d for events in thread: %sn", context->line_offset, strerror(errno));
        gpiod_chip_close(context->chip);
        context->chip = nullptr;
        context->line = nullptr;
        context->running = false;
        return nullptr;
    }

    fprintf(stdout, "GPIO monitor thread for %s:%d started.n", context->chip_path, context->line_offset);

    struct gpiod_line_event event;
    struct timespec timeout = { .tv_sec = 1, .tv_nsec = 0 }; // 1 second poll timeout

    context->running = true;
    while (context->running) {
        ret = gpiod_line_event_read(context->line, &event);
        if (ret == 0) {
            // Timeout, no event
            continue;
        } else if (ret < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // Should not happen with gpiod_line_event_read, but good to check
                continue;
            } else {
                fprintf(stderr, "Error reading GPIO event in thread: %sn", strerror(errno));
                context->running = false; // Exit loop on error
            }
        } else {
            gpio_event_callback_internal(context->line_offset, event.event_type, event.ts, context->event_sink);
        }
    }

    fprintf(stdout, "GPIO monitor thread for %s:%d stopping.n", context->chip_path, context->line_offset);
    if (context->line) {
        gpiod_line_release(context->line);
        context->line = nullptr;
    }
    if (context->chip) {
        gpiod_chip_close(context->chip);
        context->chip = nullptr;
    }
    // No need to free context here, it's managed by active_gpio_monitors map
    return nullptr;
}

// Event Channel Handler
static FlMethodResponse* gpio_event_stream_handler_on_listen(FlEventChannel* channel, FlValue* arguments, FlEventSink sink, gpointer user_data) {
    if (arguments == nullptr || !fl_value_is_map(arguments)) {
        return FL_METHOD_RESPONSE_ERROR("INVALID_ARGUMENTS", "Arguments must be a map.", nullptr);
    }

    FlValue* chip_path_value = fl_value_lookup_string(arguments, "chipPath");
    FlValue* line_offset_value = fl_value_lookup_string(arguments, "lineOffset");

    if (!chip_path_value || !fl_value_is_string(chip_path_value) ||
        !line_offset_value || !fl_value_is_int(line_offset_value)) {
        return FL_METHOD_RESPONSE_ERROR("INVALID_ARGUMENTS", "Missing chipPath (string) or lineOffset (int).", nullptr);
    }

    const char* chip_path = fl_value_get_string(chip_path_value);
    int line_offset = fl_value_get_int(line_offset_value);

    pthread_mutex_lock(&monitors_mutex);
    GpioMonitorContext* context = find_monitor_context(chip_path, line_offset);
    if (context) {
        if (context->running) {
            fprintf(stderr, "GPIO monitor for %s:%d already running, re-attaching sink.n", chip_path, line_offset);
            context->event_sink = sink; // Update the sink if already running
            pthread_mutex_unlock(&monitors_mutex);
            return FL_METHOD_RESPONSE_SUCCESS(nullptr);
        } else {
            // Monitor exists but is not running, possibly error state. Clean up and restart.
            fprintf(stderr, "GPIO monitor for %s:%d found but not running. Cleaning up and restarting.n", chip_path, line_offset);
            // Assuming cleanup done by thread func, just remove from map and create new
            active_gpio_monitors.erase({chip_path, line_offset});
            free(context); // Free the old context
            context = nullptr;
        }
    }

    // If context is null, create a new one
    if (!context) {
        context = (GpioMonitorContext*)calloc(1, sizeof(GpioMonitorContext));
        if (!context) {
            pthread_mutex_unlock(&monitors_mutex);
            return FL_METHOD_RESPONSE_ERROR("ALLOCATION_FAILED", "Failed to allocate GpioMonitorContext.", nullptr);
        }
        strncpy(context->chip_path, chip_path, sizeof(context->chip_path) - 1);
        context->chip_path[sizeof(context->chip_path) - 1] = ''; // Ensure null-termination
        context->line_offset = line_offset;
        context->event_sink = sink;
        active_gpio_monitors[{chip_path, line_offset}] = context;

        // Start the monitoring thread
        int result = pthread_create(&context->thread_id, nullptr, gpio_monitor_thread_func, context);
        if (result != 0) {
            fprintf(stderr, "Failed to create GPIO monitor thread: %sn", strerror(result));
            active_gpio_monitors.erase({chip_path, line_offset});
            free(context);
            pthread_mutex_unlock(&monitors_mutex);
            return FL_METHOD_RESPONSE_ERROR("THREAD_CREATION_FAILED", "Failed to create native thread.", nullptr);
        }
    }
    pthread_mutex_unlock(&monitors_mutex);
    return FL_METHOD_RESPONSE_SUCCESS(nullptr);
}

static FlMethodResponse* gpio_event_stream_handler_on_cancel(FlEventChannel* channel, FlValue* arguments, gpointer user_data) {
    if (arguments == nullptr || !fl_value_is_map(arguments)) {
        return FL_METHOD_RESPONSE_ERROR("INVALID_ARGUMENTS", "Arguments must be a map.", nullptr);
    }

    FlValue* chip_path_value = fl_value_lookup_string(arguments, "chipPath");
    FlValue* line_offset_value = fl_value_lookup_string(arguments, "lineOffset");

    if (!chip_path_value || !fl_value_is_string(chip_path_value) ||
        !line_offset_value || !fl_value_is_int(line_offset_value)) {
        return FL_METHOD_RESPONSE_ERROR("INVALID_ARGUMENTS", "Missing chipPath (string) or lineOffset (int).", nullptr);
    }

    const char* chip_path = fl_value_get_string(chip_path_value);
    int line_offset = fl_value_get_int(line_offset_value);

    pthread_mutex_lock(&monitors_mutex);
    GpioMonitorContext* context = find_monitor_context(chip_path, line_offset);
    if (context) {
        context->running = false; // Signal thread to stop
        // Detach event sink, but keep context until thread actually stops
        context->event_sink = nullptr;
        // pthread_join(context->thread_id, nullptr); // Do not join here, it will block UI thread
        active_gpio_monitors.erase({chip_path, line_offset});
        free(context); // Free the context after removing from map
    }
    pthread_mutex_unlock(&monitors_mutex);
    return FL_METHOD_RESPONSE_SUCCESS(nullptr);
}

static void gpio_method_channel_handler(FlMethodChannel* channel, FlMethodCall* method_call) {
    const char* method = fl_method_call_get_name(method_call);
    FlValue* args = fl_method_call_get_args(method_call);

    if (strcmp(method, "initGpioLine") == 0) {
        // This method is now implicitly handled by the event channel's onListen.
        // For explicit setup, we could put the gpiod_chip_open/get_line/request_line here
        // and store in a map, then onListen would just use existing context.
        // For simplicity, we'll let onListen handle the full lifecycle.
        fl_method_call_respond_success(method_call, nullptr, nullptr);
    } else if (strcmp(method, "releaseGpioLine") == 0) {
        // This method is now implicitly handled by the event channel's onCancel.
        fl_method_call_respond_success(method_call, nullptr, nullptr);
    } else {
        fl_method_call_respond_not_implemented(method_call, nullptr);
    }
}

void hardware_input_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
    // Register Method Channel
    g_autoptr(FlMethodChannel) method_channel =
        fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
                              "com.example.hardware_input/gpio_methods",
                              FL_METHOD_CODEC(fl_standard_method_codec_new()));
    fl_method_channel_set_method_call_handler(method_channel, gpio_method_channel_handler, nullptr);

    // Register Event Channel
    g_autoptr(FlEventChannel) event_channel =
        fl_event_channel_new(fl_plugin_registrar_get_messenger(registrar),
                             "com.example.hardware_input/gpio_events",
                             FL_METHOD_CODEC(fl_standard_method_codec_new()));
    fl_event_channel_set_stream_handler(event_channel,
                                        gpio_event_stream_handler_on_listen,
                                        gpio_event_stream_handler_on_cancel,
                                        nullptr, nullptr); // No user_data
}

hardware_input_plugin.h

#ifndef FLUTTER_PLUGIN_HARDWARE_INPUT_PLUGIN_H_
#define FLUTTER_PLUGIN_HARDWARE_INPUT_PLUGIN_H_

#include <flutter_linux/flutter_linux.h>

G_BEGIN_DECLS

#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define FLUTTER_PLUGIN_EXPORT
#endif

FLUTTER_PLUGIN_EXPORT void hardware_input_plugin_register_with_registrar(FlPluginRegistrar* registrar);

G_END_DECLS

#endif // FLUTTER_PLUGIN_HARDWARE_INPUT_PLUGIN_H_

CMakeLists.txt (添加 libgpiod 依赖)

linux/CMakeLists.txt 中找到 target_link_libraries,添加 gpiod

# ... (existing CMakeLists.txt content) ...

target_link_libraries(${PROJECT_NAME}_plugin PUBLIC
  flutter_linux_wrapper
  gpiod # Add this line
)

注意:

  • 原生代码中需要处理线程管理,确保 GPIO 监听不在 UI 线程上进行。pthread 用于创建新线程。
  • FlEventSink 必须在 onListen 回调中获取,并在 onCancel 回调中释放。
  • FlValue 是 Flutter 平台通道的数据类型,用于在 C++ 和 Dart 之间传递数据。
  • 为了简化,initGpioLinereleaseGpioLine 方法通道的逻辑被合并到 EventChannelonListenonCancel 中。这意味着第一次订阅事件时会自动初始化 GPIO,取消所有订阅时会自动释放。如果需要更精细的控制,可以独立使用 MethodChannel

四、Flutter 手势系统解析

在深入集成硬件事件之前,理解 Flutter 的手势系统至关重要。

4.1 输入事件流

Flutter 的输入事件处理是一个多层次的管道:

  1. 平台原始事件: 操作系统(例如,Android、iOS、Linux)报告原始输入事件(触摸、鼠标、键盘)。
  2. GestureBinding Flutter 引擎接收这些原始事件,并将其转换为 Flutter 内部的 PointerEvent 对象。
  3. PointerRouter GestureBindingPointerEvent 分发给所有注册的 PointerRouter 客户端。
  4. GestureArena 当多个 GestureRecognizer 可能对同一组 PointerEvent 感兴趣时,GestureArena 会介入,通过竞争机制来决定哪个 GestureRecognizer 最终赢得手势识别权。
  5. GestureRecognizer 负责识别特定手势(如点击、滑动、缩放)的逻辑单元。
  6. Widget Tree: 识别出的手势通过回调通知到 Widget 树中的 GestureDetectorRawGestureDetector

4.2 PointerEvent 的类型

PointerEvent 是 Flutter 手势系统的基础,它封装了触摸、鼠标或笔输入的详细信息。关键属性包括:

  • pointer:唯一标识符,区分不同的触摸点或输入设备。
  • timeStamp:事件发生的时间。
  • position:事件在屏幕上的坐标。
  • delta:自上次事件以来的位置变化。
  • kind:输入设备的类型(触摸、鼠标、笔)。
  • buttons:鼠标按钮状态。

常见的 PointerEvent 子类:

  • PointerDownEvent:指针接触屏幕。
  • PointerMoveEvent:指针在屏幕上移动。
  • PointerUpEvent:指针离开屏幕。
  • PointerCancelEvent:指针事件被取消(例如,父级手势胜出)。

4.3 GestureRecognizer

GestureRecognizer 是 Flutter 手势系统的核心抽象,用于识别特定手势。

  • 生命周期: addAllowedPointer (指针进入竞技场) -> handleEvent (处理事件) -> acceptGesture (赢得竞技场) / rejectGesture (失去竞技场) -> stopTrackingPointer (指针离开竞技场)。
  • 竞技场 (GestureArena): 当一个 PointerDownEvent 发生时,所有可能识别该手势的 GestureRecognizer 都会加入一个 GestureArena。它们通过声明 acceptreject 来竞争。最终,只有一个 GestureRecognizer 能赢得竞技场,其余的都会被拒绝。这解决了手势冲突问题(例如,是滚动还是点击)。
  • 自定义 GestureRecognizer 扩展 GestureRecognizer 基类是实现自定义手势的关键。这允许我们定义自己的状态机,根据一系列 PointerEvent 或自定义事件来识别手势。

4.4 GestureDetectorRawGestureDetector

  • GestureDetector 方便的 Widget,封装了常用的 GestureRecognizer(如 TapGestureRecognizer, PanGestureRecognizer 等)。
  • RawGestureDetector 提供了更大的灵活性,可以直接传入一个 Map<Type, GestureRecognizerFactory> 来自定义 GestureRecognizer。这是集成硬件事件时经常使用的 Widget。

五、集成策略:将硬件事件映射到 Flutter 手势系统

将原生硬件事件集成到 Flutter 手势系统有多种策略,每种都有其适用场景和优缺点。

5.1 策略一:简单事件分发 (非手势系统集成)

这是最直接的方式,不涉及 Flutter 的手势系统,只是将硬件事件作为数据流直接用于更新 UI 或触发业务逻辑。

  • 机制: 在 Dart 端订阅 EventChannel 返回的 Stream,然后使用 StreamBuilder 或手动订阅 Stream 来更新 StatefulWidget 的状态。
  • 优点: 实现简单,适用于按钮切换状态、传感器数据显示等非手势交互。
  • 缺点: 无法利用 Flutter 手势系统的强大功能(如手势竞技场、组合手势),不能与触摸手势进行竞争或协作。
  • 适用场景: 独立的硬件控制,不与屏幕触摸交互产生冲突。

代码示例 (基于 GpioButtonEvent):

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:hardware_input_plugin/hardware_input_plugin.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hardware Input Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const GpioButtonScreen(),
    );
  }
}

class GpioButtonScreen extends StatefulWidget {
  const GpioButtonScreen({super.key});

  @override
  State<GpioButtonScreen> createState() => _GpioButtonScreenState();
}

class _GpioButtonScreenState extends State<GpioButtonScreen> {
  String _buttonState = 'Released';
  DateTime? _lastEventTimestamp;

  @override
  void initState() {
    super.initState();
    _initAndListenGpio();
  }

  Future<void> _initAndListenGpio() async {
    // Replace with your actual GPIO chip path and line offset
    const String chipPath = '/dev/gpiochip0';
    const int lineOffset = 23;

    try {
      // Initialize the GPIO line (this might be handled by the first stream listener)
      // await HardwareInputPlugin.initGpioLine(chipPath, lineOffset);
      print('Initialized GPIO line $lineOffset on $chipPath');

      // Subscribe to events
      HardwareInputPlugin.gpioButtonEvents.listen((event) {
        if (event.lineOffset == lineOffset) {
          setState(() {
            _buttonState = event.type == GpioEventType.buttonDown ? 'Pressed' : 'Released';
            _lastEventTimestamp = event.timestamp;
            print('Button event: ${event.type} at ${event.timestamp}');
          });
        }
      }).onError((error) {
        print('Error listening to GPIO events: $error');
        setState(() {
          _buttonState = 'Error: $error';
        });
      });
    } catch (e) {
      print('Failed to initialize GPIO or listen to events: $e');
      setState(() {
        _buttonState = 'Init Error: $e';
      });
    }
  }

  @override
  void dispose() {
    // Optionally release GPIO line, but `onCancel` in native code handles stream close.
    // However, if you have multiple listeners and only want to stop when all are gone,
    // or stop explicitly, you might manage it here.
    // For this simple example, we rely on the native cleanup when stream is cancelled.
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GPIO Button Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Hardware Button State:',
              style: TextStyle(fontSize: 24),
            ),
            Text(
              _buttonState,
              style: TextStyle(
                fontSize: 48,
                fontWeight: FontWeight.bold,
                color: _buttonState == 'Pressed' ? Colors.red : Colors.green,
              ),
            ),
            if (_lastEventTimestamp != null)
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text(
                  'Last event: ${_lastEventTimestamp!.toLocal()}',
                  style: const TextStyle(fontSize: 16),
                ),
              ),
            const SizedBox(height: 30),
            const Text(
              'This demonstrates direct event consumption, bypassing Flutter's gesture system.',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 14, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

5.2 策略二:合成 PointerEvent 并注入 (通常不推荐)

这种策略试图将硬件事件伪装成 Flutter 的 PointerEvent,并将其注入到 GestureBinding.instance.handlePointerEvent 方法中。

  • 机制:
    1. 硬件事件发生时,通过 EventChannel 传递到 Dart。
    2. 在 Dart 端,根据硬件事件的类型和状态,构造一个 PointerDownEventPointerMoveEventPointerUpEvent
    3. 使用 GestureBinding.instance.addPointer(pointerEvent)GestureBinding.instance.handlePointerEvent(pointerEvent) 将合成的事件注入到 Flutter 的事件管道中。
  • 优点: 理论上可以利用所有现有的 GestureDetectorGestureRecognizer
  • 缺点:
    • 语义不匹配: 许多硬件事件(如按钮按下)没有天然的屏幕坐标 (position) 或移动 (delta) 概念。合成这些值通常是武断且无意义的。
    • pointer ID 管理: 需要手动管理 pointer ID,以确保每个“虚拟指针”的唯一性和生命周期。
    • 与真实触摸冲突: 合成的 PointerEvent 会与真实的触摸事件在 GestureArena 中竞争,可能导致不可预测的行为。
    • 难以模拟复杂手势: 难以从离散的硬件事件中合成出流畅的 PointerMoveEvent 序列。
  • 适用场景: 仅适用于硬件本身提供类似触摸板或操纵杆的 位置信息 的情况。例如,一个自定义的轨迹球或触摸板,它可以精确地映射到屏幕坐标和移动。对于简单的按钮或旋钮,此方法通常是过度设计或错误应用。

代码示例 (概念性,不推荐在实际按钮中使用):

假设我们有一个自定义的轨迹球,它报告相对移动。

// This is a highly conceptual and usually ill-advised approach for simple buttons.
// It's primarily for devices that genuinely provide positional input.

import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hardware_input_plugin/hardware_input_plugin.dart';

// Assuming HardwareInputPlugin provides a stream of {x_delta, y_delta} for a virtual trackball
// class TrackballMoveEvent {
//   final double dx, dy;
//   final DateTime timestamp;
//   TrackballMoveEvent(this.dx, this.dy, this.timestamp);
// }

// For a button, synthesizing pointer events is problematic.
// Let's illustrate with a hypothetical trackball for which this strategy might apply.
// We'll skip the native side for this hypothetical TrackballMoveEvent and focus on Flutter injection.

class SynthesizedPointerEventScreen extends StatefulWidget {
  const SynthesizedPointerEventScreen({super.key});

  @override
  State<SynthesizedPointerEventScreen> createState() => _SynthesizedPointerEventScreenState();
}

class _SynthesizedPointerEventScreenState extends State<SynthesizedPointerEventScreen> {
  Offset _currentPosition = const Offset(100, 100); // Initial virtual pointer position
  int _pointerId = 0; // Unique ID for our virtual pointer
  bool _isDown = false; // Is our virtual button currently "pressed"?

  @override
  void initState() {
    super.initState();
    // Simulate initial PointerDown for the virtual pointer
    _injectPointerDown();

    // Example: Listen to a hypothetical stream of trackball events
    // For GPIO button, this would be `HardwareInputPlugin.gpioButtonEvents`
    // but mapping button presses to positional PointerEvents is fundamentally flawed.
    HardwareInputPlugin.gpioButtonEvents.listen((event) {
      if (event.type == GpioEventType.buttonDown && !_isDown) {
        _isDown = true;
        _injectPointerDown(); // Re-inject down to simulate a "click" or new press
      } else if (event.type == GpioEventType.buttonUp && _isDown) {
        _isDown = false;
        _injectPointerUp();
      }
      // For a real trackball, we'd get dx, dy and inject PointerMove
      // Example: _injectPointerMove(event.dx, event.dy);
    });
  }

  void _injectPointerDown() {
    // Generate a unique pointer ID
    _pointerId = PointerEvent.syntheticFactory.initialPointer;
    // GestureBinding.instance.addPointer(PointerDownEvent(
    //   pointer: _pointerId,
    //   timeStamp: DateTime.now(),
    //   position: _currentPosition,
    //   kind: PointerDeviceKind.mouse, // Or PointerDeviceKind.touch for a virtual touch
    // ));
     GestureBinding.instance.handlePointerEvent(PointerDownEvent(
      pointer: _pointerId,
      timeStamp: Duration(microseconds: DateTime.now().microsecondsSinceEpoch),
      position: _currentPosition,
      kind: PointerDeviceKind.mouse, // Or PointerDeviceKind.touch for a virtual touch
    ));
    print('Injected PointerDown at $_currentPosition with ID $_pointerId');
  }

  void _injectPointerMove(double dx, double dy) {
    if (!_isDown) return; // Only move if "button" is down
    _currentPosition = _currentPosition + Offset(dx, dy);
    // GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
    //   pointer: _pointerId,
    //   timeStamp: DateTime.now(),
    //   position: _currentPosition,
    //   delta: Offset(dx, dy),
    //   kind: PointerDeviceKind.mouse,
    // ));
    GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
      pointer: _pointerId,
      timeStamp: Duration(microseconds: DateTime.now().microsecondsSinceEpoch),
      position: _currentPosition,
      delta: Offset(dx, dy),
      kind: PointerDeviceKind.mouse,
    ));
    print('Injected PointerMove to $_currentPosition, delta ($dx, $dy)');
  }

  void _injectPointerUp() {
    // GestureBinding.instance.handlePointerEvent(PointerUpEvent(
    //   pointer: _pointerId,
    //   timeStamp: DateTime.now(),
    //   position: _currentPosition,
    //   kind: PointerDeviceKind.mouse,
    // ));
    GestureBinding.instance.handlePointerEvent(PointerUpEvent(
      pointer: _pointerId,
      timeStamp: Duration(microseconds: DateTime.now().microsecondsSinceEpoch),
      position: _currentPosition,
      kind: PointerDeviceKind.mouse,
    ));
    print('Injected PointerUp at $_currentPosition with ID $_pointerId');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Synthesized PointerEvent Demo'),
      ),
      body: GestureDetector(
        onTap: () {
          print('Tap detected by GestureDetector!');
        },
        onPanUpdate: (details) {
          print('Pan detected by GestureDetector: ${details.delta}');
          setState(() {
            _currentPosition += details.delta;
          });
        },
        child: Container(
          color: Colors.lightBlue.shade100,
          alignment: Alignment.center,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Try clicking the virtual button (via actual hardware button) or dragging this area.',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 18),
              ),
              const SizedBox(height: 20),
              Text(
                'Virtual Pointer Position: (${_currentPosition.dx.toStringAsFixed(1)}, ${_currentPosition.dy.toStringAsFixed(1)})',
                style: const TextStyle(fontSize: 16),
              ),
              const SizedBox(height: 20),
              Container(
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                  color: _isDown ? Colors.red : Colors.green,
                  shape: BoxShape.circle,
                ),
                child: Center(
                  child: Text(
                    _isDown ? 'Down' : 'Up',
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5.3 策略三:自定义 GestureRecognizer 处理硬件事件 (推荐)

这是将硬件事件深度集成到 Flutter 手势系统的最强大和推荐的方法。它允许我们为硬件输入创建自己的手势识别器,并让它们与标准的触摸手势识别器在 GestureArena 中竞争。

  • 机制:
    1. 创建一个继承自 GestureRecognizer (或其子类,如 OneSequenceGestureRecognizer) 的自定义类。
    2. 在自定义识别器内部,订阅 HardwareInputPlugin 提供的事件流(如 gpioButtonEvents)。
    3. 根据接收到的硬件事件序列,实现手势识别逻辑(例如,检测按钮的“点击”、“长按”或旋钮的“滚动”)。
    4. 通过调用 resolve(GestureDisposition.accepted)resolve(GestureDisposition.rejected) 来参与 GestureArena
    5. 当手势被识别时,触发自定义回调(例如 onTap, onLongPress)。
  • 优点:
    • 完全融入 GestureArena,能够与其他手势竞争或协作,解决手势冲突。
    • 高度灵活,可以定义任意复杂的硬件手势。
    • 遵循 Flutter 的手势系统架构,代码结构清晰,易于维护。
    • 不依赖于虚假的 PointerEvent,语义更清晰。
  • 缺点: 实现比简单事件分发复杂,需要理解 GestureRecognizer 的内部机制。
  • 适用场景: 需要将硬件事件转化为与触摸手势类似的、需要竞争或组合的交互行为,例如:
    • 物理按钮的点击、长按。
    • 旋钮的旋转作为滚动或数值调节。
    • 多按键组合手势。

5.3.1 示例:HardwareButtonTapGestureRecognizer

我们将创建一个识别物理按钮“点击”手势的识别器。


// lib/hardware_button_recognizer.dart
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/foundation.dart';
import 'package:hardware_input_plugin/hardware_input_plugin.dart';

// Callback types for our custom recognizer
typedef GestureHardwareButtonTapCallback = void Function(int lineOffset);
typedef GestureHardwareButtonLongPressCallback = void Function(int lineOffset);

/// A custom gesture recognizer that recognizes tap and long press gestures
/// from a specific hardware GPIO button.
///
/// This recognizer listens to the `GpioButtonEvent` stream from the native plugin
/// and translates sequences of `buttonDown` and `buttonUp` into tap or long press gestures.
class HardwareButtonTapGestureRecognizer extends OneSequenceGestureRecognizer {
  /// The GPIO line offset this recognizer is interested in.
  final int targetLineOffset;

  /// Called when a tap is detected.
  GestureHardwareButtonTapCallback? onTap;

  /// Called when a long press is detected.
  GestureHardwareButtonLongPressCallback? onLongPress;

  /// The maximum duration between a button down and button up event to be considered a tap.
  /// Defaults to [kTapTimeout].
  Duration tapTimeout = kTapTimeout;

  /// The duration a button must be held down to be considered a long press.
  /// Defaults to [kLongPressTimeout].
  Duration longPressTimeout = kLongPressTimeout;

  // Internal state for gesture recognition
  StreamSubscription<GpioButtonEvent>? _eventSubscription;
  GpioButtonEvent? _lastDownEvent;
  Timer? _longPressTimer;

  HardwareButtonTapGestureRecognizer({
    super.debugOwner,
    this.targetLineOffset = -1, // -1 means listen to all, or require specific target
    this.onTap,
    this.onLongPress,
    super.supportedDevices,
  }) : assert(targetLineOffset >= 0); // Must specify a target line offset

  @override
  void addAllowedPointer(PointerDownEvent event) {
    // This recognizer does not directly process PointerEvents.
    // It exists in the arena but listens to its own event stream.
    // However, it *must* add a pointer to participate in the arena.
    // We'll immediately accept it to allow it to receive the "down" signal
    // and then manage its own resolution based on hardware events.
    // This is a bit of a hack to integrate with the PointerEvent-based arena.

    // If we only want *hardware* events to trigger this recognizer,
    // we should prevent it from accepting real PointerEvents.
    // For simplicity, we'll let it join the arena, but its internal logic
    // will only react to hardware events.
    // A more robust approach might involve a custom GestureDetector that
    // *only* feeds hardware events to its recognizers, bypassing PointerEvents entirely.
    // For now, let's assume it only cares about its internal stream.
    // We call `startTrackingPointer` to make it a participant in the arena.
    // But then, we won't process the pointer event in `handleEvent`.
    super.addAllowedPointer(event);

    // If we want to participate in the arena based on PointerEvents, we'd process them here.
    // But since this is a HARDWARE button recognizer, we defer to our stream.
    // The key is to start listening to the hardware stream when the recognizer is active.
    _startListeningToHardwareEvents();
  }

  void _startListeningToHardwareEvents() {
    if (_eventSubscription == null) {
      _eventSubscription = HardwareInputPlugin.gpioButtonEvents.listen(_handleHardwareEvent);
      print('HardwareButtonTapGestureRecognizer for line $targetLineOffset started listening.');
    }
  }

  void _stopListeningToHardwareEvents() {
    _eventSubscription?.cancel();
    _eventSubscription = null;
    print('HardwareButtonTapGestureRecognizer for line $targetLineOffset stopped listening.');
  }

  void _handleHardwareEvent(GpioButtonEvent event) {
    if (event.lineOffset != targetLineOffset) {
      return; // Not our button
    }

    switch (event.type) {
      case GpioEventType.buttonDown:
        _handleButtonDown(event);
        break;
      case GpioEventType.buttonUp:
        _handleButtonUp(event);
        break;
    }
  }

  void _handleButtonDown(GpioButtonEvent event) {
    // If a previous down event wasn't resolved, it means a double-down without up,
    // or a long press timer was still active. Cancel any pending long press.
    _longPressTimer?.cancel();
    _lastDownEvent = event;

    // Start long press timer
    _longPressTimer = Timer(longPressTimeout, () {
      if (onLongPress != null && _lastDownEvent != null) {
        invokeCallback<void>('onLongPress', () => onLongPress!(targetLineOffset));
        _lastDownEvent = null; // Consume the long press
        // Resolve as accepted here, because a long press takes precedence.
        resolve(GestureDisposition.accepted);
      }
    });

    // In a `OneSequenceGestureRecognizer`, we typically call `startTrackingPointer`
    // with the *synthetic* pointer that initiated this (if any).
    // Here, we're driven by hardware. We need to tell the arena we're interested.
    // Since we don't have a PointerDownEvent directly from hardware,
    // this part needs careful thought if it's competing with *actual* touch.
    // For now, we assume this recognizer acts mostly independently or wins easily.
    // If it needs to compete, it must receive a PointerDownEvent.
    // A simple button might not have a corresponding PointerDownEvent.
    // If we are only responding to HW events, we don't need to resolve for PointerEvents.
    // But to participate in the arena, we *must* resolve.
    // We accept immediately, assuming our HW button has higher priority or is exclusive.
    // If this recognizer needs to *compete* with touch gestures for the *same visual area*,
    // then this approach has limitations without synthesizing PointerEvents.
    // However, for hardware buttons that control non-visual elements or specific widgets,
    // this works by being the 'only' recognizer for its type.
    // For now, let's make it resolve itself after a sequence.
    // We don't resolve immediately on down, but wait for up or long press.
  }

  void _handleButtonUp(GpioButtonEvent event) {
    _longPressTimer?.cancel(); // Cancel long press timer if button released
    if (_lastDownEvent == null) {
      // Button up without a preceding down event (e.g., app started while button pressed)
      // or long press already consumed it.
      return;
    }

    final Duration duration = event.timestamp.difference(_lastDownEvent!.timestamp);

    if (duration < tapTimeout) {
      // It's a tap!
      if (onTap != null) {
        invokeCallback<void>('onTap', () => onTap!(targetLineOffset));
        resolve(GestureDisposition.accepted); // Tap gesture won the arena
      } else {
        // No tap handler, but still a valid sequence. Reject or don't resolve.
        resolve(GestureDisposition.rejected);
      }
    } else {
      // Duration exceeded tap timeout but wasn't long press (e.g., long hold then release,
      // but long press already fired, or no long press handler).
      // If long press already fired, _lastDownEvent would be null.
      // So this is a release after a hold that wasn't a long press.
      resolve(GestureDisposition.rejected);
    }
    _lastDownEvent = null; // Reset for next gesture
  }

  @override
  void handleEvent(PointerEvent event) {
    // This recognizer does not directly consume PointerEvents for its core logic.
    // The `_handleHardwareEvent` method is the primary driver.
    // If this recognizer *also* needed to react to PointerEvents, this is where it would happen.
    // Since our focus is on HARDWARE events, we just ensure we track the pointer if added.
    // The `addAllowedPointer` method takes care of registering it with the arena.
    // We do NOT call `super.handleEvent` here if we don't want to process PointerEvents.
  }

  @override
  void acceptGesture(int pointer) {
    // Our custom recognizer accepted the gesture.
    // No specific action needed here beyond what `resolve` already did.
    print('HardwareButtonTapGestureRecognizer accepted gesture for pointer $pointer.');
  }

  @override
  void rejectGesture(int pointer) {
    // Our custom recognizer rejected the gesture.
    // Clean up any pending state.
    _longPressTimer?.cancel();
    _lastDownEvent = null;
    print('HardwareButtonTapGestureRecognizer rejected gesture for pointer $pointer.');
  }

  @override
  String get debugDescription => 'hardwareButtonTap';

  @override
  void didStopTrackingLastPointer(int pointer) {
    // The last pointer associated with this recognizer has stopped being tracked.
    // This is a good place to clean up internal state and stop listening to hardware events.
    _longPressTimer?.cancel();
    _lastDownEvent = null;
    _stopListeningToHardwareEvents();
  }

  @override
  void dispose() {
    _longPressTimer?.cancel();
    _eventSubscription?.cancel(); // Ensure subscription is cancelled
    super.dispose();
  }
}

/// A widget that detects taps and long presses on a specific hardware button.
class HardwareButtonGestureDetector extends StatelessWidget {
  /// The GPIO line offset to listen for.
  final int lineOffset;

  /// Called when a tap is detected on the hardware button.
  final GestureHardwareButtonTapCallback? onTap;

  /// Called when a long press is detected on the hardware button.
  final GestureHardwareButtonLongPressCallback? onLongPress;

  /// The widget below this widget in the tree.
  final Widget child;

  const HardwareButtonGestureDetector({
    super.key,
    required this.lineOffset,
    this.onTap,
    this.onLongPress,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: <Type, GestureRecognizerFactory>{
        HardwareButtonTapGestureRecognizer: GestureRecognizerFactoryWith

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注