Android: call the system camera to take photos + cut (compatible with systems above 7.0)

 

preface

  • In our daily work, we often encounter such needs: for example, to change the user's Avatar, we need to open the camera or album, select the photos, cut them, and finally upload them to the background
  • It is believed that such a function may have been common for small partners for a long time, but there are still some places to pay attention to in the process of implementation
  • This article mainly demonstrates how to realize the function of photographing + cutting, and what should be paid attention to in the process of implementation
  • The full code is given at the bottom of the article

catalogue

catalogue jpg

1, Take photos

1. define relevant variables and permission application

 

 val AUTHORITY = "com.cs.camerademo.fileProvider" //Signature of FileProvider (described later)
 val REQUEST_CODE_CAPTURE_RAW = 6 //Request code when startActivityForResult 
 var imageFile: File? = null     //Photos saved after taking photos
 var imgUri: Uri? = null         //uri of photos saved after taking photos

 

  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />

2. call system camera

 

   //Take photos (return to original image)
    private fun gotoCaptureRaw() {
        imageFile = createImageFile()   
        imageFile?.let {
            val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {  //If it is above 7.0, use FileProvider; otherwise, an error will be reported
                intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                imgUri = FileProvider.getUriForFile(this, AUTHORITY, it)
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri) //Set the location for saving pictures after taking pictures
            } else {
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(it)) //Set the location for saving pictures after taking pictures
            }
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) //Format picture save
            intent.resolveActivity(packageManager)?.let {
                startActivityForResult(intent, REQUEST_CODE_CAPTURE_RAW) //Turn up the system camera
            }
        }
    }

    //Generate a file
    fun createImageFile(isCrop: Boolean = false): File? {
        return try {
            var rootFile = File(rootFolderPath + File.separator + "capture")
            if (!rootFile.exists())
                rootFile.mkdirs()
            val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
            val fileName = if (isCrop) "IMG_${timeStamp}_CROP.jpg" else "IMG_$timeStamp.jpg"
            File(rootFile.absolutePath + File.separator + fileName)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

Before calling the camera, intent needs to set two parameters

  • Intent Putextra ("outputformat", bitmap.compressformat.jpeg.tostring()) sets the format for saving pictures

  • Intent Putextra (mediastore.extra_output, URI) sets the location where pictures are saved after taking pictures. If there is no such sentence, the camera will return a thumbnail of pictures. Please see the complete code for specific effects and codes

Note: when setting the uri of image saving, it is necessary to handle Android7.0 or above systems, otherwise FileUriExposedException will be reported

3. handling exceptions, compatible with 7.0

In systems above 7.0, Android no longer allows the file://Uri Exposed to other app s because there are certain risks, such as:

  • File is private, receive file://Uri The app for cannot access the file.
  • Introduce runtime permissions after Android6.0. If you receive file://Uri app of does not apply for READ_EXTERNAL_STORAGE permission, which will cause a crash when reading a file

The official solution is to use FileProvider. The specific steps are as follows:
Step 1: in androidmanifest Register FileProvider in XML manifest file

 

      <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.cs.camerademo.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
  • Name: the class name of the provider. If the default v4 FileProvide is used
  • android:authorities is a signature authentication that can be customized, but it needs to be consistent when obtaining URIs;
  • grantUriPermissions: the use of FileProvider requires us to grant temporary access rights (READ and WRITE) to the outgoing URI. This setting allows us to exercise this right;
  • Meta data: configures the path configuration information of the files we can access. xml files are required for configuration. The FileProvider will obtain the configuration items by parsing the xml files, where name is a fixed writing method: android Support File_ Provider_ Paths and resource are configuration items for configuring path information.

Step 2: create a folder named xml under the res directory, and then create a folder named file under the folder_ xml file for paths

 

xml directory png

 

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--path: Path requiring temporary authorized access(.For all paths)-->
    <!--name: Is that you give this access path a name-->
    <external-path name="external-path" path="CameraDemo" />
</paths>

The outermost layer of this xml file is a pair of <paths> tags. Inside the tag is one or more item s that represent paths, The writing method is < corresponding path name= "name" path= "a folder under the corresponding path directory" >

For example:
<external-path name="external-path" path="CameraDemo" />
External path means environment Getexternalstoragedirectory() directory;
Name is the name we gave it as "external path" (of course, you can also use it as an example, it seems useless...);
path means environment CameraDemo folder under getexternalstoragedirectory()

The specific rules for path and writing are as follows:

Writing method Corresponding directory
<files-path name="name" path="path" > Context.getFilesDir()
<cache-path name="name" path="path"> Context.getCacheDir()
<external-path name="name" path="path"> Environment.getExternalStorageDirectory()
<external-files-path name="name" path="path"> Context.getExternalFilesDir(null)
<external-cache-path name="name" path="path"> Context.getExternalCacheDir()

If you need to use FileProvider to obtain the uri of a file in a directory, you can declare it in the xml file according to the corresponding relationship in the above table

Step 1: when registering FileProvider in the manifest file, android:resource= "@xml/file\u paths" corresponds to the XML file we just created

Step 3: use FileProvider in Activity to obtain uri

 

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) //Request temporary read permission
imgUri = FileProvider.getUriForFile(this, AUTHORITY , imageFile)

getUriForFile is a static method of FileProvider. The first parameter is the Context object, the second parameter is the authorities defined in the manifest File, and the third parameter is a File object. The return value is the content uri object of the incoming File.

When we get the returned ContentUri object, we solve the problem that Android7.0 reports FileUriExposedException

4. receive the results returned by the camera

 

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_CAPTURE_RAW -> {   //After successful photographing, compress the picture and display the results
                    imageFile?.let {
                        compressImage(it)       //After the photo is generated, compress the picture and display it     
                    }
                }
            }
        } else {
            Log.d("tag", "Error code $resultCode")
        }
    }

    //Compress picture and display
    fun compressImage(file:File) {
        thread {
            var temp = System.currentTimeMillis()
            var bitmap = BitmapFactory.decodeFile(file.absolutePath)
            var compressBitmap = BitmapUtils.decodeBitmap(bitmap, 720, 1080)
            runOnUiThread {
                ivImage.setImageBitmap(compressBitmap)   //ivImage is an ImageView in the xml layout file
                Log.d("tag", "Original picture size  ${bitmap.width} * ${bitmap.height}")
                Log.d("tag", "Compressed picture size  ${compressBitmap.width} * ${compressBitmap.height}")
                Log.d("tag", "Time consuming to load pictures ${System.currentTimeMillis() - temp}")
            }
        }
    }

