Android 多媒体开发高级编程


Be From--http://bmbook.5d6d.com/ 移动与嵌入式开发技术 Android 多媒体开发 高级编程 ——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 (美) Shawn Van Every 著 巢文涵 译 北 京 Be From--http://bmbook.5d6d.com/ Shawn Van Every Pro Android Media: Developing Graphics, Music, Video and Rich Media Apps for Smartphones and Tablets EISBN:978-1-4302-3267-4 Original English language edition published by Apress, 2855 Telegraph Avenue, #600, Berkeley, CA 94705 USA. Copyright © 2009 by Apress L.P. Simplified Chinese-language edition copyright © 2011 by Tsinghua University Press. All rights reserved. 本书中文简体字版由 Apress 出版公司授权清华大学出版社出版。未经出版者书面许可,不得以任何方 式复制或抄袭本书内容。 北京市版权局著作权合同登记号 图字:01-2011-2258 本书封面贴有清华大学出版社防伪标签,无标签者不得销售。 版权所有,侵权必究。侵权举报电话:010-62782989 13701121933 图书在版编目(CIP)数据 Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用/ (美)艾佛瑞(Every, S. V.) 著;巢文涵 译. —北京:清华大学出版社,2012.2 (移动与嵌入式开发技术) 书名原文:Pro Android Media: Developing Graphics, Music, Video and Rich Media Apps for Smartphones and Tablets ISBN 978-7-302-27889-4 Ⅰ. A… Ⅱ. ①艾… ②巢… Ⅲ. 移动终端—应用程序—程序设计 Ⅳ. TN929.53 中国版本图书馆 CIP 数据核字(2012)第 008697 号 责任编辑:王 军 张立浩 装帧设计:牛艳敏 责任校对:成凤进 责任印制: 出版发行:清华大学出版社 网 址:http://www.tup.com.cn,http://www.wqbook.com. 地 址:北京清华大学学研大厦 A 座 邮 编:100084 社 总 机:010-62770175 邮 购:010-62786544 投稿与读者服务:010-62776969,c-service@tup.tsinghua.edu.cn 质 量 反 馈:010-62772015,zhiliang@tup.tsinghua.edu.cn 课 件 下 载:http://www.tup.com.cn,010-×××××××× 印 刷 者: 装 订 者: 经 销:全国新华书店 开 本:185mm×260mm 印 张:19 字 数:462 千字 版 次:2012 年 2 月第 1 版 印 次:2012 年 2 月第 1 次印刷 印 数:1~ 3500 定 价:48.00 元 ———————————————————————————————————————————— 产品编号: Be From--http://bmbook.5d6d.com/ 作 者 简 介 Shawn Van Every 是一位资深的移动和流媒体顾问,他帮助公司更好 地利用与音频和视频相关的新兴技术,主要是开发移动和流媒体应用程 序。他的客户范围从 19 Entertainment、MoMA 和迪斯尼(Disney),到 Morgan Stanley、雷曼兄弟(Lehman Brothers)和纽约大学医学院(NYU Media School),同时还包括无数的创业公司和其他小型客户。 此外,Shawn 是纽约大学交互式电信计划(Interactive Telecommunications Program)中通信领域内的一位兼职助理教授。他的教学范围很广泛,包括参与性和社会媒 体、编程、移动技术以及交互式电话等课程。他于 2008 年获得了大卫佩恩卡特(David Payne Carter)卓越教学奖。 他在许多会议和技术展示中演示、展示和介绍了其工作内容(包括 O'Reilly 的新兴电 话,O'Reilly 的新兴技术、ACM Multimedia、Vloggercon 以及 Strong Angle II)。他是 Open Media Developers Summit,Beyond Broadcast(开放媒体开发者峰会,超越广播)(2006)以及 iPhoneDevCamp NYC(NYC iPhone 开发夏令营)的联合组织者。 Shawn 在纽约布法罗市的 SUNY 大学获得多媒体研究(Media Study)方向的学士学位, 并在纽约大学获得交互式电信方向的硕士学位。 Be From--http://bmbook.5d6d.com/ 技术编辑简介 Steve Bull自从加入Paul Allen的 Interval Research公司(位于加利福尼 亚州的帕洛阿尔托市)之后一直从事编码工作和操纵移动设备。Bull 是一位 资深的混合媒体技术艺术家和企业家,在过去 9 年中,他创建了许多特定 位置的叙事型游戏,这些游戏利用了手机的社会性、技术性和创造性。可 以通过 www.stevebull.org 与 Steve 取得联系。 Wallace Jackson 是一位经验丰富的多媒体制作人员和 i3D 程序员,主要面向 Acrobat3D PDF、Android 移动应用程序、iTV 设计、JavaFX 和 JavaTV。自 Atari ST1040 和 AMIGA 3000 以来,他一直在设计富媒体;并且自从 20 年前 Multimedia Producer 杂志 诞生以来,他一直为顶级的多媒体出版物撰写关于新媒体内容开发的文章。可以通过 www.wallacejackson.com 与 Wallace 取得联系。 Be From--http://bmbook.5d6d.com/ 致 谢 本书的灵感来自于我在纽约大学的教学工作。非常感谢曾经鼓励过我的教师、工作人 员以及学生,他们构建了纽约大学的交互式电信计划,同时提供了无止境的灵感来源。感 谢 Red Burns 创建、推进以及改进 ITP。感谢 Dan O'Sullivan 不断向我挑战。感谢 Tom Igoe 和 Dan Shiffma 不断鼓励我可以完成本书。感谢 Rob Ryan 和 Marianne Petite 提供的所有支 持。感谢所有其他和我一起共事过的教师、工作人员。感谢我所有现在和过去的学生,他 们使我意识到讲授并看到项目逐步变得鲜活是多么伟大的奖赏;特别要感谢 Nisma Zaman, 他提供了非常宝贵的早期反馈。 如果不是 Apress 出版社专业且很有天分的工作人员,本书不会如此顺利出版。感谢 Steve Anglin、Matthew Moodie、Corbin Collins、Mary Ann Fugate、Adam Heath、Anne Collette 和其他 Apress 工作人员的非凡努力。 非常感谢 Steve Bull 和 Wallace Jackson,作为技术审稿者,他们测试了每一个代码片 段并填补了我错过的空白。你们的贡献非常宝贵! 如果没有 Android 的开发人员,那么本书也不会被撰写。感谢他们,特别是来自 Google 的 Dave Sparks,他提供了一些非常宝贵的事实检测和问题解答。 对于所有鼓励我的朋友和家人,我要衷心谢谢你们。 最后,如果没有我可爱的妻子 Karen Van Every 的支持,那么本书当然也不会存在。谢 谢你! Be From--http://bmbook.5d6d.com/ 前 言 在移动电话本身和其已经成为的所有事物当中,有一个明显的趋势是它们提供的多媒 体生产和消费功能在不断地增长。这一趋势从 20 世纪 90 年代末具备摄像功能的手机出现 开始,在过去几年随着人气激增的智能手机而戏剧性地兴起。在多媒体功能方面,今天的 手机同时是照相机、相册、摄像机、电影播放器、音乐播放器、听写机,且可能具备更多 功能。 特别是,Android 在 SDK 中具有非常丰富的功能,本书将试图通过讨论和实例对该 SDK 进行介绍,从而使您能够着手开发下一代多媒体应用程序。本书所讲解的示例不仅介绍了 如何显示和播放多媒体,而且还允许您利用摄像头、麦克风以及视频捕获功能。本书大致 由 3 个部分组成:前 4 章将处理图像;接下来的 4 章处理音频;而最后 4 章将介绍视频, 以及利用 Web 服务来查找和共享多媒体。 由于为介绍功能而开发的应用程序所需完成的工作量在不断地增加,因此随着本书的 介绍,所展示的示例将越来越具有挑战性。不管怎样,如果对 Android 应用程序的开发有 一点熟悉,读者就应该能够跳转到任何章节,利用讨论及示例代码创建一个利用当前展示 功能的应用程序。 示例通常采取扩展了 Activity 的完整类的形式,用于在 SDK 版本 4(Android 1.6)或更高版 本上运行。示例还会包含 XML 布局文件的内容,而且在许多情况下包含 AndroidManifest.xml 文件的内容。本书假设您将使用带 ADT 插件(0.9.9 或更新版本)的 Eclipse(Galileo 或更新版 本),并使用 Android SDK(r7 或更新版本)。因为本书主要是面向音频和视频,所以建议您 在手机(运行 Android 1.6 或更新版本)而非仿真程序上运行示例,因为在许多情况下示例在 仿真器中不能正常运行。 我很期待能看到多媒体应用程序在移动设备上的未来。希望能通过这本书帮助您创建 并定义这一美好未来。期待看到您实际开发的 Android 多媒体应用程序。 把所有这些都先搁在一边,让我们开始学习本书吧! Be From--http://bmbook.5d6d.com/ 目 录 第 1 章 Android 图像概述 ...................... 1 1.1 使用内置的 Camera 应用程序 捕获图像 ....................................... 1 1.1.1 从 Camera 应用程序返回 数据 ........................................... 2 1.1.2 捕获更大的图像 ....................... 5 1.1.3 显示大图像 ............................... 6 1.2 图像存储和元数据 ..................... 10 1.2.1 获得图像的 Uri ....................... 10 1.2.2 更新 CameraActivity 以 使用 MediaStore 存储图像 和关联元数据 ......................... 12 1.2.3 使用 MediaStore 检索图像 ..... 17 1.2.4 创建图像查看应用程序 ......... 18 1.2.5 内部元数据 ............................. 22 1.3 本章小结 ..................................... 22 第 2 章 构建定制的 Camera 应用程序 ................................. 25 2.1 使用 Camera 类 .......................... 25 2.1.1 CAMERA 权限 ....................... 25 2.1.2 预览 Surface ............................ 26 2.1.3 实现 Camera 对象 ................... 27 2.1.4 汇总 ......................................... 36 2.2 扩展定制的 Camera 应用 程序 ............................................. 39 2.2.1 构建基于定时器的 Camera 应用程序 ................................. 40 2.2.2 构建时间推移摄影应用 程序 ......................................... 45 2.3 本章小结 ..................................... 47 第 3 章 图像编辑和处理 ...................... 49 3.1 使用内置 Gallery 应用程序 选择图像 ..................................... 49 3.2 在位图上绘制位图 ..................... 53 3.3 基本的图像缩放和旋转 ............. 55 3.3.1 输入矩阵 ................................. 55 3.3.2 Matrix 类的方法...................... 58 3.4 图像处理 ..................................... 62 3.4.1 ColorMatrix ............................. 62 3.4.2 改变对比度和亮度 ................. 64 3.4.3 改变饱和度 ............................. 65 3.5 图像合成 ..................................... 66 3.6 本章小结 ..................................... 72 第 4 章 图形和触摸事件 ...................... 73 4.1 画布绘图 ..................................... 73 4.1.1 位图创建 ................................. 73 4.1.2 位图配置 ................................. 74 4.1.3 创建 Canvas 对象 .................... 74 4.1.4 使用 Paint 对象 ....................... 75 4.1.5 绘制形状 ................................. 76 4.1.6 绘制文本 ................................. 79 4.2 手指绘图 ..................................... 83 4.2.1 触摸事件 ................................. 83 4.2.2 在现有图像上绘制 ................. 86 4.2.3 保存基于位图的画布绘图 ...... 90 4.3 本章小结 ..................................... 93 第 5 章 Android 音频概述 .................... 95 5.1 音频播放 ..................................... 95 5.1.1 支持的音频格式 ..................... 95 5.1.2 通过意图使用内置的 音频播放器 ............................. 96 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 X 5.1.3 创建自定义的音频播放 应用程序 ................................. 98 5.1.4 用于音频的 MediaStore ....... 104 5.2 本章小结 ................................... 112 第 6 章 后台和网络音频 .................... 113 6.1 后台音频播放 ........................... 113 6.1.1 服务 ....................................... 113 6.1.2 加上 MediaPlayer 的本地 服务 ....................................... 117 6.1.3 控制服务中的 MediaPlayer .......................... 121 6.2 网络音频 ................................... 126 6.2.1 HTTP 音频播放 ................... 127 6.2.2 通过 HTTP 的流式音频 ...... 132 6.2.3 RTSP 音频流 ........................ 140 6.3 本章小结 ................................... 141 第 7 章 音频捕获 ...............................143 7.1 通过意图捕获音频 ................... 143 7.2 定制音频捕获 ........................... 146 7.2.1 MediaRecorder 音频源 ........ 147 7.2.2 MediaRecorder 输出格式..... 147 7.2.3 MediaRecorder 音频 编码器 .................................. 148 7.2.4 MediaRecorder 输出 和录制 .................................. 148 7.2.5 MediaRecorder 状态机 ........ 148 7.2.6 MediaRecorder 示例 ............ 149 7.2.7 其他的 MediaRecorder 方法 ...................................... 154 7.3 将音频插入 MediaStore ........... 160 7.4 使用 AudioRecord 录制原始 音频 ........................................... 160 7.5 使用 AudioTrack 播放原始 音频 ........................................... 163 7.6 捕获和播放原始音频的 示例 ........................................... 164 7.7 本章小结 ................................... 170 第 8 章 音频合成与分析 .................... 171 8.1 数字音频合成 ........................... 171 8.1.1 播放合成声音 ...................... 171 8.1.2 生成样本 .............................. 174 8.2 音频分析 ................................... 180 8.2.1 捕获声音以进行分析........... 180 8.2.2 可视化频率 .......................... 181 8.3 本章小结 ................................... 186 第 9 章 视频概述 ............................... 187 9.1 视频播放 ................................... 187 9.1.1 支持的格式 .......................... 187 9.1.2 使用意图播放 ...................... 188 9.1.3 使用 VideoView 播放........... 189 9.1.4 使用 MediaController 添加 控制 ...................................... 190 9.1.5 使用 MediaPlayer 播放 ........ 191 9.2 本章小结 ................................... 201 第 10 章 视频进阶 ............................. 203 10.1 使用 MediaStore 检索 视频 ......................................... 203 10.1.1 来自 MediaStore 的 视频缩略图 ...................... 204 10.1.2 完整的 MediaStore 视频示例 .......................... 204 10.2 网络视频 ................................. 211 10.2.1 支持的网络视频类型 ...... 211 10.2.2 网络视频播放 .................. 213 10.3 本章小结 ................................. 221 第 11 章 视频捕获 .............................. 223 11.1 使用意图录制视频 ................. 223 11.2 添加视频元数据 ..................... 226 11.3 定制视频捕获 ......................... 229 11.3.1 将 MediaRecorder 用于 视频 .................................. 230 11.3.2 定制视频捕获的完整 示例 .................................. 239 11.4 本章小结 ................................. 244 Be From--http://bmbook.5d6d.com/ 目 录 XI 第 12 章 使用 Web 服务的媒体 消费和发布 ..........................245 12.1 Web 服务 ................................ 245 12.2 HTTP 请求 .............................. 246 12.3 JSON ....................................... 248 12.3.1 使用 JSON 提取 Flickr 图像 ........................ 251 12.3.2 位置 .................................. 259 12.3.3 使用 JSON 和位置提取 Flickr 图像 ........................ 262 12.4 REST ....................................... 268 12.4.1 以 XML 表示数据............ 269 12.4.2 SAX 分析 ......................... 269 12.5 HTTP 文件上传 ...................... 274 12.5.1 生成 HTTP 请求 .............. 275 12.5.2 上传视频到 Blip.TV ........ 276 12.6 本章小结 ................................. 288 Be From--http://bmbook.5d6d.com/ Android 图像概述 本章将介绍有关 Android 上图像捕获和存储的基础知识。首先将探索 Android 所提供 的内置功能,然后在后续章节中更多地介绍定制软件。内置的图像捕获与存储功能为 Android 上的所有媒体功能提供了一个很好的切入点,为我们在以后的章节中处理音频和 视频奠定了基础。 考虑到这一点,我们将首先介绍如何利用内置的 Camera(摄像头)应用程序,然后介绍如 何利用 MediaStore——内置的媒体和元数据存储机制。接着,将研究如何减少内存的使用量 以及如何利用 EXIF——在消费类电子产品和图像处理软件世界中用于共享元数据的标准。 1.1 使用内置的 Camera 应用程序捕获图像 随着移动电话迅速成为移动计算机,它们在许多方面已经取代了各种各样的消费类电 子产品。最早添加到移动电话上且和电话无关的硬件功能之一是摄像头。现在,似乎很难 想象有人会购买一部不包含摄像头功能的移动电话。当然,基于 Android 的电话也不例外; 从一开始,Android SDK 就支持访问电话内置的硬件摄像头来捕获图像。 在 Android 上,完成许多事情的最便捷方式是通过使用意图(intent)来利用该设备上的 某个现有软件。意图是 Android 的核心组件,在文档中将它解释为一个“将要执行的操作 的描述”。在实践中,意图用于触发其他应用程序来完成某件事情,或者在单个应用程序的 活动之间进行切换。 所有带有合适硬件(摄像头)的原版 Android 设备都会附带 Camera 应用程序。Camera 应用程序包含一个意图过滤器(intent filter),它使得开发人员能够提供与 Camera 应用程序 同等的图像捕获能力,而不必构建他们自己的定制捕获例程。 意图过滤器是程序员用于指定其应用程序能够提供某个特定功能的一种方法。在应用 程序的 AndroidManifest.xml 文件中指定一个意图过滤器,将会告诉 Android,这个应用程 第 章 1 0 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 2 序(尤其是包含意图过滤器的活动)将根据指令执行指定的任务。 Camera 应用程序在其清单文件中指定了以下意图过滤器。这里显示的意图过滤器包含 在“Camera”活动标记内。 为了通过一个意图利用 Camera 应用程序,我们所要做的仅仅是必须构造一个将由上 述过滤器捕获的意图。 Intent i = new Intent("android.media.action.IMAGE_CAPTURE"); 在实践中,我们可能不希望直接使用动作字符串创建意图。在这种情况下,可以指定 MediaStore 类中的常量 ACTION_IMAGE_CAPTURE。应该使用常量而非字符串本身的原 因在于,如果该字符串发生了改变(当然常量也可能会不断地改变),那么使用常量将使得 我们的调用比之前使用字符串更有利于未来的变化。 Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); startActivity(i); 在一个基本的 Android 活动中使用这种意图,将导致默认的 Camera 应用程序以静止图 片模式(still picture mode)启动,如图 1-1 所示。 图 1-1 通过意图调用的内置 Camera 应用程序在模拟器中的运行结果 1.1.1 从 Camera 应用程序返回数据 当然,在捕获一张图片时,如果 Camera 应用程序没有将图片返回给调用活动,那么简 单地使用内置的 Camera 应用程序捕获图像将不具有真正的作用。而为了使得它真正有用, 可以将活动中的 startActivity 方法替换为 startActivityForResult 方法。使用该方法将允许我 Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 3 们访问从 Camera 应用程序中返回的数据,它恰好是用户以位图(Bitmap)形式捕获的图像。 以下是一个基本的示例。 package com.apress.proandroidmedia.ch1.cameraintent; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.widget.ImageView; public class CameraIntent extends Activity { final static int CAMERA_RESULT = 0; ImageView imv; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Intent i = new Intent(android.provider .MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(i, CAMERA_RESULT); } protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { Get Bundle extras = intent.getExtras(); Bitmap bmp = (Bitmap) extras.get("data"); imv = (ImageView) findViewById(R.id.ReturnedImageView); imv.setImageBitmap(bmp); } } } 它需要在项目的 layout/main.xml 文件中添加如下内容: 为了完成上述示例,以下是 AndroidManifest.xml 文件的内容。 在此示例中,Camera 应用程序在一个通过意图传递的附加值(extra)中返回图像,而该 意图将在 onActivityResult 方法中传递给主调活动。附加值的名称为“data”,它包含一个 Bitmap 对象,需要从泛型对象将它强制转换过来。 //从意图中获取附加值 Bundle extras = intent.getExtras(); //从附加值中获取返回的图像 Bitmap bmp = (Bitmap) extras.get("data"); 在我们的布局 XML (layout/main.xml)文件中,有一个 ImageView 对象。ImageView 是 泛型视图的扩展,其支持图像的显示。由于我们有一个带有指定 ReturnedImageView 编号 (id)的 ImageView 对象,因此需要在活动中获得它的引用,并通过 setImageBitmap 方法将 它的 Bitmap 对象设置为返回的图像。这将使得应用程序用户能够查看这幅捕获的图像。 为了获得 ImageView 对象的引用,使用在 Activity 类中指定的标准方法 findViewById。 该方法使得我们能够以编程方式引用在布局 XML 文件中指定的元素,我们正在通过将元 素 id 传递给 setContentView 来使用该布局 XML 文件。上述示例在 XML 中以如下方式指 定 ImageView 对象: Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 5 为了引用 ImageView 并通知它显示来自 Camera 的 Bitmap 对象,使用以下代码。 imv = (ImageView) findViewById(R.id.ReturnedImageView); imv.setImageBitmap(bmp); 当运行这个示例时,您可能会注意到结果图像很小(在我的手机上,它的宽为 121 像素, 高为 162 像素。其他设备会具有不同的默认大小)。这不是一个 bug——相反,它是经过精 心设计的。当通过一个意图触发时,Camera 应用程序不会将全尺寸的图像返回给主调活动。 通常,这样做需要大量的内存,而移动设备一般会在内存方面受限。相反,Camera 应用程 序将在返回的意图中返回一幅很小的缩略图,如图 1-2 所示。 图 1-2 在 ImageView 中显示的 121×162 像素的结果图像 1.1.2 捕获更大的图像 为了绕过大小限制,从 Android 1.5 开始,在大多数设备上可以将一个附加值传递给触 发 Camera 应用程序的意图。这个附加值的名称在 MediaStore 类中指定,它是一个常量, 称为 EXTRA_OUTPUT。这个附加值(采用名-值对的形式)将以 URI 的方式指示 Camera 应 用程序您想要将捕获的图像保存在什么位置。 以下代码片段指示 Camera 应用程序应该将图像保存到设备的 SD 卡上,文件名为 myfavoritepicture.jpg。 String imageFilePath = Environment.getExternalStorageDirectory() .getAbsolutePath() + "/myfavoritepicture.jpg"; File imageFile = new File(imageFilePath); Uri imageFileUri = Uri.fromFile(imageFile); Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageFileUri); startActivityForResult(i, CAMERA_RESULT); Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 6 注意:上述为图像文件创建 URI 的代码片段可以简化为下列形式: imageFileUri = Uri.parse("file:///sdcard/myfavoritepicture.jpg"); 然而在实践中,使用以上所示的方法将会使得代码更加具有设备独立性,并且对于 SD 卡的命名约定或本地文件系统的 URI 语法变化具有更好的适应性。 1.1.3 显示大图像 加载并显示一幅图像对内存使用情况具有显著的影响。例如,HTC G1 电话带有一个 320 万像素的摄像头。320 万像素的摄像头通常会捕获 2048×1536 像素的图像。显示如此 大小的 32 位图像将需要超过 100 663kb 或大约 13MB 的内存。虽然我们的应用程序不一定 会因此而耗尽内存,但是这肯定会使得内存更加容易耗尽。 Android 提供了一个名为 BitmapFactory 的实用程序类,该程序类提供了一系列的静态 方法,允许通过各种来源加载 Bitmap 图像。针对我们的需求,将从文件加载图像,并在最 初的活动中显示它。幸运的是,BitmapFactory 中的可用方法将会调用 BitmapFactory.Options 类,这使得我们能够定义如何将 Bitmap 读入内存。具体而言,当加载图像时,可以设置 BitmapFactory 应该使用的采样大小。在 BitmapFactory.Options 中指定 inSampleSize 参数, 这将表明一旦加载时结果 Bitmap 图像所占的比例。例如,在这里将 inSampleSize 设置为 8, 这会产生一幅大小是原始图像大小 1/8 的图像。 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inSampleSize = 8; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); imv.setImageBitmap(bmp); 这是一种快速加载大图像的方法,但是没有真正考虑图像的原始大小,也没有考虑屏 幕的大小。最好能够将图像缩放到刚好适合屏幕。 下面的代码片段演示了如何使用显示维度来确定在加载图像时应该发生的减采样量。 当使用这些方法时,应确保该图像尽可能多地填充显示范围。但如果该图像只是要在任何 一个维度中显示 100 个像素,那么应该使用这个值而不是显示维度,可以通过如下方式获 得该值。 Display currentDisplay = getWindowManager().getDefaultDisplay(); int dw = currentDisplay.getWidth(); int dh = currentDisplay.getHeight(); 为了确定图像的所有尺寸(用于计算),我们使用了 BitmapFactory 和 BitmapFactory. Options, Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 7 并将 BitmapFactory.Options.inJustDecodeBounds 变量设置为 true。这将通知 BitmapFactory 类只须返回该图像的范围,而无须尝试解码图像本身。当使用此方法时,BitmapFactory. Options.outHeight 和 BitmapFactory.Options.outWidth 变量将会被赋值。 // 加载图像的尺寸而不是图像本身 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); int heightRatio = (int)Math.ceil(bmpFactoryOptions.outHeight/(float)dh); int widthRatio = (int)Math.ceil(bmpFactoryOptions.outWidth/(float)dw); Log.v("HEIGHTRATIO",""+heightRatio); Log.v("WIDTHRATIO",""+widthRatio); 简单地将图像的尺寸除以显示的尺寸将获得显示的比率。然后,可以选择是否使用高 度比率或宽度比率,这取决于它们当中谁更大。只须将这个比率作为 BitmapFactory.Options. inSampleSize 变量,这将产生一幅应该加载到内存中的图像,其尺寸接近于我们在这种情 况下所需要的尺寸,也接近于显示本身的尺寸。 // 如果两个比率都大于 1, // 那么图像的一条边将大于屏幕 if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { // 若高度比率更大,则根据它缩放 bmpFactoryOptions.inSampleSize = heightRatio; } else { // 若宽度比率更大,则根据它缩放 bmpFactoryOptions.inSampleSize = widthRatio; } } //对它进行真正的解码 bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); 下面是通过一个意图使用内置摄像头并显示结果图片的完整示例代码。图 1-3 显示了 一幅由此示例生成的屏幕大小的结果图像。 package com.apress.proandroidmedia.ch1.sizedcameraintent; import java.io.File; import android.app.Activity; Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 8 import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.Display; import android.widget.ImageView; public class SizedCameraIntent extends Activity { final static int CAMERA_RESULT = 0; ImageView imv; String imageFilePath; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); imageFilePath = Environment.getExternalStorageDirectory() .getAbsolutePath() + "/myfavoritepicture.jpg"; File imageFile = new File(imageFilePath); Uri imageFileUri = Uri.fromFile(imageFile); Intent i = new Intent(android.provider.MediaStore. ACTION_IMAGE_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageFileUri); startActivityForResult(i, CAMERA_RESULT); } protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { // 获取 ImageView 的引用 imv = (ImageView) findViewById(R.id.ReturnedImageView); Display currentDisplay = getWindowManager().getDefaultDisplay(); int dw = currentDisplay.getWidth(); int dh = currentDisplay.getHeight(); // 加载图像的尺寸而不是图像本身 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory .Options(); bmpFactoryOptions.inJustDecodeBounds = true; Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 9 Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); int heightRatio = (int)Math.ceil(bmpFactoryOptions. outHeight/(float)dh); int widthRatio = (int)Math.ceil(bmpFactoryOptions. outWidth/(float)dw); Log.v("HEIGHTRATIO",""+heightRatio); Log.v("WIDTHRATIO",""+widthRatio); // 如果两个比率都大于 1, // 那么图像的一条边将大于屏幕 if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { // 若高度比率更大,则根据它缩放 bmpFactoryOptions.inSampleSize = heightRatio; } else { // 若宽度比率更大,则根据它缩放 bmpFactoryOptions.inSampleSize = widthRatio; } } //对它进行真正的解码 bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); // 显示图像 imv.setImageBitmap(bmp); } } } Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 10 图 1-3 在 ImageView 中显示屏幕大小的结果图像 上述代码需要下列 layout/main.xml 文件: 1.2 图像存储和元数据 Android 拥有一种在应用程序之间共享数据的标准方法。负责这个功能的类称为内容 提供器。内容提供器为不同类型数据的存储和检索提供了一个标准接口。 图像(以及音频和视频)的标准内容提供器是 MediaStore。MediaStore 在设备上的一个标 准位置存放文件的设置,并且为存储和检索该文件的元数据提供便利。元数据是关于数据 的数据;它可以包括文件本身的数据信息,如它的大小和名称,但是 MediaStore 还允许设 置各种各样的其他数据,诸如标题、描述、经度和纬度等。 为了开始使用 MediaStore,我们对 SizedCameraIntent 活动进行一些改动,使它利用 MediaStore 进行图像存储及元数据关联,而不是将图像存储在 SD 卡上的一个任意文件中。 1.2.1 获得图像的 Uri 为了获得存储图像的标准位置,首先需要获取 MediaStore 的引用。为此,使用一个内 Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 11 容解析器。内容解析器是用于访问内容提供器(例如 MediaStore)的方法。 通过传递一个特定的 Uri,内容解析器将提供一个 MediaStore 接口作为内容提供器。由 于是插入一幅新图像,因此我们使用的方法是 insert,并且应该使用的 Uri 将包含在 android. provider.MediaStore.Images.Media 类的常量 EXTERNAL_CONTENT_URI 中。这意味着我们 想要将图像存储在设备的主要外部存储器上(一般是 SD 卡)。反之,如果想要将它存储在设 备的内存中,那么可以使用 INTERNAL_CONTENT_URI。然而,对于存储如图像、音频、 视频等大小可能会相当大的媒体,通常需要使用 EXTERNAL_CONTENT_URI。 前面显示的插入调用返回一个 Uri,可以利用它来写入图像文件的二进制数据。在当 前情况下,由于正处于 CameraActivity 中,因此我们希望简单地将它作为触发 Camera 应用 程序的意图中的一个附加值来传递。 Uri imageFileUri = getContentResolver().insert( Media.EXTERNAL_CONTENT_URI, new ContentValues()); Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageFileUri); startActivityForResult(i, CAMERA_RESULT); 您将注意到我们也传入一个新的 ContentValues 对象。该 ContentValues 对象是我们希 望在记录创建时与它相关联的元数据。前面的示例则是传入一个空的 ContentValues 对象。 1. 预填充关联元数据 如果想要预填充元数据,那么可以使用 put 方法为它添加一些数据。ContentValues 的 数据形式是名-值对。其中,名称是标准的,定义为 android.provider. MediaStore.Images.Media 类中的常量(一些常量实际上位于 android.provider.MediaStore. MediaColumns 接口中,由 Media 类实现该接口)。 // 在 ContentValues 映射中保存图像的名称和描述 ContentValues contentValues = new ContentValues(3); contentValues.put(Media.DISPLAY_NAME, "This is a test title"); contentValues.put(Media.DESCRIPTION, "This is a test description"); contentValues.put(Media.MIME_TYPE, "image/jpeg"); // 添加一条新的记录,没有指定位图,但设置了一些值 // insert()返回新记录的 Uri Uri imageFileUri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, contentValues); 同样,这个调用返回的是一个 Uri,可以通过意图将它传递给 Camera 应用程序,以指 定该图像应该保存的位置。 如果通过 Log 命令输出这个 Uri,那么它看起来应该如下所示: content://media/external/images/media/16 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 12 您可能注意到的第一件事情是它看上去像普通的 URL,正如在 Web 浏览器中所使用 的一样;但是,它不是以类似 http(传输网页的协议)的字符串开始,而是以 content 作为开 始。在 Android 中,当一个 Uri 以 content 开始时,它将由内容提供器(如 MediaStore)使用。 2. 检索保存的图像 对于之前所获得的用来保存图像的相同 Uri,同样也可以将其用于访问该图像。无须 将该文件的完整路径传递给 BitmapFactory,相反,我们可以通过内容解析器为图像打开一 个 InputStream,并将它传递给 BitmapFactory。 Bitmap bmp = BitmapFactory.decodeStream( getContentResolver().openInputStream(imageFileUri), null, bmpFactoryOptions); 3. 后期添加元数据 在将图像捕获到 MediaStore 中之后,如果希望将图像与更多的元数据关联,那么可以 使用内容解析器的 update 方法。除了现在直接使用其 Uri 访问图像文件之外,它与之前所 用的 insert 方法非常类似。 // 更新记录的标题和描述 ContentValues contentValues = new ContentValues(3); contentValues.put(Media.DISPLAY_NAME, "This is a test title"); contentValues.put(Media.DESCRIPTION, "This is a test description"); getContentResolver().update(imageFileUri,contentValues,null,null); 1.2.2 更新 CameraActivity 以使用 MediaStore 存储图像和关联元数据 以下代码是上述示例的更新,它将在 MediaStore 中保存图像,然后允许添加标题和描 述。此外,该版本包含几个 UI 元素,我们将基于用户在该应用程序的操作进程对这些元 素的可见性进行管理。 package com.apress.proandroidmedia.ch1.MediaStorecameraintent; import java.io.FileNotFoundException; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 13 import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import android.provider.MediaStore.Images.Media; import android.content.ContentValues; public class MediaStoreCameraIntent extends Activity { final static int CAMERA_RESULT = 0; Uri imageFileUri; // 在 res/layout/main.xml 中指定的用户界面元素 ImageView returnedImageView; Button takePictureButton; Button saveDataButton; TextView titleTextView; TextView descriptionTextView; EditText titleEditText; EditText descriptionEditText; 我们包括了几个用户界面元素。在 layout/main.xml 中将它们指定为正常显示,并且在 上述代码中声明了它们的对象。 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 将内容视图设置为在 res/layout/main.xml 文件中定义的视图 setContentView(R.layout.main); // 获取 UI 元素的引用 returnedImageView = (ImageView) findViewById(R.id.ReturnedImageView); takePictureButton = (Button) findViewById(R.id.TakePictureButton); saveDataButton = (Button) findViewById(R.id.SaveDataButton); titleTextView = (TextView) findViewById(R.id.TitleTextView); descriptionTextView = (TextView) findViewById(R.id.DescriptionTextView); titleEditText = (EditText) findViewById(R.id.TitleEditText); descriptionEditText = (EditText) findViewById(R.id.DescriptionEditText); 在标准活动 onCreate 方法中,调用 setContentView 之后将会实例化用户界面元素,然 后需要在代码中对它们进行控制。在通过 findViewById 方法获得这些元素之后,必须将它 们都转换为合适的类型。 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 14 // 除 takePictureButton 之外,将其他所有的元素都设置为初始时不可见 // View.GONE 表示不可见,且不占用布局上的空间 returnedImageView.setVisibility(View.GONE); saveDataButton.setVisibility(View.GONE); titleTextView.setVisibility(View.GONE); descriptionTextView.setVisibility(View.GONE); titleEditText.setVisibility(View.GONE); descriptionEditText.setVisibility(View.GONE); 接下来,将所有的用户界面元素都设置为不可见,且不占用布局上的空间。可以在 setVisibility方法中设置View.GONE常量来达到这个目的。另一个选项——View.INVISIBLE ——将隐藏元素,但是它们仍占用布局空间。 // 当单击拍照按钮时 takePictureButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { // 添加一条不带位图的新记录 // 返回新记录的 Uri imageFileUri = getContentResolver() .insert(Media.EXTERNAL_CONTENT_URI, new ContentValues()); // 启动 Camera 应用程序 Intent i = new Intent(android.provider.MediaStore. ACTION_IMAGE_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageFileUri); startActivityForResult(i, CAMERA_RESULT); } }); 在 takePictureButton 的 OnClickListener 中,创建了用于内置摄像头的标准意图,并且 调用了 startActivityForResult 方法。在这里(而非直接在 onCreate 方法中)做这些工作将产生 稍微好一点的用户体验。 saveDataButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { // 更新 MediaStore 中记录的标题和描述 ContentValues contentValues = new ContentValues(3); contentValues.put(Media.DISPLAY_NAME, titleEditText.getText().toString()); contentValues.put(Media.DESCRIPTION, descriptionEditText.getText().toString()); getContentResolver().update(imageFileUri,contentValues, null,null); Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 15 // 通知用户 Toast bread = Toast.makeText(MediaStoreCameraIntent this, "Record Updated", Toast.LENGTH_SHORT); bread.show(); // 回到初始状态,设置拍照按钮为可见 // 隐藏其他 UI 元素 takePictureButton.setVisibility(View.VISIBLE); returnedImageView.setVisibility(View.GONE); saveDataButton.setVisibility(View.GONE); titleTextView.setVisibility(View.GONE); descriptionTextView.setVisibility(View.GONE); titleEditText.setVisibility(View.GONE); descriptionEditText.setVisibility(View.GONE); } }); } 当 Camera 应用程序返回一幅图像时,saveDataButton 按钮变得可见,其 onClickListener 事件完成将图像与元数据相关联的工作。它获得用户输入到各个 EditText 元素中的值,并 创建一个 ContentValues 对象,该对象用于更新 MediaStore 中关于该图像的记录。 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { // Camera 应用程序已经返回 // 隐藏拍照按钮 takePictureButton.setVisibility(View.GONE); // 显示其他 UI 元素 saveDataButton.setVisibility(View.VISIBLE); returnedImageView.setVisibility(View.VISIBLE); titleTextView.setVisibility(View.VISIBLE); descriptionTextView.setVisibility(View.VISIBLE); titleEditText.setVisibility(View.VISIBLE); descriptionEditText.setVisibility(View.VISIBLE); // 缩放图像 int dw = 200; // 使它最多宽 200 个像素 int dh = 200; // 使它最多高 200 个像素 try { // 加载图像的尺寸而非图像本身 BitmapFactory.Options bmpFactoryOptions = Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 16 new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeStream(getContentResolver(). openInputStream(imageFileUri), null, bmpFactoryOptions); int heightRatio = (int)Math.ceil(bmpFactoryOptions. outHeight/(float)dh); int widthRatio = (int)Math.ceil(bmpFactoryOptions. outWidth/(float)dw); Log.v("HEIGHTRATIO",""+heightRatio); Log.v("WIDTHRATIO",""+widthRatio); // 如果两个比率都大于 1, // 那么图像的一条边将大于屏幕 if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { // 若高度比率较大,则根据它进行缩放 bmpFactoryOptions.inSampleSize = heightRatio; } else { //若宽度比率较大,则根据它进行缩放 bmpFactoryOptions.inSampleSize = widthRatio; } } // 对它进行真正的解码 bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeStream(getContentResolver(). openInputStream(imageFileUri), null, bmpFactoryOptions); // 显示图像 returnedImageView.setImageBitmap(bmp); } catch (FileNotFoundException e) { Log.v("ERROR",e.toString()); } } } } 下面是布局 XML 文件,即在上述示例中使用的“main.xml”。 与前面的示例一样,当 Camera 应用程序返回时触发 onActivityResult 方法。将新创建 的图像解码成 Bitmap 形式并显示。在此版本中,对相关的用户界面元素也进行了管理。 1.2.3 使用 MediaStore 检索图像 为了说明在 Android 上使用共享内容提供器的能力,一个示例是使用它们可以很容易 地创建一个类似的图像库应用程序。由于内容提供器(当前为 MediaStore)在应用程序之间 共享,因此为了使得我们自己的应用程序可以查看图像,实际上并不需要创建一个摄像头 应用程序并采取方法来存储图像。由于大多数应用程序将使用默认的 MediaStore,因此可 以利用它来建立我们自己的图像库应用程序。 从 MediaStore 中选择非常简单。可以使用用于创建新记录的相同 Uri 来从中选择记录。 Media.EXTERNAL_CONTENT_URI MediaStore 和所有的内容提供器都以一种类似数据库的方式运作。从它们中选择记录, 获得一个 Cursor 对象,并通过它来遍历结果。 为了实现选择,首先需要创建一个想要返回的列的字符串数组。对于 MediaStore 中的 图像,其标准列在 MediaStore.Images.Media 类中表示。 String[] columns = { Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME }; Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 18 为了执行实际查询,可以使用活动 managedQuery 方法。第一个参数是 Uri,随后是列 名称的数组,后跟一条限定的 WHERE 子句和 WHERE 子句的任何参数,最后是一条 ORDER BY 子句。 以下将选择在最近一个小时内创建的记录,并按照从最远到最近的顺序对它们排序。 首先,创建一个称为 oneHourAgo 的变量,该变量将存储从 1970 年 1 月 1 日到一小时 前所经过的秒数。System.currentTimeMillis( )将返回相同日期以来的毫秒数,所以除以 1000 将获得秒数。如果减去 60 分×60 秒,那么将获得一个小时前的值。 long oneHourAgo = System.currentTimeMillis()/1000 - (60 * 60); 接着,将该值放入作为 WHERE 子句参数的字符串数组。 String[] whereValues = {""+oneHourAgo}; 然后,选择想要返回的列。 String[] columns = {Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME, Media.DATE_ADDED }; 最后执行该查询。WHERE 子句中有一个“?”,它将被下一个参数中的值所替换。如 果存在多个“?”,那么在传入的数组中必须有多个值。这里使用 ORDER BY 子句指定返回 的数据将以添加的日期升序排列。 cursor=managedQuery(Media.EXTERNAL_CONTENT_URI, columns, Media.DATE_ADDED + " > ?",whereValues, Media.DATE_ADDED + " ASC"); 当然,如果希望返回所有记录,那么可以对后面的 3 个参数传入 null 值。 Cursor cursor = managedQuery(Media.EXTERNAL_CONTENT_URI, columns, null, null, null); 返回的游标会告诉我们当前选择的每个列的索引。 displayColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images. Media.DATA); 为了从游标中选择字段,需要使用此索引。首先通过调用 moveToFirst 方法,确保游 标是有效的并包含一些结果。如果游标不包含任何结果,那么该方法将返回 false。我们使 用 Cursor 类中的几种方法之一来选择实际的数据。选择的方法取决于数据的类型,例如 getString 用于字符串,getInt 用于整数等。 if (cursor.moveToFirst()) { String displayName = cursor.getString(displayColumnIndex); } Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 19 1.2.4 创建图像查看应用程序 下面是一个完整的示例,它查询 MediaStore 以发现图像,并以幻灯片的形式一幅接一 幅地向用户展示图像。 package com.apress.proandroidmedia.ch1.MediaStoregallery; import android.app.Activity; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.provider.MediaStore; import android.provider.MediaStore.Images.Media; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.TextView; public class MediaStoreGallery extends Activity { public final static int DISPLAYWIDTH = 200; public final static int DISPLAYHEIGHT = 200; 没有使用屏幕大小来加载和显示图像,我们将使用上述常量来决定如何显示它们。 TextView titleTextView; ImageButton imageButton; 在此示例中,使用一个 ImageButton 来代替 ImageView。这使得我们同时具有 Button 的功能(可单击)和 ImageView 的功能(可显示一幅图像)。 Cursor cursor; Bitmap bmp; String imageFilePath; int fileColumn; int titleColumn; int displayColumn; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); titleTextView = (TextView) this.findViewById(R.id.TitleTextView); Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 20 imageButton = (ImageButton) this.findViewById(R.id.ImageButton); 以下代码指定想要返回的列,它必须是字符串数组的形式。在下面的代码中将这个数 组传递给 managedQuery 方法。 String[] columns = { Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME }; cursor = managedQuery(Media.EXTERNAL_CONTENT_URI, columns, null, null, null); 我们将需要知道从 Cursor 对象获取数据的每个列的索引。在此示例中,从 Media.DATA 切换到 MediaStore.Images.Media.DATA。这仅仅是为了说明它们是相同的。Media.DATA 仅 仅是可以使用的简写形式,因为有一条包含它的 import 语句:android.provider.MediaStore. Images.Media。 fileColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Images. Media.TITLE); displayColumn= cursor.getColumnIndexOrThrow(MediaStore.Images.Media. DISPLAY_NAME); 运行查询并获得一个结果 Cursor 对象之后,调用该对象上的 moveToFirst 方法,以确 保它包含结果。 if (cursor.moveToFirst()) { //titleTextView.setText(cursor.getString(titleColumn)); titleTextView.setText(cursor.getString(displayColumn)); imageFilePath = cursor.getString(fileColumn); bmp = getBitmap(imageFilePath); // 显示图像 imageButton.setImageBitmap(bmp); } 然后,为 imageButton 指定一个新的 onClickListener,其调用 Cursor 对象上的 moveToNext 方法。它将遍历结果集,获取并显示返回的每幅图像。 imageButton.setOnClickListener( new OnClickListener() { public void onClick(View v) { if (cursor.moveToNext()) { //titleTextView.setText(cursor.getString(titleColumn)); titleTextView.setText(cursor.getString(displayColumn)); imageFilePath = cursor.getString(fileColumn); Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 21 bmp = getBitmap(imageFilePath); imageButton.setImageBitmap(bmp); } } } ); } 下面是一个称为 getBitmap 的方法,它封装了图像的缩放和加载功能。这么做是为了 在显示这些图像时避免产生本章前面所讨论的内存问题。 private Bitmap getBitmap(String imageFilePath) { // 加载图像的尺寸而不是图像本身 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); int heightRatio = (int) Math.ceil(bmpFactoryOptions.outHeight / (float) DISPLAYHEIGHT); int widthRatio = (int) Math.ceil(bmpFactoryOptions.outWidth / (float) DISPLAYWIDTH); Log.v("HEIGHTRATIO", ""+ heightRatio); Log.v("WIDTHRATIO", ""+ widthRatio); // 如果两个比率都大于 1,那么图像的一条边大于屏幕 if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { // 若高度比率比较大,则根据它进行缩放 bmpFactoryOptions.inSampleSize = heightRatio; } else { // 若宽度比率比较大,则根据它进行缩放 bmpFactoryOptions.inSampleSize = widthRatio; } } // 对它进行真正的解码 bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); return bmp; } } 下面是与上述活动对应的布局 XML,应该将它放在 res/layout/main.xml 文件中。 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 22 1.2.5 内部元数据 EXIF 表示可交换的图像文件格式(Exchangeable Image File Format),它是在图像文件中 保存元数据的一种标准方式。许多数字摄像头和桌面应用程序都支持使用 EXIF 数据。由 于 EXIF 数据实际上是文件的一部分,因此当文件从一个位置传送到另一个位置时,不应 该将它丢失。例如,当将一个文件从 Android 设备的 SD 卡复制到一台家庭计算机时,此 数据应保持完好。如果在一个应用程序(例如 iPhoto)中打开该文件,那么该数据将会呈现。 一般而言,EXIF 数据是非常技术化的;标准中的大多数标记与所捕获图像本身的数据 相关,如 ExposureTime 和 ShutterSpeedValue。 然而,有一些标记可以考虑填写或修改。其中的一些标记如下: ● UserComment:由用户生成的备注 ● ImageDescription:标题 ● Artist:图像的创建者或接受者 ● Copyright:图像的版权持有人 ● Software:用于创建图像的软件 幸运的是,Android 为我们提供了一种读取和写入 EXIF 数据的好方法。该方法的主 要类是 ExifInterface。 以下代码显示如何使用 ExifInterface 从一个图像文件读取特定的 EXIF 数据: ExifInterface ei = new ExifInterface(imageFilePath); String imageDescription = ei.getAttribute("ImageDescription"); if (imageDescription != null) { Log.v("EXIF", imageDescription); } Be From--http://bmbook.5d6d.com/ 第 1 章 Android 图像概述 23 以下代码显示如何使用 ExifInterface 将 EXIF 数据保存到图像文件中: ExifInterface ei = new ExifInterface(imageFilePath); ei.setAttribute("ImageDescription","Something New"); ExifInterface 包括一组定义了典型数据集的常量,它们会由 Camera 应用程序自动地包 含在捕获的图像中。 Exif 规范的最新版本是 2010 年 4 月发布的 2.3 版本。可以在以下网址中在线获取它: www.cipa.jp/english/hyoujunka/kikaku/pdf/DC-008-2010_E.pdf. 1.3 本章小结 本章介绍了 Android 上图像捕获和存储的基础知识。我们看到了使用 Android 上内置 的 Camera 应用程序的强大功能,以及通过一个意图如何有效地利用其功能。我们也了解 到 Camera 应用程序提供了一个良好的和一致的接口,能够为任何 Android 应用程序添加 图像捕获功能。 我们也注意到在处理大图像时需要意识到内存的使用量,并且了解到 BitmapFactory 类会帮助载入图像的缩放版本以节省内存。注意内存的需要提醒我们移动电话不是看似具 有无限内存的桌面计算机。 接着,我们使用了 Android 内置的针对图像的内容提供器 MediaStore,了解了如何使 用它将图像保存到设备上的一个标准位置,以及如何查询它来快速构建使用已经捕获的图 像的应用程序。 最后,我们查看了如何利用称为 EXIF 的标准将图像与某些元数据相关联,EXIF 是可 传输的,可用于多种设备和软件应用程序。 本章为探索如何进一步处理 Android 上的多媒体提供了一个良好的起点。 Be From--http://bmbook.5d6d.com/ 构建定制的 Camera 应用程序 在第 1 章中,我们了解了如何利用 Android 内置的 Camera 应用程序,为任何其他的应 用程序提供一个现成的照片捕获组件。虽然这为最终用户提供了一个标准的接口,并且对 程序员而言非常简单,但是它并没有提供太多的灵活性。例如,如果希望照片捕获应用程 序支持时间推移摄影,那么不能简单地使用内置的应用程序来实现该功能。 幸运的是,Android 并不限制我们只能通过内置应用程序来访问硬件摄像头。我们有 足够多的方法可以访问底层硬件,并且有许多与 Camera 应用程序本身一样的方法,从而 能够在任何想要的应用程序类型中使用这些功能。 本章将探讨如何利用底层的 Camera 类来构建一个照相应用程序,并学习如何利用所 提供的功能。我们将讨论构建以下几种不同应用程序所需的步骤: ● 一个简单的点击照相应用程序 ● 一个倒计时风格的计时器 ● 一个时间推移照相应用程序 2.1 使用 Camera 类 可以使用 Android 中的 Camera 类访问该设备上的摄像头硬件。它使我们能够真正捕获 图像,并通过其嵌套的 Camera.Parameters 类更改不同的属性设置,例如是否应该将闪光灯 激活以及如何设置白平衡的值。 http://developer.android.com/reference/android/hardware/Camera.html 2.1.1 CAMERA 权限 为了使用 Camera 类捕获图像,需要在 AndroidManifest.xml 文件中指定必需的 CAMERA 第 章 2 0 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 26 权限。 2.1.2 预览 Surface 同样,在开始使用摄像头之前,还需要创建某种类型的 Surface(表面),使得 Camera 应用程序能够在其上绘制取景器(viewfinder)或预览图像。Surface 是 Android 中的一个抽象 类,表示绘制图形或图像的位置。提供一个绘图 Surface 的简单方法是使用 SurfaceView 类。 SurfaceView 是在标准视图中提供 Surface 的具体类。 为了在布局中指定 SurfaceView,只须在任何普通的布局 XML 中使用 元素。以下是一种基本的布局,其仅仅在用于摄像头预览的 LinearLayout(线性布局)中实现 一个 SurfaceView。 为了在代码中实现通过 Camera 类使用此 SurfaceView,需要添加一个 SurfaceHolder 类。SurfaceHolder 类可以作为 Surface 上的一个监控器,并且通过回调提供接口,从而让 我们知道什么时候创建、销毁或更改 Surface。同时,SurfaceView 类还提供了一个 getHolder 方法,用于获得一个对应其 Surface 的 SurfaceHolder 对象。 以下是一个代码片段,其访问在布局 XML 中声明的 SurfaceView,并从中获得一个 SurfaceHolder。同时,它还设置该 Surface 是一个“推送”类型的 Surface,这意味着在 Surface 本身的外部维持绘图缓冲区。在这种情况下,该缓冲区由 Camera 类管理。“推送”类型的 Surface 是 Camera 预览所需的 Surface。 SurfaceView cameraView = (CameraView) this.findViewById(R.id.CameraView); SurfaceHolder surfaceHolder = cameraView.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 另外,我们可能会希望在活动中实现 SurfaceHolder.Callback,从而使得在创建、修改 及销毁该 Surface 时活动将会获得通知。为了实现回调,需要添加以下方法。 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {} public void surfaceCreated(SurfaceHolder holder) {} public void surfaceDestroyed(SurfaceHolder holder) {} Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 27 最后,需要告诉 SurfaceHolder 使用该活动作为回调处理程序。 surfaceHolder.addCallback(this); 现在活动应该看起来如下所示。 package com.apress.proandroidmedia.ch2.snapshot; import android.app.Activity; import android.os.Bundle; import android.view.SurfaceHolder; import android.view.SurfaceView; public class SnapShot extends Activity implements SurfaceHolder.Callback { SurfaceView cameraView; SurfaceHolder surfaceHolder; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); cameraView = (SurfaceView) this.findViewById(R.id.CameraView); surfaceHolder = cameraView.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.addCallback(this); } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { } public void surfaceCreated(SurfaceHolder holder) { } public void surfaceDestroyed(SurfaceHolder holder) { } } 2.1.3 实现 Camera 对象 既然已经建立了活动及预览 Surface,现在我们准备好开始使用实际的 Camera 对象。 当创建 Surface 时,由于 SurfaceHolder.Callback 的存在,它将在代码中的触发调用 surfaceCreated 方法。此时可以通过调用 Camera 类上的静态方法 open 获得 Camera 对象。 Camera camera; public void surfaceCreated(SurfaceHolder holder) { camera = Camera.open(); Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 28 随后,我们想要将预览显示设置为正在使用的 SurfaceHolder,它通过回调提供给我们 的方法。需要将该方法包装在 try...catch 块中,因为它可能会抛出 IOException。如果发生 了这种情况,那么我们会希望释放该 Camera 对象;否则,它将绑定摄像头的硬件资源, 使其不能用于其他应用程序。 try { camera.setPreviewDisplay(holder); } catch (IOException exception) { camera.release(); } 最后,启动摄像头预览。 camera.startPreview(); } 相应地,在 surfaceDestroyed 中也需要释放该 Camera 对象。我们将首先调用 stopPreview,以确保应该释放的资源都被清理。 public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); } 运行这段代码,您可能会发现预览有些奇怪。它会逆时针旋转预览图像 90°,如图 2-1 所示。 图 2-1 旋转 90°的摄像头预览 Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 29 产生这种旋转的原因是 Camera 对象假定方向是水平或横向模式。修正旋转的最简单 方法是使活动以横向模式显示。为此,可以在活动的 onCreate 方法中添加以下代码。 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 现在摄像头预览将会正确地显示,如图 2-2 所示。但是,我们的应用程序现在被限定 在横向模式。 图 2-2 横向模式的摄像头预览 1. 设置 Camera 对象的参数 前面提及,Camera 类有一个嵌套的 Camera.Parameters 类。这个类有一系列重要的属 性或设置,可以用来改变 Camera 对象运作的方式。其中一个现在能够帮助我们的参数可 用来处理在预览时遇到的旋转/横向问题。 可以对 Camera 对象使用的 Parameters 做如下修改: Camera.Parameters parameters = camera.getParameters(); parameters.set("some parameter", "some value"); // 或者 parameters.set("some parameter", some_int); camera.setParameters(parameters); 此处有两个不同的通用 Parameters.set 方法。第一个方法的参数名称和值都采用字符 串,而第二个方法的参数名称为字符串,但是值为整数。 应该在创建 Camera 对象和指定它的预览 Surface 之后立即在 surfaceCreated 方法中设 置 Parameters。 以下代码展示了如何使用 Parameters 请求 Camera 对象采用纵向方向而非横向方向。 public void surfaceCreated(SurfaceHolder holder) { camera = Camera.open(); Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 30 try { Camera.Parameters parameters = camera.getParameters(); if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { //这是一个众所周知但未文档化的特性 parameters.set("orientation", "portrait"); //对于 Android 2.2 及以上版本 //camera.setDisplayOrientation(90); //对于 Android 2.2 及以上版本取消注释 //parameters.setRotation(90); } else { //这是一个众所周知但未文档化的特性 parameters.set("orientation", "landscape"); //对于 Android 2.2 及以上版本 //camera.setDisplayOrientation(0); //对于 Android 2.2 及以上版本取消注释 //parameters.setRotation(0); } camera.setParameters(parameters); camera.setPreviewDisplay(holder); } catch (IOException exception) { camera.release(); Log.v(LOGTAG,exception.getMessage()); } camera.startPreview(); } 上述代码首先检查设备配置(通过调用 Context.getResources().getConfiguration())以查看 当前的方向。如果方向不是横向模式,那么它设置 Camera.Parameters 的“orientation”值 为“portrait”。此外,调用 Camera.Parameters 的 setRotation 方法,并传入 90°的参数。该 方法在 API Level 5(2.0 版)和更高版本上可用,它实际上并不执行任何旋转;相反,它会告 诉 Camera 对象在 EXIF 数据中指定该图像应该旋转 90°显示。如果没有包含该信息,那 么在其他应用程序中查看该图像时,它可能会侧面显示。 注意:以上所示的通过使用 Camera.Parameters 修改 Camera 对象旋转的方法用于 Android 2.1 和更早的版本。在 Android 2.2 中引入了 Camera 类的一个新方法 setDisplayOrientation (int degrees)。该方法接受一个整数,表示图像应该旋转的度数。有效的度数为 0、90、180 和 270。 Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 31 大多数可以或应该修改的参数都有与它们相关联的特定方法。如同我们所看到的 setRotation 方法一样,这些方法遵循 Java 的获取器和设置器设计模式。例如,可以使用 setFlashMode(Camera.Parameters.FLASH_MODE_AUTO)来设置 Camera 对象的闪光灯模 式,同时可以使用 getFlashMode()获得它的当前值,而无须使用通用的 Parameters.set 方法。 从 Android 2.0 开始,存在一个可用于展示的有趣参数,使用该参数可以修改颜色效 果。对应的获取器和设置器方法是 getColorEffect 和 setColorEffect。同时还存在一个 getSupportedColorEffects 方法,它返回一个 String 对象的列表,对应特定设备上所支持的 各种效果。事实上,这种方法对于所有具有获取器和设置器方法的参数都存在,用于在使 用某个功能之前确保所请求的功能是可用的。 Camera.Parameters parameters = camera.getParameters(); List colorEffects = parameters.getSupportedColorEffects(); Iterator cei = colorEffects.iterator(); while (cei.hasNext()) { String currentEffect = cei.next(); Log.v("SNAPSHOT","Checking " + currentEffect); if (currentEffect.equals(Camera.Parameters.EFFECT_SOLARIZE)) { Log.v("SNAPSHOT","Using SOLARIZE"); parameters.setColorEffect(Camera.Parameters.EFFECT_SOLARIZE); break; } } Log.v("SNAPSHOT","Using Effect:" + parameters.getColorEffect()); camera.setParameters(parameters); 在上述代码中,首先查询 Camera.Parameters 对象,以通过 getSupportedColorEffects 方 法查看所支持的效果列表。然后,使用迭代器循环查询该效果列表,并判断其中是否有一 个效果能够匹配我们想要的效果,在当前情况下是 Camera.Parameters.EFFECT_SOLARIZE。 如果该效果出现在列表中,那么它是获得支持的,我们可以继续操作,在 Camera.Parameters 对象上调用 setColorEffect,并传入 EFFECT_SOLARIZE 常量。图 2-3 显示了使用中的 Camera. Parameters.EFFECT_SOLARIZE 效果。 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 32 图 2-3 来自摄像头的过度曝光预览图像 其他可能的效果也以常量的形式在 Camera.Parameters 类中列出。 ● EFFECT_NONE ● EFFECT_MONO ● EFFECT_NEGATIVE ● EFFECT_SOLARIZE ● EFFECT_SEPIA ● EFFECT_POSTERIZE ● EFFECT_WHITEBOARD ● EFFECT_BLACKBOARD ● EFFECT_AQUA 还存在用于抗条带(antibanding)、闪光灯模式(flash mode)、聚焦模式(focus mode),情 景模式(scene mode)及白平衡(white balance)等参数的类似常量。 2. 更改摄像头预览大小 另一个在 Camera.Parameters 中特别有用的设置是能够设置预览大小。与其他的设置一 样,首先将查询参数对象并获得所支持的值。在获得所支持的大小列表之后,就可以在设 置之前通过遍历它来确保所想要的大小是否获得支持。 在这个示例中,我们不是指定一个精确的大小,而是选择接近但不超过一对常量的大 小。图 2-4 展示了这个示例的输出。 ... public static final int LARGEST_WIDTH = 200; public static final int LARGEST_HEIGHT= 200; ... Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 33 图 2-4 以较小的预览大小进行 Camera 预览 与所有的 Camera.Parameters 一样,在已经打开 Camera 对象并设置它的预览显示 Surface 之后,就可以在 surfaceCreated 中获取和设置它们。 public void surfaceCreated(SurfaceHolder holder) { camera = Camera.open(); try { camera.setPreviewDisplay(holder); Camera.Parameters parameters = camera.getParameters(); 我们将采用以下两个变量来记录小于但最接近上述常量的值。 int bestWidth = 0; int bestHeight = 0; 然后,就可以获得设备所支持的所有大小的列表。这将返回一个 Camera.Size 对象的 列表,可以对其进行循环遍历。 List previewSizes = parameters. getSupportedPreviewSizes(); if (previewSizes.size() > 1) { Iterator cei = previewSizes.iterator(); while (cei.hasNext()) { Camera.Size aSize = cei.next(); 如果该列表中的当前大小大于保存的最佳大小,并且小于或等于 LARGEST_WIDTH 和 LARGEST_HEIGHT 常量,那么将在 bestWidth 和 bestHeight 变量中保存这个高度和宽 度并继续检查。 Log.v("SNAPSHOT","Checking " + aSize.width + " x " + aSize.height); if (aSize.width > bestWidth && aSize.width <= LARGEST_WIDTH && aSize.height > bestHeight && aSize.height <= LARGEST_HEIGHT) { // 迄今为止,它是最大的大小,且不超过屏幕尺寸 bestWidth = aSize.width; Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 34 bestHeight = aSize.height; } } 在遍历完所有支持的大小之后,必须确保获得了所需要的值。如果 bestHeight 和 bestWidth 变量等于 0,那么没有发现任何与我们的需要相匹配的大小,或者只存在一种支 持的大小,从而不应采取任何操作。反之,如果它们有值,那么将使用 bestWidth 和 bestHeight 变量调用 Camera.Parameters 对象上的 setPreviewSize 方法。 另外,还需要告诉摄像头预览 SurfaceView 对象(即 cameraView)以该大小进行显示。 如果不这么做,那么 SurfaceView 不会改变大小,且来自摄像头的预览图像会扭曲或质量 非常低。 if (bestHeight != 0 && bestWidth != 0) { Log.v("SNAPSHOT", "Using " + bestWidth + " x " + bestHeight); parameters.setPreviewSize(bestWidth, bestHeight); cameraView.setLayoutParams(new LinearLayout.LayoutParams( bestWidth, bestHeight)); } } camera.setParameters(parameters); 在设置该参数之后,剩余的工作就是关闭 surfaceCreated 方法。 } catch (IOException exception) { camera.release(); } } 3. 捕获和保存图像 要采用 Camera 类捕获图像,必须调用 takePicture 方法。该方法接受 3 个或 4 个参数, 所有这些参数都是回调方法。takePicture 方法的最简单形式是将所有的参数都设置为 null。 尽管能够捕获照片,但是不能获得它的引用。因此,至少应该实现一种回调方法。一种最 安全的回调方法是 Camera.PictureCallback.onPictureTaken。它确保会被调用,并且在压缩 图像时被调用。为了利用该方法,我们将在活动中实现 Camera.PictureCallback,并添加一 个 onPictureTaken 方法。 public class SnapShot extends Activity implements SurfaceHolder.Callback, Camera.PictureCallback { public void onPictureTaken(byte[] data, Camera camera) { } 该 onPictureTaken 方法有两个参数:第一个是实际的 JPEG 图像数据的字节数组,第 二个是捕获该图像的 Camera 对象的引用。 Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 35 由于给定了实际的 JPEG 数据,因此为了保存它,只需要将其写入磁盘的某个位置。 正如我们已经知道的那样,可以利用 MediaStore 指定它的位置和元数据。 当执行onPictureTaken方法时,可以调用Camera对象上的startPreview。当调用takePicture 方法时预览已经自动暂停,并且这个方法会告诉我们,现在可以安全地重新启动它。 public void onPictureTaken(byte[] data, Camera camera) { Uri imageFileUri = getContentResolver().insert(Media.EXTERNAL_ CONTENT_URI, new ContentValues()); try { OutputStream imageFileOS = getContentResolver(). openOutputStream(imageFileUri); imageFileOS.write(data); imageFileOS.flush(); imageFileOS.close(); }catch (FileNotFoundException e) { } catch (IOException e) { } camera.startPreview(); } 上述的代码片段向 MediaStore 中插入了一条新记录,并返回一个 URI。然后,利用这 个 URI 可以获得一个 OutputStream,用于写入 JPEG 数据。这将在 MediaStore 指定的位置 中创建一个文件,并将它链接到新的记录。 如果后面想要更新存储在 MediaStore 记录中的元数据,那么如同第 1 章所描述的一样, 可以利用一个新的 ContentValues 对象对记录进行更新。 ContentValues contentValues = new ContentValues(3); contentValues.put(Media.DISPLAY_NAME, "This is a test title"); contentValues.put(Media.DESCRIPTION, "This is a test description"); getContentResolver().update(imageFileUri,contentValues,null,null); 最后,必须实际调用 Camera.takePicture。为此,需要设置预览屏幕为“可单击 (clickable)”,同时在 onClick 方法中完成照相。 在活动中将实现一个 OnClickListener,并设置 SurfaceView 的 onClickListener 为活动 本身。然后,使用setClickable(true)设置SurfaceView为“可单击”。另外,需要设置SurfaceView 为“可聚焦(focusable)”。默认情况下 SurfaceView 不可聚焦,因此必须使用 setFocusable(true) 对它进行显式的设置。同样,当处于“触摸模式”时,通常会禁用焦点,所以必须使用 setFocusInTouchMode(true)对其进行显式的设置,使这种情况不会发生。 public class SnapShot extends Activity implements OnClickListener, SurfaceHolder.Callback, Camera.PictureCallback { ... public void onCreate(Bundle savedInstanceState) { Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 36 ... cameraView.setFocusable(true); cameraView.setFocusableInTouchMode(true); cameraView.setClickable(true); cameraView.setOnClickListener(this); } public void onClick(View v) { camera.takePicture(null, null, null, this); } 4. 其他的 Camera 回调方法 除了 Camera.PictureCallback 之外,还有其他一些值得提及的回调方法。 ● Camera.PreviewCallback:定义了 onPreviewFrame(byte[] data, Camera camera) 方法, 当存在预览帧(preview frame)时调用该方法。可以传入保存当前图像像素的字节数 组。在 Camera 对象上,有 3 种不同的方式使用这个回调: · setPreviewCallback(Camera.PreviewCallback):使用此方法注册一个 Camera. PreviewCallback,这将确保在屏幕上显示一个新的预览帧时调用 onPreviewFrame 方法。传递到 onPreviewFrame 方法中的数据字节数组最有可能采用 YUV 格式。 但是,Android 2.2 是第一个包含了 YUV 格式解码器(YuvImage)的版本;在以 前的版本中,必须手动完成解码。 · setOneShotPreviewCallback(Camera.PreviewCallback):利用 Camera 对象上的这 个方法注册 Camera.PreviewCallback,从而当下一幅预览图像可用时调用一次 onPreviewFrame。同样,传递到 onPreviewFrame 方法的预览图像数据最有可能采 用 YUV 格式。可以通过使用 ImageFormat 中的常量检查 Camera. getParameters(). getPreviewFormat()返回的结果来确定这一点。 · setPreviewCallbackWithBuffer(Camera.PreviewCallback):在 Android 2.2 中引入 了该方法,其与 setPreviewCallback 的工作方式相同,但要求指定一个字节数 组作为缓冲区,用于预览图像数据。这是为了能够更好地管理处理预览图像时 使用的内存。 ● Camera.AutoFocusCallback:定义了 onAutoFocus 方法,当完成一个自动聚焦活动 时调用它。通过传入此回调接口的一个实例,在调用 Camera 对象上的 autoFocus 方法时会触发自动聚焦。 ● Camera.ErrorCallback:定义了 onError 方法,当发生一个 Camera 错误时调用它。 有两个常量可用于与传入的错误代码进行比较:CAMERA_ERROR_UNKNOWN 和 CAMERA_ERROR_SERVER_DIED。 ● Camera.OnZoomChangeListener:定义了 onZoomChange 方法,当正在进行或完成 “平滑缩放”(慢慢缩小或放大)时调用它。在 Android 2.2 (API Level 8)中引入了这 个类和方法。 ● Camera.ShutterCallback:定义了 onShutter 方法,当捕获图像时立刻调用它。 Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 37 2.1.4 汇总 让我们看一下完整的示例。下面编写的代码可在 Android 2.2 和更高版本上运行,但是 只须稍做改动,这段代码应该就能够在 Android 1.6 和更高版本上运行。在注释中特别注明 了要求高于 1.6 版本的部分。 package com.apress.proandroidmedia.ch2.snapshot; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.List; import android.app.Activity; import android.content.ContentValues; import android.content.res.Configuration; import android.hardware.Camera; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore.Images.Media; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast; public class SnapShot extends Activity implements OnClickListener, SurfaceHolder.Callback, Camera.PictureCallback { SurfaceView cameraView; SurfaceHolder surfaceHolder; Camera camera; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); cameraView = (SurfaceView) this.findViewById(R.id.CameraView); surfaceHolder = cameraView.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.addCallback(this); cameraView.setFocusable(true); cameraView.setFocusableInTouchMode(true); Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 38 cameraView.setClickable(true); cameraView.setOnClickListener(this); } public void onClick(View v) { camera.takePicture(null, null, this); } 随后是之前所描述的 onPictureTaken 方法。 public void onPictureTaken(byte[] data, Camera camera) { Uri imageFileUri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, new ContentValues()); try { OutputStream imageFileOS = getContentResolver().openOutputStream(imageFileUri); imageFileOS.write(data); imageFileOS.flush(); imageFileOS.close(); } catch (FileNotFoundException e) { Toast t = Toast.makeText(this,e.getMessage(), Toast.LENGTH_SHORT); t.show(); } catch (IOException e) { Toast t = Toast.makeText(this,e.getMessage(), Toast.LENGTH_SHORT); t.show(); } camera.startPreview(); } 最后,需要使用各种 SurfaceHolder.Callback 方法,在其中可以建立 Camera 对象。 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { camera.startPreview(); } public void surfaceCreated(SurfaceHolder holder) { camera = Camera.open(); try { camera.setPreviewDisplay(holder); Camera.Parameters parameters = camera.getParameters(); if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { parameters.set("orientation", "portrait"); // Android 2.2 和以上版本 camera.setDisplayOrientation(90); // Android 2.0 和以上版本 parameters.setRotation(90); } Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 39 // 用于 Android 2.0 和更高版本的效果 List colorEffects = parameters. getSupportedColorEffects(); Iterator cei = colorEffects.iterator(); while (cei.hasNext()) { String currentEffect = cei.next(); if (currentEffect.equals(Camera.Parameters. EFFECT_SOLARIZE)) { parameters.setColorEffect(Camera.Parameters. EFFECT_SOLARIZE);break; } } //结束 Android 2.0 和更高版本的效果 camera.setParameters(parameters); } catch (IOException exception) { camera.release(); } } public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); } } //结束活动 以上是 Snapshot 活动的代码。下面是该活动正在使用的布局 XML,它位于 res/layout/ main.xml 中。 最后,需要在 AndroidManifest.xml 文件中添加 CAMERA 权限。以下是整个代码清单。 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 40 这个示例包括了构建一个基于摄像头的定制应用程序的基本要素。接下来,让我们看 看如何扩展这个应用程序,实现在内置 Camera 应用程序中不存在的功能。 2.2 扩展定制的 Camera 应用程序 Android 上内置的 Camera 应用程序缺少几个基本功能,其中之一是在少量时间(例如 10 秒或 30 秒)之后照相的能力。此功能通常对于安装了三角架的摄像头非常有用。它能完 成的一件事就是摄影师能够建立一个场景,设置计时器,然后跑进该场景。 虽然不会经常这样使用移动电话,但是它在某些情况下将会非常有用。例如,当希望 给自己和在一起的某个人照相时,我们会喜欢这个功能。目前,当尝试这么做时会遇到困 难,此时我们看不到触摸屏界面,因为它与我们的脸部有一段距离。我们会在屏幕上到处 乱点,希望能够按到拍照按钮。 2.2.1 构建基于定时器的 Camera 应用程序 为了纠正刚才所描述的情况,可以在照相时添加一个时间延迟。接下来更新 SnapShot 示例,使得在按下按钮 10 秒钟后开始照相。 为了实现这个功能,需要使用一个类似 java.util.Timer 的对象。但是,在 Android 中使 用 Timer 对象会导致一些问题,因为它引入了一个单独的线程。为了使得单独的线程与 UI 交互,需要使用一个 Handler 对象在主线程中触发一个动作。 使用 Handler 对象的另一个用途是安排在将来发生的操作。Handler 对象所拥有的功能 使得我们不必使用 Timer 对象。 为了创建在将来执行某些动作的 Handler 对象,只须构建一个通用的 Handler 对象: Handler timerHandler = new Handler(); 然后,必须创建一个 Runnable 对象,在其 run 方法中包含后面将要发生的动作。在当 前情况下,我们希望这个动作在 10 秒钟之后发生,触发照相操作: Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 41 Runnable timerTask = new Runnable() { public void run() { camera.takePicture(null,null,null,TimerSnapShot.this); } }; 现在当单击一个按钮时,只需要这样安排操作: timerHandler.postDelayed(timerTask, 10000); 这将通知 timerHandler 在 10 秒钟(10000 毫秒)之后调用 timerTask 方法。 下面的示例将创建一个 Handler 对象,并使它每秒钟调用一个方法。通过采用这种方 式,可以在屏幕上为用户提供倒计时。 package com.apress.proandroidmedia.ch2.timersnapshot; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.List; import android.app.Activity; import android.content.ContentValues; import android.content.res.Configuration; import android.hardware.Camera; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.provider.MediaStore.Images.Media; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; public class TimerSnapShot extends Activity implements OnClickListener, SurfaceHolder.Callback, Camera.PictureCallback { SurfaceView cameraView; SurfaceHolder surfaceHolder; Camera camera; 这种活动非常类似于 SnapShot 活动。我们需要添加一个 Button 对象来触发倒计时的 开始,并且添加一个 TextView 对象来显示倒计时。 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 42 Button startButton; TextView countdownTextView; 还需要一个 Handler 对象,在当前情况下是 timerUpdateHandler;需要一个布尔值来帮 助我们跟踪计时器是否已经开始(timerRunning);同时还要有一个整数(currentTime)用于跟 踪倒计时。 Handler timerUpdateHandler; boolean timerRunning = false; int currentTime = 10; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); cameraView = (SurfaceView) this.findViewById(R.id.CameraView); surfaceHolder = cameraView.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.addCallback(this); 接下来,获得新 UI 元素(在布局 XML 中定义)的引用,并使我们的活动作为 Button 对 象的 OnClickListener。可以这么做是因为该活动实现了 OnClickListener。 countdownTextView = (TextView) findViewById(R.id. CountDownTextView); startButton = (Button) findViewById(R.id.CountDownButton); startButton.setOnClickListener(this); 在 onCreate 方法中做的最后一件事情是实例化 Handler 对象。 timerUpdateHandler = new Handler(); } 在按下 startButton Button 时将调用 onClick 方法。通过检查 timerRunning 布尔值, 可 以判断计时器例程是否还没有运行;如果还没有运行,那么立即通过 Handler 对象 (timerUpdateHandler)调用下面将描述的 timerUpdateTask Runnable 对象。 public void onClick(View v) { if (!timerRunning) { timerRunning = true; timerUpdateHandler.post(timerUpdateTask); } } 下面的代码是称为 timerUpdateTask 的 Runnable 对象。该对象包含 run 方法,通过 timer- Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 43 UpdateHandler 对象触发它。 private Runnable timerUpdateTask = new Runnable() { public void run() { 如果 currentTime(保存倒计时的整数)大于 1,那么将对它进行递减,同时安排 1 秒钟 后再次调用该 Handler 对象。 if (currentTime > 1) { currentTime--; timerUpdateHandler.postDelayed(timerUpdateTask, 1000); } else { 如果 currentTime 不再大于 1,那么将实际触发摄像头以使其照相,并重置所有的跟踪 变量。 camera.takePicture(null,null ,TimerSnapShot.this); timerRunning = false; currentTime = 10; } 无论如何,我们都将更新 TextView 对象,使得它在照相之前一直显示当前的时间。 countdownTextView.setText(""+currentTime); } }; 该活动的其余动作与前面所述的 SnapShot 示例基本相同。 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { camera.startPreview(); } public void surfaceCreated(SurfaceHolder holder) { camera = Camera.open(); try { camera.setPreviewDisplay(holder); Camera.Parameters parameters = camera.getParameters(); if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { parameters.set("orientation", "portrait"); // 对于 Android 2.2 及以上版本 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 44 camera.setDisplayOrientation(90); // 对于 Android 2.0 及以上版本 parameters.setRotation(90); } camera.setParameters(parameters); } catch (IOException exception) { camera.release(); } } public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); } public void onPictureTaken(byte[] data, Camera camera) { Uri imageFileUri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, new ContentValues()); try { OutputStream imageFileOS = getContentResolver().openOutputStream(imageFileUri); imageFileOS.write(data); imageFileOS.flush(); imageFileOS.close(); Toast t = Toast.makeText(this,"Saved JPEG!", Toast.LENGTH_SHORT); t.show(); } catch (FileNotFoundException e) { Toast t = Toast.makeText(this,e.getMessage(), Toast.LENGTH_SHORT); t.show(); } catch (IOException e) { Toast t = Toast.makeText(this,e.getMessage(), Toast.LENGTH_SHORT); t.show(); } camera.startPreview(); } } 然而,布局 XML 稍微有点不同。这个应用程序在一个包含 LinearLayout 的 FrameLayout 中显示摄像头预览 SurfaceView,其中包含 TextView 来显示倒计时,并且包含 Button 来触 发倒计时。该 FrameLayout 中的所有对象均左上角对齐,并且依次堆叠。采用这种方式, TextView 和 Button 均显示在摄像头预览的顶部。 最后,需要确保在 AndroidManifest.xml 文件中包含 CAMERA 权限。图 2-5 显示了最 终的效果。 图 2-5 采用倒计时计时器的 Camera 应用程序 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 46 2.2.2 构建时间推移摄影应用程序 我们都已看过时间推移摄影的完美范例。它是在一段时间内拍摄多张照片的过程。它 可能是每分钟、每小时甚至每周拍摄一张照片。通过查看一系列时间推移的照片,可以了 解事物如何随时间而变化。一个可能的示例是观察一幢建筑物如何建造,另一个可能的示 例是记录一朵花如何成长和盛开。 由于已经构建了一个基于定时器的 Camera 应用程序,因此将它更新成一个时间推移 应用程序非常简单。 首先需要改变一些实例变量并加入一个常量。 ... public class TimelapseSnapShot extends Activity implements OnClickListener, SurfaceHolder.Callback, Camera.PictureCallback { SurfaceView cameraView; SurfaceHolder surfaceHolder; Camera camera; 需要将 Button 重命名为 startStopButton,因为它现在将处理两个动作,同时将对其余 的变量名执行几个细小的更新。 Button startStopButton; TextView countdownTextView; Handler timerUpdateHandler; boolean timelapseRunning = false; 与前述的示例一样,currentTime 整数将用于累加两次照相之间的时间量,而不是从总 延迟递减。将一个称为 SECONDS_BETWEEN_PHOTOS 的常量设置为 60。正如其名称所 暗示的那样,该常量将用于设定拍摄照片之间等待的时间。 int currentTime = 0; public static final int SECONDS_BETWEEN_PHOTOS = 60; // 1 分钟 onCreate 方法大体上保持相同——只是将引用新的变量名称。 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); cameraView = (SurfaceView) this.findViewById(R.id.CameraView); surfaceHolder = cameraView.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.addCallback(this); countdownTextView = (TextView) findViewById(R.id. CountDownTextView); startStopButton = (Button) findViewById(R.id.CountDownButton); Be From--http://bmbook.5d6d.com/ 第 2 章 构建定制的 Camera 应用程序 47 startStopButton.setOnClickListener(this); timerUpdateHandler = new Handler(); } 将一个基于定时器的应用程序转换成一个时间推移应用程序的批量更改将出现 onClick 方法中,这是在按下按钮时由 Handler 安排的在 Runnable 方法中发生的操作。 onClick 方法首先检查时间推移进程目前是否正在运行(以前是否已经按下按钮),如果 还没有运行,那么将它设置为运行,并以 Runnable(此处描述了该对象)作为参数调用这个 Handler 的 post 方法。 如果时间推移进程正在运行,那么按下该按钮意味着停止它,从而调用该 Handler(即 timerUpdateHandler)上的 removeCallbacks 方法。这将清除作为参数传递的 Runnable 上所有 等待的调用。 public void onClick(View v) { if (!timelapseRunning) { startStopButton.setText("Stop"); timelapseRunning = true; timerUpdateHandler.post(timerUpdateTask); } else { startStopButton.setText("Start"); timelapseRunning = false; timerUpdateHandler.removeCallbacks(timerUpdateTask);} } 当处理一个 Handler 以执行计划时,我们将拥有一个 Runnable 对象,该 Handler 将在 到达指定时间时调用它。该 Handler 的 run 方法首先检查 currentTime 整数是否小于想要在 拍摄照片之间等待的秒数(SECONDS_BETWEEN_PHOTOS)。如果小于,那么只须简单地 增加 currentTime。如果 currentTime 超过等待的时间,那么会通知 Camera 应用程序拍照, 并将 currentTime 设置回 0,使其继续累加。 每次发生这些情况之后,只须使用 currentTime 更新 TextView,并安排一个在下一秒 指向自身的调用。 private Runnable timerUpdateTask = new Runnable() { public void run() { if (currentTime < SECONDS_BETWEEN_PHOTOS) { currentTime++; } else { camera.takePicture(null,null,null,TimelapseSnapShot.this); currentTime = 0; } timerUpdateHandler.postDelayed(timerUpdateTask, 1000); countdownTextView.setText(""+currentTime); } Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 48 }; 当然,此示例的 res/layout/main.xml 界面和 AndroidManifest.xml 与倒计时计时器的版 本相同。 2.3 本章小结 可能有无数的理由需要建立自己的 Camera 应用程序,而不仅仅是在自已的应用中利 用内置的应用程序。我们可以随心所欲地实现各种 Camera 应用程序,从简单的倒计时拍 照应用程序到自己的时间推移系统,以及更多的应用程序。 接下来将介绍如何处理已经捕获的图像。 Be From--http://bmbook.5d6d.com/ 图像编辑和处理 随着手持设备变得越来越强大,许多曾经只在桌面计算机上存在的功能如今在移动设 备上也成为可能。虽然图像编辑和处理曾经是诸如 Photoshop 之类的桌面应用程序的功能 范畴,但是现在也可以在手机上实现这些功能。 本章将介绍如何处理捕获后的图像:了解如何通过旋转和缩放来改变它们,如何调整 亮度和对比度,以及如何合成两幅或更多的图像。 3.1 使用内置 Gallery 应用程序选择图像 为了使用一个预装 Android 应用程序中存在的功能,利用意图通常是最快捷的方式。 出于介绍本章中示例的目的,让我们看看如何利用内置的 Gallery(图像库)应用程序选择希 望使用的图像。 我们将要使用的意图是一个通用的 Intent.ACTION_PICK,它通知 Android:我们想要 选择一块数据。同时,我们还提供了一个将要从中获取数据的 URI。在当前情况下,使用 android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,这意味着会选择通 过使用 MediaStore 存储在 SD 卡上的图像。 Intent choosePictureIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 当触发这个意图时,它会以用户能够选择一幅图像的模式启动 Gallery 应用程序。 与通常从意图返回一样,在用户选中图像之后将触发 onActivityResult 方法。在返回的 意图数据中,将返回所选择图像的 URI。 onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); 第 章 3 0 Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 50 if (resultCode == RESULT_OK) { Uri imageFileUri = intent.getData(); } } 以下是完整的示例: package com.apress.proandroidmedia.ch3.choosepicture; import java.io.FileNotFoundException; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Display; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; 我们的活动会响应由按钮触发的单击事件,因此将实现 OnClickListener。在 onCreate 方法中,使用通常的 findViewById 方法访问在布局 XML 中定义的必要 UI 元素。 public class ChoosePicture extends Activity implements OnClickListener { ImageView chosenImageView; Button choosePicture; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); chosenImageView = (ImageView) this.findViewById(R.id.ChosenImageView); choosePicture = (Button) this.findViewById(R.id.ChoosePictureButton); choosePicture.setOnClickListener(this); } 随后是 onClick 方法,它将响应 choosePicture 按钮的单击事件,该按钮在应用程序启 动时显示,如图 3-1 所示。这个方法创建意图,其将触发 Gallery 应用程序,使其以允许用 户选择一张图片的模式启动,如图 3-2 所示。 public void onClick(View v) { Intent choosePictureIntent = new Intent(Intent.ACTION_PICK, Be From--http://bmbook.5d6d.com/ 第 3 章 图像编辑和处理 51 android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(choosePictureIntent, 0); } 图 3-1 choosePicture 按钮,当应用程序首次 启动时显示 图 3-2 由 ACTION_PICK 意图触发时 Gallery 应用程序 的视图,其中提示用户选择一幅图像;Gallery 应用程序的 UI 可能因设备而有所不同 当 Gallery 应用程序在用户选择图像之后返回时,将调用 onActivityResult 方法。在传 递到意图的数据中,可以得到所选择图像的 URI。 protected void onActivityResult(int requestCode, int resultCode, Intent intent){ super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { Uri imageFileUri = intent.getData(); 由于返回的图像可能太大而无法完全加载到内存中,因此在加载图像时将使用在第 1 章中介绍过的技术对其大小进行调整。整数 dw 和 dh 分别表示最大宽度和高度。最大高度 小于屏幕高度的一半,因为最终将会以垂直对齐的方式显示两幅图像。 Display currentDisplay = getWindowManager().getDefaultDisplay(); int dw = currentDisplay.getWidth(); int dh = currentDisplay.getHeight()/2 - 100; try { //加载图像的尺寸而非图像本身 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Be From--http://bmbook.5d6d.com/ Android 多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用 52 Bitmap bmp = BitmapFactory.decodeStream(getContentResolver(). openInputStream(imageFileUri), null, bmpFactoryOptions); int heightRatio = (int)Math.ceil(bmpFactoryOptions. outHeight/(float)dh); int widthRatio = (int)Math.ceil(bmpFactoryOptions. outWidth/(float)dw); if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { bmpFactoryOptions.inSampleSize = heightRatio; } else { bmpFactoryOptions.inSampleSize = widthRatio; } } bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeStream(getContentResolver(). openInputStream(imageFileUri), null, bmpFactoryOptions); choosenImageView.setImageBitmap(bmp); } catch (FileNotFoundException e) { Log.v("ERROR",e.toString()); } } } } 需要在项目的 layout/main.xml 文件中包含如下内容:
还剩80页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 6 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

小虎子

贡献于2013-03-20

下载需要 6 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf