Android CameraX自定义照相机实践

前言

最近公司有一个Android 盒子项目,需要用到自定义照相机功能,就去重新看一下Android 端的照相机功能。

几年前使用的是Camera+SurfaceView自定义相机拍照,目前看来,有点老了,现在要看Jetpack里面的Camera X 。所以想挑战一下,使用CameraX来改造一下。

总结了一下自定义照相机有

1、Camera + SurfaceView(不推荐了)

2、Camera2 + TextureView

3、CameraX +  PreviewView

基本都符合这几个步骤

  • 权限配置
  • 布局配置
  • 预览设置
  • 拍照设置

目前只有拍照部分,关于相机预览变形和旋转的问题,因为是盒子,减少了这部分思考了,不像手机还得思考旋转等问题,盒子是固定上去,特大的号的平板。

基础实现

先进行权限配置,配置权限如下

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
<uses-feature android:name="android.hardware.camera.autofocus" /> 
<!-- 相机自动对焦配置 -->
<uses-feature android:name="android.hardware.camera"
        android:required="true" />

代码里面要进行申请权限

// 代码

if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, 101);
      return;
}

布局部分,在xml 配置出来不同的ui界面,根据UI设计

我用我现在开源的模版【Ruoyi-Android-App】基础框架进行的。Ruoyi-Android-App: 🎉 RuoYi APP 移动端框架,基于kotlin封装的一套基础模版, 实现了与RuoYi-Go、RuoYi-Vue、RuoYi-Cloud后台完美对接。

进行的主要用dialog 部分。提交内容

提交部分

xml 部分

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/common_color_translucent_gray_bg"
        android:orientation="vertical">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginLeft="80dp"
            android:layout_marginTop="40dp"
            android:layout_marginRight="80dp"
            android:layout_weight="1"
            android:background="#FF95A9C3">

            <androidx.camera.view.PreviewView
                android:id="@+id/mainPreView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#FF95A9C3"
                android:visibility="gone" />

            <TextView
                android:id="@+id/tv_msg"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="相机启动中..."
                android:textColor="@color/white"
                android:textSize="18sp" />

            <ImageView
                android:id="@+id/iv_people"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY" />

        </FrameLayout>


        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="22dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_back"
                android:layout_width="wrap_content"
                android:layout_height="29dp"
                android:layout_centerVertical="true"
                android:layout_marginStart="20dp"
                android:drawablePadding="@dimen/dp_4"
                android:gravity="center"
                android:text="后退"
                android:textColor="#FFF4F8FA"
                android:textSize="16sp"
                app:drawableLeftCompat="@mipmap/back_write" />

            <LinearLayout
                android:id="@+id/btn_take_picture"
                android:layout_width="160dp"
                android:layout_height="48dp"
                android:layout_centerInParent="true"
                android:layout_gravity="center"
                android:layout_marginEnd="8dp"
                android:background="@drawable/drawable_bt_press"
                android:gravity="center"
                android:orientation="horizontal"
                android:textSize="18sp">

                <ImageView
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_marginRight="10dp"
                    android:src="@mipmap/camera" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_marginEnd="8dp"
                    android:gravity="center"
                    android:text="拍照"
                    android:textColor="@color/white"
                    android:textStyle="bold"
                    android:textSize="18sp" />

            </LinearLayout>

            <LinearLayout
                android:id="@+id/ll_sure_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:orientation="horizontal"
                android:visibility="gone">

                <LinearLayout
                    android:id="@+id/btn_cancle"
                    android:layout_width="166dp"
                    android:layout_height="50dp"
                    android:layout_gravity="center"
                    android:layout_marginEnd="8dp"
                    android:background="@drawable/drawable_bt_ash_press"
                    android:gravity="center"
                    android:orientation="horizontal"
                    android:textSize="18sp">

                    <ImageView
                        android:layout_width="20dp"
                        android:layout_height="20dp"
                        android:layout_marginRight="8dp"
                        android:src="@mipmap/icon_refresh" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="重拍"
                        android:textColor="#FF8A99AC"
                        android:textSize="18sp" />

                </LinearLayout>

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/btn_sure_pic"
                    android:layout_width="166dp"
                    android:layout_height="50dp"
                    android:layout_marginStart="8dp"
                    android:background="@drawable/drawable_bt_press"
                    android:gravity="center"
                    android:text="确认"
                    android:textColor="@color/white"
                    android:textSize="18sp"
                    android:textStyle="bold"  />

            </LinearLayout>

        </RelativeLayout>

    </LinearLayout>

xml 里面的逻辑是拍照,显示在imageview 中,进行确定和重新拍照功能,确定后进行返回去。

CameraDialog.kt 里面开始进行xml 加载和button 点击实践处理

import android.graphics.Bitmap
import android.view.Surface
import android.view.View
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.lifecycle.LifecycleOwner
import com.drake.net.utils.runMain
import com.drake.net.utils.scope
import com.tjzxsw.code.dialog.base.BaseBindingDialog
import com.tjzxsw.code.utils.SoundUtils
import com.tjzxsw.devices.api.Contents
import com.tjzxsw.devices.saver.OnTakeCameraCallback
import com.tjzxsw.devices.ui.work.SignatureActiveActivity
import kotlinx.coroutines.delay


class CameraDialog(
    private val activity: XXXActivity,
    private val lifecycleOwner: LifecycleOwner = activity,
    private val onPhotoCallback: OnTakeCameraCallback? = null
) : BaseBindingDialog<DialogCameraBinding>(activity, themeResId = R.style.Dialog_Fullscreen) {// 全屏的主题

    private val cameraProviderFuture by lazy {
        ProcessCameraProvider.getInstance(activity)
    }

// 预览处理
    private val preview = Preview.Builder()
        .setTargetAspectRatio(AspectRatio.RATIO_16_9)
        .setTargetRotation(Surface.ROTATION_270)
        .build()

// 照相机处理 后摄像头
    private val cameraSelector =
        CameraSelector
            .Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()

    private var bitmap: Bitmap? = null

    override fun initView() {
        // 点击空白区域不关闭 Dialog(默认为 true)
        setCanceledOnTouchOutside(false)

        binding.tvBack.setOnClickListener {
            cancelView()
            dismiss()
            onPhotoCallback?.onCancel()
        }

        binding.btnCancle.setOnClickListener {
            onPhotoCallback?.onUploadTime()
            cancelView()
        }

        binding.btnSurePic.setOnClickListener {
            bitmap?.let { it1 -> onPhotoCallback?.onCompleted(it1) }
        }

        binding.btnTakePicture.setOnClickListener {
            binding.llSureView.visibility = View.VISIBLE
            //获取bitmap
            bitmap = binding.mainPreView.bitmap

            binding.ivPeople.setImageBitmap(bitmap)
            binding.mainPreView.visibility = View.GONE
            binding.btnTakePicture.visibility = View.GONE
            binding.ivPeople.visibility = View.VISIBLE
        }
        binding.btnCancle.setOnClickListener {
            cancelView()
        }
    }

    private fun bindPreview(previewView: PreviewView) {
        val cameraProvider = cameraProviderFuture.get();
        preview.setSurfaceProvider(previewView.surfaceProvider)
        cameraProvider.unbindAll()
        val camera = cameraProvider.bindToLifecycle(
            lifecycleOwner,
            cameraSelector,
            preview
        )
        camera.cameraInfo.let { observeCameraState(it) }
    }

    //这样写了,仿照启动有黑色的区域块,用加载中...展示
    private fun observeCameraState(cameraInfo: CameraInfo) {
        cameraInfo.cameraState.observe(lifecycleOwner) { cameraState ->
            when (cameraState.type) {
                CameraState.Type.PENDING_OPEN -> {
                }
                CameraState.Type.OPENING -> {
                    binding.mainPreView.visibility = View.GONE
                    binding.tvMsg.visibility = View.VISIBLE
                }
                CameraState.Type.OPEN -> {
                    scope {
                        delay(500) // 延时
                        binding.mainPreView.visibility = View.VISIBLE
                        binding.tvMsg.visibility = View.GONE
                    }
                }
                CameraState.Type.CLOSING -> {

                }
                CameraState.Type.CLOSED -> {

                }
            }

            cameraState.error?.let { error ->
                runMain {
//                    toast("照相机启动失败")
                    dismiss()
                }
            }
        }
    }


    private fun cancelView() {
        bitmap = null
        binding.ivPeople.setImageBitmap(null)
        binding.ivPeople.visibility = View.GONE
        binding.llSureView.visibility = View.GONE
        binding.tvMsg.visibility = View.GONE
        binding.btnTakePicture.visibility = View.VISIBLE
        binding.mainPreView.visibility = View.VISIBLE
       
    }

    override fun show() {
        super.show()

        val mainPreView = binding.mainPreView
        mainPreView.visibility = View.GONE

        bindPreview(mainPreView)
       
    }

    override fun dismiss() {
        super.dismiss()

    }

   
}

启动camearx 启动的时候有一个黑色模块,启动不是很快,用加载中... 展示。

您可以将 CameraX 设置为忽略其他摄像头,从而缩短应用所用摄像头的启动延迟时间。

可以实现

如果传递给 CameraXConfig.Builder.setAvailableCamerasLimiter() 的 CameraSelector 过滤掉了某个摄像头,则 CameraX 在运行时会假定该摄像头不存在。例如,以下代码会限制应用只能使用设备的默认后置摄像头:

class MainApplication : Application(), CameraXConfig.Provider {
   override fun getCameraXConfig(): CameraXConfig {
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
              .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
              .build()
   }
}

我没有测试,这样写是不是会快很多。 

预览设置 部分看 bindPreview 方法里面,进行ui绑定。preview中预览。

拍摄部分 是预览中获取bitmap处理。处理就很快。

实战部分

部分 MVVM,Jetpack,Data Binding,Ruoyi-Android-App: 🎉 RuoYi APP 移动端框架,基于kotlin封装的一套基础模版, 实现了与RuoYi-Go、RuoYi-Vue、RuoYi-Cloud后台完美对接。

基本已经实现,启动部分

点击出现dialog 展示

val dialog = CameraDialog(this,object: OnTakeCameraCallback. onPhotoCallback{
fun onResult(bitmap:Bitmap){
}
})
dialog.show()

 onPhotoCallback就是一个接口,根据自己需要进行,我们上传得是oss 里面,直接转流就行了。有些接口需要保存本地数据,然后在上传到服务器上;

用dialog 展示减少activity 或fragment 的逻辑处理。

应用的每个模块的 build.gradle 文件中:

dependencies {
  // CameraX core library using the camera2 implementation
  def camerax_version = "1.3.0-alpha04"
  // The following line is optional, as the core library is included indirectly by camera-camera2
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  // If you want to additionally use the CameraX Lifecycle library
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  // If you want to additionally use the CameraX VideoCapture library
  implementation "androidx.camera:camera-video:${camerax_version}"
  // If you want to additionally use the CameraX View class
  implementation "androidx.camera:camera-view:${camerax_version}"
  // If you want to additionally add CameraX ML Kit Vision Integration
  implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
  // If you want to additionally use the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

关于android camerax 的部分,

CameraX 支持搭载 Android 5.0(API 级别 21)或更高版本的设备,覆盖现有 Android 设备的 98% 以上。

CameraX 基于 Camera2 构建而成,并且 CameraX 提供了在 Camera2 实现中读取甚至写入属性的方式。

Camera2 的执行方式

拍照
CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
builder.addTarget(mPreviewSurface);
builder.addTarget(mImageReader.getSurface());
CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {};
mCameraCaptureSession.capture(builder.build(), captureCallback, mBackgroundHandler);
// 展示图片
ImageReader.OnImageAvailableListener listener = new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader imageReader) {
        Image image = imageReader.acquireNextImage();   //  取出一个图像
        saveImage(image, picFile);    //  将图像保存到文件
    }
};
mImageReader.setOnImageAvailableListener(listener, mBackgroundHandler); //  设置监听器,拍照完成后会执行上面的方法

这样就出现了时间差,拍照之后通过setOnImageAvailableListener 回调来获取图片,这样保存图片在处理图片就不是那么即使了。camera2 打开速度快,保存图片有点时间差。

写camear2代码是为了比对他们俩启动和保存图片不同。俩个里面的方法并不能同时使用。

视频拍摄:通过 VideoCapture 拍摄视频和音频

还有细节东西:例如camearx中生命周期,还有旋转屏幕方向已锁定或屏幕方向配置更改会被覆盖

private val orientationEventListener by lazy {
        object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) {
                    return
                }

                val rotation = when (orientation) {
                     in 45 until 135 -> Surface.ROTATION_270
                     in 135 until 225 -> Surface.ROTATION_180
                     in 225 until 315 -> Surface.ROTATION_90
                     else -> Surface.ROTATION_0
                 }

                 imageAnalysis.targetRotation = rotation
                 imageCapture.targetRotation = rotation
            }
        }
    }

CameraX Extensions API 是在 camera-extensions 库中实现的。这些扩展依赖于 CameraX 核心模块(corecamera2lifecycle)。

CameraX 应用可以通过 CameraX Extensions API 使用扩展。CameraX Extensions API 可用于管理可用扩展的查询、配置扩展相机会话以及与相机扩展 OEM 库的通信。这样,您的应用就可以使用夜间、HDR、自动、焦外成像或脸部照片修复等功能。

最后是build中配置

dependencies {
  def camerax_version = "1.2.0-rc01"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  //the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
    ...
}

感谢

zGitHub - android/camera-samples: Multiple samples showing the best practices in camera APIs on Android.

【精选】【Android实战】2、用 CameraX 实现:preview 预览、imageCapture 拍照、videoCapture 录像、videoAnalysis 分析各帧_camerax videocapture-CSDN博客

后续

USB连接设备,有些Android设备,可以连接USB摄像头,进行监控和录像等功能。

需要首先的是设备授权,在进行其他的操作一样了。

收集两个项目
https://github.com/mik3y/usb-serial-for-android

https://blog.csdn.net/hanshiying007/article/details/124118486

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
安卓 Camerax 是一个用于相机应用开发的官方库,它提供了简化相机功能的 API。要使用 Camerax 自定义相机并适配高清,我们可以按照以下步骤进行: 1. 导入依赖:在项目的 `build.gradle` 文件中添加 Camerax 依赖项。 2. 检查权限:在 AndroidManifest.xml 文件中添加相机权限。 3. 配置相机用例:使用 Camerax 的 CameraSelector 创建相机用例,并设置预览分辨率,默认情况下,它会自动选择最佳分辨率。 4. 创建预览界面:在布局文件中添加一个预览视图,用于显示相机预览画面。 5. 实现相机功能:使用 Camerax 的 ImageCapture 和 ImageAnalysis 实现拍照和图像分析等功能。 6. 配置相机参数:通过设置不同的参数来适配高清。 7. 监听图片捕获事件:通过添加图像捕获监听器,可以在相机捕获照片后获取图像数据。 8. 处理图像数据:根据需求对捕获的图像数据进行处理,例如保存到本地或进行进一步的图像处理。 9. 销毁相机用例:在相机不再使用时,调用 CameraX.unbindAll() 方法来停止相机和释放资源。 总结而言,使用 Camerax 自定义相机并适配高清的关键步骤包括导入依赖、配置相机用例、创建预览界面、实现相机功能、配置相机参数、监听图片捕获事件、处理图像数据和销毁相机用例。通过这些步骤,我们可以实现一个功能完整的自定义相机,并且可以适配高清图像的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值