Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android N for Developers

$
0
0

Android N 仍处于活动的开发状态,但现在您可以将其作为 N Developer Preview 的一部分进行试用。 以下部分重点介绍面向开发者的一些新功能。

请务必查阅行为变更以了解平台变更可能影响您的应用的领域,看看开发者指南,了解有关关键功能的更多信息,并下载 API 参考以获取新 API 的详细信息。

多窗口支持


在 Android N 中,我们为该平台引入了一个新的而且非常需要的多任务处理功能 — 多窗口支持。

现在,用户可以一次在屏幕上打开两个应用。

  • 在运行 Android N 的手机和平板电脑上,用户可以并排运行两个应用,或者处于分屏模式时一个应用位于另一个应用之上。 用户可以通过拖动两个应用之间的分隔线来调整应用。
  • 在 Android TV 设备上,应用可以将自身置于画中画模式,从而让它们可以在用户浏览或与其他应用交互时继续显示内容。

图 1. 在分屏模式下运行的应用。

多窗口支持为您提供新的吸引用户方式,特别是在平板电脑和其他更大屏幕的设备上。 您甚至可以在您的应用中启用拖放,从而使用户可以方便地将内容拖放到您的应用或从其中拖出内容—这是一个非常好的增强用户体验的方式。

向您的应用添加多窗口支持并配置多窗口显示的处理方式非常简单。 例如,您可以指定您的 Activity 允许的最小尺寸,从而防止用户将 Activity 调整到该尺寸以下。 您还可以为应用禁用多窗口显示,这可确保系统将仅以全屏模式显示应用。

如需了解详细信息,请参阅多窗口支持开发者文档。

通知增强功能


在 Android N 中,我们重新设计了通知,使其更易于使用并且速度更快。 部分变更包括:

  • 模板更新:我们正在更新通知模板,新强调了英雄形象和化身。 开发者将能够充分利用新模板,只需进行少量的代码调整。
  • 消息样式自定义:您可以自定义更多与您的使用 MessageStyle 类的通知相关的用户界面标签。 您可以配置消息、会话标题和内容视图。
  • 捆绑通知:系统可以将消息组合在一起(例如,按消息主题)并显示组。 用户可以适当地进行 Dismiss 或 Archive 等操作。 如果您已实现 Android Wear 的通知,那么您已经很熟悉此模型。
  • 直接回复:对于实时通信应用,Android 系统支持内联回复,以便用户可以直接在通知界面中快速回复短信。
  • 自定义视图:两个新的 API 让您在通知中使用自定义视图时可以充分利用系统装饰元素,如通知标题和操作。

图 2. 绑定的通知和直接回复。

如需了解如何实现新功能的信息,请参阅通知指南。

个人资料指导的 JIT/AOT 编译


在 Android N 中,我们添加了 Just in Time (JIT) 编译器,对 ART 进行代码分析,让它可以在应用运行时持续提升 Android 应用的性能。 JIT 编译器对 Android 运行组件当前的 Ahead of Time (AOT) 编译器进行了补充,有助于提升运行时性能,节省存储空间,加快应用更新和系统更新速度。

个人资料指导的编译让 Android 运行组件能够根据应用的实际使用以及设备上的情况管理每个应用的 AOT/JIT 编译。 例如,Android 运行组件维护每个应用的热方法的个人资料,并且可以预编译和缓存这些方法以实现最佳性能。 对于应用的其他部分,在实际使用之前不会进行编译。

除提升应用的关键部分的性能外,个人资料指导的编译还有助于减少整个 RAM 占用,包括关联的二进制文件。 此功能对于低内存设备非常尤其重要。

Android 运行组件在管理个人资料指导的编译时,可最大程度降低对设备电池的影响。 仅当设备处于空闲状态和充电时才进行编译,从而可以通过提前执行该工作节约时间和省电。

快速的应用安装路径


Android 运行组件的 JIT 编译器最实际的好处之一是应用安装和系统更新的速度。 即使在 Android 6.0 中需要几分钟进行优化和安装的大型应用,现在只需几秒钟就可以完成安装。 系统更新也变得更快,因为省去了优化步骤。

随时随地低电耗模式...


Android 6.0 推出了低电耗模式,即设备处于空闲状态时,通过推迟应用的 CPU 和网络活动以实现省电目的的系统模式,例如,设备放在桌上或抽屉里时。

现在,在 Android N 中,低电耗模式又前进了一步,随时随地可以省电。只要屏幕关闭了一段时间,且设备未插入电源,低电耗模式就会对应用使用熟悉的 CPU 和网络限制。这意味着用户即使将设备放入口袋里也可以省电。

图 3. 低电耗模式现在应用限制以延长电池寿命,即使设备未处于静止状态。

屏幕关闭片刻后,设备在使用电池时,低电耗模式将限制网络访问,同时延迟作业和同步。 在短暂的维护时间范围后,其允许应用访问网络,并执行延迟的作业/同步。 打开屏幕或将设备插入电源会使设备退出低电耗模式。

当设备再次处于静止状态时,屏幕关闭且使用电池一段时间,低电耗模式针对 PowerManager.WakeLockAlarmManager 警报和 GPS/Wi-Fi 扫描应用完整 CPU 和网络限制。

无论设备是否处于运动状态,将应用调整到低电耗模式的最佳做法均相同,因此,如果您已更新应用以妥善处理低电耗模式,则一切就绪。 如果不是,请立即开始将应用调整到低电耗模式

Project Svelte:后台优化


Project Svelte 在持续改善,以最大程度减少生态系统中一系列 Android 设备中系统和应用使用的 RAM。 在 Android N 中,Project Svelte 注重优化在后台中运行应用的方式。

后台处理是大多数应用的一个重要部分。处理得当,可让您实现非常棒的用户体验 — 即时、快速和情境感知。如果处理不得当,后台处理会毫无必要地消耗 RAM(和电池),同时影响其他应用的系统性能。

自 Android 5.0 发布以来,JobScheduler 已成为执行后台工作的首选方式,其工作方式有利于用户。 应用可以在安排作业的同时允许系统基于内存、电源和连接情况进行优化。 JobScheduler 可实现控制和简洁性,我们想要所有应用都使用它。

另一个非常好的选择是 GCMNetworkManager(Google Play 服务的一部分),其在旧版 Android 中提供类似的作业安排和兼容性。

我们在继续扩展 JobScheduler 和 GCMNetworkManager,以符合多个用例 — 例如,在 Android N 中,现在,您可以基于内容提供程序中的更改安排后台工作。 同时,我们开始弃用一些较旧的模式,这些模式会降低系统性能,特别是低内存设备的系统性能。

在 Android N 中,我们删除了三个常用隐式广播 — CONNECTIVITY_ACTIONACTION_NEW_PICTURE 和 ACTION_NEW_VIDEO — 因为这些广播可能会一次唤醒多个应用的后台进程,同时会耗尽内存和电池。 如果您的应用收到这些广播,请充分利用 N Developer Preview 以迁移到 JobScheduler 和相关的 API。

如需了解详情,请查看后台优化文档。

Data Saver


图 4. 设置中的 Data Saver

在移动设备的整个生命周期,蜂窝数据计划的成本通常会超出设备本身的成本。 对于许多用户而言,蜂窝数据是他们想要节省的昂贵资源。

Android N 推出了 Data Saver 模式,这是一项新的系统服务,有助于减少应用使用的蜂窝数据,无论是在漫游,账单周期即将结束,还是使用少量的预付费数据包。 Data Saver 让用户可以控制应用使用蜂窝数据的方式,同时让开发者打开 Data Saver 时可以提供更多有效的服务。

用户在 Settings 中启用 Data Saver 且设备位于按流量计费的网络上时,系统屏蔽后台流量消耗,同时指示应用在前台尽可能使用较少的流量 — 例如,通过限制用于流媒体服务的比特率、降低图片质量、延迟最佳的预缓冲等方法来实现。 用户可以将特定应用加入白名单以允许后台按流量的流量消耗,即使在打开 Data Saver 时也是如此。

Android N 扩展了 ConnectivityManager,以便为应用检索用户的 Data Saver 首选项监控首选项变更提供一种方式。 所有应用均应检查用户是否已启用 Data Saver 并努力限制前台和后台流量消耗。

Vulkan API


Android N 将一项新的 3D 渲染 API Vulkan™ 集成到平台中。就像 OpenGL™ ES 一样,Vulkan 是 3D 图形和渲染的一项开放标准,由 Khronos Group 维护。

Vulkan 是完全从零开始设计,以最小化驱动器中的 CPU 开销,并能让您的应用更直接地控制 GPU 操作。 Vulkan 还允许多个线程同时执行工作,如命令缓冲区构建,以获得更好的并行化。

Vulkan 开发工具和库都已卷入 Android NDK。它们包括:

  • 验证层(调试库)
  • SPIR-V 着色程序编译器
  • SPIR-V 运行时着色器编译库

Vulkan 仅适用于已启用 Vulkan 硬件的设备上的应用,如 Nexus 5X、Nexus 6P 和 Nexus Player。 我们正在与合作伙伴密切合作,以尽快使 Vulkan 能面向更多的设备。

如需要了解更多信息,请参阅 API 文档

Quick Settings Tile API


图 5. 通知栏中的快速设置图块。

“快速设置”通常用于直接从通知栏显示关键设置和操作,非常简单。 在 Android N 中,我们已扩展“快速设置”的范围,使其更加有用更方便。

我们为额外的“快速设置”图块添加了更多空间,用户可以通过向左或向右滑动跨分页的显示区域访问它们。 我们还让用户可以控制显示哪些“快速设置”图块以及显示的位置 — 用户可以通过拖放图块来添加或移动图块。

对于开发者,Android N 还添加了一个新的 API,从而让您可以定义自己的“快速设置”图块,使用户可以轻松访问您应用中的关键控件和操作。

对于急需或频繁使用的控件和操作,保留“快速设置”图块,且不应将其用作启动应用的快捷方式。

定义图块后,您可以将它们显示给用户,用户可通过拖放将图块添加到“快速设置”。

如需创建应用图块的更多信息,请参阅可下载的 API 参考中的文件android.service.quicksettings.Tile

号码屏蔽


Android N 现在支持在平台中进行号码屏蔽,提供框架 API,让服务提供商可以维护屏蔽的号码列表。 默认短信应用、默认手机应用和提供商应用可以对屏蔽的号码列表进行读取和写入操作。 其他应用则无法访问此列表。

通过使号码屏蔽成为平台的标准功能,Android 为应用提供一致的方式来支持广泛的设备上的号码屏蔽。 应用可以利用的其他优势包括:

  • 还会屏蔽已屏蔽的来电号码发出的短信
  • 通过 Backup & Restore(备份和还原)功能可以跨重置和设备保留屏蔽的号码
  • 多个应用可以使用相同的屏蔽号码列表

此外,通过 Android 的运营商应用集成表示运营商可以读取设备上屏蔽的号码列表,并为用户执行服务端屏蔽,以阻止不需要的来电和短信通过任何介质(如 VOIP 端点或转接电话)到达用户。

如需了解详细信息,请参阅可下载的 API 参考中的 android.provider.BlockedNumberContract

来电过滤


Android N 允许默认的手机应用过滤来电。手机应用执行此操作的方式是实现新的 CallScreeningService,该方法允许手机应用基于来电的 Call.Details执行大量操作,例如:

  • 拒绝来电
  • 不允许来电到达通话记录
  • 不向用户显示来电通知

如需了解详细信息,请参阅可下载的 API 参考中的 android.telecom.CallScreeningService

多区域设置支持、多语言


Android N 现在允许用户在设置中选择多个区域设置,以更好地支持双语用例。 应用可以使用新的 API 获取用户选择的区域设置,然后为多区域设置用户提供更成熟的用户体验 — 如以多个语言显示搜索结果,并且不会以用户了解的语言翻译网页。

除多区域设置支持外,Android N 还扩展了用户可用的语言范围。 它针对常用语言提供超过 25 种的变体,如英语、西班牙语、法语和阿拉伯语。 它还针对 100 多种新语言添加了部分支持。

应用可以通过调用 LocaleList.GetDefault() 获取用户设置的区域设置列表。 为支持扩展的区域设置数量,Android N 正在改变其解析资源的方式。 请务必使用新的资源解析逻辑测试和验证您的应用是否能如期运行。

如需有关新资源解析行为和应遵循的最佳做法的更多信息,请参阅多语言支持

新增的表情符号


Android N 引入更多表情符号和表情符号相关功能,包括肤色表情符号和支持变量选择符。 如果您的应用支持表情符号,请遵循以下准则,以便能充分利用这些表情符号相关功能优势。

  • 在插入之前,检查设备是否包含表情符号。 若要检查系统字体中有哪些表情符号,使用 hasGlyph(String) 方法。
  • 检查表情符号是否支持变量选择符。 变量选择符使您能够呈现一些彩色或黑白的表情符号。 在移动设备上,应用应呈现彩色的表情符号,而不是黑白的。但是,如果您的应用显示嵌入在文本中的表情符号,那应使用黑白变量。 若要确定表情符号是否有变量,使用变量选择符。 如需有关支持变量的字符的完整清单,请参阅变量的 Unicode 文档中的 表情符号变量序列部分。
  • 检查表情符号是否支持肤色。Android N 允许用户按照他们的喜好修改表情符号呈现的肤色。 键盘应用应为有多个肤色的表情符号提供可视化的指示,并应允许用户选择他们喜欢的肤色。 若要确定哪些系统表情符号有肤色修改器,使用 hasGlyph(String) 方法。 您可以通过读取 Unicode 文档来确定哪些表情符号使用肤色。

Android 中的 ICU4J API


Android N 目前在 Android 框架(位于 android.icu 软件包下)中提供 ICU4J API 的子集。 迁移很简单,主要是需要从 com.java.icu 命名空间更改为android.icu。 如果您已在您的应用中使用 ICU4J 捆绑包,切换到 Android 框架中提供的 android.icu API 可以大量节省 APK 大小。

如果要了解有关 Android ICU4J API 的更多信息,请参阅 ICU4J 支持

OpenGL™ ES 3.2 API


Android N 添加了框架接口和对 OpenGL ES 3.2 的平台支持,包括:

  • 来自 Android 扩展包 (AEP) 的所有扩展(EXT_texture_sRGB_decode 除外)。
  • 针对 HDR 的浮点帧缓冲和延迟着色。
  • BaseVertex 绘图调用可实现更好的批处理和流媒体服务。
  • 强大的缓冲区访问控制可减少 WebGL 开销。

Android N 上适用于 OpenGL ES 3.2 的框架 API 与 GLES32 类一起提供。 使用 OpenGL ES 3.2 时,请务必通过 <uses-feature> 标记和android:glEsVersion 属性在您的清单中声明要求。

如需了解有关使用 OpenGL ES 的信息,包括如何在运行时检查设备支持的 OpenGL ES 版本,请参阅 OpenGL ES API 指南

Android TV 录制


Android N 通过新的录制 API 添加了从 Android TV 输入服务录制和播放内容的功能。 构建在现有时移 API 之上,TV 输入服务可以控制能够录制的渠道数据、保存录制的会话的方式,同时可通过录制的内容管理用户交互。

如需了解详细信息,请参阅 Android TV 录制 API

Android for Work


Android for Work 针对运行 Android N 的设备添加了许多新功能和 API。部分重要内容如下— 有关变更的完整列表,请参阅 Android for Work 更新

工作资料安全性挑战

面向 N SDK 的个人资料所有者可以为在工作资料中运行的应用指定单独的安全性挑战。 当用户尝试打开任何工作应用时将显示工作挑战。 成功完成安全性挑战可解锁工作资料并将其解密(如果需要)。 对于个人资料所有者,ACTION_SET_NEW_PASSWORD 提示用户设置工作挑战,ACTION_SET_NEW_PARENT_PROFILE_PASSWORD 提示用户设置设备锁。

个人资料所有者可以使用 setPasswordQuality()setPasswordMinimumLength() 和相关方法针对工作挑战设置不同的密码策略(例如,PIN 必须多长,或是否可以使用指纹解锁个人资料)。 个人资料所有者还可以使用新的 getParentProfileInstance() 方法返回的 DevicePolicyManager 实例设置设备锁定。 此外,个人资料所有者可以使用新的 setOrganizationColor() 和 setOrganizationName() 方法针对工作挑战自定义凭据屏幕。

关闭工作

在有工作资料的设备上,用户可以切换工作模式。工作模式关闭时,管理的用户临时关闭,其禁用托管工作资料应用、后台同步和通知。 这包括个人资料所有者应用。 关闭工作模式时,系统显示永久状态图标,以提醒用户他们无法启动工作应用。 启动器指示该工作应用和小组件无法访问。

Always on VPN

设备所有者和个人资料所有者可以确保工作应用始终通过指定的 VPN 连接。 系统在设备启动后自动启动该 VPN。

新的 DevicePolicyManager 方法为 setAlwaysOnVpnPackage() 和 getAlwaysOnVpnPackage()

由于 VPN 服务无需应用交互即可由系统直接绑定,因此,VPN 客户端必须针对 Always on VPN 处理新的入口点。 和以前一样,由与操作匹配的 Intent 过滤器将服务指示给系统。android.net.VpnService

用户还可以使用 Settings>More>Vpn 在主要用户中手动设置实现 VPNService 方法的 Always on VPN 客户端。

自定义配置

应用可以用企业颜色和徽标来自定义个人资料所有者和设备所有者配置流程。DevicePolicyManager.EXTRA_PROVISIONING_MAIN_COLOR 自定义流程颜色。DevicePolicyManager.EXTRA_PROVISIONING_LOGO_URI 用企业徽标自定义流程。

无障碍增强功能


Android N 现在针对新的设备设置直接在欢迎屏幕上提供“Vision Settings”。 这使用户可以更容易发现和配置他们设备上的无障碍功能,包括放大手势、字体大小、显示屏尺寸和 TalkBack。

随着这些无障碍功能更为突出,在启用这些功能后,您的用户更可能试用您的应用。 请务必提前启用这些设置测试您的应用。 您可以通过 Settings > Accessibility 启用它们。

还是在 Android N 中,无障碍服务现在可以帮助具有动作障碍的用户触摸屏幕。 全新的 API 允许使用人脸追踪、眼球追踪、点扫描等功能构建服务,以满足这些用户的需求。

如需了解详细信息,请参阅可下载的 API 参考 中的 android.accessibilityservice.GestureDescription 

直接启动


直接启动可以缩短设备启动时间,让注册的应用具有有限的功能,即使在意外重启后。例如,如果当用户睡觉时加密的设备重启,那么注册的警报、消息和来电现在可以和往常一样继续通知用户。 这也意味着重启后无障碍服务会立即可用。

在 Android N 中,直接启动充分利用基于文件的加密,以针对系统和应用数据启用细化的加密策略。为系统和应用数据。系统针对选定的系统数据和显式注册的应用数据使用设备加密的存储。 默认情况下,凭据加密的存储可用于所有其他系统数据、用户数据、应用及应用数据。

启动时,系统在受限的模式中启动,仅访问设备加密的数据,不会对应用或数据进行常规访问。如果您有想要在此模式下运行的组件,您可以通过在清单文件中设置标记注册它们。 重启后,系统通过广播 LOCKED_BOOT_COMPLETED Intent 激活注册的组件。 系统确保注册的设备加密的应用数据在解锁前可用。 所有其他数据在用户确认锁定屏幕凭据进行解密前均不可用。

如需了解详细信息,请参阅直接启动

密钥认证


使用硬件支持的密钥库,可更安全地在 Android 设备上创建、存储和使用加密密钥。 它们可保护密钥免受 Linux 内核、潜在的 Android 漏洞的攻击,也可防止从已取得根权限的设备提取密钥。

为了让硬件支持的密钥库使用起来更简单和更安全,Android N 引入了密钥认证。 应用和关闭的设备可使用密钥认证以坚决地确定 RSA 或 EC 密钥对是否受硬件支持、密钥对的属性如何,以及其使用和有效性有何限制。

应用和关闭的设备服务可以通过 X.509 认证证书(必须由有效的认证密钥签署)请求有关密钥对的信息。 认证密钥是一个 ECDSA 签署密钥,其在出厂时被注入设备的硬件支持的密钥库。因此,有效的认证密钥签署的认证证书可确认硬件支持的密钥库是否存在,以及该密钥库中密钥对的详细信息。

为确保设备使用安全的官方 Android 出厂映像,密钥认证要求设备 bootloader 向可信执行环境 (TEE) 提供以下信息:

  • 设备上安装的操作系统版本和补丁级别
  • 验证的启动公钥和锁定状态。

如需了解有关硬件支持的密钥库功能的详细信息,请参阅硬件支持的密钥库指南。

除密钥认证外,Android N 还推出了指纹绑定密钥,在指纹注册时不会撤销。

网络安全性配置


在 Android N 中,通过使用说明性“网络安全性配置”(而不是使用传统的易出错的编程 API(例如,X509TrustManager)),应用可以安全地自定义其安全(HTTPS、TLS)连接的行为,无需任何代码修改。

支持的功能:

  • 自定义信任锚。让应用可以针对安全连接自定义哪些证书颁发机构 (CA) 值得信赖。 例如,信任特定的自签署证书或限制应用信任的公共 CA 集。
  • 仅调试重写。让应用开发者可以安全调试其应用的安全连接,而不会增加安装基础的风险。
  • 明文流量选择退出。让应用可以防止自身意外使用明文流量。
  • 证书固定。这是一项高级功能,让应用可以针对安全连接限制哪些服务器密钥受信任。

如需了解详细信息,请参阅网络安全性配置

默认受信任的证书颁发机构


默认情况下,面向 Android N 的应用仅信任系统提供的证书,且不再信任用户添加的证书颁发机构 (CA)。 如果面向 Android N 的应用希望信任用户添加的 CA,则应使用网络安全性配置以指定信任用户 CA 的方式。

APK signature scheme v2


Android N 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。 在默认情况下,Android Studio 2.2 和 Android Gradle 2.2 插件会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。

虽然我们建议您对您的应用采用 APK Signature Scheme v2,但这项新方案并非强制性的。 如果您的应用在使用 APK Signature Scheme v2 时不能正确构建,您可以停用这项新方案。 禁用过程会导致 Android Studio 2.2 和 Android Gradle 2.2 插件仅使用传统签名方案来签署您的应用。 若要仅用传统方案签署,打开多层 build.gradle 文件,然后将行 v2SigningEnabled false 添加到您的版本签名配置中:

  android {
    ...
    defaultConfig { ... }
    signingConfigs {
      release {
        storeFile file("myreleasekey.keystore")
        storePassword "password"
        keyAlias "MyReleaseKey"
        keyPassword "password"
        v2SigningEnabled false
      }
    }
  }

注意:如果您使用 APK Signature Scheme v2 签署您的应用,并对应用进行了进一步更改,则应用的签名将无效。 出于这个原因,请在使用 APK Signature Scheme v2 之前、而非之后使用 zipalign 等工具。

如需更多信息,请阅读介绍如何在 Android Studio 中签署一项应用以及如何使用 Android Gradle 插件来为签署应用配置构建文件

作用域目录访问


在 Android N 中,应用可以使用新的 API 请求访问特定的外部存储目录,包括可移动媒体上的目录,如 SD 卡。 新 API 大大简化了应用访问标准外部存储目录的方式,如 Pictures 目录。 应用(如照片应用)可以使用这些 API(而不是使用 READ_EXTERNAL_STORAGE),其授予所有存储目录的访问权限或存储访问框架,从而让用户可以导航到目录。

此外,新的 API 简化了用户向应用授予外部存储访问权限的步骤。 当您使用新的 API 时,系统使用一个简单的权限 UI,其清楚地详细介绍应用正在请求访问的目录。

如需了解详细信息,请参阅作用域目录访问开发者文档。

键盘快捷键辅助工具


在 Android N 中,用户可以按“Alt + /”触发“键盘快捷键”屏幕,它会显示的系统和对焦的应用中可用的所有快捷键。 这些是从应用菜单(如可用)中自动检索到的,但开发者可以提供自己的屏幕微调快捷键。 您可以通过重写新 Activity.onProvideKeyboardShortcuts() 的方法来进行这项操作,如可下载的API 参考 中所述。

若要在您的应用程序的任何地方触发键盘快捷键辅助工具,为相关活动调用 Activity.requestKeyboardShortcutsHelper()

持续性能 API


长期运行的应用的性能可能会显著波动,因为系统会阻止系统芯片在设备组件达到温度限制时启动。 这种波动是建立高性能长期运行应用的应用开发者的移动目标。

为解决这些限制,Android N 包括了“持续性能模式”支持,帮助原始设备制造商 (OEM) 提供关于长期运行应用的设备性能能力的提示。 应用开发者可以使用这些提示来根据可预测的一致设备性能水平调整长期应用。

应用开发者只能在 Nexus 6P 设备的 N Developer Preview 上尝试这项新的 API。 若要使用此功能,为您希望以持续性能模式运行的窗口设置持续性能窗口标记。 使用 Window.setSustainedPerformanceMode() 方法设置此举报。 当窗口不再对焦时,系统会自动停用此模式。

VR 支持


Android N 添加了新的 VR 模式的平台支持和优化,以使开发者能为用户打造高质量移动 VR 体验。 新版针对开发者提供了大量性能增强特性,包括单一缓冲区渲染以及允许 VR 应用访问某个专属的 CPU 核心。在您的应用中,您可以享受到专为 VR 设计的平滑头部跟踪和立体声通知功能。 最重要的是,Android N 的图形延时非常低。 如需有关构建面向的 Android N 的 VR 应用的完整信息,请参阅 面向 Android 的 Google VR SDK


在 Android N 中,打印服务开发者现在可以公开关于个别打印机和打印作业的其他信息。

在列出各打印机时,打印服务现在可以通过两种方式来设置按打印机的图标:

  • 您可以通过调用 PrinterInfo.Builder.setResourceIconId() 设置源于资源 ID 的图标
  • 您可以通过调用 PrinterInfo.Builder.setHasCustomPrinterIcon(),并针对使用android.printservice.PrinterDiscoverySession.onRequestCustomPrinterIcon() 请求图标的情况设置回调来显示源自网络的图标

此外,您还可以通过调用 PrinterInfo.Builder.setInfoIntent() 提供按打印机活动,以显示其他信息。

您可以通过分别调用 android.printservice.PrintJob.setProgress() 和 android.printservice.PrintJob.setStatus() 在打印任务通知中指示打印任务的进度和状态。

如需有关这些方法的详细信息,请参阅可下载的 API 参考

FrameMetricsListener API


FrameMetricsListener API 允许应用监测它的 UI 渲染性能。 API 通过公开流式传输 Pub/Sub API 来提供此能力,以传递应用当前窗口的帧计时信息。 返回的数据相当于 adb shell dumpsys gfxinfo framestats 显示的数据,但不限定于在过去的 120 帧内。

您可以使用 FrameMetricsListener 来衡量生产中的交互级 UI 性能,无需 USB 连接。 API 允许在比 adb shell dumpsys gfxinfo 更高的粒度上收集数据。 因为系统可以从应用中的特定交互中收集数据,因此更高的粒度变得可行;系统不需要采集关于完整应用性能的全局概要或清除任何全局状态。 您可以使用这种能力来针对应用的真实使用案例收集性能数据和捕捉 UI 性能回归。

若要监测一个窗口,实现 FrameMetricsListener.onMetricsAvailable() 回叫方法,并在窗口上注册。 如需了解详细信息,请参阅可下载的 API 参考 中的 FrameMetricsListener 类文档。

API 提供了一个包含计时数据的 FrameMetrics 对象,其渲染子系统会在一帧长度内报告各种里程碑。支持的指标有:UNKNOWN_DELAY_DURATIONINPUT_HANDLING_DURATIONANIMATION_DURATIONLAYOUT_MEASURE_DURATIONDRAW_DURATIONSYNC_DURATIONCOMMAND_ISSUE_DURATIONSWAP_BUFFERS_DURATIONTOTAL_DURATION和 FIRST_DRAW_FRAME

虚拟文件


在较早的 Android 版本中,您的应用可以使用存储访问框架来允许用户从他们的云存储帐户中选择文件,如 Google 云端硬盘。 但是,不能表示没有直接字节码表示的文件;每个文件都必须提供一个输入流。

Android N 在存储访问框架中增加了“虚拟文件”的概念。 虚拟文件功能可以让您的 DocumentsProvider 返回可与 ACTION_VIEWIntent 使用的文件 URI,即使它们没有直接字节码表示。 Android N 还允许您为用户文件(虚拟或其他类)提供备用格式。

为获得您的应用中的虚拟文件的 URI,首先您应创建一个 Intent 以打开文件选择器 UI。 由于应用不能使用 openInputStream() 方法来直接打开一个虚拟文件,因此如果您包括了 CATEGORY_OPENABLE 类别,您的应用不会收到任何虚拟文件。

在用户选择之后,系统调用 onActivityResult() 方法。 您的应用可以检索虚拟文件的URI,并得到一个输入流,这表现在以下片段中的代码。

  // Other Activity code ...

  final static private int REQUEST_CODE = 64;

  // We listen to the OnActivityResult event to respond to the user's selection.
  @Override
  public void onActivityResult(int requestCode, int resultCode,
    Intent resultData) {
      try {
        if (requestCode == REQUEST_CODE &&
            resultCode == Activity.RESULT_OK) {

            Uri uri = null;

            if (resultData != null) {
                uri = resultData.getData();

                ContentResolver resolver = getContentResolver();

                // Before attempting to coerce a file into a MIME type,
                // check to see what alternative MIME types are available to
                // coerce this file into.
                String[] streamTypes =
                  resolver.getStreamTypes(uri, "*/*");

                AssetFileDescriptor descriptor =
                    resolver.openTypedAssetFileDescriptor(
                        uri,
                        streamTypes[0],
                        null);

                // Retrieve a stream to the virtual file.
                InputStream inputStream = descriptor.createInputStream();
            }
        }
      } catch (Exception ex) {
        Log.e("EXCEPTION", "ERROR: ", ex);
      }
  }

如需有关访问用户文件的更多信息,请参阅 存储访问框架指南

作者:ccj659 发表于2016/10/28 17:20:17 原文链接
阅读:39 评论:0 查看评论

Android自定义View之仿QQ侧滑菜单实现

$
0
0

最近,由于正在做的一个应用中要用到侧滑菜单,所以通过查资料看视频,学习了一下自定义View,实现一个类似于QQ的侧滑菜单,顺便还将其封装为自定义组件,可以实现类似QQ的侧滑菜单和抽屉式侧滑菜单两种菜单。

下面先放上效果图:

侧滑菜单
抽屉式侧滑

我们这里的侧滑菜单主要是利用HorizontalScrollView来实现的,基本的思路是,一个布局中左边是菜单布局,右边是内容布局,默认情况下,菜单布局隐藏,内容布局显示,当我们向右侧滑,就会将菜单拉出来,而将内容布局的一部分隐藏,如下图所示:

这里写图片描述

下面我们就一步步开始实现一个侧滑菜单。

一、定义一个类SlidingMenu继承自HorizontalScrollView

我们后面所有的逻辑都会在这个类里面来写,我们先写上其构造方法

public class SlidingMenu extends HorizontalScrollView {
    /**
     * 在代码中使用new时会调用此方法
     * @param context
     */
    public SlidingMenu(Context context) {
        this(context, null);
    }

    /**
     * 未使用自定义属性时默认调用
     * @param context
     * @param attrs
     */
    public SlidingMenu(Context context, AttributeSet attrs) {
        //调用三个参数的构造方法
        this(context, attrs, 0);

    }

    /**
     * 当使用了自定义属性时会调用此方法
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

二、定义菜单布局文件

left_menu.xml文件代码如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            android:gravity="left|center"
            android:drawableLeft="@mipmap/ic_launcher"
            android:text="第一个Item"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            android:gravity="left|center"
            android:drawableLeft="@mipmap/ic_launcher"
            android:text="第二个Item"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            android:gravity="left|center"
            android:drawableLeft="@mipmap/ic_launcher"
            android:text="第三个Item"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            android:gravity="left|center"
            android:drawableLeft="@mipmap/ic_launcher"
            android:text="第四个Item"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            android:gravity="left|center"
            android:drawableLeft="@mipmap/ic_launcher"
            android:text="第五个Item"/>
    </LinearLayout>
</RelativeLayout>

上面其实就是定义了一列TextView来模仿菜单的Item项

三、定义主布局文件,使用自定义的View

activity_main.xml文件代码如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--自定义View-->
   <com.codekong.qq_50_slidingmenu.view.SlidingMenu
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:rightPadding="100dp"
       app:drawerType="false"
       android:scrollbars="none">
       <LinearLayout
           android:layout_width="wrap_content"
           android:layout_height="match_parent"
           android:orientation="horizontal">
            <!--引入菜单布局-->
           <include layout="@layout/left_menu"/>
            <!--内容布局-->
           <LinearLayout
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:background="@drawable/qq_bg">

           </LinearLayout>
       </LinearLayout>
   </com.codekong.qq_50_slidingmenu.view.SlidingMenu>
</RelativeLayout>

四、自定义成员变量

我们定义一些成员变量以便于后面使用

//自定义View布局中内嵌的第一层的LinearLayout
private LinearLayout mWapper;
//菜单布局
private ViewGroup mMenu;
//内容布局
private ViewGroup mContent;
//屏幕宽度
private int mScreenWidth;
//菜单距屏幕右侧的距离,单位dp
private int mMenuRightPadding = 50;
//菜单的宽度
private int mMenuWidth;
//定义标志,保证onMeasure只执行一次
private boolean once = false;
//菜单是否是打开状态
private boolean isOpen = false;

五、拿到屏幕宽度的像素值

因为目前为止,我们没有使用自定义属性,所以自定义View默认会调用两个参数的构造方法,但因为我们第一步中写构造方法时是在两个参数的构造方法中调用了三个参数的构造方法,所以,我们将获取屏幕宽度的代码写在三个参数的构造方法中,后面我们自定义属性后获取属性值也是在三个参数的构造方法中书写相应的逻辑。

/**
 * 当使用了自定义属性时会调用此方法
 * @param context
 * @param attrs
 * @param defStyleAttr
 */
public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //通过以下步骤拿到屏幕宽度的像素值
    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    windowManager.getDefaultDisplay().getMetrics(displayMetrics);
    mScreenWidth = displayMetrics.widthPixels;
}

