Android NDK开发详解相机之相机预览

注意:本页介绍的是 Camera2 软件包。除非您的应用需要 Camera2 的特定低层级功能,否则我们建议您使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。

在 Android 设备上,相机和相机预览并非总是具有相同的屏幕方向。

无论设备是手机、平板电脑还是计算机,摄像头都位于设备上的固定位置。当设备屏幕方向发生变化时,相机屏幕方向也会随之更改。

因此,相机应用通常假定设备的屏幕方向和相机预览的宽高比之间存在固定关系。当手机处于竖屏模式时,系统会假定相机预览的高度大于宽度。当手机(和相机)旋转为横向时,相机预览的宽度预计大于高度。

但是,这些假设受到了新的外形规格(例如可折叠设备)和显示模式(例如多窗口模式和多屏幕)的挑战。可折叠设备无需改变方向即可更改显示屏尺寸和宽高比。多窗口模式会将相机应用限制为在屏幕的一部分上显示,并且无论设备屏幕方向如何,都会缩放相机预览。多屏幕模式支持使用辅助屏幕,而辅助屏幕的方向可能与主要屏幕的方向不同。

摄像头方向

Android 兼容性定义中规定,摄像头图像传感器“必须朝向正确方向,以便摄像头的长度方向与屏幕的长度方向对齐。也就是说,当设备处于横向时,摄像头必须横向拍摄。无论设备的自然方向为何,此规则都适用;也就是说,它适用于以横屏为主的设备以及以竖屏为主的设备。"

摄像头与屏幕的排列方式可最大限度地增加摄像头应用中摄像头取景器的显示区域。此外,图像传感器通常以横向宽高比输出数据,4:3 是最常见的模式。
手机和摄像头传感器均处于纵向模式。
在这里插入图片描述

图 1. 手机和摄像头传感器方向的典型关系。

摄像头传感器的自然屏幕方向为横向。在图 1 中,为了符合 Android 兼容性定义,前置摄像头(摄像头与显示屏方向相同的方向)的传感器相对于手机旋转了 270 度。

为了向应用公开传感器旋转效果,camera2 API 包含一个 SENSOR_ORIENTATION 常量。对于大多数手机和平板电脑,设备前置摄像头报告的传感器方向为 270 度,后置摄像头报告的传感器方向为 90 度(设备背面的视角),这会使传感器的长边与设备的长边对齐。笔记本电脑摄像头报告的传感器方向通常为 0 度或 180 度。
注意 :自然(或原生)屏幕方向是设备或传感器的典型屏幕方向 - 手机为纵向,平板电脑和笔记本电脑为横向,相机图像传感器为横向。

由于相机图像传感器在传感器的自然方向(横向)下输出其数据(图像缓冲区),因此图像缓冲区必须旋转 SENSOR_ORIENTATION 指定的角度,才能使相机预览在设备的自然屏幕方向下垂直显示。对于前置摄像头,旋转为逆时针;对于后置摄像头,旋转为顺时针。

例如,对于图 1 中的前置摄像头,摄像头传感器生成的图像缓冲区如下所示:
相机传感器已旋转为横向,图片位于一侧,位于左上角。
在这里插入图片描述

图片必须逆时针旋转 270 度,以便预览的屏幕方向与设备屏幕方向保持一致:
摄像头传感器处于纵向模式,图片保持竖直。
在这里插入图片描述

后置摄像头会生成一个方向与上述缓冲区相同的图像缓冲区,但 SENSOR_ORIENTATION 为 90 度。因此,缓冲区会顺时针旋转 90 度。

设备旋转

设备旋转角度是指设备从其自然方向旋转的角度数。例如,当手机处于横屏模式时,其设备旋转角度为 90 度或 270 度,具体取决于旋转方向。

除了传感器方向的角度之外,相机传感器图像缓冲区的旋转角度必须与设备旋转的角度相同,相机预览才能垂直显示。

方向计算

相机预览的正确方向会考虑传感器方向和设备旋转。

可以使用以下公式计算传感器图像缓冲区的整体旋转:

rotation = (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % 360

其中,sign 1对于前置摄像头,-1对于后置摄像头。

对于前置摄像头,图像缓冲区(从传感器的自然方向)逆时针旋转。对于后置摄像头,传感器图像缓冲区顺时针旋转。

对于后置摄像头,表达式 deviceOrientationDegrees * sign + 360 会将设备旋转从逆时针转换为顺时针(例如,将逆时针转换为 90 度)。模数运算将结果缩放到小于 360 度(例如,将 540 度旋转的角度调整为 180 度)。

不同的 API 以不同的方式报告设备旋转:

Display#getRotation() 提供设备(从用户的视角)逆时针旋转。该值会按原样插入上述公式。
OrientationEventListener#onOrientationChanged() 会返回设备(从用户的角度)顺时针旋转的角度。对以上公式中使用的值求反。

前置摄像头

在这里插入图片描述

相机预览和传感器处于横向时,传感器位于右侧。
图 2. 将手机旋转 90 度至横向的相机预览和传感器。

下面是图 2 中相机传感器生成的图像缓冲区:
在这里插入图片描述
摄像头传感器处于横屏模式,图片保持竖直。

缓冲区必须逆时针旋转 270 度,才能针对传感器方向进行调整(请参阅上面的相机方向):
在这里插入图片描述

相机传感器已旋转为纵向方向,图片方向为横向,位于右上角。

然后,将缓冲区逆时针旋转 90 度以考虑设备旋转,从而获得图 2 中相机预览的正确方向:
在这里插入图片描述

相机传感器已旋转为横向,图片保持竖直。

以下是相机已向右转为横向的情形:
在这里插入图片描述

屏幕方向为横向的相机预览和传感器,但传感器上下颠倒。
图 3. 摄像头预览和传感器,将手机 270 度(或 -90 度)置于横屏模式。

这里是图像缓冲区:
在这里插入图片描述

相机传感器旋转为横向,图片上下颠倒。

缓冲区必须逆时针旋转 270 度,才能针对传感器方向进行调整:
在这里插入图片描述

摄像头传感器评级为纵向,图片侧面,左上角。

然后,将缓冲区逆时针再旋转 270 度,以考虑设备旋转:
在这里插入图片描述

相机传感器已旋转为横向,图片保持竖直。

后置摄像头

后置摄像头的传感器方向通常为 90 度(从设备背面看)。调整相机预览的朝向时,传感器图像缓冲区按传感器旋转量顺时针旋转(而不是像前置摄像头一样逆时针旋转),然后图像缓冲区按设备的旋转量逆时针旋转。
屏幕方向为横向的相机预览和传感器,但传感器上下颠倒。
图 4. 手机配备后置摄像头且屏幕方向为 270 或 -90 度。
在这里插入图片描述

下面是图 4 中相机传感器的图像缓冲区:
在这里插入图片描述

相机传感器旋转为横向,图片上下颠倒。

缓冲区必须顺时针旋转 90 度,才能针对传感器方向进行调整:
在这里插入图片描述

摄像头传感器评级为纵向,图片侧面,左上角。

然后,将缓冲区逆时针旋转 270 度,以便将设备旋转考虑在内:
在这里插入图片描述

相机传感器已旋转为横向,图片保持竖直。

宽高比

当设备屏幕方向发生变化时,当可折叠设备折叠和展开时、在多窗口环境中调整窗口大小时,以及在辅助显示屏上打开应用时,屏幕宽高比也会发生变化。

当界面动态更改屏幕方向(无论是否设备屏幕方向发生变化)时,相机传感器图像缓冲区的朝向和缩放都必须与取景器界面元素的方向和宽高比相匹配。

在新外形规格的设备或者多窗口或多显示屏环境中,如果您的应用假设相机预览的屏幕方向与设备相同(纵向或横向),则预览可能会朝向不正确和/或未正确缩放。
在这里插入图片描述

处于展开状态的可折叠设备,人像摄像头预览向一侧倾斜。
图 5. 可折叠设备的宽高比从纵向转换为横向,但摄像头传感器仍保持纵向。

在图 5 中,应用错误地假设设备逆时针旋转了 90 度,因此应用旋转了相同的量。
在这里插入图片描述

展开的可折叠设备,相机预览为竖直,但由于缩放不正确而挤压。
图 6. 可折叠设备的宽高比从纵向转换为横向,但摄像头传感器仍保持纵向。

在图 6 中,应用未调整图片缓冲区的宽高比,因此无法正确缩放以适应相机预览界面元素的新尺寸。

屏幕方向固定的相机应用通常会在可折叠设备和其他大屏设备(如笔记本电脑)上遇到问题:
在这里插入图片描述

笔记本电脑上的相机预览是竖直显示,但应用界面是方向有误的。
图 7. 笔记本电脑上屏幕方向固定的纵向应用。

在图 7 中,相机应用的界面是横向的,因为应用的屏幕方向仅限于纵向。取景器图像相对于相机传感器的方向正确。

插入人像模式

对于不支持多窗口模式 (resizeableActivity=“false”) 且限制其方向(screenOrientation=“portrait” 或 screenOrientation=“landscape”)的相机应用,可以在大屏设备上以边衬区人像模式放置,以便正确确定相机预览的朝向。

在纵向模式下仅支持纵向模式的信箱模式(边衬区)应用,即使显示屏宽高比为横向也是如此。在横向模式下,仅使用横向模式的应用会在显示黑边的情况下显示,即使显示屏宽高比是纵向也是如此。旋转相机图片以与应用界面对齐,剪裁以匹配相机预览的宽高比,然后进行缩放以填充预览。

当相机图像传感器的宽高比与应用主要 activity 的宽高比不匹配时,会触发边衬区人像模式。
在这里插入图片描述

笔记本电脑上适当纵向的摄像头预览和应用界面。 宽幅预览图片会缩放和剪裁,以适应纵向模式。
图 8. 在笔记本电脑上以边衬区显示纵向模式的固定屏幕方向纵向应用。

在图 8 中,仅纵向模式的相机应用经过旋转,在笔记本电脑显示屏上直立显示界面。由于纵向应用和横向显示屏的宽高比不同,应用会显示信箱模式。系统已旋转相机预览图像以补偿应用的界面旋转(由于插入了人像模式),并且图像已经过剪裁和缩放以适应纵向方向,从而缩小了视野范围。

旋转、剪裁、缩放

在具有横向宽高比的屏幕上,针对仅支持纵向模式的相机应用调用边衬区人像模式:
在这里插入图片描述

笔记本电脑上的相机预览是竖直显示,但应用界面是方向有误的。
图 9. 笔记本电脑上屏幕方向固定的纵向应用。

应用在纵向模式下显示为信箱模式:
在这里插入图片描述

应用已旋转为纵向并采用信箱模式。图片翻转,显示在右上角。

相机图片旋转 90 度,以根据应用的重定向进行调整:
在这里插入图片描述

传感器图片旋转了 90 度,使其保持竖直。

图片剪裁为相机预览的宽高比,然后缩放以填充预览(视野范围缩小):
在这里插入图片描述

已裁剪的相机图片已缩放,以填充相机预览。

在可折叠设备上,当显示屏的宽高比为横向时,摄像头传感器的朝向可以是纵向:
在这里插入图片描述

相机预览和应用界面侧向展开的宽屏显示屏。
图 10. 处于展开状态的设备,带有仅支持人像模式的相机应用,并且采用了不同的相机传感器和显示屏宽高比。

由于相机预览会旋转以针对传感器方向进行调整,因此图片在取景器中可以正确朝向,但仅支持纵向模式的应用会侧向显示。

边衬区纵向模式只需在纵向模式下为应用添加信箱模式,即可正确调整应用和相机预览的朝向:
采用竖屏模式且在可折叠设备上竖直显示相机预览的信箱模式应用。
在这里插入图片描述

API

从 Android 12(API 级别 31)开始,应用还可以通过 CaptureRequest 类的 SCALER_ROTATE_AND_CROP 属性明确控制边衬区人像模式。

默认值为 SCALER_ROTATE_AND_CROP_AUTO,可让系统调用边衬区人像模式。SCALER_ROTATE_AND_CROP_90 是如上所述的边衬区人像模式的行为。

并非所有设备都支持全部 SCALER_ROTATE_AND_CROP 值。如需获取受支持值的列表,请参阅 CameraCharacteristics#SCALER_AVAILABLE_ROTATE_AND_CROP_MODES。
注意 :只有具有支持 SCALER_ROTATE_AND_CROP API 的相机硬件抽象层 (HAL) 的设备才能启用边衬区人像模式。

CameraX

注意 :Jetpack CameraX 可向后兼容 Android 5.0(API 级别 21)。

借助 Jetpack CameraX 库,创建可适应传感器方向和设备旋转的相机取景器是一项简单的任务。

PreviewView 布局元素会创建相机预览,自动调整传感器方向、设备旋转和缩放。PreviewView 通过应用 FILL_CENTER 缩放类型来保持相机图片的宽高比,该类型可将图片居中,但可能会根据 PreviewView 的尺寸进行剪裁。如需为相机图片添加遮幅式黑边,请将缩放类型设置为 FIT_CENTER。

如需了解使用 PreviewView 创建相机预览的基础知识,请参阅实现预览。

如需查看完整的示例实现,请参阅 GitHub 上的 CameraXBasic 代码库。

相机取景器

注意 :CameraViewfinder 库向后兼容 Android 5.0(API 级别 21)。

与 Preview 用例类似,CameraViewfinder 库提供了一组工具来简化相机预览的创建。它不依赖于 CameraX Core,因此您可以将其无缝集成到现有的 Camera2 代码库中。

您可以使用 CameraViewfinder widget 显示 Camera2 的摄像头画面,而不是直接使用 Surface。

CameraViewfinder 在内部使用 TextureView 或 SurfaceView 显示摄像头画面,并对其应用所需的转换以正确显示取景器。这涉及到更正其宽高比、缩放和旋转。

如需从 CameraViewfinder 对象请求 Surface,您需要创建一个 ViewfinderSurfaceRequest。

此请求包含对 CameraCharacteristics 中的 Surface 分辨率和相机设备信息的要求。

调用 requestSurfaceAsync() 会将请求发送到 Surface 提供程序(TextureView 或 SurfaceView),并获取 Surface 的 ListenableFuture。

调用 markSurfaceSafeToRelease() 会通知 Surface 提供程序不需要 Surface,可以释放相关资源。
Kotlin

fun startCamera(){
    val previewResolution = Size(width, height)
    val viewfinderSurfaceRequest =
        ViewfinderSurfaceRequest(previewResolution, characteristics)
    val surfaceListenableFuture =
        cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest)

    Futures.addCallback(surfaceListenableFuture, object : FutureCallback {
        override fun onSuccess(surface: Surface) {
            /* create a CaptureSession using this surface as usual */
        }
        override fun onFailure(t: Throwable) { /* something went wrong */}
    }, ContextCompat.getMainExecutor(context))
}

Java

    void startCamera(){
        Size previewResolution = new Size(width, height);
        ViewfinderSurfaceRequest viewfinderSurfaceRequest =
                new ViewfinderSurfaceRequest(previewResolution, characteristics);
        ListenableFuture surfaceListenableFuture =
                cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest);

        Futures.addCallback(surfaceListenableFuture, new FutureCallback() {
            @Override
            public void onSuccess(Surface result) {
                /* create a CaptureSession using this surface as usual */
            }
            @Override public void onFailure(Throwable t) { /* something went wrong */}
        },  ContextCompat.getMainExecutor(context));
    }

SurfaceView

如果预览不需要处理且没有动画效果,那么 SurfaceView 是一种创建相机预览的简单方法。

SurfaceView 会根据传感器方向和设备旋转,自动旋转相机传感器图像缓冲区以匹配屏幕方向。不过,系统会缩放图片缓冲区以适应 SurfaceView 尺寸,而不考虑宽高比。

您必须确保图片缓冲区的宽高比与 SurfaceView 的宽高比一致,这可以通过在组件的 onMeasure() 方法中缩放 SurfaceView 的内容来实现:

(computeRelativeRotation() 源代码位于下面的相对旋转状态。)
Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)

    val relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees)

    if (previewWidth > 0f && previewHeight > 0f) {
        /* Scale factor required to scale the preview to its original size on the x-axis. */
        val scaleX =
            if (relativeRotation % 180 == 0) {
                width.toFloat() / previewWidth
            } else {
                width.toFloat() / previewHeight
            }
        /* Scale factor required to scale the preview to its original size on the y-axis. */
        val scaleY =
            if (relativeRotation % 180 == 0) {
                height.toFloat() / previewHeight
            } else {
                height.toFloat() / previewWidth
            }

        /* Scale factor required to fit the preview to the SurfaceView size. */
        val finalScale = min(scaleX, scaleY)

        setScaleX(1 / scaleX * finalScale)
        setScaleY(1 / scaleY * finalScale)
    }
    setMeasuredDimension(width, height)
}

