Command Palette

Search for a command to run...

Học cách tạo ứng dụng Camera tùy chỉnh bằng React Native và Expo

Học cách tạo ứng dụng Camera tùy chỉnh bằng React Native và Expo

Ở bài trước, chúng ta đã học cách "gõ cửa" hệ điều hành để xin quyền truy cập. Hôm nay, khi cánh cửa đã mở, chúng ta sẽ tiến thẳng vào một trong những phần cứng thú vị nhất của chiếc điện thoại: Camera và Gallery.

Thử nghĩ xem, có mạng xã hội nào tồn tại mà không cho phép người dùng thay đổi Avatar? Có ứng dụng bán hàng nào không cần người dùng chụp ảnh sản phẩm để đánh giá? Chụp và chọn ảnh là tính năng "bắt buộc phải biết" của mọi lập trình viên Mobile.

Trong bài viết này, chúng ta sẽ sử dụng 2 "vũ khí" chính chủ từ hệ sinh thái Expo: expo-image-picker (để chọn ảnh có sẵn) và expo-camera (để tự tay xây dựng một máy ảnh thu nhỏ).

1. Chọn ảnh từ Thư viện với expo-image-picker

Khởi động với tính năng đơn giản và được sử dụng nhiều nhất: Chọn một bức ảnh đã có sẵn trong máy.

expo-image-picker

Bước 1: Cài đặt expo-image-picker

Mở Terminal và chạy lệnh sau:

npx expo install expo-image-picker

Bước 2: Viết code gọi Thư viện ảnh

Thư viện này cung cấp hàm launchImageLibraryAsync giúp mở thẳng ứng dụng Thư viện ảnh (Photos) của điện thoại. Bạn có thể cho phép cắt cúp ảnh (crop) ngay sau khi chọn!

import React, { useState } from 'react'
import { Button, Image, View, StyleSheet, Text, TouchableOpacity } from 'react-native'
import * as ImagePicker from 'expo-image-picker' // 1. Import thư viện

export default function ImagePickerScreen() {
  const [imageUri, setImageUri] = useState(null)

  const pickImage = async () => {
    // 2. Mở thư viện ảnh (Hàm này tự động xin quyền đọc ảnh trên hầu hết thiết bị)
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images, // Chỉ chọn ảnh, không chọn video
      allowsEditing: true, // Cho phép người dùng crop ảnh
      aspect: [1, 1], // Ép khung crop hình vuông (tỷ lệ 1:1, phù hợp làm Avatar)
      quality: 0.8, // Giảm chất lượng xuống 80% để nhẹ máy, tải lên mạng nhanh hơn
    })

    // 3. Nếu người dùng không bấm "Hủy" (Cancel)
    if (!result.canceled) {
      // result.assets[0].uri chứa đường dẫn tới bức ảnh vừa chọn
      setImageUri(result.assets[0].uri)
    }
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Cập Nhật Ảnh Đại Diện</Text>

      {/* Hiển thị ảnh nếu đã chọn, nếu chưa thì hiển thị khung màu xám */}
      {imageUri ? <Image source={{ uri: imageUri }} style={styles.avatar} /> : <View style={styles.placeholder} />}

      <TouchableOpacity style={styles.button} onPress={pickImage}>
        <Text style={styles.buttonText}>📁 Chọn Ảnh Từ Máy</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
  avatar: { width: 150, height: 150, borderRadius: 75, marginBottom: 20 },
  placeholder: { width: 150, height: 150, borderRadius: 75, backgroundColor: '#dfe6e9', marginBottom: 20 },
  button: { backgroundColor: '#0984e3', padding: 12, borderRadius: 8 },
  buttonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
})

2. Nâng cao: Tự xây dựng Máy ảnh với expo-camera

Nếu image-picker cũng có tính năng mở camera mặc định của máy, thì tại sao chúng ta lại cần expo-camera?

Lý do là: image-picker văng ra khỏi ứng dụng của bạn để mở app Camera gốc. Còn với expo-camera, bạn có thể nhúng trực tiếp khung ngắm camera vào trong giao diện app của bạn. Bạn có thể đặt các bộ lọc, nút bấm tùy chỉnh đè lên trên khung ngắm đó (giống hệt cách TikTok hay Instagram làm).

(Lưu ý: Code dưới đây sử dụng component <CameraView> hiện đại nhất của Expo SDK 50/51, thay thế cho thẻ <Camera> cũ đã lỗi thời).

expo-camera

Bước 1: Cài đặt expo-camera

npx expo install expo-camera

Bước 2: Xây dựng màn hình Camera

Với Camera, BẮT BUỘC phải xin quyền rành mạch trước khi hiển thị khung ngắm. Chúng ta cũng cần Hook useRef để ra lệnh cho máy ảnh chụp hình.

import React, { useState, useRef } from 'react'
import { StyleSheet, Text, TouchableOpacity, View, Image } from 'react-native'
import { CameraView, useCameraPermissions } from 'expo-camera'

export default function CustomCameraScreen() {
  // 1. Quản lý quyền
  const [permission, requestPermission] = useCameraPermissions()

  // 2. State quản lý hướng camera (trước/sau) và ảnh đã chụp
  const [facing, setFacing] = useState('back')
  const [photo, setPhoto] = useState(null)

  // 3. Tham chiếu (Ref) để điều khiển CameraView
  const cameraRef = useRef(null)

  // Màn hình chờ nếu chưa kiểm tra xong quyền
  if (!permission) return <View />

  // Nếu chưa cấp quyền, hiển thị nút xin quyền
  if (!permission.granted) {
    return (
      <View style={styles.container}>
        <Text style={{ textAlign: 'center' }}>Cần cấp quyền để sử dụng Camera</Text>
        <Button onPress={requestPermission} title="Cấp quyền ngay" />
      </View>
    )
  }

  // Hàm lật camera trước/sau
  const toggleCameraFacing = () => {
    setFacing((current) => (current === 'back' ? 'front' : 'back'))
  }

  // Hàm chụp ảnh
  const takePic = async () => {
    // Nếu ref đã được gắn vào CameraView
    if (cameraRef.current) {
      const options = { quality: 0.8, base64: true } // Cấu hình chất lượng
      const newPhoto = await cameraRef.current.takePictureAsync(options)
      setPhoto(newPhoto.uri) // Lưu đường dẫn ảnh vừa chụp vào state
    }
  }

  // Nếu ĐÃ CHỤP ẢNH xong, hiển thị ảnh chụp để xem trước (Preview)
  if (photo) {
    return (
      <View style={styles.container}>
        <Image source={{ uri: photo }} style={{ flex: 1 }} />
        <TouchableOpacity style={styles.retakeButton} onPress={() => setPhoto(null)}>
          <Text style={styles.text}>Chụp lại</Text>
        </TouchableOpacity>
      </View>
    )
  }

  // Giao diện LÚC ĐANG NGẮM CHỤP
  return (
    <View style={styles.container}>
      <CameraView style={styles.camera} facing={facing} ref={cameraRef}>
        <View style={styles.buttonContainer}>
          {/* Nút lật Camera */}
          <TouchableOpacity style={styles.flipButton} onPress={toggleCameraFacing}>
            <Text style={styles.text}>Lật Cam</Text>
          </TouchableOpacity>

          {/* Nút Bấm Chụp */}
          <TouchableOpacity style={styles.captureButton} onPress={takePic}>
            <View style={styles.innerCapture} />
          </TouchableOpacity>
        </View>
      </CameraView>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', backgroundColor: '#000' },
  camera: { flex: 1 },
  buttonContainer: {
    flex: 1,
    flexDirection: 'row',
    backgroundColor: 'transparent',
    justifyContent: 'space-around',
    alignItems: 'flex-end',
    paddingBottom: 40,
  },
  flipButton: { alignSelf: 'flex-end', padding: 15, backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 10 },
  text: { fontSize: 18, fontWeight: 'bold', color: 'white' },
  captureButton: {
    width: 70,
    height: 70,
    borderRadius: 35,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 4,
    borderColor: 'rgba(0,0,0,0.2)',
  },
  innerCapture: { width: 54, height: 54, borderRadius: 27, backgroundColor: 'white' },
  retakeButton: {
    position: 'absolute',
    bottom: 40,
    alignSelf: 'center',
    backgroundColor: '#d63031',
    padding: 15,
    borderRadius: 10,
  },
})

