Yarn 下一代 Node 包管理器

最近前端圈子里最热的话题应该就是 Facebook 新出的包管理器—— Yarn 了,它解决了前端工程师构建项目中许多痛点,类比到 Java 圈子大概就是从 Maven 切换到 Gradle 的爽快吧。

阅读更多

Android开发最佳实践——2.使用Kotlin开发Android

Android 的官方开发语言是 Java,那为什么我们不继续使用 Java 开发 Android 呢?可能有人会说出很多理由,如:

  • 没有函数式的支持
  • Android 上只能用到 Java 6
  • 令人烦躁的 NullPointException
  • ……

但实际上我觉得让我们选择 Kotlin 而不是 Java 的原因只有一个:Kotlin 拥有更高的生产力

下面我就介绍一下 Kotlin 这个语言和它的好处,以及如何使用它编写 Android 程序。

What

Kotlin 是公司 JetBrains 研发的语言(他们家代表产品有 IntellJ Idea、Android Studio 等)。他们的网站上,他们是这样描述 Kotlin 的:

为 JVM、Android 和浏览器而生的静态编程语言。

相比起其它 JVM 上的语言,它拥有无数的优点:

  • 为 Java 作扩展而不是重写 Java,所以它的方法数相比 Groovy 和 Scala 少了很多
  • 和 Java 可以无缝调用,完美利用 JVM 生态
  • 面向对象和函数式的结合,支持多种范式
  • 现代化的语法,解决了 Java 无数痛点(如 NullPointException)
  • ……

Kotlin 习语

下面简单介绍一些 Kotlin 的习语,看看 Kotlin 是如何简化我们的编码的。

数据类

Kotlin 中创建数据类非常简单,我们只需要如下编写代码即可:

1
data class Customer(val name: String, val email: String)

使用了data class关键词之后,Kotlin 会自动帮我们生成getter(如果属性是使用var声明的则还会生成setter)、equals()hashCode()toString()copy()等常用方法。

而在 Java 中写一个 JavaBean,我们需要一个个声明属性并写出它们对应的gettersetter以及其它代码,相比之下 Kotlin 的代码量小了一大半。

Lambda

因为历史原因我们还在使用 Java6 编写 Android 代码,无法使用到 Java8 的新特性之 Lambda 表达式。而使用 Kotlin 的话是天生支持 Lambda 的,可以大大减少代码量。

Java ver:

1
2
3
4
5
6
mTextView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
// todo ...
}
});

Kotlin ver:

