
كيفية برمجة تطبيق مرآة احترافي على اندرويد ستوديو | Java
في عالم تطوير تطبيقات الأندرويد المتنامي، تزداد الحاجة إلى تطبيقات بسيطة
وعملية تلبي احتياجات المستخدمين اليومية. يعتبر تطبيق المرآة مثالاً ممتازاً على
هذه الفئة، حيث يحول كاميرا الهاتف الأمامية إلى مرآة فورية. هذا المقال سيوضح
لك كيفية برمجة تطبيق مرآة احترافي باستخدام أندرويد ستوديو ولغة جافا، مع التركيز
على تحسين الأداء وتقديم تجربة مستخدم سلسة. سنتناول الخطوات الأساسية لبناء تطبيق
كاميرا يمكن الاعتماد عليه، بدءًا من تصميم الواجهة ووصولاً إلى وظائف الكاميرا المتقدمة.
خطوات برمجة تطبيق مرآة احترافي باستخدام كاميرا الهاتف
إن بناء تطبيق مرآة احترافي يتطلب فهمًا جيدًا لـواجهات برمجة تطبيقات
الكاميرا في أندرويد وأفضل الممارسات في تصميم تطبيقات الأندرويد.
يهدف هذا الدليل إلى تبسيط عملية تطوير تطبيق المرآة، موفرًا خارطة طريق واضحة
لمساعدتك على إنشاء تطبيق كاميرا فعال وموثوق. سنركز على البرمجة بلغة
جافا داخل بيئة أندرويد ستوديو، مع توضيح المفاهيم الأساسية للكاميرا 2 API
وكيفية معالجة بث الفيديو لتقديم وظيفة المرآة الحقيقية.
1. التحضير والبيئة
- المتطلبات:
Android Studio (آخر إصدار)
لغة البرمجة: Java أو Kotlin (سنستخدم Java هنا)
API Level: 21 أو أعلى (Camera2 API)
صلاحيات الكاميرا .
2. إنشاء المشروع
الخطوات:
افتح Android Studio
File > New Project > Empty Activity
اختر لغة Java ، سَمِّ المشروع: SmartMirrorApp
3. إضافة الصلاحيات في AndroidManifest.xml
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.front" android:required="false" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<application
android:usesCleartextTraffic="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
...
</application>
--
4. تصميم واجهة المستخدم activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<SurfaceView android:id="@+id/camera_preview_surface" android:layout_width="match_parent" android:layout_height="match_parent" />
<ImageButton android:id="@+id/toggle_camera_button" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_camera" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="Toggle Camera" android:scaleType="fitCenter" android:padding="8dp"/>
</RelativeLayout>
--
5. تهيئة الكاميرا (Camera2 API) في MainActivity.java
الخطوات:
استخدام TextureView لعرض الكاميرا، فتح الكاميرا الأمامية،
عرض الفيديو بدون التقاط صورة.
java
import android.Manifest;import android.content.Context;import android.content.pm.PackageManager;import android.graphics.ImageFormat;import android.graphics.SurfaceTexture;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraCaptureSession;import android.hardware.camera2.CameraCharacteristics;import android.hardware.camera2.CameraDevice;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CaptureRequest;import android.hardware.camera2.params.StreamConfigurationMap;import android.media.ImageReader;import android.os.Bundle;import android.os.Handler;import android.os.HandlerThread;import android.util.Log;import android.util.Size;import android.view.Surface;import android.view.SurfaceHolder;import android.view.SurfaceView;import android.view.TextureView; // بديل لـ SurfaceView إذا كنت تستخدم TextureViewimport android.view.View;import android.widget.ImageButton;import android.widget.Toast;
import androidx.annotation.NonNull;import androidx.appcompat.app.AppCompatActivity;import androidx.core.app.ActivityCompat;import androidx.core.content.ContextCompat;
import java.util.Arrays;import java.util.Collections;import java.util.Comparator;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CAMERA_PERMISSION = 200; private static final String TAG = "MirrorApp";
private SurfaceView cameraPreviewSurface; private SurfaceHolder surfaceHolder;
private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; private CaptureRequest.Builder captureRequestBuilder; private Size imageDimension; private ImageReader imageReader;
private Handler mBackgroundHandler; private HandlerThread mBackgroundThread;
private String cameraId; private int currentCameraFacing = CameraCharacteristics.LENS_FACING_FRONT; // الكاميرا الأمامية افتراضياً
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
cameraPreviewSurface = findViewById(R.id.camera_preview_surface); surfaceHolder = cameraPreviewSurface.getHolder(); surfaceHolder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { // عند إنشاء السطح، نبدأ بفتح الكاميرا checkCameraPermissions(); }
@Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { // يمكن هنا تعديل أبعاد العرض إذا تغير حجم السطح }
@Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { // عند تدمير السطح، نغلق الكاميرا closeCamera(); } });
ImageButton toggleCameraButton = findViewById(R.id.toggle_camera_button); toggleCameraButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { toggleCamera(); } }); }
// --- إدارة دورة حياة التطبيق والخيوط الخلفية ---
@Override protected void onResume() { super.onResume(); startBackgroundThread(); if (cameraPreviewSurface.isAvailable()) { checkCameraPermissions(); } else { // إذا لم يكن السطح جاهزًا، فإن callback: surfaceCreated سيتم استدعاؤها لاحقًا } }
@Override protected void onPause() { closeCamera(); stopBackgroundThread(); super.onPause(); }
private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); }
private void stopBackgroundThread() { if (mBackgroundThread != null) { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { Log.e(TAG, "Error stopping background thread", e); } } }
// --- إدارة الأذونات ---
private void checkCameraPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { openCamera(); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } }
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CAMERA_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { openCamera(); } else { Toast.makeText(this, "Camera permission is required to use this app.", Toast.LENGTH_LONG).show(); finish(); } } }
// --- فتح الكاميرا وتهيئة الإعدادات ---
private void openCamera() { CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { // البحث عن الكاميرا الأمامية أو الخلفية بناءً على currentCameraFacing for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); if (facing != null && facing == currentCameraFacing) { this.cameraId = cameraId; StreamConfigurationMap map = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map == null) { continue; } // اختيار أفضل حجم لعرض الكاميرا imageDimension = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), cameraPreviewSurface.getWidth(), cameraPreviewSurface.getHeight());
// يمكنك أيضًا إعداد ImageReader لالتقاط الصور الثابتة هنا imageReader = ImageReader.newInstance(imageDimension.getWidth(), imageDimension.getHeight(), ImageFormat.JPEG, 1); // imageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
break; } }
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // يجب أن تكون الأذونات قد تم التعامل معها بالفعل بواسطة checkCameraPermissions return; } manager.openCamera(cameraId, stateCallback, mBackgroundHandler);
} catch (CameraAccessException e) { Log.e(TAG, "Camera Access Exception", e); } }
private void closeCamera() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); cameraCaptureSession = null; } if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; } if (imageReader != null) { imageReader.close(); imageReader = null; } }
private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { Log.e(TAG, "Camera Opened"); cameraDevice = camera; createCameraPreview(); // بمجرد فتح الكاميرا، نبدأ المعاينة }
@Override public void onDisconnected(@NonNull CameraDevice camera) { Log.e(TAG, "Camera Disconnected"); camera.close(); cameraDevice = null; }
@Override public void onError(@NonNull CameraDevice camera, int error) { Log.e(TAG, "Camera Error: " + error); camera.close(); cameraDevice = null; Toast.makeText(MainActivity.this, "Camera error: " + error, Toast.LENGTH_SHORT).show(); finish(); } };
// --- اختيار الحجم الأمثل للكاميرا (مساعد) --- private static Size chooseOptimalSize(Size[] choices, int width, int height) { // جمع الأحجام المدعومة التي هي أكبر من أو تساوي حجم المعاينة // والحفاظ على نسبة العرض إلى الارتفاع Size bestSize = choices[0]; long minDiff = Long.MAX_VALUE;
for (Size option : choices) { long diff = Math.abs(option.getWidth() * option.getHeight() - width * height); if (diff < minDiff) { minDiff = diff; bestSize = option; } else if (diff == minDiff && option.getWidth() * option.getHeight() > bestSize.getWidth() * bestSize.getHeight()) { // إذا كانت الفروق متساوية، اختر الأكبر bestSize = option; } } return bestSize; }
// --- تبديل الكاميرا (أمامية/خلفية) --- private void toggleCamera() { closeCamera(); // أغلق الكاميرا الحالية currentCameraFacing = (currentCameraFacing == CameraCharacteristics.LENS_FACING_FRONT) ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT; openCamera(); // افتح الكاميرا الجديدة }
// ... (تكملة الكود في الأقسام التالية)}
--
* عرض فيديو الكاميرا المباشر :
بعد فتح الكاميرا، نقوم بإنشاء CaptureSession وCaptureRequest
لربط بث الكاميرا بـ SurfaceView.
الكود البرمجي في MainActivity.java (تكملة للكود السابق):
Java
// ... (تكملة من الكود السابق)
private void createCameraPreview() { try { Surface surface = surfaceHolder.getSurface();
// بناء CaptureRequest للمعاينه (preview) captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); captureRequestBuilder.addTarget(surface); // ربط البث بسطح العرض
cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), // أضف imageReader.getSurface لالتقاط الصور new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { if (cameraDevice == null) { return; // الكاميرا مغلقة بالفعل } MainActivity.this.cameraCaptureSession = cameraCaptureSession; updatePreview(); // بدء تحديث المعاينة }
@Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { Toast.makeText(MainActivity.this, "Configuration changed", Toast.LENGTH_SHORT).show(); } }, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, "Camera Access Exception creating preview", e); } }
private void updatePreview() { if (cameraDevice == null) { Log.e(TAG, "updatePreview error, cameraDevice is null"); return; } captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); try { cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, "Error updating preview", e); } }
// ... (تكملة الكود في الأقسام التالية)}
--
ميزات اضافة لبرمجة تطبيق المرآة احترافيًا
إليك الشرح الكامل لكل الميزات و الاضافات لجعل تطبيق المرآة احترافيًا بالكامل :
1. تصميم واجهة بـ Material 3 (XML + Material Components)
* في themes.xml :
xml
<!-- Theme.Material3.Light.NoActionBar -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/purple_500</item>
<item name="colorOnPrimary">@color/white</item>
<item name="android:windowLightStatusBar">true</item>
</style>
--
* واجهة activity_main.xml :
xml
<com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="0dp" app:cardCornerRadius="0dp">
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent">
<TextureView android:id="@+id/textureView" android:layout_width="match_parent" android:layout_height="match_parent" />
<!-- زر قلب الصورة --> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/flipButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_margin="16dp" android:src="@drawable/ic_flip" app:backgroundTint="@color/purple_500" android:layout_alignParentStart="true" />
<!-- زر التقاط صورة --> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/captureButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" android:layout_margin="16dp" android:src="@drawable/ic_camera" app:backgroundTint="@color/purple_500" /> </RelativeLayout></com.google.android.material.card.MaterialCardView>
--
2. زر التقاط صورة (Snapshot)
* أضف داخل MainActivity.java :
java
ImageButton captureButton = findViewById(R.id.captureButton);
captureButton.setOnClickListener(v -> captureImage());
private void captureImage() {
Bitmap bitmap = textureView.getBitmap();
if (bitmap != null) {
try {
File file = new File(getExternalFilesDir(null), "mirror_" + System.currentTimeMillis() + ".jpg");
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
out.close();
Toast.makeText(this, "تم حفظ الصورة", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--
3. تأثيرات تجميل (Beautify Filter)
* يمكنك محاكاة فلتر بسيط باستخدام ColorMatrix:
java
private void applyBeautifyFilter() {
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(1.5f); // زيادة حيوية الألوان
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
textureView.setColorFilter(filter);
}
--
4. شاشة بيضاء كـ "فلاش أمامي"
* في MainActivity.java:
java
View flashOverlay;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
flashOverlay = new View(this);
flashOverlay.setBackgroundColor(Color.WHITE);
flashOverlay.setAlpha(0f);
addContentView(flashOverlay, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
private void flashScreen() {
flashOverlay.setAlpha(1f);
flashOverlay.animate().alpha(0f).setDuration(500).start();
}
--
* ونستخدمه داخل ()captureImage :
java
flashScreen();
--
5. الوضع الليلي + عكس تلقائي
* في themes.xml :
xml
<item name="android:forceDarkAllowed">true</item>
* ولعكس الصورة تلقائيًا:
java
@Override
protected void onResume() {
super.onResume();
textureView.setScaleX(-1f); // Mirror by default
}
--
6. تحسين الأداء (الخيوط الخلفية)
* تشغيل الكاميرا على Thread منفصل :
java
private void startBackgroundThread() {
backgroundThread = new HandlerThread("CameraThread");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
--
7. إدارة إيقاف التشغيل التلقائي :
* أضف هذا داخل ()onPause :
java
@Override
protected void onPause() {
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
stopBackgroundThread();
super.onPause();
}
--
8. صور شاشة ذات جودة عالية
* النصائح:
- التقط صورًا من داخل التطبيق الحقيقي عبر Emulator أو هاتف
- استخدم أداة Android Studio > Device Manager > Screenshot
- قم بتعديلها عبر Canva أو Photoshop
- احفظها بأبعاد: 1080x1920 px.
* خاتمة:
باستخدام هذه المجموعة الكاملة من الأدوات والتقنيات، يمكنك برمجة تطبيق
مرآة احترافي على أندرويد بواجهة أنيقة وميزات متقدمة. سواء كنت مطورًا مبتدئًا
أو تبحث عن تحسين تطبيقك للنشر، فإن هذه الأدوات تضمن تجربة مستخدم سلسة وجذابة.
مع نهاية هذا الدليل، تكون قد اكتسبت فهمًا شاملاً لـخطوات برمجة تطبيق مرآة احترافي
باستخدام أندرويد ستوديو بلغة جافا. لقد قمنا بتغطية كل شيء بدءًا من إعداد المشروع
وتصميم واجهة المستخدم البديهية، وصولاً إلى التعامل مع الكاميرا 2 API ومعالجة
بث الفيديو لتقديم وظيفة المرآة الحقيقية. تذكر أن تحسين الأداء واختبار
التطبيق على أجهزة متنوعة هما مفتاحان لـتطبيق كاميرا مستقر وعالي الجودة.
واصل استكشاف وثائق أندرويد الرسمية والموارد المتاحة لتوسيع ميزات تطبيقك،
مثل إضافة تحكم بالتكبير أو تأثيرات بصرية، لجعله تطبيق مرآة احترافي حقًا.