智能眼镜开发:图片翻译与EXIF的重要性

Android 图片翻译中的 EXIF:从 CameraX、Glide、ImageView 到服务端 OCR

最近有一个需求是竖屏拍照并翻译(见下图),开发完才发现细节很多,特有此文做记录并方便回顾。

图片翻译看似是"拍照、上传、OCR、返回译图"四步,但只要页面锁定竖屏、用户横着拿手机拍照,方向问题就会贯穿整条链路:

text 复制代码
手机物理方向
    ↓
CameraX 输出像素与 EXIF
    ↓
上传前压缩是否保留 EXIF
    ↓
服务端是否按 EXIF 旋正后 OCR
    ↓
Glide 是否按 EXIF 解码
    ↓
ImageView 是否还需要额外旋转

任何一层把"像素方向"和"显示方向"混为一谈,都可能出现预览正常、OCR 横着识别,或者服务端结果正常、客户端却再次旋转的问题。

本文先介绍 EXIF,再完整说明这几层之间的关系,以及强制竖屏页面如何根据手机物理方向生成正确的照片,并在同一个 ImageView 中适配四种持机方向。

一、什么是 EXIF

EXIF(Exchangeable Image File Format)是图片文件中的一组元数据,常见于 JPEG,也可以出现在部分其他图片容器中。

EXIF 可以记录:

  • 拍摄时间;
  • 相机品牌与型号;
  • 曝光、焦距、ISO;
  • GPS 位置;
  • 缩略图;
  • 图片方向 Orientation

图片方向问题主要关注 Orientation。它描述的不是"把文件里的像素真的旋转了多少度",而是告诉解码器:应该怎样变换原始像素矩阵,才能得到人眼看到的正向图片。

例如,一张 JPEG 的原始像素尺寸可能是 4032 × 3024,但 EXIF Orientation 为 6。支持 EXIF 的查看器会顺时针旋转 90° 后显示,用户看到的视觉尺寸就相当于 3024 × 4032

因此,一张图同时存在两个方向概念:

text 复制代码
Stored pixels     = 文件中实际保存的像素排列
Visual image      = 对像素应用 EXIF Orientation 后看到的图片

Orientation 的八种取值

EXIF Orientation 不只有 0°、90°、180°、270°,还包含镜像情况:

Android 常量 显示变换
1 ORIENTATION_NORMAL 不变换
2 ORIENTATION_FLIP_HORIZONTAL 水平镜像
3 ORIENTATION_ROTATE_180 旋转 180°
4 ORIENTATION_FLIP_VERTICAL 垂直镜像
5 ORIENTATION_TRANSPOSE 转置
6 ORIENTATION_ROTATE_90 顺时针旋转 90°
7 ORIENTATION_TRANSVERSE 横向转置
8 ORIENTATION_ROTATE_270 顺时针旋转 270°

读取时可以把缺失、非法值降级为正常方向:

kotlin 复制代码
private fun readExifOrientation(file: File): Int {
    val orientation = runCatching {
        ExifInterface(file.absolutePath).getAttributeInt(
            ExifInterface.TAG_ORIENTATION,
            ExifInterface.ORIENTATION_NORMAL
        )
    }.getOrDefault(ExifInterface.ORIENTATION_NORMAL)

    return if (
        orientation in ExifInterface.ORIENTATION_NORMAL..
            ExifInterface.ORIENTATION_ROTATE_270
    ) {
        orientation
    } else {
        ExifInterface.ORIENTATION_NORMAL
    }
}

二、文件 EXIF、Glide 与 ImageView 是什么关系

理解图片方向时,可以把客户端拆成三层:

text 复制代码
图片文件
  ├─ 原始像素矩阵
  └─ EXIF Orientation
          ↓
Glide 解码层
  └─ 读取 EXIF,对像素应用方向变换,得到正向 Drawable
          ↓
ImageView 展示层
  ├─ scaleType 决定 Drawable 如何放入 View
  └─ rotation 决定整个 View 如何在页面坐标系中旋转

Glide 会处理 EXIF,ImageView 不会

使用 Glide 直接加载 JPEG 文件时:

kotlin 复制代码
Glide.with(imageView)
    .load(imageFile)
    .into(imageView)

Glide 会在解码阶段读取图片方向,并把交给 ImageView 的资源调整为正确视觉方向。