After successfully receiving the returned result in onActivityResult, compress the picture and display it in ImageView.
Since image compression is a time-consuming operation, a sub thread is started for compression processing. The original image size, the compressed image size and the compression time are input in the Log. The partners can see the corresponding image information in the logcat

Picture information png

Because the pixels of mobile phones are very high, the photos taken are very large. If they are not compressed and displayed directly on the screen, it will not only consume memory, but also cause OUTOFMEMORY (memory overflow)

OK, this completes the function of calling the system camera to take photos and display. Paste the following rendering:

 

Take photos gif

2, Photographing + cropping

The first part has introduced how to call the system camera to take photos, but sometimes we don't need the whole picture, but only need to take a part of the picture, so we need to use the cropping function

To realize the photographing + cropping function, the steps are as follows:

1. call the system camera to take photos and receive the returned results.

This step has been introduced above, so I won't repeat it. The only difference from the above is that after a successful return result is received in onActivityResult, the image is no longer compressed and displayed, but the clipping function of the system is called

 


    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_CAPTURE -> { //Return result of successful photographing
                    val sourceUri = FileProvider.getUriForFile(this, AUTHORITY, imageFile) //Create a Uri of content type through FileProvider
                    gotoCrop(sourceUri)  //Crop picture
                }
                REQUEST_CODE_CAPTURE_CROP -> { //Results returned after clipping
                    imageCropFile?.let { //display picture
                        ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
                    }
                }
            }
        } else {
            Log.d("tag", "Error code $resultCode")
        }
    }

