Android:Camera X 使用详解(超简单)

前言

CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。对于新应用,我们建议从 CameraX 开始。它提供一致且易于使用的 API,适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。

CameraX 安卓官方文档

1. 添加依赖项

dependencies {

    //CameraX依赖项
    def camerax_version = "1.1.0-beta03"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-video:${camerax_version}"
    implementation "androidx.camera:camera-view:${camerax_version}"
    implementation "androidx.camera:camera-extensions:${camerax_version}"
}

2. CameraActivity

(已删除部分动画效果)

class CameraActivity : AppCompatActivity() {

    private lateinit var camera: Camera
    private var imageCapture: ImageCapture? = null
    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null
    private lateinit var cameraExecutor: ExecutorService
    private var imgPath: String? = null
    private var videoPath: String? = null

    private val viewFinder: PreviewView by lazy { findViewById(R.id.viewFinder) }  //cameraX的预览
    private val layCamera: FrameLayout by lazy { findViewById(R.id.lay_camera) }  //拍照时的布局
    private val layPreview: LinearLayout by lazy { findViewById(R.id.lay_preview) }  //拍照完成的布局
    private val photoPreview: ImageView by lazy { findViewById(R.id.photo_preview) }  //拍照完成显示照片
    private val videoPreview: VideoView by lazy { findViewById(R.id.video_preview) }  //拍照完成显示视频
    private val photoShutter: ImageView by lazy { findViewById(R.id.photo_shutter) }
    private val videoShutter: ImageView by lazy { findViewById(R.id.video_shutter) }
    private val tabLayout: TabLayout by lazy { findViewById(R.id.tabLayout_camera) }
    private val btnCancel: Button by lazy { findViewById(R.id.btn_cancel) }
    private val btnConfirm: Button by lazy { findViewById(R.id.btn_confirm) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)

        viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER  //设置PreviewView缩放类型
        cameraExecutor = Executors.newSingleThreadExecutor()

        if (allPermissionsGranted()) {  //检查全部权限
            initCamera()
        } else {
            requestPermission()
        }

        photoShutter.setOnClickListener {
            takePhoto()
        }

        videoShutter.setOnClickListener {
            recordVideo()
        }

        tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab?) {
                if (tab?.position == 0) {
                    photoShutter.visibility = View.VISIBLE
                    videoShutter.visibility = View.GONE
                } else {
                    photoShutter.visibility = View.GONE
                    videoShutter.visibility = View.VISIBLE
                }
            }
            override fun onTabUnselected(tab: TabLayout.Tab?) {
            }
            override fun onTabReselected(tab: TabLayout.Tab?) {
            }
        })

        btnCancel.setOnClickListener {   //取消按钮
            layPreview.visibility = View.GONE
            layCamera.visibility = View.VISIBLE
        }

        btnConfirm.setOnClickListener {  //确认按钮,跳转PostActivity
            val intent = Intent(this, PostActivity::class.java)
            startActivity(intent)
        }
    }

    private fun initCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)  //创建ProcessCameraProvider实例
        cameraProviderFuture.addListener({  //给cameraProviderFuture添加监听
            val cameraProvider = cameraProviderFuture.get()  //获取相机信息
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA  //默认后置摄像头
            val preview = Preview.Builder().build().also { it.setSurfaceProvider(viewFinder.surfaceProvider) }  //viewFinder设置预览画面
            imageCapture = ImageCapture.Builder().build()
            videoCapture = VideoCapture.withOutput(Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HD)).build())
            cameraProvider.unbindAll()  //先解除再绑定生命周期
            camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, videoCapture)  //Bind use cases to camera
        }, ContextCompat.getMainExecutor(this))
    }

    private fun takePhoto() {
        val imgName = "IMG_" + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(System.currentTimeMillis())  //使用当前时间来命名
        val contentValues = ContentValues().apply {  //contentValues
            put(MediaStore.MediaColumns.DISPLAY_NAME, imgName)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/TikTok") }
        }
        val outputOptions = ImageCapture.OutputFileOptions  //创建OutputFileOptions
            .Builder(contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            .build()
        imageCapture!!.takePicture(outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults){
                imgPath = uriToPath(this@CameraActivity, output.savedUri!!)  //更新照片的path name
                Log.d("wdw", "照片已保存至:${output.savedUri} \n path = $imgPath")
                layPreview.visibility = View.VISIBLE
                photoPreview.visibility = View.VISIBLE
                videoPreview.visibility = View.GONE
                layCamera.visibility = View.GONE
                photoPreview.setImageURI(output.savedUri)
            }
            override fun onError(exc: ImageCaptureException) {
                Log.e("wdw", "imageCapture failed: ${exc.message}", exc)
            }
        })
    }

    @SuppressLint("MissingPermission")
    private fun recordVideo() {
        if (recording != null) {  //清空已存在的recording会话
            recording!!.stop()
            recording = null
            return
        }
        val videoName =  "VID_" + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, videoName)
            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/TikTok") }
        }
        val mediaStoreOutputOptions = MediaStoreOutputOptions
            .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
            .setContentValues(contentValues)
            .build()
        recording = videoCapture!!.output
            .prepareRecording(this, mediaStoreOutputOptions)
            .withAudioEnabled()  //包括录制声音
            .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
                when(recordEvent) {
                    is VideoRecordEvent.Start -> {  //开始录像
                        Toast.makeText(this, "开始录制", Toast.LENGTH_SHORT).show()
                    }
                    is VideoRecordEvent.Finalize -> {  //结束录像
                        if (!recordEvent.hasError()) {  //判断录像是否有错
                            videoPath = uriToPath(this, recordEvent.outputResults.outputUri)  //更新视频的path name
                            Log.d("wdw","视频已保存至:${recordEvent.outputResults.outputUri} \n videoPath = $videoPath")
                            layPreview.visibility = View.VISIBLE
                            photoPreview.visibility = View.GONE
                            videoPreview.visibility = View.VISIBLE
                            layCamera.visibility = View.GONE
                            videoPreview.setVideoURI(recordEvent.outputResults.outputUri)
                            videoPreview.start()  //播放刚刚拍的视频
                            videoPreview.setOnCompletionListener { videoPreview.start() }  //循环播放拍的视频
                        } else {
                            recording?.close()
                            recording = null
                            Log.e("wdw", "videoCapture error: ${recordEvent.error}")
                        }
                    }
                }
            }
    }
    
    //返回键关闭相机
    override fun onBackPressed() {
        super.onBackPressed()
        cameraExecutor.shutdown()
    }

    //检查是否拥有全部权限
    private fun allPermissionsGranted() = PERMISSIONS_REQUIRED.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }

    //申请未获得的权限
    private fun requestPermission() {
        val permissions = mutableListOf<String>()
        for (per in PERMISSIONS_REQUIRED) {
            if (ContextCompat.checkSelfPermission(this, per) != PackageManager.PERMISSION_GRANTED) {
                permissions.add(per)
            }
        }
        ActivityCompat.requestPermissions(this, permissions.toTypedArray(), PERMISSIONS_REQUEST_CODE)
    }

    //返回用户选择的权限结果
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            if (allPermissionsGranted()) {
                initCamera()
            } else {
                Toast.makeText(this, "权限获取失败", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    companion object {
        private const val PERMISSIONS_REQUEST_CODE = 1001
        private val PERMISSIONS_REQUIRED = mutableListOf (
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.READ_EXTERNAL_STORAGE
        ).apply {
            if (Build.VERSION.SDK_INT <= 28) {
                add(Manifest.permission.WRITE_EXTERNAL_STORAGE)  //SdkVersion<=28要加上写权限
            }
        }.toTypedArray()
    }

}

3. activity_camera.XML布局文件

(已删除部分控件)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:orientation="vertical"
    android:background="@color/black"
    tools:context=".post.CameraActivity">

    <FrameLayout
        android:id="@+id/lay_camera"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.camera.view.PreviewView
            android:id="@+id/viewFinder"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="100dp"/>

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabLayout_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center_horizontal"
            android:layout_marginBottom="150dp"
            android:background="#00000000"
            app:tabRippleColor="#00000000"
            app:tabMode="scrollable"
            app:tabTextColor="@color/white"
            app:tabTextAppearance="@style/TabLayoutTextStyle"
            app:tabSelectedTextColor="#ffce16"
            app:tabIndicator="@drawable/news_indicator"
            app:tabIndicatorColor="#ffce16"
            app:tabIndicatorAnimationMode="elastic"
            app:tabIndicatorFullWidth="false">

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="照片" />

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="视频" />

        </com.google.android.material.tabs.TabLayout>

        <ImageView
            android:id="@+id/img_focusing"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_gravity="center"
            android:src="@drawable/ic_focusing"
            android:alpha="0" />

        <ImageView
            android:id="@+id/photo_shutter"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_gravity="center_horizontal|bottom"
            android:layout_marginBottom="20dp"
            android:src="@drawable/ic_camera" />

        <ImageView
            android:id="@+id/video_shutter"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_gravity="center_horizontal|bottom"
            android:layout_marginBottom="20dp"
            android:src="@drawable/ic_record_1"
            android:visibility="gone" />

    </FrameLayout>

    <LinearLayout
        android:id="@+id/lay_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        android:orientation="vertical"
        android:visibility="gone" >

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">

            <ImageView
                android:id="@+id/photo_preview"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <VideoView
                android:id="@+id/video_preview"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </FrameLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginVertical="15dp"
            android:layout_marginHorizontal="10dp">

            <Button
                android:id="@+id/btn_cancel"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_marginEnd="5dp"
                android:text="× 取消"
                android:textSize="18sp"
                android:textColor="@color/black"
                android:background="@drawable/btn_selector_1"/>

            <Button
                android:id="@+id/btn_confirm"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="3"
                android:layout_marginStart="5dp"
                android:text="→ 下一步"
                android:textSize="18sp"
                android:textColor="@color/white"
                android:background="@drawable/btn_selector_2"/>

        </LinearLayout>

    </LinearLayout>

</FrameLayout>

4.


5.


6.


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

leekey_sjtu

我们一起从小白开始加油~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值