0%

TraceView 是 Android 自带的工具, 可以在 DDMS 中找到. 最终的分析结果会导出为 .trace 格式的文件. 文件获取有以下方式:

获取方式
  1. 手动检测:

    • 在 DDMS 中选择进程.
    • 点击 Start Method Profiling 开始记录.
    • 操作完成后点击 Stop Method Profiling 结束记录.
  2. 代码检测 (需要权限 WRITE_EXTERNAL_STORAGE):

    • 在开始监控的地方调用 Debug.startMethodTracing("name") .
    • 在结束监控的地方调用 Debug.stopMethodTracing().
    • 在 sd 卡中生成 name.trace 文件.

通过 DDMS 打开 trace 文件, 下面会有一个表格展示每个函数的各项数据, 分别介绍一下各个数据的含义.

分析表格

以下面这个函数块为例子:

    void main() {                --- 10s
        A();                    --- 1s
        B();                    --- 5s
        XXX;                    --- 4s
    }
  • Incl Cpu Time % : 表示每个函数运行所占 Cpu 时间的百分比, 包括该函数内所调用的其他函数的时间.

    如果记录的正好是 main() 的执行周期, 那么 main() 的百分比就是 100%. 以 100% 为总和来计算, 每个函数执行的时间所占百分比就是其 Incl Cpu Time % 的值.

      void main() {                --- 100%
          A();                    --- 10%
          B();                    --- 50%
          XXX;                    --- 40%
      }
  • Incl Cpu Time : 表示每个函数运行所占的 Cpu 时间, 包括该函数内调用的其他函数的时间.

  • Excl Cpu Time % : 表示每个函数运行所占 Cpu 时间的百分比, 不包括该函数内所调用的其他函数的时间.

  • Excl Cpu Time : 表示每个函数运行所占的 Cpu 时间, 不包括该函数内调用的其他函数的时间.

  • Incl Real Time % : 表示每个函数运行所占实际时间的百分比, 包括该函数内调用其他函数的时间.

    Cpu Time Real Time
    进程运行在 Cpu 上的时间 从进程开始到进程结束的整个时间, 包括 I/O 的等待时间
  • Incl Real Time : 表示每个函数运行所占的实际时间, 包括该函数内调用其他函数的时间.

  • Excl Real Time % : 表示每个函数运行所占实际时间的百分比, 不包括该函数内调用其他函数的时间.

  • Excl Real Time : 表示每个函数运行所占的实际时间, 不包括该函数内调用其他函数的时间.

  • Calls + Recur Calls / Total : 对于父函数而言, 会显示 调用次数 + 递归调用次数. 对于子函数而言, 会显示 该子函数被父函数调用的次数 / 该子函数总共被调用的次数.

    以下面的表格为例子.

    Name Calls + Recur Calls / Total 说明
    main() 10 + 0 main() 是父函数, 被调用了10次, 被递归调用了0次.
    A() 1 / 3 A() 是子函数, 被 main() 调用了1次, 被其他函数调用了3次.
  • Cpu Time / Call : Incl Cpu Time / Calls , 表示每个函数在 Cpu 上的执行时间.

  • Real Time / Call : Incl Real Time / Calls , 表示每个函数运行的实际时间.

特点

TraceView 的思路是把一段时间内所有的函数都记录下来, 因为记录的东西太多, 所以记录的结果比不开启 TraceView 要慢一些.

插件地址: Jetbrains Plugin Page

Github 地址: Android-Resource-Usage-Count

插件说明

使用 IntelliJ IDEA 或者 Android Studio 打开 Android 项目的资源文件时, 会自动对文件中的资源标签统计其被引用次数, 展示在标签的前面, 统计结果会过滤 build 路径和 bin 路径的引用.

  • 支持的标签:

    • array
    • attr
    • bool
    • color
    • declare-styleable
    • dimen
    • drawable
    • eat-comment
    • fraction
    • integer
    • integer-array
    • item
    • plurals
    • string
    • string-array
    • style
  • 颜色说明:

    • 0 - 灰色
    • 1 - 蓝色
    • 其他 - 红色
    • 可以在 Preferences - Other Settings - Android Resource Usage Count 中自定义颜色

开发背景

在 Android 项目开发过程中, 我一直都是把各种 string color 等资源定义在资源文件中, 再在代码中引用它. 如果有新的 string 或者 color, 我会先对比一下是否存在, 如果存在直接使用, 不存在才创建新的资源文件.

但是资源文件可能会改动, 经常产品需要改动一个 string, 或者设计需要改动一个 color, 我都要先手动在那个资源文件上右键 - Find Usage , 引用次数为1就直接改动这个标签内容, 否则要新建一个标签. 但是觉得每次都要搜索很繁琐, 所以才产生了开发这个插件的想法, 直接显示每个资源文件的引用次数.

开发过程

其实插件开发的文档还是比较少的, 官网 提供的只是一个通用的开发过程, 按照文档你可以顺利创建项目, 然后就懵 B 了.

不过这里也是因为不同的插件需求不同, 所以还是建议想清楚自己想实现的功能和展示方式, 再多参考一下已有的系统功能怎么实现的, 已有的开源库怎么实现的, 最后当然是善用搜索.

说说我自己的开发过程, 我前期想实现的效果是

  1. 打开资源文件时自动对每个标签统计引用次数. (功能)
  2. 在标签上以 hint 的形式展示其引用次数. (展示形式)

先说功能这块:

  • 开发的时候我也是先从一个大多数例子都会提到的 Action 开始, 可是我怎么也找不到自己想要的 Action (打开某个资源文件). 最后只好随便建一个 Action 作为触发按钮, 先实现 FindUsage 的功能.

  • 然后在实现 FindUsage 的功能时, 直接参考系统的 FindUsageAction 类, 还可以通过 debug 模式直接断点代码, 简直不能更爽. 根据 FindUsageAction 类的实现过程, 把核心代码复制到自己的工具类, 简单修改后就实现了功能. 这里要说明的是, 尽量使用老版本的代码, 新版本的代码可能会在老版本上找不到方法而报错.

然后是展示形式:

  • 一开始我是希望像 Methods Count 这样
    不过我找不到它是怎么实现的…尴尬
  • 后面翻到了 LineMarkerProvider 这个类, 感觉这也是一种实现形式, 查看一下实现后也很简单, 最重要的是 LineMarkerProvider 是通过打开文件触发, 我还不用去找 Action 了.

最后是设置界面:

打包发布

  • 打包主要按照 官网 操作.
  • 发布时, 在提交页面提交 jar 包后会进去编辑页面, 在 Supported products 模块去掉勾选 Determine supported products by dependencies in plugin.xml, 改为自定义支持所需平台就好. 我发布时发现 plugin.xml 无法解析 com.intellij.modules.androidstudio, 不知道是我的问题还是写法不对.

