You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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实现裁剪:

  1. 修改布局文件,简化容器层级,设置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>
  1. 在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实现:

  1. 创建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)
    }
}
  1. 修改布局使用自定义视图:
<?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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 15:45:55