惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LangChain Blog
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
小众软件
小众软件
The Register - Security
The Register - Security
C
Check Point Blog
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
爱范儿
爱范儿
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
Vercel News
Vercel News
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
P
Proofpoint News Feed
IT之家
IT之家
Martin Fowler
Martin Fowler
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
V2EX
MyScale Blog
MyScale Blog
Y
Y Combinator Blog
博客园 - 【当耐特】
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
S
Schneier on Security
G
Google Developers Blog
Hugging Face - Blog
Hugging Face - Blog
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
T
The Exploit Database - CXSecurity.com
罗磊的独立博客
Spread Privacy
Spread Privacy
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Cloudflare Blog
Latest news
Latest news
GbyAI
GbyAI
P
Privacy International News Feed
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
K
Kaspersky official blog

掘金

Win 安装Claude Code FastAPI 的 CORSMiddleware 跨域中间件 Java 自研 ReAct Agent 半年后,我用 LangGraph 验证了这些设计取舍 🚀AI编程工作流终极形态:GitNexus!零Token消耗实现代码知识图谱化!让Claude Code和Codex拥有上帝视角彻底告别盲目改代码,复杂项目重 LeetCode 72. 编辑距离:动态规划经典题解 被The Graph的GraphQL查询坑了三天,我用一个真实DeFi项目把链上数据索引彻底搞懂了 (AI) 编写简单 AI 助手 (ds-agent) 别再让 pnpm 跟着 nvm 跑了!独立安装终极指南 Claude Code 为什么这么顺?Anthropic 最新复盘:真正撑住它的不是模型,而是缓存 从 /simplify 指令深挖 Claude Code 多 Agent 协同机制 Function-Calling与工具使用 新手上路(六):Claude code装上ECC全家桶:38 个子代理、156 个技能、生产级 Hooks 与 Rules 体系 我在 Claude、Kimi、opencode 三个 AI 之间搭了一条自动协作管道 【技能篇】OpenClaw Skill 详解:给 AI 装上"专业外挂" wagmi v2 多链钱包切换:一个 Uniswap 仿盘项目让我踩了三天坑 两周浅学 RAG 我把 Python re 模块比喻成摸金手套 新手上路(三):Claude Code Skills 装了一堆没用?20+ 个 Skill 横向对比 + 三套组合方案,按需抄 K2.6、DeepSeek V4、GPT-5.5 都来了,组合拳打起来 Claude Code 进阶之路:从记忆系统到子代理编排 [java] 编译之后的记录类(Record Classes)长什么样子(上) 国产大模型能力大比拼,社区有话说 我研读了 500 个 Spring Boot 生产级代码库,90% 都犯了这 7 个致命错误 JAVA重点难点 转发-中央网信办部署开展“清朗·整治AI应用乱象”专项行动 合同同步逻辑 【合并已排序数组的三种实现策略,哪一种更可取?】 30天减20斤挑战:少一斤发100红包(2) 我竟然被JavaScript的隐式类型转换坑了三天! 二十五.Electron 初体验与进阶 本地到生产,解决 AI 全栈最后一公里——构建&部署&运维 程序员创业半年:顺的事、不顺的事,和我一直没想清楚的事 UI组件库elementplus 像使用 Redis 一样操作 LocalStorage 向量检索的流程是怎样的?Embedding 和 Rerank 各自的作用? LangChain DeepAgents 速通指南(七)—— DeepAgents使用Agent Skill 为什么越来越多的大厂抛弃MCP,转向CLI? 【节点】[SquareRoot节点]原理解析与实际应用 juejin.cn juejin.cn 从 “存得下” 到 “算得快”:工业物联网需要新一代时序数据平台越来越多工业用户开始意识到一个问题:**数据是存下来了, - 掘金 放弃 Claude 订阅?我用 8 年前的服务器,强跑 Google 最强开源模型 Gemma 4 真实测评! Python开发者狂喜!200+课时FastAPI全栈实战合集,10大模块持续更新中🔥 从 Claw-Code 看 AI 驱动的大型项目开发:2 人 + 10 个自治 Agent 如何产出 48K 行 Rust 代码 秒级创建实例,火山引擎 Milvus Serverless 让 AI Agent 开发更快更省火山引擎MilvusSer MediaPlayer 播放器架构:NuPlayer 的 Source/Decoder/Renderer 三驾马车 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn OrbStack:一键将你的 Mac 变为本地服务器 NginxPulse:Nginx日志监控革命!实时洞察Web流量与安全态势的智能利器引言:当Nginx日志成为运维的“数 - 掘金 juejin.cn 大V说’AI替代不了你’,但现实是——用AI的人正在替代你2026年是AI落地的元年,自从Claude Code爆火之后 - 掘金 juejin.cn 你以为是技术问题,其实是流程问题:工程效率的真相引言 在软件工程领域,效率问题始终是团队管理者和工程师们关注的焦点。当项 - 掘金 大模型工程三驾马车:Prompt Engineering、Context Engineering 与 Harness Engineering 深度解析 juejin.cn 4.响应式系统基础:从发布订阅模式的角度理解 Vue3 的数据响应式原理本文从发布订阅模式的核心思想出发,深入剖析了 V - 掘金 慌了!Android 17 取消图标文字,你的 App 可能要找不到了用户终于可以隐藏桌面图标下面的文字了。 这个功能在 juejin.cn 我用 AI 搓了一个"比谁更持久"的微信小游戏,AI实现只用了一天,微信审核却用了一个月!!!起因:一个沙雕想法的诞生 - 掘金 juejin.cn 第12章 工具(Tools)与函数调用(LangChain实战)在前几章中,我们搭建的RAG系统、对话链,核心能力局限 - 掘金 juejin.cn CmComposeUI —— 基于 Kotlin Multiplatform Compose 的 UI 组件库 Android 开发的 AI coding 与 AI debugging在目前整个行业都在大规模使用 AI coding juejin.cn juejin.cn juejin.cn juejin.cn 一文搞懂Harness Engineering与Meta-Harness 越用越强不是广告语:拆解 Hermes Agent 的三层学习机制 P2G-Python字符串方法完全指南-split、join、strip、replace的Python编程利器 AI 周刊【2026.04.06-04.12】:Anthropic 藏起最强模型、AI 社会矛盾激化、"欢乐马"登顶 从 AI Skills 学实战技能(六):让 AI 帮你总结网页、PDF、视频 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 详解 karpathy 的 microgpt:实现一个浏览器运行的 gpt 不用 Tailscale:3 步把 Mac mini 通过 FRP 暴露到公网(稳定开机自启) P2B-Python可迭代对象完全指南-从列表到生成器的Python编程利器 手把手带你部署本地模型,让你Token自由(小白专属) juejin.cn 10分钟掌握 JSON-RPC 协议,面试加分、设计不踩坑 ReAct:让大模型学会边想边做 聊聊AI的发展史,AI的爆发并不是偶然 Python的列表推导式里藏了个坑,差点让我加班到凌晨 重排、重绘与合成——浏览器渲染性能的底层逻辑 podman与docker的区别和生产环境最佳实践 juejin.cn ConcurrentHashMap线程安全实现原理全解析 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn OpenAI Codex深度解析:终端里的AI代码特工,一个指令重构整个项目 UE5.6 Cesium 插件编译踩坑记录(UE 5.6 + MSVC 14.38 + CMake 3.31)
Android 相册选择与拍照接入实践:MediaStore 分页、权限适配与 FileProvider
rocpp · 2026-05-27 · via 掘金

