前言
最近公司有一个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 核心模块(core
、camera2
、lifecycle
)。
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}"
...
}
感谢
后续
USB连接设备,有些Android设备,可以连接USB摄像头,进行监控和录像等功能。
需要首先的是设备授权,在进行其他的操作一样了。
收集两个项目
https://github.com/mik3y/usb-serial-for-android
https://blog.csdn.net/hanshiying007/article/details/124118486