Kotlin Custom Camera Preview Tutorial Example With Surfaceview Tutorial With Example.
Here, you will learn how to use Surfaceview to capture images using the Kotlin language.
We will be able to create custom camera preview so that you can add custom capture button and other UI widgets on the camera preview surface.
See the below video for more reference.
Step 1. Permissions in New Project
Make a new project in android studio with empty activity as the default one.
Now in your AndroidManifest.xml file, add the following lines.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.CAMERA"/>
We have added three permissions using above lines. Read, Write and Camera permissions.
Now go to your build.gradle(Module: app) file and write the following lines
implementation 'com.squareup.picasso:picasso:2.4.0' implementation 'com.karumi:dexter:5.0.0'
First line will add the picasso library in our project.
Second one will add dexter into our app. This dexter library will give us some idea for implementation of the runtime permissions.
Step 2. Writing Runtime Permissions
In your MainActivity.kt file, write down the below lines
import android.Manifest import android.content.Intent import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.os.StrictMode import android.widget.Toast import com.karumi.dexter.Dexter import com.karumi.dexter.MultiplePermissionsReport import com.karumi.dexter.PermissionToken import com.karumi.dexter.listener.DexterError import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.PermissionRequestErrorListener import com.karumi.dexter.listener.multi.MultiplePermissionsListener class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val builder = StrictMode.VmPolicy.Builder() StrictMode.setVmPolicy(builder.build()) requestReadPermissions() } private fun requestReadPermissions() { Dexter.withActivity(this) .withPermissions( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) .withListener(object : MultiplePermissionsListener { override fun onPermissionsChecked(report: MultiplePermissionsReport) { // check if all permissions are granted if (report.areAllPermissionsGranted()) { Toast.makeText(applicationContext, "All permissions are granted by user!", Toast.LENGTH_SHORT) .show() val intent = Intent(this@MainActivity, CaptureActivity::class.java) startActivity(intent) finish() } // check for permanent denial of any permission if (report.isAnyPermissionPermanentlyDenied) { // show alert dialog navigating to Settings //openSettingsDialog(); } } override fun onPermissionRationaleShouldBeShown(permissions: List<PermissionRequest>, token: PermissionToken) { token.continuePermissionRequest() } }).withErrorListener(object : PermissionRequestErrorListener { override fun onError(error: DexterError) { Toast.makeText(applicationContext, "Some Error! ", Toast.LENGTH_SHORT).show() } }) .onSameThread() .check() } }
Inside onCreate() method, there is another method called requestReadPermissions()
Below is the code snippet for requestReadPermissions()
private fun requestReadPermissions() { Dexter.withActivity(this) .withPermissions( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) .withListener(object : MultiplePermissionsListener { override fun onPermissionsChecked(report: MultiplePermissionsReport) { // check if all permissions are granted if (report.areAllPermissionsGranted()) { Toast.makeText(applicationContext, "All permissions are granted by user!", Toast.LENGTH_SHORT) .show() val intent = Intent(this@MainActivity, CaptureActivity::class.java) startActivity(intent) finish() } // check for permanent denial of any permission if (report.isAnyPermissionPermanentlyDenied) { // show alert dialog navigating to Settings //openSettingsDialog(); } } override fun onPermissionRationaleShouldBeShown(permissions: List<PermissionRequest>, token: PermissionToken) { token.continuePermissionRequest() } }).withErrorListener(object : PermissionRequestErrorListener { override fun onError(error: DexterError) { Toast.makeText(applicationContext, "Some Error! ", Toast.LENGTH_SHORT).show() } }) .onSameThread() .check() }
We are covering three types of permissions in the above method. One is Read external storage, second is write external storage and last is camera.
This method simplifies the process of runtime permissions.
You just need to add the permissions and rest will be taken cared by this method.
When the user grants all the permissions, system will open the CaptureActivity.
Step 3. Making Camera Preview
Create a new Kotlin class and give it a name like CameraPreview.kt
Write down the below lines in CameraPreview.kt
import java.io.IOException import android.content.Context import android.hardware.Camera import android.util.Log import android.view.SurfaceHolder import android.view.SurfaceView import android.view.View class CameraPreview(context: Context, private var mCamera: Camera?) : SurfaceView(context), SurfaceHolder.Callback { private val mHolder: SurfaceHolder init { mHolder = holder mHolder.addCallback(this) // deprecated setting, but required on Android versions prior to 3.0 mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS) } override fun surfaceCreated(holder: SurfaceHolder) { try { // create the surface and start camera preview if (mCamera == null) { mCamera!!.setPreviewDisplay(holder) mCamera!!.startPreview() } } catch (e: IOException) { Log.d(View.VIEW_LOG_TAG, "Error setting camera preview: " + e.message) } } fun refreshCamera(camera: Camera?) { if (mHolder.surface == null) { // preview surface does not exist return } // stop preview before making changes try { mCamera!!.stopPreview() } catch (e: Exception) { // ignore: tried to stop a non-existent preview } // set preview size and make any resize, rotate or // reformatting changes here // start preview with new settings setCamera(camera) try { mCamera!!.setPreviewDisplay(mHolder) mCamera!!.startPreview() } catch (e: Exception) { Log.d(View.VIEW_LOG_TAG, "Error starting camera preview: " + e.message) } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) { // If your preview can change or rotate, take care of those events here. // Make sure to stop the preview before resizing or reformatting it. refreshCamera(mCamera) } fun setCamera(camera: Camera?) { //method to set a camera instance mCamera = camera } override fun surfaceDestroyed(holder: SurfaceHolder) { // TODO Auto-generated method stub // mCamera.release(); } }
This class will help us to generate the camera preview. It will create the objects of Camera and Surface classes.
Both these objects will work together to make smooth camera preview.
Step 4. Writing Capture Activity
Make a new activity named as CaptureActivity. You need to add the following lines in activity_capture.xml file.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:id="@+id/cPreview"> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="bottom|center" android:orientation="horizontal"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnCam" android:textSize="15sp" android:text="Take Photo"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnSwitch" android:layout_marginLeft="0dp" android:textSize="15sp" android:text="Switch Camera"/> </LinearLayout> </RelativeLayout>
Now see the above code. There are two Linear Layouts inside the Relative Layout.
First linear layout will work as the preview surface. We will initiate the camera preview in this linear layout.
Second linear layout have two buttons. They are besides each other. One button is “TAKE PHOTO” and another is “SWITCH CAMERA”
Now in your CaptureActivity.kt class, you should add the below source snippet
import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.hardware.Camera import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.os.StrictMode import android.util.Log import android.view.WindowManager import android.widget.Button import android.widget.LinearLayout class CaptureActivity : AppCompatActivity() { private var mCamera: Camera? = null private var mPreview: CameraPreview? = null private var mPicture: Camera.PictureCallback? = null private var capture: Button? = null private var switchCamera: Button? = null private var myContext: Context? = null private var cameraPreview: LinearLayout? = null private var cameraFront = false private val pictureCallback: Camera.PictureCallback get() = object : Camera.PictureCallback { override fun onPictureTaken(data: ByteArray, camera: Camera) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val intent = Intent(this@CaptureActivity, PictureActivity::class.java) startActivity(intent) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_capture) val builder = StrictMode.VmPolicy.Builder() StrictMode.setVmPolicy(builder.build()) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) myContext = this initCamere() } private fun initCamere(){ mCamera = Camera.open() mCamera!!.setDisplayOrientation(90) cameraPreview = findViewById(R.id.cPreview) as LinearLayout mPreview = CameraPreview(myContext as CaptureActivity, mCamera) cameraPreview!!.addView(mPreview) capture = findViewById(R.id.btnCam) as Button capture!!.setOnClickListener { mCamera!!.takePicture(null, null, mPicture) } switchCamera = findViewById(R.id.btnSwitch) as Button switchCamera!!.setOnClickListener { //get the number of cameras val camerasNumber = Camera.getNumberOfCameras() if (camerasNumber > 1) { //release the old camera instance //switch camera, from the front and the back and vice versa releaseCamera() chooseCamera() } else { } } mCamera!!.startPreview() } private fun findFrontFacingCamera(): Int { var cameraId = -1 // Search for the front facing camera val numberOfCameras = Camera.getNumberOfCameras() for (i in 0 until numberOfCameras) { val info = Camera.CameraInfo() Camera.getCameraInfo(i, info) if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { cameraId = i cameraFront = true break } } return cameraId } private fun findBackFacingCamera(): Int { var cameraId = -1 //Search for the back facing camera //get the number of cameras val numberOfCameras = Camera.getNumberOfCameras() //for every camera check for (i in 0 until numberOfCameras) { val info = Camera.CameraInfo() Camera.getCameraInfo(i, info) if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { cameraId = i cameraFront = false break } } return cameraId } public override fun onResume() { super.onResume() if (mCamera == null) { mCamera = Camera.open() mCamera!!.setDisplayOrientation(90) mPicture = pictureCallback mPreview!!.refreshCamera(mCamera) Log.d("noooo", "null") } else { Log.d("noooo", "no null") } } fun chooseCamera() { //if the camera preview is the front if (cameraFront) { val cameraId = findBackFacingCamera() if (cameraId >= 0) { //open the backFacingCamera //set a picture callback //refresh the preview mCamera = Camera.open(cameraId) mCamera!!.setDisplayOrientation(90) mPicture = pictureCallback mPreview!!.refreshCamera(mCamera) } } else { val cameraId = findFrontFacingCamera() if (cameraId >= 0) { //open the backFacingCamera //set a picture callback //refresh the preview mCamera = Camera.open(cameraId) mCamera!!.setDisplayOrientation(90) mPicture = pictureCallback mPreview!!.refreshCamera(mCamera) } } } override fun onPause() { super.onPause() //when on Pause, release camera in order to be used from other applications releaseCamera() } private fun releaseCamera() { // stop and release camera if (mCamera != null) { mCamera!!.stopPreview() mCamera!!.setPreviewCallback(null) mCamera!!.release() mCamera = null } } companion object { lateinit var bitmap: Bitmap } }
Deeper Explanation
First of all, see the following
private var mCamera: Camera? = null private var mPreview: CameraPreview? = null private var mPicture: Camera.PictureCallback? = null private var capture: Button? = null private var switchCamera: Button? = null private var myContext: Context? = null private var cameraPreview: LinearLayout? = null private var cameraFront = false
System will create the objects of various classes like Camera, CameraPreview, Button, Context etc.
An object of LinearLayout class is also there. A boolean variable is there whose name is cameraFront.
Now read the below lines
private val pictureCallback: Camera.PictureCallback get() = object : Camera.PictureCallback { override fun onPictureTaken(data: ByteArray, camera: Camera) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val intent = Intent(this@CaptureActivity, PictureActivity::class.java) startActivity(intent) } }
Compiler will execute the above code snippet when the user have clicked the image.
This coding lines will create the bitmap of the image data using BitmapFactory.
Then it will start the PictureActivity. We will create this PictureActivity in the next step.
Now see the below code lines
val builder = StrictMode.VmPolicy.Builder() StrictMode.setVmPolicy(builder.build()) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
These lines will help us to keep preview screen smooth and remove all other widgets from the screen.
Now focus on the following
private fun initCamere(){ mCamera = Camera.open() mCamera!!.setDisplayOrientation(90) cameraPreview = findViewById(R.id.cPreview) as LinearLayout mPreview = CameraPreview(myContext as CaptureActivity, mCamera) cameraPreview!!.addView(mPreview) capture = findViewById(R.id.btnCam) as Button capture!!.setOnClickListener { mCamera!!.takePicture(null, null, mPicture) } switchCamera = findViewById(R.id.btnSwitch) as Button switchCamera!!.setOnClickListener { //get the number of cameras val camerasNumber = Camera.getNumberOfCameras() if (camerasNumber > 1) { //release the old camera instance //switch camera, from the front and the back and vice versa releaseCamera() chooseCamera() } else { } } mCamera!!.startPreview() }
This method will initiate the process of creating a camera preview using surface view.
Another thing in these lines are the code for button click. When the user clicks on “TAKE PHOTO” button, compiler will execute the takePicture() method.
This method will simply capture the image.
Another button is “SWITCH CAMERA’. When user clicks this button, compiler will run the code for switching the camera between front and reverse.
For this, it will call the chooseCamera() method. Below is the code lines for chooseCamera() method.
fun chooseCamera() { //if the camera preview is the front if (cameraFront) { val cameraId = findBackFacingCamera() if (cameraId >= 0) { //open the backFacingCamera //set a picture callback //refresh the preview mCamera = Camera.open(cameraId) mCamera!!.setDisplayOrientation(90) mPicture = pictureCallback mPreview!!.refreshCamera(mCamera) } } else { val cameraId = findFrontFacingCamera() if (cameraId >= 0) { //open the backFacingCamera //set a picture callback //refresh the preview mCamera = Camera.open(cameraId) mCamera!!.setDisplayOrientation(90) mPicture = pictureCallback mPreview!!.refreshCamera(mCamera) } } }
This method has one if condition. This condition will be true if current camera is front camera. Otherwise compiler will go into the else statement.
Step 5. Displaying the Image
Now it is time to display the captured image. For this, make a new activity and give it a name like PictureActivity.
You need to write the below in the activity_picture.xml file.
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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=".PictureActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="You have just taken below picture" tools:ignore="MissingConstraints"/> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/img" android:padding="20dp" android:src="@mipmap/ic_launcher"/> </android.support.constraint.ConstraintLayout>
This file contains one image view and one text view. Text view will not change it’s value.
We will preview the image into the image view.
Now your PictureActivity.kt file should have the below code snippet.
import android.graphics.Bitmap import android.media.MediaScannerConnection import android.os.Environment import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.widget.ImageView import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.Calendar class PictureActivity : AppCompatActivity() { private var imageView: ImageView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_picture) imageView = findViewById(R.id.img) imageView!!.setImageBitmap(CaptureActivity.bitmap) saveImage(CaptureActivity.bitmap) } fun saveImage(myBitmap: Bitmap): String { val bytes = ByteArrayOutputStream() myBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bytes) val wallpaperDirectory = File( Environment.getExternalStorageDirectory().toString() + IMAGE_DIRECTORY ) // have the object build the directory structure, if needed. if (!wallpaperDirectory.exists()) { Log.d("dirrrrrr", "" + wallpaperDirectory.mkdirs()) wallpaperDirectory.mkdirs() } try { val f = File( wallpaperDirectory, Calendar.getInstance() .timeInMillis.toString() + ".jpg" ) f.createNewFile() //give read write permission val fo = FileOutputStream(f) fo.write(bytes.toByteArray()) MediaScannerConnection.scanFile( this, arrayOf(f.path), arrayOf("image/jpeg"), null ) fo.close() Log.d("TAG", "File Saved::--->" + f.absolutePath) return f.absolutePath } catch (e1: IOException) { e1.printStackTrace() } return "" } companion object { private val IMAGE_DIRECTORY = "/CustomImage" } }
First of all, compiler will just show the image into the image view.It will use bitmap to preview the image.
Then it will save the image using saveImage() method.
Download Code of Kotlin Custom Camera
https://github.com/demonuts/Kotlin-Custom-Camera-Preview-Tutorial-Example-With-Surfaceview