六、实现onMeasure()方法

onMeasure()方法是自定义View的正式第一步,它用来决定内部View(子View)的宽和高,以及自身的宽和高,下面是具体的代码逻辑。

/**
* 设置子View的宽和高
* 设置自身的宽和高
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   if (!once){
       once = true;
       mWapper = (LinearLayout) getChildAt(0);
       mMenu = (ViewGroup) mWapper.getChildAt(0);
       mContent = (ViewGroup) mWapper.getChildAt(1);
       //菜单和内容区域的高度都可以保持默认match_parent
       //菜单宽度 = 屏幕宽度 - 菜单距屏幕右侧的间距
       mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;
       mContent.getLayoutParams().width = mScreenWidth;
       //当设置了其中的菜单的宽高和内容区域的宽高之后,最外层的LinearLayout的mWapper就自动设置好了
   }
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

七、实现onLayout()方法

onLayout()方法中主要是确定自定义View中子View放置的位置。下面是具体的代码。

/**
 * 通过设置偏移量将Menu隐藏
 * @param changed
 * @param l
 * @param t
 * @param r
 * @param b
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {

    super.onLayout(changed, l, t, r, b);
    if (changed){
        //布局发生变化时调用(水平滚动条向右移动menu的宽度,则正好将menu隐藏)
        this.scrollTo(mMenuWidth, 0);
    }
}

这个比较好理解,由于我们使用的是水平滚动布局,我们默认情况下相当于将水平滚动条向右拖动菜单宽度的的距离,这样左边布局的菜单就正好被隐藏了。

八、onTouchEvent()方法

该方法主要处理内部内部View的移动,我们可以在其中写一些逻辑控制自定义View内部的滑动事件。
由于我们的自定义View是继承自HorizontalScrollView,我们不再处理按下和移动事件,保持HorizontalScrollView默认的即可,但对于手指抬起事件,我们需要根据手指在水平X轴方向的位移来做出打开菜单或关闭菜单的操作,所以我们的逻辑代码如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    //按下和移动使用HorizontalScrollView的默认处理
    switch (action){
        case MotionEvent.ACTION_UP:
            //隐藏在左边的位置
            int scrollX = getScrollX();
            if (scrollX > mMenuWidth / 2){
                //隐藏的部分较大, 平滑滚动不显示菜单
                this.smoothScrollTo(mMenuWidth, 0);
                isOpen = false;
            }else{
                //完全显示菜单
                this.smoothScrollTo(0, 0);
                isOpen = true;
            }
            return true;
    }
    return super.onTouchEvent(ev);
}

上面最难理解的的就是getScrollX(),它指的是菜单隐藏未显示的那部分的宽度。关于详细的解释,大家可以去看源码,也可以去看看这篇博客 图解Android View的scrollTo(),scrollBy(),getScrollX(), getScrollY(),讲的很清楚。

其实到这一步为止,一个基本的侧滑菜单已经做出来了,下面我们将使用属性动画对我们的自定义View进行扩展,使其实现最开始展示的抽屉式侧滑菜单。

九、属性动画实现抽屉式侧滑

接下来我们实现抽屉式侧滑,抽屉式侧滑说白了就是,我们的菜单不是一点点被拉出来,而是看起来菜单就藏在页面的背后,随着我们向右滑动,一点点显露出来。
实现的思路很简单,当我们拖动时,我们让菜单布局的偏移量等于getScrollX()的值,也就是时刻把菜单隐藏在左边的部分向右偏移出来,这样我们看起来就像菜单藏在页面后面。如下图:

这里写图片描述

当我们左右滑动时会触发onScrollChanged()方法,我们在此处算出菜单需要的实时的偏移量,然后调用属性动画即可。
下面说说具体实现代码:

/**
 * 滚动发生时调用
 * @param l  getScrollX()
 * @param t
 * @param oldl
 * @param oldt
 */
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    float scale = l * 1.0f / mMenuWidth;  //1 ~ 0
    //调用属性动画,设TranslationX
    mMenu.setTranslationX(mMenuWidth * scale);
}

上面方法中的 l 就是上一步中提到的getScrollX()获得的值。当我们没有拉菜单时,菜单需要的偏移量就是整个菜单的宽度,当我们将菜单完全拉出时,菜单就不需要偏移量了,此时偏移量为0。此时我们的抽屉式侧滑就做好了。

注:此处的属性动画是在Android3.0之后引入的,如果需要兼容更早的版本,可以用相关的兼容库。

十、自定义属性实现灵活配置

自定义属性主要是方便使用者可以根据具体的场景实现不同的效果。比如,我们可以通过在xml文件中配置,实现菜单是普通的侧滑式还是抽屉式。在刚开始,我们在自定义View中将菜单打开时,菜单右边缘距离屏幕右边缘的值设置为50dp,我们通过自定义属性可以实现在xml文件中自己配置合适的值。

自定义属性按下面的步骤进行:

1 . 在 res/values 目录下新建attr.xml文件,文件中写入的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <attr name="rightPadding" format="dimension"/>
   <attr name="drawerType" format="boolean"/>
   <declare-styleable name="SlidingMenu">
       <attr name="rightPadding"/>
       <attr name="drawerType"/>
   </declare-styleable>
</resources>

上面的比较好理解,我们先在上面声明两个自定义的属性的名称及其对应的类型,然后再在下面的具体的自定义样式中引用它们。上面两个自定义的属性分别是菜单拉开时右边缘距离屏幕右边缘的距离,以及菜单是否是抽屉式布局。

2 . 在自定义View类中获取到自定义的属性值。如果用户在xml文件中自定义了属性值,我们则获取,如果没有显式设置,则使用默认值即可。

顺便说一下,前面提到当我们使用自定义属性时,会默认调用三个参数的构造方法,所以我们获取自定义属性值的代码也是写在三个参数的构造方法中。

下面是获取属性值的代码:

/**
 * 当使用了自定义属性时会调用此方法
 * @param context
 * @param attrs
 * @param defStyleAttr
 */
public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    //获取我们自定义的属性
    TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,
            R.styleable.SlidingMenu, defStyleAttr, 0);
    int n = typedArray.getIndexCount();
    //遍历每一个属性
    for (int i = 0; i < n; i++) {
        int attr = typedArray.getIndex(i);
        switch (attr){
            //对我们自定义属性的值进行读取
            case R.styleable.SlidingMenu_rightPadding:
                //如果在应用样式时没有赋值则使用默认值50,如果有值则直接读取
                mMenuRightPadding = typedArray.getDimensionPixelSize(attr,
                        (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMenuRightPadding, context.getResources().getDisplayMetrics()));
                break;
            case R.styleable.SlidingMenu_drawerType:
                isDrawerType = typedArray.getBoolean(attr, false);
                break;
            default:
                break;
        }
    }
    //释放,一定要释放
    typedArray.recycle();
}

3 . 上面的代码中我们已经可以读取到设置的属性值,我们可以如下面一样设置自定义属性值:

<com.codekong.qq_50_slidingmenu.view.SlidingMenu
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:rightPadding="100dp"
       app:drawerType="true"
       android:scrollbars="none">
 </com.codekong.qq_50_slidingmenu.view.SlidingMenu>

4 . 使用属性值控制具体的逻辑,我们的rightPadding一旦获取到就会在onMeasure()方法中被设置,而drawerType被获取到就可以控制是否会调用onScrollChanged()中的代码。代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   if (!once){
       once = true;
       mWapper = (LinearLayout) getChildAt(0);
       mMenu = (ViewGroup) mWapper.getChildAt(0);
       mContent = (ViewGroup) mWapper.getChildAt(1);
       //菜单和内容区域的高度都可以保持默认match_parent
       //菜单宽度 = 屏幕宽度 - 菜单距屏幕右侧的间距
       mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;
       mContent.getLayoutParams().width = mScreenWidth;
       //当设置了其中的菜单的宽高和内容区域的宽高之后,最外层的LinearLayout的mWapper就自动设置好了
   }
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    if (isDrawerType){
        float scale = l * 1.0f / mMenuWidth;  //1 ~ 0
        //调用属性动画,设TranslationX
        mMenu.setTranslationX(mMenuWidth * scale);
    }
}

十一、给自定义View设置方法

对于我们的滑动式菜单,我们最常用的功能便是菜单的打开和关闭,所以我们可以在自定义View中定义这两个方法,方便我们的使用,下面是具体的代码:

/**
 * 打开菜单
 */
public void openMenu(){
    if (!isOpen){
        this.smoothScrollTo(0, 0);
        isOpen = true;
    }
}

/**
 * 关闭菜单
 */
public void closeMenu(){
    if (isOpen){
        this.smoothScrollTo(mMenuWidth, 0);
        isOpen = false;
    }
}

/**
 * 切换菜单
 */
public void toggleMenu(){
    if (isOpen){
        closeMenu();
    }else{
        openMenu();
    }
}

当我们在Activity中使用时可以按下面的代码使用:

SlidingMenu slidingMenu = (SlidingMenu) findViewById(R.id.sliding_menu);
slidingMenu.toggleMenu();

最后面放上完整的自定义View的代码:

public class SlidingMenu extends HorizontalScrollView {
   //自定义View布局中内嵌的最外层的LinearLayout
   private LinearLayout mWapper;
   //菜单布局
   private ViewGroup mMenu;
   //内容布局
   private ViewGroup mContent;
   //屏幕宽度
   private int mScreenWidth;
   //菜单距屏幕右侧的距离,单位dp
   private int mMenuRightPadding = 50;
   //菜单的宽度
   private int mMenuWidth;
   //定义标志,保证onMeasure只执行一次
   private boolean once = false;
   //菜单是否是打开状态
   private boolean isOpen = false;
   //是否是抽屉式
   private boolean isDrawerType = false;
   /**
    * 在代码中使用new时会调用此方法
    * @param context
    */
   public SlidingMenu(Context context) {
       this(context, null);
   }

   /**
    * 未使用自定义属性时默认调用
    * @param context
    * @param attrs
    */
   public SlidingMenu(Context context, AttributeSet attrs) {
       //调用三个参数的构造方法
       this(context, attrs, 0);

   }

   /**
    * 当使用了自定义属性时会调用此方法
    * @param context
    * @param attrs
    * @param defStyleAttr
    */
   public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);

       //获取我们自定义的属性
       TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,
               R.styleable.SlidingMenu, defStyleAttr, 0);
       int n = typedArray.getIndexCount();
       //遍历每一个属性
       for (int i = 0; i < n; i++) {
           int attr = typedArray.getIndex(i);
           switch (attr){
               //对我们自定义属性的值进行读取
               case R.styleable.SlidingMenu_rightPadding:
                   //如果在应用样式时没有赋值则使用默认值50,如果有值则直接读取
                   mMenuRightPadding = typedArray.getDimensionPixelSize(attr,
                           (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMenuRightPadding, context.getResources().getDisplayMetrics()));
                   break;
               case R.styleable.SlidingMenu_drawerType:
                   isDrawerType = typedArray.getBoolean(attr, false);
                   break;
               default:
                   break;
           }
       }
       //释放
       typedArray.recycle();

       //通过以下步骤拿到屏幕宽度的像素值
       WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
       DisplayMetrics displayMetrics = new DisplayMetrics();
       windowManager.getDefaultDisplay().getMetrics(displayMetrics);
       mScreenWidth = displayMetrics.widthPixels;
   }

   /**
    * 设置子View的宽和高
    * 设置自身的宽和高
    * @param widthMeasureSpec
    * @param heightMeasureSpec
    */
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       if (!once){
           once = true;
           mWapper = (LinearLayout) getChildAt(0);
           mMenu = (ViewGroup) mWapper.getChildAt(0);
           mContent = (ViewGroup) mWapper.getChildAt(1);
           //菜单和内容区域的高度都可以保持默认match_parent
           //菜单宽度 = 屏幕宽度 - 菜单距屏幕右侧的间距
           mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;
           mContent.getLayoutParams().width = mScreenWidth;
           //当设置了其中的菜单的宽高和内容区域的宽高之后,最外层的LinearLayout的mWapper就自动设置好了
       }
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   }

   /**
    * 通过设置偏移量将Menu隐藏
    * @param changed
    * @param l
    * @param t
    * @param r
    * @param b
    */
   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {

       super.onLayout(changed, l, t, r, b);
       if (changed){
           //布局发生变化时调用(水平滚动条向右移动menu的宽度,则正好将menu隐藏)
           this.scrollTo(mMenuWidth, 0);
       }
   }

   @Override
   public boolean onTouchEvent(MotionEvent ev) {
       int action = ev.getAction();
       //按下和移动使用HorizontalScrollView的默认处理
       switch (action){
           case MotionEvent.ACTION_UP:
               //隐藏在左边的位置
               int scrollX = getScrollX();
               if (scrollX > mMenuWidth / 2){
                   //隐藏的部分较大, 平滑滚动不显示菜单
                   this.smoothScrollTo(mMenuWidth, 0);
                   isOpen = false;
               }else{
                   //完全显示菜单
                   this.smoothScrollTo(0, 0);
                   isOpen = true;
               }
               return true;
       }
       return super.onTouchEvent(ev);
   }

   /**
    * 打开菜单
    */
   public void openMenu(){
       if (!isOpen){
           this.smoothScrollTo(0, 0);
           isOpen = true;
       }
   }

   /**
    * 关闭菜单
    */
   public void closeMenu(){
       if (isOpen){
           this.smoothScrollTo(mMenuWidth, 0);
           isOpen = false;
       }
   }

   /**
    * 切换菜单
    */
   public void toggleMenu(){
       if (isOpen){
           closeMenu();
       }else{
           openMenu();
       }
   }

   /**
    * 滚动发生时调用
    * @param l  getScrollX()
    * @param t
    * @param oldl
    * @param oldt
    */
   @Override
   protected void onScrollChanged(int l, int t, int oldl, int oldt) {
       super.onScrollChanged(l, t, oldl, oldt);
       if (isDrawerType){
           float scale = l * 1.0f / mMenuWidth;  //1 ~ 0
           //调用属性动画,设TranslationX
           mMenu.setTranslationX(mMenuWidth * scale);
       }
   }
}
作者:bingjianIT 发表于2016/10/28 17:23:40 原文链接
阅读:40 评论:0 查看评论

Android——插件化学习笔记(一)

$
0
0

写了一个月应用层代码,感觉写呕了,最近在研究插件化动态加载方面的东西。

没错就是360的开源库:DroidPluginTeam

还有一位大神写的很好的源码分析总结:understand-plugin-framework

本文主要对第一篇:Android插件化原理解析——Hook机制之动态代理 遇到的一些问题以及解决最后的作业部分,并记录下作为学习心得笔记。对于刚接触这个的学者,可以起到一定作用避免少走弯路吧~建议先下载其源代码,然后结合本文来看。

本文需要解决的作业:在Activity自身的跳转中进行Hook。

 

先简要说下遇到的几个坑以及后面的学习整理:

Activity 启动流程和Context类详解:

关于Context的理解可以先参考以下两篇博客:

Android中Context详解 ---- 你所不知道的Context

深入理解Context

比较重要的点是要理清Context的继承关系


由图可一目了然Activity的继承关系,由于Activity是继承自Context的包装类ContextWrapper的,在我们的Activity得到Context对象时,会通过重写的attachBaseContext方法得到Context实例。

这也就是在第一个例子中为什么在attachBaseContext中执行hook方法的原因:


然而这里要特别注意一点,作者的tainn的例子中使用的是ApplicationContext,然而作业使用到的并不是ContextImpl的mInstrumentation而是自己的mInstrumentation,我们来看看Activity中调用attachBaseContext调用的地方:


可以看到attachBaseContext是在Activity的成员变量mInstrumentation初始化之前进行调用的,所以不能像作者一样在attachBaseContext中执行hook方法,这里我选择了在执行startActivity之前调用以保证能hook到。


作业流程:

1.首先修改MainActivity中的点击事件和hook触发方法的:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // TODO: 16/1/28 支持Activity直接跳转请在这里Hook
        // 家庭作业,留给读者完成.

        LinearLayout linearLayout = new LinearLayout(this);
        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
        LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(300, 100);
        btnParams.setMargins(10, 10, 10, 10);

//        Button tv = new Button(this);
//        tv.setLayoutParams(btnParams);
//        tv.setText("测试1");
//        linearLayout.addView(tv);

        Button tv2 = new Button(this);
        tv2.setLayoutParams(btnParams);
        tv2.setText("家庭作业");
        linearLayout.addView(tv2);

        setContentView(linearLayout);

//        tv.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//                Intent intent = new Intent(Intent.ACTION_VIEW);
//                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//                intent.setData(Uri.parse("http://www.baidu.com"));
//                // 注意这里使用的ApplicationContext 启动的Activity
//                // 因为Activity对象的startActivity使用的并不是ContextImpl的mInstrumentation
//                // 而是自己的mInstrumentation, 如果你需要这样, 可以自己Hook
//                // 比较简单, 直接替换这个Activity的此字段即可.
//                getApplicationContext().startActivity(intent);
//            }
//        });

        tv2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(MainActivity.this,OtherActivity.class);
                // 在这里进行Hook
                Log.d("xiaonangua","开始hook");

                try {
                    MyHookHelper.attachContext(MainActivity.this);
                } catch (Exception e) {
                    Log.d("xiaonangua",e.getMessage().toString());

                    e.printStackTrace();
                }
                startActivity(i);
            }
        });
    }

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
          //  HookHelper.attachContext();
        } catch (Exception e) {
            Log.d("xiaonangua",e.getMessage().toString());
            e.printStackTrace();
        }
    }
}

2.与tainn作者分析Context的调用链不同,我们先来看看Activity.startActivity()源码:

public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
        if (mParent == null) {
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
<span style="white-space:pre">			</span>.....
接下来要做的就是把 mInstrumentation换成我们修改过的代理对象,通过反射得到Activity的Instrumentation字段然后创建代理对象并偷梁换柱QAQ:

public class MyHookHelper {
    public static void attachContext(Activity mActivity) throws Exception{
        // 先获取到当前的ActivityThread对象
        Class<?> activityClass = Class.forName("android.app.Activity");

        // 拿到原始的 mInstrumentation字段
        Field mInstrumentationField = activityClass.getDeclaredField("mInstrumentation");//获取属性
        //打破封装
        mInstrumentationField.setAccessible(true);

        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(mActivity);

        // 创建代理对象
        Instrumentation myEvilInstrumentation = new MyEvillnstrumentation(mInstrumentation);

        // 偷梁换柱
        mInstrumentationField.set(mActivity, myEvilInstrumentation);
    }
}
代理类基本和作者的一样,加了一个小字段而已:

public class MyEvillnstrumentation  extends Instrumentation {

    private static final String TAG = "xiaonangua";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public MyEvillnstrumentation(Instrumentation base) {
        mBase = base;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        // Hook之前, XXX到此一游!
        Log.d(TAG, "\n南瓜执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);

            execStartActivity.setAccessible(true);
            Log.d("xiaonangua",execStartActivity.toString());
            return (ActivityResult) execStartActivity.invoke(mBase,who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            Log.d("xiaonangua",e.getMessage().toString());
            return null;
            // 某该死的rom修改了  需要手动适配
            //throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}


然后启动app,运行,打印log如下:

10-28 16:51:02.520 26242-26242/com.weishu.upf.dynamic_proxy_hook.app2 D/xiaonangua: 开始hook
10-28 16:51:02.530 26242-26242/com.weishu.upf.dynamic_proxy_hook.app2 D/xiaonangua: 南瓜执行了startActivity, 参数如下: 
                                                                                    who = [com.weishu.upf.dynamic_proxy_hook.app2.MainActivity@3ec0c1af], 
                                                                                    contextThread = [android.app.ActivityThread$ApplicationThread@2f6ff9bc], 
                                                                                    token = [android.os.BinderProxy@2222360], 
                                                                                    target = [com.weishu.upf.dynamic_proxy_hook.app2.MainActivity@3ec0c1af], 
                                                                                    intent = [Intent { cmp=com.weishu.upf.dynamic_proxy_hook.app2/.OtherActivity }], 
                                                                                    requestCode = [-1], 
                                                                                    options = [null]

Success~成功地完成了第一篇家庭作业哈哈哈。




 



作者:qq_22770457 发表于2016/10/28 17:26:48 原文链接
阅读:47 评论:0 查看评论

MultiDex使用方法及由此导致的crash、ANR问题解决方案

$
0
0

Android开发的朋友,如果是在开发一款中大型应用时,都会碰到这么一个问题,就是dex分拆问题, google给出的解决方案MultiDex。

现象:

有些APP本身功能比较多,再加上一些其它三方的SDK,慢慢的发现dex越来越大,直到有一天编译出现如下错误:

Error:The number of method references in a .dex file cannot exceed 64K.
Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html

错误原因比较明确了,打开Gradle Console查看详细信息,我们在一堆错误中找到如下提醒:

"UNEXPECTED TOP-LEVEL EXCEPTION:\ncom.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536\n\tat com.android.dx.merge.DexMerger$6.updateIndex(DexMerger

原因:

问题原因是因为早期Android系统设计时用一个short来表示dex里的每个method的id,我们知道short的上限是64K,所以就遗留了这样一个问题,既然存在了,肯定要找方法解决,Google刚开始给出建议是使用proguard,但是再怎么proguard也还是会突破64K的,所以后面Google给出了MultiDex方案。就是我们经常看到的一个apk里,有classes.dex, classes2.dex,甚至还有classes3.dex等。

MultiDex实现步骤:

用这种方法来突破64K的method id数量的限制,具体实现步骤如下:
1. 在Module的build.gradle里添加
multiDexEnabled true

例如:

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "x.x.x"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 17
        versionName "1.2.9"

        //添加这一行
        multiDexEnabled true

    }

}

2. 接着在Module的build.gradle里添加

dependencies {
  compile 'com.android.support:multidex:1.0.0'
}

3.第三步有两种情况,
1)如果你的apk没有定义application,则在AndroidManifest.xml里的application里做如下修改:
添加MultiDexApplication

<application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="android.support.multidex.MultiDexApplication"
        tools:replace="android:icon, android:name"
       >

2)如果apk有自己的appliation,比如叫MyApplication,则在MyApplication实现里加如下代码:

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

至此,MultiDex的编译配置已经完成,你只要重新编译即可,apk就会生成两个(或多个) dex。如果有兴趣了解下MultiDex的实现原理,可以查看源代码,https://android.googlesource.com/platform/frameworks/multidex 源代码相对比较简单的。基本原理是使用java的class loader,这个在一些热修复技术上也在使用,比如企鹅工厂开源的Tinker。

存在的坑及解决方法:

虽然MultiDex分包已经完成,但是实际使用过程中,可能存在一些坑,比如之前笔者碰到的一个VerifyError的错误(http://blog.csdn.net/zhuobattle/article/details/47153025)
1. 分拆导致的crash

这个问题的表现除了报VerifyError外,还有可能报Could not find class,NoClassDefFoundError, Could not find method等。是因为我们在main dex中调用的函数或类被放在了classes2.dex中,而在classes2.dex还没有被完全加载前,调用这些api就会导致这种问题。要确认是否是这个问题导致的错误,我们可以查看:
app\build\intermediates\multi-dex\debug\maindexlist.txt 这个文本文件,这里列出来的类都会被放在主dex中,那么问题是我们要如何解决这个问题呢?
解决方案:
首先,我们来看下在编译过程中,multidex是做了哪几步,这个打开Gradle Console可以找到multidex相关的步骤,其中一个步骤是关键就是生成maindexlist.txt的步骤:createDebugMainDexClassList就是这里生成maindexlist.txt 的,但是这个文件直接修改又没有什么用,因为每次编译都会重新生成一次的,笔者在实践者发现可以用自定义的方式:multiDexKeepFile file(‘multiDexKeep.txt’)
例子:

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"


    defaultConfig {
        applicationId "x.x.x"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 17
        versionName "1.2.9"

        multiDexEnabled true

        //添加这一行
        multiDexKeepFile file('multiDexKeep.txt')
    }

}

内容和上面提到的createDebugMainDexClassList生成的maindexlist.txt一样即可,记得把这个multiDexKeep.txt文件放在app目录 下。multiDexKeep.txt内容可以如下:

com/test/Util.class
com/test/help/b.class

实测起作用,被keep的class全都留在了classes.dex中(release版本要配合mapping使用)。

2. ANR
也就是我们常说的卡顿,为什么会卡顿呢?这里因为我们把multidex的install放在了attachBaseContext中,而这个调用又是在MainActivity的onCreate之前的,所以如果2.dex,3.dex第一次加载时间很长(生成odex文件会耗费一定的时间), 就有可能会导致第一次启动卡上一小会(实测在Dalvik手机上一般classes.dex比较小的情况下,卡顿不明显,线上就不好说了)。

解决方法是在 APP第一次启动时(卸载、重装都会做一遍2odex,具体可以查看/data/data//code_cache/secondary-dexes/目录下的odex文件,所以这里判断要准确),把install放到异步线程里去做。这是很多网上的解决方法,但是如果这样做,你就必须再写一个类似initAfterDex2Installed(),来保证2.dex里的类不会提前被调用到,或者输出一个启动界面,停留几秒的样子,比如很多APP启动都有开机广告,或者开机画面,以此来解决app在2.dex加载之前部分功能无法使用的问题。网上大多是这种解决方案。本人测试后发现,如果MultiDex.install(this),放在后面或者异步来做的话,在MainActivity里的onCreate函数:
setContentView这里就出错了,堆栈如下:

java.lang.NoClassDefFoundError: android.support.v7.appcompat.R$attr
                                                       at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:289)
                                                       at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:246)
                                                       at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)
                                                       at com.cn.x.x.MainActivity.onCreate(MainActivity.java:86)

很明显android.support.v7.appcompat.R$attr在classes2.dex中,在调用时,还没有完成classes2.dex的加载,所以如果要解决的话,要不把这个类放到maindex中,要不让MainActivity的onCreate函数延迟调用。如果其它类还有类似问题,就要一个一个的试,成本有点高,有可能在不同系统,不同手机上还会出各种奇芭错误。由此看来,install延期加载方案并不合适。为了说明问题,也分析了网易几款大的产品,比如新闻、云音乐,全部是使用的mutlidex方式,都没有延期加载,全部是在application的attachBaseContext直接调用install.

那么我们要如何解决 2.dex, 3.dex在第一次启动apk时,有可能产生的耗时呢?

1)重新设计,或者在开发之前就设计好,使用插件化的方式来解决maindex的问题,把一些功能做成插件,保证这些dex在首屏启动时不需要被加载
2) 自己实现多dex框架,有兴趣可以看下微信的实现框架,并没有使用MultiDex,而是使用自己的Tinker(一种动态加载dex的方案,也被用于热更新)
微信使用的是TinkerAppliation. 另外一个例子是手机QQ的实现技术,手Q里居然有classes6.dex,也就是总共有6个dex,感兴趣的朋友可以分析下手Q的实现方案,笔者动态看过,基本上也是在手Q启动界面还没出来时,所有的dex会全部完成2odex的转换,在手机上第一次运行还是会花费不少时间的。而且笔者测试了很多手机,基本上2个dex不会发生anr,当然真实情况怎么样,还是要放到线上才能说明问题,也就是接收众多手机,不同品牌,不同性能的手机测试才知道,好在有一些知名公司的质量跟踪平台可以线上捕捉这些ANR,比如网易云捕

3) ART以后的实际情况是,不管你分成几个dex,在安装时都已经全部完成OAT的转换,这样分dex至少在ART以后也就是5.0(部分4.4机型可以选ART模式)以后不存在调用install占用时间的问题,而且实际测试也是在5.0以后,即使不调用install(this)也能正常工作,为了证实,我们拿网易新闻测试,在安装完后,在目录 /data/dalvik-cache//data@app@com.netease.newsreader.activity-1@base.apk@classes.dex,这个oat文件有84M,所以你可以知道
为什么第一次安装相比 dalivk慢了。
打开这个文件,后缀虽然还是dex,其实是一个ELF文件,也就是OAT文件。
这里写图片描述
classes.dex在这里。
这里写图片描述

这里写图片描述
classes2.dex, classes3.dex都已经被优化成OAT文件了。

这些处理都已经在安装的时候完成,就不存在启动时再对classes2.dex和classes3.dex进行处理了,避免了第一次启动可能导致的ANR.

总结

总结一下就是我们可以通过自定义maindexlist来控制哪些类一定出现在main dex中,这样可以避免crash;
而针对ANR还是不建议使用异步加载,合理设计和插件形式会比较合理,如果大家有其它更好的方法可以讨论。
不过在ART以后,不管是classes.dex还是classes2.dex, classes3.dex在安装时就已经完成了OAT的转换了,由分包导致的ANR的可能性就小了很多。

文章写完的时候,翻了一下其它博客,找到有一位朋友对ANR另外的一种解决方案,可以借鉴:
http://blog.csdn.net/qq_17766199/article/details/51285868

作者:zhuobattle 发表于2016/10/28 17:44:10 原文链接
阅读:29 评论:0 查看评论

Android BroadcastReceiver

$
0
0
Base class for code that will receive intents sent by sendBroadcast().
--BroadcastReceiver是够接收sendBroadcast方法发送的intent的基类。


If you don't need to send broadcasts across applications, consider using this class with LocalBroadcastManager instead of the more general facilities described below. This will give you a much more efficient implementation (no cross-process communication needed) and allow you to avoid thinking about any security issues related to other applications being able to receive or send your broadcasts.
--如果你不需要在app间传递broadcasts,那么可以考虑使用LocalBroadcastManager


You can either dynamically register an instance of this class with Context.registerReceiver() or statically publish an implementation through the <receiver> tag in your AndroidManifest.xml.
--你可以动态的注册实例,使用Context.registerReceiver() ,或者静态的注册 在AndroidManifest.xml 中使用 <receiver> 的标签。


Note:    If registering a receiver in your Activity.onResume() implementation, you should unregister it in Activity.onPause(). (You won't receive intents when paused, and this will cut down on unnecessary system overhead). Do not unregister in Activity.onSaveInstanceState(), because this won't be called if the user moves back in the history stack.
-- 如果在activity 中的onResume方法中注册了receiver ,那么就需要在onPause 方法中移除receiver的注册,(在paused状态下,你不能接收intent ,这样可以减少不必要的系统开销),不要在onSaveInstanceState()方法中移除receiver的注册,因为在用户回退到历史栈中的时候,这个方法不会被调用


There are two major classes of broadcasts that can be received:
-- 下面有2个主要的broadcasts的类可以接受intent

Normal broadcasts (sent with Context.sendBroadcast) are completely asynchronous. All receivers of the broadcast are run in an undefined order, often at the same time. This is more efficient, but means that receivers cannot use the result or abort APIs included here.
-- 普通的broadcasts(调用Context.sendBroadcast)是完全异步的,所有的接收者都是无序的,通常在同一时间(指的接收消息),这是很有效率的,不能使用result 和 终止广播的api


Ordered broadcasts (sent with Context.sendOrderedBroadcast) are delivered to one receiver at a time. As each receiver executes in turn, it can propagate a result to the next receiver, or it can completely abort the broadcast so that it won't be passed to other receivers. The order receivers run in can be controlled with the android:priority attribute of the matching intent-filter; receivers with the same priority will be run in an arbitrary order.
-- 有序的broadcasts(调用Context.sendOrderedBroadcast )可以在同一时间只提交一个receiver,每个receiver 依次执行,可以传递结果给另一个receiver,也可以终止这个广播,这样的话,就不会传递给别的receiver,有序的接受者可以通过android:priority 被控制来适配intent-filter,如果他们的优先权的权重一致,那么他们的运行就是无序的。



Even in the case of normal broadcasts, the system may in some situations revert to delivering the broadcast one receiver at a time. In particular, for receivers that may require the creation of a process, only one will be run at a time to avoid overloading the system with new processes. In this situation, however, the non-ordered semantics hold: these receivers still cannot return results or abort their broadcast.


Note that, although the Intent class is used for sending and receiving these broadcasts, the Intent broadcast mechanism here is completely separate from Intents that are used to start Activities with Context.startActivity(). There is no way for a BroadcastReceiver to see or capture Intents used with startActivity(); likewise, when you broadcast an Intent, you will never find or start an Activity. These two operations are semantically very different: starting an Activity with an Intent is a foreground operation that modifies what the user is currently interacting with; broadcasting an Intent is a background operation that the user is not normally aware of.

The BroadcastReceiver class (when launched as a component through a manifest's <receiver> tag) is an important part of an application's overall lifecycle.
-- 如果使用了<receiver>注册,那么receiver的生命周期就是application级的。


Developer Guides
For information about how to use this class to receive and resolve intents, read the Intents and Intent Filters developer guide.

Security
Receivers used with the Context APIs are by their nature a cross-application facility, so you must consider how other applications may be able to abuse your use of them. Some things to consider are:
-- receiver 是在app间可以被调用的,可能会造成滥用,所以需要考虑下面的事:


The Intent namespace is global. Make sure that Intent action names and other strings are written in a namespace you own, or else you may inadvertently conflict with other applications.



When you use registerReceiver(BroadcastReceiver, IntentFilter), any application may send broadcasts to that registered receiver. You can control who can send broadcasts to it through permissions described below.
When you publish a receiver in your application's manifest and specify intent-filters for it, any other application can send broadcasts to it regardless of the filters you specify. To prevent others from sending to it, make it unavailable to them with android:exported="false".
When you use sendBroadcast(Intent) or related methods, normally any other application can receive these broadcasts. You can control who can receive such broadcasts through permissions described below. Alternatively, starting with ICE_CREAM_SANDWICH, you can also safely restrict the broadcast to a single application with Intent.setPackage
None of these issues exist when using LocalBroadcastManager, since intents broadcast it never go outside of the current process.

Access permissions can be enforced by either the sender or receiver of a broadcast.

To enforce a permission when sending, you supply a non-null permission argument to sendBroadcast(Intent, String) or sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle). Only receivers who have been granted this permission (by requesting it with the <uses-permission> tag in their AndroidManifest.xml) will be able to receive the broadcast.

To enforce a permission when receiving, you supply a non-null permission when registering your receiver -- either when calling registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler) or in the static <receiver> tag in your AndroidManifest.xml. Only broadcasters who have been granted this permission (by requesting it with the <uses-permission> tag in their AndroidManifest.xml) will be able to send an Intent to the receiver.