ImageView 自己不认识 EXIF。它只接收 Glide 解码完成的 Drawable,然后根据自己的宽高、scaleType 和变换属性绘制。

因此下面三个动作完全不同:

text 复制代码
修改 EXIF Orientation  → 修改文件元数据,不立即改变 View
Glide 按 EXIF 解码      → 改变交给 ImageView 的视觉像素方向
imageView.rotation      → 旋转页面上的 View,不修改文件和 EXIF

如果 Glide 已经按 EXIF 把图片旋正,再根据同一个 EXIF 值手动旋转 ImageView,就会发生二次旋转。

Bitmap 是一个重要边界

EXIF 属于文件,不属于 Bitmap。一旦使用 BitmapFactory 把 JPEG 解码成 Bitmap,得到的只是像素数据,原 EXIF 不会附着在 Bitmap 对象上。

随后执行:

kotlin 复制代码
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)

会生成新的 JPEG,但不会自动复制原图 EXIF。若后续 OCR 依赖 Orientation,就必须显式写回。

三、服务端 OCR 为什么也必须理解 EXIF

OCR 真正处理的是像素。服务端收到图片后,如果直接把 JPEG 的原始像素交给 OCR,而没有应用 EXIF Orientation,文字可能会横着或倒着进入识别模型。

正确的数据契约是:

text 复制代码
客户端上传:原始像素轴 + 与像素匹配的 EXIF Orientation
                         ↓
服务端解码:读取并应用 Orientation,得到视觉正向图片
                         ↓
OCR:在正向图片上检测文字、识别和翻译
                         ↓
译图:保持正确的像素与 EXIF 关系

方向处理只有两种自洽方式:

  1. 不旋转原始像素,保留原 Orientation;
  2. 物理旋正像素,同时把 Orientation 改为 ORIENTATION_NORMAL 或移除。

不能物理旋正像素后仍保留原 Orientation,否则服务端或 Glide 会再次旋转。也不能保持原始像素轴却丢掉 Orientation,否则 OCR 会看到错误方向。

四、上传前压缩如何保留 OCR 方向

图片翻译通常有上传体积限制。实现中采用以下规则:

  • 文件没有超过限制时,直接上传原文件,像素和完整 EXIF 都保持不变;
  • 文件超过限制时,按 JPEG 质量优先压缩;
  • 重编码后的文件只写回 OCR 需要的 Orientation;
  • 当前尺寸最低质量仍超限时,再逐级等比缩小。

核心原则是:压缩过程中保持原始像素轴,不提前按 EXIF 旋转 Bitmap,然后把原 Orientation 写回新文件。

kotlin 复制代码
suspend fun prepareImageForUpload(
    context: Context,
    sourceFile: File
): File = withContext(Dispatchers.IO) {
    require(sourceFile.exists() && sourceFile.length() > 0L)

    if (sourceFile.length() <= MAX_UPLOAD_SIZE) {
        return@withContext sourceFile
    }

    val orientation = readExifOrientation(sourceFile)
    compressWithoutChangingPixelAxis(
        context = context,
        sourceFile = sourceFile,
        orientation = orientation
    )
}

BitmapFactory.decodeFile() 在这条链路中负责解码存储像素,不主动根据 EXIF 旋正。Bitmap 重新编码为 JPEG 后元数据会丢失,因此需要把 Orientation 写回:

kotlin 复制代码
private fun writeJpeg(
    targetFile: File,
    bitmap: Bitmap,
    quality: Int,
    orientation: Int
) {
    FileOutputStream(targetFile).use { outputStream ->
        check(
            bitmap.compress(
                Bitmap.CompressFormat.JPEG,
                quality,
                outputStream
            )
        )
    }

    ExifInterface(targetFile.absolutePath).apply {
        setAttribute(
            ExifInterface.TAG_ORIENTATION,
            orientation.toString()
        )
        saveAttributes()
    }
}

写入 EXIF 后要再次检查文件体积,因为 saveAttributes() 也会改变最终文件:

kotlin 复制代码
writeJpeg(targetFile, bitmap, quality, orientation)

if (targetFile.length() in 1..MAX_UPLOAD_SIZE) {
    return targetFile
}

targetFile.delete()

这里刻意只复制 Orientation,不把 GPS、设备型号等无关元数据写入压缩产物。没有经过重编码的小图则保持原文件字节,因此也会保留原文件已有的其他 EXIF 信息。

