Anroid中的自定义View绘制

虽然我们在开发中基本可以用 Android 自带的各种控件实现绝大多数的功能,但难以避免还是有一些需求是自带的控件无法实现的。这个时候我们通常会想到去 Github 上寻找开源控件,但有的东西是有成熟的实现如:ViewPager 的 Indicator。而有的就没那么容易找到了。

还有就是虽然我们平时的一些需求可以使用图片资源代替,但过多的图片资源不仅会使得应用体积增大,还会使得加载的过程中消耗不少的系统资源(内存以及 CPU)—— 我曾经就这么干过,至少这种方法做东西很快(但也很坑)。

这个时候我们就应该想到自定义 View 了,下面就讲讲我在学习自定义 View 的一些心得体会吧。

View绘制流程

View 的绘制是从 ViewRoot 的performTraversals()方法开始的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视图的位置(layout),以及是否需要重绘(draw)视图,其流程图如下所示:

流程图

而我们今天讲的自定义 View 的绘制,主要就是在是否需要重新 draw 这一步来实现。

三个绘图工具类简介

要在自定义 View 中进行重新绘制,我们首先需要了解一下 Android 中的三个重要的绘图工具类,它们就是Paint(画笔)、Canvas(画布)以及Path(路径)。当然其实不仅仅只有这三个可以作用于画图和图像处理,但它们是最基础的。

Paint

Paint 就是画笔,在 Android 图形绘制的时候,我们就好像真的有一个人拿着画笔把图像画出来一样,所以画笔这个类也给了我们和现实世界作画的时候一样的一些设定。

我们可以通过 Paint 来设定线宽(就像现实中画笔的粗细)、颜色(颜料)、透明度以及填充风格等。

我们可以通过它的构造函数来新建一个画笔

1
Paint paint = new Paint();

然后对它进行一些设定

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
paint.setARGB(255, 255, 0, 0); // 设置 ARGB 颜色 int
paint.setAlpha(0); // 设置透明度 int
paint.setColor(getResources().getColor(android.R.color.black)); // 设置颜色
paint.setAntiAlias(true); // 开启抗锯齿
paint.setDither(true); // 开启抖动处理,使得绘制的图形更清晰
paint.setFilterBitmap(true); // 滤掉对Bitmap图像的优化操作,加快显示速度
paint.setMaskFilter(maskFilter); // 添加滤镜
paint.setColorFilter(colorFilter); // 设置颜色过滤器
paint.setPathEffect(pathEffect); // 设置路径效果(如虚线等)
paint.setShader(shader); // 设置渐变效果
paint.setShadowLayer(2, 2, 2, Color.GRAY); // 半径2,x,y 距离为2,颜色灰色的阴影
paint.setStyle(Paint.Style.FILL_AND_STROKE); // 画笔样式(内部、边框还是both,画封闭图形的时候比较重要)
paint.setStrokeCap(Paint.Cap.SQUARE); // 方形笔刷
paint.setStrokeJoin(Paint.Join.MITER); // 各图形的结合方式
paint.setStrokeWidth(2); // 画笔粗细
paint.setXfermode(xfermode); // 图形重叠时的处理方式
paint.setFakeBoldText(true); // 模拟粗体
paint.setSubpixelText(true); // 提升文字在 LCD 的显示效果
paint.setTextAlign(Paint.Align.CENTER); // 文字对齐方向
paint.setTextScaleX(0.5); // 文字 X 轴缩放
paint.setTextSize(40); // 文字大小
paint.setTextSkewX(30); // 文字倾斜度
paint.setTypeface(Typeface.SANS_SERIF); // 字体风格
paint.setUnderlineText(true); // 下划线
paint.setStrikeThruText(true); // 删除线
paint.setStrokeJoin(Paint.Join.ROUND); // 结合处风格
paint.setStrokeMiter(30); // 画笔倾斜度
paint.setStrokeCap(Paint.Cap.ROUND); // 拐角处风格
paint.ascent(); // baseline之上至字符最高处的距离
paint.descent(); // baseline之下至字符最低处的距离
paint.clearShadowLayer(); // 清除阴影
// 等等

但我们光有画笔还是不够的,我们至少还需要画布(Canvas)才可以真正开始作画呢。

Canvas

Canvas 就是画布,我们有了画笔和画布就可以开始作画(图形绘制)了。

我们有两种创建 Canvas 的方法:

1
2
Canvas canvas = new Canvas();
Canvas canvasByBitmap = new Canvas(bitmap);

其中传入 Bitmap 的方法会将 Bitmap 作为画布的背景。

下面是常用的drawXXX()方法,它们被用于绘制不同的图形

1
2
3
4
5
6
7
8
9
10
canvas.drawRect(new RectF(0, 0, 100, 100), mPaint); // 绘制一个方形
canvas.drawRect(0, 0, 100, 100, mPaint); // 绘制一个方形
canvas.drawPath(path, paint); // 绘制一个路径
canvas.drawBitmap(bitmap, src, dst, mPaint); // 第二和第三个参数是 Rect
canvas.drawLine(0, 0, 100, 100, mPaint); // 画线
canvas.drawPoint(100, 20, mPaint); // 画点
canvas.drawText("这是一段文字", 0, 0, mPaint); // 画文字
canvas.drawOval(new RectF(0, 0, 100, 200), mPaint); // 画方形的内切椭圆
canvas.drawCircle(300, 300, 100, mPaint); // 画圆
canvas.drawArc(new RectF(0, 0, 100, 100), 0, 30, true, mPaint); // 一个矩形内的扇形

还有clipXXX()方法,它们是裁剪一块新的区域用于绘图,这里就不详细说明了。

save()restore()方法用来保存和恢复 Canvas 的状态,简单而言就是一个存档,一个恢复存档。

还有就是三个变换方法:translate(平移)、scale(缩放)以及rotate(旋转)了,它们可以控制画布的一些动作,就好像我们真实世界中作画的时候对画布的一些动作一样(除了缩放,2333)。

Path

其实在有了上面两个类之后我们就已经可以开始绘制了,但还是先把 Path 也介绍完毕之后再开始真实案例吧。

Path 就是路径,有点像我们在初中数学中学习函数的时候,可以根据几个点确认画出一个函数的图形。

下面是一些常用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
path.addArc(new RectF(0, 0, 100, 100), 0, 30); // 添加一段圆弧
path.addCircle(300, 300, 100, Path.Direction.CW); // 顺时针圆
path.addOval(rectF, Path.Direction.CCW); // 逆时针椭圆
path.addRect(rectF, Path.Direction.CW); // 添加矩形
path.addRoundRect(rectF, {5, 5, 5, 5}, path.Direction.CW); // 添加圆角矩形
path.isEmpty(); // 是否无路径
path.transform(matrix); // 矩阵变换
path.moveTo(100, 100); // 移动画笔而不绘制
path.lineTo(300, 300); // 默认从(0,0)开始绘制,可以用 moveTo 移动起始点,调用 canvas.drawPath(path, paint) 绘制
path.quadTo(x1, y1, x2, y2); // 绘制贝塞尔曲线,三点(起始点默认(0, 0))确认
path.rCubicTo(x1, y1, x2, y2, x3, y3); // 多一个控制点的贝塞尔曲线
path.arcTo(rectF, 0, 50); // 圆弧

开始绘制

介绍完了三个绘制 UI 的基础类,那么我们现在来动手试试吧。难度从低到高,循序渐进完成自定义 View 中复杂图形的绘制。

我们自定义一个 View 并且要重新绘制的话,我们只需要新建一个类继承 View 并且实现onDraw(Canvas canvas)即可,View 会调用子类实现的onDraw完成绘制。

那么我们接下来的示例就只列出onDraw方法和对应的效果图了。

简单图形

矩形

1
2
3
4
5
6
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 在构造函数中初始化画笔并设置为黑色
canvas.drawRect(0, 0, 100, 200, mPaint);
}

黑色矩形

线段

1
2
3
4
5
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(0, 0, 100, 200, mPaint);
}

线段

圆形

1
2
3
4
5
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(100, 100, 100, mPaint);
}

圆形

画布底色