在做图片/视频素材选择页时,很多项目会直接接入系统 Picker 或第三方相册库。但在一些业务场景里,我们需要更强的可控性:列表里要展示自己的选中状态,要支持多选上限,要把拍照入口放在网格第一位,还要兼容 Android 13/14 的媒体权限变化。这篇文章结合一个实际的 SystemPictureFragment,总结一次“本地相册选择 + 调用相机拍照”的实现思路。

页面职责

这个页面承担三件事:

  1. 从系统相册读取图片或视频,并以 4 列网格展示。
  2. 在图片模式下,把“拍照”入口插入到列表第一个位置。
  3. 用户选中素材后,通过共享的 ImageVideoViewModel 维护选择态,最终由外层 Activity 回传。

整体流程可以概括为:

进入页面 -> 检查媒体权限 -> 查询 MediaStore -> 展示网格 -> 点击图片切换选中 / 点击相机拍照 -> 更新列表和选择结果

一、用 Activity Result API 管理权限和拍照

页面没有再使用旧的 startActivityForResult,而是统一通过 Activity Result API 注册三个 Launcher:

private lateinit var albumPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var cameraPermissionLauncher: ActivityResultLauncher<String>
private lateinit var cameraLauncher: ActivityResultLauncher<Uri>

相册权限使用 RequestMultiplePermissions,因为 Android 14 场景下可能同时请求完整媒体权限和部分访问权限;相机权限使用 RequestPermission;真正拍照则使用 ActivityResultContracts.TakePicture(),由调用方提前传入一个可写入的 content:// Uri。