这意味着"小图原样上传"也可能同时上传 GPS、设备型号和拍摄时间。采用这种策略时,应确保服务端数据处理和隐私声明覆盖这些元数据;如果业务只需要 OCR 方向,真正不可缺少的字段只有 Orientation。

一个容易判断方向是否正确的方法

假设原 JPEG 的存储尺寸是 640 × 480,Orientation 是 6

  • 压缩后仍是 640 × 480,Orientation 仍是 6:像素轴与 EXIF 保持一致;
  • 压缩后变成 480 × 640,Orientation 仍是 6:很可能已经旋转像素又保留方向,会二次旋转;
  • 压缩后仍是 640 × 480,Orientation 丢失:OCR 很可能横着识别。

五、强制竖屏为什么拿不到正确的拍摄方向

图片翻译页常因交互布局固定为竖屏:

xml 复制代码
<activity
    android:name=".ImageTranslateActivity"
    android:screenOrientation="portrait" />

不是读取 EXIF 失败,而是写入 EXIF 的方向依据失真

强制竖屏不会妨碍 ExifInterface 读取一张已有图片的 Orientation,也不会修改相册图片原本的 EXIF。

问题发生在拍摄新照片时。CameraX 保存 JPEG 前,需要根据相机传感器方向和 ImageCapture.targetRotation 计算输出方向,再决定怎样组织像素或写入旋转元数据:

text 复制代码
相机 Sensor orientation + ImageCapture.targetRotation
                         ↓
             JPEG 像素与 EXIF Orientation

正常的可旋转页面中,用户横竖切换手机会带动 Display rotation 变化,应用可以把新的 rotation 传给 CameraX。但页面被锁定为竖屏后,Android 需要维持竖屏窗口,下面这些值通常只描述"当前竖屏窗口",不再描述手机真实的物理姿态:

信息来源 强制竖屏后的表现
resources.configuration.orientation 始终是 ORIENTATION_PORTRAIT
windowManager.defaultDisplay.rotation 通常保持 Surface.ROTATION_0
previewView.display.rotation 跟随锁定后的显示 Surface,通常也保持 0°
onConfigurationChanged() 横持手机时不会得到一次正常的横屏布局切换

因此,下面这类常见写法在强制竖屏页面里只能得到窗口方向:

kotlin 复制代码
imageCapture.targetRotation =
    previewView.display?.rotation ?: Surface.ROTATION_0

假设用户把手机逆时针横持 90°,页面和 previewView.display.rotation 仍然是 ROTATION_0。CameraX 收到的 targetRotation 没有变化,就会继续按竖持状态计算 JPEG 方向。最终文件的像素可能是横着的,但 EXIF 却无法表达用户这次真实的横持姿态。Glide 和服务端 OCR 即使完全按照 EXIF 工作,也只能忠实地应用这份错误或过期的方向信息。

所以更准确的说法不是"强制竖屏读不到正确 EXIF",而是:

text 复制代码
强制竖屏切断了物理持机方向到 Display rotation 的传递;
CameraX 使用固定的 targetRotation,因而无法生成与真实拍摄姿态一致的方向信息。

EXIF 不能反过来告诉 CameraX 用户怎样拿手机

EXIF 是拍照输出的一部分,只有文件保存后才能读取。它不是一个实时方向传感器,不能在按快门前用来决定本次照片应该写什么方向。

如果拍完后才发现 Orientation 不正确,再凭 JPEG 宽高猜测用户横持方向也不可靠:后置相机传感器可能天然是横向安装,CameraX 可能旋转像素,也可能使用元数据表达方向,而且 4032 × 3024 无法区分用户是向左横持还是向右横持。

因此,拍摄前必须从独立于 Display 的传感器通道获取物理方向。

Android 的 OrientationEventListener 封装了底层方向传感器计算,可以根据重力方向返回 0..359 的设备角度。业务层不需要直接处理加速度计的三轴值,只要把角度归入四个稳定象限。

六、把物理方向映射为 CameraX targetRotation

OrientationEventListener 的角度方向与 CameraX 使用的 Surface rotation 映射不是简单同值。四个稳定方向使用下面的映射:

kotlin 复制代码
fun resolveTargetRotation(
    orientationDegrees: Int,
    fallbackRotation: Int
): Int {
    if (orientationDegrees !in 0..359) {
        return fallbackRotation
    }

    return when (orientationDegrees) {
        in 45..134 -> Surface.ROTATION_270
        in 135..224 -> Surface.ROTATION_180
        in 225..314 -> Surface.ROTATION_90
        else -> Surface.ROTATION_0
    }
}

