news 2026/9/9 7:03:04

Android自定义遮罩相机实战:CameraX预览、坐标换算与Bitmap合成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android自定义遮罩相机实战:CameraX预览、坐标换算与Bitmap合成

简介:面向Android开发者的自定义相机示例工程,演示在SurfaceView预览画面叠加半透明遮罩,并在拍照后仅裁剪矩形区域图像,便于把有效画面交给后续图像识别或上传服务。项目围绕Camera API展开,覆盖权限声明、相机初始化、SurfaceView与SurfaceHolder回调、预览参数设置、拍照回调、JPEG数据获取等关键步骤,并给出基于Bitmap.createBitmap的区域裁剪实现,可用于证件照拍摄、二维码识别、局部增强等定制场景,适合正在学习相机底层交互或图像预处理的中级开发者。压缩包共1246个文件,以Java源码、class与dex构建产物、JSON和XML配置为主,另含少量PNG资源与APK文件,整体8.3MB,工程目录清晰,包含可直接运行的构建结构,便于导入IDE对照调试。目前已有1408人下载学习,参考该项目既能理解自定义相机的完整链路,也能在此基础上扩展滤镜、对焦、第三方识别等更丰富的功能。 上个月整理移动端项目仓库的时候,翻出一个叫CustomMaskCamera.rar的旧工程压缩包。解压之后发现是早年做的一个 Android 自定义遮罩相机项目,完整程度超出我预期:相机预览、触摸调节遮罩位置和大小、多种遮罩图形、保存到相册全都有。当时这项目是给一个短视频拍摄工具做技术验证用的,后来一直没正式上线,但里面关于"自定义遮罩"的折腾过程,放到现在看依然有不少可复用的地方。这篇文章就把这套实现思路拆开讲清楚,包括遮罩层怎么画、怎么与帧数据合成、实际使用中踩过的坑,以及如果你也想在项目里做类似功能时可以参考的选型建议。

1. 先搞清楚 CustomMaskCamera 到底在解决什么问题

"遮罩相机"这个概念听起来有点绕,但拆开来看就很直白:普通相机拍照是"取景框里看到什么就保存什么",而自定义遮罩相机在保存照片之前,会在画面上面叠一层透明或半透明的图案,最终落盘的图片是"取景内容 + 遮罩图层"的合成结果。这种能力常见于创意贴纸相机、隐私打码工具、证件照排版,甚至一些互动营销场景——用户对着镜头拍脸,遮罩层提供半透明的猫耳、眼镜边框等轮廓,引导用户把脸对齐到指定位置。

1.1 这个项目的核心需求拆解

从代码结构来看,整个工程围绕三条主线展开:

  • 相机预览:需要实时把摄像头画面渲染到屏幕上,同时能在这层画面之上叠加遮罩图形,并要求遮罩与预览画面保持同步联动。
  • 遮罩交互:用户用手指拖动、双指缩放、旋转遮罩,调节它在画面中的位置和尺寸,这在拍证件照、制作大头贴时是刚需操作。
  • 照片合成:点击快门之后,不仅要保存摄像头输出的原始画面,还必须把当前遮罩在屏幕上的实际位置、大小、形状原样合成到照片中,做到"所见即所得"。

这里最容易出问题的就是第三点。很多初做类似功能的人,只考虑了预览层怎么把遮罩画上去,却忽略了"屏幕坐标系"和"图片像素坐标系"的换算关系,结果屏幕上看着遮罩稳稳地贴在模特脸上,保存下来的照片里遮罩却跑偏了。这个问题我在后面会专门展开。

1.2 为什么选择 Android 原生方案而不是第三方滤镜引擎

