Files
photobooth-app/36_PRESET_APPLY.md
T

8.5 KiB

Hai lỗi này liên quan trực tiếp đến hiện tượng bất đồng bộ luồng (Race Condition)quản lý vòng đời bộ đệm (State Lifecycle/Caching) giữa tầng Flutter UI và tầng Native Android (MainActivity.kt).

  • Lỗi nhấn shutter lúc chụp lúc không: Do khi bạn đổi Preset, luồng ngầm xử lý ma trận màu của Preset đó đang chiếm dụng tài nguyên đồ họa (Bitmap/ImageReader) cùng lúc với lệnh chụp takePicture(). Nếu lệnh chụp đến trước khi Preset xử lý xong \rightarrow Chụp lỗi/Bị nuốt lệnh.
  • Lỗi đổi Preset phải bấm vài lần mới ăn: Do biến lưu cấu hình Preset ở tầng Native (activePreset) không được cập nhật volatile thời gian thực, hoặc do bộ đệm xem trước (Preview Request Builder) không được gọi lệnh làm tươi (setRepeatingRequest) ngay lập tức sau khi gán thông số mới.

Dưới đây là kế hoạch Markdown chi tiết để sửa dứt điểm hai lỗi trên, đảm bảo đổi bộ lọc màu ăn ngay lập tức và nút bấm Shutter phản hồi chính xác 100%.


🛠️ KẾ HOẠCH FIX LỖI: ĐỒNG BỘ ĐỒNG THỜI PRESET REAL-TIME VÀ SỬA KHÓA LỆNH SHUTTER


📐 1. Khắc Phục Lỗi Thay Đổi Preset Không Ăn Ngay Lập Tức (MainActivity.kt)

Giải pháp: 1. Thêm từ khóa volatile vào biến lưu trữ Preset ở tầng Native để đảm bảo mọi luồng xử lý (Main Thread và Background Thread) đều đọc chung một giá trị mới nhất ngay khi người dùng chạm tay chọn. 2. Ép cấu hình Camera2 API làm tươi (Refresh Buffer) ngay lập tức bằng lệnh setRepeatingRequest sau khi người dùng thay đổi thông số.

import android.hardware.camera2.CaptureRequest

class MainActivity : FlutterActivity() {
    
    // 🟢 SỬA LỖI: Dùng @Volatile để đảm bảo thông số Preset cập nhật ngay lập tức xuyên luồng
    @Volatile 
    private var currentActivePreset: ColorPreset? = null