3. Kiến thức thực chiến: Upload ảnh lên Server thế nào?

Cả hai cách trên đều trả về cho bạn một biến uri (Ví dụ: file:///data/user/0/.../image.jpg).

Nhưng đây chỉ là đường dẫn nằm trong ổ cứng của điện thoại. Nếu bạn mang biến uri này lưu vào cơ sở dữ liệu trên Server, những người dùng khác sẽ không thể xem được ảnh của bạn!

Để đưa bức ảnh này lên mạng (Upload), bạn không thể dùng JSON.stringify() như gửi Text thông thường. Bức ảnh là một file nhị phân. Bạn bắt buộc phải dùng công nghệ FormData.

Đây là đoạn code "vàng" giúp bạn biến file từ điện thoại thành dữ liệu gửi đi được qua API:

const uploadImageToServer = async (localUri) => {
  // 1. Lấy tên file và đuôi mở rộng từ đường dẫn
  const filename = localUri.split('/').pop()
  const match = /\.(\w+)$/.exec(filename)
  const type = match ? `image/${match[1]}` : `image`

  // 2. Tạo một túi hồ sơ FormData
  const formData = new FormData()

  // 3. Bỏ bức ảnh vào túi (Lưu ý phải truyền đủ uri, name, type)
  formData.append('photo', { uri: localUri, name: filename, type })

  // Bạn có thể bỏ thêm dữ liệu khác (như ID người dùng)
  formData.append('userId', '12345')

  // 4. Gửi bằng Fetch API
  try {
    const response = await fetch('https://api.domain-cua-ban.com/upload', {
      method: 'POST',
      body: formData,
      headers: {
        'Content-Type': 'multipart/form-data', // Bắt buộc khi dùng FormData
      },
    })
    const result = await response.json()
    console.log('Upload thành công! Link ảnh trên mạng là:', result.imageUrl)
  } catch (error) {
    console.error('Lỗi upload:', error)
  }
}

Kết luận: Bạn đã nắm trong tay đầy quyền năng

Bạn đã nắm trong tay cách điều khiển thị giác của thiết bị di động:

  1. Dùng expo-image-picker cho 90% nhu cầu thông thường: Chọn Avatar, gửi ảnh có sẵn, mở camera cơ bản.
  2. Dùng expo-camera khi bạn muốn thiết kế các app chuyên về nhiếp ảnh, quét mã QR, hoặc làm khung ngắm tùy chỉnh như Instagram Story.
  3. Luôn nhớ dùng FormData để đẩy file ảnh thực sự lên Server.

Nếu Camera là con mắt, thì ở bài sau, chúng ta sẽ cho ứng dụng một khả năng định hướng tuyệt vời. Hãy sẵn sàng để bước đi tiếp nào!

Bài viết liên quan

Hướng dẫn tích hợp Push Notifications với React Native và Expo

Hướng dẫn chi tiết cách cài đặt và cấu hình Push Notifications trong React Native sử dụng hệ thống Expo. Nắm vững cách xin quyền, lấy Token và gửi thông báo kiểm thử.

Cách gọi API trong React Native: Hướng dẫn Fetch Data và xử lý JSON

Hướng dẫn chi tiết cách gọi API trong React Native. Nắm vững kỹ thuật sử dụng fetch, axios kết hợp với useEffect, useState để tải và hiển thị dữ liệu server mượt mà.

Phân tích luồng chạy ứng dụng Expo chi tiết từ A-Z

Tìm hiểu cách ứng dụng khởi chạy, làm chủ thư mục app/, cấu trúc file cơ bản và phân tích cú pháp JSX một cách cực kỳ dễ hiểu.

Tìm hiểu 5 Core Components quan trọng nhất trong React Native

Hướng dẫn cách sử dụng các Core Components trong React Native. Nắm vững View, Text, Image, TextInput và ScrollView để xây dựng mọi giao diện mobile.