Java

@Override
void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    int relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees);

    if (previewWidth > 0f && previewHeight > 0f) {

        /* Scale factor required to scale the preview to its original size on the x-axis. */
        float scaleX = (relativeRotation % 180 == 0)
                       ? (float) width / previewWidth
                       : (float) width / previewHeight;

        /* Scale factor required to scale the preview to its original size on the y-axis. */
        float scaleY = (relativeRotation % 180 == 0)
                       ? (float) height / previewHeight
                       : (float) height / previewWidth;

        /* Scale factor required to fit the preview to the SurfaceView size. */
        float finalScale = Math.min(scaleX, scaleY);

        setScaleX(1 / scaleX * finalScale);
        setScaleY(1 / scaleY * finalScale);
    }
    setMeasuredDimension(width, height);
}

如需详细了解如何以相机预览的形式实现 SurfaceView,请参阅相机方向。

TextureView

TextureView 的性能不如 SurfaceView,但工作量更大,但 TextureView 可让您最大限度地控制相机预览。

TextureView 会根据传感器方向旋转传感器图像缓冲区,但不处理设备旋转或预览缩放。

缩放和旋转可通过矩阵转换进行编码。如需了解如何正确缩放和旋转 TextureView,请参阅在相机应用中支持可调整大小的 Surface