1
2
3
4
5
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(getResources().getColor(android.R.color.darker_gray));
}

画布底色

复杂图形

刻度尺

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 防止数字0出界
canvas.translate(0, 50);

for (int i = 0; i <= 100; i++) {
if (i % 10 == 0) {
// 带有数字的长刻度
canvas.drawLine(0, 0, 70, 0, mPaint);
// 画文字
canvas.drawText(String.format(Locale.CHINESE, "%d", i / 10), 100, 10, mPaint);
} else if (i % 5 == 0) {
// 每隔5的中等长度的刻度
canvas.drawLine(0, 0, 40, 0, mPaint);
} else {
// 其它小刻度
canvas.drawLine(0, 0, 30, 0, mPaint);
}
// 每个刻度画完之后位移
canvas.translate(0, 15);
}
}

刻度尺

手表表盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制外圈圆
canvas.drawCircle(400, 400, 400, mPaint);

// 绘制分针和时针
canvas.drawLine(400, 400, 400, 200, mPaint);
canvas.drawLine(400, 400, 550, 400, mPaint);

// 绘制刻度和文字
for (int i = 0; i < 12; i++) {
canvas.drawLine(400, 0, 400, 10, mPaint);
canvas.drawText(String.format(Locale.CHINESE, "%d", i == 0 ? 12 : i),
400, 100, mTextPaint);
// 旋转画布
canvas.rotate(30, 400, 400);
}
}

表盘

总结

其实 Android 中的图形绘制基本就是靠这三个类扩展变化而来,掌握了它们的使用方式我们也就可以定义各种各样的好看的自定义控件了。

那么我们掌握了绘制之后,我们还要考虑的就是自定义 View 的测量了,我会在之后再写一篇博文来总结我学习自定义 View 的测量的一些经验,感谢观看(虽然并不会有多少人看……)。

博客迁移到 Hexo

Github Pages 服务的 Jekyll 升级了,干脆我就趁着这次机会把博客迁移到 Hexo 好了。

Hexo 是 Node.js 的一个静态博客系统,相比起 Ruby 实现的 Jekyll,它生成的速度更快而且更加现代化。当然最重要的就是对前端工程师更友好啦,毕竟是用 javascript 写的嘛

使用 Hexo 和 Jekyll 的不同点在于 Hexo 是生成静态文件后上传到 Github Pages 服务上,而 Jekyll 是上传源码然后在服务器上生成静态文件。

如何使用Hexo

安装Hexo

  1. 安装 Node.js
  2. 安装 Hexo
    1
    npm install hexo-cli -g

生成静态博客项目

只需要输入以下命令就会生成一个静态博客项目

1
2
3
hexo init blog
cd blog
npm install

然后等待 npm 安装完成

运行博客

输入以下命令,然后就可以在浏览器地址栏中输入http://localhost:4000/打开博客

1
hexo server

编写文章

source/_posts文件夹下放入对应格式的 markdown 文件,hexo 就会根据模板将其渲染为对应格式的 html 静态文件。

从Jekyll迁移

迁移文章

_posts文件夹内的所有文件复制到source/_posts文件夹,并在_config.yml中修改new_post_name参数。

1
new_post_name: :year-:month-:day-:title.md

文章格式修改

Jekyll 特定的Front-matter需要删掉并且替换为对应的 Hexo 的Front-matter,并且文章的 markdown 格式可能需要修改

部署到 Github Pages

和 Jekyll 类似,我们还是需要一个username.github.io的项目。但和 Jekyll 不同的是我们需要把生成的静态文件部署上去而不是将 markdown 文件部署上去。

在本地输入

1
2
3
hexo g
# 或者
hexo generate

即可在本地生成静态页面,然后打开config.yml,修改为自己的项目信息就可以了

1
2
3
4
deploy:
type: git
repo: git@github.com:loshine/loshine.github.io.git
branch: master

高级

设置

config.yml文件有许多的可配置选项,可以参照这里设置

主题

默认情况下使用的是 landscape 主题,我们也可以在这里挑选主题

总结

其实博客迁移完毕已经挺久了,我终于在今天(2016-03-22)想起来把这个过程记录下来了,也可以给其他需要迁移的人一个参考吧。

Kotlin与DataBinding协作

DataBinding 是 Google 爹地为我们这群苦逼的 Android 开发者推出的 MVVM 框架。本文解决 Kotlin 和 DataBindin 共用时报错的问题。

如下修改即可

app 的 build.gradle 中添加如下部分

1
2
3
4
5
6
7
dependencies {
// ...
kapt 'com.android.databinding:compiler:1.0-rc5'//改为对应版本
}
kapt {
generateStubs = true
}

Kotlin中的委托属性

Kotlin 是 Jetbrain 推出的一门运行在 JVM 上的语言,它结合了面向对象以及函数式语言的特性,超甜的语法糖以及来自知名 IDE 大厂 Jetbrain 的出身让它初一面世就广受瞩目,特别是在 Android 开发社区中。它相比起 Java 拥有了许许多多的优秀特性,并且几乎每一个新特性都对应解决了 Java 开发时的痛苦之处,本篇文章主要讲解 Kotlin 中的委托属性这一特性。

委托属性(Delegated Properties)

我们先看看官网的定义:

有一些种类的属性,虽然我们可以在每次需要的时候手动实现它们,但是如果能够把他们之实现一次 并放入一个库同时又能够一直使用它们那会更好。例如:

  • 延迟属性(lazy properties): 数值只在第一次被访问的时候计算。
  • 可控性(observable properties): 监听器得到关于这个特性变化的通知,
  • 把所有特性储存在一个映射结构中,而不是分开每一条。

为了支持这些(或者其他)例子,Kotlin 采用 委托属性。

简言之就是简化手动实现的属性,将其抽象出一个库

如何使用

定义一个委托

Kotlin 中有两种属性:用var修饰的可变属性和由val修饰的只读属性。由val修饰的只读属性使用的委托需要实现ReadOnlyProperty,而var修饰的可变属性则需要实现ReadWriteProperty

在调用被委托的属性的gettersetter时,对应操作会被委托给getValue()以及setValue()

如实现一个最简单的委托Delegate

1
2
3
4
5
6
7
8
9
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'")
}
}

使用定义好的委托属性

语法为val/var <property name>: <Type> by <expression>

1
2
3
class Example {
var p: String by Delegate()
}

by后面的是委托表达式,我们调用这个对象并使用属性:

1
2
3
4
val e = Example()
println(e.p)

e.p = "NEW"

打印结果为:

1
2
Example@33a17727, thank you for delegating 'p' to me!
NEW has been assigned to 'p' in Example@33a17727.

如上可知,thisRef对应的是拥有该被委托属性的对象实例,property则是属性,value是调用setter时的传入值。

实例讲解

lazy 懒加载

Kotlin 标准库自带的懒加载委托,在属性第一次被使用时才进行初始化。

函数lazy()接受一个 lambda 然后返回一个可以作为委托Lazy<T> 实例来实现延迟属性: 第一个调用getter执行变量传递到lazy()并记录结果, 后来的getter调用只会返回记录的结果。

1
2
3
4
5
6
7
8
9
val lazyValue: String by lazy {
println("computed!")
"Hello"
}

fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}

其打印结果:

1
2
3
computed!   # 第一次使用时先初始化
Hello # getter
Hello # 后续都只会调用 getter

懒加载委托在实际编码中应用十分广泛,比如 Android 中我们可以把很多在OnCreate中需要进行的初始化操作使用懒加载委托来实现。

使用委托操作 SharedPreferences

本例出自《Kotlin for Android Developer》,使用了when表达式和委托属性巧妙地使得SharedPrefences的读写变得十分简便

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
class Preference<T>(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> {

val prefs by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }

override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return findPreference(name, default)
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}

private fun <U> findPreference(name: String, default: U): U = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}

res as U
}

private fun <U> putPreference(name: String, value: U) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}.apply()
}
}

在代码中我们可以如下使用

1
2
3
4
5
6
7
8
class WhateverActivity : Activity() {
var aInt: Int by Preference(this, "aInt", 0)

fun whatever() {
println(aInt) // 会从 SharedPreference 取这个数据
aInt = 9 // 会将这个数据写入 SharedPreference
}
}

