دليلك الشامل: بناء تطبيق كاميرا احترافي باستخدام Expo و React Native

دقائق القراءة: 11

يُعد Expo بيئة عمل متكاملة ومجموعة أدوات قوية تُسهّل بشكل كبير عملية بناء تطبيقات React Native. فهو يقلل من تعقيد إعداد بيئة التطوير ويوفر عليك عناء تثبيت وتكوين العديد من المكونات الأساسية. في هذا الدليل الشامل، سنتعلم خطوة بخطوة كيفية إنشاء تطبيق كاميرا بسيط لكنه فعال، يتيح للمستخدمين التقاط الصور، معاينتها، استخدام وضع الفلاش، والتبديل بسلاسة بين الكاميرتين الأمامية والخلفية.

المتطلبات الأساسية لبدء تطوير تطبيق الكاميرا

لا يتطلب Expo الكثير لبدء بناء أول تطبيق React Native لك. يمكنك استكشاف المزيد حول تثبيت Expo و expo-cli في الوثائق الرسمية.

ملاحظة: في هذا الشرح، سنستخدم نظامي التشغيل macOS و iOS. ومع ذلك، يمكنك استخدام Android أيضًا، حيث لا يوجد فرق جوهري في استخدام Expo في هذه المرحلة.

يمكنك تثبيت Expo و expo-cli بشكل عام (globally) عن طريق تشغيل الأمر التالي في سطر الأوامر:

npm install --global expo-cli

يتطلب Expo وجود بيئة Node.js ليعمل بشكل صحيح. يمكنك الحصول على أحدث إصدار من Node.js من الموقع الرسمي.

البدء في العمل: تهيئة مشروع Expo جديد

بعد تثبيت Expo و Node.js بنجاح، يمكنك البدء في إنشاء مشروع Expo جديد باستخدام الأمر التالي:

expo init expo-camera-app

تثبيت الحزم وتشغيل التطبيق على جهازك

يوفر لنا Expo تطبيقًا عميلاً (client app) يمكننا من خلاله تشغيل التطبيق الذي نبنيه ومعاينة نتائجه مباشرة. هذا التطبيق متاح للتنزيل على كل من App Store و Google Play.

هذه هي واجهة التطبيق التي ستراها:

واجهة تطبيق Expo Go على الهاتف

تهيئة مشروع Expo: اختيار القالب المناسب

انتقل إلى دليل التطبيق الذي تم إنشاؤه حديثًا وقم بتشغيل الأمر التالي:

cd expo-camera-app

سيُطلب منك الإجابة على بعض الأسئلة لاختيار القالب الافتراضي للتطبيق. في هذا الدليل، سنختار خيار blank (TypeScript)، ولكن يمكنك دائمًا اختيار ما يناسب احتياجات مشروعك.

اختيار قالب مشروع Expo Blank TypeScript

تشغيل التطبيق ومعاينته على الأجهزة

بعد تهيئة المشروع، يمكننا تشغيل التطبيق باستخدام الأمر expo run:

تشغيل مشروع Expo عبر الأمر expo run

سيؤدي هذا إلى فتح نافذة في متصفحك حيث يمكنك رؤية سجلات (logs) التطبيق. كما سيقوم بتوليد رمز QR يمكنك مسحه ضوئيًا لتشغيل التطبيق على جهازك الفعلي. الميزة الرائعة في Expo هي أنك لست بحاجة إلى تثبيت وتكوين المحاكيات (simulators) لتشغيل التطبيق، على الرغم من أنه يمنحك خيار تشغيل Expo على المحاكي إذا قمت بتثبيته وتكوينه بنفسك.

بالعودة إلى تطبيقنا، بافتراض أنك قمت بتشغيل التطبيق بنجاح على جهازك، ستكون هذه هي الشاشة الافتراضية:

شاشة التطبيق الافتراضية بعد التشغيل الناجح

افتح دليل التطبيق في محرر الأكواد المفضل لديك (مثل VS Code). سيبدو ملف App.tsx على النحو التالي:

 import {StatusBar} from 'expo-status-bar'
 import React from 'react'
 import {StyleSheet, Text, View} from 'react-native'
 
 export default function App ( ) {
   return (
     < View style = {styles.container} >
       < Text > Open up App.tsx to start working on your app! </ Text >
       < StatusBar style = "auto" />
     </ View >
   )
 }
 
 const styles = StyleSheet.create({
   container : {
     flex : 1 ,
     backgroundColor : '#fff' ,
     alignItems : 'center' ,
     justifyContent : 'center'
   }
 })

بناء واجهة المستخدم (UI) لتطبيق الكاميرا

بعد تشغيل المشروع بنجاح، حان الوقت الآن للبدء في بناء واجهة المستخدم.

