178 lines
7.6 KiB
Markdown
178 lines
7.6 KiB
Markdown
Dựa trên hình ảnh giao diện `image.png` và các yêu cầu cải tiến trải nghiệm người dùng (UX) của bạn, dưới đây là kế hoạch Markdown chi tiết để triển khai cử chỉ vuốt đồng bộ trên ảnh, tạo hiệu ứng chớp màn hình khi chụp (Shutter Flash) và chuyển toàn bộ tác vụ xử lý ảnh AI/Merge xuống nền (Background Processing) để tối ưu hóa hiệu năng.
|
|
|
|
---
|
|
|
|
# 🛠️ KẾ HOẠCH CẢI TIẾN UX: ĐỒNG BỘ CỬ CHỈ SWIPE & XỬ LÝ ẢNH CHỤP DƯỚI NỀN
|
|
|
|
Mục tiêu:
|
|
|
|
1. Cho phép người dùng vuốt trực tiếp trên vùng ảnh xem trước (Camera Preview) để thay đổi thông số Ruler (cho cả Presets màu và Độ nhòe Blur) theo cùng một chiều đồng nhất: **Sang phải = Tăng, Sang trái = Giảm**.
|
|
2. Loại bỏ hoàn toàn vòng xoay Loading (UI Freeze) gây ức chế khi bấm nút Shutter.
|
|
3. Tạo hiệu ứng màn hình chớp trắng (Flash Effect) và hiệu ứng thu nhỏ ảnh (Fly-to-Thumbnail) bay về góc dưới bên trái để thay thế biểu tượng Media (như vị trí icon Gallery trong file `image.png`).
|
|
|
|
---
|
|
|
|
## 📐 1. Cải Tiến Cử Chỉ Vuốt Đồng Nhất Trên Ảnh (Gesture Control)
|
|
|
|
Chúng ta sẽ bao bọc vùng kính ngắm `CameraPreview` bằng một Widget `GestureDetector` để bắt sự kiện kéo ngang (`onHorizontalDragUpdate`). Cần tính toán một hệ số nhạy (Sensitivity) để bước chuyển số được mượt mà.
|
|
|
|
### 💻 Đoạn mã cấu trúc cử chỉ vuốt (`CameraScreen.dart`):
|
|
|
|
```dart
|
|
double _dragStartX = 0.0;
|
|
|
|
Widget _buildCameraPreviewWithGestures() {
|
|
return GestureDetector(
|
|
// Bắt đầu chạm vào ảnh
|
|
onHorizontalDragStart: (details) {
|
|
_dragStartX = details.globalPosition.dx;
|
|
},
|
|
// Thực hiện kéo (Swipe)
|
|
onHorizontalDragUpdate: (details) {
|
|
// Tính toán khoảng cách dịch chuyển (dx dương = sang phải, dx âm = sang trái)
|
|
double deltaX = details.delta.dx;
|
|
|
|
// Hệ số nhạy điều chỉnh (Càng nhỏ thì vuốt phải dài hơn mới tăng/giảm nhiều)
|
|
double sensitivity = 0.5;
|
|
double changeValue = deltaX * sensitivity;
|
|
|
|
setState(() {
|
|
if (_currentTab == EditorTab.portraitBlur) {
|
|
// 1. Nếu đang ở tab Xóa nền -> Tăng giảm độ nhòe (_blurIntensity từ 0.0 -> 1.0)
|
|
_blurIntensity = (_blurIntensity + (changeValue / 100.0)).clamp(0.0, 1.0);
|
|
} else if (_currentTab == EditorTab.presets && _selectedAttribute != null) {
|
|
// 2. Nếu đang ở tab Presets -> Tăng giảm thông số slider đang chọn hiện tại
|
|
_updateCurrentPresetAttribute(changeValue);
|
|
}
|
|
});
|
|
},
|
|
child: AspectRatio(
|
|
aspectRatio: 3 / 4,
|
|
child: Stack(
|
|
children: [
|
|
CameraPreview(_cameraController),
|
|
if (_selectedBgMode == BackgroundMode.blur) _buildLiveBlurOverlay(),
|
|
// Lớp hiệu ứng chớp màn hình (Flash Effect)
|
|
if (_showFlashEffect) _buildFlashOverlay(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## ⚡ 2. Hiệu Ứng Chớp Máy & Thu Nhỏ Ảnh Về Biểu Tượng Media
|
|
|
|
Thay vì khóa màn hình bằng vòng quay Loading, ngay khi nhấn nút Shutter, ứng dụng sẽ thực hiện song song 2 việc: Chớp sáng màn hình và bất đồng bộ hóa luồng lưu ảnh.
|
|
|
|
### 🎥 Bước 2.1: Tạo hiệu ứng chớp trắng (Shutter Flash)
|
|
|
|
```dart
|
|
bool _showFlashEffect = false;
|
|
String? _latestThumbnailPath; // Đường dẫn ảnh vừa chụp xong để làm thumbnail
|
|
|
|
void _triggerShutterAnimation() {
|
|
setState(() {
|
|
_showFlashEffect = true;
|
|
});
|
|
|
|
// Tự động tắt lớp màu trắng sau 100 miligiây để tạo cảm giác chớp máy như máy ảnh thật
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
setState(() {
|
|
_showFlashEffect = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
Widget _buildFlashOverlay() {
|
|
return Positioned.fill(
|
|
child: Container(color: Colors.white),
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
### 💻 Bước 2.2: Cập nhật Panel Shutter ở đáy màn hình
|
|
|
|
Dựa theo thiết kế trong ảnh `image.png`, nút Media nằm ở góc dưới bên trái của nút Shutter chính. Chúng ta sẽ thay biểu tượng mặc định bằng ảnh vừa chụp dạng bo góc tròn (Thumbnail preview).
|
|
|
|
```dart
|
|
Widget _buildMediaButton() {
|
|
return Container(
|
|
width: 45,
|
|
height: 45,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[900],
|
|
borderRadius: BorderRadius.circular(8),
|
|
image: _latestThumbnailPath != null
|
|
? DecorationImage(
|
|
image: FileImage(File(_latestThumbnailPath!)),
|
|
fit: BoxFit.cover,
|
|
)
|
|
: null,
|
|
),
|
|
child: _latestThumbnailPath == null
|
|
? const Icon(Icons.image, color: Colors.white, size: 24) // Icon mặc định như trong ảnh mẫu
|
|
: null,
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## ⚙️ 3. Kiến Trúc Xử Lý Ẩn Dưới Nền (Background Work Pipeline)
|
|
|
|
Để nút Shutter phản hồi ngay lập tức, toàn bộ tiến trình xử lý nặng (AI nhận diện, tách nền, áp thông số sliders, nén tệp) sẽ được tách hoàn toàn ra khỏi luồng giao diện chính (UI Thread) bằng cách sử dụng **Flutter Isolate** (`compute`).
|
|
|
|
```dart
|
|
Future<void> onShutterPressed() async {
|
|
// 1. Kích hoạt hiệu ứng chớp máy ngay lập tức
|
|
_triggerShutterAnimation();
|
|
|
|
try {
|
|
// 2. Chụp ảnh thô thật nhanh từ phần cứng camera để lấy file tạm
|
|
final XFile rawPhoto = await _cameraController.takePicture();
|
|
|
|
// 3. Cập nhật ngay lập tức ảnh thô này làm ảnh đại diện cho nút Media (Tạo cảm giác phản hồi tức thì)
|
|
setState(() {
|
|
_latestThumbnailPath = rawPhoto.path;
|
|
});
|
|
|
|
// 4. ĐẨY LỆNH XỬ LÝ NẶNG XUỐNG LUỒNG NGẦM (BACKGROUND ISOLATE)
|
|
// Máy không xuất hiện vòng xoay, người dùng có thể tiếp tục chỉnh sửa hoặc chụp phát tiếp theo
|
|
_runBackgroundProcessingPipeline(
|
|
rawPath: rawPhoto.path,
|
|
bgMode: _selectedBgMode,
|
|
blurValue: _blurIntensity,
|
|
bgAsset: _selectedBgAssetPath,
|
|
presetJson: _currentPreset.toJson(),
|
|
isFrame: _isFrameApplied,
|
|
);
|
|
|
|
} catch (e) {
|
|
print("Lỗi chụp ảnh: $e");
|
|
}
|
|
}
|
|
|
|
// Hàm chạy ngầm tách biệt, không gây lag kính ngắm camera
|
|
void _runBackgroundProcessingPipeline({required String rawPath, ...}) async {
|
|
// Thực hiện gọi luồng ngầm qua Isolate tính toán:
|
|
// - Quét AI bóc tách viền tóc mảnh mượt mà
|
|
// - Áp dụng dải màu sliders hoành tráng
|
|
// - Ép đè khung instaxframe.png
|
|
// - Cuối cùng gọi Gal.putImage() để ghi âm thầm vào thư viện máy.
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 4. Các Đầu Việc Cần Triển Khai (Checklist)
|
|
|
|
* [ ] **Đồng bộ hóa chiều vuốt:** Kiểm tra kỹ xem khi vuốt sang phải, giá trị trên thanh Ruler của tab `portraitBlur` và các thuộc tính trong `presets` có tăng tiến đồng đều không hay có cái nào bị ngược hướng.
|
|
* [ ] **Tối ưu hóa bộ đệm (Thumbnail Cache):** Đảm bảo tệp ảnh hiển thị ở nút Media được thu nhỏ (Kích thước khoảng 100x100 pixel) để không làm hao tổn bộ nhớ RAM khi hiển thị xem trước ở góc trái ảnh `image.png`.
|
|
* [ ] **Kiểm thử chụp liên tiếp (Burst Shot Test):** Thử nghiệm nhấn chụp liên tục 3-4 lần để kiểm tra xem hệ thống hàng đợi (Queue) dưới nền có xử lý tuần tự và lưu đủ số lượng ảnh vào Gallery hệ thống mà không gây văng ứng dụng hay không. |