CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。对于新应用,我们建议从 CameraX 开始。它提供一致且易于使用的 API,适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。
CameraX 安卓官方文档
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}"
}
(已删除部分动画效果)
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()
}
}
(已删除部分控件)
<?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>