تثبيت حزمة الكاميرا Expo

الخطوة التالية هي تثبيت حزمة expo-camera، وذلك باستخدام الأمر التالي:

expo install expo-camera

سنقوم بإنشاء واجهة مستخدم بسيطة تسمح للمستخدم ببدء عملية استخدام الكاميرا.

واجهة مستخدم بسيطة مع زر التقاط الصورة

 import {StatusBar} from 'expo-status-bar'
 import React from 'react'
 import {StyleSheet, Text, View, TouchableOpacity} from 'react-native'
 
 export default function App ( ) {
   return (
     < View style = {{ flex: 1 , backgroundColor: '# fff ', justifyContent: ' center ', alignItems: ' center ' }} >
       < TouchableOpacity style = {{ width: 130 , borderRadius: 4 , backgroundColor: '# 14274e ', flexDirection: ' row ', justifyContent: ' center ', alignItems: ' center ', height: 40 }} >
         < Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
       </ TouchableOpacity >
     </ View >
   )
 }
 
 const styles = StyleSheet.create({
   container : {
     flex : 1 ,
     backgroundColor : '#fff' ,
     alignItems : 'center' ,
     justifyContent : 'center'
   }
 })

هذه واجهة مستخدم بسيطة: قمنا باستيراد TouchableOpacity للزر وأضفنا بعض التنسيقات الأساسية.

الآن، نحتاج إلى استخدام hook من نوع useState لإدارة الحالة وعرض معاينة الكاميرا عندما يضغط المستخدم على زر “التقاط الصورة”.

 <TouchableOpacity onPress={__startCamera} style={{ width : 130 , borderRadius : 4 , backgroundColor : '#14274e' , flexDirection : 'row' , justifyContent : 'center' , alignItems : 'center' , height : 40 }} >
   < Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
 </TouchableOpacity>
 const [startCamera,setStartCamera] = React.useState( false )
 const __startCamera = ()=> {
   // Function logic will go here
 }

هناك أمران مهمان يجب القيام بهما عندما يضغط المستخدم على الزر:

  1. طلب الإذن للوصول إلى الكاميرا. في تطوير تطبيقات الجوال، غالبًا ما يكون الوصول إلى العديد من واجهات برمجة التطبيقات (APIs) والميزات الأصلية (native features) مقيدًا بأذونات المستخدم والخصوصية. هذا أمر يجب أن تعتاد عليه عند تطوير تطبيقات الجوال.
  2. تغيير الحالة وعرض الكاميرا.

لنقم باستيراد وحدة الكاميرا من expo-camera باستخدام هذا الأمر:

 import {Camera} from 'expo-camera'

ثم أضف عرض الكاميرا (camera view) على النحو التالي:

 <Camera style={{ flex : 1 , width : "100%" }} ref={ ( r ) => { camera = r }} ></Camera>