See the Security and Permissions document for more information on permissions and security in general.
Receiver Lifecycle
A BroadcastReceiver object is only valid for the duration of the call to onReceive(Context, Intent). Once your code returns from this function, the system considers the object to be finished and no longer active.

This has important repercussions to what you can do in an onReceive(Context, Intent) implementation: anything that requires asynchronous operation is not available, because you will need to return from the function to handle the asynchronous operation, but at that point the BroadcastReceiver is no longer active and thus the system is free to kill its process before the asynchronous operation completes.

In particular, you may not show a dialog or bind to a service from within a BroadcastReceiver. For the former, you should instead use the NotificationManager API. For the latter, you can use Context.startService() to send a command to the service.
Process Lifecycle
A process that is currently executing a BroadcastReceiver (that is, currently running the code in its onReceive(Context, Intent) method) is considered to be a foreground process and will be kept running by the system except under cases of extreme memory pressure.

Once you return from onReceive(), the BroadcastReceiver is no longer active, and its hosting process is only as important as any other application components that are running in it. This is especially important because if that process was only hosting the BroadcastReceiver (a common case for applications that the user has never or not recently interacted with), then upon returning from onReceive() the system will consider its process to be empty and aggressively kill it so that resources are available for other more important processes.

This means that for longer-running operations you will often use a Service in conjunction with a BroadcastReceiver to keep the containing process active for the entire time of your operation.

作者:cocoooooa 发表于2016/10/28 17:57:16 原文链接
阅读:39 评论:0 查看评论

【Android】掌握自定义LayoutManager(二) 实现流式布局

$
0
0

转载请标明出处:
http://blog.csdn.net/zxt0601/article/details/52956504
本文出自:【张旭童的博客】

本系列文章相关代码传送门:
自定义LayoutManager实现的流式布局
欢迎star,pr,issue。

本系列文章目录:
掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
掌握自定义LayoutManager(二) 实现流式布局

一 概述

在开始之前,我想说,如果需求是每个Item宽高一样,实现起来复杂度比每个Item宽高不一样的,要小10+倍。
然而我们今天要实现的流式布局,恰巧就是至少每个Item的宽度不一样,所以在计算坐标的时候算的我死去活来。先看一下效果图:
这里写图片描述
艾玛,换成妹子图后貌似好看了许多,我都不认识它了,好吧,项目里它一般长下面这样:
这里写图片描述
往常这种效果,我们一般使用自定义ViewGroup实现,我以前也写了一个。自定义VG实现流式布局
这不最近再研究自定义LayoutManager么,想来想去也没有好的创意,就先拿它开第一刀吧。
(后话:流式布局Item宽度不一,不知不觉给自己挖了个大坑,造成拓展一些功能难度倍增,观之网上的DEMO,99%Item的大小都是一样的,so,这个系列的下一篇我计划 实现一个Item大小一样 的酷炫LayoutManager。但是最终做成啥样的效果还没想好,有朋友看到酷炫的效果可以告诉我,我去高仿一个。)

自定义LayoutManager的步骤:

以本文的流式布局为例,需求是一个垂直滚动的布局,子View以流式排列。先总结一下步骤:

一 实现 generateDefaultLayoutParams()
二 实现 onLayoutChildren()
三 竖直滚动需要 重写canScrollVertically()和scrollVerticallyBy()

下面我们就一步一步来吧。

二 实现generateDefaultLayoutParams()

如果没有特殊需求,大部分情况下,我们只需要如下重写该方法即可。

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

RecyclerView.LayoutParams是继承自android.view.ViewGroup.MarginLayoutParams的,所以可以方便的使用各种margin。

这个方法最终会在recycler.getViewForPosition(i)时调用到,在该方法浩长源码的最下方:

            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
            //这里会调用mLayout.generateDefaultLayoutParams()为每个ItemView设置LayoutParams
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrap && bound;
            return holder.itemView;

重写完这个方法就能编译通过了,只不过然并卵,界面上是一片空白,下面我们就走进onLayoutChildren()方法 ,为界面添加Item。

注:99%用不到的情况:如果需要存储一些额外的东西在LayoutParams里,这里返回你自定义的LayoutParams即可。
当然,你自定义的LayoutParams需要继承自RecyclerView.LayoutParams

三 onLayoutChildren()

该方法是LayoutManager的入口。它会在如下情况下被调用:
1 在RecyclerView初始化时,会被调用两次
2 在调用adapter.notifyDataSetChanged()时,会被调用。
3 在调用setAdapter替换Adapter时,会被调用。
4 在RecyclerView执行动画时,它也会被调用。
即RecyclerView 初始化数据源改变时 都会被调用。
(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)

在系列开篇我已经提到,它相当于ViewGroup的onLayout()方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:

    private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断
    private int mFirstVisiPos;//屏幕可见的第一个View的Position
    private int mLastVisiPos;//屏幕可见的最后一个View的Position
        @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {//没有Item,界面空着吧
            detachAndScrapAttachedViews(recycler);
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的
            return;
        }
        //onLayoutChildren方法在RecyclerView 初始化时 会执行两遍
        detachAndScrapAttachedViews(recycler);
        //初始化
        mVerticalOffset = 0;
        mFirstVisiPos = 0;
        mLastVisiPos = getItemCount();

        //初始化时调用 填充childView
        fill(recycler, state);
    }

这个fill(recycler, state);方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务:
在考虑滑动位移的情况下:
1 回收所有屏幕不可见的子View
2 layout所有可见的子View

在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.

            int topOffset = getPaddingTop();//布局时的上偏移
            int leftOffset = getPaddingLeft();//布局时的左偏移
            int lineMaxHeight = 0;//每一行最大的高度
            int minPos = mFirstVisiPos;//初始化时,我们不清楚究竟要layout多少个子View,所以就假设从0~itemcount-1
            mLastVisiPos = getItemCount() - 1;
            //顺序addChildView
            for (int i = minPos; i <= mLastVisiPos; i++) {
                //找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChildWithMargins(child, 0, 0);
                //计算宽度 包括margin
                if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
                    layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

                    //改变 left  lineHeight
                    leftOffset += getDecoratedMeasurementHorizontal(child);
                    lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
                } else {//当前行排列不下
                    //改变top  left  lineHeight
                    leftOffset = getPaddingLeft();
                    topOffset += lineMaxHeight;
                    lineMaxHeight = 0;

                    //新起一行的时候要判断一下边界
                    if (topOffset - dy > getHeight() - getPaddingBottom()) {
                        //越界了 就回收
                        removeAndRecycleView(child, recycler);
                        mLastVisiPos = i - 1;
                    } else {
                        layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

                        //改变 left  lineHeight
                        leftOffset += getDecoratedMeasurementHorizontal(child);
                        lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
                    }
                }
            }

用到的一些工具函数(在系列开篇已介绍过):

    //模仿LLM Horizontal 源码

    /**
     * 获取某个childView在水平方向所占的空间
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /**
     * 获取某个childView在竖直方向所占的空间
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }

    public int getVerticalSpace() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

    public int getHorizontalSpace() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }

如上编写一个超级简单的fill()方法,运行,你的程序应该就能看到流式布局的效果出现了。
可是千万别开心,因为痛苦的计算远没到来。
如果这些都看不懂,那么我建议:
一,直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。
二,去学习一下自定义ViewGroup的知识。

此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。

四,动起来

想让我们自定义的LayoutManager动起来,最简单的写法如下:

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复

        offsetChildrenVertical(-realOffset);

        return realOffset;
    }

offsetChildrenVertical(-realOffset);这句话移动所有的childView.
返回值会被RecyclerView用来判断是否达到边界, 如果返回值!=传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。

写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。

好了,下面开始正经的写它吧,

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //位移0、没有子View 当然不移动
        if (dy == 0 || getChildCount() == 0) {
            return 0;
        }

        int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复
        //边界修复代码
        if (mVerticalOffset + realOffset < 0) {//上边界
            realOffset = -mVerticalOffset;
        } else if (realOffset > 0) {//下边界
            //利用最后一个子View比较修正
            View lastChild = getChildAt(getChildCount() - 1);
            if (getPosition(lastChild) == getItemCount() - 1) {
                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
                if (gap > 0) {
                    realOffset = -gap;
                } else if (gap == 0) {
                    realOffset = 0;
                } else {
                    realOffset = Math.min(realOffset, -gap);
                }
            }
        }

        realOffset = fill(recycler, state, realOffset);//先填充,再位移。

        mVerticalOffset += realOffset;//累加实际滑动距离

        offsetChildrenVertical(-realOffset);//滑动

        return realOffset;
    }

这里用realOffset变量保存实际的位移,也是return 回去的值。大部分情况下它=dy。
在边界处,为了防止越界,做了一些处理,realOffset 可能不等于dy。
别的文章不同的是,我参考了LinearLayoutManager的源码,先考虑滑动位移进行View的回收、填充(fill()函数),然后再真正的位移这些子Item。


fill()的过程中

流程:

一 会先考虑到dy回收界面上不可见的Item。
填充布局子View
三 判断是否将dy都消费掉了,如果消费不掉:例如滑动距离太多,屏幕上的View已经填充完了,仍有空白,那么就要修正dy给realOffset。

注意事项一:考虑滑动的方向

在填充布局子View的时候,还要考虑滑动的方向,即填充的顺序,是从头至尾填充,还是从尾至头部填充。
如果是向底部滑动,那么是顺序填充,显示底端position更大的Item。( dy>0)
如果是向顶部滑动,那么是逆序填充,显示顶端positon更小的Item。(dy<0)

注意事项二:流式布局 逆序布局子View的问题

再啰嗦最后一点,我们想象一下这个逆序填充的过程:
正序过程可以自上而下,自左向右layout 子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度,如果超过了就另起一行。
逆序时,有两种方案:

1 利用Rect保存子View边界

正序排列时,保存每个子View的Rect
逆序时,直接拿出来,layout

2 逆序化

自右向左layout子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度,
如果超过了就另起一行。并且判断最后一个子View距离父控件左边的offset,平移这一行的所有子View,较复杂,采用方案1.
(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。)
下面上码:

private SparseArray<Rect> mItemRects;//key 是View的position,保存View的bounds ,
/**
     * 填充childView的核心方法,应该先填充,再移动。
     * 在填充时,预先计算dy的在内,如果View越界,回收掉。
     * 一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.
     *
     * @param recycler
     * @param state
     * @param dy       RecyclerView给我们的位移量,+,显示底端, -,显示头部
     * @return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)
     */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {

        int topOffset = getPaddingTop();

        //回收越界子View
        if (getChildCount() > 0) {//滑动时进来的
            for (int i = getChildCount() - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (dy > 0) {//需要回收当前屏幕,上越界的View
                    if (getDecoratedBottom(child) - dy < topOffset) {
                        removeAndRecycleView(child, recycler);
                        mFirstVisiPos++;
                        continue;
                    }
                } else if (dy < 0) {//回收当前屏幕,下越界的View
                    if (getDecoratedTop(child) - dy > getHeight() - getPaddingBottom()) {
                        removeAndRecycleView(child, recycler);
                        mLastVisiPos--;
                        continue;
                    }
                }
            }
            //detachAndScrapAttachedViews(recycler);
        }

        int leftOffset = getPaddingLeft();
        int lineMaxHeight = 0;
        //布局子View阶段
        if (dy >= 0) {
            int minPos = mFirstVisiPos;
            mLastVisiPos = getItemCount() - 1;
            if (getChildCount() > 0) {
                View lastView = getChildAt(getChildCount() - 1);
                minPos = getPosition(lastView) + 1;//从最后一个View+1开始吧
                topOffset = getDecoratedTop(lastView);
                leftOffset = getDecoratedRight(lastView);
                lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(lastView));
            }
            //顺序addChildView
            for (int i = minPos; i <= mLastVisiPos; i++) {
                //找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChildWithMargins(child, 0, 0);
                //计算宽度 包括margin
                if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
                    layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

                    //保存Rect供逆序layout用
                    Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
                    mItemRects.put(i, rect);

                    //改变 left  lineHeight
                    leftOffset += getDecoratedMeasurementHorizontal(child);
                    lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
                } else {//当前行排列不下
                    //改变top  left  lineHeight
                    leftOffset = getPaddingLeft();
                    topOffset += lineMaxHeight;
                    lineMaxHeight = 0;

                    //新起一行的时候要判断一下边界
                    if (topOffset - dy > getHeight() - getPaddingBottom()) {
                        //越界了 就回收
                        removeAndRecycleView(child, recycler);
                        mLastVisiPos = i - 1;
                    } else {
                        layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

                        //保存Rect供逆序layout用
                        Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
                        mItemRects.put(i, rect);

                        //改变 left  lineHeight
                        leftOffset += getDecoratedMeasurementHorizontal(child);
                        lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
                    }
                }
            }
            //添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
            View lastChild = getChildAt(getChildCount() - 1);
            if (getPosition(lastChild) == getItemCount() - 1) {
                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
                if (gap > 0) {
                    dy -= gap;
                }

            }

        } else {
            /**
             * ##  利用Rect保存子View边界
             正序排列时,保存每个子View的Rect,逆序时,直接拿出来layout。
             */
            int maxPos = getItemCount() - 1;
            mFirstVisiPos = 0;
            if (getChildCount() > 0) {
                View firstView = getChildAt(0);
                maxPos = getPosition(firstView) - 1;
            }
            for (int i = maxPos; i >= mFirstVisiPos; i--) {
                Rect rect = mItemRects.get(i);

                if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {
                    mFirstVisiPos = i + 1;
                    break;
                } else {
                    View child = recycler.getViewForPosition(i);
                    addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
                    measureChildWithMargins(child, 0, 0);

                    layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
                }
            }
        }


        Log.d("TAG", "count= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size() + ", dy:" + dy + ",  mVerticalOffset" + mVerticalOffset+", ");

        return dy;
    }

思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。
值得一提的是,可以通过getChildCount()recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0.
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。
原因在系列开篇也提过,不再赘述。

至此我们的自定义LayoutManager已经可以用了,使用的效果就和文首的两张图一模一样。

下面再提及一些其他注意点和适配事项:

五 适配notifyDataSetChanged()

此时会回调onLayoutChildren()函数。因为我们流式布局的特殊性,每个Item的宽度不一致,所以化简处理,每次这里归零。

        //初始化区域
        mVerticalOffset = 0;
        mFirstVisiPos = 0;
        mLastVisiPos = getItemCount();

如果每个Item的大小都一样,逆序顺序layoutChild都比较好处理,则应该在此判断,getChildCount(),大于0说明是DatasetChanged()操作,(初始化的第二次也会childCount>0)。根据当前记录的position和位移信息去fill视图即可。

六 适配 Adapter的替换。

我根据24.2.1源码,发现网上的资料对这里的处理其实是不必要的。

一 资料中的做法如下:

当对RecyclerView设置一个新的Adapter时,onAdapterChanged()方法会被回调,一般的做法是在这里remove掉所有的View。此时onLayoutChildren()方法会被再次调用,一个新的轮回开始。

    @Override
    public void onAdapterChanged(final RecyclerView.Adapter oldAdapter, final RecyclerView.Adapter newAdapter) {
        removeAllViews();
    }

二 我的新观点:

通过查看源码+打断点跟踪分析,调用RecyclerView.setAdapter后,调用顺序依次为

1 Recycler.setAdapter():

    public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true); //张旭童注:注意第三个参数是true
        requestLayout();
    }

那么我们查看setAdapterInternal()方法:

private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        ...
        //张旭童注:removeAndRecycleViews 参数此时为ture
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            ...
            if (mLayout != null) {
             //张旭童注: 所以如果我们更换Adapter时,mLayout不为空,会先执行如下操作,
                mLayout.removeAndRecycleAllViews(mRecycler);
                mLayout.removeAndRecycleScrapInt(mRecycler);
            }
            // we should clear it here before adapters are swapped to ensure correct callbacks.
            //张旭童注:而且还会清空Recycler的缓存
            mRecycler.clear();
        }
        ...
        if (mLayout != null) {
        //张旭童注:这里才调用的LayoutManager的方法
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        //张旭童注:这里调用Recycler的方法
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        ...
    }

也就是说 更换Adapter一开始,还没有执行到LayoutManager.onAdapterChanged()界面上的View都已经被remove掉了,我们的操作属于多余的

2 LayoutManager.onAdapterChanged()

空实现:也没必要实现了

        public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
        }

3 Recycler.onAdapterChanged():

该方法先清空scapCache区域(貌似也是多余,一开始被清空过了),然后调用RecyclerViewPool.onAdapterChanged()

        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
                boolean compatibleWithPrevious) {
            clear();
            getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
        }

        public void clear() {
            mAttachedScrap.clear();
            recycleAndClearCachedViews();
        }

4 RecyclerViewPool.onAdapterChanged()

如果没有别的Adapter在用这个RecyclerViewPool,会清空RecyclerViewPool的缓存。

        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
                boolean compatibleWithPrevious) {
            if (oldAdapter != null) {
                detach();
            }
            if (!compatibleWithPrevious && mAttachCount == 0) {
                clear();
            }
            if (newAdapter != null) {
                attach(newAdapter);
            }
        }

5 LayoutManager.onLayoutChildren()

新的布局开始。

七 总结:

引用一段话

They are also extremely complex, and hard to get right. For every amount of effort RecyclerView requires of you, it is doing 10x more behind the scenes.

本文Demo仍有很大完善空间,有些需要完善的细节非常复杂,需要经过多次试验才能得到正确的结果(这里我更加敬佩Google提供的三个LM)。每一个我们想要实现的需求,可能要花费比我们想象的时间*10倍的时间。
上篇也提及到的,不要过度优化,达成需求就好。

可以通过getChildCount()recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0.
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。

感兴趣的同学可以对网上的各个Demo打印他们onCreateViewHolder执行的次数,以及上述两个参数的值,和官方的LayoutManager比较,这三个参数先达标,才算是及格的LayoutManager,但后续优化之路仍很长。

本系列文章相关代码传送门:
自定义LayoutManager实现的流式布局
欢迎star,pr,issue。

作者:zxt0601 发表于2016/10/28 17:58:17 原文链接
阅读:79 评论:0 查看评论

android 自定义View弯曲滑竿指示器

$
0
0

android 自定义弯曲滑竿指示器

效果说明:滑竿指示器,是一段弯曲的圆弧,要求在杆上,有滑动小球事件,小球会根据下标文字的起始角度与终止角度,是否选择滑倒下一个位置。当点击下标文字时,小球也要做出相应的指示。

1)MainActivity

package com.example.chenkui.myapplication;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intIndicatorTabView();

//        setContentView(R.layout.watch_view);

    }
    private void intIndicatorTabView() {
        IndicatorTabView view = (IndicatorTabView) findViewById(R.id.myView);
        List<String> list = new ArrayList<>();
        list.add("待评价");
        list.add("待付款");
        list.add("待发货");
        list.add("待收货");
        view.setTabInfo(list);
        view.setSelection(3);//初始化选择指示器小球位置;
        view.setTabChangeListener(new IndicatorTabView.OnTabChangeListener() {
            @Override
            public void onTabSelected(View v, int position) {

            }
        });
    }
}

2)IndicatorTabView
在绘制时,按照图层的结构,先绘制底层颜色,再绘制上一层图形,对于特殊绘制的,如旋转画布,移动绘制圆心坐标,一般先 canvas.save();保存已经绘制图层,canvas.restore();//他的作用为,将之前的绘制保存的图片save(),进行合并.

package com.example.chenkui.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;

public class IndicatorTabView extends View {
    private String TAG = IndicatorTabView.class.getSimpleName();
    private static final float DEFAULT_SWEEP_ANGLE = 48.0f;
    private float mSweepAngle = DEFAULT_SWEEP_ANGLE;
    private float mStartAngle = (180.0f - mSweepAngle) / 2;
    private List<IndicatorTabItem> mTabItems = new ArrayList<>();
    private int mSelectTabIndex = -1;
    private Paint mTabBackColorPaint;
    private Paint mTabPaint;
    private Paint mTabTtileTextPaint;//滑竿或点击标题

    private Paint mTabWheelPaint;
    private Paint mTabTextPaint;
    private Paint mTabPointerPaint;

    private float mWheelCenterX;
    private float mWheelCenterY;
    private float mWheelRadius;
    private RectF mWheelArcRect;

    private float mPointerAngle;
    private float mPointerRadius;
    private boolean mIsMovingPointer = false;
    private OnTabChangeListener mTabChangeListener = null;

    private List<IndicatorTabRectItem> mTabRectItem = new ArrayList<IndicatorTabRectItem>();
    private Paint textPaint = new Paint();
    private float mMinRectRadius;//文字区域,
    private float mMaxRectRadius;//文字区域,


    public IndicatorTabView(Context context) {
        this(context, null);
    }

    public IndicatorTabView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public IndicatorTabView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        mTabBackColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//绘制最大的里边图层背景
        mTabBackColorPaint.setStyle(Paint.Style.FILL);
        mTabBackColorPaint.setColor(Color.argb(255, 35, 47, 62));

        mTabPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//绘制里面最小图层背景。
        mTabPaint.setStyle(Paint.Style.FILL);
        mTabPaint.setColor(Color.argb(255, 253,253, 254));

        mTabTtileTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//绘制里面最小图层背景。
        mTabTtileTextPaint.setStyle(Paint.Style.FILL);
        mTabTtileTextPaint.setColor(Color.WHITE);

        mTabWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTabWheelPaint.setStyle(Paint.Style.STROKE);
        mTabWheelPaint.setStrokeWidth(getResources().getDimension(R.dimen.tab_wheel_width));
        mTabWheelPaint.setColor(Color.argb(200, 253, 250, 245));
        mTabWheelPaint.setStrokeCap(Paint.Cap.ROUND);//这个是设置绘制弧是,两端圆滑;

        mTabTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//绘制文字
        mTabTextPaint.setColor(Color.argb(255, 253, 253, 254));
        mTabTextPaint.setTextSize(getResources().getDimension(R.dimen.tab_text));
        mTabTextPaint.setStrokeWidth(5);

        mTabPointerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//绘制小点
        mTabPointerPaint.setStyle(Paint.Style.FILL);
        mTabPointerPaint.setColor(Color.argb(255, 255, 255, 255));
        mPointerRadius = getResources().getDimension(R.dimen.tab_pointer_radius);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制深蓝背景。
        canvas.drawCircle(mWheelCenterX, mWheelCenterY - getResources().getDimension(R.dimen.tab_wheel_padding_inner), mWheelRadius*1.2f, mTabBackColorPaint);

        //绘制里面第一条弧
        canvas.drawCircle(mWheelCenterX, mWheelCenterY - getResources().getDimension(R.dimen.tab_wheel_padding_inner), mWheelRadius, mTabPaint);

        canvas.drawArc(mWheelArcRect, mStartAngle, mSweepAngle, false, mTabWheelPaint);//绘制滑杆

        for (int i = 0; i < mTabItems.size(); i++) {
            IndicatorTabItem tempItem = mTabItems.get(i);
            float angle = (tempItem.getStartAngle() + tempItem.getEndAngle()) / 2 - 90.0f;
            canvas.save();//将之前绘制图片保存起来,
            canvas.rotate(angle, mWheelCenterX, mWheelCenterY);
            canvas.drawText(tempItem.getName(), mWheelCenterX - tempItem.getMesureWidth() / 2,
                    getResources().getDimension(R.dimen.tab_wheel_width) + mWheelCenterY + mWheelRadius + getResources().getDimension(R.dimen.tab_wheel_padding_inner),
                    mTabTextPaint);
            /***********************************************************************************************************/
            Log.d(TAG, "-----------onDraw()-----------" + "X===[" + (mWheelCenterX - tempItem.getMesureWidth() / 2) + "]-------Y==={" + (getResources().getDimension(R.dimen.tab_wheel_width) + mWheelCenterY + mWheelRadius + getResources().getDimension(R.dimen.tab_wheel_padding_inner)) + "}");

            float rectWidth = mTabTextPaint.measureText(tempItem.getName());
            Paint.FontMetrics fm = mTabTextPaint.getFontMetrics();

            float offsetAscent = fm.ascent;
            float offsetBottom = fm.bottom;
            float startX = mWheelCenterX - tempItem.getMesureWidth() / 2;
            float startY = getResources().getDimension(R.dimen.tab_wheel_width) + mWheelCenterY + mWheelRadius + getResources().getDimension(R.dimen.tab_wheel_padding_inner);
            Log.d(TAG, "TEXT-offsetAscent=" + offsetAscent);
//            RectF testRect = new RectF(
//                    startX,
//                    (float) (startY + offsetAscent),
//                    (float) (startX + rectWidth),
//                    (float) (startY + offsetBottom)
//            );
//            textPaint.setColor(Color.argb(100, 233, 233, 0));
//            canvas.drawRect(testRect, textPaint);
/*************************************************************************************************************************/
            canvas.restore();//他的作用为,将之前的绘制保存的图片save(),进行合并.

            mMinRectRadius = distance(mWheelCenterX, startY + offsetAscent);//计算
            mMaxRectRadius = distance(mWheelCenterX, startY + offsetBottom);


        }

        float[] pointerPosition = calculatePointerPosition(mPointerAngle);
        canvas.drawCircle(mWheelCenterX + pointerPosition[0], mWheelCenterY + pointerPosition[1], mPointerRadius, mTabPointerPaint);//绘制小球
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        mWheelRadius = (float) (widthSize / (2.0f * Math.sin(Math.toRadians(32))));

        int offset = (int) (heightSize - getResources().getDimension(R.dimen.tab_wheel_padding_bottom));

        mWheelCenterY = offset - (int) Math.sqrt(Math.pow((double) mWheelRadius, 2) - Math.pow((double) (widthSize / 2), 2));

        mWheelCenterX = widthSize / 2.0f;

        mWheelArcRect = new RectF(mWheelCenterX - mWheelRadius, mWheelCenterY - mWheelRadius,
                mWheelCenterX + mWheelRadius, mWheelCenterY + mWheelRadius);

    }

    private float[] calculatePointerPosition(float angle) {
        float x = (float) (mWheelRadius * Math.cos(Math.toRadians(angle)));
        float y = (float) (mWheelRadius * Math.sin(Math.toRadians(angle)));

        return new float[]{x, y};
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX() - mWheelCenterX;
        float y = event.getY() - mWheelCenterY;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float[] pointerPosition = calculatePointerPosition(mPointerAngle);
                if (x >= (pointerPosition[0] - mPointerRadius * 2)
                        && x <= (pointerPosition[0] + mPointerRadius * 2)
                        && y >= (pointerPosition[1] - mPointerRadius * 2)
                        && y <= (pointerPosition[1] + mPointerRadius * 2)) {
                    mIsMovingPointer = true;
                    return true;
                }
                float pointerLength = distanceRelative(x, y);//计算触摸点距离圆心的坐标:
                //计算文本触摸区域的顶部距离圆心的坐标的距离:
                //计算文本触摸区域的底部距离圆心的坐标的距离;
                //计算触摸点的角度,

                float tempPointerRectFAngle = (float) Math.toDegrees(Math.atan2(y, x));//文本触摸区域的的角度
                if (pointerLength >= mMinRectRadius - mPointerRadius && pointerLength <= mMaxRectRadius + mPointerRadius) {
                    int willSelectedIndex = -1;
                    for (int i = 0; i < mTabRectItem.size(); i++) {
                        IndicatorTabRectItem item = mTabRectItem.get(i);
                        if (tempPointerRectFAngle >= item.getStartAngle() && tempPointerRectFAngle <= item.getEndAngle()) {
                            willSelectedIndex = i;
                            break;
                        }
                    }
                    if (mSelectTabIndex != willSelectedIndex) {
                        if (mTabChangeListener != null) {
                            mTabChangeListener.onTabSelected(this, willSelectedIndex);
                        }
                    }
                    setSelection(willSelectedIndex);
                    invalidate();
                    return true;
                }


                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsMovingPointer) {
                    float tempPointerAngle = (float) Math.toDegrees(Math.atan2(y, x));
                    if (tempPointerAngle >= mTabItems.get(0).getStartAngle()
                            && tempPointerAngle <= mTabItems.get(mTabItems.size() - 1).getEndAngle()) {
                        mPointerAngle = tempPointerAngle;
                        invalidate();
                    }

                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsMovingPointer) {
                    mIsMovingPointer = false;
                    smoothMove();
                    return true;
                }
                break;
        }

        return false;
    }

    /**
     * 根据触摸点距离圆心的距离,与每一块的活动角度,确定惟一的文本触摸块。
     */
//    private void calculateTouchRect(float x, float y) {
////        =x-mWheelCenterX;
//        float dpow2 = (float) (Math.pow((x - mWheelCenterX), 2) + Math.pow((y - mWheelCenterY), 2));
//        float d = (float) Math.sqrt(dpow2);
//        Log.d(TAG, "TouchRect---d=" + d);
//        Log.d(TAG, "mMinRectRadius====" + mMinRectRadius);
//        Log.d(TAG, "mMAXRectRadius====" + mMaxRectRadius);
//
//
//    }

    /**
     * 相对与(x-mWheelCenterX,y-mWheelCenterY)坐标,
     * 计算触摸点。求两点间距离,此时的圆心为
     */
    public float distanceRelative(float x, float y) {
        float dx = Math.abs(x);
        float dy = Math.abs(y);
        Log.d(TAG, "distance---d=" + (float) Math.hypot(dx, dy));
        Log.d(TAG, "mMinRectRadius====" + mMinRectRadius);
        Log.d(TAG, "mMAXRectRadius====" + mMaxRectRadius);
        return (float) Math.hypot(dx, dy);
    }

    /**
     * 计算触摸点。求两点间距离
     */
    public float distance(float x, float y) {
        float dx = Math.abs(x - mWheelCenterX);
        float dy = Math.abs(y - mWheelCenterY);
        Log.d(TAG, "distance---d=" + (float) Math.hypot(dx, dy));
        Log.d(TAG, "mMinRectRadius====" + mMinRectRadius);
        Log.d(TAG, "mMAXRectRadius====" + mMaxRectRadius);
        return (float) Math.hypot(dx, dy);
    }


    private void smoothMove() {
        int willSelectedIndex = -1;
        for (int i = 0; i < mTabItems.size(); i++) {
            IndicatorTabItem item = mTabItems.get(i);
            if (mPointerAngle >= item.getStartAngle() && mPointerAngle <= item.getEndAngle()) {
                willSelectedIndex = i;
                break;
            }
        }

        if (mSelectTabIndex != willSelectedIndex) {
            if (mTabChangeListener != null) {
                mTabChangeListener.onTabSelected(this, willSelectedIndex);
            }
        }

        setSelection(willSelectedIndex);
    }

    public void setTabInfo(List<String> tabInfo) {
        if (tabInfo == null && (tabInfo.size() == 0 && tabInfo.size() > 4)) {
            return;
        }

        float totalPercent = 0.0f;
        for (int i = 0; i < tabInfo.size(); i++) {
            IndicatorTabItem item = new IndicatorTabItem();
            item.setName(tabInfo.get(i));
            item.setMesureWidth(mTabTextPaint.measureText(item.getName()));

            Log.d(TAG, "--------setTabInfo()-------" + item.getName());
            totalPercent += item.getMesureWidth();
            mTabItems.add(item);
        }

        float startAngle = mStartAngle;
        for (int i = 0; i < mTabItems.size(); i++) {
            IndicatorTabItem tempItem = mTabItems.get(i);
            float itemSweepAngle = mSweepAngle * tempItem.getMesureWidth() / totalPercent;
            tempItem.setStartAngle(startAngle);
            tempItem.setEndAngle(startAngle + itemSweepAngle);
            startAngle += itemSweepAngle;
            Log.d(TAG, "startAngle" + i + "======" + startAngle);
            Log.d(TAG, "EndAngle" + i + "======" + (startAngle + itemSweepAngle));
        }
        setSelection(0);

        initIndicatorTabRectItem(tabInfo);
    }


    private void initIndicatorTabRectItem(List<String> tabInfo) {
        if (tabInfo == null && (tabInfo.size() == 0 && tabInfo.size() > 4)) {
            return;
        }
        float totalPercent = 0.0f;
        for (int i = 0; i < tabInfo.size(); i++) {
            IndicatorTabRectItem rectItem = new IndicatorTabRectItem();
            rectItem.setRectName(tabInfo.get(i));
            rectItem.setRectWidth(mTabTextPaint.measureText(rectItem.getRectName()));
            totalPercent += rectItem.getRectWidth();
            Log.d(TAG, "----initIndicatorTabRectItem--------setRectWidth" + i + "======" + mTabTextPaint.measureText(rectItem.getRectName()));
            mTabRectItem.add(rectItem);
        }
        float startAngle = mStartAngle;

        for (int i = 0; i < mTabRectItem.size(); i++) {
            IndicatorTabRectItem tempItem = mTabRectItem.get(i);
            float itemSweepAngle = mSweepAngle * tempItem.getRectWidth() / totalPercent;

            tempItem.setStartAngle(startAngle);
            tempItem.setEndAngle(startAngle + itemSweepAngle);
            startAngle += itemSweepAngle;
            Log.d(TAG, "----initIndicatorTabRectItem-------startAngle" + i + "======" + startAngle);
            Log.d(TAG, "----initIndicatorTabRectItem-------EndAngle" + i + "======" + (startAngle + itemSweepAngle));
        }
    }

    /**
     * 设置选择小点的每一段中心点角度。
     *
     * @param index
     */
    public void setSelection(int index) {
        if (index < 0 || index > mTabItems.size() - 1) {
            return;
        }

        mSelectTabIndex = index;
        mPointerAngle = (mTabItems.get(mSelectTabIndex).getStartAngle() + mTabItems.get(mSelectTabIndex).getEndAngle()) / 2;
        invalidate();
    }

    public void setTabChangeListener(OnTabChangeListener tabChangeListener) {
        this.mTabChangeListener = tabChangeListener;
    }

    public interface OnTabChangeListener {
        void onTabSelected(View v, int position);
    }
}

3)IndicatorTabRectItem

package com.example.chenkui.myapplication;

/**
 * Created by chenkui on 2016/10/11.
 */
public class IndicatorTabRectItem {
    private float startX;
    private float endX;
    private float startY;
    private float endY;
    private float rectWidth;
    private float rectHeigth;
    private  String rectName;

    private float startAngle;
    private float endAngle;


    public float getStartX() {
        return startX;
    }

    public void setStartX(float startX) {
        this.startX = startX;
    }

    public float getEndX() {
        return endX;
    }

    public void setEndX(float endX) {
        this.endX = endX;
    }

    public float getStartY() {
        return startY;
    }

    public void setStartY(float startY) {
        this.startY = startY;
    }

    public float getEndY() {
        return endY;
    }

    public void setEndY(float endY) {
        this.endY = endY;
    }

    public float getRectWidth() {
        return rectWidth;
    }

    public void setRectWidth(float rectWidth) {
        this.rectWidth = rectWidth;
    }

    public float getRectHeigth() {
        return rectHeigth;
    }

    public void setRectHeigth(float rectHeigth) {
        this.rectHeigth = rectHeigth;
    }

    public String getRectName() {
        return rectName;
    }

    public void setRectName(String rectName) {
        this.rectName = rectName;
    }

    public float getStartAngle() {
        return startAngle;
    }

    public void setStartAngle(float startAngle) {
        this.startAngle = startAngle;
    }

    public float getEndAngle() {
        return endAngle;
    }

    public void setEndAngle(float endAngle) {
        this.endAngle = endAngle;
    }
}

4)IndicatorTabItem


package com.example.chenkui.myapplication;


public class IndicatorTabItem  {

    private String name;
    private float mesureWidth;
    private float startAngle;
    private float endAngle;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getMesureWidth() {
        return mesureWidth;
    }

    public void setMesureWidth(float mesureWidth) {
        this.mesureWidth = mesureWidth;
    }

    public float getStartAngle() {
        return startAngle;
    }

    public void setStartAngle(float startAngle) {
        this.startAngle = startAngle;
    }

    public float getEndAngle() {
        return endAngle;
    }

    public void setEndAngle(float endAngle) {
        this.endAngle = endAngle;
    }
}

5)activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="188dp"
                android:orientation="vertical">


    <com.example.chenkui.myapplication.IndicatorTabView
        android:id="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
    <ImageView
        android:id="@+id/titleTavIconImg"
        android:layout_width="80dp"
        android:layout_height="42dp"
        android:layout_marginTop="15dp"
       android:layout_centerHorizontal="true"
        android:background="@drawable/title_icon" />

</RelativeLayout>

dimen.xml

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="tab_text">16dp</dimen>
    <dimen name="tab_wheel_width">10dp</dimen>
    <dimen name="tab_wheel_padding_bottom">130dp</dimen>
    <dimen name="tab_wheel_padding_outer">30dp</dimen>
    <dimen name="tab_wheel_padding_inner">20dp</dimen>
    <dimen name="tab_pointer_radius">10dp</dimen>
</resources>
作者:qq_26337701 发表于2016/10/28 18:05:40 原文链接
阅读:44 评论:0 查看评论

IO流之字节流

$
0
0

学习导航

第一节:IO流之字符流http://blog.csdn.net/bobo8945510/article/details/52957339

IO流之基础讲解

一、什么是IO流?

这里写图片描述


二、字节和字符的区别!

这里写图片描述


三、字节流和字符流的区别?

这里写图片描述


四、IO流的分叉图!

这里写图片描述


五、字节流(FileInputStream和FileOutputStream)的使用!

这里写图片描述

五—-1:FileInputStream的使用及有缓存和没缓存的区别

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;


public class TextRWByteStream01 {
    /*
     * 我们比较一下用缓存和没有缓冲的区别
     * 我们同样使用这个gi
     * */
    //-------------------------------------无缓存---------------------------------------
    public static void main(String[] args) {

        try {
            //读取我放在d://data//dongtai.gif的图片,使其转换为字节
            FileInputStream fin = new FileInputStream("d://data//dongtai.gif");

            //创建一个byte,长度为100.你设置的越少,访问磁盘的次数越多
            byte[] bytes = new byte[100];
            int num = 0;
            //开始读取我们获取当前时间
            long begin = System.currentTimeMillis();
            //判断是否读取到末尾
            while (fin.read(bytes)!=-1) {
                num++;
            }
            System.out.println("没有缓冲请求磁盘的次数:"+num);
            String end = System.currentTimeMillis()-begin+"ms";
            System.out.println("没有缓冲读取dongtai.gif图耗时:"+end);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    //-------------------------------------有缓存---------------------------------------
        System.out.println("==================打印数据分界线=====================");

        try {
            //读取我放在d://data//dongtai.gif的图片,使其转换为字节
            FileInputStream fin = new FileInputStream("d://data//dongtai.gif");
            BufferedInputStream bis = new BufferedInputStream(fin);
            //创建一个byte,长度为100.你设置的越少,访问磁盘的次数越多
            byte[] bytes = new byte[100];
            int num = 0;
            //开始读取我们获取当前时间
            long begin = System.currentTimeMillis();
            //判断是否读取到末尾
            while (bis.read(bytes)!=-1) {
                num++;

            }
            System.out.println("有缓冲请求磁盘的次数:"+num);
            String end = System.currentTimeMillis()-begin+"ms";
            System.out.println("有缓冲读取dongtai.gif图耗时:"+end);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果:可以清晰的看出,带缓冲的明显效率更高
这里写图片描述


五—-2:使用FileInputStream和FileOutputStream实现文件的复制,此demo没有用缓存

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;


public class TextRWByteStream02 {
    /*
     * 运动字节流来复制一张图片
     * 1、我已经在我的D盘data文件夹下放置了一张gif图
     * 2、此demo没有用缓存
     * */
    public static void main(String[] args) {

        try {
            //读取我放在d://data//dongtai.gif的图片,使其转换为字节
            FileInputStream fin = new FileInputStream("d://data//dongtai.gif");
            //把我们获得的图片字节流,写成一个图片
            FileOutputStream outputStream = new FileOutputStream("d://data//newdongtai.gif");

            //创建一个byte,长度为1024.自己根据实际情况调整数组
            byte[] bytes = new byte[1024];
            //判断是否读取到末尾
            while (fin.read(bytes)!=-1) {
                //如果还有字节,就一直写入,到写入完
                outputStream.write(bytes);
            }
            outputStream.close();
            fin.close();
            System.out.println("复制成功");

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果:成功
这里写图片描述


五—-3:使用FileInputStream和FileOutputStream实现文件的复制,用缓存

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;


public class TextRWByteStream03 {
    /*
     * 运动字节流来复制一张图片---带缓冲
     * 1、我已经在我的D盘data文件夹下放置了一张gif图
     * 2、此demo有用缓存
     * */
    public static void main(String[] args) {

        try {
            //输入我放在d://data//dongtai.gif的图片,使其转换为字节,
            //添加缓存,并且还可以制定缓存区的大小。合理的添加缓存区的大小合理有效的节约读取时间。
            //由于我的这张图片不到1M。所以缓存区200就差不多了
            FileInputStream fin = new FileInputStream("d://data//dongtai.gif");
            BufferedInputStream bfin = new BufferedInputStream(fin,200);

            //输入流我们也添加缓存区
            FileOutputStream fout = new FileOutputStream("d://data//newdongtai.gif");
            BufferedOutputStream bfout = new BufferedOutputStream(fout);


            //创建一个byte,长度为1024.自己根据文件实际大小情况调整数组,
            //文件过大可以调大,文件过小就不要设置太大
            byte[] bytes = new byte[1024];
            //判断是否读取到末尾
            while (bfin.read(bytes)!=-1) {
                //如果还有字节,就一直写入,到写入完
                fout.write(bytes);
            }
            bfin.close();
            fin.close();
            bfout.close();
            fout.close();
            System.out.println("复制成功");

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果:成功
这里写图片描述

demo地址:http://download.csdn.net/detail/bobo8945510/9667082

作者:bobo8945510 发表于2016/10/28 18:14:09 原文链接
阅读:44 评论:0 查看评论

Android 轮播图的实现

$
0
0

开始之前

环境准备

开发环境

  • Android Studio 2.2.1
  • JDK1.7
  • API 24
  • Gradle 2.2.1

开发开始

先上效果预览

效果预览

案例分析

这个案例网上也很多, 质量参差不齐, 我也就根据自己的理解来分析分析需要实现的几个功能点:

  • 轮播图有n张图片和相对应的n个小圆点(指示器 indicator) 实现联动
  • 除了可以手动滑动外, 也可以自动滚动(轮播) 可以考虑使用Handler实现
  • 实现无限轮回滚动
  • 当手指按下图片后不再自动滚动

根据上述分析进行开发

接下来搭建布局

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.lulu.shufflingpicdemo.MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="275dp">
        <FrameLayout

            android:layout_width="match_parent"
            android:layout_height="220dp">
            <!--轮播图位置-->
            <android.support.v4.view.ViewPager
                android:id="@+id/live_view_pager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <!--右下角小圆点-->
            <LinearLayout
                android:layout_marginRight="5dp"
                android:layout_gravity="bottom|right"
                android:id="@+id/live_indicator"
                android:orientation="horizontal"
                android:layout_width="wrap_content"
                android:layout_height="10dp"/>
        </FrameLayout>
    </LinearLayout>
</RelativeLayout>

指示器 小点绘制文件

indicator_select.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="oval">
    <size
        android:width="20dp"
        android:height="20dp"/>
    <solid android:color="#c213b7"/>
</shape>

indicator_no_select.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="oval">
    <size
        android:width="20dp"
        android:height="20dp"/>
    <solid android:color="#fff"/>
</shape>

ViewPager的实现

MyPagerAdapter.java


public class MyPagerAdapter extends PagerAdapter {

    public static final int MAX_SCROLL_VALUE = 10000;

    private List<ImageView> mItems;
    private Context mContext;
    private LayoutInflater mInflater;

    public MyPagerAdapter(List<ImageView> items, Context context) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mItems = items;
    }

    /**
     * @param container
     * @param position
     * @return 对position进行求模操作
     * 因为当用户向左滑时position可能出现负值,所以必须进行处理
     */
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        View ret = null;

        //对ViewPager页号求摸取出View列表中要显示的项
        position %= mItems.size();
        Log.d("Adapter", "instantiateItem: position: " + position);
        ret = mItems.get(position);
        //如果View已经在之前添加到了一个父组件,则必须先remove,否则会抛出IllegalStateException。
        ViewParent viewParent = ret.getParent();
        if (viewParent != null) {
            ViewGroup parent = (ViewGroup) viewParent;
            parent.removeView(ret);
        }
        container.addView(ret);

        return ret;
    }
    /**
     * 由于我们在instantiateItem()方法中已经处理了remove的逻辑,
     * 因此这里并不需要处理。实际上,实验表明这里如果加上了remove的调用,
     * 则会出现ViewPager的内容为空的情况。
     *
     * @param container
     * @param position
     * @param object
     */
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        //警告:不要在这里调用removeView, 已经在instantiateItem中处理了
    }


    @Override
    public int getCount() {
        int ret = 0;
        if (mItems.size() > 0) {
            ret = MAX_SCROLL_VALUE;
        }
        return ret;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == (View) object;
    }
}

Note: 一定不要在destroyItem中再调用removeView了, 因为咱们已经instantiateItem中做了处理

在MainActivity.java中给ViewPager设置Adapter

mItems = new ArrayList<>();
mViewPager.setAdapter(mAdapter);

addImageView();
mAdapter.notifyDataSetChanged();

private void addImageView(){
    ImageView view0 = new ImageView(this);
    view0.setImageResource(R.mipmap.pic0);
    ImageView view1 = new ImageView(this);
    view1.setImageResource(R.mipmap.pic1);
    ImageView view2 = new ImageView(this);
    view2.setImageResource(R.mipmap.pic2);

    view0.setScaleType(ImageView.ScaleType.CENTER_CROP);
    view1.setScaleType(ImageView.ScaleType.CENTER_CROP);
    view2.setScaleType(ImageView.ScaleType.CENTER_CROP);

    mItems.add(view0);
    mItems.add(view1);
    mItems.add(view2);

}

Note: 因为咱们做的是Demo, 所以我们传入的是一个ImageView的集合, 真正开发时, 需要传入含有图片url的实体类, 在Adapter中可以使用加载图片的类库加载

实现右下角指示器

添加指示器

在onCreate中添加

//获取指示器(下面三个小点)
mBottomLiner = (LinearLayout) findViewById(R.id.live_indicator);
//右下方小圆点
mBottomImages = new ImageView[mItems.size()];
for (int i = 0; i < mBottomImages.length; i++) {
    ImageView imageView = new ImageView(this);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(20, 20);
    params.setMargins(5, 0, 5, 0);
    imageView.setLayoutParams(params);
    //如果当前是第一个 设置为选中状态
    if (i == 0) {
        imageView.setImageResource(R.drawable.indicator_select);
    } else {
        imageView.setImageResource(R.drawable.indicator_no_select);
    }
    mBottomImages[i] = imageView;
    //添加到父容器
    mBottomLiner.addView(imageView);
}

实现联动

添加ViewPager的监听事件, 实现ViewPager.OnPageChangeListener接口


mViewPager.addOnPageChangeListener(this);

回调事件



///////////////////////////////////////////////////////////////////////////
// ViewPager的监听事件
///////////////////////////////////////////////////////////////////////////
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override
public void onPageSelected(int position) {

    currentViewPagerItem = position;
    if (mItems != null) {
        position %= mBottomImages.length;
        int total = mBottomImages.length;

        for (int i = 0; i < total; i++) {
            if (i == position) {
                mBottomImages[i].setImageResource(R.drawable.indicator_select);
            } else {
                mBottomImages[i].setImageResource(R.drawable.indicator_no_select);
            }
        }
    }
}

@Override
public void onPageScrollStateChanged(int state) {

}

实现自动滚动

在mBottomImages初始化之后 开启一个线程 进行定时发送一个空消息给Handler处理, 由Handler决定切换到下一页


//让其在最大值的中间开始滑动, 一定要在 mBottomImages初始化之前完成
int mid = MyPagerAdapter.MAX_SCROLL_VALUE / 2;
mViewPager.setCurrentItem(mid);
currentViewPagerItem = mid;

//定时发送消息
mThread = new Thread(){
    @Override
    public void run() {
        super.run();
        while (true) {
            mHandler.sendEmptyMessage(0);
            try {
                Thread.sleep(MainActivity.VIEW_PAGER_DELAY);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
};
mThread.start();

自定义Handler


///////////////////////////////////////////////////////////////////////////
// 为防止内存泄漏, 声明自己的Handler并弱引用Activity
///////////////////////////////////////////////////////////////////////////
private static class MyHandler extends Handler {
    private WeakReference<MainActivity> mWeakReference;

    public MyHandler(MainActivity activity) {
        mWeakReference = new WeakReference<MainActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 0:
                MainActivity activity = mWeakReference.get();
                if (activity.isAutoPlay) {

                    activity.mViewPager.setCurrentItem(++activity.currentViewPagerItem);
                }

                break;
        }

    }
}

Note: 其中isAutoPlay是一个用来判断当前是否是自动轮播的boolean值变量, 主要用于实现我们接下来说的 当手指按下图片后不再滚动

实现当手指按下图片后不再滚动

思路: 我们可以考虑对ViewPager的触摸事件进行监听, 然后设置一个上节说到的isAutoPlay的boolean变量用来让Handler判断是否进行轮播滚动

代码实现:

ViewPager设置监听

mViewPager.setOnTouchListener(this);

事件回调

@Override
public boolean onTouch(View v, MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            isAutoPlay = false;
            break;
        case MotionEvent.ACTION_UP:
            isAutoPlay = true;
            break;
    }
    return false;
}

注: 细心的同学可能会看出来, 我们没有单独说 无限循环 如何实现, 其实, 它的实现已经隐藏在了代码中, 在这里我简单的说一下思路:
给ViewPager的条目个数设置个较大值, 该案例中为10000, 然后我们刚进入时选中的位置为 10000/2=5000, 也就是说我们可以向左或向右滑动约5000多张图片, 但这是不现实的, 所以就给用户造成了无限循环的假象

我把源码放在了github 上, 希望大家多多支持

作者:u013144863 发表于2016/10/28 18:26:16 原文链接
阅读:52 评论:0 查看评论

Android 微信小视频录制功能实现

$
0
0

目录

开发之前

这几天接触了一下和视频相关的控件, 所以, 继之前的微信摇一摇, 我想到了来实现一下微信小视频录制的功能, 它的功能点比较多, 我每天都抽出点时间来写写, 说实话, 有些东西还是比较费劲, 希望大家认真看看, 说得不对的地方还请大家在评论中指正. 废话不多说, 进入正题.

开发环境

最近刚更新的, 没更新的小伙伴们抓紧了

  • Android Studio 2.2.2
  • JDK1.7
  • API 24
  • Gradle 2.2.2

相关知识点

  • 视频录制界面 SurfaceView 的使用

  • Camera的使用

  • 相机的对焦, 变焦

  • 视频录制控件MediaRecorder的使用

  • 简单自定义View

  • GestureDetector(手势检测)的使用

用到的东西真不少, 不过别着急, 咱们一个一个来.

开始开发

案例预览

请原谅Gif图的粗糙

微信小视频

案例分析

大家可以打开自己微信里面的小视频, 一块简单的分析一下它的功能点有哪些 ?

  • 基本的视频预览功能

  • 长按 “按住拍” 实现视频的录制

  • 录制过程中的进度条从两侧向中间变短

  • 当松手或者进度条走到尽头视频停止录制 并保存

  • 从 “按住拍” 上滑取消视频的录制

  • 双击屏幕 变焦 放大

根据上述的分析, 我们一步一步的完成

搭建布局

布局界面的实现还可以, 难度不大


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/main_tv_tip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:layout_marginBottom="150dp"
        android:elevation="1dp"
        android:text="双击放大"
        android:textColor="#FFFFFF"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <SurfaceView
            android:id="@+id/main_surface_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="3"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:background="@color/colorApp"
            android:orientation="vertical">
            <RelativeLayout
                android:id="@+id/main_press_control"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <com.lulu.weichatsamplevideo.BothWayProgressBar
                    android:id="@+id/main_progress_bar"
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:background="#000"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:text="按住拍"
                    android:textAppearance="@style/TextAppearance.AppCompat.Large"
                    android:textColor="#00ff00"/>
            </RelativeLayout>
        </LinearLayout>
    </LinearLayout>
</FrameLayout>

视频预览的实现

step1: 得到SufaceView控件, 设置基本属性和相应监听(该控件的创建是异步的, 只有在真正”准备”好之后才能调用)


mSurfaceView = (SurfaceView) findViewById(R.id.main_surface_view);
 //设置屏幕分辨率
mSurfaceHolder.setFixedSize(videoWidth, videoHeight);
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mSurfaceHolder.addCallback(this);

step2: 实现接口的方法, surfaceCreated方法中开启视频的预览, 在surfaceDestroyed中销毁


//////////////////////////////////////////////
// SurfaceView回调
/////////////////////////////////////////////
@Override
public void surfaceCreated(SurfaceHolder holder) {
    mSurfaceHolder = holder;
    startPreView(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    if (mCamera != null) {
        Log.d(TAG, "surfaceDestroyed: ");
        //停止预览并释放摄像头资源
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }
    if (mMediaRecorder != null) {
        mMediaRecorder.release();
        mMediaRecorder = null;
    }
}

step3: 实现视频预览的方法


/**
 * 开启预览
 *
 * @param holder
 */
private void startPreView(SurfaceHolder holder) {
    Log.d(TAG, "startPreView: ");

    if (mCamera == null) {
        mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
    }
    if (mMediaRecorder == null) {
        mMediaRecorder = new MediaRecorder();
    }
    if (mCamera != null) {
        mCamera.setDisplayOrientation(90);
        try {
            mCamera.setPreviewDisplay(holder);
            Camera.Parameters parameters = mCamera.getParameters();
            //实现Camera自动对焦
            List<String> focusModes = parameters.getSupportedFocusModes();
            if (focusModes != null) {
                for (String mode : focusModes) {
                    mode.contains("continuous-video");
                    parameters.setFocusMode("continuous-video");
                }
            }
            mCamera.setParameters(parameters);
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Note: 上面添加了自动对焦的代码, 但是部分手机可能不支持

自定义双向缩减的进度条

有些像我一样的初学者一看到自定义某某View, 就觉得比较牛X. 其实呢, Google已经替我们写好了很多代码, 所以我们用就行了.而且咱们的这个进度条也没啥, 不就是一根线, 今天咱就来说说.

step1: 继承View, 完成初始化


private static final String TAG = "BothWayProgressBar";
//取消状态为红色bar, 反之为绿色bar
private boolean isCancel = false;
private Context mContext;
//正在录制的画笔
private Paint mRecordPaint;
//上滑取消时的画笔
private Paint mCancelPaint;
//是否显示
private int mVisibility;
// 当前进度
private int progress;
//进度条结束的监听
private OnProgressEndListener mOnProgressEndListener;

public BothWayProgressBar(Context context) {
     super(context, null);
}
public BothWayProgressBar(Context context, AttributeSet attrs) {
   super(context, attrs);
   mContext = context;
   init();
}
private void init() {
   mVisibility = INVISIBLE;
   mRecordPaint = new Paint();
   mRecordPaint.setColor(Color.GREEN);
   mCancelPaint = new Paint();
   mCancelPaint.setColor(Color.RED);
}

Note: OnProgressEndListener, 主要用于当进度条走到中间了, 好通知相机停止录制, 接口如下:


public interface OnProgressEndListener{
    void onProgressEndListener();
}
/**
 * 当进度条结束后的 监听
 * @param onProgressEndListener
 */
public void setOnProgressEndListener(OnProgressEndListener onProgressEndListener) {
    mOnProgressEndListener = onProgressEndListener;
}

step2 :设置Setter方法用于通知我们的Progress改变状态



/**
 * 设置进度
 * @param progress
 */
public void setProgress(int progress) {
    this.progress = progress;
    invalidate();
}

/**
 * 设置录制状态 是否为取消状态
 * @param isCancel
 */
public void setCancel(boolean isCancel) {
    this.isCancel = isCancel;
    invalidate();
}
/**
 * 重写是否可见方法
 * @param visibility
 */
@Override
public void setVisibility(int visibility) {
    mVisibility = visibility;
    //重新绘制
    invalidate();
}

step3 :最重要的一步, 画出我们的进度条,使用的就是View中的onDraw(Canvas canvas)方法

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mVisibility == View.VISIBLE) {
        int height = getHeight();
        int width = getWidth();
        int mid = width / 2;


        //画出进度条
        if (progress < mid){
            canvas.drawRect(progress, 0, width-progress, height, isCancel ? mCancelPaint : mRecordPaint);
        } else {
            if (mOnProgressEndListener != null) {
                mOnProgressEndListener.onProgressEndListener();
            }
        }
    } else {
        canvas.drawColor(Color.argb(0, 0, 0, 0));
    }
}

录制事件的处理

录制中触发的事件包括四个:


  1. 长按录制
  2. 抬起保存
  3. 上滑取消
  4. 双击放大(变焦)

现在对这4个事件逐个分析:
前三这个事件, 我都放在了一个onTouch()回调方法中了
对于第4个, 我们待会谈
我们先把onTouch()中局部变量列举一下:

@Override
public boolean onTouch(View v, MotionEvent event) {
   boolean ret = false;
   int action = event.getAction();
   float ey = event.getY();
   float ex = event.getX();
   //只监听中间的按钮处
   int vW = v.getWidth();
   int left = LISTENER_START;
   int right = vW - LISTENER_START;
   float downY = 0;
   // ...
}
长按录制

长按录制我们需要监听ACTION_DOWN事件, 使用线程延迟发送Handler来实现进度条的更新

switch (action) {
  case MotionEvent.ACTION_DOWN:
      if (ex > left && ex < right) {
          mProgressBar.setCancel(false);
          //显示上滑取消
          mTvTip.setVisibility(View.VISIBLE);
          mTvTip.setText("↑ 上滑取消");
          //记录按下的Y坐标
          downY = ey;
          // TODO: 2016/10/20 开始录制视频, 进度条开始走
          mProgressBar.setVisibility(View.VISIBLE);
          //开始录制
          Toast.makeText(this, "开始录制", Toast.LENGTH_SHORT).show();
          startRecord();
          mProgressThread = new Thread() {
              @Override
              public void run() {
                  super.run();
                  try {
                      mProgress = 0;
                      isRunning = true;
                      while (isRunning) {
                          mProgress++;
                          mHandler.obtainMessage(0).sendToTarget();
                          Thread.sleep(20);
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          };
          mProgressThread.start();
          ret = true;
      }
      break;
      // ...
      return true;
}

Note: startRecord()这个方法先不说, 我们只需要知道执行了它就可以录制了, 但是Handler事件还是要说的, 它只负责更新进度条的进度



////////////////////////////////////////////////////
// Handler处理
/////////////////////////////////////////////////////
private static class MyHandler extends Handler {
    private WeakReference<MainActivity> mReference;
    private MainActivity mActivity;

    public MyHandler(MainActivity activity) {
        mReference = new WeakReference<MainActivity>(activity);
        mActivity = mReference.get();
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 0:
                mActivity.mProgressBar.setProgress(mActivity.mProgress);
                break;
        }
    }
}
抬起保存

同样我们这儿需要监听ACTION_UP事件, 但是要考虑当用户抬起过快时(录制的时间过短), 不需要保存. 而且, 在这个事件中包含了取消状态的抬起, 解释一下: 就是当上滑取消时抬起的一瞬间取消录制, 大家看代码

case MotionEvent.ACTION_UP:
  if (ex > left && ex < right) {
      mTvTip.setVisibility(View.INVISIBLE);
      mProgressBar.setVisibility(View.INVISIBLE);
      //判断是否为录制结束, 或者为成功录制(时间过短)
      if (!isCancel) {
          if (mProgress < 50) {
              //时间太短不保存
              stopRecordUnSave();
              Toast.makeText(this, "时间太短", Toast.LENGTH_SHORT).show();
              break;
          }
          //停止录制
          stopRecordSave();
      } else {
          //现在是取消状态,不保存
          stopRecordUnSave();
          isCancel = false;
          Toast.makeText(this, "取消录制", Toast.LENGTH_SHORT).show();
          mProgressBar.setCancel(false);
      }

      ret = false;
  }
  break;

Note: 同样的, 内部的stopRecordUnSave()和stopRecordSave();大家先不要考虑, 我们会在后面介绍, 他俩从名字就能看出 前者用来停止录制但不保存, 后者停止录制并保存

上滑取消

配合上一部分说得抬起取消事件, 实现上滑取消


case MotionEvent.ACTION_MOVE:
  if (ex > left && ex < right) {
      float currentY = event.getY();
      if (downY - currentY > 10) {
          isCancel = true;
          mProgressBar.setCancel(true);
      }
  }
  break;

Note: 主要原理不难, 只要按下并且向上移动一定距离 就会触发,当手抬起时视频录制取消

双击放大(变焦)

这个事件比较特殊, 使用了Google提供的GestureDetector手势检测 来判断双击事件

step1: 对SurfaceView进行单独的Touch事件监听, why? 因为GestureDetector需要Touch事件的完全托管, 如果只给它传部分事件会造成某些事件失效

mDetector = new GestureDetector(this, new ZoomGestureListener());
/**
 * 单独处理mSurfaceView的双击事件
 */
mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        mDetector.onTouchEvent(event);
        return true;
    }
});

step2: 重写GestureDetector.SimpleOnGestureListener, 实现双击事件


///////////////////////////////////////////////////////////////////////////
// 变焦手势处理类
///////////////////////////////////////////////////////////////////////////
class ZoomGestureListener extends GestureDetector.SimpleOnGestureListener {
    //双击手势事件
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        super.onDoubleTap(e);
        Log.d(TAG, "onDoubleTap: 双击事件");
        if (mMediaRecorder != null) {
            if (!isZoomIn) {
                setZoom(20);
                isZoomIn = true;
            } else {
                setZoom(0);
                isZoomIn = false;
            }
        }
        return true;
    }
}

step3: 实现相机的变焦的方法


/**
 * 相机变焦
 *
 * @param zoomValue
 */
public void setZoom(int zoomValue) {
    if (mCamera != null) {
        Camera.Parameters parameters = mCamera.getParameters();
        if (parameters.isZoomSupported()) {//判断是否支持
            int maxZoom = parameters.getMaxZoom();
            if (maxZoom == 0) {
                return;
            }
            if (zoomValue > maxZoom) {
                zoomValue = maxZoom;
            }
            parameters.setZoom(zoomValue);
            mCamera.setParameters(parameters);
        }
    }

}

Note: 至此我们已经完成了对所有事件的监听, 看到这里大家也许有些疲惫了, 不过不要灰心, 现在完成我们的核心部分, 实现视频的录制

实现视频的录制

说是核心功能, 也只不过是我们不知道某些API方法罢了, 下面代码中我已经加了详细的注释, 部分不能理解的记住就好^v^

/**
 * 开始录制
 */
private void startRecord() {
    if (mMediaRecorder != null) {
        //没有外置存储, 直接停止录制
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return;
        }
        try {
            //mMediaRecorder.reset();
            mCamera.unlock();
            mMediaRecorder.setCamera(mCamera);
            //从相机采集视频
            mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            // 从麦克采集音频信息
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            // TODO: 2016/10/20  设置视频格式
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            mMediaRecorder.setVideoSize(videoWidth, videoHeight);
            //每秒的帧数
            mMediaRecorder.setVideoFrameRate(24);
            //编码格式
            mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            // 设置帧频率,然后就清晰了
            mMediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);
            // TODO: 2016/10/20 临时写个文件地址, 稍候该!!!
            File targetDir = Environment.
                    getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
            mTargetFile = new File(targetDir,
                    SystemClock.currentThreadTimeMillis() + ".mp4");
            mMediaRecorder.setOutputFile(mTargetFile.getAbsolutePath());
            mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
            mMediaRecorder.prepare();
            //正式录制
            mMediaRecorder.start();
            isRecording = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

实现视频的停止

大家可能会问, 视频的停止为什么单独抽出来说呢? 仔细的同学看上面代码会看到这两个方法: stopRecordSave和stopRecordUnSave, 一个停止保存, 一个是停止不保存, 接下来我们就补上这个坑

停止并保存


private void stopRecordSave() {
    if (isRecording) {
        isRunning = false;
        mMediaRecorder.stop();
        isRecording = false;
        Toast.makeText(this, "视频已经放至" + mTargetFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
    }
}

停止不保存


private void stopRecordUnSave() {
    if (isRecording) {
        isRunning = false;
        mMediaRecorder.stop();
        isRecording = false;
        if (mTargetFile.exists()) {
            //不保存直接删掉
            mTargetFile.delete();
        }
    }
}

Note: 这个停止不保存是我自己的一种想法, 如果大家有更好的想法, 欢迎大家到评论中指出, 不胜感激

完整代码

源码我已经放在了github上了, 写博客真是不易! 写篇像样的博客更是不易, 希望大家多多支持

总结

终于写完了!!! 这是我最想说得话, 从案例一开始到现在已经过去很长时间. 这是我写得最长的一篇博客, 发现能表达清楚自己的想法还是很困难的, 这是我最大的感受!!!
实话说这个案例不是很困难, 但是像我这样的初学者拿来练练手还是非常好的, 在这里还要感谢再见杰克的博客, 也给我提供了很多帮助
最后, 希望大家共同进步!

作者:u013144863 发表于2016/10/28 18:26:49 原文链接
阅读:84 评论:0 查看评论

Android 微信摇一摇功能实现

$
0
0

开发之前

今天学习了一下传感器, 脑子里就蹦出了微信的摇一摇, 于是鼓了鼓勇气抽空写了出来, 本人菜鸟一枚, 希望大神们多多指点

开发环境

  • Android Studio 2.2.1
  • JDK1.7
  • API 24
  • Gradle 2.2.1

相关知识点

  • 加速度传感器
  • 补间动画
  • 手机震动 (Vibrator)
  • 较短 声音/音效 的播放 (SoundPool)

开始开发

案例预览

weichat.gif

案例分析

我们接下来分析一下这个案例, 当用户晃动手机时, 会触发加速传感器, 此时加速传感器会调用相应接口供我们使用, 此时我们可以做一些相应的动画效果, 震动效果和声音效果. 大致思路就是这样. 具体功能点:

  • 用户晃动后两张图片分开, 显示后面图片
  • 晃动后伴随震动效果, 声音效果

根据以上的简单分析, 我们就知道该怎么做了, Just now

先搭建布局

布局没啥可说的, 大家直接看代码吧


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff222222"
    android:orientation="vertical"
    tools:context="com.lulu.weichatshake.MainActivity">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--摇一摇中心图片-->
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:src="@mipmap/weichat_icon"/>
        <LinearLayout
            android:gravity="center"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true">
            <!--顶部的横线和图片-->
            <LinearLayout
                android:gravity="center_horizontal|bottom"
                android:id="@+id/main_linear_top"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <ImageView
                    android:src="@mipmap/shake_top"
                    android:id="@+id/main_shake_top"
                    android:layout_width="wrap_content"
                    android:layout_height="100dp"/>
                <ImageView
                    android:background="@mipmap/shake_top_line"
                    android:id="@+id/main_shake_top_line"
                    android:layout_width="match_parent"
                    android:layout_height="5dp"/>
            </LinearLayout>
            <!--底部的横线和图片-->
            <LinearLayout
                android:gravity="center_horizontal|bottom"
                android:id="@+id/main_linear_bottom"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <ImageView
                    android:background="@mipmap/shake_bottom_line"
                    android:id="@+id/main_shake_bottom_line"
                    android:layout_width="match_parent"
                    android:layout_height="5dp"/>
                <ImageView
                    android:src="@mipmap/shake_bottom"
                    android:id="@+id/main_shake_bottom"
                    android:layout_width="wrap_content"
                    android:layout_height="100dp"/>
            </LinearLayout>
        </LinearLayout>
    </RelativeLayout>
</LinearLayout>

得到加速度传感器的回调接口

step1: 在onStart() 方法中获取传感器的SensorManager


@Override
protected void onStart() {
    super.onStart();
    //获取 SensorManager 负责管理传感器
    mSensorManager = ((SensorManager) getSystemService(SENSOR_SERVICE));
    if (mSensorManager != null) {
        //获取加速度传感器
        mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (mAccelerometerSensor != null) {
            mSensorManager.registerListener(this, mAccelerometerSensor, SensorManager.SENSOR_DELAY_UI);
        }
    }
}

step2: 紧接着我们就要在Pause中注销传感器


@Override
protected void onPause() {
    // 务必要在pause中注销 mSensorManager
    // 否则会造成界面退出后摇一摇依旧生效的bug
    if (mSensorManager != null) {
        mSensorManager.unregisterListener(this);
    }
    super.onPause();
}

Note: 至于为什么我们要在onStart和onPause中就行SensorManager的注册和注销, 就是因为, 防止在界面退出(包括按Home键)时, 摇一摇依旧生效(代码中有注释)

step3: 在step1中的注册监听事件方法中, 我们传入了当前Activity对象, 故让其实现回调接口, 得到以下方法


///////////////////////////////////////////////////////////////////////////
// SensorEventListener回调方法
///////////////////////////////////////////////////////////////////////////
@Override
public void onSensorChanged(SensorEvent event) {
    int type = event.sensor.getType();

    if (type == Sensor.TYPE_ACCELEROMETER) {
        //获取三个方向值
        float[] values = event.values;
        float x = values[0];
        float y = values[1];
        float z = values[2];

        if ((Math.abs(x) > 17 || Math.abs(y) > 17 || Math
                .abs(z) > 17) && !isShake) {
            isShake = true;
            // TODO: 2016/10/19 实现摇动逻辑, 摇动后进行震动
            Thread thread = new Thread() {
                @Override
                public void run() {
                    super.run();
                    try {
                        Log.d(TAG, "onSensorChanged: 摇动");

                        //开始震动 发出提示音 展示动画效果
                        mHandler.obtainMessage(START_SHAKE).sendToTarget();
                        Thread.sleep(500);
                        //再来一次震动提示
                        mHandler.obtainMessage(AGAIN_SHAKE).sendToTarget();
                        Thread.sleep(500);
                        mHandler.obtainMessage(END_SHAKE).sendToTarget();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
        }
    }
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}

Note: 当用户晃动手机会调用onSensorChanged方法, 可以做一些相应的操作
为解决动画和震动延迟, 我们开启了一个子线程来实现.
子线程中会通过发送Handler消息, 先开始动画效果, 并伴随震动和声音
先把Handler的实现放一放, 我们再来看一下震动和声音初始化

动画, 震动和音效实现

step 1: 先获取到震动相关的服务,注意要加权限. 至于音效, 我们采用SoundPool来播放, 在这里非常感谢Vincent 的贴子, 好初始化SoundPool

震动权限

    <uses-permission android:name="android.permission.VIBRATE"/>

//初始化SoundPool
mSoundPool = new SoundPool(1, AudioManager.STREAM_SYSTEM, 5);
mWeiChatAudio = mSoundPool.load(this, R.raw.weichat_audio, 1);

//获取Vibrator震动服务
mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

Note: 大家可能发现SoundPool的构造方法已经过时, 不过不用担心这是Api21之后过时的, 所以也不算太”过时”吧
 

step2: 接下来我们就要介绍Handler中的实现了, 为避免Activity内存泄漏, 采用了软引用方式

private static class MyHandler extends Handler {
    private WeakReference<MainActivity> mReference;
    private MainActivity mActivity;
    public MyHandler(MainActivity activity) {
        mReference = new WeakReference<MainActivity>(activity);
        if (mReference != null) {
            mActivity = mReference.get();
        }
    }
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case START_SHAKE:
                //This method requires the caller to hold the permission VIBRATE.
                mActivity.mVibrator.vibrate(300);
                //发出提示音
                mActivity.mSoundPool.play(mActivity.mWeiChatAudio, 1, 1, 0, 0, 1);
                mActivity.mTopLine.setVisibility(View.VISIBLE);
                mActivity.mBottomLine.setVisibility(View.VISIBLE);
                mActivity.startAnimation(false);//参数含义: (不是回来) 也就是说两张图片分散开的动画
                break;
            case AGAIN_SHAKE:
                mActivity.mVibrator.vibrate(300);
                break;
            case END_SHAKE:
                //整体效果结束, 将震动设置为false
                mActivity.isShake = false;
                // 展示上下两种图片回来的效果
                mActivity.startAnimation(true);
                break;
        }
    }
}

Note: 内容不多说了, 代码注释中很详细, 还有一个startAnimation方法
我先来说一下它的参数, true表示布局中两张图片从打开到关闭的动画, 反之, false是从关闭到打开状态, 上代码

step3: startAnimaion方法上的实现



/**
 * 开启 摇一摇动画
 *
 * @param isBack 是否是返回初识状态
 */
private void startAnimation(boolean isBack) {
    //动画坐标移动的位置的类型是相对自己的
    int type = Animation.RELATIVE_TO_SELF;

    float topFromY;
    float topToY;
    float bottomFromY;
    float bottomToY;
    if (isBack) {
        topFromY = -0.5f;
        topToY = 0;
        bottomFromY = 0.5f;
        bottomToY = 0;
    } else {
        topFromY = 0;
        topToY = -0.5f;
        bottomFromY = 0;
        bottomToY = 0.5f;
    }

    //上面图片的动画效果
    TranslateAnimation topAnim = new TranslateAnimation(
            type, 0, type, 0, type, topFromY, type, topToY
    );
    topAnim.setDuration(200);
    //动画终止时停留在最后一帧~不然会回到没有执行之前的状态
    topAnim.setFillAfter(true);

    //底部的动画效果
    TranslateAnimation bottomAnim = new TranslateAnimation(
            type, 0, type, 0, type, bottomFromY, type, bottomToY
    );
    bottomAnim.setDuration(200);
    bottomAnim.setFillAfter(true);

    //大家一定不要忘记, 当要回来时, 我们中间的两根线需要GONE掉
    if (isBack) {
        bottomAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {}
            @Override
            public void onAnimationRepeat(Animation animation) {}
            @Override
            public void onAnimationEnd(Animation animation) {
                //当动画结束后 , 将中间两条线GONE掉, 不让其占位
                mTopLine.setVisibility(View.GONE);
                mBottomLine.setVisibility(View.GONE);
            }
        });
    }
    //设置动画
    mTopLayout.startAnimation(topAnim);
    mBottomLayout.startAnimation(bottomAnim);

}

至此 核心代码已经介绍完毕 , 但是还有部分小细节不得不提一下

细枝末节

  1. 大家要在初始化View之前将上下两条横线GONE掉, 用GONE是不占位的

mTopLine.setVisibility(View.GONE);
mBottomLine.setVisibility(View.GONE);

2.咱们的摇一摇最好是只竖屏 (毕竟我也没见过横屏的摇一摇), 加上下面代码

//设置只竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

完整代码

源码我已经发在了github上, 希望大家多多支持!

作者:u013144863 发表于2016/10/28 18:27:32 原文链接
阅读:57 评论:0 查看评论

AppBarLayout CollapsingToolbarLayout 的进一步使用

$
0
0

​ 最近有个项目,虽然暂时停了,但是有效果还是想做一下;一方面是自己好奇,另一方面又怕领导突然一拍脑门,又重新做起来。正好利用到之前说过的AppBarLaout,CollapsingToolbarLayout,所以趁着之前的热乎劲一块搞出来就完了。关于这两个控件的使用请看一下AppBarLayout 介绍和简单实用和 CollapsingToolbarLayout 介绍和简单使用

​ 首先看一下要实现的大概效果:

​ 首先我们分析一下这个效果,应该使用什么控件。这里有折叠效果,肯定会有CollapsingToolbarLayout;而且ToolBar 跟 AppBarLayout 也肯定少不了,要不然使用CollaspingToolbarLayout 就如同鸡肋了。下面的滑动标签使用的是TabLayout,最下面的内容使用的是ViewPager+Fragment。

​ 好。分析完了,布局基本就出来了:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/colorAccent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed" >

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="match_parent"
                android:layout_height="260dp"
                android:background="@drawable/header_bar"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:navigationIcon="@mipmap/ic_launcher"
                app:title="标题"/>
        </android.support.design.widget.CollapsingToolbarLayout>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@android:color/white"/>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/nestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v4.view.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

​ 尤其需要我们注意一下的是,我们的TabLayout没有被折叠起来,只是随着被滑动到顶部,所以,我们把它放到了CollapsingToolbarLayout的下面。

​ 我们可以看到我们的布局顶到了状态栏,这是4.4之后才可以用的设置,需要我们注意一下。设置的属性为android:fitsSystemWindows,设置为true时,系统会为顶部的状态栏留出空间;当设置为false时,我们的布局就会占顶到顶部状态栏。还有我们需要设置一下android:windowTranslucentStatus",这个设置在4.4和5.x之后有一些不同;4.4的设置之后状态栏是渐变的,而5.x之后就是半透明的。但是这个属性是在Android API19 之后出来的,所以我们需要在另创建两个文件夹vaules-19,vaules-21,里面创建一个style;因为我创建的style使用values下的一些东西,所以我们先看一下values下的style文件:

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    <style name="AppTheme.NoActionBar">
        <item name="android:windowActionBar">false</item>
        <item name="android:windowNoTitle">true</item>
    </style>
</resources>

然后我们去看一下vaules-19,vaules-21下的style文件,它们两个是一样的:

<resources>
    <style name="AppTheme.NoActionBar">
        <item name="android:fitsSystemWindows">false</item>
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>

接下来我们看一下代码:

public class MainActivity extends AppCompatActivity {
    private NestedScrollView mNestedScrollView;
    private TabLayout mTabLayout;
    private ViewPager mViewPager;

    private String[] mTitles = {"西游记", "西游记", "西游记"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mViewPager = (ViewPager) findViewById(R.id.viewPager);
        mTabLayout = (TabLayout) findViewById(R.id.tabs);
        mNestedScrollView = (NestedScrollView) findViewById(R.id.nestedScrollView);
        //设置 NestedScrollView 的内容是否拉伸填充整个视图,
        //这个设置是必须的,否者我们在里面设置的ViewPager会不可见
        mNestedScrollView.setFillViewport(true);

        mTabLayout.setupWithViewPager(mViewPager);
        MyAdapter adapter = new MyAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(adapter);
    }

    private class MyAdapter extends FragmentPagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }
        @Override
        public Fragment getItem(int position) {
            return new MyFragment();
        }
        @Override
        public int getCount() {
            return 3;
        }
        @Override
        public CharSequence getPageTitle(int position) {
            return mTitles[position];
        }
    }

    public static class MyFragment extends Fragment {
        @Nullable
        @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            String string = getString(R.string.text);
            List<String> strings = new ArrayList<>();
            for (int i = 0; i < 3; i++) {
                strings.add(string);
            }
            RecyclerView recyclerView = new RecyclerView(getActivity());
            recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
            recyclerView.setAdapter(new MyListAdapter(strings));
            return recyclerView;
        }


        class MyListAdapter extends RecyclerView.Adapter {
            private List<String> mStrings;
            public MyListAdapter(List<String> strings) {
                this.mStrings = strings;
            }
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                View convertView = LayoutInflater.from(getActivity()).inflate(R.layout.item, parent, false);
                return new MyListViewHolder(convertView);
            }
            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                MyListViewHolder viewHolder = (MyListViewHolder) holder;
                viewHolder.mTextView.setText(mStrings.get(position));
            }
            @Override
            public int getItemCount() {
                return 3;
            }
        }

        class MyListViewHolder extends RecyclerView.ViewHolder {
            private TextView mTextView;
            public MyListViewHolder(View itemView) {
                super(itemView);
                mTextView = (TextView) itemView.findViewById(R.id.tv_content);
            }
        }
    }
}

​ 这里需要强调一下,Fragment里的列表必须使用RecyclerView,而不能使用ListView,否者列表不能滑动,具体原因有时间我们在探讨。

​ 还有我们的 NestedScrollView 调用了一个setFillViewport这个方法,这个方法会将NestedScrollView里填充内容的高拉伸,用来填充整个Viewport。如果不设置,那我们的Veiwpager则不会显示。其它的代码没有什么难度这里就不在说明了。

​ 现在我们运行一下这些代码,会看到如下效果:

我们可以看到,顶部的Toolbar的顶到了状态栏,这样不太好,我们在Toolbar上给它设置一个android:layout_marginTop,这个值设置为25dp(状态栏大概就25dp),为了适配各个API Level ,我们分别在vaules-19,vaules-21里创建一个dimens文件,然后配置一下:

<dimen name="dimen_status_height">25dp</dimen>

我们在布局引用一下就OK了。

然后我们再看一下效果:

上图是在API 22上的效果,下面的是在API 19 上的效果:

可能大家以为到这里就结束了,当然不会的。有没有人看到标题栏里的标题那两个字随着上滑就感觉特别讨厌呢?是的,有的,我们的产品大人就很讨厌,所以她把标题放到的最上面变成字体颜色渐变。她要求当折叠结束时文字显示,下滑后文字逐渐消失。怎么办呢?我们慢慢来实现。

​ 首先我们需要修改Toolbar里文字的透明度,所以我们需要修改一下布局,在Toolbar里放一下TextView:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/colorAccent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="match_parent"
                android:layout_height="260dp"
                android:background="@drawable/header_bar"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:layout_marginTop="@dimen/dimen_status_height"
                app:layout_collapseMode="pin"
                app:navigationIcon="@mipmap/ic_launcher">
                <TextView
                    android:id="@+id/tv_title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="标题"/>
            </android.support.v7.widget.Toolbar>
        </android.support.design.widget.CollapsingToolbarLayout>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@android:color/white"/>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/nestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" >
        <android.support.v4.view.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

​ 相较于之前的布局我们只是在Toolbar里添加了一个TextView。不是特别的麻烦。

​ 接下来我们需要去代码动态设置标题文字的透明度,我们知道当我们上滑时的折叠效果是通过CollapsingToolbarLayout 实现的。那我们怎么监听它的折叠呢?我去看了看它的源码,发现它还是使用的AppBarLayout 的 OffsetUpdateListener

​ 首先看它如何绑定的监听:

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();、
    // Add an OnOffsetChangedListener if possible
    final ViewParent parent = getParent();
    if (parent instanceof AppBarLayout) {
        // Copy over from the ABL whether we should fit system windows
        ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));

        if (mOnOffsetChangedListener == null) {
            mOnOffsetChangedListener = new OffsetUpdateListener();
        }
      //看这里,它还是使用的AppBarLayout的 mOnOffsetChangedListener
        ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);

        // We're attached, so lets request an inset dispatch
        ViewCompat.requestApplyInsets(this);
    }
}