2. call the clipping function of the system and receive the returned results.

 

    //Cropping (the parameter is the URI of the image to be cropped)
    private fun gotoCrop(sourceUri: Uri) {
        imageCropFile = FileUtil.createImageFile(true) //Create a File to save the cropped photo
        imageCropFile?.let {
            val intent = Intent("com.android.camera.action.CROP")
            intent.putExtra("crop", "true")
            intent.putExtra("aspectX", 1)    //Scale in X direction
            intent.putExtra("aspectY", 1)    //Scale in Y direction
            intent.putExtra("outputX", 500)  //Width of Crop Region
            intent.putExtra("outputY", 500) //Height of Crop Region
            intent.putExtra("scale ", true)  //Keep proportion
            intent.putExtra("return-data", false) //Whether to return pictures in Intent
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) //Format output picture

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) //Adding this sentence means temporarily authorizing the file represented by the Uri for the target application
                intent.setDataAndType(sourceUri, "image/*")  //Set the data source, which must be a ContentUri created by FileProvider

                var imgCropUri = Uri.fromFile(it)
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri) //Setting output does not require ContentUri, otherwise it fails
                Log.d("tag", "input $sourceUri")
                Log.d("tag", "output ${Uri.fromFile(it)}")
            } else {
                intent.setDataAndType(Uri.fromFile(imageFile!!), "image/*")
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(it))
            }
            startActivityForResult(intent, REQUEST_CODE_CAPTURE_CROP)
        }
    }

Note:
Android7.0 or above, intent Putextra (mediastore.extra_output, Uri), Uri must use ContentUri instead of Uri Fromfile (file)
It's different when cutting. You must use ContentUri when setting the image source, and continue to use URI when setting the output location Fromfile (file).

Questions:
I have always had questions about this place. When calling clipping and setting input, shouldn't ContentUri also be used??? However, after practice, it is found that the cropped image cannot be obtained with ContentUri. If there are guys who know the reason, they can leave a message. I want to see the source code, but I can't find the source code of system tailoring-_-|| Guys, you can leave me a message

3. receive cutting results and display them

 

 REQUEST_CODE_CAPTURE_CROP -> {   //After the clipping is successful, the result is displayed
       imageCropFile?.let {
                      ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
          }
    }

This completes the function of calling the system camera to take photos + crop. Paste the following rendering:

 

Photo + crop gif

3, Album selection + cropping

Select a photo from the album, then crop and display it. The process is as follows:

1. open the system album

 

 //Open system album
 private fun gotoGallery() {
     var intent = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
     startActivityForResult(intent, REQUEST_CODE_ALBUM)
 }

2. receive the returned url in onActivityResult and call the clipping method

 

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_ALBUM -> { //Crop after selecting photos from album
                    data?.let {
                        gotoCrop(it.data)
                    }
                }
                REQUEST_CODE_CAPTURE_CROP -> {   //After the clipping is successful, the result is displayed
                    imageCropFile?.let {
                        ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
                    }
                }
            }
        } else {
            Log.d("tag", "Error code $resultCode")
        }
    }

Note:
After opening the system album and selecting a photo, the data field of the returned result in the Intent data is a uri, indicating the location of the selected photo

Specific cutting related codes are available above and will not be shown here

3. receive cutting results and display them

 

 REQUEST_CODE_CAPTURE_CROP -> {   //After the clipping is successful, the result is displayed
       imageCropFile?.let {
                      ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
          }
    }

The renderings are as follows:

 

Photo album + cropping gif

4, Record video

Call the recording function of the system to record a video and play it

1. call the video recording function

 

    //record video 
    private fun gotoCaptureVideo() {
        var intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
        if (intent.resolveActivity(packageManager) != null)
            startActivityForResult(intent, REQUEST_CODE_VIDEO)
    }

2. receive the returned results and play them with VideoView

 

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_VIDEO -> {   //Play after recording video successfully
                    data?.let {
                        var uri = it.data
                        videoView.visibility = View.VISIBLE
                        videoView.setVideoURI(uri)
                        videoView.start()
                        Log.d("tag", "video uri $uri")
                    }
                }
            }
        } else {
            Log.d("tag", "Error code $resultCode")
        }
    }

The result returned after recording the video is a uri in the data field of Intent, pointing to the location where the video is saved

The renderings are as follows:

 

Record video gif

5, Summary

  1. Android7.0 system or above, and direct use is no longer allowed file://URI The solution is that the FileProvider class generates ContentUri
  2. For Android7.0 system or above, ContentUri must be used to set the output position when calling the camera;
    During cropping, ContentUri must be used when setting the image source, and continue to be used when setting the output position file://URI

Complete sample code:

https://github.com/smashinggit/Study

Note: this project contains multiple module s. The code used in this article is in the camerademo folder



Transferred from: https://www.jianshu.com/p/eca7335602c1

Posted by zimick on Mon, 30 May 2022 12:14:47 +0530