从此操作SharedPreferences变得如此简单 ~

简单实现一个 KotterKnife

KotterKnife 是一个 Android 控件依赖注入框架,使用它可以很方便地初始化 Activity、Fragment、View 等的控件。

KotterKnife 的实现原理就是使用了委托属性,下面我就使用委托属性简单实现一个 View 注入功能

实现

我们平时是这样初始化 View 的

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val textView = findViewById(R.id.text_view) as TextView
}

考虑到通常我们在onCreate方法中将其初始化,我们可以用 lazy 委托,在第一次使用该控件的时候才将其初始化,这样可以减少不必要的内存消耗。

1
2
3
val mTextView by lazy {
findViewById(R.id.text_view) as TextView
}

对其抽取简化

1
2
3
4
5
6
7
@Suppress("UNCHECKED_CAST")
fun <V : View> Activity.bindView(id: Int): Lazy<V> = lazy {
viewFinder(id) as V
}

private val Activity.viewFinder: Activity.(Int) -> View?
get() = { findViewById(it) }

之后我们就可以在 Activity 中这样注入 View 了

1
val mTextView by bindView<TextView>(R.id.text_view)

如上实现了类似 KotterKnife 的控件注入功能,当然 KotterKnife 中还有更加强大的可选绑定以及数组绑定,本文中我们就不细说了,有兴趣的读者可以阅读 KotterKnife源码

小结

本文分析了 Kotlin 中的委托属性,并对其实际应用做了示例分析。委托属性是 Kotlin 语言的一个特性,灵活使用可以解决实际编码中的许多问题,减少大量重复代码,而由于其与属性的gettersetter直接绑定所以使用起来也十分灵活方便。

总而言之:这真是极好的

Kotlin中实现Parcelable

在Android中,如果需要序列化对象可以选择实现 SerializableParceable。如果是在使用内存的情况下,Parcelable 的效率比 Serializable 高。但 Parcelable 不能被持久化存储,此时还是需要实现 Serializable

Java实现

首先我们看一个普通的 JavaBean

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
/**
* 帖子实体类
* <p/>
* Created by Loshine on 15/9/8.
*/
public class PostEntity {

/**
* 帖子标题
*/
private String name;
/**
* 帖子类别
*/
private String category;
/**
* 帖子链接
*/
private String link;
/**
* 评论数
*/
private String comments;
/**
* 发布者
*/
private String announcer;
/**
* 最新回复时间
*/
private String replyTime;

/*
* 省略 getter setter...
*/

其中的代码都是 JavaBean 的属性以及 gettersetter

如果其实现 Parcelable,则是这样的

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
/**
* 帖子实体类
* <p/>
* Created by Loshine on 15/9/8.
*/
public class PostEntity implements Parcelable {

/**
* 帖子标题
*/
private String name;
/**
* 帖子类别
*/
private String category;
/**
* 帖子链接
*/
private String link;
/**
* 评论数
*/
private String comments;
/**
* 发布者
*/
private String announcer;
/**
* 最新回复时间
*/
private String replyTime;

/*
* 省略 getter setter...
*/

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeString(this.category);
dest.writeString(this.link);
dest.writeString(this.comments);
dest.writeString(this.announcer);
dest.writeString(this.replyTime);
}

public PostEntity() {
}

protected PostEntity(Parcel in) {
this.name = in.readString();
this.category = in.readString();
this.link = in.readString();
this.comments = in.readString();
this.announcer = in.readString();
this.replyTime = in.readString();
}

public static final Parcelable.Creator<PostEntity> CREATOR = new Parcelable.Creator<PostEntity>() {
public PostEntity createFromParcel(Parcel source) {
return new PostEntity(source);
}

public PostEntity[] newArray(int size) {
return new PostEntity[size];
}
};

在实现Parcelable的时候我们需要重写两个方法

  • public void writeToParcel(Parcel dest, int flags)
  • public int describeContents()

其中describeContents只需要返回 0 即可

writeToParcel方法中我们把需要序列化的属性使用writeXXX的方式写入 Parcel

之后是 CREATOR 对象,这个对象负责从 Parcel 中读取对象,所以我们需要重写其方法来读取对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected PostEntity(Parcel in) {
this.name = in.readString();
this.category = in.readString();
this.link = in.readString();
this.comments = in.readString();
this.announcer = in.readString();
this.replyTime = in.readString();
}

public static final Parcelable.Creator<PostEntity> CREATOR = new Parcelable.Creator<PostEntity>() {
public PostEntity createFromParcel(Parcel source) {
return new PostEntity(source);
}

public PostEntity[] newArray(int size) {
return new PostEntity[size];
}
};

这一段就是其实现方式,可见主要是将对象从 Parcel 中读取出来。

Kotlin实现

看过了冗长的 Java 实现方式,我们来看看kotlin是如何实现的吧。

首先使用插件将其转换为 Kotlin 文件,并修改其中的错误

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
class PostEntity : Parcelable {

/**
* 帖子标题
*/
var name: String? = null
/**
* 帖子类别
*/
var category: String? = null
/**
* 帖子链接
*/
var link: String? = null
/**
* 评论数
*/
var comments: String? = null
/**
* 发布者
*/
var announcer: String? = null
/**
* 最新回复时间
*/
var replyTime: String? = null


override fun describeContents(): Int {
return 0
}

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(this.name)
dest.writeString(this.category)
dest.writeString(this.link)
dest.writeString(this.comments)
dest.writeString(this.announcer)
dest.writeString(this.replyTime)
}

constructor() {
}

protected constructor(`in`: Parcel) {
this.name = `in`.readString()
this.category = `in`.readString()
this.link = `in`.readString()
this.comments = `in`.readString()
this.announcer = `in`.readString()
this.replyTime = `in`.readString()
}

companion object {

val CREATOR: Parcelable.Creator<PostEntity> = object : Parcelable.Creator<PostEntity> {
override fun createFromParcel(source: Parcel): PostEntity {
return PostEntity(source)
}

override fun newArray(size: Int): Array<PostEntity?> {
return arrayOfNulls(size)
}
}
}
}

这就是 Kotlin 实现 Parcelable 的方式了

优化

经过插件转化的 kotlin 代码其实使用的还是 java 的方式和 java 的思想,我们可以将其完全转化为 kotlin 的方式并对其优化

首先把其转化为数据类,这样会自动为我们生成

  • equals()/hashCode()
  • toString()
  • componentN()
  • copy()

我们只需要将其改为这样

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
data class PostEntity(var name: String? = null, /* 帖子标题*/
var category: String? = null, /* 帖子类别 */
var link: String? = null, /* 帖子链接 */
var comments: String? = null, /* 评论数 */
var announcer: String? = null, /* 发布者 */
var replyTime: String? = null /* 最新回复时间 */
) : Parcelable {

override fun describeContents(): Int {
return 0
}

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(this.name)
dest.writeString(this.category)
dest.writeString(this.link)
dest.writeString(this.comments)
dest.writeString(this.announcer)
dest.writeString(this.replyTime)
}

protected constructor(`in`: Parcel) : this() {
this.name = `in`.readString()
this.category = `in`.readString()
this.link = `in`.readString()
this.comments = `in`.readString()
this.announcer = `in`.readString()
this.replyTime = `in`.readString()
}

companion object {

val CREATOR: Parcelable.Creator<PostEntity> = object : Parcelable.Creator<PostEntity> {
override fun createFromParcel(source: Parcel): PostEntity {
return PostEntity(source)
}

override fun newArray(size: Int): Array<PostEntity?> {
return arrayOfNulls(size)
}
}
}
}

再之后观察发现,所有的 Parcelable 都需要有一个 CREATOR

1
2
3
4
5
6
7
8
9
10
11
12
companion object {

val CREATOR: Parcelable.Creator<PostEntity> = object : Parcelable.Creator<PostEntity> {
override fun createFromParcel(source: Parcel): PostEntity {
return PostEntity(source)
}

override fun newArray(size: Int): Array<PostEntity?> {
return arrayOfNulls(size)
}
}
}