对应关系如下:

方向监听角度 CameraX targetRotation
315°..359°、0°..44° Surface.ROTATION_0
45°..134° Surface.ROTATION_270
135°..224° Surface.ROTATION_180
225°..314° Surface.ROTATION_90

在象限边界附近,角度可能抖动。这里按最新稳定象限更新;当监听器返回 ORIENTATION_UNKNOWN 时,沿用上一次有效值,而不是随机回到 0°。

kotlin 复制代码
private var captureTargetRotation = Surface.ROTATION_0

private val orientationListener = object : OrientationEventListener(this) {
    override fun onOrientationChanged(orientation: Int) {
        val nextRotation = resolveTargetRotation(
            orientationDegrees = orientation,
            fallbackRotation = captureTargetRotation
        )
        if (nextRotation == captureTargetRotation) return

        captureTargetRotation = nextRotation
        imageCapture?.targetRotation = nextRotation
    }
}

监听器只在页面前台运行:

kotlin 复制代码
override fun onResume() {
    super.onResume()
    orientationListener.enable()
}

override fun onPause() {
    orientationListener.disable()
    super.onPause()
}

设备不支持方向检测时,继续使用竖屏方向,不能因此阻断基础拍照能力。

七、让 CameraX 写入正确的 EXIF

创建 ImageCapture 时就设置最近一次稳定方向:

kotlin 复制代码
val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
    .setTargetRotation(captureTargetRotation)
    .build()

按下快门时再次设置,并冻结本次拍摄方向:

kotlin 复制代码
private fun takePhoto(outputFile: File) {
    val frozenTargetRotation = captureTargetRotation
    val displayRotationDegrees = when (frozenTargetRotation) {
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> 0
    }

    imageCapture.targetRotation = frozenTargetRotation

    val outputOptions = ImageCapture.OutputFileOptions.Builder(outputFile)
        .build()

    imageCapture.takePicture(
        outputOptions,
        cameraExecutor,
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(
                result: ImageCapture.OutputFileResults
            ) {
                runOnUiThread {
                    showCapturedImage(
                        file = outputFile,
                        displayRotationDegrees = displayRotationDegrees
                    )
                    uploadForTranslation(outputFile)
                }
            }

            override fun onError(exception: ImageCaptureException) = Unit
        }
    )
}

这里不是手动计算 TAG_ORIENTATION=68 再写入照片。物理方向先转换成 CameraX 的 targetRotation,CameraX 在保存 JPEG 时负责让输出像素和 EXIF Orientation 保持一致。

方向必须在快门时冻结。拍摄完成后用户可能已经转动手机,如果展示原图和译图时继续读取实时传感器角度,两张静态图会跟着手机变化,甚至与本次照片的 EXIF 失去对应关系。

八、强制竖屏时 ImageView 如何适配 EXIF

强制竖屏下的静态图展示包含两个独立步骤:

  1. Glide 按文件自己的 EXIF 解码,得到视觉正向的 Drawable;
  2. ImageView.rotation 使用快门时冻结的角度,让图片在锁定竖屏的页面中仍朝向当时横持或倒持手机的用户。
kotlin 复制代码
Glide.with(imageView)
    .load(imageFile)
    .into(imageView)

imageView.rotation = displayRotationDegrees.toFloat()

第二步不是再次处理 EXIF。它处理的是两个坐标系之间的差异:

text 复制代码
EXIF / Glide   → 文件像素坐标系中的正确方向
ImageView      → 强制竖屏页面坐标系中的用户观看方向

为什么 90° 和 270° 要交换 ImageView 宽高

View.rotation 只改变绘制变换,不会触发父布局按旋转后的包围盒重新测量。

如果一个 match_parent × match_parentImageView 直接旋转 90°,它仍然保留旋转前的布局宽高。容器不是正方形时,横图就可能被缩窄或裁切。

可以在旋转 90° 或 270° 时,先交换 ImageView 的布局宽高,再绕中心旋转:

kotlin 复制代码
private fun applyDisplayRotation(
    container: FrameLayout,
    imageView: ImageView,
    rotationDegrees: Int
) {
    container.doOnLayout {
        val swapDimensions =
            rotationDegrees == 90 || rotationDegrees == 270

        imageView.updateLayoutParams<FrameLayout.LayoutParams> {
            width = if (swapDimensions) {
                container.height
            } else {
                ViewGroup.LayoutParams.MATCH_PARENT
            }
            height = if (swapDimensions) {
                container.width
            } else {
                ViewGroup.LayoutParams.MATCH_PARENT
            }
            gravity = Gravity.CENTER
        }

        imageView.rotation = rotationDegrees.toFloat()
    }
}

配合 fitCenter,同一个 ImageView 就能覆盖 0°、90°、180°、270°,不需要准备横竖两套布局:

xml 复制代码
<ImageView
    android:id="@+id/static_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="fitCenter" />

相册图片没有本次快门时的物理方向,因此额外 View 旋转使用 0°,只让 Glide 根据文件自身 EXIF 正常展示。

九、服务端译图返回后的方向处理

服务端返回的译图字节直接写入缓存,不在客户端重新编码:

kotlin 复制代码
suspend fun writeResultToCache(
    targetFile: File,
    imageBytes: ByteArray
): File = withContext(Dispatchers.IO) {
    require(imageBytes.isNotEmpty())

    FileOutputStream(targetFile).use { outputStream ->
        outputStream.write(imageBytes)
    }
    targetFile
}

这样可以避免缓存过程破坏服务端返回图片的像素和 EXIF。展示时仍然交给 Glide:

kotlin 复制代码
Glide.with(resultImageView)
    .load(translatedFile)
    .into(resultImageView)

如果译图保留了 Orientation,Glide 会按它解码;如果服务端已经物理旋正像素并移除了 Orientation,Glide 也会按普通正向图片显示。

对于拍照来源,原图和译图共用快门时冻结的 displayRotationDegrees,保证用户在强制竖屏页面里切换两张图时方向一致。相册来源则统一使用 0° 的额外 View 旋转。

十、EXIF 错误如何影响下载和分享

图片在当前页面里看起来正确,不代表下载或分享出去的文件也是正确的。

这是因为 ImageView.rotation 只作用于当前应用的 View:

text 复制代码
ImageView 中看起来正确
        ≠
文件像素和 EXIF 一定正确

用户下载图片或通过 FileProvider 分享时,传递给外部应用的是图片文件,而不是当前 ImageView 的绘制结果。ImageView 的 rotation、缩放和布局宽高都不会写回 JPEG。

kotlin 复制代码
fun shareImage(
    activity: Activity,
    imageFile: File,
    authority: String,
    mimeType: String
) {
    val imageUri = FileProvider.getUriForFile(
        activity,
        authority,
        imageFile
    )

    val intent = Intent(Intent.ACTION_SEND).apply {
        type = mimeType
        putExtra(Intent.EXTRA_STREAM, imageUri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    activity.startActivity(Intent.createChooser(intent, null))
}

这段分享代码只是授予接收方读取原文件的权限,不会替图片修复方向。如果文件 EXIF 不正确,用户可能遇到以下体验:

  • 系统相册或文件管理器中的缩略图横着、倒着;
  • 聊天软件预览正常,但发送完成后的大图方向错误;
  • 某些社交平台上传时移除 EXIF,原本依靠错误或残缺元数据勉强显示的图片永久变横;
  • 图片编辑器按错误 Orientation 打开,裁剪框和文字方向不一致;
  • 再次导出、压缩或转码后,不同应用得到不同方向;
  • 用户保存服务端译图后,译文虽然正确,但整张图片需要手动旋转才能使用。

不同应用对 EXIF 的处理并不完全一致:有的在解码时应用 Orientation,有的会在上传时物理旋正并删除 EXIF,还有的直接剥离元数据但不旋转像素。因此,错误文件可能在 A 应用里正常、在 B 应用里横倒,造成非常不稳定的使用体验。

为什么应用内的额外旋转可能掩盖问题

假设文件的像素和 EXIF 本身不匹配,但页面又执行了:

kotlin 复制代码
imageView.rotation = 90f

图片可能恰好在当前页面中转正,让问题在测试时不容易被发现。一旦分享原文件,外部应用不会知道这次 View 旋转,错误方向就会重新出现。

所以验收图片方向时,不能只看应用内的 ImageView,还要检查真正被上传、下载和分享的文件。

像素与 Orientation 的结果组合

文件像素 EXIF Orientation 应用内与外部使用结果
已物理旋正 NORMAL 或缺失 最终文件方向自洽
保持相机原始像素轴 与像素匹配 支持 EXIF 的解码器可正确显示
已物理旋正 仍保留原旋转值 解码器会二次旋转
保持原始像素轴 NORMAL、缺失或错误 图片横倒,OCR 和外部应用都可能出错

本文采用"保持像素轴并保留正确 Orientation"。服务端下载得到的译图则原样落盘,分享时也直接分享该文件。因此,服务端返回结果必须继续满足"像素与 Orientation 成对正确"这一条件。

下载和分享前应验证什么

至少要覆盖四种拍摄方向,并分别验证:

  1. 原图在 Glide 中显示正确;
  2. 上传文件的原始像素宽高与 Orientation 匹配;
  3. 服务端 OCR 识别方向正确;
  4. 下载后的译图在系统相册中方向正确;
  5. 译图分享至至少一个聊天应用和一个图片编辑器后方向正确;
  6. 接收方再次保存或转码后,图片仍没有发生二次旋转。

这一步能发现仅靠应用内 ImageView 无法暴露的问题。

十一、完整链路

最终的方向链路可以概括为:

text 复制代码
OrientationEventListener 感知手机物理方向
                    ↓
四象限映射为 CameraX targetRotation
                    ↓
按下快门,冻结 targetRotation 和页面显示角度
                    ↓
CameraX 保存像素与 EXIF 一致的 JPEG
                    ↓
上传前若重编码,只复制 EXIF Orientation
                    ↓
服务端按 Orientation 旋正后进行 OCR 和翻译
                    ↓
译图原始字节落盘,不在客户端二次重编码
        ├───────────┴───────────┐
        ↓                       ↓
Glide 按 EXIF 解码       下载或 FileProvider 分享原文件
        ↓                       ↓
ImageView 补偿页面角度    外部应用按各自策略处理 EXIF

这套实现最重要的原则是:EXIF 负责描述文件像素,Glide 负责把文件解码正确,ImageView 负责页面坐标系中的展示,服务端 OCR 负责在识别前应用文件方向。四层各自只处理自己的职责,就不会发生丢失方向或重复旋转。

总结

图片方向不是一个单独的角度,而是一份贯穿客户端、文件和服务端的契约:

  • EXIF Orientation 描述原始像素应如何变换后显示;
  • Glide 会读取图片 EXIF,ImageView 本身不会;
  • ImageView.rotation 是额外的界面变换,不会修改 EXIF;
  • Bitmap 重编码会丢失 EXIF,上传前必须显式保留 Orientation;
  • 服务端必须应用 EXIF 后再进行 OCR;
  • 强制竖屏时要独立感知物理方向,并把它映射为 CameraX targetRotation
  • 90°/270° 展示时先交换 ImageView 宽高,再执行 View 旋转;
  • 原图和译图应共用快门时冻结的页面显示角度;
  • 下载与分享传递的是文件,ImageView 的旋转不会随文件传出去。

只要始终分清"文件像素方向""EXIF 显示方向"和"View 页面方向",图片翻译链路中的旋转问题就会从一组偶发现象,变成一套可以验证的确定性规则。

注:部分内容由AI根据项目代码总结生成。

参考资料

相关推荐
律宏阔1 小时前
Android 车载蓝牙开发笔记:HFP、A2DP、AVRCP、PBAP、MAP、BLE 与系统 API
android·蓝牙
@Adzc2 小时前
Android 16 省电模式 Tile 点击导致状态栏收起
android
杉氧2 小时前
打破边界(二):在 React Native 中嵌入原生 UI 组件 (Native UI Components)
android·前端·react native
古法安卓2 小时前
Android-切换白天黑夜内存不足问题排查
android·java·android studio
聚美智数2 小时前
快递拦截-物流在途拦截-在途拦截API接口介绍
android
flower_drop2 小时前
从 QtScrcpy 到 TabQA:如何在浏览器侧边栏搞定 Android 投屏与测试提单?
android
脚踏实地,坚持不懈!4 小时前
Android 系统工程师(性能/功耗/稳定性)岗位问题深度解析:从内核源码到实战排查(完善版)
android·linux·运维·服务器
Kapaseker4 小时前
适配小米“中”折屏,非得买一台吗
android·kotlin
恋猫de小郭4 小时前
Flutter 状态管理基准测评,一个很有趣的观点
android·前端·flutter