其它暂时不再深究了,既然我们知道了它也是使用的AppBarLayout的OffsetChangedListener,那我们也使用它。首先看看这个监听:

mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
    }
});

我们看一下这个回调方法理由有一个 verticalOffset,这个就是我们竖直方向上AppBarLayout偏移量,也可以理解为AppBarLayout移动距离的变化。知道这些我们只要根据这个值,动态的去计算一下 透明度就OK了。我们去代码去计算一下:

mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        int scrollRangle = appBarLayout.getTotalScrollRange();
        //初始verticalOffset为0,不能参与计算。
        if (verticalOffset == 0) {
            mTvTitle.setAlpha(0.0f);
        } else {
            //保留一位小数
            float alpha = Math.abs(Math.round(1.0f * verticalOffset / scrollRangle) * 10) / 10;
            mTvTitle.setAlpha(alpha);
        }
    }
});

​ 需要注意的是,在未滑动时verticalOffset为0,在计算时一定要注意。

然后我们看一下效果:

现在基本效果实现了,可能还有一些瑕疵,希望大家给多提建议。

最后源码奉上!!!

作者:litengit 发表于2016/10/28 18:37:45 原文链接
阅读:28 评论:0 查看评论

Android简易实战教程--第三十七话《NotifiCation》

$
0
0

通知的使用,无疑是Android系统的亮点之一;就连IOS在5.0开始也引入了类似通知的技巧。可见它的实用性。

今天这个小案例,就学习一下通知的基本使用,API是使用最新的API,4.3以前创建通知的API已经过时。

首先定义个布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/show"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="22dp"
        android:text="开启通知" />

    <Button
        android:id="@+id/cancel"
        android:layout_marginLeft="22dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="关闭通知" />

</LinearLayout>

布局很简单,一个按钮用于开启通知,一个用于关闭通知。

接着就是通知的业务:

package com.example.notification;

import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {

	private Button btShow;
	private Button btCancel;
	private NotificationManager manager;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		btShow = (Button) findViewById(R.id.show);
		btCancel = (Button) findViewById(R.id.cancel);
		
		/**获取通知对象*/
		manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

		btShow.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				// 展示通知
				showNotificationNewAPI();
			}
		});

		btCancel.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				// 关闭通知
				cancelNotification();
			}
		});
	}
	
	
	/**新API展示通知*/
	public void showNotificationNewAPI(){
		NotificationCompat.Builder builder = new Builder(getApplicationContext());
		
		//真正的意图
		Intent intent = new Intent(this, DemoActivity.class);
		//延迟意图,用于启动活动、服务、发送广播等。携带真正的意图对象
		PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		
		
		builder.setSmallIcon(R.drawable.notification_music_playing)//设置通知显示的图标
		.setTicker("ticker")//设置通知刚刚展示时候瞬间展示的通知信息
		.setWhen(System.currentTimeMillis())//设置通知何时出现,System.currentTimeMillis()表示当前时间显示
		/**以上三种方式必须设置,少一项都不会展示通知*/
		.setOngoing(true)//设置滑动通知不可删除
		.setContentTitle("这是通知标题")
		.setContentText("我是通知的内容")
		.setContentIntent(pendingIntent);
		
		//开启通知,第一个参数类似代表该通知的id,标记为1
		manager.notify(1, builder.build());
	}
	/**取消通知*/
	public void cancelNotification(){
		//1表示我要取消标记的通知
		manager.cancel(1);
	}
}

值得一提的就是showNotificationNewAPI()里面创建通知的方式。

对于详细的解释,已经在代码中注明了,相信1分钟搞定~

好啦,运行看看创建的通知的样子吧:

这样的通知还是平凡的一笔,下一篇小案例就针对通知在实际开发中使用,完成自定义的通知,让我们的通知布局“靓起来”。

欢迎关注本博客,不定义更新简单有趣的小文章哦~

作者:qq_32059827 发表于2016/10/28 19:45:00 原文链接
阅读:29 评论:0 查看评论

深入理解ART虚拟机—ART的函数运行机制

$
0
0

前面两篇文章介绍了ART的启动过程,而在启动之后,我们感兴趣的就是ART是怎么运行的。回顾一下虚拟机系列的前面几篇文章,我们可以理一下思路:

一,apk以进程的形式运行,进程的创建是由zygote。

参考文章《深入理解Dalvik虚拟机- Android应用进程启动过程分析》

二,进程运行起来之后,初始化JavaVM

参考文章《深入理解ART虚拟机—虚拟机的启动》

三,JavaVM创建之后,我们就有了JNINativeInterface,里面包含了所有的Java接口,比如FindClass,NewObject,CallObjectMethod等

参考文章《深入理解ART虚拟机—虚拟机的启动》

四,Java的运行时的功能简单来说分为:类的加载和函数Method的执行

参考文章《深入理解Dalvik虚拟机- 解释器的运行机制》

art的JNINativeInterface的定义如下:

const JNINativeInterface gJniNativeInterface = {
  nullptr,  // reserved0.
  nullptr,  // reserved1.
  nullptr,  // reserved2.
  nullptr,  // reserved3.
  JNI::GetVersion,
  JNI::DefineClass,
  JNI::FindClass,
  JNI::FromReflectedMethod,
  JNI::FromReflectedField,
  JNI::ToReflectedMethod,
  JNI::GetSuperclass,
  JNI::IsAssignableFrom,
  JNI::ToReflectedField,
  JNI::Throw,
  JNI::ThrowNew,
  JNI::ExceptionOccurred,
  JNI::ExceptionDescribe,
  JNI::ExceptionClear,
  JNI::FatalError,
  JNI::PushLocalFrame,
  JNI::PopLocalFrame,
  JNI::NewGlobalRef,
  JNI::DeleteGlobalRef,
  JNI::DeleteLocalRef,
  JNI::IsSameObject,
  JNI::NewLocalRef,
  JNI::EnsureLocalCapacity,
  JNI::AllocObject,
  JNI::NewObject,
  JNI::NewObjectV,
  JNI::NewObjectA,
  JNI::GetObjectClass,
  JNI::IsInstanceOf,
  JNI::GetMethodID,
  JNI::CallObjectMethod,
  JNI::CallObjectMethodV,
  JNI::CallObjectMethodA,
  JNI::CallBooleanMethod,
  JNI::CallBooleanMethodV,
  JNI::CallBooleanMethodA,
  JNI::CallByteMethod,
  JNI::CallByteMethodV,
  JNI::CallByteMethodA,
  JNI::CallCharMethod,
  JNI::CallCharMethodV,
  JNI::CallCharMethodA,
  JNI::CallShortMethod,
  JNI::CallShortMethodV,
  JNI::CallShortMethodA,
  JNI::CallIntMethod,
  JNI::CallIntMethodV,
  JNI::CallIntMethodA,
  JNI::CallLongMethod,
  JNI::CallLongMethodV,
  JNI::CallLongMethodA,
  JNI::CallFloatMethod,
  JNI::CallFloatMethodV,
  JNI::CallFloatMethodA,
  JNI::CallDoubleMethod,
  JNI::CallDoubleMethodV,
  JNI::CallDoubleMethodA,
  JNI::CallVoidMethod,
  JNI::CallVoidMethodV,
  JNI::CallVoidMethodA,
  JNI::CallNonvirtualObjectMethod,
  JNI::CallNonvirtualObjectMethodV,
  JNI::CallNonvirtualObjectMethodA,
  JNI::CallNonvirtualBooleanMethod,
  JNI::CallNonvirtualBooleanMethodV,
  JNI::CallNonvirtualBooleanMethodA,
  JNI::CallNonvirtualByteMethod,
  JNI::CallNonvirtualByteMethodV,
  JNI::CallNonvirtualByteMethodA,
  JNI::CallNonvirtualCharMethod,
  JNI::CallNonvirtualCharMethodV,
  JNI::CallNonvirtualCharMethodA,
  JNI::CallNonvirtualShortMethod,
  JNI::CallNonvirtualShortMethodV,
  JNI::CallNonvirtualShortMethodA,
  JNI::CallNonvirtualIntMethod,
  JNI::CallNonvirtualIntMethodV,
  JNI::CallNonvirtualIntMethodA,
  JNI::CallNonvirtualLongMethod,
  JNI::CallNonvirtualLongMethodV,
  JNI::CallNonvirtualLongMethodA,
  JNI::CallNonvirtualFloatMethod,
  JNI::CallNonvirtualFloatMethodV,
  JNI::CallNonvirtualFloatMethodA,
  JNI::CallNonvirtualDoubleMethod,
  JNI::CallNonvirtualDoubleMethodV,
  JNI::CallNonvirtualDoubleMethodA,
  JNI::CallNonvirtualVoidMethod,
  JNI::CallNonvirtualVoidMethodV,
  JNI::CallNonvirtualVoidMethodA,
  JNI::GetFieldID,
  JNI::GetObjectField,
  JNI::GetBooleanField,
  JNI::GetByteField,
  JNI::GetCharField,
  JNI::GetShortField,
  JNI::GetIntField,
  JNI::GetLongField,
  JNI::GetFloatField,
  JNI::GetDoubleField,
  JNI::SetObjectField,
  JNI::SetBooleanField,
  JNI::SetByteField,
  JNI::SetCharField,
  JNI::SetShortField,
  JNI::SetIntField,
  JNI::SetLongField,
  JNI::SetFloatField,
  JNI::SetDoubleField,
  JNI::GetStaticMethodID,
  JNI::CallStaticObjectMethod,
  JNI::CallStaticObjectMethodV,
  JNI::CallStaticObjectMethodA,
  JNI::CallStaticBooleanMethod,
  JNI::CallStaticBooleanMethodV,
  JNI::CallStaticBooleanMethodA,
  JNI::CallStaticByteMethod,
  JNI::CallStaticByteMethodV,
  JNI::CallStaticByteMethodA,
  JNI::CallStaticCharMethod,
  JNI::CallStaticCharMethodV,
  JNI::CallStaticCharMethodA,
  JNI::CallStaticShortMethod,
  JNI::CallStaticShortMethodV,
  JNI::CallStaticShortMethodA,
  JNI::CallStaticIntMethod,
  JNI::CallStaticIntMethodV,
  JNI::CallStaticIntMethodA,
  JNI::CallStaticLongMethod,
  JNI::CallStaticLongMethodV,
  JNI::CallStaticLongMethodA,
  JNI::CallStaticFloatMethod,
  JNI::CallStaticFloatMethodV,
  JNI::CallStaticFloatMethodA,
  JNI::CallStaticDoubleMethod,
  JNI::CallStaticDoubleMethodV,
  JNI::CallStaticDoubleMethodA,
  JNI::CallStaticVoidMethod,
  JNI::CallStaticVoidMethodV,
  JNI::CallStaticVoidMethodA,
  JNI::GetStaticFieldID,
  JNI::GetStaticObjectField,
  JNI::GetStaticBooleanField,
  JNI::GetStaticByteField,
  JNI::GetStaticCharField,
  JNI::GetStaticShortField,
  JNI::GetStaticIntField,
  JNI::GetStaticLongField,
  JNI::GetStaticFloatField,
  JNI::GetStaticDoubleField,
  JNI::SetStaticObjectField,
  JNI::SetStaticBooleanField,
  JNI::SetStaticByteField,
  JNI::SetStaticCharField,
  JNI::SetStaticShortField,
  JNI::SetStaticIntField,
  JNI::SetStaticLongField,
  JNI::SetStaticFloatField,
  JNI::SetStaticDoubleField,
  JNI::NewString,
  JNI::GetStringLength,
  JNI::GetStringChars,
  JNI::ReleaseStringChars,
  JNI::NewStringUTF,
  JNI::GetStringUTFLength,
  JNI::GetStringUTFChars,
  JNI::ReleaseStringUTFChars,
  JNI::GetArrayLength,
  JNI::NewObjectArray,
  JNI::GetObjectArrayElement,
  JNI::SetObjectArrayElement,
  JNI::NewBooleanArray,
  JNI::NewByteArray,
  JNI::NewCharArray,
  JNI::NewShortArray,
  JNI::NewIntArray,
  JNI::NewLongArray,
  JNI::NewFloatArray,
  JNI::NewDoubleArray,
  JNI::GetBooleanArrayElements,
  JNI::GetByteArrayElements,
  JNI::GetCharArrayElements,
  JNI::GetShortArrayElements,
  JNI::GetIntArrayElements,
  JNI::GetLongArrayElements,
  JNI::GetFloatArrayElements,
  JNI::GetDoubleArrayElements,
  JNI::ReleaseBooleanArrayElements,
  JNI::ReleaseByteArrayElements,
  JNI::ReleaseCharArrayElements,
  JNI::ReleaseShortArrayElements,
  JNI::ReleaseIntArrayElements,
  JNI::ReleaseLongArrayElements,
  JNI::ReleaseFloatArrayElements,
  JNI::ReleaseDoubleArrayElements,
  JNI::GetBooleanArrayRegion,
  JNI::GetByteArrayRegion,
  JNI::GetCharArrayRegion,
  JNI::GetShortArrayRegion,
  JNI::GetIntArrayRegion,
  JNI::GetLongArrayRegion,
  JNI::GetFloatArrayRegion,
  JNI::GetDoubleArrayRegion,
  JNI::SetBooleanArrayRegion,
  JNI::SetByteArrayRegion,
  JNI::SetCharArrayRegion,
  JNI::SetShortArrayRegion,
  JNI::SetIntArrayRegion,
  JNI::SetLongArrayRegion,
  JNI::SetFloatArrayRegion,
  JNI::SetDoubleArrayRegion,
  JNI::RegisterNatives,
  JNI::UnregisterNatives,
  JNI::MonitorEnter,
  JNI::MonitorExit,
  JNI::GetJavaVM,
  JNI::GetStringRegion,
  JNI::GetStringUTFRegion,
  JNI::GetPrimitiveArrayCritical,
  JNI::ReleasePrimitiveArrayCritical,
  JNI::GetStringCritical,
  JNI::ReleaseStringCritical,
  JNI::NewWeakGlobalRef,
  JNI::DeleteWeakGlobalRef,
  JNI::ExceptionCheck,
  JNI::NewDirectByteBuffer,
  JNI::GetDirectBufferAddress,
  JNI::GetDirectBufferCapacity,
  JNI::GetObjectRefType,
};
这些函数的定义在jni_internal.cc。

我们要分析art的运行机制,就需要弄清楚类的加载和art函数的执行:

一,类的加载
dalvik的类加载我们已经在《深入理解Dalvik虚拟机- Android应用进程启动过程分析》分析了,Android应用进程启动的时候会创建BaseDexClassLoader,这个BaseDexClassLoader包含了自身apk。再回顾一下过程:
1, app_process作为zygote server通过local socket处理进程创建请求,zygote server是在ZygoteInit.main函数里调用ZygoteInit.runSelectLoop监听。
2, 接收到zygote client的fork请求之后,调用ZygoteConnection.runOnce,调用Zygote.forkAndSpecialize创建新进程
3, 进程创建之后,由ZygoteConnection.handleParentProc来初始化进程,最终会调用ActivityThread.main函数
4, ActivityThread.main -> ActivityThread.attach ->  ActivityThread.bindApplication -> Activity.handleBindApplication,handleBindApplication会初始化BaseDexClassLoader。
5, 类的加载经过了ClassLoader.loadClass->BaseDexClassLoader.findClass->DexPathList.findClass->DexFile.loadClassBinaryName->DexFile.defineClassNative->DexFile_defineClassNative(runtime/native/dalvik_system_DexFile.cc)
这个初始化过程,art和dalvik都是一样的。art的DexFile_defineClassNative由ClassLinker的DefineClass来加载类。

static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
                                        jobject cookie) {
  std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
  if (dex_files.get() == nullptr) {
    VLOG(class_linker) << "Failed to find dex_file";
    DCHECK(env->ExceptionCheck());
    return nullptr;
  }

  ScopedUtfChars class_name(env, javaName);
  if (class_name.c_str() == nullptr) {
    VLOG(class_linker) << "Failed to find class_name";
    return nullptr;
  }
  const std::string descriptor(DotToDescriptor(class_name.c_str()));
  const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
  for (auto& dex_file : *dex_files) {
    const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
    if (dex_class_def != nullptr) {
      ScopedObjectAccess soa(env);
      ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
      class_linker->RegisterDexFile(*dex_file);
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(
          hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
      mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
                                                        class_loader, *dex_file, *dex_class_def);
      if (result != nullptr) {
        VLOG(class_linker) << "DexFile_defineClassNative returning " << result
                           << " for " << class_name.c_str();
        return soa.AddLocalReference<jclass>(result);
      }
    }
  }
  VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
  return nullptr;
}
类的加载除了创建Class只外,还有加载类的字段和方法,这个由ClassLinker::LoadClass来完成。

void ClassLinker::LoadClass(Thread* self, const DexFile& dex_file,
                            const DexFile::ClassDef& dex_class_def,
                            Handle<mirror::Class> klass) {
  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
  if (class_data == nullptr) {
    return;  // no fields or methods - for example a marker interface
  }
  bool has_oat_class = false;
  if (Runtime::Current()->IsStarted() && !Runtime::Current()->IsAotCompiler()) {
    OatFile::OatClass oat_class = FindOatClass(dex_file, klass->GetDexClassDefIndex(),
                                               &has_oat_class);
    if (has_oat_class) {
      LoadClassMembers(self, dex_file, class_data, klass, &oat_class);
    }
  }
  if (!has_oat_class) {
    LoadClassMembers(self, dex_file, class_data, klass, nullptr);
  }
}

二,函数的执行

一旦类的加载完成,那么就可以调用类的成员函数了,之前的解释器运行机制那篇文章介绍过,Java的执行是以Method为执行单元的,所以我们分析art的运行机制,其实就是分析Method的运行机制。

《深入理解Dalvik虚拟机- Android应用进程启动过程分析》可知,ActivityThread是进程在启动的时候传类名,在进程启动之后,由handleParentProc执行main函数,因此第一个被执行的java函数是ActivityThread.main。

 Process.ProcessStartResult startResult = Process.start("android.app.ActivityThread",  
                    app.processName, uid, uid, gids, debugFlags, mountExternal,  
                    app.info.targetSdkVersion, app.info.seinfo, null);
ActivityThread.main是最终由AndroidRuntime::callMain执行

status_t AndroidRuntime::callMain(const String8& className, jclass clazz,
    const Vector<String8>& args)
{
    JNIEnv* env;
    jmethodID methodId;

    ALOGD("Calling main entry %s", className.string());

    env = getJNIEnv();
    if (clazz == NULL || env == NULL) {
        return UNKNOWN_ERROR;
    }
    
    methodId = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V");
    if (methodId == NULL) {
        ALOGE("ERROR: could not find method %s.main(String[])\n", className.string());
        return UNKNOWN_ERROR;
    }
    
    /*
     * We want to call main() with a String array with our arguments in it.
     * Create an array and populate it.
     */
    jclass stringClass;
    jobjectArray strArray;
    
    const size_t numArgs = args.size();
    stringClass = env->FindClass("java/lang/String");
    strArray = env->NewObjectArray(numArgs, stringClass, NULL);

    for (size_t i = 0; i < numArgs; i++) {
        jstring argStr = env->NewStringUTF(args[i].string());
        env->SetObjectArrayElement(strArray, i, argStr);
    }

    env->CallStaticVoidMethod(clazz, methodId, strArray);
    return NO_ERROR;
}
实际会调用JNINativeInterface的CallStaticVoidMethod,上面已经介绍过,该函数的定义在runtime/jni_internal.cc里:

  static void CallStaticVoidMethod(JNIEnv* env, jclass, jmethodID mid, ...) {
    va_list ap;
    va_start(ap, mid);
    CHECK_NON_NULL_ARGUMENT_RETURN_VOID(mid);
    ScopedObjectAccess soa(env);
    InvokeWithVarArgs(soa, nullptr, mid, ap);
    va_end(ap);
  }
InvokeWithVarArgs是执行函数的入口,定义在runtime/reflection.cc,最终是调用了ArtMethod::Invoke

JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa, jobject obj, jmethodID mid,
                         va_list args)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  // We want to make sure that the stack is not within a small distance from the
  // protected region in case we are calling into a leaf function whose stack
  // check has been elided.
  if (UNLIKELY(__builtin_frame_address(0) < soa.Self()->GetStackEnd())) {
    ThrowStackOverflowError(soa.Self());
    return JValue();
  }

  ArtMethod* method = soa.DecodeMethod(mid);
  bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();
  if (is_string_init) {
    // Replace calls to String.<init> with equivalent StringFactory call.
    method = soa.DecodeMethod(WellKnownClasses::StringInitToStringFactoryMethodID(mid));
  }
  mirror::Object* receiver = method->IsStatic() ? nullptr : soa.Decode<mirror::Object*>(obj);
  uint32_t shorty_len = 0;
  const char* shorty = method->GetShorty(&shorty_len);
  JValue result;
  ArgArray arg_array(shorty, shorty_len);
  arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);
  InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
  if (is_string_init) {
    // For string init, remap original receiver to StringFactory result.
    UpdateReference(soa.Self(), obj, result.GetL());
  }
  return result;
}
static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  uint32_t* args = arg_array->GetArray();
  if (UNLIKELY(soa.Env()->check_jni)) {
    CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(sizeof(void*)), args);
  }
  method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}

我们知道ART的运行模式是AOT的,在apk安装的时候,每个DexMethod都会由dex2oat编译成目标代码,而不再是虚拟机执行的字节码,但同时Dex字节码仍然还在OAT里存在,所以ART的代码执行既支持QuickCompiledCode模式,也同时支持解释器模式以及JIT执行模式。看ArtMethod::Invoke

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
  if (UNLIKELY(__builtin_frame_address(0) < self->GetStackEnd())) {
    ThrowStackOverflowError(self);
    return;
  }

  if (kIsDebugBuild) {
    self->AssertThreadSuspensionIsAllowable();
    CHECK_EQ(kRunnable, self->GetState());
    CHECK_STREQ(GetInterfaceMethodIfProxy(sizeof(void*))->GetShorty(), shorty);
  }

  // Push a transition back into managed code onto the linked list in thread.
  ManagedStack fragment;
  self->PushManagedStackFragment(&fragment);

  Runtime* runtime = Runtime::Current();
  // Call the invoke stub, passing everything as arguments.
  // If the runtime is not yet started or it is required by the debugger, then perform the
  // Invocation by the interpreter.
  if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this))) {
    if (IsStatic()) {
      art::interpreter::EnterInterpreterFromInvoke(self, this, nullptr, args, result);
    } else {
      mirror::Object* receiver =
          reinterpret_cast<StackReference<mirror::Object>*>(&args[0])->AsMirrorPtr();
      art::interpreter::EnterInterpreterFromInvoke(self, this, receiver, args + 1, result);
    }
  } else {
    DCHECK_EQ(runtime->GetClassLinker()->GetImagePointerSize(), sizeof(void*));
constexpr bool kLogInvocationStartAndReturn = false;
    bool have_quick_code = GetEntryPointFromQuickCompiledCode() != nullptr;
    if (LIKELY(have_quick_code)) {
      if (kLogInvocationStartAndReturn) {
        LOG(INFO) << StringPrintf(
            "Invoking '%s' quick code=%p static=%d", PrettyMethod(this).c_str(),
            GetEntryPointFromQuickCompiledCode(), static_cast<int>(IsStatic() ? 1 : 0));
      }

      // Ensure that we won't be accidentally calling quick compiled code when -Xint.
      if (kIsDebugBuild && runtime->GetInstrumentation()->IsForcedInterpretOnly()) {
        DCHECK(!runtime->UseJit());
        CHECK(IsEntrypointInterpreter())
            << "Don't call compiled code when -Xint " << PrettyMethod(this);
      }

#if defined(__LP64__) || defined(__arm__) || defined(__i386__)
      if (!IsStatic()) {
        (*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
      } else {
        (*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
      }
#else
      (*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
#endif
      if (UNLIKELY(self->GetException() == Thread::GetDeoptimizationException())) {
        // Unusual case where we were running generated code and an
        // exception was thrown to force the activations to be removed from the
        // stack. Continue execution in the interpreter.
        self->ClearException();
        ShadowFrame* shadow_frame =
            self->PopStackedShadowFrame(StackedShadowFrameType::kDeoptimizationShadowFrame);
        result->SetJ(self->PopDeoptimizationReturnValue().GetJ());
        self->SetTopOfStack(nullptr);
        self->SetTopOfShadowStack(shadow_frame);
        interpreter::EnterInterpreterFromDeoptimize(self, shadow_frame, result);
      }
      if (kLogInvocationStartAndReturn) {
        LOG(INFO) << StringPrintf("Returned '%s' quick code=%p", PrettyMethod(this).c_str(),
                                  GetEntryPointFromQuickCompiledCode());
      }
    } else {
      LOG(INFO) << "Not invoking '" << PrettyMethod(this) << "' code=null";
      if (result != nullptr) {
        result->SetJ(0);
      }
    }
  }

  // Pop transition.
  self->PopManagedStackFragment(fragment);
}

Invoke可以进入OAT,Interpreter模式执行Method,如果当前是Interpreter模式,就调用art::interpreter::EnterInterpreterFromInvoke,如果是OAT模式,就调用art_quick_invoke_stub/art_quick_invoke_static_stub。

EnterInterpreterFromInvoke函数里会判断是native还是解释器执行:

void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
                                uint32_t* args, JValue* result) {
  DCHECK_EQ(self, Thread::Current());
  bool implicit_check = !Runtime::Current()->ExplicitStackOverflowChecks();
  if (UNLIKELY(__builtin_frame_address(0) < self->GetStackEndForInterpreter(implicit_check))) {
    ThrowStackOverflowError(self);
    return;
  }

  const char* old_cause = self->StartAssertNoThreadSuspension("EnterInterpreterFromInvoke");
  const DexFile::CodeItem* code_item = method->GetCodeItem();
  uint16_t num_regs;
  uint16_t num_ins;
  if (code_item != nullptr) {
    num_regs =  code_item->registers_size_;
    num_ins = code_item->ins_size_;
  } else if (method->IsAbstract()) {
    self->EndAssertNoThreadSuspension(old_cause);
    ThrowAbstractMethodError(method);
    return;
  } else {
    DCHECK(method->IsNative());
    num_regs = num_ins = ArtMethod::NumArgRegisters(method->GetShorty());
    if (!method->IsStatic()) {
      num_regs++;
      num_ins++;
    }
  }
  // Set up shadow frame with matching number of reference slots to vregs.
  ShadowFrame* last_shadow_frame = self->GetManagedStack()->GetTopShadowFrame();
  void* memory = alloca(ShadowFrame::ComputeSize(num_regs));
  ShadowFrame* shadow_frame(ShadowFrame::Create(num_regs, last_shadow_frame, method, 0, memory));
  self->PushShadowFrame(shadow_frame);

  size_t cur_reg = num_regs - num_ins;
  if (!method->IsStatic()) {
    CHECK(receiver != nullptr);
    shadow_frame->SetVRegReference(cur_reg, receiver);
    ++cur_reg;
  }
  uint32_t shorty_len = 0;
  const char* shorty = method->GetShorty(&shorty_len);
  for (size_t shorty_pos = 0, arg_pos = 0; cur_reg < num_regs; ++shorty_pos, ++arg_pos, cur_reg++) {
    DCHECK_LT(shorty_pos + 1, shorty_len);
    switch (shorty[shorty_pos + 1]) {
      case 'L': {
        Object* o = reinterpret_cast<StackReference<Object>*>(&args[arg_pos])->AsMirrorPtr();
        shadow_frame->SetVRegReference(cur_reg, o);
        break;
      }
      case 'J': case 'D': {
        uint64_t wide_value = (static_cast<uint64_t>(args[arg_pos + 1]) << 32) | args[arg_pos];
        shadow_frame->SetVRegLong(cur_reg, wide_value);
        cur_reg++;
        arg_pos++;
        break;
      }
      default:
        shadow_frame->SetVReg(cur_reg, args[arg_pos]);
        break;
    }
  }
  self->EndAssertNoThreadSuspension(old_cause);
  // Do this after populating the shadow frame in case EnsureInitialized causes a GC.
  if (method->IsStatic() && UNLIKELY(!method->GetDeclaringClass()->IsInitialized())) {
    ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
    StackHandleScope<1> hs(self);
    Handle<mirror::Class> h_class(hs.NewHandle(method->GetDeclaringClass()));
    if (UNLIKELY(!class_linker->EnsureInitialized(self, h_class, true, true))) {
      CHECK(self->IsExceptionPending());
      self->PopShadowFrame();
      return;
    }
  }
  if (LIKELY(!method->IsNative())) {
    JValue r = Execute(self, code_item, *shadow_frame, JValue());
    if (result != nullptr) {
      *result = r;
    }
  } else {
    // We don't expect to be asked to interpret native code (which is entered via a JNI compiler
    // generated stub) except during testing and image writing.
    // Update args to be the args in the shadow frame since the input ones could hold stale
    // references pointers due to moving GC.
    args = shadow_frame->GetVRegArgs(method->IsStatic() ? 0 : 1);
    if (!Runtime::Current()->IsStarted()) {
      UnstartedRuntime::Jni(self, method, receiver, args, result);
    } else {
      InterpreterJni(self, method, shorty, receiver, args, result);
    }
  }
  self->PopShadowFrame();
}
这个函数前面部分都在做参数压栈操作,最后几行进入主题,如果不是Native,那么调用Execute执行;Native函数则调用InterpreterJni。Execute就是art的解释器代码,Dex的字节码是通过ArtMethod::GetCodeItem函数获得,由Execute逐条执行。InterpreterJni通过GetEntryPointFromJni来获得native的函数,并执行。

if (LIKELY(!method->IsNative())) {
    JValue r = Execute(self, code_item, *shadow_frame, JValue());
    if (result != nullptr) {
      *result = r;
    }
  } else {
    // We don't expect to be asked to interpret native code (which is entered via a JNI compiler
    // generated stub) except during testing and image writing.
    // Update args to be the args in the shadow frame since the input ones could hold stale
    // references pointers due to moving GC.
    args = shadow_frame->GetVRegArgs(method->IsStatic() ? 0 : 1);
    if (!Runtime::Current()->IsStarted()) {
      UnstartedRuntime::Jni(self, method, receiver, args, result);
    } else {
      InterpreterJni(self, method, shorty, receiver, args, result);
    }
  }

再回调OAT的模式,art_quick_invoke_stub/art_quick_invoke_static_stub最终会调用到art_quick_invoke_stub_internal(arch/arm/quick_entrypoints_arm.S)

ENTRY art_quick_invoke_stub_internal
    push   {r4, r5, r6, r7, r8, r9, r10, r11, lr}               @ spill regs
    .cfi_adjust_cfa_offset 16
    .cfi_rel_offset r4, 0
    .cfi_rel_offset r5, 4
    .cfi_rel_offset r6, 8
    .cfi_rel_offset r7, 12
    .cfi_rel_offset r8, 16
    .cfi_rel_offset r9, 20
    .cfi_rel_offset r10, 24
    .cfi_rel_offset r11, 28
    .cfi_rel_offset lr, 32
    mov    r11, sp                         @ save the stack pointer
    .cfi_def_cfa_register r11

    mov    r9, r3                          @ move managed thread pointer into r9

    add    r4, r2, #4                      @ create space for method pointer in frame
    sub    r4, sp, r4                      @ reserve & align *stack* to 16 bytes: native calling
    and    r4, #0xFFFFFFF0                 @ convention only aligns to 8B, so we have to ensure ART
    mov    sp, r4                          @ 16B alignment ourselves.

    mov    r4, r0                          @ save method*
    add    r0, sp, #4                      @ pass stack pointer + method ptr as dest for memcpy
    bl     memcpy                          @ memcpy (dest, src, bytes)
    mov    ip, #0                          @ set ip to 0
    str    ip, [sp]                        @ store null for method* at bottom of frame

    ldr    ip, [r11, #48]                  @ load fp register argument array pointer
    vldm   ip, {s0-s15}                    @ copy s0 - s15

    ldr    ip, [r11, #44]                  @ load core register argument array pointer
    mov    r0, r4                          @ restore method*
    add    ip, ip, #4                      @ skip r0
    ldm    ip, {r1-r3}                     @ copy r1 - r3
#ifdef ARM_R4_SUSPEND_FLAG
    mov    r4, #SUSPEND_CHECK_INTERVAL     @ reset r4 to suspend check interval
#endif

    ldr    ip, [r0, #ART_METHOD_QUICK_CODE_OFFSET_32]  @ get pointer to the code
    blx    ip                              @ call the method

    mov    sp, r11                         @ restore the stack pointer
    .cfi_def_cfa_register sp

    ldr    r4, [sp, #40]                   @ load result_is_float
    ldr    r9, [sp, #36]                   @ load the result pointer
    cmp    r4, #0
    ite    eq
    strdeq r0, [r9]                        @ store r0/r1 into result pointer
    vstrne d0, [r9]                        @ store s0-s1/d0 into result pointer

    pop    {r4, r5, r6, r7, r8, r9, r10, r11, pc}               @ restore spill regs
END art_quick_invoke_stub_internal

找到ArtMethod的entry_point_from_quick_compiled_code_字段,这个就是EntryPointFromQuickCompiledCode,从而进入OAT函数执行。

#define ART_METHOD_QUICK_CODE_OFFSET_32 36
ADD_TEST_EQ(ART_METHOD_QUICK_CODE_OFFSET_32,
            art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(4).Int32Value())

EntryPointFromQuickCompiledCode的初始化在class_linker的LoadClassMembers时调用的LinkCode,有下面几种类型

1,SetEntryPointFromQuickCompiledCode(GetQuickCode());   // 这个是执行OatMethod

2,SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());  //  Dex Method

3,SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());  // Native Method

4,SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());   // method->IsStatic() && !method->IsConstructor()

如果是强制使用了解释器模式,那么执行的是代码GetQuickToInterpreterBridge(non-static, non-native)或GetQuickGenericJniStub(non-static, native)或GetQuickResolutionStub(static),这几个EntryPoint对应的实际执行函数如下。

GetQuickGenericJniStub — artQuickGenericJniTrampoline

GetQuickResolutionStub — artQuickResolutionTrampoline

GetQuickToInterpreterBridge — artQuickToInterpreterBridge

ArtMthod被Resolve之后,如果是走Oat模式就会执行GetQuickCode。

楼上是EntryPointFromQuickCompiledCode的情况:

不同的执行模式有不同的EntryPoint:

1,解释器 - EntryPointFromInterpreter

在interpreter/interpreter_common.cc里会在执行解释器函数时,会获得ArtMethod的Interpret EntryPoint执行

2,Jni - EntryPointFromJni

interpreter/interpreter.cc,InterpreterJni函数会获得ArtMethod的Jni EntryPoint执行

3,Oat - EntryPointFromQuickCompiledCode

DexCache在Init的时候会将Method都初始化为ResolutionMethod,这个Resolution Method是没有dex method id的,是个RuntimeMethod,这是lazy load method,运行时resolve之后才会替换成实际的ArtMethod。

void DexCache::Init(const DexFile* dex_file, String* location, ObjectArray<String>* strings,
                    ObjectArray<Class>* resolved_types, PointerArray* resolved_methods,
                    PointerArray* resolved_fields, size_t pointer_size) {
  CHECK(dex_file != nullptr);
  CHECK(location != nullptr);
  CHECK(strings != nullptr);
  CHECK(resolved_types != nullptr);
  CHECK(resolved_methods != nullptr);
  CHECK(resolved_fields != nullptr);

  SetDexFile(dex_file);
  SetFieldObject<false>(OFFSET_OF_OBJECT_MEMBER(DexCache, location_), location);
  SetFieldObject<false>(StringsOffset(), strings);
  SetFieldObject<false>(ResolvedFieldsOffset(), resolved_fields);
  SetFieldObject<false>(OFFSET_OF_OBJECT_MEMBER(DexCache, resolved_types_), resolved_types);
  SetFieldObject<false>(ResolvedMethodsOffset(), resolved_methods);

  Runtime* const runtime = Runtime::Current();
  if (runtime->HasResolutionMethod()) {
    // Initialize the resolve methods array to contain trampolines for resolution.
    Fixup(runtime->GetResolutionMethod(), pointer_size);
  }
}

void DexCache::Fixup(ArtMethod* trampoline, size_t pointer_size) {
  // Fixup the resolve methods array to contain trampoline for resolution.
  CHECK(trampoline != nullptr);
  CHECK(trampoline->IsRuntimeMethod());
  auto* resolved_methods = GetResolvedMethods();
  for (size_t i = 0, length = resolved_methods->GetLength(); i < length; i++) {
    if (resolved_methods->GetElementPtrSize<ArtMethod*>(i, pointer_size) == nullptr) {
      resolved_methods->SetElementPtrSize(i, trampoline, pointer_size);
    }
  }
}
resolution method的EntryPointFromQuickCompiledCode指向GetQuickResolutionStub,意思就是一开始,这些函数的执行点都是从artQuickResolutionTrampoline开始。

// Lazily resolve a method for quick. Called by stub code.
extern "C" const void* artQuickResolutionTrampoline(
    ArtMethod* called, mirror::Object* receiver, Thread* self, ArtMethod** sp)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  ScopedQuickEntrypointChecks sqec(self);
  // Start new JNI local reference state
  JNIEnvExt* env = self->GetJniEnv();
  ScopedObjectAccessUnchecked soa(env);
  ScopedJniEnvLocalRefState env_state(env);
  const char* old_cause = self->StartAssertNoThreadSuspension("Quick method resolution set up");

  // Compute details about the called method (avoid GCs)
  ClassLinker* linker = Runtime::Current()->GetClassLinker();
  ArtMethod* caller = QuickArgumentVisitor::GetCallingMethod(sp);
  InvokeType invoke_type;
  MethodReference called_method(nullptr, 0);
  const bool called_method_known_on_entry = !called->IsRuntimeMethod();
  if (!called_method_known_on_entry) {
    uint32_t dex_pc = caller->ToDexPc(QuickArgumentVisitor::GetCallingPc(sp));
    const DexFile::CodeItem* code;
    called_method.dex_file = caller->GetDexFile();
    code = caller->GetCodeItem();
    CHECK_LT(dex_pc, code->insns_size_in_code_units_);
    const Instruction* instr = Instruction::At(&code->insns_[dex_pc]);
    Instruction::Code instr_code = instr->Opcode();
    bool is_range;
    switch (instr_code) {
      case Instruction::INVOKE_DIRECT:
        invoke_type = kDirect;
        is_range = false;
        break;
      case Instruction::INVOKE_DIRECT_RANGE:
        invoke_type = kDirect;
        is_range = true;
        break;
      case Instruction::INVOKE_STATIC:
        invoke_type = kStatic;
        is_range = false;
        break;
      case Instruction::INVOKE_STATIC_RANGE:
        invoke_type = kStatic;
        is_range = true;
        break;
      case Instruction::INVOKE_SUPER:
        invoke_type = kSuper;
        is_range = false;
        break;
      case Instruction::INVOKE_SUPER_RANGE:
        invoke_type = kSuper;
        is_range = true;
        break;
      case Instruction::INVOKE_VIRTUAL:
        invoke_type = kVirtual;
        is_range = false;
        break;
      case Instruction::INVOKE_VIRTUAL_RANGE:
        invoke_type = kVirtual;
        is_range = true;
        break;
      case Instruction::INVOKE_INTERFACE:
        invoke_type = kInterface;
        is_range = false;
        break;
      case Instruction::INVOKE_INTERFACE_RANGE:
        invoke_type = kInterface;
        is_range = true;
        break;
      default:
        LOG(FATAL) << "Unexpected call into trampoline: " << instr->DumpString(nullptr);
        UNREACHABLE();
    }
    called_method.dex_method_index = (is_range) ? instr->VRegB_3rc() : instr->VRegB_35c();
  } else {
    invoke_type = kStatic;
    called_method.dex_file = called->GetDexFile();
    called_method.dex_method_index = called->GetDexMethodIndex();
  }
  uint32_t shorty_len;
  const char* shorty =
      called_method.dex_file->GetMethodShorty(
          called_method.dex_file->GetMethodId(called_method.dex_method_index), &shorty_len);
  RememberForGcArgumentVisitor visitor(sp, invoke_type == kStatic, shorty, shorty_len, &soa);
  visitor.VisitArguments();
  self->EndAssertNoThreadSuspension(old_cause);
  const bool virtual_or_interface = invoke_type == kVirtual || invoke_type == kInterface;
  // Resolve method filling in dex cache.
  if (!called_method_known_on_entry) {
    StackHandleScope<1> hs(self);
    mirror::Object* dummy = nullptr;
    HandleWrapper<mirror::Object> h_receiver(
        hs.NewHandleWrapper(virtual_or_interface ? &receiver : &dummy));
    DCHECK_EQ(caller->GetDexFile(), called_method.dex_file);
    called = linker->ResolveMethod(self, called_method.dex_method_index, caller, invoke_type);
  }
  const void* code = nullptr;
  if (LIKELY(!self->IsExceptionPending())) {
    // Incompatible class change should have been handled in resolve method.
    CHECK(!called->CheckIncompatibleClassChange(invoke_type))
        << PrettyMethod(called) << " " << invoke_type;
    if (virtual_or_interface) {
      // Refine called method based on receiver.
      CHECK(receiver != nullptr) << invoke_type;

      ArtMethod* orig_called = called;
      if (invoke_type == kVirtual) {
        called = receiver->GetClass()->FindVirtualMethodForVirtual(called, sizeof(void*));
      } else {
        called = receiver->GetClass()->FindVirtualMethodForInterface(called, sizeof(void*));
      }

      CHECK(called != nullptr) << PrettyMethod(orig_called) << " "
                               << PrettyTypeOf(receiver) << " "
                               << invoke_type << " " << orig_called->GetVtableIndex();

      // We came here because of sharpening. Ensure the dex cache is up-to-date on the method index
      // of the sharpened method avoiding dirtying the dex cache if possible.
      // Note, called_method.dex_method_index references the dex method before the
      // FindVirtualMethodFor... This is ok for FindDexMethodIndexInOtherDexFile that only cares
      // about the name and signature.
      uint32_t update_dex_cache_method_index = called->GetDexMethodIndex();
      if (!called->HasSameDexCacheResolvedMethods(caller)) {
        // Calling from one dex file to another, need to compute the method index appropriate to
        // the caller's dex file. Since we get here only if the original called was a runtime
        // method, we've got the correct dex_file and a dex_method_idx from above.
        DCHECK(!called_method_known_on_entry);
        DCHECK_EQ(caller->GetDexFile(), called_method.dex_file);
        const DexFile* caller_dex_file = called_method.dex_file;
        uint32_t caller_method_name_and_sig_index = called_method.dex_method_index;
        update_dex_cache_method_index =
            called->FindDexMethodIndexInOtherDexFile(*caller_dex_file,
                                                     caller_method_name_and_sig_index);
      }
      if ((update_dex_cache_method_index != DexFile::kDexNoIndex) &&
          (caller->GetDexCacheResolvedMethod(
              update_dex_cache_method_index, sizeof(void*)) != called)) {
        caller->SetDexCacheResolvedMethod(update_dex_cache_method_index, called, sizeof(void*));
      }
    } else if (invoke_type == kStatic) {
      const auto called_dex_method_idx = called->GetDexMethodIndex();
      // For static invokes, we may dispatch to the static method in the superclass but resolve
      // using the subclass. To prevent getting slow paths on each invoke, we force set the
      // resolved method for the super class dex method index if we are in the same dex file.
      // b/19175856
      if (called->GetDexFile() == called_method.dex_file &&
          called_method.dex_method_index != called_dex_method_idx) {
        called->GetDexCache()->SetResolvedMethod(called_dex_method_idx, called, sizeof(void*));
      }
    }

    // Ensure that the called method's class is initialized.
    StackHandleScope<1> hs(soa.Self());
    Handle<mirror::Class> called_class(hs.NewHandle(called->GetDeclaringClass()));
    linker->EnsureInitialized(soa.Self(), called_class, true, true);
    if (LIKELY(called_class->IsInitialized())) {
      if (UNLIKELY(Dbg::IsForcedInterpreterNeededForResolution(self, called))) {
        // If we are single-stepping or the called method is deoptimized (by a
        // breakpoint, for example), then we have to execute the called method
        // with the interpreter.
        code = GetQuickToInterpreterBridge();
      } else if (UNLIKELY(Dbg::IsForcedInstrumentationNeededForResolution(self, caller))) {
        // If the caller is deoptimized (by a breakpoint, for example), we have to
        // continue its execution with interpreter when returning from the called
        // method. Because we do not want to execute the called method with the
        // interpreter, we wrap its execution into the instrumentation stubs.
        // When the called method returns, it will execute the instrumentation
        // exit hook that will determine the need of the interpreter with a call
        // to Dbg::IsForcedInterpreterNeededForUpcall and deoptimize the stack if
        // it is needed.
        code = GetQuickInstrumentationEntryPoint();
      } else {
        code = called->GetEntryPointFromQuickCompiledCode();
      }
    } else if (called_class->IsInitializing()) {
      if (UNLIKELY(Dbg::IsForcedInterpreterNeededForResolution(self, called))) {
        // If we are single-stepping or the called method is deoptimized (by a
        // breakpoint, for example), then we have to execute the called method
        // with the interpreter.
        code = GetQuickToInterpreterBridge();
      } else if (invoke_type == kStatic) {
        // Class is still initializing, go to oat and grab code (trampoline must be left in place
        // until class is initialized to stop races between threads).
        code = linker->GetQuickOatCodeFor(called);
      } else {
        // No trampoline for non-static methods.
        code = called->GetEntryPointFromQuickCompiledCode();
      }
    } else {
      DCHECK(called_class->IsErroneous());
    }
  }
  CHECK_EQ(code == nullptr, self->IsExceptionPending());
  // Fixup any locally saved objects may have moved during a GC.
  visitor.FixupReferences();
  // Place called method in callee-save frame to be placed as first argument to quick method.
  *sp = called;

  return code;
}
上面代码可知,找到当前ArtMethod的流程大致的逻辑就是,根据caller函数ArtMethod的dex代码,可以找到这个ArtMethod的函数调用类型(INVOKE_DIRECT,INVOKE_STATIC,INVOKE_SUPER,INVOKE_VIRTUAL etc.),不同的类型查找的方式不一样,比如Virtual Method要从虚表里找,Super Method要从父类的Method里去找,找到之后调用ClassLinker的ResolveMethod来解析,解析出来的ArtMethod的就是上面LinkCode过的ArtMethod。

下面就是ResolveMethod函数的实现,Calss查找到Method,之后在赋值到DexCache里,这样下次再执行就能直接找到Resolved Method。

ArtMethod* ClassLinker::ResolveMethod(const DexFile& dex_file, uint32_t method_idx,
                                      Handle<mirror::DexCache> dex_cache,
                                      Handle<mirror::ClassLoader> class_loader,
                                      ArtMethod* referrer, InvokeType type) {
  DCHECK(dex_cache.Get() != nullptr);
  // Check for hit in the dex cache.
  ArtMethod* resolved = dex_cache->GetResolvedMethod(method_idx, image_pointer_size_);
  if (resolved != nullptr && !resolved->IsRuntimeMethod()) {
    DCHECK(resolved->GetDeclaringClassUnchecked() != nullptr) << resolved->GetDexMethodIndex();
    return resolved;
  }
  // Fail, get the declaring class.
  const DexFile::MethodId& method_id = dex_file.GetMethodId(method_idx);
  mirror::Class* klass = ResolveType(dex_file, method_id.class_idx_, dex_cache, class_loader);
  if (klass == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return nullptr;
  }
  // Scan using method_idx, this saves string compares but will only hit for matching dex
  // caches/files.
  switch (type) {
    case kDirect:  // Fall-through.
    case kStatic:
      resolved = klass->FindDirectMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClassUnchecked() != nullptr);
      break;
    case kInterface:
      resolved = klass->FindInterfaceMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
      break;
    case kSuper:  // Fall-through.
    case kVirtual:
      resolved = klass->FindVirtualMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      break;
    default:
      LOG(FATAL) << "Unreachable - invocation type: " << type;
      UNREACHABLE();
  }
  if (resolved == nullptr) {
    // Search by name, which works across dex files.
    const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
    const Signature signature = dex_file.GetMethodSignature(method_id);
    switch (type) {
      case kDirect:  // Fall-through.
      case kStatic:
        resolved = klass->FindDirectMethod(name, signature, image_pointer_size_);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClassUnchecked() != nullptr);
        break;
      case kInterface:
        resolved = klass->FindInterfaceMethod(name, signature, image_pointer_size_);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
        break;
      case kSuper:  // Fall-through.
      case kVirtual:
        resolved = klass->FindVirtualMethod(name, signature, image_pointer_size_);
        break;
    }
  }
  // If we found a method, check for incompatible class changes.
  if (LIKELY(resolved != nullptr && !resolved->CheckIncompatibleClassChange(type))) {
    // Be a good citizen and update the dex cache to speed subsequent calls.
    dex_cache->SetResolvedMethod(method_idx, resolved, image_pointer_size_);
    return resolved;
  } else {
    // If we had a method, it's an incompatible-class-change error.
    if (resolved != nullptr) {
      ThrowIncompatibleClassChangeError(type, resolved->GetInvokeType(), resolved, referrer);
    } else {
      // We failed to find the method which means either an access error, an incompatible class
      // change, or no such method. First try to find the method among direct and virtual methods.
      const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
      const Signature signature = dex_file.GetMethodSignature(method_id);
      switch (type) {
        case kDirect:
        case kStatic:
          resolved = klass->FindVirtualMethod(name, signature, image_pointer_size_);
          // Note: kDirect and kStatic are also mutually exclusive, but in that case we would
          //       have had a resolved method before, which triggers the "true" branch above.
          break;
        case kInterface:
        case kVirtual:
        case kSuper:
          resolved = klass->FindDirectMethod(name, signature, image_pointer_size_);
          break;
      }

      // If we found something, check that it can be accessed by the referrer.
      bool exception_generated = false;
      if (resolved != nullptr && referrer != nullptr) {
        mirror::Class* methods_class = resolved->GetDeclaringClass();
        mirror::Class* referring_class = referrer->GetDeclaringClass();
        if (!referring_class->CanAccess(methods_class)) {
          ThrowIllegalAccessErrorClassForMethodDispatch(referring_class, methods_class, resolved,
                                                        type);
          exception_generated = true;
        } else if (!referring_class->CanAccessMember(methods_class, resolved->GetAccessFlags())) {
          ThrowIllegalAccessErrorMethod(referring_class, resolved);
          exception_generated = true;
        }
      }
      if (!exception_generated) {
        // Otherwise, throw an IncompatibleClassChangeError if we found something, and check
        // interface methods and throw if we find the method there. If we find nothing, throw a
        // NoSuchMethodError.
        switch (type) {
          case kDirect:
          case kStatic:
            if (resolved != nullptr) {
              ThrowIncompatibleClassChangeError(type, kVirtual, resolved, referrer);
            } else {
              resolved = klass->FindInterfaceMethod(name, signature, image_pointer_size_);
              if (resolved != nullptr) {
                ThrowIncompatibleClassChangeError(type, kInterface, resolved, referrer);
              } else {
                ThrowNoSuchMethodError(type, klass, name, signature);
              }
            }
            break;
          case kInterface:
            if (resolved != nullptr) {
              ThrowIncompatibleClassChangeError(type, kDirect, resolved, referrer);
            } else {
              resolved = klass->FindVirtualMethod(name, signature, image_pointer_size_);
              if (resolved != nullptr) {
                ThrowIncompatibleClassChangeError(type, kVirtual, resolved, referrer);
              } else {
                ThrowNoSuchMethodError(type, klass, name, signature);
              }
            }
            break;
          case kSuper:
            if (resolved != nullptr) {
              ThrowIncompatibleClassChangeError(type, kDirect, resolved, referrer);
            } else {
              ThrowNoSuchMethodError(type, klass, name, signature);
            }
            break;
          case kVirtual:
            if (resolved != nullptr) {
              ThrowIncompatibleClassChangeError(type, kDirect, resolved, referrer);
            } else {
              resolved = klass->FindInterfaceMethod(name, signature, image_pointer_size_);
              if (resolved != nullptr) {
                ThrowIncompatibleClassChangeError(type, kInterface, resolved, referrer);
              } else {
                ThrowNoSuchMethodError(type, klass, name, signature);
              }
            }
            break;
        }
      }
    }
    Thread::Current()->AssertPendingException();
    return nullptr;
  }
}