此处使用了 Kotlin伴生对象,使得调用 CREATOR 类似于 Java 中的静态属性

可以使用 Kotlin 的函数式编程特性抽取

新建文件ParcelableExt.kt

1
2
3
4
5
public inline fun createParcel<reified T : Parcelable>(crossinline createFromParcel: (Parcel) -> T?): Parcelable.Creator<T> =
object : Parcelable.Creator<T> {
override fun createFromParcel(source: Parcel): T? = createFromParcel(source)
override fun newArray(size: Int): Array<out T?> = arrayOfNulls(size)
}

此处使用了 Kotlin 的内联函数,然后我们就可以将 PostEntity 精简为如下

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
data class PostEntity(var name: String? = null, /* 帖子标题*/
var category: String? = null, /* 帖子类别 */
var link: String? = null, /* 帖子链接 */
var comments: String? = null, /* 评论数 */
var announcer: String? = null, /* 发布者 */
var replyTime: String? = null /* 最新回复时间 */
) : Parcelable {

override fun describeContents(): Int {
return 0
}

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(this.name)
dest.writeString(this.category)
dest.writeString(this.link)
dest.writeString(this.comments)
dest.writeString(this.announcer)
dest.writeString(this.replyTime)
}

protected constructor(`in`: Parcel) : this() {
this.name = `in`.readString()
this.category = `in`.readString()
this.link = `in`.readString()
this.comments = `in`.readString()
this.announcer = `in`.readString()
this.replyTime = `in`.readString()
}

companion object {
val CREATOR = createParcel { PostEntity(it) }
}
}

总结

虽然可以直接将 Java 文件转化为 Kotlin 文件,但这样毕竟没有办法学习到 Kotlin 的精髓

使用一门语言就应该按照这门语言的编码风格以及规范去实现,这样才会让我们的学习更加有效率且养成良好的编码习惯

Kotlin 是一门典型的函数式编程语言,学习它的风格有利于我们了解函数式编程思想

在实现 Parceable 时我们使用了 Kotlin 的几个特性

  • 数据类
  • 二级构造函数
  • 内联函数

查阅官方文档完成的同时我也学会了新的姿势知识,想一想也有点小激动呢

打磨真正属于自己的输入法-RIME

很久之前就发现了 RIME 这个输入法,但因为在 windows 下的种种不便,把玩了一段时间后终于还是将之打入箱底。近期由于换上了 mac 作为主力开发机,于是又将其从箱底翻出,仔细把玩打磨一段时间之后已经十分跟手了,于是乎在这里记录一下打磨过程。

简介

RIME,全称是中州韵输入法引擎,是由佛振先生开发的跨平台开源输入法。

它在不同平台上有不同的名字:

  • Windows:小狼毫 Weasel
  • Mac OS X: 鼠须管 Squirrel
  • Linux:ibus-rime or fcitx-rime

其内置的输入方案包括

  • 朙月拼音
  • 朙月拼音 语句流
  • 朙月拼音 简化字模式
  • 朙月拼音 臺灣正體模式
  • 地球拼音 dì qiú pīn yīn
  • 注音
  • 注音 臺灣正體模式
  • 粵拼
  • 倉頡五代
  • 倉頡 快打模式
  • 速成
  • 五笔86
  • 五笔拼音混合輸入
  • 自然碼雙拼
  • 微軟雙拼
  • 智能ABC雙拼
  • 小鶴雙拼
  • 吳語上海話(新派)
  • 吳語上海話(老派)
  • 中古漢語三拼
  • 中古漢語全拼
  • X-SAMPA 國際音標
  • emoji表情

再由于其强大的可定制性,也有许多爱好者为其制作了如下输入方案

  • 英文
  • 日语
  • 山人全息码
  • ……

它也集成了许多好看的主题,当然你也可以自定义自己喜欢的配色主题。

rime_themes

安装

前往下载地址选择对应的版本下载,Mac用户选择鼠须管 Squirrel即可。

下载完成之后,解压zip压缩包,然后打开Squirrel-versioncode.pkg按步骤安装即可。

使用

  1. Mac用户在输入源中添加鼠须管
  2. ⌘ + Space切换输入法到鼠须管
  3. 按下Ctrl + `,依次选择 2、4 切换到简体输入
  4. Enjoy it!

自定义

点击右上角输入法按钮,选择用户设定,可以看到所有鼠须管的配置文件。鼠须管的配置文件都是yaml格式的,我们如果想要自定义,可以新建一个*.custom.yaml对其进行修改,这样可以防止更新程序的时候修改内容被覆盖,也方便同步。

输入方案

新建一个default.custom.yaml文件,将如下内容复制进去,然后根据自己的需求注释或者去掉注释即可。在完成之后,不要忘记重新部署一下,之后就可以使用Ctrl + `切换输入方案了。

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
patch:
schema_list:
- schema: luna_pinyin # 朙月拼音
- schema: luna_pinyin_fluency # 朙月拼音 语句流
# - schema: luna_pinyin_simp # 朙月拼音 简化字模式
# - schema: luna_pinyin_tw # 朙月拼音 臺灣正體模式
- schema: terra_pinyin # 地球拼音 dì qiú pīn yīn
# - schema: bopomofo # 注音
# - schema: bopomofo_tw # 注音 臺灣正體模式
# - schema: jyutping # 粵拼
# - schema: cangjie5 # 倉頡五代
# - schema: cangjie5_express # 倉頡 快打模式
# - schema: quick5 # 速成
# - schema: wubi86 # 五笔86
- schema: wubi_pinyin # 五笔拼音混合輸入
# - schema: double_pinyin # 自然碼雙拼
# - schema: double_pinyin_mspy # 微軟雙拼
# - schema: double_pinyin_abc # 智能ABC雙拼
# - schema: double_pinyin_flypy # 小鶴雙拼
# - schema: wugniu # 吳語上海話(新派)
# - schema: wugniu_lopha # 吳語上海話(老派)
# - schema: sampheng # 中古漢語三拼
# - schema: zyenpheng # 中古漢語全拼
# - schema: ipa_xsampa # X-SAMPA 國際音標
- schema: emoji # emoji表情

外观

新建一个squirrel.custom.yaml文件,向其中写入如下内容,然后根据自己的需求修改。

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
# 適用於【鼠鬚管】0.9.13+
# 位置:~/Library/Rime/squirrel.custom.yaml
# 用法:想要哪項生效,就刪去該行行首的#字符,但注意保留用於縮進的空格

patch:
# us_keyboard_layout: true # 鍵盤選項:應用美式鍵盤佈局
# show_notifications_when: growl_is_running # 狀態通知,默認裝有Growl時顯示,也可設爲全開(always)全關(never)
# style/horizontal: true # 候選窗横向顯示
# style/inline_preedit: false # 非內嵌編碼行
# style/font_face: "儷黑 Pro" # 我喜歡的字體名稱
# style/font_point: 21 # 字號
# style/corner_radius: 10 # 窗口圓角半徑
# style/border_height: 0 # 窗口邊界高度,大於圓角半徑才有效果
# style/border_width: 0 # 窗口邊界寬度,大於圓角半徑才有效果
# style/color_scheme: luna # 選擇配色方案

# 註:預設的配色方案及代碼(指定爲 style/color_scheme )
# 碧水 - aqua
# 青天 - azure
# 明月 - luna
# 墨池 - ink
# 孤寺 - lost_temple
# 暗堂 - dark_temple
# 星際我爭霸 - starcraft
# 谷歌 - google

主题

如上一部分,只能设置默认的主题。虽然默认的主题也挺不错的,但是我们也可以定制自定义的主题。

还是在squirrel.custom.yaml中,在刚刚加入的内容后面加入如下内容

1
2
3
4
5
6
7
8
9
10
11
preset_color_schemes/textflow:
author: "name <name@gmail.com>"
back_color: 0xFFFFFF #背景
border_color: 0xE0B693 #边框
candidate_text_color: 0x000000 #非第一候选项
hilited_back_color: 0xEBECED #拼音串高亮背景
hilited_candidate_back_color: 0xFFFFFF #第一候选项背景
hilited_candidate_text_color: 0xF57C75 #第一候选项
hilited_text_color: 0xFF6666 #拼音串高亮
name: Textflow
text_color: 0x000000 #拼音串

