【Android CameraX】CameraXBasic —— 官方CameraX实例源码分析

一、简介

本片文章主要针对官方提供的CameraX实现进行简要的解析,从而学会如何编写一个简单的CameraX实例的相机Demo。本篇文章会分析如下几大块:

  1. CameraX实例实现的关键变量
  2. 官方是如何对CameraX实例进行初始化的
  3. 官方是如何使用CameraX实例进行拍照的
  4. 释放实例等其他模块

相关文章和资源推荐:

  1. Android Camera系列文章目录索引汇总
  2. Android CameraX 综述
  3. 官方Demo传送门
  4. CameraX库发版记录

二、源码分析

2.1 build.gradle

  • Kotlin_version:androidx.core:core-ktx:1.6.0
  • camerax_version:1.1.0-alpha07

注意:

  1. 官方给的Demo已经很久没有更新过了,使用的camerax版本相对较老,请访问CameraX库发板记录获取最新代码。
  2. CameraX相关库大概每一个月更新一个版本。需定期及时关注并更新相应库。
  3. 目前本人使用CameraX最新的库即:
    kotlin: 1.6.10
    camerax : 1.2.0-alpha03

相关库的配置请参考官方或者我提供的对应版本。

2.2 代码结构

在这里插入图片描述
代码结构很简单,核心类在CameraFragment,其他可自己根据兴趣查看。

2.3 变量

在这里插入图片描述
CameraFragment里重要参数如下:

变量 说明
lensFacing 相机ID
preview 预览画面
Image capture 用于拍照处理
Image Analyzer 用于照片分析处理
camera 相机实例
cameraProvider 用于提供消息实例

2.3.1 lensFacing

该变量很好理解,为0或者1,分别对应如下


/** A camera on the device facing the same direction as the device's screen. */
public static final int LENS_FACING_FRONT = 0;
/** A camera on the device facing the opposite direction as the device's screen. */
public static final int LENS_FACING_BACK = 1;

于此同时也提供了相应的方法,来判断手机设备是否支持前置或者后置摄像头:

/** Returns true if the device has an available back camera. False otherwise */
private fun hasBackCamera(): Boolean {
    
    
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}

/** Returns true if the device has an available front camera. False otherwise */
private fun hasFrontCamera(): Boolean {
    
    
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}

2.3.2 preview

preview的类型为Preview。继承UseCase,这里只要了解该变量提供了Camera预览画面以及相关的操作方法,具体的阐述如下,具体的源码细节在后续相关文章中再逐步分析。
A use case that provides a camera preview stream for displaying on-screen.

扫描二维码关注公众号,回复: 14852965 查看本文章

2.3.3 Image capture

preview一样,Image capture也继承于UseCase,主要用于处理拍照相关的操作设置。

  1. A use case for taking a picture.
  2. This class is designed for basic picture taking.
  3. It provides takePicture() functions to take a picture to memory or save to a file, and provides image metadata.

2.3.4 Image Analyzer

继承于UseCase,用于做图片分析功能的类,平时基本用不到

A use case providing CPU accessible images for an app to perform image analysis on.

2.3.5 camera

参看源码使用就好,用到的地方不多

The camera interface is used to control the flow of data to use cases, control the camera via the CameraControl, and publish the state of the camera via CameraInfo.

2.3.6 cameraProvider

A singleton which can be used to bind the lifecycle of cameras to any LifecycleOwner within an application’s process.

2.4 初始化流程

有一部分代码,作为变量的初始化,或者更改一些UI界面,线程切换,监听手机方向,按键广播等,这块每个APP有不同的实现方案,就不具体阐述了,感兴趣的可自行分析。核心的相机初始化流程氛围如下几部分:

  1. 获取cameraProvider实例【单例】
  2. 判断并设置lensFacing
  3. 实力化preview和imageCapture, ImageAnalyzer【可选】
  4. 生成camera实例

1,2步骤在方法setUpCamera;3,4部分在bindCameraUseCases

//部分1
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
    
    
    cameraProvider = cameraProviderFuture.get()
    
    //部分2判断并设置lensFacing
    lensFacing = when {
    
    
        hasBackCamera() -> CameraSelector.LENS_FACING_BACK
        hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
        else -> throw IllegalStateException("Back and front camera are unavailable")
    }
    //部分3 实力化preview和imageCapture, ImageAnalyzer【可选】
    //部分4 生成camera实例
    bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
private fun bindCameraUseCases(){
    
    
	//*********部分三:实力化preview和imageCapture, ImageAnalyzer***********
	 // CameraSelector
	val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
	
	// 实例化Preview
	preview = Preview.Builder()
	        // We request aspect ratio but no resolution
	        //设置屏幕宽高比
	        .setTargetAspectRatio(screenAspectRatio)
	        // Set initial target rotation
	        //设置屏幕方向
	        .setTargetRotation(rotation)
	        .build()
	
	// 实例化ImageCapture
	imageCapture = ImageCapture.Builder()
	//设置拍照模式为延迟优先
	     .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
	        // We request aspect ratio but no resolution to match preview config, but letting
	        // CameraX optimize for whatever specific resolution best fits our use cases
	        //设置宽高比
	        .setTargetAspectRatio(screenAspectRatio)
	        // Set initial target rotation, we will have to call this again if rotation changes
	        // during the lifecycle of this use case
	        .setTargetRotation(rotation)
	        .build()
	// ImageAnalysis
	imageAnalyzer = ImageAnalysis.Builder()
	        // We request aspect ratio but no resolution
	        .setTargetAspectRatio(screenAspectRatio)
	        // Set initial target rotation, we will have to call this again if rotation changes
	        // during the lifecycle of this use case
	        .setTargetRotation(rotation)
	        .build()
	        // The analyzer can then be assigned to the instance
	        .also {
    
    
	            it.setAnalyzer(cameraExecutor, LuminosityAnalyzer {
    
     luma ->
	                // Values returned from our analyzer are passed to the attached listener
	                // We log image analysis results here - you should do something useful
	                // instead!
	                Log.d(TAG, "Average luminosity: $luma")
	            })
	        }
	
	//*********部分4:生成camera实例***********
	cameraProvider.unbindAll()
	try {
    
    
	    // A variable number of use-cases can be passed here -
	    // camera provides access to CameraControl & CameraInfo
	    camera = cameraProvider.bindToLifecycle(
	            this, cameraSelector, preview, imageCapture, imageAnalyzer)
	
	    // Attach the viewfinder's surface provider to preview use case
	    preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
	} catch (exc: Exception) {
    
    
	    Log.e(TAG, "Use case binding failed", exc)
	}

}

2.5 拍照

使用Image Capture

cameraUiContainerBinding?.cameraCaptureButton?.setOnClickListener {
    
    

    // Get a stable reference of the modifiable image capture use case
    imageCapture?.let {
    
     imageCapture ->
		******1.获取拍照输出文件******
        // Create output file to hold the image
        val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)

        // Setup image capture metadata
        val metadata = Metadata().apply {
    
    

            // Mirror image when using the front camera
            isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
        }

		******2.配置ImageCapture输出选项******
        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
                .setMetadata(metadata)
                .build()

        // Setup image capture listener which is triggered after photo has been taken
        ******3.调用takePicture方法******
        imageCapture.takePicture(
                outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
            override fun onError(exc: ImageCaptureException) {
    
    
                Log.e(TAG, "Photo capture failed: ${
      
      exc.message}", exc)
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
    
    
                ******4.回调******
                val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                Log.d(TAG, "Photo capture succeeded: $savedUri")
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
                    requireActivity().sendBroadcast(
                            Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
                    )
                }
        })
    }
}

2.6 Release

释放Camera实例方法很简单:

cameraProviderFuture.get().unbindAll();