相对旋转

摄像头传感器的相对旋转角度是将摄像头传感器输出与设备方向对齐所需的旋转量。

SurfaceView 和 TextureView 等组件使用相对旋转角度来确定预览图片的 x 和 y 缩放比例。它还可用于指定传感器图像缓冲区的旋转。

借助 CameraCharacteristics 和 Surface 类,可以计算相机传感器的相对旋转角度:
Kotlin

/**
 * Computes rotation required to transform the camera sensor output orientation to the
 * device's current orientation in degrees.
 *
 * @param characteristics The CameraCharacteristics to query for the sensor orientation.
 * @param surfaceRotationDegrees The current device orientation as a Surface constant.
 * @return Relative rotation of the camera sensor output.
 */
public fun computeRelativeRotation(
    characteristics: CameraCharacteristics,
    surfaceRotationDegrees: Int
): Int {
    val sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    // Reverse device orientation for back-facing cameras.
    val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
    ) 1 else -1

    // Calculate desired orientation relative to camera orientation to make
    // the image upright relative to the device orientation.
    return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360
}

Java

/**
 * Computes rotation required to transform the camera sensor output orientation to the
 * device's current orientation in degrees.
 *
 * @param characteristics The CameraCharacteristics to query for the sensor orientation.
 * @param surfaceRotationDegrees The current device orientation as a Surface constant.
 * @return Relative rotation of the camera sensor output.
 */