然后启用这个主题就可以了

如果你想要更多的配色,可以参照RIME吧:配色方案专帖

生僻字乱码

生僻字乱码是因为默认字库中不含生僻字所导致的,解决方案就是为其设置字库齐全的备用字体。

一般的选择是「花园明朝」,下载下来安装好之后,将 外观 这一步中的

1
style/font_face: "儷黑 Pro"    # 我喜歡的字體名稱

改为

1
style/font_face: "PingFang SC,HanaMinA"    # 我喜歡的字體名稱

然后重新部署即可

根据程序切换中英文

鼠须管 0.9.9 之后开始支持在指定的应用程序中设定输入法的初始状态。

如在

  • 终端 TerminaliTerm2
  • 编辑器 sublimeatom
  • 启动工具 QuickSilverAlfred

中我们很少会用到中文,于是我们可能设置鼠须管在这种程序中默认英文输入

如果想要设置应用中输入法的初始状态,我们首先要查看应用的Info.plist文件得到该应用的Bundle Identifier,通常是形如com.apple.Xcode的字符串

例如,要在Xcode中默认英文输入,又要在Alfred中恢复中文输入,可如下设定:

1
2
3
4
5
# example squirrel.custom.yaml
patch:
app_options/com.apple.Xcode:
ascii_mode: true
app_options/com.alfredapp.Alfred: {}

emoji表情以及特殊符号

有时候我们会有输入emoji表情的需求,但仅仅只是开启了emoji输入方案的话,我们每次都要切换输入方案才可以输入emoji表情,还是很不方便的。如果可以直接在一种输入方案中加入emoji表情输入就更好了

其实可以做到,你只需要这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# luna_pinyin.custom.yaml
#
# 在【朙月拼音】裏使用Emoji表情
#
# 保存到Rime用戶文件夾後,重新部署生效
# ~/.config/ibus/rime (linux)
# ~/Library/Rime (macos)
# %APPDATA%\Rime (windows)
#
# 如果目標文件已經包含其他修改內容,只需按照縮進合併 patch: 以下的部分
#

patch:
schema/dependencies:
- emoji
reverse_lookup:
dictionary: emoji
enable_completion: false
prefix: "`"
tips: 〔表情〕

以上我们就完成了emoji表情输入的设置,现在输入一个`加随意一些字母来看看效果吧

但是仅仅表情还不够,我们有时候需要输入形如之类的特殊符号,查看symbols.yaml我们可以发现及其丰富的特殊符号,但是如何才能使用它们呢?

只需要在我们上面已经添加了emoji表情相关的配置文件末尾再加上如下配置即可

1
2
3
4
5
6
7
8
punctuator:
import_preset: symbols

recognizer:
import_preset: default
patterns:
reverse_lookup: "`[a-z]*'?$"
punct: "^/([a-z]+|[0-9]0?)$"

大功告成,重新部署之后使用/lm看看吧,更多符号输入方式请查看symbols.yaml

同步

做了这么多的个性化设置之后,其实输入法应该已经很符合自己的心意了。但是如果我们更换了一台设备或者需要在其它设备上保持同样的习惯的话怎么办呢?我们使用这么久已经养成了的个人词库可以在其它设备上使用么?

当然可以!

RIME是支持同步的,但是它的同步方式和市面上主流的输入法不一样,需要我们自己使用同步网盘或者U盘来同步。

推荐国内用户使用坚果云作为同步盘使用,坚果云拥有多平台客户端且在墙内使用情况良好。若肉身在墙外则推荐DropBox

1. 设定同步位置

默认情况下,个人词库和配置都将备份到RIME用户文件夹\sync\UUID这里。如果使用网盘或者U盘在不同机器、系统之间同步,则需要设定同步的目标文件夹,此时这些文件都会备份到设定好的目录。

直接編輯用戶文件夾下的installation.yaml,添加:

1
sync_dir: 'D:\Dropbox\RimeSync'

又如 Mac 上添加:

1
sync_dir: '/Users/fred/Dropbox/RimeSync'

又如使用 USB 存儲來同步:(真實案例)

1
sync_dir: '/Volumes/USBDRIVE/RimeSync'

2. 同步标识

默认情况下,RIME会为随机生成每一个UUID作为标识。这个UUID会被用来区分來自不同机器/系统的个人词库和设置。

如果你想要让其更加便于管理,那么你可以设置一个有意义的ID用于区分,直接编辑installation.yaml中如下所示的内容即可

1
installation_id: 'loshine-macbook'

注意:此ID不能使用非法字符,建议只用小写字母、数字、横线和下划线

我的成品

成品效果演示

rime

几个配置文件

  • squirrel.custom.yaml
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
# 適用於【鼠鬚管】0.9.13+
# 位置:~/Library/Rime/squirrel.custom.yaml
# 用法:想要哪項生效,就刪去該行行首的#字符,但注意保留用於縮進的空格

patch:
# us_keyboard_layout: true # 鍵盤選項:應用美式鍵盤佈局
show_notifications_when: appropriate # 狀態通知,默認裝有Growl時顯示,也可設爲全開(always)全關(never)
# style/horizontal: true # 候選窗横向顯示
# style/inline_preedit: false # 非內嵌編碼行
# style/font_face: "PingFang SC,HanaMinA" # 我喜歡的字體名稱
style/font_point: 18 # 字號
style/corner_radius: 2 # 窗口圓角半徑
style/border_height: 7 # 窗口邊界高度,大於圓角半徑才有效果
style/border_width: 7 # 窗口邊界寬度,大於圓角半徑才有效果
style/color_scheme: luna # 選擇配色方案

# 註:預設的配色方案及代碼(指定爲 style/color_scheme )
# 碧水 - aqua
# 青天 - azure
# 明月 - luna
# 墨池 - ink
# 孤寺 - lost_temple
# 暗堂 - dark_temple
# 星際我爭霸 - starcraft
# 谷歌 - google
preset_color_schemes/textflow:
author: "name <name@gmail.com>"
back_color: 0xFFFFFF #背景
border_color: 0xE0B693 #边框
candidate_text_color: 0x000000 #非第一候选项
hilited_back_color: 0xEBECED #拼音串高亮背景
hilited_candidate_back_color: 0xFFFFFF #第一候选项背景
hilited_candidate_text_color: 0xF57C75 #第一候选项
hilited_text_color: 0xFF6666 #拼音串高亮
name: Textflow
text_color: 0x000000 #拼音串

# Xcode
app_options/com.apple.Xcode:
ascii_mode: true
# alfred
app_options/com.alfredapp.Alfred:
ascii_mode: true
# Android Studio
app_options/com.google.android.studio:
ascii_mode: true
# atom
app_options/com.github.atom:
ascii_mode: true
  • default.custom.yaml
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
patch:
schema_list:
- schema: luna_pinyin # 朙月拼音
- schema: luna_pinyin_fluency # 朙月拼音 语句流
# - schema: luna_pinyin_simp # 朙月拼音 简化字模式
# - schema: luna_pinyin_tw # 朙月拼音 臺灣正體模式
- schema: terra_pinyin # 地球拼音 dì qiú pīn yīn
# - schema: bopomofo # 注音
# - schema: bopomofo_tw # 注音 臺灣正體模式
# - schema: jyutping # 粵拼
# - schema: cangjie5 # 倉頡五代
# - schema: cangjie5_express # 倉頡 快打模式
# - schema: quick5 # 速成
# - schema: wubi86 # 五笔86
- schema: wubi_pinyin # 五笔拼音混合輸入
# - schema: double_pinyin # 自然碼雙拼
# - schema: double_pinyin_mspy # 微軟雙拼
# - schema: double_pinyin_abc # 智能ABC雙拼
# - schema: double_pinyin_flypy # 小鶴雙拼
# - schema: wugniu # 吳語上海話(新派)
# - schema: wugniu_lopha # 吳語上海話(老派)
# - schema: sampheng # 中古漢語三拼
# - schema: zyenpheng # 中古漢語全拼
# - schema: ipa_xsampa # X-SAMPA 國際音標
- schema: emoji # emoji表情
{% endhighlight %}
* terra_pinyin.custom.yaml

