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
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.
Now create the cameraDisplayRotation() method to set the rotation to the camera according to device rotation
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.
Here these all click events will be implemented later
After that before doing other step let's define our instance variables and onCreate() method on top of our MainActivty.java class first.
CameraSurfaceTextureListener will be implement later in step 5.
<?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... 😋😋😋
Hi guy. I am getting an issue with TaskStackBuilder when notification comes.
ReplyDeleteMy 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?
Please post your code for that part.
ReplyDelete