三、相关练习

  1. git clone官方源码分析其他相关的类
  2. 自己参考官方源码完成一个CameraX实例Demo的实现。

附录 CameraFragment源码:

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.example.cameraxbasic.fragments

import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.ImageFormat
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.*
import android.webkit.MimeTypeMap
import androidx.camera.core.*
import androidx.camera.core.ImageCapture.FLASH_MODE_ON
import androidx.camera.core.ImageCapture.Metadata
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.Navigation
import androidx.window.WindowManager
import com.android.example.cameraxbasic.KEY_EVENT_ACTION
import com.android.example.cameraxbasic.KEY_EVENT_EXTRA
import com.android.example.cameraxbasic.MainActivity
import com.android.example.cameraxbasic.R
import com.android.example.cameraxbasic.databinding.CameraUiContainerBinding
import com.android.example.cameraxbasic.databinding.FragmentCameraBinding
import com.android.example.cameraxbasic.utils.ANIMATION_FAST_MILLIS
import com.android.example.cameraxbasic.utils.ANIMATION_SLOW_MILLIS
import com.android.example.cameraxbasic.utils.simulateClick
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.ArrayDeque
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

/** Helper type alias used for analysis use case callbacks */
typealias LumaListener = (luma: Double) -> Unit

/**
 * Main fragment for this app. Implements all camera operations including:
 * - Viewfinder
 * - Photo taking
 * - Image analysis
 */
class CameraFragment : Fragment() {
    
    

    private var _fragmentCameraBinding: FragmentCameraBinding? = null

    private val fragmentCameraBinding get() = _fragmentCameraBinding!!

    private var cameraUiContainerBinding: CameraUiContainerBinding? = null

    private lateinit var outputDirectory: File
    private lateinit var broadcastManager: LocalBroadcastManager

    private var displayId: Int = -1
    private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
    private var preview: Preview? = null
    private var imageCapture: ImageCapture? = null
    private var imageAnalyzer: ImageAnalysis? = null
    private var camera: Camera? = null
    private var cameraProvider: ProcessCameraProvider? = null
    private lateinit var windowManager: WindowManager

    private val displayManager by lazy {
    
    
        requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
    }

    /** Blocking camera operations are performed using this executor */
    private lateinit var cameraExecutor: ExecutorService

    /** Volume down button receiver used to trigger shutter */
    private val volumeDownReceiver = object : BroadcastReceiver() {
    
    
        override fun onReceive(context: Context, intent: Intent) {
    
    
            when (intent.getIntExtra(KEY_EVENT_EXTRA, KeyEvent.KEYCODE_UNKNOWN)) {
    
    
                // When the volume down button is pressed, simulate a shutter button click
                KeyEvent.KEYCODE_VOLUME_DOWN -> {
    
    
                    cameraUiContainerBinding?.cameraCaptureButton?.simulateClick()
                }
            }
        }
    }

    /**
     * We need a display listener for orientation changes that do not trigger a configuration
     * change, for example if we choose to override config change in manifest or for 180-degree
     * orientation changes.
     */
    private val displayListener = object : DisplayManager.DisplayListener {
    
    
        override fun onDisplayAdded(displayId: Int) = Unit
        override fun onDisplayRemoved(displayId: Int) = Unit
        override fun onDisplayChanged(displayId: Int) = view?.let {
    
     view ->
            if (displayId == this@CameraFragment.displayId) {
    
    
                Log.d(TAG, "Rotation changed: ${
      
      view.display.rotation}")
                imageCapture?.targetRotation = view.display.rotation
                imageAnalyzer?.targetRotation = view.display.rotation
            }
        } ?: Unit
    }