至此,Art Method的执行机制就算介绍完了,我们对整个函数执行机制都有个全局的概念了,包括:

1,Art怎么进入第一个Method

2,ClassLinker在初始化的时候怎么加载成员函数(初始化几个EntryPoint)

3,DexCache初始化的时候将ArtMethod初始化成Resolution Method,后续在运行时ResolveMethod

4,解释器模式在Art下是如何运行的

作者:threepigs 发表于2016/11/4 16:09:57 原文链接
阅读:113 评论:0 查看评论

Android TV横向滚动网格布局——RecyclerView的使用

$
0
0

最近在做一个Android盒子的项目,主要是Launcher有一个横向滚动的界面。主要使用的是RecyclerView。总结一下。

一、先了解下RecyclerView
RecyclerView是类似于ListView、GridView的一种AdapterView。相比较的优势是使用更加灵活,可以满足实现更多不同的效果。

在我要实现的水平滚动网格布局中就得到了很好的满足。因为使用HorizentalScrollView + GridView的模式会十分复杂,并且焦点、动作的监听会比较混乱。事件冲突处理起来特别麻烦。

二、实现简单例子
记录一下我自己学习的过程。先写了一下RecyclerViewTest的工程。这个工程的主要效果是在页面上显示Android设备上所安装的所有应用。并且点击应用图标可以进入相应的应用。点击菜单键可以卸载该应用。

大概就是这样:
这里写图片描述

1.先要关联recyclerview的jar包:

dependencies {
    ...
    compile 'com.android.support:recyclerview-v7:23.0.1'
}

2.然后就可以使用RecyclerView了,先根据需求自定义了一个SimpleRecyclerView.java

public class SimpleRecycleView extends RecyclerView {
    private static final String TAG = SimpleRecycleView.class.getSimpleName();
    // 一个滚动对象
    private Scroller mScroller;
    private int mLastX = 0;

    public SimpleRecycleView(Context context) {
        super(context);
        init(context);
    }

    public SimpleRecycleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SimpleRecycleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    // 一个初始化方法,传入了一个上下文对象,用来初始化滚动对象
    private void init(Context context){
        mScroller = new Scroller(context);
    }

    // 重写了计算滚动方法
    @Override
    public void computeScroll() {
        if(mScroller!=null && mScroller.computeScrollOffset()){
            scrollBy(mLastX - mScroller.getCurrX(), 0);
            mLastX = mScroller.getCurrX();
            postInvalidate();
        }
    }



    /**
     * 调用此方法滚动到目标位置,其中(fx, fy)表示最终要滚到的目标位置的坐标值
     * duration表示期间滚动的耗时。
     *
     * @param fx 目标位置的X向坐标值
     * @param fy 目标位置的Y向坐标值
     * @param duration 滚动到目标位置所消耗的时间毫秒值
     */
    @SuppressWarnings("unused")
    public void smoothScrollTo(int fx, int fy,int duration) {
        int dx = 0;
        int dy = 0;
        // 计算变化的位移量
        if(fx != 0) {
            dx = fx - mScroller.getFinalX();
        }
        if(fy!=0) {
            dy = fy - mScroller.getFinalY();
        }
        Log.i(TAG, "fx:" + fx + ", getFinalX:" + mScroller.getFinalX() + ", dx:" + dx);
        smoothScrollBy(dx, dy, duration);
    }

    /**
     * 调用此方法设置滚动的相对偏移
     */
    public void smoothScrollBy(int dx, int dy, int duration) {
        if(duration > 0) {
            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
        } else {
            // 设置mScroller的滚动偏移量
            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
        }
        // 重绘整个view,重绘过程会调用到computeScroll()方法。
        // 这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
        invalidate();
    }

    /**
     * 此方法用来检查自动调节
     *
     * @param position 要检查的位置
     */
    @SuppressWarnings("unused")
    public void checkAutoAdjust(int position){
        int childCount = getChildCount();
        // 获取可视范围内的选项的头尾位置
        int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
        int lastVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
        Log.d(TAG, "childCount:" + childCount + ", position:" + position + ", firstVisibleItemPosition:" + firstVisibleItemPosition
                + "  lastVisibleItemPosition:" + lastVisibleItemPosition);
        if(position == (firstVisibleItemPosition + 1) || position == firstVisibleItemPosition){
            // 当前位置需要向右平移
            leftScrollBy(position, firstVisibleItemPosition);
        } else if (position == (lastVisibleItemPosition - 1) || position == lastVisibleItemPosition){
            // 当前位置需要向左平移
            rightScrollBy(position, lastVisibleItemPosition);
        }
    }

    private void leftScrollBy(int position, int firstVisibleItemPosition){
        View leftChild = getChildAt(0);
        if(leftChild != null){
            int startLeft = leftChild.getLeft();
            int endLeft = (position == firstVisibleItemPosition ? leftChild.getWidth() : 0);
            Log.d(TAG, "startLeft:" + startLeft + " endLeft" + endLeft);
            autoAdjustScroll(startLeft, endLeft);
        }
    }

    private void rightScrollBy(int position, int lastVisibleItemPosition){
        int childCount = getChildCount();
        View rightChild = getChildAt(childCount - 1);
        if(rightChild != null){
            int startRight = rightChild.getRight() - getWidth();
            int endRight = (position == lastVisibleItemPosition ? (-1 * rightChild.getWidth()) : 0);
            Log.d(TAG,"startRight:" + startRight + " endRight:" + endRight);
            autoAdjustScroll(startRight, endRight);
        }
    }

    /**
     *
     * @param start 滑动起始位置
     * @param end 滑动结束位置
     */
    private void autoAdjustScroll(int start, int end){
        mLastX = start;
        mScroller.startScroll(start, 0, end - start, 0);
        postInvalidate();
    }


    /**
     * 将指定item平滑移动到整个view的中间位置
     * @param position 指定的item的位置
     */
    public void smoothScrollMaster(int position) {
        // 这个方法是为了设置Scroller的滚动的,需要根据业务需求,编写算法。
    }

}

3.然后就可以在布局文件中使用自定义的控件了:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/home_background">

    <com.jiuzhou.porter.launcher.widget.SimpleRecycleView
        android:id="@+id/home_apps"
        android:layout_marginLeft="@dimen/px_positive_80"
        android:layout_marginRight="@dimen/px_positive_80"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scrollbars="none" />
</RelativeLayout>

我去不小心暴露了我的包名(@^_^@)

4.在MainActivity.java中编写代码:

//初始化RecyclerView,设置布局管理器、间距、适配器、数据等
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main)
    ...
    // 1.初始化SimpleRecyclerView
    mRecyclerView = (SimpleRecycleView) findViewById(R.id.home_apps);
    // 2.使用自定义的工具类获得设备中安装的APP
    mListOfApps = LauncherCommonUtils.getAllApk(this);
    // 3.初始化适配器
    SimpleRecyclerAdapter mAdapter = new SimpleRecyclerAdapter(this, mListOfApps);
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    // 4.设置布局管理器:瀑布流式
    StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3 , StaggeredGridLayoutManager.HORIZONTAL);
    // 5.根据需要设置间距等其他内容
    mRecyclerView.setLayoutManager(staggeredGridLayoutManager);
    int right = (int) getResources().getDimension(R.dimen.px_positive_5);
    int bottom = (int) getResources().getDimension(R.dimen.px_positive_1);
    RecyclerView.ItemDecoration spacingInPixel = new SpaceItemDecoration(right, bottom);
    mRecyclerView.addItemDecoration(spacingInPixel);
    // 6.关联适配器
    mRecyclerView.setAdapter(mAdapter);
}
  • 这里有必要说一下:
    关于布局管理器的内容:
    RecyclerView的使用时是必须设置布局管理器的。因为不同的布局管理器决定了展现出来的演示是怎样的。
    常见的集中布局管理器有LinearLayoutManager、RelativeLayoutManager、GridLayoutManager、StaggeredGridLayoutManager等。
    意思一目了然。只提一下StaggeredGridLayoutManager,这个是水平方向的Grid

这里比较重要的是Adapter
5. 关于SimpleRecyclerAdapter

/*
 *  Copyright (c) 2016.  Project Launcher
 *  Source SimpleRecyclerAdapter
 *  Author 沈煜
 *  此源码及相关文档等附件由 沈煜 编写,作者保留所有权利
 *  使用必须注明出处。
 *  The code and documents is write by the author. All rights are reserved.
 *  Use must indicate the source.
 *
 */
public class SimpleRecyclerAdapter extends RecyclerView.Adapter<SimpleRecyclerAdapter.ViewHolder>{
    private static final String TAG = SimpleRecyclerAdapter.class.getSimpleName();
    private LayoutInflater mInflater;
    private List<AppBean> mListOfApps;
    private int currentPosition = 0;
    private Context context;

    public SimpleRecyclerAdapter(Context context, List<AppBean> mListOfApps){
        mInflater = LayoutInflater.from(context);
        this.context = context;
        this.mListOfApps = mListOfApps;
    }

    @SuppressWarnings("unused")
    public void setData(List<AppBean> mListOfApps){
        this.mListOfApps = mListOfApps;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.item_grid_apps, parent, false);
        ViewHolder vh = new ViewHolder(view);
        vh.mImageView = (ImageView) view.findViewById(R.id.home_grid_item_icon);
        vh.mTextView = (TextView) view.findViewById(R.id.home_grid_item_name);
        return vh;
    }

    private View mOldFocus;
    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) {
        holder.mImageView.setImageDrawable(mListOfApps.get(position).getAppIcon());
        holder.mTextView.setText(mListOfApps.get(position).getAppName());

        // 设置itemView可以获得焦点
        holder.itemView.setFocusable(true);
        holder.itemView.setTag(position);
        holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    currentPosition = (int) holder.itemView.getTag();
                    mOnItemSelectListener.onItemSelect(holder.itemView, currentPosition);

                    if (v != mOldFocus) {
                        View vb = v.findViewById(R.id.home_back_2);
                        GradientDrawable gd = (GradientDrawable) vb.getBackground();
                        int width = (int) context.getResources().getDimension(R.dimen.px_positive_3);
                        int color = context.getResources().getColor(R.color.color0);
                        int radius = (int) context.getResources().getDimension(R.dimen.px_positive_25);
                        gd.setStroke(width, color);
                        gd.setCornerRadius(radius);

                        if (mOldFocus != null) {
                            View ovb = mOldFocus.findViewById(R.id.home_back_2);
                            GradientDrawable ogd = (GradientDrawable) ovb.getBackground();
                            ogd.setStroke(0, Color.parseColor("#00000000"));
                        }
                    }
                    mOldFocus = v;
                } else {
                    if (v != null) {
                        View ovb2 = v.findViewById(R.id.home_back_2);
                        GradientDrawable ogd2 = (GradientDrawable) ovb2.getBackground();
                        ogd2.setStroke(0, Color.parseColor("#00000000"));
                    }
                }
            }
        });

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mOnItemClickListener.onItemClick(v, currentPosition);
            }
        });

        holder.itemView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                mOnItemKeyListener.OnItemKey(v, keyCode, event, currentPosition);
                return false;
            }
        });
    }

    @Override
    public int getItemCount() {
        return mListOfApps.size();
    }

    private int index = 0;
    class ViewHolder extends RecyclerView.ViewHolder{
        ImageView mImageView;
        TextView mTextView;

        ViewHolder(View itemView) {
            super(itemView);
            ImageView back2 = (ImageView) itemView.findViewById(R.id.home_back_2);
            GradientDrawable background = (GradientDrawable) back2.getBackground();
            TypedArray ta = context.getResources().obtainTypedArray(R.array.appBackgroundColors);
            int count = ta.length();
            int [] colorsArray = new int[count];
            for (int i=0;i<count;i++) {
                int resId = ta.getResourceId(i, -1);
                colorsArray[i] = resId;
            }
            /*Random random = new Random();
            int index = random.nextInt(count);
            while (oldIndex == index) {
                index = random.nextInt();
            }
            oldIndex = index;*/
            background.setColor(context.getResources().getColor(colorsArray[index]));
            if (index < count - 1) {
                index += 1;
            } else {
                index = 0;
            }

            ta.recycle();
        }
    }

    private OnItemSelectListener mOnItemSelectListener;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private OnItemKeyListener mOnItemKeyListener;

    public interface OnItemSelectListener {
        void onItemSelect(View view, int position);
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }

    public interface OnItemLongClickListener {
        void onItemLongClick(View view, int position);
    }

    public interface OnItemKeyListener {
        void OnItemKey(View view, int keyCode, KeyEvent event, int position);
    }

    public void setOnItemSelectListener(OnItemSelectListener listener){
        mOnItemSelectListener = listener;
    }

    public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {
        this.mOnItemClickListener = mOnItemClickListener;
    }

    public void setOnItemLongClickListener(OnItemLongClickListener mOnItemLongClickListener) {
        this.mOnItemLongClickListener = mOnItemLongClickListener;
    }

    public void setOnItemKeyListener(OnItemKeyListener mOnItemKeyListener) {
        this.mOnItemKeyListener = mOnItemKeyListener;
    }

}

最后有一个重要的地方就是Adapter中写了许多监听器。这个是RecyclerView特有的,因为RecyclerView没有监听器!!!(简直了,不能忍好嘛。所以要在Adapter中自己定义监听。因为你监听的是其中的itemView,当然了也可以去RecyclerView里面写诸如OnItemClick这样的监听器,会麻烦一点。不过那样封装起来比较牛。去GitHub上应该有这样的jar可以用。)

差不多了吧

————————-不怎么华丽的分割线—————–

MDZZ,这个里面的焦点控制忘了写!!!厉害了我的哥,下期详解。

作者:shenyu_njau 发表于2016/11/4 16:11:00 原文链接
阅读:72 评论:0 查看评论

10 条提升 Android 性能的建议

$
0
0
摘要:每个人都知道一个 App 的成功,更这个 App 的性能体验有着很密切的关系。但是如何让你的 App 拥有极致性能体验呢?在 DroidCon NYC 2015 的这个分享里,Boris Farber 带来了他关于 Android Api 以及如何避免一些常见的坑的经验。了解如何缩短启动时间,优化滑动效果,创建更加顺滑的用户体验。
 

About the Speaker: Boris Farber

每个人都知道一个 App 的成功,更这个 App 的性能体验有着很密切的关系。但是如何让你的 App 拥有极致性能体验呢?在 DroidCon NYC 2015 的这个分享里,Boris Farber 带来了他关于 Android Api 以及如何避免一些常见的坑的经验。了解如何缩短启动时间,优化滑动效果,创建更加顺滑的用户体验。

@borisfarber

Save the date for Droidcon SF in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.

简介(0:00)

大家好,我是 Boris,现在是 Google 的一枚员工,目前专注于需要高性能的 App。这个分享是我长期以来从错误中,以及在给合作伙伴做咨询的时候攒下的最佳实践。如果你有一个小型的 App,读过之后,会在你的 App 成长阶段起到帮助。

我常常会见到那些启动时间很长,滑动不流畅,甚至出现没有反应的 App。我们通常要花很多时间去改善这些问题,毕竟我们都希望自己的 App 能够成功。

Activity 泄漏(1:17)

我们第一个需要修复的问题就是 Activity 泄漏,我们先来看看内存泄漏是怎么发生的。 Activity 泄漏通常是内存泄漏的一种。为什么会泄漏呢?如果你持有一个未使用的 Activity 的引用,其实也就持有了 Activity 的布局,自然也就包含了所有的 View。最棘手的是持有静态引用。别忘了,Activity 和 Fragment 都有自己的生命周期。一旦我们持有了静态引用,Activity 和 Fragment 就不会被垃圾回收器清理掉了。这就是为什么静态引用很危险。

m_staticActivity = staticFragment.getActivity()

我看过太多次这样的代码了。

另外,泄漏 Listener 也是经常会发生的事情。比如说,我有下面的代码。 LeakActivity 继承自 Activity ,我们有一个单例: NastyManager ,当我们通过 addListener(this) 将 Activity 作为 Listener 和 NastyManager 绑定起来的时候,不好的事情就发生了。

1 public class LeakActivity extends Activity {
2   @Override
3   protected void onCreate(Bundle savedInstanceState) {
4     super.onCreate(savedInstanceState);
5     NastyManager.getInstance().addListener(this);
6   }
7 }

想要修复这样的 Bug,其实相当简单,就是在你的 Acitivity 被销毁的时候,将他和 NastyManager 取消掉绑定就好了。
1 @Override
2 public void onDestroy() {
3   super.onDestroy();
4  
5   NastyManager.getInstance().removeListener(this);
6 }

相对上面的解决方案,我们自然还有更好的。比如我们真的需要用到单例吗?通常,并不需要。不过某些时候可能真的很需要。我们得权衡和设计。不过无论如何,记住, 当 Activity 销毁的时候,在单例中移除掉对 Activity 的引用 。下面我们讨论下: 如果是内部类,会发生什么 ?比如说,我们有一个在 Activity 里有一个很简短的非静态 Handler。

尽管它看起来很短,但是只要它还存活着,那么包含它的 Activity 就会存活着。如果你不信我,在 VM 里试试看。这就是另一个内存泄漏的案例: Activity 内部的 Handler

01 public class MainActivity extends Activity {
02   //...
03   Handler handler;
04   @Override
05   protected void onCreate(Bundle savedInstanceState) {
06     super.onCreate(savedInstanceState);
07     //...
08     handler = new Handler() {
09       @Override
10       public void handleMessage(Message msg) {
11               }
12   }
13 }

Handler 是个很常用也很有用的类,异步,线程安全等等。如果有下面这样的代码,会发生什么呢? handler.postDeslayed ,假设 delay 时间是几个小时… 这意味着什么?意味着只要 handler 的消息还没有被处理结束,它就一直存活着,包含它的 Activity 就跟着活着。我们来想办法修复它,修复的方案是 WeakReference ,也就是所谓的弱引用。垃圾回收器在回收的时候,是会忽视掉弱引用的,所以包含它的 Activity 会被正常清理掉。大概代码如下:
01 private static class MyHandler extends Handler {
02   private final WeakReference<MainActivity> mActivity;
03   // ...
04   public MyHandler(MainActivity activity) {
05     mActivity = new WeakReference<MainActivity>(activity);
06     //...
07   }
08  
09   @Override
10   public void handleMessage(Message msg) {
11   }
12   //...
13 }

概括来说:我们有个内部类,就像 Handler,内部非静态类是不能脱离所属类而单独存活的,Android 里通常是 Activity。所以,看看你的代码里的内部类,确保他们没有出现内存泄漏。

相比非静态内部类,最好使用静态内部类。区别就是静态内部类不依赖所属类,他们拥有不同的生命周期。我经常见到类似的原因引起的内存泄露。

如何避免 Activity 泄漏?(8:37)

  • 移除掉所有的静态引用。

  • 考虑用 EventBus 来解耦 Listener。

  • 记着在不需要的时候,解除 Listener 的绑定。

  • 尽量用静态内部类。

  • 做 Code Review。个人经验:Code Review 能很早的发现内存泄漏。

  • 了解你程序的结构。

  • 用类似 MAT,Eclipse Analyzer,LeakCanary 这样的工具分析内存。

  • 在 Callback 里打印 Log。

滑动(10:05)

实现流畅滑动的技巧:UI 线程只用作 UI 渲染。这一条真谛能够解决 99% 的滑动卡顿问题。不要在 UI 线程做下面的事情:

  • 载入图片
  • 网络请求
  • 解析 JSON
  • 读取数据库

做这些操作是很慢的,像图片,网络,JSON考虑用现成的库,有很多社区提供的解决方案,数据库考虑下用 Loader,支持批量更新和载入。

图片(11:26)

图片相关的库有很多,比如 Glide , Picasso , Fresco 。你可以自己去了解下他们之间的区别,以帮助自己在特定场景下做出取舍。

内存(12:13)

Bitmap 操作是很需要技巧的,图片一般比较大,而且系统对最大内存又有限制和要求。在我面对 4.0 之前的系统的时候,我简直要崩溃了。内存管理也很需要技巧。有的时候需要放到文件里,有的时候需要放到内存里,别忘了,我们还有一个很有用的工具:LRUCache。

网络(12:54)

首先,Java 的网络请求确实是 Android 的一个阻碍。很多 Java.net 的 API 都是阻断执行的,切记不可在 UI 线程执行网络请求。在线程里执行或者直接使用第三方库吧。

异步 HTTP 其实也挺麻烦的,4.4 起 OkHttp 就成了 Android 代码的一部分了,然而… 如果你需要最新版本的 OkHttp ,可以考虑自己引入。另外有个不错的库叫: Volley ,也可以试试 Square 的 Retrofit 。这些都能让你的网络请求变得更友好。

大 JSON(14:35)

在 UI 线程,也不做解析 Json 的事情,因为这是一个很耗时的事情。试着用 Google 的 GSON 来做反序列化的操作。

对于巨大的 JSON 解析,建议用更快的 Jackson 以及 ig-json-parser ,这两个工具在 JSON 的解析上做的非常漂亮。从公司的反馈结果来看 ig-json-parser 的效率是最高的。

Looper.myLooper() == Looper.getMainLooper() 是可以帮助你确定你是否在主线程的代码。

如何优化滑动速度?(16:56)

  • UI 线程只做 UI 更新。
  • 理解并发 API。
  • 开始使用优秀的第三方库。
  • 使用 Loader 加载数据库数据

之所以要用第三方库,是因为你自己去完善一个复杂功能是需要花时间的。如果你打算专注在自己的功能性的 App 上,那么用库吧。

并发 APIs(18:00)

如何让 App 快速响应请求是个很重要。开发者们,甚至包括我,经常忘记 Service 的方法是在 UI 线程执行的。请考虑使用 IntentServiceAsyncTaskExecutorsHandlerLoopers

我们来盘点下这些的区别:

IntentService(19:07)

我在之前的公司,我用 IntentService 来执行上传功能。IntentService 是一个单线程,一次一个任务的工作流。我们没有很复杂的任务系统。如果你有大型复杂的任务,而且这个任务不需要跟 UI 打交道,那么考虑用 IntentService 吧。

AsyncTask(19:56)

如果你的任务需要更新 UI,那么考虑用 AsyncTask 吧,AsyncTask 虽然相对容易,但是有些坑得留意。当你旋转手机的时候,Activity 会被关闭,然后重启。不然可能造成内存泄露。

Executor Framework(21:11)

这是 Java 6 自带的并发方案。默认是存在一个由系统管理的线程池,你可以通过 callback,future 来控制和管理。这根 MapRedues 发难有点像,面对复杂的任务,你希望能够把他们拆分交给多个线程来处理。Executor 的框架就很能胜任这种场景。

如何适应并发APIs?(22:07)

  • 学会和理解 API,懂得权衡
  • 确保找到了问题的正确解决方案

  • 了解问题真实所在

  • 重构代码

Deprecation(22:42)

我们肯定都知道,最好能够避免使用废弃的 API。比如以下的例子:

  • 不要通过反射来调用私有 API。
  • 不要再 NDK 和 C 语言层调用私有 Native 方法。
  • 不要轻易调用 Runtime.exec 指令完成进程通讯功能。
  • adb shell am 做进程通讯并不好。

废弃的意思是这些 API 将会被移除,通常在正式版发布 1,2天左右,你的 App 就不会工作了。更糟糕的情况是,如果你的 App 依赖了一些库,而这些库哟改了废弃的 Api 或者工具。那可就惨了,如果一旦作者没有更新…你懂得。

不要用废弃 Api 的另一个原因是性能问题和安全问题。

如何避免废弃 Api:

  • 使用正确的 API。
  • 重构依赖。
  • 不要滥用系统。
  • 更新依赖和工具。
  • 越新的通常越好。

用 Toolbar 而非 ActionBar,在需要动画的时候用 RecyclerView,因为它专门为动画做过优化。同时 Android M 里移除了 Apache Http Connection。请使用 HttpURLConnection,它拥有更简单的 API,更小的体积,默认的压缩功能,更好的 Response 缓存,等等其他很赞的功能。

架构(27:03)

架构中的 Bug 总是最为烦人。想要避免这种问题,学习下 App 组件的生命周期。比如什么是 Activity 的 Flag?什么是 Fragment?什么事 stated fragment?什么是 task?读读文档,尝试下用回调的 log 搞清楚这些概念。

时常有人问我:“Picasso 和 Glide 哪个更好?我改用 Volley 还是 OkHttp?”,这种问题根本没有 100% 正确的答案。不过,当我在选择一个库的时候,我会用下面的 Checklist 来决策:

  • 确保它能够解决你的问题。
  • 确保它和当前所有的依赖能正常工作。
  • 检查依赖
    • 留意一下依赖的版本冲突
  • 了解维护情况和成本

总的来说,提及架构和设计,最好的方法就是让你的程序最快响应。确保用户能够快速理解你的 App,并且拥有良好体验。

See the discussion on Hacker News .

作者:qq_35114086 发表于2016/11/4 16:17:15 原文链接
阅读:88 评论:0 查看评论

Android开发之五大存储方式之一数据库存储

$
0
0

这里写图片描述

废话不多说,直接看代码

activity_main:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_search"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:hint="请输入搜索的姓名"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="btn_search"
            android:text="搜索" />
    </LinearLayout>
    <EditText
        android:id="@+id/et_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入姓名" />

    <EditText
        android:id="@+id/et_sex"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入性别" />

    <EditText
        android:id="@+id/et_age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入年龄" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btn_insert"
            android:text="添加" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btn_delete"
            android:text="删除" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btn_select"
            android:text="查询" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btn_update"
            android:text="修改" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="#000000" >

        <TextView
            android:id="@+id/tv_id"
            android:layout_width="0dp"
            android:background="#ffffff"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="学号"
            android:layout_margin="3dp"
            android:textColor="#ff0000" />

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:background="#ffffff"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="姓名"
            android:layout_margin="3dp"
            android:textColor="#ff0000" />

        <TextView
            android:layout_margin="3dp"
            android:background="#ffffff"
            android:id="@+id/tv_sex"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="性别"
            android:textColor="#ff0000" />

        <TextView
            android:background="#ffffff"
            android:layout_margin="3dp"
            android:id="@+id/tv_age"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="年龄"
            android:textColor="#ff0000" />
    </LinearLayout>

    <ListView
        android:id="@+id/lv_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

item_layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="#000000" >

    <TextView
        android:id="@+id/tv_id"
        android:layout_width="0dp"
        android:background="#ffffff"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:layout_margin="3dp"
        android:textColor="#ff0000" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="0dp"
        android:background="#ffffff"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:layout_margin="3dp"
        android:textColor="#ff0000" />

    <TextView
        android:layout_margin="3dp"
        android:background="#ffffff"
        android:id="@+id/tv_sex"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:textColor="#ff0000" />

    <TextView
        android:background="#ffffff"
        android:layout_margin="3dp"
        android:id="@+id/tv_age"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:textColor="#ff0000" />


</LinearLayout>

Student:

package com.example.jhl.jhllearn;

/**
 * Created by Administrator on 2016/11/4.
 */
public class Student {

    private int id;
    private String name;
    private String sex;
    private int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + ", sex=" + sex
                + ", age=" + age + "]";
    }

    public Student(int id, String name, String sex, int age) {
        super();
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.age = age;
    }

    public Student() {
        super();
        // TODO Auto-generated constructor stub
    }
}

MyAdapter:

package com.example.jhl.jhllearn;

/**
 * Created by Administrator on 2016/11/4.
 */
import java.util.List;

import android.content.Context;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter {

    private Context context;
    private List<Student> oList;
    private LayoutInflater inflater;
    public MyAdapter(Context context,List<Student> oList)
    {
        this.context=context;
        this.oList=oList;
        this.inflater=LayoutInflater.from(context);
    }
    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return oList.size();
    }

    @Override
    public Object getItem(int position) {
        // TODO Auto-generated method stub
        return oList.get(position);
    }

    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return position;
    }

    @Override
    public View getView(int position, View v, ViewGroup parent) {
        // TODO Auto-generated method stub
        ViewHolder holder;
        if (v==null) {
            holder=new ViewHolder();
            v=inflater.inflate(R.layout.item_layout, null);
            holder.tv_id=(TextView)v.findViewById(R.id.tv_id);
            holder.tv_name=(TextView)v.findViewById(R.id.tv_name);
            holder.tv_sex=(TextView)v.findViewById(R.id.tv_sex);
            holder.tv_age=(TextView)v.findViewById(R.id.tv_age);
            v.setTag(holder);
        }else {
            holder=(ViewHolder)v.getTag();
        }
        holder.tv_id.setText( oList.get(position).getId()+"");
        holder.tv_name.setText( oList.get(position).getName());
        holder.tv_sex.setText( oList.get(position).getSex());
        holder.tv_age.setText( oList.get(position).getAge()+"");
        return v;
    }

    public class ViewHolder
    {
        TextView tv_id,tv_name,tv_sex,tv_age;

    }

}