public int computeRelativeRotation(
    CameraCharacteristics characteristics,
    int surfaceRotationDegrees
){
    Integer sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

    // Reverse device orientation for back-facing cameras.
    int sign = characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT ? 1 : -1;

    // Calculate desired orientation relative to camera orientation to make
    // the image upright relative to the device orientation.
    return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360;
}

窗口指标

不应使用屏幕尺寸来确定相机取景器的尺寸;相机应用可能会在屏幕的一部分中运行,在移动设备上处于多窗口模式,或者在 ChromeOS 上处于释放模式。

WindowManager#getCurrentWindowMetrics()(在 API 级别 30 中新增)会返回应用窗口的大小,而不是屏幕的大小。Jetpack WindowManager 库方法 WindowMetricsCalculator#computeCurrentWindowMetrics() 和 WindowInfoTracker#currentWindowMetrics() 可提供类似的支持,并向后兼容 API 级别 14。
注意:Display 的 getRealSize() 和 getRealMetrics() 方法自 API 级别 31 起已弃用,这些方法不支持多窗口模式或自由窗口模式。如需获取显示密度,请使用 Configuration#densityDpi,而不是 getRealMetrics()。

旋转 180 度

设备旋转 180 度(例如,从自然屏幕方向转为自然屏幕方向倒立)不会触发 onConfigurationChanged() 回调。因此,相机预览可能会上下颠倒。

如需检测 180 度旋转,请实现 DisplayListener,并在 onDisplayChanged() 回调中调用 Display#getRotation() 来检查设备旋转情况。

专属资源

在 Android 10 之前,只有多窗口环境中最顶层的可见 activity 处于 RESUMED 状态。这会让用户感到困惑,因为系统不会提供关于恢复了哪个 activity 的指示。

Android 10(API 级别 29)引入了多项恢复功能,其中所有可见 activity 都处于 RESUMED 状态。可见 activity 仍可进入 PAUSED 状态,例如,透明 activity 位于 activity 之上或 activity 不可聚焦时(如在画中画模式下),请参见画中画支持。

使用相机、麦克风或者 API 级别 29 或更高级别的任何专用或单例资源的应用必须支持多项恢复。例如,如果三个已恢复的 activity 想要使用相机,则只有一个可以访问此专属资源。每个 activity 都必须实现 onDisconnected() 回调,才能知晓优先级较高的 activity 对相机的抢占性访问。
注意 :设置 resizeableActivity=“false” 并不保证能够访问专属资源,因为另一个 activity 或在辅助屏幕上运行的应用可能具有更高的优先级。此外,从 Android 12(API 级别 31)开始,resizeableActivity=“false” 不会阻止 activity 进入多窗口模式,在该模式下,其他 activity 可能优先获取专属资源。

如需了解详情,请参阅多项恢复。

其他资源

如需查看 Camera2 示例,请参阅 GitHub 上的 Camera2Basic 应用。
如需了解 CameraX 预览用例,请参阅 CameraX 实现预览。
如需查看 CameraX 相机预览实现示例,请参阅 GitHub 上的 CameraXBasic 代码库。
如需了解 ChromeOS 上的相机预览,请参阅相机方向。
如需了解如何针对可折叠设备进行开发,请参阅了解可折叠设备。

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2023-11-08。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

五一编程

程序之路有我与你同行

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

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

打赏作者

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

抵扣说明:

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

余额充值