يمكننا استخدام السمة ref للوصول إلى وظائف الكاميرا (camera's methods):

 let camera: Camera

عند الضغط على زر “التقاط الصورة” (take picture button)، سيتم استدعاء الدالة __startCamera:

 const __startCamera = async () => {
   const {status} = await Camera.requestPermissionsAsync()
   if (status === 'granted' ){
     // Do something if permission is granted
   } else {
     Alert.alert( "Access denied" )
   }
 }

ستطلب الدالة الإذن أولاً. إذا منح المستخدم الوصول إلى الكاميرا، يمكننا المتابعة وفتح الكاميرا. وإلا، سنعرض تنبيهًا بسيطًا.

إضافة مكون الكاميرا وعرضها شرطيًا

لنقم بعرض الكاميرا عندما يمنح المستخدم الوصول إلى كاميرا الجهاز:

 const __startCamera = async () => {
   const {status} = await Camera.requestPermissionsAsync()
   if (status === 'granted' ) {
     // Start the camera
     setStartCamera( true )
   } else {
     Alert.alert( 'Access denied' )
   }
 }

يجب علينا إجراء بعض التغييرات على واجهة المستخدم (UI) وإضافة عرض شرطي (conditional rendering). سنعرض الكاميرا فقط عندما يطلبها المستخدم، وإلا سنعرض الشاشة الافتراضية.

 {startCamera ? (
   < Camera style = {{flex: 1 , width: " 100 %"}} ref = {(r) => { camera = r }} >
   </ Camera >
 ) : (
   < View style = {{ flex: 1 , backgroundColor: '# fff ', justifyContent: ' center ', alignItems: ' center ' }} >
     < TouchableOpacity onPress = {__startCamera} style = {{ width: 130 , borderRadius: 4 , backgroundColor: '# 14274e ', flexDirection: ' row ', justifyContent: ' center ', alignItems: ' center ', height: 40 }} >
       < Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
     </ TouchableOpacity >
   </ View >
 )}

عرض الكاميرا بعد منح الإذن في تطبيق Expo

رائع، الآن نحتاج إلى إضافة زر حتى نتمكن من التقاط الصورة الفعلية.

إضافة زر التقاط الصورة

زر التقاط الصورة داخل واجهة الكاميرا

هذا عبارة عن View بسيط داخل عرض الكاميرا (camera view) له موضع مطلق (absolute position). لذا نتأكد من أنه دائمًا في الجزء العلوي من الكاميرا.

 <View style={{ position : 'absolute' , bottom : 0 , flexDirection : 'row' , flex : 1 , width : '100%' , padding : 20 , justifyContent : 'space-between' }} >
   < View style = {{ alignSelf: ' center ', flex: 1 , alignItems: ' center ' }} >
     < TouchableOpacity onPress = {__takePicture} style = {{ width: 70 , height: 70 , bottom: 0 , borderRadius: 50 , backgroundColor: '# fff ' }} />
   </ View >
 </View>

آلية التقاط الصورة وعرض المعاينة

يجب أن يلتقط التطبيق صورة عند الضغط على زر الالتقاط. ستبدو هذه الدالة على النحو التالي:

 const __takePicture = async () => {
   if (!camera) return
   const photo = await camera.takePictureAsync()
 }

أولاً، نتحقق من أن لدينا وصولاً إلى مكون Camera باستخدام ref:

 if (!camera) return // إذا كانت الكاميرا غير معرفة أو فارغة (null)، نوقف تنفيذ الدالة

ثم نلتقط الصورة عن طريق استدعاء الدالة takePictureAsync. تُرجع هذه الدالة وعدًا (promise) وكائنًا (object) يحتوي على تفاصيل الصورة. ستبدو النتيجة هكذا:

 Object {
   "height" : 4224 ,
   "uri" : "file:///var/mobile/Containers/Data/Application/E6740A15-93AF-4120-BF11-6E8B74AFBF93/Library/Caches/ExponentExperienceData/%2540anonymous%252Fcamera-app-ee0fa3c8-1bb1-4d62-9863-33bf26341c55/Camera/19F0C5DD-7CA6-4043-8D89-AF65A1055C7E.jpg" ,
   "width" : 1952 ,
 }

نحن مهتمون فقط بمسار الصورة (Picture URL) الذي يُشار إليه بـ uri. بعد التقاط الصورة، يجب أن نعرض معاينة الصورة ونخفي عرض الكاميرا. للقيام بذلك، سنستخدم اثنين من الـ hooks لتغيير الحالة:

 const [previewVisible, setPreviewVisible] = useState( false )
 const [capturedImage, setCapturedImage] = useState<any>( null )
 const __takePicture = async () => {
   if (!camera) return
   const photo = await camera.takePictureAsync()
   console .log(photo)
   setPreviewVisible( true )
   setCapturedImage(photo)
 }

نستخدم setPreviewVisible(true) لعرض المعاينة و setCapturedImage(photo) لتخزين كائن النتيجة.

ثم نعرض المعاينة على النحو التالي:

 {previewVisible && capturedImage ? (
   < CameraPreview photo = {capturedImage} />
 ) : (
   < Camera style = {{flex: 1 }} ref = {(r) => { camera = r }} >
     < View style = {{ flex: 1 , width: ' 100 %', backgroundColor: ' transparent ', flexDirection: ' row ' }} >
       < View style = {{ position: ' absolute ', bottom: 0 , flexDirection: ' row ', flex: 1 , width: ' 100 %', padding: 20 , justifyContent: ' space-between ' }} >
         < View style = {{ alignSelf: ' center ', flex: 1 , alignItems: ' center ' }} >
           < TouchableOpacity onPress = {__takePicture} style = {{ width: 70 , height: 70 , bottom: 0 , borderRadius: 50 , backgroundColor: '# fff ' }} />
         </ View >
       </ View >
     </ View >
   </ Camera >
 )}

مكون CameraPreview يبدو كالتالي:

 const CameraPreview = ( {photo}: any ) => {
   console .log( 'sdsfds' , photo)
   return (
     < View style = {{ backgroundColor: ' transparent ', flex: 1 , width: ' 100 %', height: ' 100 %' }} >
       < ImageBackground source = {{uri: photo && photo.uri }} style = {{ flex: 1 }} />
     </ View >
   )
 }

والنتيجة تبدو هكذا:

معاينة الصورة الملتقطة في تطبيق الكاميرا

إعادة التقاط الصورة: خيارات إضافية للمعاينة

يمكننا إضافة بعض الأزرار إلى شاشة المعاينة التي ستسمح للمستخدم بأداء المزيد من الإجراءات، مثل إعادة التقاط الصورة أو حفظها.

خيارات إعادة التقاط الصورة وحفظها

أضف خاصيتي savePhoto و retakePicture إلى مكون CameraPreview على النحو التالي:

<CameraPreview photo={capturedImage} savePhoto={__savePhoto} retakePicture={__retakePicture} />

عند الضغط على زر “إعادة التقاط” (Re-take)، سيتعين علينا إخفاء المعاينة، وإزالة الصورة الحالية، وعرض الكاميرا مرة أخرى. نفعل ذلك باستخدام الكود التالي:

 const __retakePicture = () => {
   setCapturedImage( null )
   setPreviewVisible( false )
   __startCamera()
 }

عملية إعادة التقاط الصورة بعد المعاينة

إضافة خيارات متقدمة: الكاميرا الأمامية/الخلفية والفلاش

توفر حزمة expo-camera العديد من الخيارات لتخصيص الكاميرا، مثل وضع الفلاش (FlashMode)، وتحديد نوع الكاميرا (أمامية/خلفية)، والتكبير (zooming)، وغير ذلك.

تفعيل وضع الفلاش (FlashMode)

لنضف خيارًا يتيح للمستخدم تشغيل وضع الفلاش أو إيقافه:

تفعيل وإلغاء تفعيل وضع الفلاش في تطبيق الكاميرا

نقوم ببساطة بإنشاء زر صغير لتبديل الفلاش بين التشغيل والإيقاف، هكذا:

 <TouchableOpacity onPress={__handleFlashMode} style={{ position : 'absolute' , left : '5%' , top : '10%' , backgroundColor : flashMode === 'off' ? '#000' : '#fff' , borderRadius : '50%' , height : 25 , width : 25 }} >
   < Text style = {{ fontSize: 20 }} > ⚡️ </ Text >
 </TouchableOpacity>

ونقوم بتغيير الحالة (state) عند الضغط على الزر:

 const [flashMode, setFlashMode] = React.useState( 'off' )
 const __handleFlashMode = () => {
   if (flashMode === 'on' ) {
     setFlashMode( 'off' )
   } else if (flashMode === 'off' ) {
     setFlashMode( 'on' )
   } else {
     setFlashMode( 'auto' )
   }
 }

ثم نضيف خاصية flashMode إلى مكون الكاميرا:

 <Camera flashMode={flashMode} style={{ flex : 1 }} ref={ ( r ) => { camera = r }} ></Camera>

التبديل بين الكاميرا الأمامية والخلفية

سنضيف زرًا للتبديل بين الكاميرا الخلفية والأمامية. يمكننا الحصول على نوع الكاميرا الافتراضي مباشرة من وحدة الكاميرا كما يلي:

 const [cameraType, setCameraType] = React.useState(Camera.Constants.Type.back)

أضف خاصية type على النحو التالي:

 <Camera type={cameraType} flashMode={flashMode} style={{ flex : 1 }} ref={ ( r ) => { camera = r }} ></Camera>

ثم أضف زر التبديل:

<TouchableOpacity onPress={__switchCamera} style={{ marginTop : 20 , borderRadius : '50%' , height : 25 , width : 25 }} >
  < Text style = {{ fontSize: 20 }} > {cameraType === 'front' ? '?' : '?'} </ Text >
</TouchableOpacity>

ودالة التبديل:

 const __switchCamera = () => {
   if (cameraType === 'back' ) {
     setCameraType( 'front' )
   } else {
     setCameraType( 'back' )
   }
 }

هذه هي النتيجة:

التبديل بين الكاميرا الأمامية والخلفية في تطبيق Expo

يمكنك العثور على الكود المصدري الكامل لهذا المشروع على GitHub.

الخلاصة التقنية

بشكل عام، يُعد Expo أداة مذهلة يمكنها توفير الكثير من الوقت والجهد للمطورين. فهو يتيح لك البدء في بناء التطبيقات مباشرة دون الحاجة إلى القلق بشأن إعداد بيئة التطوير المعقدة. ومع ذلك، في بعض الحالات المتقدمة، قد تحتاج إلى بناء ملحقات أصلية (native extensions) أو التعامل مع ميزات الجهاز بطرق مخصصة. في هذه السيناريوهات، قد يكون استخدام React Native CLI خيارًا أفضل، حيث يمنحك تحكمًا كاملاً في الكود الأصلي (native code) ويسمح لك بتعديله بسهولة لتلبية متطلباتك الخاصة. يظل Expo خيارًا ممتازًا للمشاريع التي تتطلب سرعة التطوير وتبسيط العمليات، بينما يوفر React Native CLI المرونة القصوى للمطورين ذوي الاحتياجات المحددة على مستوى النظام الأصلي.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *