Skip to main content

How to create a custom camera in Android

In android we can easily access the default camera app using an implicit intent. But it is not easy to develop a custom camera app that matches to your own requirements. Because, there are lots of limitations such as configuration changes like device rotation, onStop and onResume statuses, Memory management, permissions handling and so on.  

Here is a preview of what we are about to build


So today I would like to give you a camera app that will take care of above limitations well and also you can use this code in any project you want. Also I am going to talk about each and every step in detail as far as I can. My camera app has below features:
  • Handling screen rotations(Landscape and portrait modes)
  • Handling onStop and onResume statuses
  • Capture images from both front and back cameras
  • Save images to gallery
  • Handling permissions to support devices with Marshmallow or above
  • Accessing flash light
  • So far I have tested this app on several devices such as Samsung(Tabs 8.4, J7 2016 and S4) and Sony Ericsson(Xperia Mini) devices.
Also if you guys find any function that I have written can be optimized or I have written it wrong way please write that in comments so I can take necessary actions to solve those problems and avoid other people facing the same problem again.

Step 1:

Download the drawable-xxhdpi PNGs(I have used only xxhdpi drawbles in this project) that I have used or you can use your own.

Step 2:

Now go to Android Studio and start a new project.


Step 3:

After creating the project copy your drwables to the corresponding folders and copy below xml file and paste it to to your project. In my case my xml file is activity_main.xml.
 <?xml version="1.0" encoding="utf-8"?>  
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:orientation="vertical">  
   <TextureView  
     android:id="@+id/texture_view"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_weight="1" />  
   <LinearLayout  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignParentBottom="true"  
     android:layout_centerHorizontal="true">  
     <ImageButton  
       android:id="@+id/btnFlash"  
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:layout_centerInParent="true"  
       android:onClick="onFlashClick"  
       android:src="@drawable/ic_flash"  
       android:text="New Button" />  
     <ImageButton  
       android:id="@+id/btnCapture"  
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:layout_centerInParent="true"  
       android:onClick="onCaptureClick"  
       android:src="@drawable/ic_capture"  
       android:text="New Button" />  
     <ImageButton  
       android:id="@+id/btnSwitchCamera"  
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:layout_centerInParent="true"  
       android:onClick="onSwitchCameraClick"  
       android:src="@drawable/ic_switch_camera"  
       android:text="New Button" />  
   </LinearLayout>  
 </RelativeLayout>  

Here these all click events will be implemented later
 android:onClick="onFlashClick"  
 android:onClick="onCaptureClick"  
 android:onClick="onSwitchCameraClick"  


After that before doing other step let's define our instance variables and onCreate() method on top of our MainActivty.java class first.
 private static final int MY_PERMISSIONS_REQUEST_CAMERA = 100;  
   private int mCamId;  
   private Camera.CameraInfo mCurrentCameraInfo;  
   private Camera mCamera;  
   private Camera.Size mOptimalCameraPreviewSize;  
   private boolean mTurnFlash;  
   private TextureView mTextureView;  
   private CameraSurfaceTextureListener mCameraSurfaceTextureListener;  
   @Override  
   protected void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     requestWindowFeature(Window.FEATURE_NO_TITLE);  
     getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);  
     setContentView(R.layout.activity_main);  
     mCameraSurfaceTextureListener = new CameraSurfaceTextureListener();  
     mTextureView = (TextureView) findViewById(R.id.texture_view);  
   }  

CameraSurfaceTextureListener will be implement later in step 5. 

Step 4:

Now we need to add following permissions to AndroidManifest.xml
 <uses-feature android:name="android.hardware.camera" />  
 <uses-permission android:name="android.permission.RECORD_AUDIO" />  
 <uses-permission android:name="android.permission.CAMERA" />  
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
 <uses-permission  
     android:name="android.permission.FLASHLIGHT"  
     android:permissionGroup="android.permission-group.HARDWARE_CONTROLS"  
     android:protectionLevel="normal" />  
 <uses-permission android:name="android.permission.READ_PHONE_STATE" />  
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  
 <uses-permission android:name="com.sonyericsson.permission.CAMERA_EXTENDED"/>  

and for handling permissions for Marshmallow devices we use below code. So copy and paste this code to you MainActivty.java class.
   private void checkPermissions() {  
     //first check permission has granted  
     if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&  
         ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&  
         ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED &&  
         ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {  
       startCam();  
     } else {  
       //otherwise send a request to user  
       ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE,  
           Manifest.permission.READ_PHONE_STATE, Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_CAMERA);  
     }  
   }  
   
   private void startCam() {  
   
   }  
   
  @Override  
   public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {  
     super.onRequestPermissionsResult(requestCode, permissions, grantResults);  
     switch (requestCode) {  
       case MY_PERMISSIONS_REQUEST_CAMERA:  
         if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {  
           startCam();  
         } else {  
           System.exit(0);  
         }  
         break;  
     }  
   }  
   
  @Override  
   protected void onResume() {  
     super.onResume();  
     checkPermissions();  
   }  

Step 5:

Up to now we have camera interface, Android Manifest files and permissions requests setup. As you can see in activity_main.xml we are using TextureView as our preview texture in our custom camera. So If we want to get TextureView's SurfaceTexture we have to invoke getSurfaceTexture() or we have to use TextureView.SurfaceTextureListener and it is important to know that SurfaceTexture is available only when the TextureView is attached to a window. Here I am using SurfaceTextureListener to get the SurfaceTexture when the app loading for the first time. In order to do that we have to implements TextureView.SurfaceTextureListener as below. You can create an inner class call CameraSurfaceTextureListener and paste below code to it.
   class CameraSurfaceTextureListener implements TextureView.SurfaceTextureListener {  
   
     @Override  
     public void onSurfaceTextureUpdated(SurfaceTexture surface) {  
     }  
   
     @Override  
     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {  
   
     }  
   
     @Override  
     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {  
       releaseCam();  
       return true;  
     }  
   
     @Override  
     public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {  
       Log.d("!!!!", "onSurfaceTextureAvailable!!!");  
       setupCamera(surface, width, height);  
     }  
   }  

This code will show some errors in releaseCam() and  setupCamera() lines keep them commented or create just empty methods for now, because we are going to implement them later.

Step 6:

Before implementing releaseCam() and  setupCamera() we have to implement getCamera() method to get the camera info. So copy and paste below code for that.
   private Pair<Camera.CameraInfo, Integer> getCamera(int facing) {  
     Camera.CameraInfo cameraInfo = new Camera.CameraInfo();  
     final int numberOfCameras = Camera.getNumberOfCameras();  
   
     for (int i = 0; i < numberOfCameras; ++i) {  
       Camera.getCameraInfo(i, cameraInfo);  
       if (cameraInfo.facing == facing) {  
         return new Pair<>(cameraInfo, i);  
       }  
     }  
     return null;  
   }  


And then implement getOptimalPreviewSize() method to get the optimal preview size that we can use according to the device

   private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int width, int height) {  
   
     List<Camera.Size> collectorSizes = new ArrayList<>();  
   
     for (Camera.Size size : sizes) {  
       if (width > height) {  
         if (size.width > width && size.height > height) {  
           collectorSizes.add(size);  
         }  
       } else {  
         if (size.width > height && size.height > width) {  
           collectorSizes.add(size);  
         }  
       }  
     }  
   
     if (collectorSizes.size() > 0) {  
       return Collections.min(collectorSizes, new Comparator<Camera.Size>() {  
         @Override  
         public int compare(Camera.Size lhs, Camera.Size rhs) {  
           return Long.signum(lhs.width * lhs.height - rhs.width * rhs.height);  
         }  
       });  
     }  
     return sizes.get(0);  
   }  

Now create the cameraDisplayRotation() method to set the rotation to the camera according to device rotation
   private int cameraDisplayRotation() {  
     int rotation = getWindowManager().getDefaultDisplay().getRotation();  
     int degrees = 0;  
     switch (rotation) {  
       case Surface.ROTATION_0:  
         degrees = 0;  
         break;  
       case Surface.ROTATION_90:  
         degrees = 90;  
         break;  
       case Surface.ROTATION_180:  
         degrees = 180;  
         break;  
       case Surface.ROTATION_270:  
         degrees = 270;  
         break;  
     }  
   
     if (mCurrentCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {  
       // frontFacing  
       rotation = (mCurrentCameraInfo.orientation + degrees) % 330;  
       rotation = (360 - rotation) % 360;  
   
       if (rotation > 90 && rotation <= 180) {  
         rotation = 180;  
       } else if (rotation > 180 && rotation <= 360) {  
         rotation = 0;  
       }  
   
     } else {  
       // Back-facing  
       rotation = (mCurrentCameraInfo.orientation - degrees + 360) % 360;  
     }  
     mCamera.setDisplayOrientation(rotation);  
   
     return degrees;  
   }  

Step 7:

Now we can implement the setupCamera(),releaseCam() and startCam() as below.
   private void setupCamera(SurfaceTexture surface, int width, int height) {  
     Pair<Camera.CameraInfo, Integer> backCamera = getCamera(mCamId);  
     mCurrentCameraInfo = backCamera.first;  
     mCamera = Camera.open(mCamId);  
     cameraDisplayRotation();  
   
     try {  
       mCamera.setPreviewTexture(surface);  
       mCamera.startPreview();  
     } catch (IOException ioe) {  
       // Something bad happened  
     }  
   
     if (mCamera == null) {  
       return;  
     }  
   
   
     Camera.Parameters parameters = mCamera.getParameters();  
     mOptimalCameraPreviewSize = getOptimalPreviewSize(parameters.getSupportedPreviewSizes(), width, height);  
     parameters.setPreviewSize(mOptimalCameraPreviewSize.width, mOptimalCameraPreviewSize.height);  
   
     if (parameters.getSupportedPictureSizes().size() > 0) {  
       mOptimalCameraPreviewSize = parameters.getSupportedPictureSizes().get(0);  
       parameters.setPictureSize(mOptimalCameraPreviewSize.width, mOptimalCameraPreviewSize.height);  
     }  
   
   
     List<String> supportedFocusModes = parameters.getSupportedFocusModes();  
     if (supportedFocusModes != null && supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {  
       parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);  
     }  
   
     List<String> supportedSceneModes = parameters.getSupportedSceneModes();  
     if (supportedSceneModes != null && supportedSceneModes.contains(Camera.Parameters.SCENE_MODE_AUTO)) {  
       parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);  
     }  
   
     List<String> supportedWhiteBalanceModes = parameters.getSupportedWhiteBalance();  
     if (supportedWhiteBalanceModes != null && supportedWhiteBalanceModes.contains(Camera.Parameters.WHITE_BALANCE_AUTO)) {  
       parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);  
     }  
   
     parameters.setExposureCompensation(0);  
   
     List<Integer> supportedImageFormats = parameters.getSupportedPictureFormats();  
     if (supportedImageFormats != null && supportedImageFormats.contains(ImageFormat.JPEG)) {  
       parameters.setPictureFormat(ImageFormat.JPEG);  
     }  
   
     parameters.setJpegQuality(100);  
     mCamera.setParameters(parameters);  
   
   }  

Note:

Keep in mind to override the onStop() method and call releaseCam() method there as below. If you not release the camera other apps won't be able to access the camera.
   @Override  
   protected void onStop() {  
     super.onStop();  
     releaseCam();  
   }  


So now you have come to the step that you have waiting for... 😉 Now let's run the application for testing the functions we have implemented so far are working correctly.

But if you click any button in the app, it crashes 😒 because still we have not written the click events.

Step 8:

Now let's write the onCaptureClick () event. But before we write it we have to implement takeImage() and getRotation() method. Copy and paste below code to your activity.
   private void takeImage() {  
     mCamera.takePicture(null, null, new Camera.PictureCallback() {  
   
       private File imageFile;  
   
       @Override  
       public void onPictureTaken(byte[] data, Camera camera) {  
         try {  
           // convert byte array into bitmap  
           Bitmap rotatedBitmap = CompressImage.compressImage(data, getRotation());  
           String state = Environment.getExternalStorageState();  
           File folder = null;  
           if (state.contains(Environment.MEDIA_MOUNTED)) {  
             folder = new File(Environment  
                 .getExternalStorageDirectory() + "/Demo");  
           } else {  
             folder = new File(Environment  
                 .getExternalStorageDirectory() + "/Demo");  
           }  
   
           boolean success = true;  
           if (!folder.exists()) {  
             success = folder.mkdirs();  
           }  
           if (success) {  
             String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());  
             imageFile = new File(folder.getAbsolutePath()  
                 + File.separator  
                 + timeStamp  
                 + "Image.jpg");  
   
             imageFile.createNewFile();  
   
             Toast.makeText(MainActivity.this, "Success, Taken an image", Toast.LENGTH_SHORT).show();  
           } else {  
             Toast.makeText(MainActivity.this, "Image Not saved", Toast.LENGTH_SHORT).show();  
             return;  
           }  
   
           ByteArrayOutputStream ostream = new ByteArrayOutputStream();  
   
           // save image into gallery  
           rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, ostream);  
   
           FileOutputStream fout = new FileOutputStream(imageFile);  
           fout.write(ostream.toByteArray());  
           fout.close();  
           ContentValues values = new ContentValues();  
   
           values.put(MediaStore.Images.Media.DATE_TAKEN,  
               System.currentTimeMillis());  
           values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");  
           values.put(MediaStore.MediaColumns.DATA,  
               imageFile.getAbsolutePath());  
   
           getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);  
           camera.startPreview();  
         } catch (Exception e) {  
           e.printStackTrace();  
         }  
   
       }  
     });  
   }  

This method will get the current rotation degree of the device
   private int getRotation() {  
     int rotation = getWindowManager().getDefaultDisplay().getRotation();  
     int degree = 0;  
     switch (rotation) {  
       case Surface.ROTATION_0:  
         degree = 0;  
         break;  
       case Surface.ROTATION_90:  
         degree = 90;  
         break;  
       case Surface.ROTATION_180:  
         degree = 180;  
         break;  
       case Surface.ROTATION_270:  
         degree = 270;  
         break;  
   
       default:  
         break;  
     }  
   
     if (mCurrentCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {  
       // frontFacing  
       rotation = (mCurrentCameraInfo.orientation + degree) % 330;  
       rotation = (360 - rotation) % 360;  
       if (rotation > 90 && rotation <= 180) {  
         rotation = 180;  
       } else if (rotation > 180 && rotation <= 360) {  
         rotation = 0;  
       } else if (rotation == 90) {  
         rotation = -90;  
       }  
     } else {  
       // Back-facing  
       rotation = (mCurrentCameraInfo.orientation - degree + 360) % 360;  
     }  
     return rotation;  
   }  

And create below CompressImage.java class to handle memory and correct the rotation errors of the taken image.
 public class CompressImage {  
   public static Bitmap compressImage(byte[] data, float rotation) {  
   
     Bitmap scaledBitmap = null;  
   
     BitmapFactory.Options options = new BitmapFactory.Options();  
   
 //   by setting this field as true, the actual bitmap pixels are not loaded in the memory. Just the bounds are loaded. If  
 //   you try the use the bitmap here, you will get null.  
     options.inJustDecodeBounds = true;  
     Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length, options);  
   
     int actualHeight = options.outHeight;  
     int actualWidth = options.outWidth;  
   
 //   max Height and width values of the compressed image is taken as 816x612  
     float maxHeight = 816.0f;  
     float maxWidth = 612.0f;  
     float imgRatio = actualWidth / actualHeight;  
     float maxRatio = maxWidth / maxHeight;  
   
 //   width and height values are set maintaining the aspect ratio of the image  
     if (actualHeight > maxHeight || actualWidth > maxWidth) {  
       if (imgRatio < maxRatio) {  
         imgRatio = maxHeight / actualHeight;  
         actualWidth = (int) (imgRatio * actualWidth);  
         actualHeight = (int) maxHeight;  
       } else if (imgRatio > maxRatio) {  
         imgRatio = maxWidth / actualWidth;  
         actualHeight = (int) (imgRatio * actualHeight);  
         actualWidth = (int) maxWidth;  
       } else {  
         actualHeight = (int) maxHeight;  
         actualWidth = (int) maxWidth;  
   
       }  
     }  
   
 //   setting inSampleSize value allows to load a scaled down version of the original image  
     options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);  
   
 //   inJustDecodeBounds set to false to load the actual bitmap  
     options.inJustDecodeBounds = false;  
   
 //   this options allow android to claim the bitmap memory if it runs low on memory  
     options.inPurgeable = true;  
     options.inInputShareable = true;  
     options.inTempStorage = new byte[16 * 1024];  
   
     try {  
 //     load the bitmap from its path  
       bmp = BitmapFactory.decodeByteArray(data, 0, data.length, options);  
     } catch (OutOfMemoryError exception) {  
       exception.printStackTrace();  
   
     }  
     try {  
       scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.ARGB_8888);  
     } catch (OutOfMemoryError exception) {  
       exception.printStackTrace();  
     }  
   
     float ratioX = actualWidth / (float) options.outWidth;  
     float ratioY = actualHeight / (float) options.outHeight;  
     float middleX = actualWidth / 2.0f;  
     float middleY = actualHeight / 2.0f;  
   
     Matrix scaleMatrix = new Matrix();  
     scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);  
   
     Canvas canvas = new Canvas(scaledBitmap);  
     canvas.setMatrix(scaleMatrix);  
     canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));  
   
   
     Matrix matrix = new Matrix();  
     matrix.setRotate(rotation);  
     scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0,  
         scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix,  
         true);  
   
     return scaledBitmap;  
   
   }  
   
   public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {  
     final int height = options.outHeight;  
     final int width = options.outWidth;  
     int inSampleSize = 1;  
   
     if (height > reqHeight || width > reqWidth) {  
       final int heightRatio = Math.round((float) height / (float) reqHeight);  
       final int widthRatio = Math.round((float) width / (float) reqWidth);  
       inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
     }  
     final float totalPixels = width * height;  
     final float totalReqPixelsCap = reqWidth * reqHeight * 2;  
     while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {  
       inSampleSize++;  
     }  
   
     return inSampleSize;  
   }  
 }  

And now create the onCaptureClick () as below 
   public void onCaptureClick(View view) {  
     takeImage();  
   }  

Step 9:

Now we can implement onSwitchCameraClick(). Copy following methods and paste into your activity
   public void onSwitchCameraClick(View view) {  
   
     int tempCamId = (mCamId == Camera.CameraInfo.CAMERA_FACING_BACK ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK);  
     if (hasCamera(tempCamId)) {  
       mCamId = tempCamId;  
       releaseCam();  
       startCam();  
       return;  
     }  
   
     Toast.makeText(this, "Requested camera is not available", Toast.LENGTH_SHORT).show();  
   }  
   
   private boolean hasCamera(int facing) {  
     Camera.CameraInfo cameraInfo = new Camera.CameraInfo();  
     final int numberOfCameras = Camera.getNumberOfCameras();  
   
     for (int i = 0; i < numberOfCameras; ++i) {  
       Camera.getCameraInfo(i, cameraInfo);  
       if (cameraInfo.facing == facing) {  
         return true;  
       }  
     }  
     return false;  
   }  

Step 10:


Finally implement onFlashClick() event

   public void onFlashClick(View view) {  
   
     if (mCamera != null) {  
   
       mTurnFlash = mTurnFlash ? false : true;  
       Camera.Parameters p = mCamera.getParameters();  
       if (mTurnFlash) {  
         p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);  
       } else {  
         p.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);  
       }  
   
       mCamera.setParameters(p);  
     }  
   }  



Wow, now we have successfully completed our custom camera... let's run and enjoy the app... 😋😋😋




Comments

  1. Hi guy. I am getting an issue with TaskStackBuilder when notification comes.

    My issue is,when I tab notification from status bar,app transit into SecondActivity,after I press back button,app will back to MainActivity (as parent activity) but MainActivity will be re-create. I don't want that because MainActivity has BackStack of some fragment. I logged in debug mode,MainActivity was destroyed when I press notification.

    Can you explain for me?

    ReplyDelete
  2. Please post your code for that part.

    ReplyDelete

Post a Comment