    // Hàm nhận lệnh đổi Preset từ Flutter UI truyền xuống thông qua MethodChannel
    fun updateActivePresetFromUI(newPreset: ColorPreset) {
        this.currentActivePreset = newPreset

        // 🟢 SỬA LỖI PHẢI BẤM VÀI LẦN: Ép thấu kính camera cập nhật ma trận màu Viewfinder ngay lập tức
        try {
            previewRequestBuilder.set(
                CaptureRequest.COLOR_CORRECTION_MODE, 
                CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX
            )
            
            // Cập nhật ma trận màu phần cứng dựa trên thông số Preset mới chọn
            applyPresetToPreviewBuilder(previewRequestBuilder, newPreset)
            
            // Lệnh làm tươi Live Preview ngay một miligiây sau khi chọn
            cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}


2. Khắc Phục Lỗi Nút Shutter Lúc Chụp Lúc Không (Chống Nghẽn Tài Nguyên)

Nguyên nhân: Khi bạn nhấn Shutter, ứng dụng chụp lại ảnh thô. Nếu lúc đó bạn vừa đổi Preset, hệ thống đồ họa bị "xung đột" quyền truy cập Bitmap. Nếu luồng chụp không khóa hàng đợi (Thread Lock), lệnh capture sẽ bị hủy âm thầm khiến camera đứng im không chụp.

Giải pháp: Sử dụng cờ trạng thái isCapturing và cơ chế Đóng băng cấu hình tạm thời (Snapshot Parameter). Khi bấm Shutter, ta nhân bản nhanh thông số Preset tại thời điểm đó rồi đẩy vào luồng xử lý riêng, giải phóng luồng chính để sẵn sàng nhận lệnh chụp tiếp theo.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean

// Sử dụng AtomicBoolean chống hiện tượng kích kép (Double tap) hoặc nghẽn lệnh Shutter
private val isCapturing = AtomicBoolean(false)

fun onShutterButtonPressed() {
    // Nếu đang trong tiến trình bắt ảnh thô từ cảm biến, chặn không cho bấm tiếp để tránh nghẽn phần cứng
    if (isCapturing.get()) {
        print("Camera đang bận chụp tấm trước, vui lòng đợi mốc miligiây!")
        return 
    }

    // Khóa trạng thái bắt đầu chụp
    isCapturing.set(true)
    triggerFlashAnimationUI() // Kích hoạt hiệu ứng chớp màn hình lập tức cho người dùng biết

    // 🟢 CHỐNG XUNG ĐỘT: Sao chép nhanh (Snapshot) thông số Preset hiện tại ra một biến độc lập
    val presetSnapshot = this.currentActivePreset?.copy()

    // Tiến hành gọi phần cứng chụp ảnh thô
    val captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
    captureBuilder.addTarget(imageReader.surface)

    cameraCaptureSession.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
        override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
            super.onCaptureCompleted(session, request, result)

            // MỞ KHÓA CAMERA LẬP TỨC: Khôi phục luồng ngắm để người dùng bấm chụp phát tiếp theo được ngay
            runOnUiThread {
                cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
                // Đã giải phóng xong phần cứng camera, nút Shutter sẵn sàng hoạt động 100%
                isCapturing.set(false) 
            }

            // ĐẨY LUỒNG XỬ LÝ ẢNH NẶNG XUỐNG COROUTINE NGẦM HOÀN TOÀN
            GlobalScope.launch(Dispatchers.Default) {
                val rawBitmap = getAndLockBitmapFromReader(imageReader)
                
                // Áp dụng bản sao Preset đã chụp snapshot (Không sợ bị lỗi khi người dùng đổi màu khác lúc đang lưu)
                val finalFilteredBitmap = if (presetSnapshot != null) {
                    applyHighImpactPreset(rawBitmap, presetSnapshot)
                } else {
                    rawBitmap
                }

                // Chạy luồng lưu file ngầm
                saveBitmapToSystemGalleryIO(finalFilteredBitmap)
                
                // Thu hồi RAM dọn dẹp bộ nhớ đệm sạch sẽ
                rawBitmap.recycle()
                finalFilteredBitmap.recycle()
            }
        }
    }, null)
}


🌐 3. Đồng Bộ Chặt Chẽ Tại Tầng Giao Diện Người Dùng (CameraScreen.dart)

Ở tầng Flutter UI, khi người dùng trượt Ruler hoặc nhấn chọn Preset, ta gọi trực tiếp MethodChannel xuống Native một cách dứt khoát, không bọc qua các hàm delay trung gian:

// Mỗi lần người dùng trượt Ruler thay đổi thông số màu
void onPresetSliderChanged(double value) {
  _currentPreset.brightness = value;
  
  // Gửi trực tiếp xuống Native ép mô-tơ đồ họa và camera làm tươi dữ liệu ngay
  _methodChannel.invokeMethod('updatePreset', _currentPreset.toJson());
  
  setState(() {}); // Làm mới thanh trượt trên UI của Flutter
}


📅 4. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist)

  • Kiểm thử đổi Preset ăn ngay: Nhấn liên tục vào các Preset khác nhau màu Trắng Đen \rightarrow Vintage \rightarrow Rực rỡ. (Yêu cầu: Kính ngắm Viewfinder phải đổi màu theo ngay lập tức ngay tại thời điểm chạm ngón tay, không có độ trễ, không phải bấm 2-3 lần mới ăn).
  • Kiểm thử Shutter chính xác 100%: Bấm nút Shutter liên tục (Burst-click) khoảng 5-10 lần với tốc độ nhanh. (Yêu cầu: Mỗi lần bấm màn hình đều phải chớp sáng sáng 1 cái biểu thị đã ghi nhận lệnh chụp, không xuất hiện tình trạng "bấm nhưng camera đơ ra không chụp").
  • Kiểm thử lưu ảnh đúng màu: Đang đứng ở Preset Trắng Đen, bấm nút Shutter, rồi lập tức bấm tay chuyển nhanh sang Preset Rực Rỡ. Mở bộ sưu tập ảnh kiểm tra tấm ảnh vừa chụp xem có lưu đúng màu Trắng Đen của thời điểm bấm nút chụp không. (Yêu cầu: Đúng màu nhờ thuật toán Snapshot ở Mục 2).