{% highlight yaml %}
# luna_pinyin.custom.yaml
#
# 在【地球拼音】裏使用Emoji表情
#
# 保存到Rime用戶文件夾後,重新部署生效
# ~/.config/ibus/rime (linux)
# ~/Library/Rime (macos)
# %APPDATA%\Rime (windows)
#
# 如果目標文件已經包含其他修改內容,只需按照縮進合併 patch: 以下的部分
#

patch:
schema/dependencies:
- emoji
# abc_segmentor/extra_tags:
# - reverse_lookup
reverse_lookup:
dictionary: emoji
enable_completion: false
prefix: "`"
tips: 〔表情〕

punctuator:
import_preset: symbols

recognizer:
import_preset: default
patterns:
reverse_lookup: "`[a-z]*'?$"
punct: "^/([a-z]+|[0-9]0?)$"
  • installation.yaml
1
2
3
4
5
6
7
distribution_code_name: Squirrel
distribution_name: "鼠鬚管"
distribution_version: 0.9.26.1
install_time: "Mon Aug 3 15:24:12 2015"
installation_id: "loshine-macbook"
rime_version: 1.2.9
sync_dir: '/Users/Loshine/我的坚果云/RimeSync'

Android网络通信框架Volley

之前我们在Android应用中的网络通信,一般都是使用AsyncTaskLoader,HttpURLConnection,AsyncTask,HTTPClient(Apache)等。但在 Google I/O 2013上,Google 发布了一个新的网络通信框架——Volley。Volley 适合数据量不大但通信十分频繁的场景,它使得Http通信操作相比以前更加简单、快捷、健壮。

Why

之前从网络下载图片可能要这样:

  1. ListAdapter#getView()里开始图像的读取
  2. 通过 AsyncTask 等机制使用 HttpURLConnection 从服务器去的图片资源
  3. AsyncTask#onPostExecute()里设置相应 ImageView 的属性

再有,屏幕旋转的时候,有时候会导致再次从网络取得数据。为了防止不必要的网络访问,可能我们要自己实现 cache。

还有 ListView 滚动过快时,可能会导致有些网络请求返回数据时早已不需要显示了。

这些问题使用 Volley 都可以很简单地解决

Volley 提供了如下的便捷功能:

  • JSON,图像的异步下载
  • 网络请求序列
  • 网络请求优先级处理
  • 缓存
  • 多级别取消请求
  • 和 Activity 生命周期的联动

人生苦短,快用 Volley!

How

集成

三种集成方法,其中后两种都是非官方渠道:

  • 编译源码导入项目

    1. 从 Git 库 clone 最新版
      1
      git clone https://android.googlesource.com/platform/frameworks/volley
    2. 编译为 jar 包
    3. 导入项目
  • Gradle构建(非官方渠道,推荐)

    1
    compile 'com.mcxiaoke.volley:library:1.0.18'
  • Maven构建(非官方渠道)

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.mcxiaoke.volley</groupId>
    <artifactId>library</artifactId>
    <version>{latest-version}</version>
    </dependency>

使用

具体使用流程如下:

  1. 在 Activity 中构造一个(一个就够了)RequestQueue 请求序列对象
  2. 创建一个 Request 对象(子类实现)
  3. 添加到序列
  4. 开始序列

在这四步中,我们着重要关注的是第二步,Request 是一个抽象类,我们所以我们要用到它的子类实现,Volley中已经实现了以下几个子类:

  • ClearCacheRequest
  • ImageRequest
  • JSONRequest
  • StringRequest

下面我们就对这几个子类进行介绍以及给出示例

ClearCacheRequest

虚构的请求,用于清空已有的缓存文件。

ClearCacheRequest的优先级很高,为Priority.IMMEDIATE,所以在被添加到 RequestQueue 后能很快执行。并且清空缓存的方法mCache.clear()写在了isCanceled()方法体中,能最早的得到执行。

ImageRequest

待完成

JSONRequest

JSONRequest 也是一个抽象类,所以我们在使用时要用到它的两个实现子类——JsonObjectRequest 和 JsonArrayRequest。

JsonObjectRequest 允许上传 JsonObject 数据,并根据请求返回数据。但 JsonArrayRequest 的实现过于简单,不能携带上传 json 数据,只能使用 GET 方式请求网络。

StringRequest

StringRequest 是最为常用也是最灵活的 Request 实现。一个简单的 Get 请求获取百度首页的例子:

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
public class StringRequestActivity extends AppCompatActivity {

/**
* Volley 请求队列对象
*/
private RequestQueue mRequestQueue;

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

// 构造请求队列
mRequestQueue = Volley.newRequestQueue(context);

StringRequest request = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
JSONObject jsonObject = JSON.parseObject(response, JSONObject.class);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.i("net error", error.getLocalizedMessage());
}
});
mRequestQueue.add(request);
}

@Override
protected void onStop() {
super.onStop();
// 退出时取消所有网络请求
mRequestQueue.cancelAll(this);
}
}

自定义Request

一个简单的使用 Pull 解析的 XMLRequest

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
public class XMLRequest extends Request<XmlPullParser> {  

private final Listener<XmlPullParser> mListener;

public XMLRequest(int method, String url, Listener<XmlPullParser> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}

public XMLRequest(String url, Listener<XmlPullParser> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}

@Override
protected Response<XmlPullParser> parseNetworkResponse(NetworkResponse response) {
try {
String xmlString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlString));
return Response.success(xmlPullParser, HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (XmlPullParserException e) {
return Response.error(new ParseError(e));
}
}

@Override
protected void deliverResponse(XmlPullParser response) {
mListener.onResponse(response);
}

}

parseNetworkResponse()方法中将服务器响应的数据解析成一个字符串,然后设置到XmlPullParser对象中。在deliverResponse()方法中则是将XmlPullParser对象进行回调。

与生命周期的联动

当 Activity 里面启动了网络请求,如果这个网络请求还没返回结果的时候,Activity 就被结束了。此时如果继续使用其中的 Context 等会消耗没有必要的系统资源,而且还有可能会导致程序 crash。

所以在使用 Volley 时,我们应该在 Activity 停止的时候,同时取消所有或部分未完成的网络请求。Volley 里所有的请求结果会返回给主进程,如果在主进程里取消了某些请求,则这些请求将不会被返回给主线程。Volley 支持多种 Request 取消方式。

  • 可以针对某些个 request 做取消操作
    1
    2
    3
    4
    5
    6
    @Override
    public void onStop() {
    for (Request <?> req : mRequestQueue) {
    req.cancel();
    }
    }
  • 取消这个队列里的所有请求
    1
    2
    3
    4
    5
    @Override
    protected void onStop() {
    super.onStop();
    mRequestQueue.cancelAll(this);
    }
  • 可以根据 RequestFilter 或者 Tag 来终止某些请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override  
    protected void onStop() {
    super.onStop();

    // 根据 RequestFilter
    mRequestQueue.cancelAll(new RequestFilter() {});
    // 根据 Tag
    mRequestQueue.cancelAll(new Object());
    }

Java中的模板方法模式

准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。模版方法模式是基于继承的代码复用的基本技术。

结构

模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。

模板方法所代表的行为称为顶级行为,其逻辑称为顶级逻辑。模板方法模式的静态结构图如下所示:

template

这里涉及到两个角色:

抽象模板(Abstract Template)角色:

  • 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
  • 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。

具体模板(Concrete Template)角色:

  • 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
  • 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。

示例代码

抽象模板角色类,abstractMethod()doHookMethod()等基本方法是顶级逻辑的组成步骤,这个顶级逻辑由templateMethod()方法代表。

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
public abstract class AbstractTemplate {

/**
* 模板方法
*/
public void templateMethod(){
// 调用基本方法
abstractMethod();
doHookMethod();
concreteMethod();
}

/**
* 抽象方法,子类必须实现的方法
*/
protected abstract void abstractMethod();

/**
* 钩子方法,子类可选择是否实现。注意钩子方法一般以 do 开头
*/
protected void doHookMethod(){}

/**
* 具体方法,由父类实现,子类无法 override
*/
private final void concreteMethod(){
// 业务相关的代码
}
}