这样拆分的好处是:权限结果、拍照结果都回到 Fragment 内部,生命周期安全,也不需要在 Activity 里维护一堆 requestCode。

二、Android 13/14 媒体权限适配

相册权限的变化是这类页面最容易踩坑的地方。当前实现按系统版本分三层处理:

private fun getAlbumPermissions(): Array<String> {
    return when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> arrayOf(
            getAlbumPrimaryPermission(),
            Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
        )
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> arrayOf(
            getAlbumPrimaryPermission()
        )
        else -> arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
    }
}

其中 getAlbumPrimaryPermission() 会根据页面当前媒体类型返回 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO。也就是说,图片页只申请图片权限,视频页只申请视频权限,不做过度申请。

Android 14 还引入了“部分照片和视频访问权限”。页面通过 READ_MEDIA_VISUAL_USER_SELECTED 判断用户是否只授权了部分资源:

private fun hasPartialAlbumPermission(): Boolean {
    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
        !hasFullAlbumPermission() &&
        hasPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
}

如果只有部分权限,页面仍然先加载可访问资源,同时弹出说明,引导用户按需补充授权。这一点体验上比较重要:不要因为不是“完整授权”就把用户挡在页面外。

三、自己查询 MediaStore,而不是直接跳系统相册

这个页面的核心不是“打开系统选择器”,而是自己查询 MediaStore 后渲染到 RecyclerView。图片查询使用:

val type = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
    MediaStore.MediaColumns._ID,
    MediaStore.MediaColumns.DISPLAY_NAME,
    MediaStore.MediaColumns.DATE_ADDED
)

拿到 _ID 后,通过 ContentUris.withAppendedId(type, id) 拼出真正可访问的 content:// Uri:

val contentUri = ContentUris.withAppendedId(type, id)

然后组装成业务层统一使用的 AssetsBean.ImageListBean。这样做的收益很明显:本地相册、服务端资产、拍照新增图片都可以收敛成同一套 AssetsBean,Adapter 和选择逻辑不需要关心来源。

四、分页:用 DATE_ADDED + ID 做稳定游标

系统相册可能有几千张图,一次性查完既慢又浪费内存。当前页面每页加载 100 条,并按 DATE_ADDED DESC, _ID DESC 排序。

加载更多时,不是简单靠 offset,而是保存上一页最后一条数据的 DATE_ADDED_ID,再构建游标条件:

val selection =
    "(${MediaStore.MediaColumns.DATE_ADDED} < ?) OR " +
    "(${MediaStore.MediaColumns.DATE_ADDED} = ? AND ${MediaStore.MediaColumns._ID} < ?)"