1
mTextView.setOnClickListener { v -> // todo...}

默认非空

Kotlin 里声明的类型都是默认非空的,可空的类型必须要在声明类型的时候在类型后面加一个?,而 Kotlin 也提供了语法糖来搞定null判断

1
2
3
Student aStudent

println(aStudent.name)

aStudent永远不会为空

而如果是一个可空的 Student

1
2
3
Student? bStudent

println(bStudent?.name)

因为声明类型的时候添加了一个?表示可空,所以bStudent是可能为null的。但 Kotlin 提供的语法糖在使用的时候bStudent?.name不会产生 NullPointException

字符串插值

Java 中格式化字符串是这样的:

1
String.format(Locale.getDefault(), "Name: %s", student.getName());

Kotlin 中我们可以这样:

1
"Name: ${student.name}"

when 表达式

Kotlin 里的 when 表达式非常强大,可以替换掉if-elseif-else

1
2
3
4
5
when (x) {
is Foo -> ...
is Bar -> ...
else -> ...
}

总结

上面讲了一些常用的 Kotlin 特性,还有更多没有写出的如:不可变集合,扩展函数,参数默认值等。

我个人的体验是使用 Kotlin 可以大大减少模版代码,让 Coding 更加愉悦。推荐在没有历史包袱的项目中使用。

但如果是有历史包袱的项目或者项目组成员不愿意去另外学习一门语言的话,那可能就无法享受到 Kotlin 的好处了,大家酌情选择即可。

Android开发最佳实践——1.接口设计

一个项目刚开始的时候,最需要确认的就是接口设计了:数据如何传递,使用什么格式什么协议乃至如何保证安全性。如果一个项目的接口设计不合理——比如没有考虑到安全性,后期为了增加安全验证又要对部分 API 推倒重做,那么前端(泛指 Android、iOS 以及 Web)就必须对整个项目进行改动,甚至可能导致之前发布的版本无法使用的囧事。

那么本文就谈谈我认为的一个好的接口应该是如何设计的。

设计

使用 RESTful 风格的 API 设计。

协议

使用 HTTPS 协议,保证 HTTP 的方便的同时保证一定的安全性。

域名

尽量部署在专属域名下,如 github:

1
https://api.github.com/

版本

应该把版本号放到 URL 中,如 API 有改版的时候,应保证老版的 API 持续提供服务一段时间。

1
https://api.example.com/v1/

路径和资源

在 Restful 风格的 API 中,每个路径都代表着互联网中的一个资源。所以 URL 地址中应该使用名词,并且因为大多是资源集合,所以应该使用复数形式。如果是有从属关系的资源,应该服从从属关系,下面给出几个例子:`

  • https://api.example.com/v1/posts
  • https://api.example.com/v1/posts/{postId}
  • https://api.example.com/v1/posts/{postId}/comments

HTTP 动词

HTTP 动词可以完美对应数据库的增删查改操作,于是我们就把 HTTP 动词和我们的增删查改操作对应起来:

  • GET:查询数据
  • POST:增加数据
  • PUT:更新数据(客户端提供改变后的完整资源)
  • PATCH:更新数据(客户端更新某几条属性)
  • DELETE:删除数据

示例

下面是结合 HTTP 动词和路径提供一些示例:

  • GET /posts :获取所有文章
  • POST /posts :创建一篇文章
  • GET /posts/{postId} :获取指定 Id 的文章信息
  • PUT /posts/{postId} :修改指定 Id 的文章信息(客户端需要提供全部属性)
  • PATCH /post/{postId} :修改指定 Id 的文章信息(客户端提供需要修改的部分属性)
  • DELETE /post/{postId} :删除指定 Id 的文章
  • GET /posts/{postId}/comments :获取指定 Id 文章的所有评论
  • POST /posts/{postId}/comments :在指定 Id 文章下创建一条评论

Query 查询

在 GET 查询的时候我们不可能一次性获取所有资源,那么我们需要提供一些查询条件。

下面是一些常用的查询:

  • ?index=2&size=20 :第二页每页20条

  • ?sortby=name&order=asc :按指定规则与顺序排序

    ……

全局信息

全局通用信息应该放在请求头里,避免使用 Query 拼接,如:

  • APPID(Android/iOS/H5)
  • APPVER(版本号)
  • CHANNEL(渠道号)
  • APP-BUILD-NUM(内部小版本号)
  • TOKEN
  • NETWORK(网络环境)
  • LANGUAGE(语言)

传输数据

Request

使用 json 格式传输数据,如果需要上传文件则使用表单的形式提交。

Response

使用 json 格式传输数据,Content-Type一致设定为application/json

响应格式应该统一,下面给出一个例子:

名字 类型 含义
code int 状态码
message String 状态信息
data List or Object 数据
time long 时间戳

具体的响应如下:

1
2
3
4
5
6
{
'code': 0,
'message': '获取成功',
'data': [{}, {}, {}], // 返回一个集合
'time': 1472435695000
}

或者返回某一个数据:

1
2
3
4
5
6
{
'code': 0,
'message': '获取成功',
'data': {}, // 返回一个对象
'time': 1472435695000
}

安全

为了保证客户端与服务端通信的安全,我们使用 HTTPS 协议。

在身份认证上使用 Oauth 2.0 协议,用户登录之后在客户端保存一份 token,避免在客户端持久化存储用户名和密码。之后每次访问需要身份认证的 API 时,必须携带 token 访问。

避免空指针

API 设计的时候应该合理帮助前端避免空指针异常,在一些字段或者属性为空的时候应该返回默认值:如 String 返回"", int 返回0, Object 返回 {}, Array or List 返回 []

参考

RecyclerView.Adapter:全能notify解决方案

在之前我们用 ListView 或者 GridView 的时候,通知适配器刷新是这样的:

1
adapter.notifyDataSetChanged();

但是当我们使用了更强大的 RecyclerView 之后,如果直接这样通知适配器刷新将不会显示动画效果。它会直接将所有的 item 重新绘制。

我们需要使用如下的方法来通知适配器刷新,这样 RecyclerView 才会显示对应的动画效果:

1
2
3
4
5
6
7
adapter.notifyItemInserted();
adapter.notifyItemChanged();
adapter.notifyItemMoved();
adapter.notifyItemRemoved();
adapter.notifyItemRangeChanged();
adapter.notifyItemRangeInserted();
adapter.notifyItemRangeRemoved();

在这次更新的 Support Library 24.2.0 中添加了一个新的工具类,可以用来方便快捷的处理 RecyclerView.Adapter 的通知刷新。

DiffUtil

DifUtil 就是这次引入的工具类,它会找出 Adapter 中每一个 Item 对应发生的变化,然后对每一个变化给予对应的刷新。

最重要的就是如下的两个重载方法

1
2
DifUtil.calculateDiff(Callback cb, boolean detectMoves);
DifUtil.calculateDiff(Callback cb);

其中DifUtil.calculateDiff(Callback cb);实际上就是DifUtil.calculateDiff(callback, true);所以我们着重研究第一个方法即可。

该方法会接收两个参数,其中第二个参数是一个 boolean 值,查看源码注释我们知道这个参数有如下作用:

True if DiffUtil should try to detect moved items, false otherwise.

如果 DiffUtil 尝试检测移动的项目就设为 true,否则设为 false。

这个参数实际上是指定是否需要项目移动的检测,如果设为 false ,那么一个项目移动了会先判定为 remove,再判定为 insert。

Callback是一个抽象类,它有四个方法需要实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract static class Callback {
/**
* 旧的数据源的大小
*/
public abstract int getOldListSize();

/**
* 新的数据源的大小
*/
public abstract int getNewListSize();

/**
* 该方法用于判断两个 Object 是否是相同的 Item,比如有唯一标识的时候应该比较唯一标识是否相等
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

/**
* 当 areItemsTheSame 返回 true 时调用该方法,返回显示的 Item 的内容是否一致
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
}

如上所述,我们四个需要实现的方法的作用都在注释中写出了。前两个方法都很好理解,需要重点说明的是后两个

  • areItemsTheSame:这个方法用来判断两个 Object 是否是相同的 Item,此处最好不要简单的用equals方法判断,我们可以根据 Object 的唯一标识或者自己指定一个规则来判断两个 Object 是否是展示的相同的 Item。
  • areContentsTheSame:该方法只有在areItemsTheSame返回true之后才会被调用,我们在重写该方法的时候,只需要判断两个 Object 显示的元素是否一致即可。如我们有两个 Object,它们可能拥有很多属性,但是其中只有两个属性需要被显示出来,那只要这两个属性一致我们这个方法就要返回true

使用 DiffUtils 通知刷新

下面我们写一个简单的例子来学习使用 DiffUtil

首先我们来一个 Item 对应的数据类:

1
2
3
4
5
6
7
8
9
public class Student {
public String id; // 学号是唯一的
public String name; // 名字可能重复

public Student(String id, String name) {
this.id = id;
this.name = name;
}
}

然后写一个 Adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

private final List<Student> datas;

public MyAdapter(List<Student> datas) {
this.datas = datas;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_recycler, parent, false);
return new ViewHolder(view);
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}

@Override
public int getItemCount() {
return datas.size();
}

class ViewHolder extends RecyclerView.ViewHolder {

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

public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}

其对应的布局文件就是一个简单的 TextView:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
tools:text="content"/>

然后我们在 Activity 里使用它们并显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
mRandom = new Random();
datas = new ArrayList<>();
for (int i = 0; i < 10; i++) {
datas.add(new Student(mRandom.nextInt(3000) + "", "Students: " + i));
}

mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new MyAdapter(datas);
mRecyclerView.setAdapter(mAdapter);
// ...
}
}

这样我们就获得了一个简单的展示学生数据的 RecyclerView 了。

然后我们对 Adapter 的数据源进行更改,并通知刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 创建一个原来的 List 的副本
final ArrayList<Student> oldTemp = new ArrayList<>(datas);
// 更改原数据源
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
// 实现 Callback
DiffUtil.Callback callback = new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldTemp.size();
}

@Override
public int getNewListSize() {
return datas.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).id.equals(datas.get(newItemPosition).id);
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).name.equals(datas.get(newItemPosition).name);
}
};
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
// 把结果应用到 adapter
diffResult.dispatchUpdatesTo(mAdapter);
}
});

效果如下:

adapter_diffutil

DiffUtil 的使用就是这样,根据 DiffUtil.Callback 计算出 Result,然后应用更新到 Adapter。

封装

有的人可能说了,这样其实并不好用啊,我们原来数据的改变就直接使用对应的方法就可以了,你这里每次还要写得这么麻烦。那么我们就使用 DiffUtil 和 Adapter 结合再进行一次封装吧。

我们抽取一个 BaseAdapter 出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class BaseAdapter<T, V extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<V>{

protected final List<T> temp; // 用于保存修改之前的数据源的副本
protected final List<T> datas; // 数据源

public BaseAdapter(List<T> datas) {
this.datas = datas;
temp = new ArrayList<>(datas);
}

protected abstract boolean areItemsTheSame(T oldItem, T newItem);

protected abstract boolean areContentsTheSame(T oldItem, T newItem);

@Override
public int getItemCount() {
return datas.size();
}

public void notifyDiff() {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return temp.size();
}

@Override
public int getNewListSize() {
return datas.size();
}

// 判断是否是同一个 item
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areItemsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}

// 如果是同一个 item 判断内容是否相同
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areContentsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}
});
diffResult.dispatchUpdatesTo(this);
// 通知刷新了之后,要更新副本数据到最新
temp.clear();
temp.addAll(datas);
}
}

然后我们只需要令 Adapter 实现 BaseAdapter即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyAdapter extends BaseAdapter<Student, MyAdapter.ViewHolder> {

public MyAdapter(List<Student> datas) {
super(datas);
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_recycler, parent, false);
return new ViewHolder(view);
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}

@Override
public boolean areItemsTheSame(Student oldItem, Student newItem) {
return oldItem.id.equals(newItem.id);
}

@Override
public boolean areContentsTheSame(Student oldItem, Student newItem) {
return oldItem.name.equals(newItem.name);
}

class ViewHolder extends RecyclerView.ViewHolder {

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

public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}

之后我们如果数据源 List 中的数据有任何改动,我们只需要调用notifyDiff()就可以了:

1
2
3
4
5
6
7
8
9
10
11
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
mAdapter.notifyDiff();
}
});

总结

最新 Support 包中的 DiffUtil 类给我们带来了一个对 RecyclerView 的不同数据变化的统一处理方案,可以对所有数据变化之后的通知刷新简化,非常好用,强烈推荐使用。

参考

Android开发学习之路-DiffUtil使用教程

夜间模式初探

Android Support Library 23.2.0 版为我们带来了官方的夜间模式,现在我们可以很容易地为 App 开发夜间模式了。

如何使用

使用起来非常简单,我们只需要将主题继承其即可

1
2
3
4
<!-- parent 为 Theme.AppCompat.DayNight -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<!-- Blah blah -->
</style>

应用全局主题

然后我们在程序中调用方法设置模式即可,推荐在 Application 的onCreate()中进行设置

1
AppCompatDelegate.setDefaultNightMode(int mode);

它有四个可选值,分别是:

  • MODE_NIGHT_NO: 使用亮色(light)主题
  • MODE_NIGHT_YES:使用暗色(dark)主题
  • MODE_NIGHT_AUTO:根据当前时间自动切换 亮色(light)/暗色(dark)主题
  • MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为 MODE_NIGHT_NO

组件主题

我们也可以为某一个组件设置主题,通过getDelegate().setLocalNightMode(int mode);即可。注意如果改变了 Activity 的主题,我们需要调用recreate()重启来显示改变后的效果。

获取当前主题

应用全局主题

和设置相对应,非常简单

1
AppCompatDelegate.getDefaultNightMode();

组件主题

如果没有为组件单独设置主题,那么将会获取全局主题,否则获取到组件的主题。

1
2
3
4
5
6
7
8
9
10
int currentNightMode = getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
// Night mode is not active, we're in day time
case Configuration.UI_MODE_NIGHT_YES:
// Night mode is active, we're at night!
case Configuration.UI_MODE_NIGHT_UNDEFINED:
// We don't know what mode we're in, assume notnight
}

属性和资源

对应夜间模式,我们会需要在不同的模式下使用不同的资源文件或不同的属性,此时我们可以新建一个带-night后缀的资源文件夹,然后再创建对应的资源文件即可,比如:drawable-nightvalues-night等。

此时如果应用切换到了夜间模式,将会自动使用-night后缀中对应的资源。

非夜间模式的后缀是-notnight,但是因为不是夜间模式就不会使用-night里的资源所以一般我们没必要使用这个后缀。

主题适配

按照如上设置了之后还可能会出现一些问题如夜间模式下文字颜色还是黑色的所以看不清了(直接给 TextView 设置了textColor="@color/xxx",而比较建议的是直接引用主题属性或者给不同模式设置不同的资源。

如字体颜色一般使用?android:attr/textColorPrimary,图标颜色一般使用?attr/colorControlNormal等。

WebView 的主题适配

WebView 因为没有特别的处理,所以我们需要通过加载特殊的 css 来完成夜间模式的适配。通过判断现在处于哪种主题然后切换对应的 css 即可。

Anroid中的自定义View测量

之前已经讲过了 Android 中 View 的绘制流程,上次主要讲的是onDraw方法,这次主要讲的就是在onMeasure方法中对 View 的大小进行测量。

理解 MeasureSpec

要了解如何在onMeasure方法中对 View 进行测量,我们首先需要了解的就是onMeasure方法传入的两个 int 值:widthMeasureSpecheightMeasureSpec

它们都是32位的 int 值,高2位代表 SpecMode(测量模式),低30位代表 SpecSize(对应模式下的测量大小)。通过以下的代码我们可以了解到 MeasureSpec 的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static final int MODE_SHIFT = 30; // Mode 的移位(高2位也就是左移30位)
// 以下四个都是 Mode 常量
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
private static final int UNSPECIFIED = 0 << MODE_SHIFT;
private static final int EXACTLY = 1 << MODE_SHIFT;
private static final int AT_MOST = 2 << MODE_SHIFT;

// 该方法用于组装 MeasureSpec,其中 sUseBrokenMakeMeasureSpec 是一个兼容参数,如果为 true 时可能会出错(sdk19之后默认走底下的逻辑)
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

// 获取 Mode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

// 获取 Size
public static int getSize(int measureSpec) {
return (size & ~MODE_MASK);
}

因为 Android 中会有大量的 View 存在,所以必然会有很多 MeasureSpec,如果将 MeasureSpec 封装成一个对象必然会造成大量的对象内存分配,这也不难理解为什么要将其包装成一个 int 了。

SpecMode

SpecMode 有三类,我们在前面的代码定义中看到了有五个常量,其中两个是作为工具存在的(MODE_SHIFT 和 MODE_MASK),另外三个就是 SpecMode 了。

UNSPECIFIED

该模式下父容器不对 View 的大小有任何限制,一般不做处理。

EXACTLY

父容器已经检测出 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 指定的大小。

对应 LayoutParams 中match_parent以及具体数值。

AT_MOST

父容器指定了一个 SpecSize,View 不能大于这个值。

它对应于 LayoutParams 中的wrap_content

与 Layout_Params 的关系

在 View 测量的时候,会将 Layout_Params 在父容器的约束下转换成对应的 MeasureSpec,然后根据这个 MeasureSpec 确认 View 测量后的宽高。一旦 MeasureSpec 确认了,在onMesure中就可以确认 View 的测量宽高了。

  • match_parent: 对应 EXACTLY
  • 精确值: 对应 EXACTLY
  • wrap_content: 对应 AT_MOST

measure 过程

measure 过程要分为 View 和 ViewGroup,它们的测量是不同的

View

由其measure方法完成,该方法是final关键字修饰的,无法重写。但measure会调用onMeasure,所以只需要看onMeasure如何实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), HeightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.ATMOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}

return result;
}

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

其逻辑很简单,getDefaultSize方法中可以看出,View 的宽高由 SpecSize 决定。于是我们知道:直接继承 View 的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则使用wrap_content属性是无效的(等同于match_parent)。

所以我们可以这样实现来使得wrap_content生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

int realWidth = widthSpecMode == MeasureSpec.AT_MOST ? mWidth : widthSpecSize;
int realHeight = heightSpecMode == MeasureSpec.AT_MOST ? mHeight : heightSpecSize;

setMeasuredDimension(realWidth, realHeight);
}

在如上代码中我们只需要指定默认最小时的mWidth,mHeight即可(wrap_content的默认宽高),其它模式下交给系统测量即可。

需要注意的是onMeasure方法中获取到的测量宽高并不一定就是控件的最终宽高,比如 RelativeLayout 中的控件会有多次测量,LinearLayout 中的子控件如果设置了weight也会有多次测量,那么第一次onMeasure的就不会准了。

ViewGroup

其实就是在测量自己的宽高之后还会调用measureChildren来遍历子控件并且测量子控件的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

ViewGroup 是一个抽象类,其onMeasure方法是没有具体实现的,所以我们继承 ViewGroup 必须重写onMeasure,重写该方法需要进行的步骤如下:

  1. 调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)处理非wrap_content的情况
  2. 单独处理wrap_content,即 SpecMode 为AT_MOST的情况
  3. 遍历子 View,并测量子 View

测量子 View 我们可以使用这几个方法

1
2
3
4
5
6
7
8
9
10
11
// 使用子view自身的测量方法
subView.measure(int wSpec, int hSpec);

// ViewGroup 的测量子 View 方法
// 某一个子view,多宽,多高, 内部加上了 viewGroup 的 padding 值
measureChild(subView, int wSpec, int hSpec);
// 所有子view 都是 多宽,多高, 内部调用了 measureChild 方法
measureChildren(int wSpec, int hSpec);
// 某一个子view,多宽,多高, 内部加上了 viewGroup 的 padding 值、margin 值和传入的宽高 wUsed、hUsed
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);

总结

View 的测量基本就是如上所述了,自定义 View 需要重写onMeasure方法并对wrap_content进行特殊处理,其实说起来需要做的并不多,但原理还是满复杂的,全部了解了之后还是觉得学到了不少东西。

背景色渐变的引导页

使用什么实现

还用问么,ViewPager 以及 Fragment 呀,非常简单。

关键 API

下面的 API 可以根据初始颜色和结束颜色计算中间值。

1
Object ArgbEvaluator.evaluate(float fraction, Object startValue, Object endValue);

具体实现

布局

  • activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.loshine.guidedemo.MainActivity">

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

</RelativeLayout>

处理滑动背景色

  • MainActivity.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class MainActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener {

ViewPager mViewPager;

private int[] colors;
private int state = ViewPager.SCROLL_STATE_IDLE; // 初始位于停止滑动状态

private ArgbEvaluator mArgbEvaluator;

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

initColors();
initViewPager();
}

/**
* 初始化 ViewPager
*/
private void initViewPager() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
if (mViewPager != null) {
// 初始颜色
mViewPager.setBackgroundColor(colors[0]);
mViewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return GuideBaseFragment.newInstance(position);
}

@Override
public int getCount() {
return 4;
}
});

mViewPager.addOnPageChangeListener(this);
}
}

/**
* 初始化颜色
*/
private void initColors() {
colors = new int[4];
colors[0] = getResources().getColor(R.color.guideBackgroundColor1);
colors[1] = getResources().getColor(R.color.guideBackgroundColor2);
colors[2] = getResources().getColor(R.color.guideBackgroundColor3);
colors[3] = getResources().getColor(R.color.guideBackgroundColor4);

mArgbEvaluator = new ArgbEvaluator();
}

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// 只要不是滑动停止状态就计算颜色
if (state != ViewPager.SCROLL_STATE_IDLE) {
if (positionOffset > 0 && position < 4) {
int evaluatePreColor = (int) mArgbEvaluator
.evaluate(positionOffset, colors[position], colors[position + 1]);
mViewPager.setBackgroundColor(evaluatePreColor);
} else if (positionOffset < 0 && position > 0) {
int evaluateNextColor = (int) mArgbEvaluator
.evaluate(-positionOffset, colors[position], colors[position - 1]);
mViewPager.setBackgroundColor(evaluateNextColor);
}
}
}

@Override
public void onPageSelected(int position) {
}

@Override
public void onPageScrollStateChanged(int state) {
this.state = state;
}
}

总结

其实实现方式并不复杂,监听 ViewPager 的滚动然后计算中间值即可,重要的是又学习到酷炫的新东西了。

在Android开发中使用Lambda表达式

由于三体人对我们的科技封锁,我们无法在 Android 开发中启用 Java 1.8 的重要特性——Lambda 表达式。但现在我们可以通过一些工具启用它,然后使用 Lambda 表达式替换没有什么实际意义的单方法匿名内部类。

Lambda表达式

可能有些人还不太清楚到底什么是 Lambda 表达式,这里先对 Lambda 表达式进行一个简单的介绍。

Lambda 表达式是函数式编程语言的特性,它简单的说就是一个匿名函数

我们先看一个 Groovy 的例子:

1
[1, 2, 3, 4, 5].asList().forEach { x -> println x }

在这个例子中,我们使用foreach来遍历一个List并打印每一个值。我们传入了一个 Lambda 表达式:{ x -> println x },这个表达式就是我们对每一个值进行的操作,在本例中就是打印它们。

在这里 Lambda 表达式是一个映射函数,foreach接受了它作为参数,然后对List中的每一个值进行遍历。

在函数式编程语言中,函数是一等公民(first class)。它们也可以作为变量或者参数被传递而且它们也是一个类。

但在 Java 中函数并不是一等公民,如果我们需要传递一个方法,必须要有一个对象包含这个方法,然后把这个对象传递过去。

所以我们经常会见到类似这样的代码:

1
2
3
4
5
6
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// do what you want...
}
});

但是实际上,我们需要的只是onClick这个方法里面的内容,其它的部分(new OnClickListener)实在是没有什么实际的意义,只是一个必须的语法而已。

所以 Java 1.8 也引入了部分函数式编程的特性——Lambda 表达式。

如果使用 Lambda 表达式,上面那个例子可以被简化为这样

1
2
3
textView.setOnClickListener(v -> {
// do what you want...
});

如果只有一行代码我们还可以省略大括号

1
textView.setOnClickListener(v -> doSomething());

当啷啷~ 是不是省略了很多代码,有没有很爽的感觉。

有了 Lambda 表达式,从此我们的代码可以清爽简洁,而且看起来也很好理解:箭头的左边是形参,右边是函数体,整个 Lambda 表达式就是一个函数(就是数学中的函数)。

更多 Lambda 表达式的信息可以查看《Java 8新特性:lambda表达式》——廖雪峰

如何使用

安利了这么多 Lambda 表达式的优点,但由于众所周知的某些原因,Android 中的 Java 版本被限定在了 1.6 以下,所以也就没办法使用那么好的 Lambda 表达式了。

但 Lambda 表达式这么好,你不让我用我就不用了么?我偏要用!

好的,有以下两种方式都可以为我们开启 Lambda 表达式,我们只需要任选其一就可以了。

RetroLambda

RetroLambda 的 Gradle 插件让我们可以在 Android 中使用 Lambda 表达式,那么我们看看如何使用它吧。

Project

我们需要在项目目录下的build.gradle中加入它的classpath

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha4'
classpath 'me.tatarka:gradle-retrolambda:3.2.5'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

dependencies中加入classpath 'me.tatarka:gradle-retrolambda:3.2.5'

app module

编辑build.gradle启用插件,并把 Java 语法调整到 1.8

  1. 在顶部启用插件
    1
    apply plugin: 'me.tatarka.retrolambda'
  2. android中加入以下代码段启用 1.8 的语法
    1
    2
    3
    4
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }

Enjoy it !~

jack

jack 是 Java Android Compile Kit 的缩写,它是 Google 为 Android 推出的一个编译工具包,它的原理在这里就不详述了。它有一个特点就是可以使用 Lambda 表达式,而且配置十分简单。

准备

使用 jack 我们必须要把buildTools升级到24以上,我已经升级到了24 RC

使用

编辑 app 模块中的build.gradle,在defaultConfig中加一行 useJack true,然后在android中添加如下一段代码

1
2
3
4
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

然后就好了,是不是非常简单呢~

总结

在 Java 1.8 中的 Lambda 表达式实际上只是一个语法糖,它可以帮助我们简化代码,并且表述地更佳清晰。但 Java 目前来说并不是一门有函数式特性的编程语言,而且短期内不会加入函数式特性。如果你想使用一门拥有函数式特性的语言来写 Android Application 的话,可以考虑一下 Kotlin。