Android CameraX预览无法显示为圆形对话框问题求助
实现DialogFragment中圆形CameraX预览对话框
问题描述
为DialogFragment设计了带圆形布局的界面,希望弹出对话框时仅显示圆形区域,让CameraX的PreviewView在其中展示。尝试过自定义视图、CardView等方案,但相机预览始终呈矩形,最多只能看到CardView的圆形阴影,需要实现仅显示圆形对话框且相机预览完全适配圆形区域的效果。
当前代码
布局代码
<?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.cardview.widget.CardView android:id="@+id/cameraWrapper" android:layout_width="300dp" android:layout_height="300dp" android:foreground="@drawable/background_camera_overlay_circle" android:padding="-20dp" app:cardCornerRadius="360dp" app:cardElevation="0dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.camera.view.PreviewView android:id="@+id/preview" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </androidx.cardview.widget.CardView> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
DialogFragment代码
class CameraDialog : DialogFragment(R.layout.dialog_camera) { companion object { private const val TAG = "CameraXApp" } private lateinit var binding: DialogCameraBinding private var videoCapture: VideoCapture<Recorder>? = null private var recording: Recording? = null private lateinit var cameraExecutor: ExecutorService override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DialogCameraBinding.inflate(inflater, container, false) dialog?.window?.setBackgroundDrawable(ColorDrawable(0x00ffffff)) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) startCamera() cameraExecutor = Executors.newSingleThreadExecutor() /** * this handler makes this recording camera with delay after that saving */ view.postDelayed({ captureVideo() }, 1000) } private fun captureVideo() { val videoCapture = this.videoCapture ?: return val curRecording = recording if (curRecording != null) { curRecording.stop() recording = null return } recording = videoCapture.output .prepareRecording( requireContext(), requireActivity().contentResolver.createVideoStoreOutputOptions( ContentValues().createVideoContentValues() ) ) .apply { // Enable Audio for recording if ( PermissionChecker.checkSelfPermission( requireContext(), Manifest.permission.RECORD_AUDIO ) == PermissionChecker.PERMISSION_GRANTED ) { withAudioEnabled() } } .start(ContextCompat.getMainExecutor(requireContext())) { recordEvent -> when (recordEvent) { is VideoRecordEvent.Finalize -> { if (!recordEvent.hasError()) Toast.makeText(requireContext(), "captureVideo", Toast.LENGTH_SHORT).show() else { recording?.close() recording = null Timber.tag(TAG).e("captureVideo: got error") } dialog?.dismiss() } } } } private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // Preview val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(binding.preview.surfaceProvider) } // Video val recorder = Recorder.Builder() .setQualitySelector( QualitySelector.from( Quality.HIGHEST, FallbackStrategy.higherQualityOrLowerThan(Quality.SD) ) ) .build() videoCapture = VideoCapture.withOutput(recorder) // Select back camera as a default val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind use cases before rebinding cameraProvider.unbindAll() // Bind use cases to camera cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture) } catch (exc: Exception) { Timber.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(requireContext())) } override fun onDestroy() { super.onDestroy() cameraExecutor.shutdown() } }
当前效果与预期效果
- 当前效果:相机预览为矩形,仅外层CardView显示圆形阴影
- 预期效果:仅显示圆形区域的相机预览,无矩形边缘暴露
解决方案
问题核心在于PreviewView的Surface不受CardView圆角属性的裁剪影响,必须通过视图轮廓裁剪或自定义裁剪视图实现真正的圆形遮罩。
方案一:使用ViewOutlineProvider(性能最优)
无需额外自定义视图,利用系统API实现裁剪:
- 修改布局文件,简化容器层级,设置PreviewView充满容器:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/cameraWrapper" android:layout_width="300dp" android:layout_height="300dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.camera.view.PreviewView android:id="@+id/preview" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
- 在
onViewCreated中添加圆形裁剪逻辑:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 给容器设置圆形轮廓裁剪 binding.cameraWrapper.apply { clipToOutline = true outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val radius = view.width.coerceAtMost(view.height) / 2f outline.setRoundRect(0, 0, view.width, view.height, radius) } } } startCamera() cameraExecutor = Executors.newSingleThreadExecutor() view.postDelayed({ captureVideo() }, 1000) }
方案二:自定义圆形裁剪视图
如果需要更灵活的裁剪逻辑,可自定义View实现:
- 创建
CircleClipView.kt:
class CircleClipView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private val path = Path() override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) path.reset() val radius = w.coerceAtMost(h) / 2f path.addCircle(w/2f, h/2f, radius, Path.Direction.CW) } override fun dispatchDraw(canvas: Canvas) { val saveCount = canvas.save() canvas.clipPath(path) super.dispatchDraw(canvas) canvas.restoreToCount(saveCount) } }
- 修改布局使用自定义视图:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.yourpackage.CircleClipView android:id="@+id/cameraWrapper" android:layout_width="300dp" android:layout_height="300dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.camera.view.PreviewView android:id="@+id/preview" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.yourpackage.CircleClipView> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
额外优化
- 确保Dialog窗口完全透明,添加背景暗化效果:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) dialog.window?.apply { setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setDimAmount(0.6f) // 可选:添加背景暗化,突出圆形预览 } return dialog }
内容的提问来源于stack exchange,提问作者sina
相关产品推荐
相关产品推荐