已知问题

  1. 插件是由 LineMarkerProvider 来实现的, 打开文件会自动触发搜索统计操作. 但是有时候统计结果显示比较慢, 如果没有显示, 可以尝试重新打开文件/编辑文件/等待.

写在最后

因为自己在开发插件方面的经历实在有限, 这个插件也是根据自己的需求而实现的. 一方面是想看看大家是否也会有这样的需求, 另一方面也是希望大家可以多帮忙看看代码, 有没有更好的实现或者插件本身有值得改进的地方, 博客通知不及时, 如果遇到问题多多去 Github 上提 Issues 交流吧~

硬件分工

在计算机硬件中, 通常 CPU 用来处理数据, GPU 用来渲染数据. Android 系统也不例外, 绘制过程首先是 CPU 准备数据, 通过 Driver 层把数据交给 GPU 渲染. 其中 CPU 主要负责 Measure 、Layout 、Record 、Execute 的数据计算工作, GPU 负责 Rasterization(栅格化)、渲染. 由于图形 API 不允许 CPU 直接与 GPU 通信, 而是通过中间的一个图形驱动层(Graphics Driver)来连接这两部分. 图形驱动维护了一个队列, CPU 把 display list 添加到队列中, GPU 从这个队列取出数据进行绘制, 最终才在显示屏上显示出来.

那么无论是 CPU 准备的数据, 还是 GPU 渲染的数据, 都是以一帧一帧的形式来的. 我们所看到的界面也是有一帧一帧的图像连续显示而来. 对于人眼来说, 每秒钟看到60帧则比较流畅了, 即 FPS(Frame Per second) 为60, 1/60 = 0.01666667, 即每 16ms 进行一次准备-渲染操作.

系统变更历史

在 Android 4.1 以前, 每一次渲染的流程可以用下图表示:

横轴表示时间, 每条 VSync 线表示 16ms:

  1. 第一个16ms中, 系统显示缓存块 A 中的第0帧, 同时 CPU 和 GPU 在缓存块 B 中准备第1帧.
  2. 第二个16ms中, 系统显示缓存块 B 中的第1帧, 但是此时 CPU 可能在处理其他事情, 所以在第二个16ms快结束时才开始在缓存块 A 中准备第2帧.
  3. 第三个16ms中, 由于第2帧还没准备好, 所以只能继续显示第1帧, 此时用户就会感知到卡顿.

在 Android 4.1 版本中, 为了解决这些问题推出了 Project Butter, 主要引入 VSync, Triple Buffer和Choreographer.

  • VSync : Vertical Synchronization, 即垂直同步, 可以理解为一种定时中断, GPU 和 CPU 在收到 VSync 信号时开始准备数据.

    引入 VSync 后的渲染流程如下:

    1. 这样每次收到 VSync 中断时, CPU 和 GPU 开始工作. 只要 CPU 和 GPU 的 FPS 略高于 Display 的 FPS, 则每次都能在 16ms 之内准备好下一帧, 能够顺利显示.
    2. 如果 CPU 和 GPU 已经准备完毕, 只要没有收到 VSync 信号, 都不会进行渲染工作.
  • Triple Buffer: 即3个缓存块. 在 Android 4.1 以前, 只有2个缓存块用于准备数据, 两个缓存块交替使用. 在引入 VSync 后, 如果 CPU 和 GPU 的 FPS 比 Display 的 FPS 低, 即不能在 16ms 内准备好数据, 会导致很严重的掉帧效果.

    1. 在第一个16ms中, 系统显示缓存块A, CPU 和 GPU 在缓存块 B 中准备第1帧.
    2. 在第二个16ms中, 由于 GPU 还没有准备好, 所以只能继续显示缓存块 A 的内容, 用户感知到卡顿.

    为了解决这个问题, Android 4.1 引入第三个缓存块:

    1. 在第一个16ms中, 系统显示缓存块A, CPU 和 GPU 在缓存块 B 中准备第1帧.

    2. 在第二个16ms中, 由于 GPU 还没有准备好, 所以只能继续显示缓存块 A 的内容, 用户感知到卡顿. 但此时 CPU 可以在缓存块 C 中准备数据.

    3. 正常显示不会再丢帧.

      但是缓存块并不是越多越好, CPU 和 GPU 准备的数据最好在下一个 16ms 显示, 但是 Triple Buffer 中 CPU 准备的缓存块C, 在第四个 16ms 中才显示, 滞后了 16ms. 所以第三个缓存块主要是备用, 一般来说两个缓存块就够了.

  • Choreographer: 译为舞蹈编排, 起到调度作用, 收到 VSync 信号时调用用户设置的回调函数, 回调类型有三种:

    • CALLBACK_INPUT:优先级最高,和输入事件处理有关。
    • CALLBACK_ANIMATION:优先级其次,和Animation的处理有关。
    • CALLBACK_TRAVERSAL:优先级最低,和UI等控件绘制有关。

invalidate() 调用顺序

  • View.invalidate()
  • View.invalidateInternal() -> 找到 dirty 区域并传给 ViewParent
  • ViewParent.invalidateChild -> 接口, 由 ViewGroup 和 ViewRootImpl 实现
  • ViewGroup.invalidateChild -> 向上递归调用 ViewParent.invalidateChildInParent, 直到 ViewRootImpl
  • ViewRootImpl.invalidateChildInParent -> ViewRootImpl.scheduleTraversals
  • ViewRootImpl.scheduleTraversals -> Choreographer.postCallback(Choreographer.TRAVERSAL)
  • Choreographer 调度到下一个 VSYNC 信号来时再回调. ViewRootImpl.doTraversal
  • ViewRootImpl.doTraversal -> ViewRootImpl.performTraversals 开始绘制

这是一篇关于如何更好的编写 RecyclerViewAdapter 文章, 原文链接为 Writing-Better-Adapters .

原文中的示例代码是用 Kotlin 编写的, 这里我会变成 Java 版本, 同时也会结合自己的理解做些改变. 所以建议大家还是看一遍原文.


对于 Android 开发者来说, 实现 Adapter 是最频繁的工作之一. Adapter 是所有列表的基本, 而列表也是很多 App 的基本组成.

编写一个列表控件的方法大多数时间都是一样的: 用一个绑定了 Adapter 的 View 来展示数据. 然而一直这样会让我们对自己编写的代码变得盲目, 尽管那是辣鸡代码. 或者说, 我们一直在重复创造辣鸡代码.

让我们深入地看看 Adapter 的代码.

#RecyclerView 的基础
RecyclerView(同样适用于 ListView) 的基础操作:

  • 创建 View 和储存 View 信息的 ViewHolder.
  • 根据 ViewHolder 储存的信息来绑定 ViewHolder , 大部分是 List 中的 model.

这些步骤都比较简单, 一般不会出错.

#包含多种类型的 RecyclerView
当 View 需要展示多重不同的类型时, 事情就变得麻烦了. 比如下面这个例子.

1
2
3
4
5
interface Animal {}
class Mouse implements Animal {}
class Duck implements Animal {}
class Dog implements Animal {}
class Car

这个例子中,我们需要处理不同类型的动物, 而另一个对象”车”又与动物无关. 这意味着你需要创建不同的 ViewHolder 并对每一个 ViewHolder 初始化不同的布局. API 对每个类型定义了不同的 int 对象, 这就是辣鸡代码的开始.

让我们看看一些常见的代码, 当你需要多个类型时, 需要重写这个方法:

1
2
3
4
@Override
public int getItemViewType(int position) {
return 0;
}

默认实现返回0, 但多种类型时需要根据类型返回不同的值.

下一步,创建 ViewHolder, 即实现这个方法:

1
2
3
4
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

}

根据上一步 getItemViewType() 返回的不同 viewType 值来创建不同的 ViewHolder, 方法可能是 switch 语句或 if-else语句, 不过这个关系不大.

同样的, 在绑定这个创建了的(或者回收了的) ViewHolder 时, 也需要处理不同类型.

1
2
3
4
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {

}

因为这里没有 type 参数, 所以会根据 ViewHolder 的 Instance-of 判断不同类型, 或者也可以在这些 ViewHolder 的基类中处理 onBind .

#不好的地方
所以上面这种实现有什么问题呢? 看起来不是很直观吗?

让我们再看一下 getItemViewType() 方法: 系统需要知道每个位置的类型, 所以你需要将你的数据列表中的每一项都转化成视图类型. 那就可能产生这种代码:

1
2
3
4
5
if (things.get(position) == Duck) {
return TYPE_DUCK;
} else if (things.get(position) == Mouse) {
return TYPE_MOUSE;
}

你现在觉得这个代码糟糕吗? 如果不觉得, 那我们接下来看看 onBindViewHolder() 的实现.

1
2
3
4
5
6
7
8
9
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
Thing thing = things.get(position);
if (thing == Animal) {
((AnimalViewHolder) thing).bind((Animal) thing);
} else if (thing == Car) {
((CarViewHolder) thing).bind((Car) thing);
}
}

这段代码看起来就很乱了, Instance-of 的检查和强制类型转化使这段代码非常违背设计模式.

许多年前我的显示器上就贴着一段引用自 Scott Meyers 所写的 Effective C++ 中的一段话:

Anytime you find yourself writing code of the form “if the object is of type T1, then do something, but if it’s of type T2, then do something else,” slap yourself.

每次你发现自己写的代码是像”如果这个对象是 T1 类型, 就做这个, 或者如果这个对象是 T2 类型, 就做那个”的样式的话, 就扇自己一巴掌

所以当然回头看 Adapter 的实现时, 就需要扇自己很多下.

  • 我们有很多类型检查和强制类型转化.
  • 这是明显的适合使用面向对象却没有使用的代码.
  • 实现方式违背了 SOLID 原则中的 开闭原则 , 即拓展时不需要修改内部实现.

#尝试解决
一种替代方式就是在过程中间加上转化步骤, 比如一种很简单的方式就是把所有类的类型都放在一个 map 里面, 通过一次调用获取即可.

1
2
3
4
@Override
public int getItemViewType(int position) {
return types.get(thing.class);
}

这样做会好点吗?

不幸的是, 这不完全解决问题, 这种方式只是隐藏了 Instance-of 的检查.因为接下来实现 onBindViewHolder() 时还是会产生 如果这个对象是 T1 类型, 就做这个, 或者如果这个对象是 T2 类型, 就做那个 这样的代码.

我们的目标应该是 添加新的类型时不需要修改 Adapter 的代码 .

因此, 一开始不需要在 Adapter 中创建视图和数据的对应关系. 另外 Google 也推荐使用布局 id 来区分不同类型, 这样你就不用自己定义每种类型了.

从另一个角度想, 如果我们想创建视图和数据的对应关系, 不一定非要从数据集入手, 可以对每个数据添加一个方法:

1
2
3
public int getType() {
return R.layout.item_duck;
}

那 Adapter 中获取类型的方法就可以变成

1
2
3
4
@Override
public int getItemViewType(int position) {
return things[position].getType();
}

这样就符合了开闭原则, 当我们想要添加新类型时不需要再改变 Adapter 中的代码.

但这种做法把架构中的每个层打乱了. 实体类型知道它的表现形式, 这种关系指向是错误的. 对我们来说不可接受. 另一方面, 在数据中添加方法来表示它的类型, 这种做法并不是面向对象的, 我们只是再一次隐藏了 Instance-of 的检查.

#ViewModel 视图模型
更进一步的处理方法, 便是使用独立的视图模型而不是直接使用模型. 归根结底, 我们的数据是不想交的, 他们没有一个相同的基类: 车不是动物. 这对数据层来说是对的, 但对表现层来说, 他们都是展现在同一个视图中, 所以它们可以有一个共同的基类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class ViewModel {
abstract int type();
}

class DuckViewModel extends ViewModel {

@Override
int type() {
return R.layout.duck;
}
}

class CarViewModel extends ViewModel {

@Override
int type() {
return R.layout.car;
}
}

这样就简单封装了数据. 当添加新的视图类型时, 不需要改动 Adapter 和原来的视图类型的代码. 比如 RecyclerView 的其他类型: 分割线, 段落头部, 广告等.

这只是一个比较接近的解决方法, 但并不唯一.

#The Visitor 访问者模式
如果你有很多模型类 (Model class) , 可能你不会向对每一个再创建对应的视图模型类 (ViewModel class). 那我们再来看看如何只使用数据模型 (model) .

一开始的时候, 当我们把 type() 方法加到每个模型中, 这种方法就太耦合了. 我们应该把方法抽象出来, 比如添加一个接口:

1
2
3
interface Visitable {
int type(TypeFactory typeFactory);
}

那么每个模型就会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Animal extends Visitable {

}

class Mouse implements Visitable {

@Override
int type(TypeFactory typeFactory) {
return typeFactory.type(this);
}
}

class Car implements Visitable {

@Override
int type(TypeFactory typeFactory) {
return typeFactory.type(this);
}
}

而工厂类也应该是个抽象类, 它包含了所需的类型.

1
2
3
4
5
6
interface TypeFactory {
int type(Duck duck);
int type(Mouse mouse);
int type(Dog dog);
int type(Car car);
}

这样就完全是类型安全的实现, 不需要 Instance-of 的判断, 也不需要强制类型转化. 对于每个具体工厂的实现也是清晰的, 实现对应的类型即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TypeFactoryForList extends TypeFactory {

@Override
public int type(Duck duck) {
return R.layout.duck;
}

@Override
public int type(Mouse mouse) {
return R.layout.mouse;
}

@Override
public int type(Dog dog) {
return R.layout.dog;
}

@Override
public int type(Car car) {
return R.layout.car;
}
}

当我们还想添加新的类型时, 这是我们需要添加代码的地方. 这就非常符合 SOLID 原则. 你可能需要别的方法给新的类型, 但不需要修改任何已经存在的方法: 对拓展打开, 对修改关闭.

现在你可能会问: 为什么不直接在 Adapter 使用工厂类, 而是使用抽象工厂呢?

因为只有这样才能保证类型安全, 避免类型转化或类型检查. 花点时间认真想想这里, 这里没有用到任何一个转化. 这就是访问者模式带来的间接性.

根据以上这些步骤能保证 Adapter 非常通用.

#结论

  • 尝试保持让你的实现代码简洁.
  • Instance-of 检查应该是一个红色的警告标志.
  • 注意向下转化, 这是不好的代码的味道.
  • 尝试让上面那个点转化为正确的面向对象的用法, 想想接口和继承.
  • 尝试用通用的点来阻止转化.
  • 使用 ViewModel .
  • 注意访问者模式的使用.

我很乐于去学习更多让 Adapter 简洁的方法.


其实这篇文章着重介绍的是优化的思路, 代码实现也只是给了一些小模块, 并不是完整的 Adapter 实现.

我沿着这个思路, 写了一个 Java 版本的 Demo :

BetterAdapter. 欢迎各位指导并提出宝贵的修改意见(我也认为这个实现还是有改进空间).

在实现的过程中, 发现了一些需要注意的地方.

  • 由于每个 Adapter 中可能 model 和 type 的对应不同, 比如同一个 model M , 在 Adapter a1 中对应 type1, 在 Adapter a2 中对应 type2, 所以 onCreateViewHolder 的实现也需要放在 TypeFactory 中解耦.
  • 为了解耦(同时也是因为想不到好一点的写法), 我抽出了一个 BetterHolder 作为 ViewHolder 的基类. 然后为每个类型创建对应的 holder 来实现不同的 onCreateViewHolderonBindViewHolder.
1
2
3
4
5
6
7
8
9
10
public abstract class BetterViewHolder extends RecyclerView.ViewHolder {

public BetterViewHolder(View itemView) {
super(itemView);
}

public abstract BetterViewHolder onCreateViewHolder(ViewGroup parent);

public abstract void onBindViewHolder(BetterViewHolder holder);
}
  • 所有的数据必须以平级的方式添加进去, 并且需要按照顺序添加. 如果后台 json 数据返回的格式如下:
1
2
3
4
5
6
7
8
{
key_a : A
key_b : {
A,
A
}
key_c : C
}

那么在添加的时候:

1
2
3
4
5
mAdapter.add(A);
//把 key_b 数组遍历
mAdapter.add(A);
mAdapter.add(A);
mAdapter.add(C);

###一点题外话

  • drakeet 所写的 MultiType 也是一种很好的方法, 同时他也写了一篇博客 Android 复杂的多类型列表视图新写法:MultiType 来介绍这个库. 只是我个人不喜欢在这些很基础的地方使用自己没有理解透的第三方库, 所以才没有用到项目中. 不过我很看好这个库.
  • 无论是上面提到的 MultiType , 还是原文中提到的 BetterAdapter , 最终想要的都是解耦. 如果解决了 model 和 holder 之间的一一对应, 理论上来说一个 Application 只要维护一个 Adapter 就行了. 当然这是我的想法, 哈哈.

Android 上原生的控件 VideoView 当然是做视频播放的首选,使用简单,下面介绍一下自己在项目使用过程中的一些注意点.

  1. 宽高问题.

    1. VideoView 的源码可知,不管你设置的 View 的宽高是多少, VideoView 会根据视频文件的宽高重新设置 View 的宽高,最终显示效果类似于 ImageViewFIT_CENTER.
    2. 如果想自定义宽高,做到 FIT_XY 的效果,就需要继承 VideoView 重写 onMeasure 方法.
  2. 播放和暂停/恢复.

    1. 播放调用 setVideoPath()setVideoURI() 即可, VideoView 会异步加载视频,加载完毕回调 OnPreparedListener() .如果播放本地视频,放进 raw 文件夹,如 video_guide.mp4.

       mVideoView.setVideoPath("android.resource://" + getPackageName() + "/raw/video_guide");
       mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
           @Override
           public void onPrepared(MediaPlayer mp) {
                   mp.start();
           }
         }
    2. 暂停: pause().

    3. 恢复: start().注意这里不要调用 resume().

  3. 进度条.

    1. 进度检测

      UI中一般会有一个 SeekBar 来显示当前播放进度,这里我采取是定时刷新的的办法:

      1.在 OnPreparedListener() 中设置 SeekBar 的 max 值.

       mSeekBar.setMax(mp.getDuration());

      2.通过 Handler 每秒计算 SeekBar 的位置.

       mSeekBar.setProgress(mVideoView.getCurrentPosition()).
    2. 进度条拖动,必然是通过 OnSeekBarChangeListener() 监控.

       mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
           @Override
           public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                   //建议在这里更新显示当前拖动的时间
           }
      
           @Override
           public void onStartTrackingTouch(SeekBar seekBar) {
                   //建议在这里(开始拖动时)停止倒计时
           }
      
           @Override
           public void onStopTrackingTouch(SeekBar seekBar) {
                   //建议在这里(停止拖动时)将 VideoView seekTo 当前位置.
           }
       });
      
      同时在 `OnPreparedListener()` 中, 为 `MediaPlayer` 设置 `OnSeekCompleteListener()` 来监听滑动停止.
      
          mp.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
               @Override
               public void onSeekComplete(MediaPlayer mp) {
                   //同时开始倒计时
                   mVideoView.start();
               }
           });
  4. 显示和隐藏 Loading 图.

    如果播放网络视频,难免会遇到缓冲的情况,除了视频需要切片之外,客户端还需要监听缓冲状态以显示/隐藏 Loading 图.

     mp.setOnInfoListener(new MediaPlayer.OnInfoListener() {
                     @Override
                     public boolean onInfo(MediaPlayer mp, int what, int extra) {
                         switch (what) {
                             case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                                 //显示 Loading 图
                                 break;
                             case MediaPlayer.MEDIA_INFO_BUFFERING_END:
                                 //隐藏 Loading 图
                                 break;
                         }
                         return false;
                     }
                 });
  5. “无法加载此视频”的提示.

    如果是一个无法播放的视频,会弹出一个”无法播放此视频”的 Dialog. 从体验的角度来说就算无法播放也不能用这样的 Dialog 来提示,查看源码可知, 只需要为 VideoView 设置 OnErrorListener() 即可.

  6. 横竖屏切换.

    视频当然是全屏播放最好,此时就需要将 Activity 变为横屏.

    1. AndroidManifest.xml 设置当前 ActivityconfigChanges 属性.

       android:configChanges="orientation
    2. 通过代码改变当前 Activity 的横竖屏.

       //横屏activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
       //竖屏activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    3. ViewActivityonConfigurationChanged() 回调中处理横竖屏切换.

       @Override
       protected void onConfigurationChanged(Configuration newConfig) {
           super.onConfigurationChanged(newConfig);
           if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
               //横屏
           } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
               //竖屏
           }
       }
  7. 切换后台或打开新页面(即经过 ActivityonStop() 生命周期.

    因为 VideoView 继承了 SurfaceView , 在 ActivityonStop() 生命周期中会调用 surfaceDestroyed(), 此时会释放 MediaPlayer, 所以当切换后台或打开新页面回来,视频就会重头播放,暂时还没想到解决办法.

以上就是关于原生 VideoView 的简单使用, 下一步我考虑切换到 Google 的 ExoPlayer , 如果使用顺利我会继续总结.

由于业务需要,有一些实时性要求高的接口从无状态的 HTTP 连接切换到了长连接, 所以最近基于 Apache mina 来实现了长连接.主要说明一下 Android 客户端的实现.

依赖

对于 Android 客户端来说, 不需要引入太多的包.

首先下载 mina , 目前我用的版本是 2.0.13, 解压后引入 mina-core-2.0.13.jar 包. 然后下载 slf4j-android 包,引入 slf4j-android-1.6.1-RC1.jar包.只需两个 jar 包即可.

连接

主要代码如下(注意一定不能在主线程中连接):

SocketConnector mSocketConnector = new NioSocketConnector();
//设置协议封装解析处理
mSocketConnector.getFilterChain().addLast("protocol", new ProtocolCodecFilter(new FrameCodecFactory()));
//设置心跳包
KeepAliveFilter heartFilter = new KeepAliveFilter(new HeartbeatMessageFactory());
heartFilter.setRequestInterval(5 * 60);
heartFilter.setRequestTimeout(10);
mSocketConnector.getFilterChain().addLast("heartbeat", heartFilter);
//设置 handler 处理业务逻辑
mSocketConnector.setHandler(new SocketClientHandler());
   //配置服务器地址
InetSocketAddress mSocketAddress = new InetSocketAddress(IP, PORT);
ConnectFuture mFuture = mSocketConnector.connect(mSocketAddress);
mFuture.awaitUninterruptibly();
IoSession mSession = mFuture.getSession();

TCP 的连接重点在于协议的处理和心跳.由于三次握手和粘包等问题已经在框架中处理好了,所以只需要配置协议的封装处理, 心跳包的设计业务逻辑的处理皆可.

前两者可以使用 mina 框架的 filterchain 来处理, 通过设置 Filter 使数据的发送返回结果更加符合自己的需求. addLast 等方法只是通过 key, value 的形式添加 Filter, 前面的 key 可以随意. Filter 的用法可以参考 mina官网.

协议的封装处理.

在项目中,我把 FrameCodecFactory 用作第一个 Filter, 对接收到的数据做第一步处理, 解析为我想要的格式, 同时把发出去的数据封装为服务器接受的格式.

这里项目中所使用的是 Frame 协议, 前四位表示数据的长度,后面接收该长度的数据,解析处理.

public class FrameCodecFactory implements ProtocolCodecFactory {

    @Override
    public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception {
        return new FrameEncoder();
    }

    @Override
    public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception {
        return new FrameDecoder();
    }
}

主要内容在于 Encoder 和 Decoder.

Encoder 用于封装你所发出的数据, 封装为服务器接受的格式. 这里是将 String 转为 Frame 协议.

public class FrameEncoder implements ProtocolEncoder {

    @Override
    public void encode(IoSession ioSession, Object o, ProtocolEncoderOutput protocolEncoderOutput) throws Exception {
        if (o instanceof String) {
            String messageString = (String) o;
            //封装为 Frame 协议
            byte[] messageBytes = messageString.getBytes(Charset.forName("UTF-8"));
            int totalSize = messageBytes.length + 4;
            IoBuffer buffer = IoBuffer.allocate(totalSize);
            buffer.putInt(totalSize);
            buffer.put(messageBytes);
            buffer.flip();
            protocolEncoderOutput.write(buffer);
        }
    }

    @Override
    public void dispose(IoSession ioSession) throws Exception {

    }
}

Decoder 用于解析接收到的数据, 将 Frame 格式的数据流解析为 String.

public class FrameDecoder extends CumulativeProtocolDecoder {

    @Override
    protected boolean doDecode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput protocolDecoderOutput) throws Exception {
        int totalLength = ioBuffer.getInt();
        int messageLength = totalLength - 4;
        if (ioBuffer.remaining() >= messageLength) {
            String messageString = ioBuffer.getString(messageLength, Charset.forName("UTF-8").newDecoder());
            protocolDecoderOutput.write(messageString);
            return true;
        }
        return false;
    }
}

在 FrameCodecFactory 这个 Filter 处理完后, 对于客户端的处理便可以使用 String 来传递解析数据了.

心跳包的处理

心跳包是判断 TCP 连接是否在线的关键, 在这里直接继承 KeepAliveMessageFactory ,实现对应的方法即可.

public class HeartbeatMessageFactory implements KeepAliveMessageFactory {

    @Override
    public boolean isRequest(IoSession ioSession, Object o) {
           //如果是客户端主动向服务器发起的心跳包, return true, 该框架会发送 getRequest() 方法返回的心跳包内容. 
        return false;
    }

    @Override
    public boolean isResponse(IoSession ioSession, Object o) {
        //如果是服务器发送过来的心跳包, return true后会在 getResponse() 方法中处理心跳包.
        return false;
    }

    @Override
    public Object getRequest(IoSession ioSession) {
        //自定义向服务器发送的心跳包内容.
        return null;
    }

    @Override
    public Object getResponse(IoSession ioSession, Object o) {
        //自定义解析服务器发送过来的心跳包. 
       return null;
    }
}

主要的心跳包内容及格式都可以在这里处理, 在实例化这个对象时可以设置间隔及超时时间.

KeepAliveFilter heartFilter = new KeepAliveFilter(new HeartbeatMessageFactory());
//每 5 分钟发送一个心跳包
heartFilter.setRequestInterval(5 * 60);
//心跳包超时时间 10s
heartFilter.setRequestTimeout(10);

业务逻辑处理

数据通过 Filter 过滤后, 便会到设置的 Handler 的 messageReceived() 方法中回调. 我在这里处理对应的业务逻辑.

public class SocketClientHandler extends IoHandlerAdapter {

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        //处理业务,分发数据,可以利用广播等方式.
    }

}

断开链接

断开链接也不能在主线程中调用.

mFuture.cancel();
   mSession.closeNow();
   mSession.getCloseFuture().setClosed();
   mSession.getCloseFuture().awaitUninterruptibly();
//如果完全不连接了
mSocketConnector.dispose();

总结

之前没有实现过长连接, 一开始做的时候还以为要自己实现三次握手,拆包等问题,后面才发现这些东西都已经通过 mina 实现好了, 同时无论 netty 还是 mina, 通过 filterchain 这种方式对消息处理的解耦很方便, 每个步骤只需要实现它对应需要实现的方法即可.客户端只需要维护一个 client 对象, 通过广播等方式分发数据, 目前实现还没有遇到什么大坑~.

目前很多应用实现滤镜的方式都是使用 ndk 开发,如大名鼎鼎的 GPUImage ,但是使用 ndk 的方法就需要为不同的 abi 提供不同的 .so 文件,感觉维护起来比较麻烦,这里就给出一种简单的 Java 版本的滤镜实现.

目前可以找到的改变的值有 亮度, 饱和度, 对比度, 色温, 前三者需要用到 ColorMatrix 这个类, 后面的色温用到 ColorFilter 这个类.

  • 亮度

      ColorMatrix lMatrix = new ColorMatrix();
      lMatrix.set(new float[]{
              1, 0, 0, 0, light,
              0, 1, 0, 0, light,
              0, 0, 1, 0, light,
              0, 0, 0, 1, 0});
  • 饱和度

      ColorMatrix cMatrix = new ColorMatrix();
         cMatrix.setSaturation(saturation);
  • 对比度

      float contrast;
      float scale = (contrast / 255f - 0.5f) / -0.5f;
      ColorMatrix dMatrix = new ColorMatrix();
      dMatrix.set(new float[]{
              scale, 0, 0, 0, contrast,
              0, scale, 0, 0, contrast,
              0, 0, scale, 0, contrast,
              0, 0, 0, 1, 0});

联合以上三个属性,只需要将三个矩阵连接起来即可.

    ColorMatrix finalMatrix = new ColorMatrix();
    finalMatrix.postConcat(cMatrix);
    finalMatrix.postConcat(lMatrix);
    finalMatrix.postConcat(dMatrix);
    ImageView.setColorFilter(new ColorMatrixColorFilter(finalMatrix));
  • 色温

    色温主要调节冷暖,但是因为没有具体的换算公式,所以我采用查表的方法 色温表. 查表后对 Drawable 对象调用 setColorFilter 方法在其上面再覆盖一层颜色.

      String color = "#818181";//查表得
      drawable.setColorFilter(Color.parseColor(color), PorterDuff.Mode.MULTIPLY);

保存 Bitmap

在设置这些属性的时候,都是对 ImageView 或 Drawable 操作, 但如果需要保存, 还是需要 Bitmap 对象.以下代码便是提供保存滤镜 Bitmap 的方法:

    .....
    ColorMatrix finalMatrix = new ColorMatrix();
    finalMatrix.postConcat(cMatrix);
    finalMatrix.postConcat(lMatrix);
    finalMatrix.postConcat(dMatrix);
    ....
    Bitmap bmp = Bitmap.createBitmap(imageWidth, imageHeight,
            Bitmap.Config.ARGB_8888);
    Paint paint = new Paint();
    paint.setColorFilter(new ColorMatrixColorFilter(finalMatrix));

    Canvas canvas = new Canvas(bmp);
    canvas.drawBitmap(mOriginBitmap, 0, 0, paint);
    //此时bmp即为所需要保存的Bitmap

透明状态栏适配.主要适用场景为一个Activity多个tab在不同状态栏模式下切换.

Github Demo 链接: StatusBarCompat

参考文章:

  1. 由沉浸式状态栏引发的血案
  2. Translucent System Bar 的最佳实践
  3. 该使用 fitsSystemWindows 了!
  4. Android 透明状态栏实现方案
  5. 更简单更全的material design状态栏

首先强调,对于状态栏的处理有两种不同的方式, 这里从Translucent System Bar 的最佳实践直接盗了两张图做对比~.

全屏( ContentView 可以进入状态栏) 非全屏 ( ContentView 与状态栏分离, 状态栏直接着色)

先定义几个名词:

  1. 全屏模式: 左边图所示.
  2. 着色模式: 右边图所示.
  3. ContentView: activity.findViewById(Window.ID_ANDROID_CONTENT) 获取的 View , 即 setContentView 方法所设置的 View, 实质为 FrameLayout.
  4. ContentParent: ContentView 的 parent , 实质为 LinearLayout.
  5. ChildView: ContentView 的第一个子 View ,即布局文件中的 root layout .

再介绍一下相关的函数:

  1. fitsSystemWindows, 该属性可以设置是否为系统 View 预留出空间, 当设置为 true 时,会预留出状态栏的空间.
  2. ContentView, 实质为 ContentFrameLayout, 但是重写了 dispatchFitSystemWindows 方法, 所以对其设置 fitsSystemWindows 无效.
  3. ContentParent, 实质为 FitWindowsLinearLayout, 里面第一个 View 是 ViewStubCompat, 如果主题没有设置 title ,它就不会 inflate .第二个 View 就是 ContentView.
  4. requestApplyInsets(), 当窗口(Window)大小改变了,通知 View 去消费窗口的改变.
  5. FLAG_TRANSLUCENT_STATUS, 设置全屏的标志位, 此时界面可以延伸到状态栏.

5.0以上的处理:

自5.0引入 Material Design ,状态栏对开发者更加直接,可以直接调用 setStatusBarColor 来设置状态栏的颜色.

**着色模式: **

通过查看 setStatusBarColor() 方法的文档,发现在调用该方法时需要设置以下属性:

  1. 添加 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS Flag(绘制系统栏).
  2. 清除 FLAG_TRANSLUCENT_STATUS Flag(透明状态栏).
  3. 调用 setStatusBarColor() 设置状态栏颜色.

**全屏模式: **

由于 5.0 以上为状态栏添加了一个阴影, 所以为全屏模式添加了是否隐藏状态栏阴影的方法.

  • 隐藏阴影

    1. 像着色模式一样添加 flag ,然后通过 setStatusBarColor() 设置颜色为透明.
    2. 通过 setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) 隐藏状态栏颜色.
  • 显示阴影

    1. 设置 FLAG_TRANSLUCENT_STATUS 来隐藏状态栏.
    2. 通过 setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE) 来恢复默认状态栏样式.

4.4-5.0的处理:

4.4-5.0因为没有直接的 API 可以调用,需要自己兼容处理.参考了网上的解决方法及结合我自己遇到的坑,最后想出的解决办法如下:

着色模式

  1. DecorView 中添加一个 View, 高度为状态栏的高度(反射获取).
  2. ChildView 的 marginTop 加上状态栏的高度,以此来模拟 fitsSystemWindows.
  3. 设置 ChildView 的 fitsSystemWindow 为 false, 不预留系统栏位置.
  4. DecorView 设置一个 tag, 防止重复添加 View.

这里与其他地方不同的是:

  1. ContentView 添加 View 在部分机型(华为)上没有效果.
  2. ContentParent 上添加 View 会有一条黑线.
  3. 使用 marginTop 而不是 fitsSystemWindows 是因为无法在**不重启 Activity **的情况下切换 root layout 的fitsSystemWindows属性, 即直接设置不会生效, 所以用 marginTop 来模拟.

全屏模式

  1. 设置 ChildView 的 fitsSystemWindow 为 false, 不预留系统栏位置.
  2. 如果在 ChildView 的 marginTop 中添加了状态栏的高度, 则移除.
  3. 设置 tag, 防止重复移除.

** CollaspingToolbarLayout **
这个 support 包中的控件, 由于重写了 onApplySystemInsets() 方法, 按照我所理解的状态栏模式, 它在滑动时在两种模式中切换, 对此我的兼容方法就是让其处于 着色模式 下,在滑动时保持状态栏颜色不变. 当然有更好的解决办法, 但我这里为了方便调用(只需要传递 Activity 对象), 就用了比较简单的处理方法.

CollapsingToolbarLayout

support 包中提供的 CollapsingToolbarLayout, 在使用的时候大概的布局是这样的(参考 CheeseSquare ):

<CoordinatorLayout
    android:fitsSystemWindows="true">

    <AppBarLayout
        android:fitsSystemWindows="true">

        <CollapsingToolbarLayout
            android:fitsSystemWindows="true">

            <View
                android:fitsSystemWindows="true"
                 app:layout_collapseMode="parallax"/>

          <ToolBar
              app:layout_collapseMode="pin"/>

       </CollapsingToolbarLayout>

   <AppBarLayout/>

   <View
           app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</CoordinatorLayout>

在适配的时候主要遇到以下几个问题:

  1. 如果按照我上面介绍的方式来说, 它在显示图片的时候是 全屏模式 , 在显示 Toolbar 的时候是 着色模式.
  2. CollapsingToolbarLayout 的内部在 5.0 及以上的版本中通过 OnApplyWindowInsetsListener() 中获取的 Insets 对象, 在 Layout 的过程中将 View 向下偏移了, 所以它在 5.0 及以上系统中可以占据状态栏, 在 4.4 系统上则不能.
  3. 仿照 CheeseSquare 这个库中的写法, CollapsingToolbarLayout 需要设置 FitsSystemWindow 为 true, 而我在前面兼容 全屏模式着色模式 的时候, 都是设置 FitsSystemWindow 为 false. 同时目前我没找到方法在不重启 Activity 的情况下切换 FitsSystemWindow.

在参考了网上的文章后,最后决定通过以下方法来处理.

  • 4.4版本:

    1. 变为全屏模式,设置 View 的 fitsSystemWindow 为 false, 这一步可以使 CollapsingToolbarLayout 占据状态栏, Toolbar 也是.
    2. 改变 Toolbar 的高度, 加上状态栏的高度, 让 Toolbar 挡住状态栏位置, 同时为 Toolbar 添加 paddingTop , 这样就可以让 title 正常显示.
    3. 添加假的 StatusView 模拟状态栏颜色, 通过 AppBarLayoutOnOffsetChangedListener() 监听 AppBarLayout 的滑动, 使 StatusView 跟随 CollapsingToolbarLayout 的显示隐藏.
  • 5.0及以上版本:

    1. 变为全屏模式,设置 View 的 fitsSystemWindow 为 false, 这一步可以使 CollapsingToolbarLayout 占据状态栏, Toolbar 也是.
    2. 改变 Toolbar 的高度, 加上状态栏的高度, 让 Toolbar 挡住状态栏位置, 同时为 Toolbar 添加 paddingTop , 这样就可以让 title 正常显示. 这都和 4.4 一样.
    3. CollapsingToolbarLayout 设置 OnApplyWindowInsetsListener() 使其正常 Layout ,因为此时 CollapsingToolbarLayout 已经显示到状态栏了, 不需要在 Layout 过程中向下偏移, 但是此时collapsingToolbarLayout.setStatusBarScrimColor() 也就无效了.
    4. 通过 AppBarLayoutOnOffsetChangedListener() 监听 AppBarLayout 的滑动, 利用 setStatusBarColor() 设置状态栏颜色即可.

关于博客和库

博客主要提供思路解析,因为博客的通知不及时,大家有问题有想法想交流时还请在 Github issues 页联系.

目前很多应用都是采用的底部导航+Tab的方式.目前项目中采用的是RadioButton + FrameLayout + Fragment的实现方式,所以总结一下遇到的几个问题.

  1. Activity重建导致Fragment重叠.

    • 复现方式: 系统销毁Activity,如改变字体大小.
    • 问题原因: Activity销毁时,系统会保存该Activity状态,而onCreate(Bundle state)会导致Activity的
    • 解决方法:
      • 在Activity销毁时不保存状态.如重写onSaveInstanceState (Bundle outState)并且不调用super方法,即在Activity销毁时不保存状态.但是该方式会导致Application创建两次.
      • 在Activity创建时,调用super.onCreate(null),即不根据其保存的状态来恢复Activity状态,而是直接重新创建.这算是一种偷懒的办法吧.
      • 在利用 FragmentManager addFragment 时,将每个 Fragment 设置不同的tag. 然后在 Activity 的 onCreate (Bundle savedInstanceState) 方法中, 利用 FragmentManager 根据不同 tag 来获取已经创建并添加了的 Fragemnt, 而不用重复创建.
  2. FragmentTransaction 在 commit 时, 如果 Activity 正在销毁, 会导致崩溃.

    异常为: java.lang.IllegalStateException: Can not perform this action after onSaveInstance!

    意思就是不能在 onSaveInstance() 后调用 commit 方法, 而 onSaveInstance() 是做状态保存工作, 调用时间由系统决定的, 它只能保证在 onStop() 前调用.

    • 最好在 FragmentActivity#onResumeFragments()Activity#onPostResume() 中调用 commit . 这两个方法能确保当前 Activity 的状态已经恢复.
    • 如果确保这个 commit 丢失也没关系, 可以使用FragmentTransaction#commitAllowingStateLoss();
  1. Fragment中调用getContext()getActivity()为null导致崩溃.

    • 复现方式: detach一个Fragment,然后异步函数调用getContext().

    • 问题原因: onDetach()调用后,getContext()会置为null.

    • 解决方法:

      • 在BaseFragment中维护一个mContext,然后重写onAttach(Context context)方法:

          @Override
          public void onAttach(Activity activity) {
              super.onAttach(activity);
              this.mContext = activity;
          }
        
          @Override
          public Context getContext() {
              if (super.getContext() != null) {
                  return super.getContext();
              }
              return mContext;
          }

        然后尽量调用getContext()方法.

      • 因为getActivity()方法不能重写,所以只好加上判断:

          if(isAdded()) {
              //XXXX
          }
  2. 如果使用 ViewPager + Fragment, ViewPager 在初始化时会缓存多个 Fragment, 这里可以通过 setUserVisibleHint 来判断 Fragment 是否可见.一个简单封装的 Fragment 如下:

     public abstract class BaseFragment extends Fragment {
    
         //必须有空的构造函数
         public BaseFragment() {
    
         }
    
         //避免ViewPager在一开始创建
         private boolean hasLazyLoad = false;
    
         public void setHasLazyLoad(boolean hasLazyLoad) {
             this.hasLazyLoad = hasLazyLoad;
         }
    
         /**
          * 懒加载,防止ViewPager重复创建
          */
         protected void onLazyLoad() {
    
         }
    
         @Override
         public void setUserVisibleHint(boolean isVisibleToUser) {
             super.setUserVisibleHint(isVisibleToUser);
             if (getUserVisibleHint() && !hasLazyLoad) {
                 onLazyLoad();
                 hasLazyLoad = true;
             }
         }
    
         @Override
         public void onDestroyView() {
             super.onDestroyView();
             hasLazyLoad = false;
         }
     }

    注意需要在 onDestroyView()中复位标志位,因为 ViewPager 的切换会导致 Fragment 的销毁.

  3. 在 Fragment 中嵌套 Fragment 时, 最好使用 getChildFragmentManager() 来获取 FragmentManager 示例,同时在 Fragment 销毁时, 利用反射将其置为空. 否则会导致错误 java.lang.IllegalStateException: Activity has been destroyed :

     @Override
     public void onDestroyView() {
         super.onDestroyView();
    
         //解决嵌套 Fragment 的bug
         try {
             Field childFragmentManager = Fragment.class.getDeclaredField("mChildFragmentManager");
             childFragmentManager.setAccessible(true);
             childFragmentManager.set(this, null);
    
         } catch (NoSuchFieldException e) {
             throw new RuntimeException(e);
         } catch (IllegalAccessException e) {
             throw new RuntimeException(e);
         }
     }

现在淘宝,京东等App在节假日打开时,都是采用一套节假日的图标,这种不用发版本,又可以随着后台配置动态替换图标固然是很方便,体验很好的事情.

准备工作:全部按钮的不同状态改为纯图片实现.

现在很多应用的通知策略都是采用小红点提示,小红点可以单独画出来,与图标分离,单独控制隐藏.但是现在需要动态替换图标,小红点就不能与图片分离了,否则会导致红点位置不理想或红点被图标遮住等问题.为此,所有的状态都需要改为纯图片实现.

首页的tab,目前应用采用的是RadioButton + Fragment的实现,为此,需要自定义RadioButton的selector.

  1. 定义attr.

     //下面定义的state_new_message表示有新消息
     <declare-styleable name="MessageRadioButton">
         <attr name="state_new_message" format="boolean"/>
     </declare-styleable>
  2. 定义selector.

     //xxx为你的包名
     //为了配合state_checked,需要4张图片
     <?xml version="1.0" encoding="utf-8"?>
     <selector xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:message="http://schemas.android.com/apk/res/xxx">
         <item android:drawable="@drawable/icon_union_selected"
             android:state_checked="true"
             message:state_new_message="false"/>
         <item android:drawable="@drawable/icon_union_selected_new"
             android:state_checked="true"
             message:state_new_message="true"/>
         <item android:drawable="@drawable/icon_union_normal" 
             android:state_checked="false"
             message:state_new_message="false"/>
         <item android:drawable="@drawable/icon_union_normal_new" 
             android:state_checked="false"
             message:state_new_message="true"/>
     </selector>
  3. 重写RadioButton(ImageView等控件也同样适用).

     public class MessageRadioButton extends RadioButton {
         //定义的状态
         private static final int[] STATE_NEW_MESSAGE = {R.attr.state_new_message};
         //是否有新消息
         private boolean hasNewMessage;
    
         /**
          * 构造函数
          **/
    
         /**
          * 关键需要此函数,添加状态
          **/
         @Override
         protected int[] onCreateDrawableState(int extraSpace) {
             if (hasNewMessage) {
                 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
                 mergeDrawableStates(drawableState, STATE_NEW_MESSAGE);
                 return drawableState;
             }
             return super.onCreateDrawableState(extraSpace);
         }
    
         /**
          * 设置是否有新消息数
          * @param newMessage, 是否有新消息数
          */
         public void setHasNewMessage(boolean newMessage) {
             if (hasNewMessage != newMessage) {
                 hasNewMessage = newMessage;
                 refreshDrawableState();
             }
         }
    
     }

使用MessageRadioButton的setHasNewMessage()方法即可以显示带小红点的图片.
所有按钮改为图片实现.

创建Drawable对象

动态配置就是下载图片成功后,从文件中获取每个按钮的Drawable对象.但是实现时需要注意细节:

  1. 根据图片生成的Drawable对象即BitmapDrawable.

    • 目前BitmapDrawable对应的构造函数未被弃用的还有

        BitmapDrawable(Resources res, Bitmap bitmap)
        BitmapDrawable(Resources res, String path)

      但使用BitmapDrawable(Resources res, String path)时发现在vivo手机上没有生效,所以还是推荐BitmapDrawable(Resources res, Bitmap bitmap)方法.

    • 图片在使用时也需要区分dpi,对应Bitmap.setDensity()方法,参数为160的倍数.

  2. selector对应的Drawable对象即StateListDrawable.

    • StateListDrawable.addState(int[] stateSet, Drawable drawable)方法可以添加状态对应的drawable.

        //state_new_message为false
        listDrawable.addState(new int[]{-R.attr.state_new_message}, drawable);
        //state_new_message为true
        listDrawable.addState(new int[]{R.attr.state_new_message}, drawable);
    • listDrawable必须调用setBounds()方法才能显示.