MySqliteOpenHelper:

package com.example.jhl.jhllearn;

/**
 * Created by Administrator on 2016/11/4.
 */
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class MySqliteOpenHelper extends SQLiteOpenHelper {

    /**
     *
     * @param context
     *            上下文对象
     * @param name
     *            数据库名称
     * @param factory
     *            游标工厂,默认为空
     * @param version
     */

    // resulSet
    public MySqliteOpenHelper(Context context, String name,
                              CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 当前第一次创建数据库的时候回调
        db.execSQL("create table Student(id Integer primary key autoincrement, name varchar(10),sex varchar(4),age Integer)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 当前数据库版本更新后,回调该方法

    }

}

MainActivity:

package com.example.jhl.jhllearn;

import java.util.ArrayList;
import java.util.List;
import android.os.Bundle;
import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

public class MainActivity extends Activity {
    private EditText et_name, et_sex, et_age, et_search;
    private ListView lv_list;
    private List<Student> oList = new ArrayList<Student>();
    private MyAdapter adapter;
    // 称为数据库辅助类,用于创建和更新
    private MySqliteOpenHelper helper;
    // 操作数据库增删改查
    private SQLiteDatabase db;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        et_name = (EditText) findViewById(R.id.et_name);
        et_sex = (EditText) findViewById(R.id.et_sex);
        et_age = (EditText) findViewById(R.id.et_age);
        et_search = (EditText) findViewById(R.id.et_search);
        lv_list = (ListView) findViewById(R.id.lv_list);
        helper = new MySqliteOpenHelper(this, "student_db", null, 1);
        /**
         * getReadableDatabase与getWritableDatabase sqlite数据库有一定的大小
         * 当sqlite存取数据到达上线的时候,不能再写入数据 getReadableDatabase方法不允许读取,不允许写
         * getWritableDatabase方法不允许写,可允许读
         */
        db = helper.getWritableDatabase();
    }

    // 向SQLite中插入数据
    public void btn_insert(View v) {
        // insert_sql();
        insert();
    }
    /**
     * 以insert()方法传参方式添加数据
     */
    public void insert() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();
        // table 表名
        // nullColumnHack 当我们需要插入数据时,没有明确指定字段名,默认插入数据为空
        // values :ContentValues 插入的字段值
        ContentValues values = new ContentValues();
        values.put("name", name);
        values.put("sex", sex);
        values.put("age", Integer.parseInt(age));
        // {name="",sex="",age=""}
        // 返回的是long类型,返回的是当前插入的数据的ID号
        long index = db.insert("Student", null, values);
        if (index >= 0) {
            Show_Toase("插入成功" + index);
        }
    }

    /**
     * 以sql语句的形式添加数据
     */
    public void insert_sql() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();

        db.execSQL("insert into Student(name,sex,age) values(?,?,?)",
                new Object[]{name, sex, Integer.parseInt(age)});
        Show_Toase("添加成功");
    }

    public void btn_search(View v) {
        if (oList.size() > 0) {
            oList.clear();
        }
        String seach_str = et_search.getText().toString();

        Cursor cursor = db.rawQuery("select * from Student where name like ?",
                new String[]{"%" + seach_str + "%"});
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String sex = cursor.getString(cursor.getColumnIndex("sex"));
            int age = cursor.getInt(cursor.getColumnIndex("age"));
            Student student = new Student(id, name, sex, age);
            oList.add(student);
        }
        dateSetChang();
    }

    private void dateSetChang() {
        if (adapter == null) {
            adapter = new MyAdapter(this, oList);
            lv_list.setAdapter(adapter);
        } else {
            adapter.notifyDataSetChanged();
        }
    }

    public void Show_Toase(String text) {
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
    }

    // 删除数据
    public void btn_delete(View v) {
        delete();
        // delete_sql();
    }

    /**
     * 以delete()方法实现删除数据
     */
    public void delete() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();
        // table 表名
        // whereClause 带有占位符的条件
        // whereArgs 条件所对应的值数据
        // 当前整型返回的是删除记录的行数
        int i = db.delete("Student", "name=? and sex=? and age=?",
                new String[]{name, sex, age});
        if (i > 0) {
            Show_Toase("删除成功" + i);
        }
    }

    /**
     * 以sql语句的形式删除数据
     */
    public void delete_sql() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();
        db.execSQL("delete from Student where name=? and sex=? and age=?",
                new Object[]{name, sex, Integer.parseInt(age)});
        Show_Toase("删除成功");

        boolean is = false;
        for (int i = 0; i < oList.size() && !is; i++) {
            if (oList.get(i).getName().equals(name)
                    && oList.get(i).getSex().equals(sex)
                    && oList.get(i).getAge() == Integer.parseInt(age)) {
                oList.remove(i);
                is = true;
            }
        }
        dateSetChang();
    }

    // 查询数据
    public void btn_select(View v) {
        query();
        // query_sql();
    }

    /*
     * 使用query()方法查询数据
     */
    public void query() {
        if (oList.size()>0){
            oList.clear();
        }
        // table 表名
        // columns 查询的列名
        // selection 带有占位符的条件
        // selectionArgs 条件所对应的值
        // groupBy 分组
        // having 分组的条件
        // orderBy 排序(分为升序ASC,降序DESC)
        // select * from Student order by id asc
        Cursor cursor = db.query("Student", null, null, null, null, null,
                "id asc");
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String sex = cursor.getString(cursor.getColumnIndex("sex"));
            int age = cursor.getInt(cursor.getColumnIndex("age"));
            Student student = new Student(id, name, sex, age);
            oList.add(student);
        }
        dateSetChang();
    }

    /*
     * 使用SQL语句的形式查询数据
     */
    public void query_sql() {
        if (oList.size() > 0) {
            oList.clear();
        }
        Cursor cursor = db.rawQuery("select * from Student", new String[]{});
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String sex = cursor.getString(cursor.getColumnIndex("sex"));
            int age = cursor.getInt(cursor.getColumnIndex("age"));

            Student student = new Student(id, name, sex, age);

            oList.add(student);

        }
        dateSetChang();
    }

    // 更新数据
    public void btn_update(View v) {
        // update_sql();
        update();
    }

    /**
     * 以update()方法实现修改数据
     */
    public void update() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();
        // table 表名
        // valuse ContentValues 修改后的数据
        // whereClause 带占位符的条件
        // whereArgs 条件的值
        // update Student set name=?,age=? where sex=?
        ContentValues values = new ContentValues();
        values.put("name", name);
        values.put("age", Integer.parseInt(age));
        // 返回值表示执行修改语句后,别修改了的行数
        int i = db.update("Student", values, "sex=?", new String[]{sex});
        if (i > 0) {
            Show_Toase("修改成功" + i);
        }
    }

    /*
     * 以sql语句的形式修改数据
     */
    public void update_sql() {
        String name = et_name.getText().toString();
        String sex = et_sex.getText().toString();
        String age = et_age.getText().toString();
        db.execSQL("update Student set name=?,age=? where sex=?", new Object[]{
                name, Integer.parseInt(age), sex});
        Show_Toase("修改成功");
        for (int i = 0; i < oList.size(); i++) {
            if (oList.get(i).getSex().equals(sex)) {
                oList.get(i).setName(name);
                oList.get(i).setAge(Integer.parseInt(age));
            }
        }
        dateSetChang();
    }
}

到这里,数据库存储就OVER了,其实上面是有BUG的,比如插入的时候我的条件是根据name,age,sex,如果你有一种未填写的话,是会崩掉的,因为我没判断。这篇文章我只是具体介绍怎么学习操作数据库,其他的没去做太多处理,读者自行完善

最后如果想看创建的数据库的效果话首先必须是模拟器,真机是没用的,然后下载一个操作数据库的SQLite,下载地址:http://download.csdn.net/detail/qq_33750826/9673234
然后安装打开

然后打开AndroidStudio或者Eclipse的File Explorer:
在里面找到data->data->找到自己的项目包名
这里写图片描述

这里写图片描述

作者:qq_33750826 发表于2016/11/4 16:51:28 原文链接
阅读:38 评论:0 查看评论

如何在没有官方API的情况下写一个第三方客户端

$
0
0

作为一个学生,完全一个人写程序是一件非常苦逼的事情,没有设计天赋,要写界面,用ps,做出来还巨丑,但是好在google 推出了md 设计规范,勉强可以写出看的过去的程序,但是android毕竟还是属于前端,没有后台服务器支持的话,有很多东西实现不了,自己学android也不久,没那么多精力在入后台的坑,于是打算为一个网站写个第三方客户端练练手(好吧,其实豆瓣之类的网站是开放了api的,单纯只是因为我以前经常用这个网站刷算法题而已,当时觉得有点麻烦,正好在学android,想着能不能做出一个app出来,巩固下自己的android学习,

学android 的时间不算多,所以不敢说是教程,怕坑了新人,这里就是自己开发过程中的一个分享而已。

这里结合自己写好的 app 来说说一个第三方客户端项目的构建过程。

这里写图片描述

先放下源码地址吧:https://github.com/Thereisnospon/AcClient

构思

因为没有api,所以只能通过爬虫的方式,通过获得网页的html,再从中解析出数据。开始我是打算使用 正则表达式帮我进行解析的,然而这个方法非常蛋疼,因为一个html文件非常复杂,各种标签乱飞,解析几种页面之后,就了解到了一个神器 Jsoup 可以很方便的从 html 中 获取想要的信息。比如这里就是项目中一个代码片段,用来获取一个标签的内容:


public class CodeBuilder {
    public static String parse(String html){
        Document document= Jsoup.parse(html);
        Element element=getTexrs(document);
        if(element!=null)
            return element.text();
        else return "";

    }
    private static Element getTexrs(Document document){
        Elements elements=document.getElementsByTag("textarea");
        if(elements==null||elements.size()==0)
            return null;
        else return elements.first();
    }
}

然后就是如何获取网页的内容,进行模拟登陆,模拟注册了。这个通过抓取 http 包的信息,可以查看 post 提交信息

4344FDD1-BB2B-4AA8-A8E6-809810B1E112.png

之后通过 给出 cookie 值,就可以实现模拟登陆了。

网络请求的话,使用 OkHttp 比较方便,但是还是封装了一层,进行更好的网络请求操作,管理cookie 等信息。

9E6C5408-A2EB-46F5-9F9B-026F2F4CAB42.png

实现了 网络请求和 数据解析之后,差不多就可以做一个项目出来了,但是网络请求要处理线程的问题,AsynaCTask 不靠谱,自己写线程各种 handle ,message,或者用 EventBus 感觉还是很乱,于是使用了 RxJava 这个神器,结合MVP模式可以写出很优雅的代码。就像这样。

public class RankPresenter implements RankContact.Presenter {


    RankContact.View view;

    RankContact.Model model;

    public RankPresenter(RankContact.View view) {
        this.view = view;
        model=new RankModel();
    }

    @Override
    public void loadRankItems() {
        Observable.just(1)
                .observeOn(Schedulers.io())
                .map(new Func1<Integer, List<RankItem>>() {
                    @Override
                    public List<RankItem> call(Integer integer) {
                        return model.loadRankItems();
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<RankItem>>() {
                    @Override
                    public void call(List<RankItem> list) {
                        if (list !=null &&list.size()!=0)
                            view.onRefreshRankSuccess(list);
                        else view.onRankFailure("load null");
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        view.onRankFailure(throwable.getMessage());
                    }
                });
    }

    @Override
    public void moreRankItems() {
        Observable.just(1)
                .observeOn(Schedulers.io())
                .map(new Func1<Integer, List<RankItem>>() {
                    @Override
                    public List<RankItem> call(Integer integer) {
                        return model.moreRankItems();
                }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<RankItem>>() {
                    @Override
                    public void call(List<RankItem> list) {
                        if (list !=null &&list.size()!=0)
                            view.onMoreRanks(list);
                        else view.onRankFailure("load null");
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        view.onRankFailure(throwable.getMessage());
                    }
                });
    }
}

MVP 模式 也就是分 Model,View,Contact 层,把 Activity 非常复杂的功能分解出来,这样既可以降低耦合,又可以让代码逻辑更加清晰。

例如,要实现一个获取排名信息的界面,先定义一个接口

public interface RankContact {


    interface View{
        void onRefreshRankSuccess(List<RankItem> list);
        void onMoreRanks(List<RankItem>list);
        void onRankFailure(String msg);
    }

    interface Model{
        List<RankItem>loadRankItems();
        List<RankItem>moreRankItems();
    }

    interface Presenter{
        void loadRankItems();
        void moreRankItems();
    }
}

然后分别实现它们:

Model

public class RankModel implements RankContact.Model {



    private int currentPage=1;

    public RankModel() {
        currentPage=1;
    }

    @Override
    public List<RankItem> loadRankItems() {
        currentPage=1;
        return getRanks(currentPage);
    }

    @Override
    public List<RankItem> moreRankItems() {
        return getRanks(currentPage);
    }


    private List<RankItem>getRanks(int page){
        String html=getHtml(page);
        if(html==null)
            return null;
        List<RankItem>rankItems=RankItem.Builder.parse(html);
        if(rankItems!=null&&rankItems.size()!=0){
            currentPage=page+1;
        }
        return rankItems;
    }

    private String getHtml(int page){
        int from=(page-1)*25+1;
        IRequest request=HttpUtil.getInstance()
                .get(HdojApi.RANK)
                .addParameter("from",""+from);
        try{
            Response response=request.execute();
            String html=new String(response.body().bytes(),"gb2312");
            return html;
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;

    }
}

Presenter

public class RankPresenter implements RankContact.Presenter {


    RankContact.View view;

    RankContact.Model model;

    public RankPresenter(RankContact.View view) {
        this.view = view;
        model=new RankModel();
    }

    @Override
    public void loadRankItems() {
        Observable.just(1)
                .observeOn(Schedulers.io())
                .map(new Func1<Integer, List<RankItem>>() {
                    @Override
                    public List<RankItem> call(Integer integer) {
                        return model.loadRankItems();
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<RankItem>>() {
                    @Override
                    public void call(List<RankItem> list) {
                        if (list !=null &&list.size()!=0)
                            view.onRefreshRankSuccess(list);
                        else view.onRankFailure("load null");
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        view.onRankFailure(throwable.getMessage());
                    }
                });
    }

    @Override
    public void moreRankItems() {
        Observable.just(1)
                .observeOn(Schedulers.io())
                .map(new Func1<Integer, List<RankItem>>() {
                    @Override
                    public List<RankItem> call(Integer integer) {
                        return model.moreRankItems();
                }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<RankItem>>() {
                    @Override
                    public void call(List<RankItem> list) {
                        if (list !=null &&list.size()!=0)
                            view.onMoreRanks(list);
                        else view.onRankFailure("load null");
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        view.onRankFailure(throwable.getMessage());
                    }
                });
    }
}

View

public class RankFragment extends NormalSwipeFragment  implements RankContact.View{


    RankContact.Presenter presenter;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view=normalView(inflater,container,savedInstanceState);
        presenter=new RankPresenter(this);
        Logger.d("create viwe");
        return view;
    }

    @Override
    public BaseSwipeAdapter createItemAdapter(List list) {
        return new RankItemAdapter(list);
    }


    @Override
    public void loadMore() {
        Logger.d("laod more");
        presenter.moreRankItems();
    }

    @Override
    public void refresh() {
        Logger.d("refresh");
        presenter.loadRankItems();
    }


    @Override
    public void onRefreshRankSuccess(List<RankItem> list) {
        Logger.d("refresh success");
        onRefreshData(list);
    }

    @Override
    public void onMoreRanks(List<RankItem> list) {
        Logger.d("more ranks");
        onMoreData(list);
    }

    @Override
    public void onRankFailure(String msg) {
        Logger.e(msg);
    }
}

这样的话,作为view的 Fragment 只需要做界面显示的功能就可以了,而不用管数据怎么获取,而 Presenter 就做好 Model 和 View 的桥梁,把Model 查找的数据给View 显示就可以了。

这样 一个 Activity 就会非常简单


public class RankActivity extends SearchActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rank);
        Logger.d("create");
        initDrawer();
        changeFragment(new RankFragment());
    }


    @Override
    public boolean onQueryTextSubmit(String query) {
        Fragment fragment= SearchPeopleFragment.newInstance(query);
        changeFragment(fragment);
        return true;
    }
}

更详细的mvp教程 :

http://rocko.xyz/2015/02/06/Android%E4%B8%AD%E7%9A%84MVP/

于是整个项目的结构就成了这样:

B52C0E65-93AB-488F-BA5A-47B6B7266CAD.png

  • api 放请求的url。。好吧并不能算api,因为内容都是自己抓的orz
  • base 定义了一些基本的Activity,Fragment,
  • data 定义了数据的Bean 以及创建它们的Builder
  • event 放了关于Intent ,EventBus 相关的消息标识
  • modules 重要的部分,放置各个功能模块
  • ui 这里放了些 adapter
  • utils 放置工具类,例如网络请求
  • widget 放置自定义的组件等

大致就想到了这么些。mark,慢慢填坑.ORZ

最后还是给下地址吧:

项目的代码地址为:https://github.com/Thereisnospon/AcClient

应用的下载地址:http://www.coolapk.com/apk/thereisnospon.acclient

作者:yzr1183739890 发表于2016/11/4 16:51:40 原文链接
阅读:145 评论:0 查看评论

Android 仿QQ侧滑删除—一个满足ListView、RecyclerView以及其他View通用的侧滑删除

$
0
0

对于侧滑删除已经是见惯不惯的了,我也一直有写类似QQ那样的侧滑删除控件的想法,虽然研究一段时间的自定义View,然对自定义ViewGroup实战还是较少,并且侧滑删除还要考虑大量的事件分发机制,比如如何处理子控件与父控件之间的滑动冲突以及一系列的down->move..move.. ->up操作等等。童哥刚好写了这么一个侧滑删除,使用起来不但简单方便,更重要的是更加优雅的实现了解耦,也就是说我们不管是使用ListView还是RecyclerView还是其他ViewGroup中的子View都可以使用此方式实现侧滑,具体使用方法是:只需要在XML布局文件中用这个自定义侧滑删除类去包裹我们要执行侧滑删除的item即可(比如我们要对ListView实现侧滑删除功能,我们只需要在定义ListView的item布局文件时,用我们的自定义类作为item的根布局即可)

如果喜欢原文请移驾童哥的博客【Android】毫无耦合性,一个Item根布局搞定 item侧滑删除菜单,像IOS那样简单的使用侧滑删除

既然已经有轮子了,为啥还要再重复一遍呢?原因很简单,因为上面已经说过了,首先对ViewGroup实战偏少,加之对事件分发机制想了解的深入些,刚好童哥的文章中实现的侧滑删除demo中包含了我的知识薄弱点,所以便有了此篇博客,自己跟着敲一遍确实比只看收获的多。

扯了这么多,由于童哥的博客中介绍的很详细了,那么我就把自己在调试中理解的一些知识以及对事件分发机制大致说一下,因为童哥并没有主要介绍事件分发这块

这里写图片描述

主要解决的问题如下:

1 侧滑拉出菜单。

2 点击除了这个item的其他位置,菜单关闭。并加上了属性动画,菜单关闭有回弹效果。

3 侧滑过程中,不许父控件上下滑动(事件拦截)。

4 多指同时滑动,屏蔽后触摸的几根手指。

5 不会同时展开两个侧滑菜单。

6 侧滑菜单时 拦截了长按事件。

7 侧滑时,拦截了点击事件

8 通过开关 isLeftSwipe支持左滑右滑

9 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件(和QQ交互一样)

主要是自定义ViewGroup的知识点,通过重写onMeasure()方法来告诉父控件需要多大尺寸,在onLayout()方法中确定子View(即:childView)的位置。我们需要来遍历我们的childView

int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
//具体的操作逻辑
}
}

下面我们来看下onLayout()方法是如何确定每一个子view的位置的
先把我们的item布局贴出来,方便理解,注意:引用我们自定义ViewGroup包裹布局时要设置android:clickable=”true”这一属性

<?xml version="1.0" encoding="utf-8"?>
<com.example.swipedelete.view.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true"
    >

    <!-- 第一个子view,显示ListView数据内容-->
    <LinearLayout
        android:id="@+id/ll_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/listview_iv"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@mipmap/ic_launcher"/>
        <TextView
            android:id="@+id/listview_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是listview的item内容"
            android:layout_gravity="center_vertical"
            android:paddingLeft="5dp"
            />
    </LinearLayout>

    <!-- 下面是侧滑菜单项 即:第2+个子view-->
    <Button
        android:id="@+id/btn_zd"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:background="#d9dee4"
        android:text="置顶"
        android:textColor="@android:color/white"/>
    <Button
        android:id="@+id/btn_delete"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:background="#F76E6B"
        android:text="删除"
        android:textColor="@android:color/white"/>

</com.example.swipedelete.view.SwipeMenuLayout>
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //LogUtils.e(TAG, "onLayout() called with: " + "changed = [" + changed + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]");
        int childCount = getChildCount();
        int left = 0 + getPaddingLeft();
        int right = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() != GONE) {
                if (i == 0) {//第一个子View是内容 宽度设置为全屏
                    childView.layout(left, getPaddingTop(), left + mMaxWidth, getPaddingTop() + childView.getMeasuredHeight());
                    left = left + mMaxWidth;
                } else {
                    if (isLeftSwipe) {
                        childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
                        left = left + childView.getMeasuredWidth();
                    } else {
                        childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight());
                        right = right - childView.getMeasuredWidth();
                    }

                }
            }
        }
    }

这里需要注意的是,我们侧滑删除根布局下有三部分(一个用来显示ListView数据内容的,一个是置顶,一个是删除)。所以childCount的值为3,因此,需要判断,当 i = 0时,拿到的是第一个子view,即用来显示数据内容的,所以设置它的宽为全屏,所以left = getPaddingLeft(),如果没有设置左内边距则left = 0;当 i 的值不为0时,分为左侧滑菜单和右侧滑菜单,这里只说左侧滑,i 不为0 接着确定第二个子view的位置,由于这三部分是横向排列的,虽然此时侧滑还处于隐藏状态,但是第二个子view的left肯定是自身的宽 + 第一个子view的宽,从而来确定第二个子view的左边距位置。第三个childView则依次累加。

侧滑时,拦截了点击事件

增加一个变量存储scaleTouchSlop,这个值是系统定义的,超过这个值即判断此次动作是在滑动。我们利用这个值判断是否处于侧滑。

我们知道对于ViewGroup中事件分发包括三部分:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent(),也就是常说的:事件分发、事件拦截、事件处理。
下面我们假设一个场景:
这里写图片描述
当我们点击中间的view内中的某一点时:

1、假设都是返回默认值的情况下,事件分发的顺序是从上往下:Activity的dispatchTouchEvent()—>ViewGroup的dispatchTouchEvent()—>View的dispatchTouchEvent()
2、假设都是返回默认值的情况下,事件处理的顺序是从下往上:View的onTouchEvent()—>ViewGroup的onTouchEvent()—>Activity的onTouchEvent()
3、dispatchTouchEvent()以及onTouchEvent()有一个共同点就是:当返回值为true时,则消费此次事件,不再传递给任何view
4、Activity的dispatchTouchEvent()不管返回true还是false都是消费掉事件,返回super.xxx的时候才会分发给下一级ViewGroup的dispatchTouchEvent(),ViewGroup的dispatchTouchEvent()返回false时,则事件回溯到父类(Activity)的onTouchEvent()处理。
5、ViewGroup的dispatchTouchEvent()返回super.xxx时,事件传递给自己的onInterceptTouchEvent()处理,如果onInterceptTouchEvent()返回true,表示拦截,然后交给自己的onTouchEvent()处理(onTouchEvent()返回true则消费掉事件,谁也接收不到此事件,返回false或者super.xxx时,则交给父类的onTouchEvent()处理。),返回false或者super.xxx时会将事件交给下一级View的dispatchTouchEvent()处理。
6、View的dispatchTouchEvent()接收到事件之后,返回值为false,则回溯给父类(ViewGroup)的onTouchEvent()处理,返回值为super.xxx时,则交给自己的onTouchEvent()处理。onTouchEvent()返回true则消费掉事件,谁也接收不到此事件,返回false或者super.xxx时,则交给父类的onTouchEvent()处理。

上面简单的介绍了down的情况下Activity、ViewGroup、View的事件分发机制。有篇文章通过图文并茂的介绍了事件分发机制,推荐给大家图解 Android 事件分发机制

由效果图可以发现,我们的侧滑菜单在展开和关闭的时候会有回弹的效果,很炫酷,实现方式是通过属性动画实现的,设置了动画的变化率setInterpolator,展开时设置了:mExpandAnim.setInterpolator(new OvershootInterpolator());关于这几个属性动画变化效果简单说下:

AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
LinearInterpolator 以常量速率改变
OvershootInterpolator 向前甩一定值后再回到原来位置

如果你不想设置带回弹效果,你可以不用设置setInterpolator或者你直接设置mExpandAnim.setInterpolator(new LinearInterpolator()); LinearInterpolator():表示匀速

最后非常感谢你能耐住性子听我啰嗦半天,有些不懂得,自己可以先下载代码断点调试编译下,就会明白很多。如果想直接使用,那么请下载源代码,源代码随后奉上,使用起来也相当简单,在你需要进行侧滑删除的item中用我们的自定义ViewGroup包裹即可。最后,非常感谢原作者的无私奉献

作者:xiaxiazaizai01 发表于2016/11/4 17:10:35 原文链接
阅读:65 评论:0 查看评论

不要滥用SharedPreference

$
0
0

SharedPreference是Android上一种非常易用的轻量级存储方式,由于其API及其友好,得到了很多很多开发者的青睐。但是,SharedPreference并不是万能的,如果把它用在不合适的使用场景,那么将会带来灾难性的后果;本文将讲述一些SharedPreference的使用误区。

存储超大的value

第一次看到下面这个sp的时候,我的内心是崩溃的:
这里写图片描述

一个默认的sp有90K,当我打开它的时候,我都快哭了:除了零零星星的几个很小的key之外,存储了一个炒鸡大的key,这一个key至少占了其中的89K。知道这是什么概念吗?

在小米1S这种手机上,就算获取这个sp里面一个很小的key,会花费120+ms!!那个毫不相干的key拖慢了其他所有key的读取速度!当然,在性能稍好的手机上,这个问题不是特别严重。但是要知道,120ms这个是完全不能忍的!

之所以说SharedPreference(下文简称sp)是一种轻量级的存储方式,是它的设计所决定的:sp在创建的时候会把整个文件全部加载进内存,如果你的sp文件比较大,那么会带来两个严重问题:

第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
这些key和value会永远存在于内存之中,占用大量内存。
也许有童鞋会说,sp的加载不是在子线程么,怎么会卡住主线程?子线程IO就一定不会阻塞主线程吗?

下面是默认的sp实现SharedPreferenceImpl这个类的getString函数:

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
                    awaitLoadedLocked();
                    String v = (String)mMap.get(key);
                    return v != null ? v : defValue;
                }
            }

继续看看这个

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

一把锁就是挂在那里!!这意味着,如果你直接调用getString,主线程会等待加载sp的那么线程加载完毕!这不就把主线程卡住了么?

另外,有一个叫诀窍可以节省一下等待的时间:既然getString之类的操作会等待sp加载完成,而加载是在另外一个线程执行的,我们可以让sp先去加载,做一堆事情,然后再getString!如下:

    // 先让sp去另外一个线程加载
    SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
    // 做一堆别的事情
    setContentView(testSpJson);
    // ...

// OK,这时候估计已经加载完了吧,就算没完,我们在原本应该等待的时间也做了一些事!
String testValue = sp.getString("testKey", null);

更为严重的是,被加载进来的这些大对象,会永远存在于内存之中,不会被释放。我们看看ContextImpl这个类,在getSharedPreference的时候会把所有的sp放到一个静态变量里面缓存起来:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

注意这个static的sSharedPrefsCache,它保存了你所有使用的sp,然后sp里面有一个成员mMap保存了所有的键值对;这样,你程序中使用到的那些个sp永远就呆在内存中,是不是不寒而栗?!

所以,请不要在sp里面存储炒鸡大的key碰到这样的猪队友,请让他自行检讨!!赶紧把自家App检查一下!!

存储JSON等特殊符号很多的value

还有一些童鞋,他在sp里面存json或者HTML;这么做不是不可以,但是,如果这个json相对较大,那么也会引起sp读取速度的急剧下降。

JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。而JSON本来就是可以用来做配置文件的,你干嘛又把它放在sp里面呢?多此一举。下面我写个demo验证一下。

下面这个sp是某个app的换肤配置:
这里写图片描述
我们先用sp进行读取,然后用直接把它丢json文件,直接读取并且解析;json使用的代码如下:

public int getValueByJson(Context context, String key) {
    File jsonFile = new File(context.getFilesDir().getParent() + File.separator + SP_DIR_NAME, "skin_beta2.json");
    FileInputStream fis = null;
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    try {
        fis = new FileInputStream(jsonFile);
        FileChannel channel = fis.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1 << 13); // 8K
        int i1;
        while ((i1 = channel.read(buffer)) != -1) {
            buffer.flip();
            bao.write(buffer.array(), 0, i1);
            buffer.clear();
        }

        String content = bao.toString();
        JSONObject jsonObject = new JSONObject(content);
        return jsonObject.getInt(key);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        throw new RuntimeException("not a json file");
    } finally {
        close(fis);
        close(bao);
    }
    return 0;
}

然后我的测试结果是:直接解析JSON比在xml里面要快一倍!在小米1S上结果如下:

时间 | json |sp
Mi 1S |80| 38
Nexus5X |3.5| 6.5
这个JSON的读取还没有做任何的优化,提升潜力巨大!因此,如果你需要用JSON做配置,请不要把它存放在sp里面!!

多次edit多次apply

我见过这样的使用代码:

SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

每次edit都会创建一个Editor对象,额外占用内存;当然多创建几个对象也影响不了多少;但是,多次apply也会卡界面你造吗?

有童鞋会说,apply不是在别的线程些磁盘的吗,怎么可能卡界面?我带你仔细看一下源码。

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

注意两点,第一,把一个带有await的runnable添加进了QueueWork类的一个队列;第二,把这个写入任务通过enqueueDiskWrite丢给了一个只有单个线程的线程池执行。

到这里一切都OK,在子线程里面写入不会卡UI。但是,你去ActivityThread类的handleStopActivity里看一看:

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

    // 省略无关。。
    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    // 省略无关。。
}

waitToFinish?? 又要等?源码如下:

public static void waitToFinish() {
    Runnable toFinish;
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

还记得这个toFinish的Runnable是啥吗?就是上面那个awaitCommit它里面就一句话,等待写入线程!!如果在Activity Stop的时候,已经写入完毕了,那么万事大吉,不会有任何等待,这个函数会立马返回。但是,如果你使用了太多次的apply,那么意味着写入队列会有很多写入任务,而那里就只有一个线程在写。当App规模很大的时候,这种情况简直就太常见了!

因此,虽然apply是在子线程执行的,但是请不要无节制地apply;commit我就不多说了吧?直接在当前线程写入,如果你在主线程干这个,小心挨揍。

用来跨进程

还有童鞋发现sp有一个貌似可以提供「跨进程」功能的FLAG——MODE_MULTI_PROCESS,我们看看这个FLAG的文档:

@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

文档也说了,这玩意在某些Android版本上不可靠,并且未来也不会提供任何支持,要是用跨进程数据传输需要使用类似ContentProvider的东西。而且,SharedPreference的文档也特别说明:

Note: This class does not support use across multiple processes.

那么我们姑且看一看,设置了这个Flag到底干了啥;在SharedPreferenceImpl里面,没有发现任何对这个Flag的使用;然后我们去ContextImpl类里面找找getSharedPreference的时候做了什么:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

这个flag保证了啥?保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会重新读一遍文件,仅此而已!所以,如果仰仗这个Flag做跨进程存取,简直就是丢人现眼。

小结

总价一下,sp是一种轻量级的存储方式,使用方便,但是也有它适用的场景。要优雅滴使用sp,要注意以下几点:

不要存放大的key和value!我就不重复三遍了,会引起界面卡、频繁GC、占用内存等等,好自为之!
毫不相关的配置项就不要丢在一起了!文件越大读取越慢,不知不觉就被猪队友给坑了;蓝后,放进defalut的那个简直就是愚蠢行为!
读取频繁的key和不易变动的key尽量不要放在一起,影响速度。(如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失)
不要乱edit和apply,尽量批量修改一次提交!
尽量不要存放JSON和HTML,这种场景请直接使用json!
不要指望用这货进行跨进程通信!!!

作者:pengjary 发表于2016/11/4 17:15:06 原文链接
阅读:63 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>