做遮罩合成,业界有不少现成方案,比如 GPUImage、OpenGL ES 直接做纹理混合。但当时这个项目选择的是"CameraX + 自定义 View + Bitmap 合成"这套纯原生组合,原因有三个:

  1. 遮罩形状不只是图片贴纸,还包括纯代码绘制的图形(圆形、矩形、圆角矩形、椭圆),如果用 GPUImage 这类框架,反而要多一步"把画布渲染到纹理"的操作。
  2. 需要支持"半透明实心遮罩 + 实心边框描边 + 可拖动"这种复合交互,在自定义 View 里实现最直观,调试时看布局边界也非常方便。
  3. CameraX 的ImageCapture提供了takePicture回调输出的 JPEG 字节流,拿到 Bitmap 后直接就可以在内存里做 Canvas 合成,完全不用走相机厂商私有接口,兼容性也省心。

当然,如果对性能要求极高、需要处理 4K 视频帧级别的连续合成,那肯定还是 OpenGL ES 或 RenderScript 更合适。但只做单张拍照合成,原生 Canvas 完全够用,开发效率还能快不少。

2. 核心实现拆解:预览、遮罩绘制和照片合成三件套

既然是"CustomMaskCamera",那核心代码就分三块:相机初始化、自定义遮罩视图、合成并保存。下面逐个环节说清楚实现细节和取舍。

2.1 相机预览:绕不开的 CameraX 配置细节

项目用的是 CameraX,版本是当时稳定的1.1.0-alpha。基础配置流程大家都知道,我重点说几个容易忽略的点:

预览分辨率与屏幕比例不匹配问题。相机预览的默认纵横比是 4:3 或 16:9,而手机屏幕往往是 19.5:9 甚至更长。如果不做处理,预览画面会被拉伸。CameraX 的PreviewView默认使用FILL_CENTER模式,相当于把相机画面裁剪后铺满整个屏幕,这样遮罩图层按屏幕坐标绘制,和预览画面是完全对齐的。这是最省事的方案,但注意保存照片时必须把遮罩按同样的裁剪比例换算到照片坐标系。

旋转角度处理。手机横竖屏切换、前置摄像头镜像,都会影响预览方向和最终输出的照片方向。CameraX 的ImageCapture在保存时会自动写入 EXIF 旋转信息,但如果你像我一样先解码 Bitmap 再合成,就必须根据 EXIF 手动旋转目标 Bitmap。我当时就漏了这一层,竖拍时遮罩错位了 90 度,排查了很久才找到原因。稳妥做法是利用ExifInterface读取ORIENTATION值,然后调用Matrix.postRotate转正。

// 根据 EXIF 信息旋转 Bitmap fun rotateBitmapIfNeeded(bitmap: Bitmap, filePath: String): Bitmap { val ei = ExifInterface(filePath) val orientation = ei.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) return when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90f) ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180f) ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270f) else -> bitmap } }

2.2 自定义遮罩视图:手势、形状和绘制顺序

遮罩视图我命名为MaskOverlayView,继承自View。它的职责有两个:响应触摸手势,以及在onDraw里把遮罩画出来。这里有几个关键设计点。

遮罩数据结构。我定义了一个MaskConfig数据类,用来描述遮罩的所有属性:

data class MaskConfig( val shape: MaskShape, // CIRCLE, RECT, ROUND_RECT, OVAL, CUSTOM val positionX: Float, // 相对于视图宽度的比例坐标 val positionY: Float, // 相对于视图高度的比例坐标 val scale: Float, // 遮罩尺寸(相对短边的比例) val rotation: Float, // 旋转角度 val color: Int, // 遮罩颜色 val alpha: Float // 透明度 0~1 )

注意这里的坐标和尺寸我全部存成比例值(0~1 之间),而不是绝对像素。为什么不直接存像素?因为屏幕旋转、预览画面裁剪后,视图的宽高会变,保存照片时也需要在不同尺寸的 Bitmap 上还原遮罩,只有比例坐标才能做到多端统一。这是我做类似功能时的一个心得,比直接存绝对坐标省心得多。

手势处理。单指拖动更新positionX/positionY,双指捏合更新scale,双指旋转更新rotation。用ScaleGestureDetector已经能覆盖大部分需求,但如果要同时处理拖动和缩放,最好在onTouchEvent里做拦截判断,避免两个手势互相干扰。

override fun onTouchEvent(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_POINTER_DOWN -> { /* 进入双指模式 */ } MotionEvent.ACTION_MOVE -> { if (event.pointerCount >= 2) { handleDoubleFingerMove(event) // 缩放 + 旋转 } else { handleSingleFingerMove(event) // 拖动 } } } return true }

手势处理后调用invalidate()触发重绘,刷新非常快。

绘制顺序。onDraw里的绘制顺序直接影响视觉效果,通常是这样:

  1. 先绘制底层画面(其实底层画面不必在这里画,MaskOverlayView通常直接叠加在PreviewView之上,透明背景即可)。
  2. 绘制遮罩形状。如果是纯色半透明遮罩,直接canvas.drawXXX;如果是带边框的引导框,先填充半透明色,再绘制实色描边;如果是自定义图片遮罩,就canvas.drawBitmap并按矩阵变换。
override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.save() canvas.rotate(maskConfig.rotation, centerX, centerY) paint.color = maskConfig.color paint.alpha = (maskConfig.alpha * 255).toInt() when (maskConfig.shape) { MaskShape.CIRCLE -> canvas.drawCircle(centerX, centerY, radius, paint) MaskShape.RECT -> canvas.drawRect(rectF, paint) MaskShape.ROUND_RECT -> canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint) MaskShape.OVAL -> canvas.drawOval(rectF, paint) } // 绘制边框 borderPaint.style = Paint.Style.STROKE borderPaint.strokeWidth = dp(2f) canvas.drawCircle(centerX, centerY, radius, borderPaint) canvas.restore() }

2.3 照片合成:从屏幕坐标到照片坐标的换算关系

这是整个项目里最"阴间"但也最值得讲的部分。合成的目标是把用户在预览上看到的遮罩位置,原封不动地画到照片 Bitmap 上。

先理清一个事实:预览画面是经过FILL_CENTER裁剪后的画面,相当于把摄像头的完整 4:3 画面按比例放大到屏幕尺寸,再把超出屏幕的部分裁掉。所以照片(原始 4:3)和预览(19.5:9)之间不是简单的缩放关系,而是先放大再裁剪的关系。

具体做法分三步:

  1. 计算裁剪偏移量。 假设屏幕宽高为screenW/screenH,照片原始尺寸为photoW/photoH,预览放大到填满屏幕时,等比例缩放系数是scale = max(screenW/photoW, screenH/photoH),放大后照片在屏幕上的显示区域是photoW*scale x photpH*scale,裁剪后的起点偏移为(offsetX, offsetY) = ((screenW - photoW*scale)/2, (screenH - photoH*scale)/2)

  2. 把遮罩的屏幕坐标换算成照片的像素坐标。 遮罩在屏幕上的中心点(maskCenterX, maskCenterY),映射到照片上就是:

    photoCenterX = (maskCenterX - offsetX) / scale photoCenterY = (maskCenterY - offsetY) / scale

    遮罩半径maskRadius映射为photoRadius = maskRadius / scale

  3. 在照片 Bitmap 上用换算后的坐标绘制遮罩,然后编码保存。

fun applyMaskToPhoto(photo: Bitmap, maskConfig: MaskConfig, screenW: Int, screenH: Int): Bitmap { val photoW = photo.width val photoH = photo.height val scale = max(screenW.toFloat() / photoW, screenH.toFloat() / photoH) val offsetX = (screenW - photoW * scale) / 2f val offsetY = (screenH - photoH * scale) / 2f val bitmap = photo.copy(Bitmap.Config.ARGB_8888, true) val canvas = Canvas(bitmap) val centerX = (maskConfig.positionX * screenW - offsetX) / scale val centerY = (maskConfig.positionY * screenH - offsetY) / scale val radius = (maskConfig.scale * min(screenW, screenH) / 2f) / scale val paint = Paint() paint.color = maskConfig.color paint.alpha = (maskConfig.alpha * 255).toInt() canvas.drawCircle(centerX, centerY, radius, paint) return bitmap }

核心就是把"屏幕上的比例坐标"先转成"屏幕绝对像素",再做逆裁剪和逆缩放,回到照片像素坐标。

3. 遮罩功能升级:不只是画个圆形,还有这些高阶玩法

只画一个圆形半透明遮罩,说实话有点单薄。既然项目叫 CustomMaskCamera,遮罩肯定是自定义的,这块我把实践中验证过的几种玩法列一下,都可以直接扩展进工程里。

3.1 预定义形状库与用户自定义形状

工程里内置了圆形、矩形、圆角矩形、椭圆、多边形这几类基础形状,每类在MaskShape枚举中一个值,onDraw里用when分发。真正"自定义"的玩法是指定一张 PNG 图片作为遮罩,比如品牌 Logo、剪影图案。

图片遮罩的绘制比纯图形稍复杂一点,要先按遮罩宽高等比缩放图片,再围绕中心点旋转,使用canvas.drawBitmap(bitmap, matrix, paint)绘制。这里有个容易忽视的细节:图片遮罩在合成到照片时,同样要走换算关系,而且由于 Bitmap 自带像素尺寸,需要额外乘以一个逆缩放系数,否则照片上的 Logo 会偏大或偏小。

3.2 遮罩模式切换:正片叠底和镂空取景

做证件照或创意拍时,"反向遮罩"(即镂空取景)很常用。比如拍一张半身照,取景框是透明椭圆(用户把脸放进去),取景框之外覆盖半透明灰色。这种效果用 Canvas 实现非常容易,先绘制全屏半透明底色,然后用PorterDuff.Mode.CLEAR把遮罩区域清空,或者用canvas.clipPath裁剪掉遮罩区域再绘制底色。

合成照片时需要注意模式一致性:预览里如果是"外部变暗"效果,保存时也要执行同样的路径裁剪,而不是简单地画一个实心椭圆。这个逻辑相差不大,只是多一个 if 分支,但漏了的话就会看到"预览和出图效果完全不一致"的尴尬结果。

// 镂空取景模式:外部灰色,内部透明 if (isHollow) { canvas.drawColor(Color.argb(150, 0, 0, 0)) val path = Path().apply { addCircle(centerX, centerY, radius, Path.Direction.CW) } canvas.save() canvas.clipPath(path, Region.Op.DIFFERENCE) // 这之后绘制的灰色区域会排除圆形范围之外,就是镂空效果 canvas.restore() }

3.3 保存前实时预览与二次微调

实际做产品时,用户拍完照片、保存之前往往还想再微调一下遮罩位置。所以我还给项目加了一个"确认预览"的中间页面:把合成的 Bitmap 塞到 ImageView 里展示,遮罩仍可拖动缩放,每次操作都重新合成一次。因为合成只是内存里的 Canvas 操作,速度很快,交互不会卡顿。

这个小功能对用户体验提升很关键。相比"按快门直接出图",给用户一个反悔和调整的机会,会大大减少废片率。如果你的场景不是"即时写真"而是"证件照"或"商品拍摄",强烈建议加上这个环节。

4. 踩过的那些坑:比功能实现更值得分享的细节

做这类自定义相机项目,功能代码本身不难,难的是各种边界条件和兼容性问题。下面这几个坑都是真实遇到过、排查过程也比较曲折的,写出来给大家避雷。

4.1 EXIF 旋转导致的"看起来没旋转但遮罩歪了"

前面提过一次,但值得再强调。用ImageCapture保存的照片默认是带 EXIF 旋转信息的,很多图片加载库(比如 Glide、Coil)会自动识别并展示旋转后的效果,于是你在 ImageView 中看到的是"正"的图片。但如果你手动拿到原始 Bitmap 做合成,这个 Bitmap 是没有执行旋转的,遮罩画上去就会整体偏移。

解决方式很简单:合成前先读 EXIF,根据方向角把 Bitmap 转正。但要记得转完之后释放原 Bitmap,避免内存泄漏。我当时的做法是统一封装一个BitmapUtils.loadAndRotate(filePath),所有场景都从它取 Bitmap。

4.2 前置摄像头镜像问题

前置摄像头预览默认是镜像的(类似镜子),但保存的照片是"非镜像"的。这导致用前置摄像头自拍时,预览里遮罩贴在左脸,保存出来后遮罩跑到右脸了,方向完全相反。

处理思路有两种:

  • 妥协方案:预览时用PreviewViewscaleX = -1镜像预览,让用户自己适应"照片方向"。
  • 合成方案:保存照片时手动翻转 Bitmap(Matrix.preScale(-1f, 1f)),让照片和预览保持一致。

当时为了线上体验,我选了第二种:保存时先翻转照片再合成,这样屏幕所见即所得。注意如果使用第二种方案,需要根据CameraSelector.LENS_FACING_FRONT做条件判断,不要影响后置摄像头的逻辑。

4.3 大尺寸 Bitmap OOM 风险

现在的手机照片动辄 4000 万像素,直接用BitmapFactory.decodeFile全尺寸解码再做 Canvas 合成,很容易触发 OOM。比较稳妥的做法是:

  1. 先采样压缩到"手机屏幕宽度 x 屏幕高度"大小的 Bitmap 进行合成(这个尺寸对普通图片够用)。
  2. 如果产品需要高质量原图输出,就用ImageCapture支持ImageCapture.Metadata方式写入 EXIF,让系统在 JPEG 落盘后再用额外线程做遮罩合成;或者用BitmapRegionDecoder分块处理。
  3. 对合成本身做内存复用:同一个 Bitmap 多次合成时,尽量复用CanvasPaint,减少对象重复创建。

我在项目里采用的方案是:预览返回的 Bitmap 统一压缩到屏幕宽高的 2 倍像素,然后合成、保存。对绝大多数移动端场景来说清晰度已经足够,而且内存稳定许多。

4.4 遮罩超出屏幕边界时的处理

用户拖动遮罩是可能拖出屏幕外的,此时照片上遮罩就可能"半出画"甚至完全消失。产品上可以规定两种行为:

  • 行为 A:允许超出画外,但不把遮罩裁掉,这样用户能做出"故意留半截"的效果。
  • 行为 B:自动限制拖拽边界,遮罩中心点不能超过屏幕边距的某个值,保证任何情况下遮罩都完整可见。

项目里我用的是方案 A,好处是交互自由;但因此需要在合成时做一步"裁剪到照片区域内"的操作,用canvas.clipRect(0, 0, photoW, photoH)把多余的部分裁掉,避免保存一张带着透明黑边的 PNG。方案 B 更简单,但自由度不够,适合不允许遮罩出画的产品,比如拍证件照时的头像框。

5. 扩展思路:从单张照片走向视频流和实时滤镜

这个项目最初的验证目标是单张照片合成,但做完之后我发现,自定义遮罩这套思路完全可以平移到视频录制、实时滤镜等更重的场景,只要抽象到位,后续扩展会非常顺。

5.1 连续帧合成:预览实时显示遮罩效果

如果你不想"先拍后合",而是希望预览画面就实时带着遮罩显示——这个其实很简单。既然遮罩本来就是MaskOverlayView画在PreviewView上层的,它天然就是"实时叠加"的,根本不需要额外处理帧数据。只要你不是想"把遮罩编码进视频帧",而是单纯让用户看到实时预览效果,那用 View 叠加是最便宜的方案。

5.2 把遮罩真正编码进视频

若要让视频录制出来的每一帧都"长"着遮罩,那就不能只靠 View 叠加了。做法可以有两种:一是拿到摄像头的 YUV 帧数据后在 GPU 上用纹理混合,把遮罩作为另一个纹理叠加到每帧上,再送进编码器;二是用 Android 的VirtualDisplay+MediaCodec录屏,把"PreviewView + MaskOverlayView"组合整体录下来,但清晰度和性能都比第一种差不少。如果真做到这一步,就得引入 OpenGL ES 的SurfaceTexture管线了。建议等需求明确之后再投入,否则复杂度会显著上升。

5.3 遮罩表情与贴纸动效

想让遮罩"动"起来,比如猫耳朵一抖一抖、定制边框闪烁,也不难。在MaskOverlayView里用ValueAnimator驱动scale/rotation/alpha,每帧动画回调触发invalidate()重绘,视觉上就有动态效果。合成到照片时取动画结束的那一帧参数即可。这个功能如果要做,记得动画导致的实时预览重绘对性能有一定压力,建议把帧率限制在 30fps 以内。

6. 源码结构与拿来即用的建议

最后说回CustomMaskCamera.rar这个工程本身。解压之后的目录结构大体是:

app/ ├── src/main/java/com/example/custommaskcamera/ │ ├── camera/ # CameraX 初始化、预览、拍照管理 │ ├── mask/ # MaskConfig、MaskOverlayView、形状枚举 │ ├── synth/ # 照片合成工具类、坐标换算 │ └── ui/ # 主界面、确认预览界面 └── src/main/res/ ├── drawable/ # 预置遮罩图片资源 └── layout/ # activity_main.xml 等

如果你准备在现有项目里引入"自定义遮罩相机"功能,我的建议是不要整体照搬,优先抽出这三块:

  1. MaskOverlayView:这个是纯 View 逻辑,依赖很少,直接复制进项目就能用。
  2. 坐标换算那部分工具类:注意和你的相机预览尺寸、PreviewView模式做适配。
  3. SynthUtils里的旋转和合成逻辑:这部分和 EXIF 强相关,最好封装成公共工具,便于统一处理。

实际动手时,我又踩过一次"权限申请时机"的坑:Android 6.0 以上相机权限必须动态申请,如果直接初始化 CameraX 会抛异常。在启动预览前先requestPermissions,等权限回调成功后再 bindToLifecycle,否则会出现黑屏或者闪退。

这套东西从零开始做到基本可用,大概花了两三天时间,大头都耗在坐标换算和兼容性调试上。如果你也在做类似功能,建议先画一张坐标换算的简单示意图贴在手边,写代码的时候时刻对照,能省下不少修补时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 7:00:54

胶原蛋白流失如何应对?避开抗老误区,掌握科学护肤路径

1. 先搞懂:胶原流失是怎么让脸悄悄垮掉的“胶原流失”这四个字,几乎是所有怕老之人的头号焦虑。皮肤松弛、法令纹加深、眼周细纹冒头……很多人第一反应就是“胶原又少了,得赶紧补一补”。但我在护肤行业里摸爬滚打十多年,见过太多…

作者头像 李华
网站建设 2026/9/9 6:59:20

FPGA HDMI视频环路实验:从物理层到跨时钟域的端到端验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 6:57:15

STEP 7-MicroWIN SMART v2.6 安装故障根因与工控系统兼容性修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 6:56:45

股票数据获取与清洗实战:从数据源选型到复权校验的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 6:56:16

插墙式电源适配器高温降功率与外壳过热隐患解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 6:55:13

Vue集成Cordova:实现定位拍照振动扫码的混合开发实战

简介:面向使用Vue开发跨平台移动应用的开发者,资源系统讲解Vue与Cordova的集成方法,完整覆盖获取地理位置、手机振动、调取手机图片、扫描二维码等常见原生功能。资源以zip压缩包提供,大小14.48MB,内含集成教程&#x…

作者头像 李华