具体模板角色类,实现了父类所声明的基本方法,abstractMethod()方法所代表的就是强制子类实现的剩余逻辑,而doHookMethod()方法是可选择实现的逻辑,不是必须实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteTemplate extends AbstractTemplate {

// 基本方法的实现
@Override
public void abstractMethod() {
// 业务相关的代码
}

// 重写父类的方法
@Override
public void hookMethod() {
// 业务相关的代码
}
}

模板方法模式的关键子类可以置换掉父类的可变部分,但是子类却不可以改变模板方法所代表的顶级逻辑

每当定义一个新的子类时,不要按照控制流程的思路去想,而应当按照责任的思路去想。换言之,应当考虑哪些操作是必须置换掉的,哪些操作是可以置换掉的,以及哪些操作是不可以置换掉的。使用模板模式可以使这些责任变得清晰。

在Servlet中的应用

使用过Servlet的人都清楚,除了要在web.xml做相应的配置外,还需继承一个叫HttpServlet的抽象类。HttpService类提供了一个service()方法,这个方法调用七个do方法中的一个或几个,完成对客户端调用的响应。这些do方法需要由HttpServlet的具体子类提供,因此这是典型的模板方法模式。下面是service()方法的源代码:

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
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

String method = req.getMethod();

if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}

} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);

} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);

} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);

} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);

} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//

String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);

resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

当然,这个service()方法也可以被子类置换掉。

下面给出一个简单的 Servlet 例子:

TestServlet 类是 HttpServlet 类的子类,并且置换掉了父类的两个方法:doGet()doPost()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestServlet extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

System.out.println("using the GET method");

}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

System.out.println("using the POST method");
}

}

从上面的例子可以看出这是一个典型的模板方法模式。

HttpServlet 担任抽象模板角色

  • 模板方法:由service()方法担任。
  • 基本方法:由doPost()doGet()等方法担任。

TestServlet 担任具体模板角色

  • TestServlet 置换掉了父类 HttpServlet 中七个基本方法中的其中两个,分别是doGet()doPost()

Android Design Support Library

Google 在2015的 IO 大会上,给我们带来了更加详细的 Material Design 设计规范,同时,也给我们带来了全新的 Android Design Support Library,在这个 support 库里面,Google 给我们提供了更加规范的 Material design 设计风格的控件。本文将介绍MD设计风格的兼容库以及它们的用法,也是对自己的学习做一个记录。

使用

要使用非常简单,在Gradle中添加如下语句即可

1
compile 'com.android.support:design:23.0.0'

组件

Snackbar

Snackbar 提供了一个介于 Toast 和 AlertDialog 之间轻量级控件,它可以很方便的提供消息的提示和动作反馈。其使用方式与Toast基本相同

1
2
3
4
5
6
7
8
9
10
Snackbar.make(view, "Snackbar comes out", Snackbar.LENGTH_LONG)
.setAction("Action", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(
MainActivity.this,
"Toast comes out",
Toast.LENGTH_SHORT).show();
}
}).show();

此处注意传入的第一个 view 是 Snackbar 显示的基准元素,Snackbar 会显示在该 view 的底部位置。Action 可以传入多个,每一个都可以配置点击事件。

显示效果:

snackbar

官网API:Snackbar API

TextInputLayout

通常,单独的 EditText 会在用户输入第一个字母之后隐藏hint提示信息,但是现在你可以使用 TextInputLayout 来将 EditText 包裹起来,提示信息会变成一个显示在 EditText 之上的 floating label,这样用户就始终知道他们现在输入的是什么。同时,如果给 EditText 增加监听,还可以给它增加更多的 floating label。

使用方法:

1
2
3
4
5
6
7
8
9
10
<android.support.design.widget.TextInputLayout
android:id="@+id/til_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

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

在代码中监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.til_pwd);
EditText editText = textInputLayout.getEditText();
textInputLayout.setHint("Password");

editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (s.length() > 4) {
textInputLayout.setError("Password error");
textInputLayout.setErrorEnabled(true);
} else {
textInputLayout.setErrorEnabled(false);
}
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
}
});

注意:TextInputLayout 的颜色来自 style 中的 colorAccent 的颜色:

1
<item name="colorAccent">#1743b7</item>

显示效果:

text_input_layout_normal

text_input_layout_error

官网API:TextInputLayout API

Floating Action Button

FloatingActionButton 是一个浮动显示的圆形按钮,Design library 中的 FloatingActionButton 实现了一个默认颜色为主题中 colorAccent 的悬浮操作按钮,like this:

floating_action_button

FloatingActionButton 的使用非常简单,一般将其放入 CoordinatorLayout 中。

1
2
3
4
5
6
7
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_done"/>

通过指定layout_gravity就可以指定它的位置。

同样,你可以通过指定anchor,即显示位置的锚点:

1
2
3
4
5
6
7
8
<android.support.design.widget.FloatingActionButton
android:layout_height="wrap_content"
android:layout_width="wrap_content"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|right|end"
android:src="@android:drawable/ic_done"
android:layout_margin="15dp"
android:clickable="true"/>

除了一般大小的悬浮操作按钮,它还支持 mini size(fabSize="mini")。FloatingActionButton 继承自 ImageView,你可以使用android:src或者 ImageView 的任意方法,比如setImageDrawable()来设置 FloatingActionButton 里面的图标。

官网API:Floating Action Button

TabLayout

TabLayout既实现了固定的选项卡 - view的宽度平均分配,也实现了可滚动的选项卡 - view宽度不固定同时可以横向滚动。选项卡可以在程序中动态添加:

1
2
3
4
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.addTab(tabLayout.newTab().setText("tab1"));
tabLayout.addTab(tabLayout.newTab().setText("tab2"));
tabLayout.addTab(tabLayout.newTab().setText("tab3"));

通常 TabLayout 都会和 ViewPager 配合起来使用:

1
2
3
4
5
mViewPager = (ViewPager) findViewById(R.id.viewpager);
// 设置 ViewPager 的数据等
setupViewPager();
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);

显示效果:

tab_layout

官网API:TabLayout API

NavigationView 主要用于实现滑动显示的导航抽屉,这在 Material Design 中是十分重要的。使用 NavigationView,我们可以这样写导航抽屉了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<android.support.v4.widget.DrawerLayout
android:id="@+id/dl_main_drawer"
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:fitsSystemWindows="true">

<!-- 你的内容布局-->
<include layout="@layout/navigation_content"/>

<android.support.design.widget.NavigationView
android:id="@+id/nv_main_navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/navigation_header"
app:menu="@menu/drawer_view"/>

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

其中最重要的就是这两个属性:app:headerLayoutapp:menu

通过这两个属性,我们可以非常方便的指定导航界面的头布局和菜单布局:

navigation_view

其中最上面的布局就是app:headerLayout所指定的头布局:

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
<?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="200dp"
android:background="?attr/colorPrimaryDark"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark">

<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="16dp"
android:background="@drawable/ic_user"/>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="XuYisheng"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="20sp"/>

</LinearLayout>

而下面的菜单布局,我们可以直接通过 menu 内容自动生成,而不需要我们来指定布局:

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
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_dashboard"
android:title="CC Talk"/>
<item
android:id="@+id/nav_messages"
android:icon="@drawable/ic_event"
android:title="HJ Class"/>
<item
android:id="@+id/nav_friends"
android:icon="@drawable/ic_headset"
android:title="Words"/>
<item
android:id="@+id/nav_discussion"
android:icon="@drawable/ic_forum"
android:title="Big HJ"/>
</group>

<item android:title="Version">
<menu>
<item
android:icon="@drawable/ic_dashboard"
android:title="Android"/>
<item
android:icon="@drawable/ic_dashboard"
android:title="iOS"/>
</menu>
</item>

</menu>

你可以通过设置一个OnNavigationItemSelectedListener,使用其setNavigationItemSelectedListener()来获得元素被选中的回调事件。它可以让你处理选择事件,改变复选框状态,加载新内容,关闭导航菜单,以及其他任何你想做的操作。例如这样:

1
2
3
4
5
6
7
8
9
10
11
12
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
menuItem.setChecked(true);
mDrawerLayout.closeDrawers();
return true;
}
}
});
}

官网API:NavigationView API

AppBarLayout

AppBarLayout 是一个容器,会把所有放在里面的组件一起作为一个 AppBar。

appbar_layout

这里就是把 Toolbar 和 TabLayout 放到了 AppBarLayout 中,让他们当做一个整体作为 AppBar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

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

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

官网API:AppBarLayout API

CoordinatorLayout

CoordinatorLayout 是这次新添加的一个增强型的 FrameLayout。在 CoordinatorLayout 中,我们可以在 FrameLayout 的基础上完成很多新的操作。

Floating View

Material Design 的一个新的特性就是增加了很多可悬浮的 View,像我们前面说的 Floating Action Button。我们可以把 FAB 放在任何地方,只需要通过:

1
android:layout_gravity="end|bottom"

来指定显示的位置。同时,它还提供了layout_anchor来供你设置显示坐标的锚点:

1
app:layout_anchor="@id/appbar"

创建滚动

CoordinatorLayout 可以说是这次 support library 更新的重中之重。它从另一层面去控制子 view 之间触摸事件的布局,Design Library 中的很多控件都利用了它。

一个很好的例子就是当你将 FloatingActionButton 作为一个子 View 添加进 CoordinatorLayout 并且将 CoordinatorLayout 传递给Snackbar.make(),在3.0及其以上的设备上,Snackbar 不会显示在悬浮按钮的上面,而是 FloatingActionButton 利用 CoordinatorLayout 提供的回调方法,在 Snackbar 以动画效果进入的时候自动向上移动让出位置,并且在 Snackbar 动画地消失的时候回到原来的位置,不需要额外的代码。

官方的例子很好的说明了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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">

<! -- Your Scrollable View -->
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
...
app:layout_scrollFlags="scroll|enterAlways">

<android.support.design.widget.TabLayout
...
app:layout_scrollFlags="scroll|enterAlways">
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>

其中,一个可以滚动的组件,例如 RecyclerView、ListView(注意:目前貌似只支持RecyclerView、ListView,如果你用一个ScrollView,是没有效果的)。如果:

  1. 给这个可滚动组件设置了layout_behavior
  2. 给另一个控件设置了layout_scrollFlags

那么,当设置了layout_behavior的控件滑动时,就会触发设置了layout_scrollFlags的控件发生状态的改变。

coordinator_layout

设置的layout_scrollFlags有如下几种选项:

  • scroll: 所有想滚动出屏幕的 view 都需要设置这个 flag,没有设置这个flag的view将被固定在屏幕顶部。
  • enterAlways: 这个 flag 让任意向下的滚动都会导致该view变为可见。
  • enterAlwaysCollapsed: 当你的视图已经设置 minHeight 属性又使用此标志时,你的视图只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
  • exitUntilCollapsed: 向上滚动时收缩 View。

需要注意的是,后面两种模式基本只有在 CollapsingToolbarLayout 才有用,而前面两种模式基本是需要一起使用的,也就是说,这些 flag 的使用场景,基本已经固定了。

例如我们前面例子中的,也就是这种模式:

1
app:layout_scrollFlags="scroll|enterAlways"

PS:所有使用 scroll flag 的 view 都必须定义在没有使用 scroll flag 的 view 的前面,这样才能确保所有的 view 从顶部退出,留下固定的元素。

官网API:CoordinatorLayout

CollapsingToolbarLayout

CollapsingToolbarLayout 提供了一个可以折叠的 Toolbar,这也是 Google+、photos 中的效果。Google 把它做成了一个标准控件,更加方便使用。

这里先看一个例子:

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
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="@dimen/detail_backdrop_height"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">

<ImageView
android:id="@+id/backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_banner"
app:layout_collapseMode="parallax"/>

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

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

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

我们在 CollapsingToolbarLayout 中放置了一个 ImageView 和一个 Toolbar。并把这个 CollapsingToolbarLayout 放到 AppBarLayout 中作为一个整体。在 CollapsingToolbarLayout 中,我们分别设置了 ImageView 和一个 Toolbar 的layout_collapseMode

这里使用了 CollapsingToolbarLayout 的app:layout_collapseMode="pin"来确保 Toolbar 在 view 折叠的时候仍然被固定在屏幕的顶部。当你让 CollapsingToolbarLayout 和 Toolbar 在一起使用的时候,title 会在展开的时候自动变得大些,而在折叠的时候让字体过渡到默认值。必须注意,在这种情况下你必须在 CollapsingToolbarLayout 上调用setTitle(),而不是在 Toolbar 上。

除了固定住 view,你还可以使用app:layout_collapseMode="parallax"(以及使用app:layout_collapseParallaxMultiplier="0.7"来设置视差因子)来实现视差滚动效果(比如 CollapsingToolbarLayout 里面的一个 ImageView),这中情况和 CollapsingToolbarLayout 的app:contentScrim="?attr/colorPrimary"属性一起配合更完美。

在这个例子中,我们同样设置了:

1
app:layout_scrollFlags="scroll|exitUntilCollapsed">

来接收一个:

1
app:layout_behavior="@string/appbar_scrolling_view_behavior">

这样才能产生滚动效果,而通过layout_collapseMode,我们就设置了滚动时内容的变化效果。

collapsing_toolbar_layout

CoordinatorLayout与自定义view

有一件事情必须注意,那就是 CoordinatorLayout 并不知道 FloatingActionButton 或者 AppBarLayout 的内部工作原理,它只是以Coordinator.Behavior的形式提供了额外的 API,该 API 可以使子 View 更好的控制触摸事件与手势以及声明它们之间的依赖,并通过onDependentViewChanged()接收回调。

可以使用CoordinatorLayout.DefaultBehavior(你的View.Behavior.class)注解或者在布局中使用app:layout_behavior="com.example.app.你的View$Behavior"属性来定义view的默认行为。framework让任意view和CoordinatorLayout结合在一起成为了可能。

官方API:CollapsingToolbarLayout

总结

研究了一整天的 Android Design Support Library,感觉还是非常强大的。虽然自定义性不是很强,但已经给开发者提供了很简单方便的 Material Design 的官方实现,也不用集成很多的第三方库了,还是很不错的,推荐大家在自己的项目中使用。

参考

Thanks to 《Android Design Support Library使用详解》

Android Studio常用快捷键

之前直接把 Android Studio 的 keymap 改为了 Eclipse 模式,但毕竟这样不利于 Android Studio 的学习,所以这里也记录一下 Android Studio 的快捷键,便于以后查看。

快捷键

Action Mac OSX Win/Linux
注释代码(//) Cmd + / Ctrl + /
注释代码(/**/) Cmd + Option + / Ctrl + Shift + /
格式化代码 Cmd + Option + L Ctrl + Alt + L
清除无效包引用 Option + Control + O Alt + Ctrl + O
查找 Cmd + F Ctrl + F
查找+替换 Cmd + R Ctrl + R
上下移动代码 Option + Shift + Up/Down Alt + Shift + Up/Down
删除行 Cmd + Delete Ctrl + Y
扩大缩小选中范围 Option + Up/Down Ctrl + W/Ctrl + Shift + W
快捷生成结构体 Cmd + Option + T Ctrl + Alt + T
快捷覆写方法 Cmd + O Ctrl + O
快捷定位到行首/尾 Cmd + Left/Right Ctrl + Left/Right
折叠展开代码块 Cmd + Plus,Minus Ctrl + Plus/Minus
折叠展开全部代码块 Cmd + Shift + Plus,Minus Ctrl + Shift + Plus,Minus
文件方法结构 Cmd + F12 Ctrl + F12
查找调用的位置 Ctrl + Option + H Ctrl + Alt + H
大小写转换 Cmd + Shift + U Ctrl + Shift + U
编译项目 Cmd + F9 Ctrl + F9
运行项目 Ctrl + R Ctrl + R
代码提示 Option + Space Alt + Space

(PS: 以上快捷键没有在 win 平台尝试确认,如有错误可以在评论中反馈)