这个细节值得保留。相册数据会变化,offset 分页容易因为新增/删除资源导致重复或漏数据;使用排序字段 + 唯一 ID 做游标,稳定性更好。

Android O 以上通过 BundleQUERY_ARG_SQL_SORT_ORDERQUERY_ARG_LIMIT,低版本则回退到传统 query(uri, projection, selection, args, sortOrder)

五、把拍照入口伪装成一个普通列表 Item

拍照入口没有单独写在页面外层,而是定义成一个特殊数据类型:

data class CameraBean(val type: String = "camera") : AssetsBean()

当页面处于图片模式时,观察相册数据变化,并把 CameraBean 插入第一位:

val newList = mutableListOf<AssetsBean>()
if (mImageVideoAdapter.showCameraEntry) {
    newList.add(AssetsBean.CameraBean())
}
newList.addAll(dataList)
mImageVideoAdapter.setList(newList)

Adapter 遇到 CameraBean 时展示相机 UI,点击后回调 Fragment:

if (showCameraEntry && item is AssetsBean.CameraBean) {
    holder.setGone(R.id.group_camera, false)
    holder.itemView.setOnClickListener { onCameraClick?.invoke() }
    return
}

这个设计很轻巧:列表布局、滚动、缓存、点击区域都复用 RecyclerView,本地图片和相机入口也不会分散在两个不同层级里。

六、FileProvider + TakePicture:先造 Uri,再交给相机写入

拍照流程分两步。

第一步,在应用外部私有图片目录创建临时文件:

private fun createImageFile(): File {
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir)
}

第二步,用 FileProvider 把文件转换成可以授权给相机应用的 content:// Uri:

currentPhotoUri = FileProvider.getUriForFile(
    requireContext(),
    "${requireContext().packageName}.android7.fileprovider",
    photoFile
)
cameraLauncher.launch(currentPhotoUri)

配套的 FileProvider 需要在 Manifest 中声明,并在 file_paths.xml 中允许 Pictures 目录:

<external-files-path
    name="pictures"
    path="Pictures" />

相机拍照成功后,TakePicture 会返回 success = true。这时页面把刚才的 currentPhotoUri 包装成一条本地图片数据,并插到相册列表最前面。随后调用 mViewModel.updateValue(currentList) 刷新列表,并滚动到位置 1。这里的位置 1 是有意为之,因为位置 0 永远是相机入口。

七、选择态交给共享 ViewModel

图片点击后,Adapter 不直接持有业务结果,而是调用 ImageVideoViewModel.toggleSelection(item)。ViewModel 内部根据图片或视频的 ID 判断是否已选,已选则移除,未选且没有超过上限则加入。

外层 Activity 观察 selectedItems,单选模式下选中即返回,多选模式下展示底部已选列表,并在点击确认时通过 Intent 回传:

putParcelableArrayListExtra(
    "selected_items",
    ArrayList<AssetsBean>(mViewModel.selectedItems.value ?: emptyList())
)

这让 Fragment 只负责“展示和选择”,Activity 负责“选择结果如何结束页面”,职责比较清晰。

八、几个实践建议

  1. Android 14 的部分媒体权限不要当成失败处理,能展示多少先展示多少,再引导用户补充授权。
  2. 相册分页尽量用游标条件,不建议直接使用 offset。
  3. 拍照前一定先创建目标 Uri,TakePicture 只负责把照片写进去,不会替我们生成 Uri。
  4. FileProvider 的 authority 要和 Manifest 中一致,路径也要覆盖实际创建文件的目录。
  5. 相机入口做成列表数据项,比额外叠一个按钮更容易维护滚动、缓存和点击状态。

总结

这套实现的关键不在 API 有多复杂,而在几个边界处理:权限按版本拆分、相册查询可分页、拍照 Uri 由应用自己管理、UI 入口和真实素材用同一套数据模型承载。这样做之后,本地相册图片、视频资源、拍照新增图片都可以被统一展示和选择,后续上传、预览、排序、多选上限也都能自然接上。