    override fun onResume() {
    
    
        super.onResume()
        // Make sure that all permissions are still present, since the
        // user could have removed them while the app was in paused state.
        if (!PermissionsFragment.hasPermissions(requireContext())) {
    
    
            Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
                    CameraFragmentDirections.actionCameraToPermissions()
            )
        }
    }

    override fun onDestroyView() {
    
    
        _fragmentCameraBinding = null
        super.onDestroyView()

        // Shut down our background executor
        cameraExecutor.shutdown()

        // Unregister the broadcast receivers and listeners
        broadcastManager.unregisterReceiver(volumeDownReceiver)
        displayManager.unregisterDisplayListener(displayListener)
    }

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
    
    
        _fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false)
        return fragmentCameraBinding.root
    }

    private fun setGalleryThumbnail(uri: Uri) {
    
    
        // Run the operations in the view's thread
        cameraUiContainerBinding?.photoViewButton?.let {
    
     photoViewButton ->
            photoViewButton.post {
    
    
                // Remove thumbnail padding
                photoViewButton.setPadding(resources.getDimension(R.dimen.stroke_small).toInt())

                // Load thumbnail into circular button using Glide
                Glide.with(photoViewButton)
                        .load(uri)
                        .apply(RequestOptions.circleCropTransform())
                        .into(photoViewButton)
            }
        }
    }

    @SuppressLint("MissingPermission")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
    
        super.onViewCreated(view, savedInstanceState)

        // Initialize our background executor
        cameraExecutor = Executors.newSingleThreadExecutor()

        broadcastManager = LocalBroadcastManager.getInstance(view.context)

        // Set up the intent filter that will receive events from our main activity
        val filter = IntentFilter().apply {
    
     addAction(KEY_EVENT_ACTION) }
        broadcastManager.registerReceiver(volumeDownReceiver, filter)

        // Every time the orientation of device changes, update rotation for use cases
        displayManager.registerDisplayListener(displayListener, null)

        //Initialize WindowManager to retrieve display metrics
        windowManager = WindowManager(view.context)

        // Determine the output directory
        outputDirectory = MainActivity.getOutputDirectory(requireContext())

        // Wait for the views to be properly laid out
        fragmentCameraBinding.viewFinder.post {
    
    

            // Keep track of the display in which this view is attached
            displayId = fragmentCameraBinding.viewFinder.display.displayId

            // Build UI controls
            updateCameraUi()

            // Set up the camera and its use cases
            setUpCamera()
        }
        fragmentCameraBinding.viewFinder.setOnTouchListener {
    
     v, event ->
            if (event.action != MotionEvent.ACTION_UP) {
    
    
                return@setOnTouchListener true
            }

            val factory = fragmentCameraBinding.viewFinder.meteringPointFactory;
            val focusPoint = factory.createPoint(event.x, event.y,0.3f)
            Log.i("sun_q","point = "+focusPoint.toString())
            val focusAction = FocusMeteringAction.Builder(focusPoint).build()
            camera?.cameraControl?.startFocusAndMetering(focusAction)

            return@setOnTouchListener true
        }
    }

    /**
     * Inflate camera controls and update the UI manually upon config changes to avoid removing
     * and re-adding the view finder from the view hierarchy; this provides a seamless rotation
     * transition on devices that support it.
     *
     * NOTE: The flag is supported starting in Android 8 but there still is a small flash on the
     * screen for devices that run Android 9 or below.
     */
    override fun onConfigurationChanged(newConfig: Configuration) {
    
    
        super.onConfigurationChanged(newConfig)

        // Rebind the camera with the updated display metrics
        bindCameraUseCases()

        // Enable or disable switching between cameras
        updateCameraSwitchButton()
    }

    /** Initialize CameraX, and prepare to bind the camera use cases  */
    private fun setUpCamera() {
    
    
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        cameraProviderFuture.addListener(Runnable {
    
    

            // CameraProvider
            cameraProvider = cameraProviderFuture.get()

            // Select lensFacing depending on the available cameras
            lensFacing = when {
    
    
                hasBackCamera() -> CameraSelector.LENS_FACING_BACK
                hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
                else -> throw IllegalStateException("Back and front camera are unavailable")
            }

            // Enable or disable switching between cameras
            updateCameraSwitchButton()

            // Build and bind the camera use cases
            bindCameraUseCases()
        }, ContextCompat.getMainExecutor(requireContext()))
    }

    /** Declare and bind preview, capture and analysis use cases */
    private fun bindCameraUseCases() {
    
    

        // Get screen metrics used to setup camera for full screen resolution
        val metrics = windowManager.getCurrentWindowMetrics().bounds
        Log.d(TAG, "Screen metrics: ${
      
      metrics.width()} x ${
      
      metrics.height()}")

        val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
        Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")

        val rotation = fragmentCameraBinding.viewFinder.display.rotation

        // CameraProvider
        val cameraProvider = cameraProvider
                ?: throw IllegalStateException("Camera initialization failed.")

        // CameraSelector
        val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()

        // Preview
        preview = Preview.Builder()
                // We request aspect ratio but no resolution
                .setTargetAspectRatio(screenAspectRatio)
                // Set initial target rotation
                .setTargetRotation(rotation)
                .build()

        // ImageCapture
        imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                // We request aspect ratio but no resolution to match preview config, but letting
                // CameraX optimize for whatever specific resolution best fits our use cases
                .setTargetAspectRatio(screenAspectRatio)
                // Set initial target rotation, we will have to call this again if rotation changes
                // during the lifecycle of this use case
                .setTargetRotation(rotation)
                .build()
        // ImageAnalysis
        imageAnalyzer = ImageAnalysis.Builder()
                // We request aspect ratio but no resolution
                .setTargetAspectRatio(screenAspectRatio)
                // Set initial target rotation, we will have to call this again if rotation changes
                // during the lifecycle of this use case
                .setTargetRotation(rotation)
                .build()
                // The analyzer can then be assigned to the instance
                .also {
    
    
                    it.setAnalyzer(cameraExecutor, LuminosityAnalyzer {
    
     luma ->
                        // Values returned from our analyzer are passed to the attached listener
                        // We log image analysis results here - you should do something useful
                        // instead!
                        Log.d(TAG, "Average luminosity: $luma")
                    })
                }

        // Must unbind the use-cases before rebinding them
        cameraProvider.unbindAll()

        try {
    
    
            // A variable number of use-cases can be passed here -
            // camera provides access to CameraControl & CameraInfo
            camera = cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture, imageAnalyzer)

            // Attach the viewfinder's surface provider to preview use case
            preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
        } catch (exc: Exception) {
    
    
            Log.e(TAG, "Use case binding failed", exc)
        }
    }

    /**
     *  [androidx.camera.core.ImageAnalysis.Builder] requires enum value of
     *  [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
     *
     *  Detecting the most suitable ratio for dimensions provided in @params by counting absolute
     *  of preview ratio to one of the provided values.
     *
     *  @param width - preview width
     *  @param height - preview height
     *  @return suitable aspect ratio
     */
    private fun aspectRatio(width: Int, height: Int): Int {
    
    
        val previewRatio = max(width, height).toDouble() / min(width, height)
        if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
    
    
            return AspectRatio.RATIO_4_3
        }
        return AspectRatio.RATIO_16_9
    }

    /** Method used to re-draw the camera UI controls, called every time configuration changes. */
    private fun updateCameraUi() {
    
    

        // Remove previous UI if any
        cameraUiContainerBinding?.root?.let {
    
    
            fragmentCameraBinding.root.removeView(it)
        }

        cameraUiContainerBinding = CameraUiContainerBinding.inflate(
            LayoutInflater.from(requireContext()),
            fragmentCameraBinding.root,
            true
        )

        // In the background, load latest photo taken (if any) for gallery thumbnail
        lifecycleScope.launch(Dispatchers.IO) {
    
    
            outputDirectory.listFiles {
    
     file ->
                EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
            }?.maxOrNull()?.let {
    
    
                setGalleryThumbnail(Uri.fromFile(it))
            }
        }

        // Listener for button used to capture photo
        cameraUiContainerBinding?.cameraCaptureButton?.setOnClickListener {
    
    

            // Get a stable reference of the modifiable image capture use case
            imageCapture?.let {
    
     imageCapture ->

                // Create output file to hold the image
                val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)

                // Setup image capture metadata
                val metadata = Metadata().apply {
    
    

                    // Mirror image when using the front camera
                    isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
                }

                // Create output options object which contains file + metadata
                val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
                        .setMetadata(metadata)
                        .build()

                // Setup image capture listener which is triggered after photo has been taken
                imageCapture.takePicture(
                        outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
                    override fun onError(exc: ImageCaptureException) {
    
    
                        Log.e(TAG, "Photo capture failed: ${
      
      exc.message}", exc)
                    }

                    override fun onImageSaved(output: ImageCapture.OutputFileResults) {
    
    
                        val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                        Log.d(TAG, "Photo capture succeeded: $savedUri")

                        // We can only change the foreground Drawable using API level 23+ API
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
                            // Update the gallery thumbnail with latest picture taken
                            setGalleryThumbnail(savedUri)
                        }

                        // Implicit broadcasts will be ignored for devices running API level >= 24
                        // so if you only target API level 24+ you can remove this statement
                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
                            requireActivity().sendBroadcast(
                                    Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
                            )
                        }

                        // If the folder selected is an external media directory, this is
                        // unnecessary but otherwise other apps will not be able to access our
                        // images unless we scan them using [MediaScannerConnection]
                        val mimeType = MimeTypeMap.getSingleton()
                                .getMimeTypeFromExtension(savedUri.toFile().extension)
                        MediaScannerConnection.scanFile(
                                context,
                                arrayOf(savedUri.toFile().absolutePath),
                                arrayOf(mimeType)
                        ) {
    
     _, uri ->
                            Log.d(TAG, "Image capture scanned into media store: $uri")
                        }
                    }
                })

                // We can only change the foreground Drawable using API level 23+ API
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    

                    // Display flash animation to indicate that photo was captured
                    fragmentCameraBinding.root.postDelayed({
    
    
                        fragmentCameraBinding.root.foreground = ColorDrawable(Color.WHITE)
                        fragmentCameraBinding.root.postDelayed(
                                {
    
     fragmentCameraBinding.root.foreground = null }, ANIMATION_FAST_MILLIS)
                    }, ANIMATION_SLOW_MILLIS)
                }
            }
        }

        // Setup for button used to switch cameras
        cameraUiContainerBinding?.cameraSwitchButton?.let {
    
    

            // Disable the button until the camera is set up
            it.isEnabled = false

            // Listener for button used to switch cameras. Only called if the button is enabled
            it.setOnClickListener {
    
    
                lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
    
    
                    CameraSelector.LENS_FACING_BACK
                } else {
    
    
                    CameraSelector.LENS_FACING_FRONT
                }
                // Re-bind use cases to update selected camera
                bindCameraUseCases()
            }
        }

        // Listener for button used to view the most recent photo
        cameraUiContainerBinding?.photoViewButton?.setOnClickListener {
    
    
            // Only navigate when the gallery has photos
            if (true == outputDirectory.listFiles()?.isNotEmpty()) {
    
    
                Navigation.findNavController(
                        requireActivity(), R.id.fragment_container
                ).navigate(CameraFragmentDirections
                        .actionCameraToGallery(outputDirectory.absolutePath))
            }
        }
    }

    /** Enabled or disabled a button to switch cameras depending on the available cameras */
    private fun updateCameraSwitchButton() {
    
    
        try {
    
    
            cameraUiContainerBinding?.cameraSwitchButton?.isEnabled = hasBackCamera() && hasFrontCamera()
        } catch (exception: CameraInfoUnavailableException) {
    
    
            cameraUiContainerBinding?.cameraSwitchButton?.isEnabled = false
        }
    }

    /** Returns true if the device has an available back camera. False otherwise */
    private fun hasBackCamera(): Boolean {
    
    
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
    }

    /** Returns true if the device has an available front camera. False otherwise */
    private fun hasFrontCamera(): Boolean {
    
    
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
    }

    /**
     * Our custom image analysis class.
     *
     * <p>All we need to do is override the function `analyze` with our desired operations. Here,
     * we compute the average luminosity of the image by looking at the Y plane of the YUV frame.
     */
    private class LuminosityAnalyzer(listener: LumaListener? = null) : ImageAnalysis.Analyzer {
    
    
        private val frameRateWindow = 8
        private val frameTimestamps = ArrayDeque<Long>(5)
        private val listeners = ArrayList<LumaListener>().apply {
    
     listener?.let {
    
     add(it) } }
        private var lastAnalyzedTimestamp = 0L
        var framesPerSecond: Double = -1.0
            private set

        /**
         * Used to add listeners that will be called with each luma computed
         */
        fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)

        /**
         * Helper extension function used to extract a byte array from an image plane buffer
         */
        private fun ByteBuffer.toByteArray(): ByteArray {
    
    
            rewind()    // Rewind the buffer to zero
            val data = ByteArray(remaining())
            get(data)   // Copy the buffer into a byte array
            return data // Return the byte array
        }

        /**
         * Analyzes an image to produce a result.
         *
         * <p>The caller is responsible for ensuring this analysis method can be executed quickly
         * enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
         * images will not be acquired and analyzed.
         *
         * <p>The image passed to this method becomes invalid after this method returns. The caller
         * should not store external references to this image, as these references will become
         * invalid.
         *
         * @param image image being analyzed VERY IMPORTANT: Analyzer method implementation must
         * call image.close() on received images when finished using them. Otherwise, new images
         * may not be received or the camera may stall, depending on back pressure setting.
         *
         */
        override fun analyze(image: ImageProxy) {
    
    
            // If there are no listeners attached, we don't need to perform analysis
            if (listeners.isEmpty()) {
    
    
                image.close()
                return
            }

            // Keep track of frames analyzed
            val currentTime = System.currentTimeMillis()
            frameTimestamps.push(currentTime)

            // Compute the FPS using a moving average
            while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
            val timestampFirst = frameTimestamps.peekFirst() ?: currentTime
            val timestampLast = frameTimestamps.peekLast() ?: currentTime
            framesPerSecond = 1.0 / ((timestampFirst - timestampLast) /
                    frameTimestamps.size.coerceAtLeast(1).toDouble()) * 1000.0

            // Analysis could take an arbitrarily long amount of time
            // Since we are running in a different thread, it won't stall other use cases

            lastAnalyzedTimestamp = frameTimestamps.first

            // Since format in ImageAnalysis is YUV, image.planes[0] contains the luminance plane
            val buffer = image.planes[0].buffer

            // Extract image data from callback object
            val data = buffer.toByteArray()

            // Convert the data into an array of pixel values ranging 0-255
            val pixels = data.map {
    
     it.toInt() and 0xFF }

            // Compute average luminance for the image
            val luma = pixels.average()

            // Call all listeners with new value
            listeners.forEach {
    
     it(luma) }

            image.close()
        }
    }

    companion object {
    
    

        private const val TAG = "CameraXBasic"
        private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val PHOTO_EXTENSION = ".jpg"
        private const val RATIO_4_3_VALUE = 4.0 / 3.0
        private const val RATIO_16_9_VALUE = 16.0 / 9.0

        /** Helper function used to create a timestamped file */
        private fun createFile(baseFolder: File, format: String, extension: String) =
                File(baseFolder, SimpleDateFormat(format, Locale.US)
                        .format(System.currentTimeMillis()) + extension)
    }
}

猜你喜欢

转载自blog.csdn.net/Scott_S/article/details/126165562