前言
Android Weekly
相当于是
Android
开发社区的实时通讯录,每周报导
Android
最新讯息,包括新的库、工具和博客等,只要你有
,就可以对其进行订阅,了解更多关于安卓的消息。
Android Weekly
官网:
http://androidweekly.net/
更新日期
更新内容
2015-04-07
Issue#145
中文版发布
1
Issue #146
>
原文链接:
http://androidweekly.net/issues/issue-146
ARTICLES & TUTORIALS
Yahnac:RxJava Firebase&
内容提供
(www.malmstein.com)
大卫
·
冈萨雷斯发布了用于展示材料设计、
RxJava
和
Firebase
的全新黑客新闻客户端。
游戏性能
:
布局限定符
(android-developers.blogspot.com)
本文展示了使用
OpenGL
图形语言的成功实践,这些实例能够优化游戏性能并简化工作流。
仅用于
Android
调试的
Stetho
(littlerobots.nl)
最近
发布了名为
Stetho
的工具,用于在谷歌开发工具中检测安卓应用程序。本文介绍了如何启用调试构建。
自定义颜色跨度
(blog.stylingandroid.com)
Mark Allison
介绍了如何实现跨度,同时展示了使用自定义跨度的简便性。
在
Android Lollipop
上使用
JobScheduler API
(code.tutsplus.com)
在本教程中
,
您将学习如何在安卓
Lollipop
中使用
JobScheduler API
。
JobScheduler API
可以创建后台任务
,
使得满足特定条件时能够在后台执行。
分析清单
:
测量和寻找哪些方面
(cogitas.net)
分析有助于理解难点和用户信息
,
从而专注于消除障碍并为用户提供更好的服务。
MVC / MVP
中的
M -
模型
(medium.com)
本文从另一角度解释了模型。
使用安卓
Wear API
创建
watchface——
第
2
部分
(catinean.com)
在第二篇文章
, Andrei Catinean
展示了在安卓
Wear
应用程序中如何添加设置面板。
安卓性能案例研究
(www.curious-creature.com)
Romain Guy
深入研究了
Android UI
呈现优化。
针对
Jenkins
的谷歌商店安卓出版插件
(wiki.jenkins-ci.org)
去年
Christopher Orr
发布了这款
Jenkins
插件
,
此插件使用存储
API
来发布应用程序。
赞助
免费
Android
应用程序测试
+
优化
(software.intel.com)
针对于基于英特尔处理器的
Android
设备,能够使用免费软件测试服务来测试应用程序。点击查看详情。
设计
视觉设计新手指南
(www.willowtreeapps.com)
本文运用基本原则来构成优秀设计的基础,示例来自作者从事移动用户设计师时的经历。
招聘
安卓讲师
(Anywhere in the US)
Treehouse
是一家教育科技公司。作为
Treehouse
的教师
,
加入我们狂热的教学团队创建世界一流的视频学习材料吧。
高级安卓工程师
——PopJam
(
英国伦敦
)
Mind Candy
寻求优秀安卓工程师一同创建
PopJam, PopJam
是世界上最令人兴奋的儿童社交网络应用程序
!
你将领导整个
Android
团队
,
与设计师和高级管理人员密切合作
,
来创建奇妙的社
交应用程序
!
初级安卓开发人员
(
德国
)
如果你热爱开发,渴望获得进步,请联系我们。成为德国团队的成员吧!我们会为你提供学习成长的机会。
安卓工程师
(
旧金山
,CA)
Storehouse
寻求优秀安卓工程师。你会为项目奠定基础
,
从根本上决策程序工作方式。你会成为团队重要角色来共同塑造公司
,
产品和文化。
库和代码
Bandhook-Kotlin
(github.com)
此项目用于显示使用
Kotlin
编写的复杂项目。
AirMapView
(github.com)
AirMapView
是一个抽象视图
,
使得拥有或没有谷歌商店服务的设备均可使用互动地图。它支持包括谷歌地图
V2
和亚马逊地图
V2
在内的多个本地地图提供商。
引入
Fresco:Android
全新图像库
(code.facebook.com)
当应用程序由于许多大型位图而耗尽内存时会发生什么
?
程序会崩溃。
通过创建称为
Fresco
的库来着手解决这一问题
——
它能够管理图片和相应内存。
工具
颜色主题和字体
(InteliJ IDEA/Android Studio themes and fonts.)
InteliJ IDEA/ Android
工作室主题和字体。
视频
The Making of Falcon Pro 3 by Joaquim Vergès
(realm.io)
Joaquim Vergès
重写
Falcon Pro 3,
同时分享了一些安卓可用开源库,可用于确保用户会为此应用程序付费等等。
Yahnac:RxJava Firebase&
内容提供
你可能会问
Yahnac
是什么
?
是一个
黑客新闻
客户端
,
因为黑客新闻客户端需求远远不够
!
黑客新闻
是一个专注于计算机科学和创业的社会新闻网站,由创业公司孵化器
Y Combinator
负责运行。一般来说
,
可提交内容被定义为
“
可以满足求知欲的任何内容
”
。
不久前
Y Combinator
宣布了黑客新闻
API
。最令人兴奋的消息此
API
使用
Firebase
。
在功能方面
,
Yahnac
允许阅读所有黑客新闻内容
。最重要的是
,
你可以添加书签并长期保持
。
如果你对
Yahnac
感兴趣
,
请继续阅读
!
alttext
图片
1.1
alttext
A confession to make
我喜欢
Loader
,
内容提供商
及其样板。没有必要争论是否是最佳解决方案
,
明显不是。
几个月前
RxJava
在
Novoda
之间逐渐流行起来。像
数字音乐厅
等应用已经开发出来
,
成效显著。
我很庆幸自己拥有
Benjamin Augustin
,
Volker Leck
和
Antonio Bertucci
等出色的同事。
遵循以上原则,我使用
Rx
和
Firebase
把所有框架特定解决方案组合起来。
体系结构
原理很简单
,
数据如何从网络提取的具体方式不用考虑,只需考虑数据库存储路径即可。在网络层使用
Rx
,数据层使用
SQlite
,
UI
层使用
Loaders
即可实现这些需求。
根据目前应用的
API
,可能需要检索到第
501
个
Firebase
实例,以显示
500 top stories
的列表。一个调用将检索
Top Stories
页面所有项目的
ID
,然后需要对这些
ID
的每一个进行另一个调
用,并从新闻项目中检索数据。
Rx
似乎是一个解决这一切问题的完美候选方法,它的积极性方法可以处理所有的递归调用和线程的无缝连接。它还允许操作数据,并提供数据库层所需求的输出。这个输出将是
ContentValues
,其为一种
ContentProvider
需求。
在
UI
层面,当使用
ContentProvider
时,它可以非常容易地决定如何显示内容。
CursorLoader
允许检索含有所需数据的
Cursor
,这将非常有意义!
这里只需再进行一步,而且这是解决
RecyclerView
和
Cursor
之间不兼容问题的方法。在下面的专栏里我会深入探讨到实际的实现细节和问题,如果你有兴趣,敬请关注。
支持不同形式的因素
我对关于该
UI
和它在不同形式的因素中的样式有一些疑虑。在最开始的时候,我考虑使用
Multi Pane
,但是在使用了该应用一段时间后,我感觉有些地方不合理。
alttext
图片
1.2
alttext
在格局上,导览不是非常清晰,所以我决定使用一个类似于
Etsy
的
Staggered Grid
制作比较流行的背景。
alttext
图片
1.3
alttext
同样,智能手机的用户界面将能够适应从一列变为两列的情况。
alttext
图片
1.4
alttext
我们生活在一个物质的世界里
现如今,没有一个不包含有
Material Design principles
的得体的应用程序被推出,在
talking
和
presenting
它之后,我也不可能有不同的观点。
大多数文章都显示在
WebView
中,
WebView
没有预留很多空间用于过渡和花哨的动画显示。该应用程序没有图片,因此不能添加漂亮的
Palette
,这不是最好的情况吗?
然而,材料不仅仅是动画和颜色。也可以很方便地使用像
字体
、
空间
和
波动
等方面,形成一个很大的差异。
alttext
图片
1.5
alttext
另一个特点是,所有的材料应用表明目前是
快速返回模式
。这是一个不错的想法,使用滚动可以让用户享受到更多的内容。
alttext
图片
1.6
alttext
还有更多内容即将到来
该应用程序并没有完成,其中还有几个我想增加的特点,比如能够发布一条消息或回复评论。你还有更好的提议吗?请告诉我!该程序的源码可以从
GitHub
获得,欢迎所有的
PRs
。
游戏性能:规划限定条件
今天,我们要分享一些使用
OpenGL
着色语言(
GLSL
)的最佳实践,可以优化你的游戏性能并简化你的工作流程。具体来说,规划限定条件会使你的代码更具确定性并通过减少你的工
作量来提高性能。
让我们开始于一个简单的顶点着色器,并随着不断进行而改变它。
这个基本的顶点着色器需要位置和纹理坐标、转换位置和输出数据到片段着色器:
attribute vec4 vertexPosition;
attribute vec2 vertexUV;
uniform mat4 matWorldViewProjection;
varying vec2 outTexCoord;
void main()
{
outTexCoord = vertexUV;
gl_Position = matWorldViewProjection * vertexPosition;
}
顶点属性指针
为了在屏幕上画一个网格,你需要创建一个顶点缓冲并用顶点数据填充它,对于本实例,包括位置和纹理坐标数据。
在着色实例中,顶点数据可以布置这样:
struct Vertex
{
Vector4 Position;
Vector2 TexCoords;
};
因此,我们根据如下所述定义顶点着色器属性:
attribute vec4 vertexPosition;
attribute vec2 vertexUV;
为了将顶点数据和着色器属性结合起来,对
glGetAttribLocation
的调用将获得命名属性的句柄。随着对
glVertexAttribPointer
的调用,然后列出了详细的属性格式。
GLint handleVertexPos = glGetAttribLocation( myShaderProgram, "vertexPosition" );
glVertexAttribPointer( handleVertexPos, 4, GL_FLOAT, GL_FALSE, 0, 0 );
GLint handleVertexUV = glGetAttribLocation( myShaderProgram, "vertexUV" );
glVertexAttribPointer( handleVertexUV, 2, GL_FLOAT, GL_FALSE, 0, 0 );
你可以获得多个带有
vertexPosition
属性的着色器,并为每个着色器调用
glGetAttribLocation
会降低性能,但这会增加你的游戏加载时间。
使用规划限定条件,你可以像如下那样改变你的顶点着色器属性声明:
layout(location = 0) in vec4 vertexPosition;
layout(location = 1) in vec2 vertexUV;
为了达成这个目的,你还需要告诉着色器编译器你的着色器是针对
GL ES3.1
版本。这是通过添加一个版本声明实现的:
#version 300 es
让我们看看如何影响着色器,变化部分以粗体标记:
#version 300 es
layout(location = 0) in vec4 vertexPosition;
layout(location = 1) in vec2 vertexUV;
uniform mat4 matWorldViewProjection;
out vec2 outTexCoord;
void main()
{
outTexCoord = vertexUV;
gl_Position = matWorldViewProjection * vertexPosition;
}
应当注意到,我们还从变更到输出改变了
outTexCoord
。从版本
300es
开始就已经无法变更关键词,并需要更换着色器进行工作。
应当注意到,
Vertex Attribute qualifiers
和版本
300es
都支持
OpenGL ES 3.0
。等效的桌面支持
OpenGL 3.3
并使用
330
版本。
现在你知道你的位置属性总是在
0
和你的纹理坐标将在
1
,现在你可以不使用
glGetAttribLocation
而约束你的着色器格式。
const int ATTRIB_POS = 0;
const int ATTRIB_UV = 1;
glVertexAttribPointer( ATTRIB_POS, 4, GL_FLOAT, GL_FALSE, 0, 0 );
glVertexAttribPointer( ATTRIB_UV, 2, GL_FLOAT, GL_FALSE, 0, 0 );
这个简单的变化会导致一个更清洁的通道、简化的代码,并在加载期间保持了性能。
Android
性能扩展
Android Performance Patterns series
仅作为
Android
调试模式工具的
Stetho
最近
发布了一名为
Stetho
的工具,这个工具可以使我们通过
Chrome Developer
工具来检查
Android
应用程序。我发现这非常有用,因为这个工具还可以访问应用程序中的
SQLite
数据库。
很明显,这种类型的工具应包含于
Android
应用程序的调试模式中。这里有一个很好的方法来完成这个工作。
添加依赖
为了确保
Stetho
仅用于调试模式,你可添加一个
debugCompile
(调试编译)的依赖,而不是常常使用到的
compile
(编译)类型。
depencencies {
// your other dependencies here...
debugCompile 'com.facebook.stetho:stetho:1.0.0'
}
在调试模式中初始化
Stetho
现在我们需要在调试模式中使用
Stetho
。如何做呢?
我们可以使用具有强大功能的
Android Gradle
构建系统!
借此添加仅在调试模式中编译的一些源文件,创建一个名为
src/debug/java
的源文件夹。这个文件夹和
src/main/java
相似,但它是用来存放应用程序中的调试变量的。相反,主文件夹存放所有变量共用的源文件。
之后,按照
Stetho
主页
上描述的方式添加一个
Application
应用。
import com.facebook.stetho.Stetho;
public class MyDebugApplication extends MyApplication {
@Override
public void onCreate() {
super.onCreate();
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
.build());
}
}
看清楚这个类是如何从已经有的
MyApplication
.
类进行扩展的。这种方法确实很方便,因为你已经在应用程序中使用一个应用进行其他类型的初始化了。如果你还没有一个应用
(
application
)可从
android.app.Application
.
继承一个就行了。
激活调试应用
最后一步是确保当前应用程序的调试版本使用的是
MyDebugApplication
类。此外,在这里我们用
Gradle
搭建系统来实现这个步骤。那就是在
AndroidManifest.xml
文件添加至
src/debug
文件夹中。
<manifest
package="com.mycompany"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
tools:replace="android:name"
android:name=".MyDebugApplication"/>
</manifest>
这个
AndroidManifest.xml
文件将并入到
src/main
文件夹中的主
AndroidManifest.xml
文件里,并且会替换
标签中的
android:name
属性,其原因是我们特别使用了
tools:replace
属性。真是太棒了!
现在如果我们运行应用程序的调试模式变量,
Stetho
就将激活。如果我们转为发布变量,此变量将无迹可寻且
Stetho
也不会激活。发布版本没有出现偶然故障,程序开发人员的工作
完成的很好。
结论
使用
Android Gradle
构建系统可以很容易在应用程序中添加一些调试功能。这个技术不仅可以用在
Stetho
上,还可以用在那些仅仅希望在调试模式中添加的类库或者工具的工作中。
自定义颜色范围
《
Styling Android
》的忠实读者应该知道我是一个
Spans
的骨灰级粉丝,知道我认为掌握好
Span
对于发挥
TextView
的最大功效来说至关重要。人们说
Span
有时只做一些简单的工作,比如
仅仅改变文本的颜色,这样看起来有一点尴尬。这本文中,我们将展望如何利用我们自己的
Span
,并且看一下使用自定义
Span
可进行如何的简化。
在我们开始前,我要先简短地介绍一下。编写本文的灵感来自于一些代码,这些代码使用一个我最不喜欢的类别即
android.text.Html
中执行的文本样式。在这个类别在一个文本字符串
中解析
html
标记并生成相关的
Span
后,其本身就不是问题了。然而,我碰到的问题时是这个类别支持难以维护代码。使用
Html
的代码常常需要进行重写才可以直接利用使用
Span
的功能
以满足做出更改的需要。
一个示例可能会帮助阐明这个问题。想一想
TextView
文本的一部分需要在剩下的字符串中采用不同颜色的情况。这个任务可以使用
Html
类别来完成:
Spanned text = Html.fromHtml("One word should be <font color='16711680'>red</font>");
textView.setText(text);
我首先遇到的麻烦是它看起来很不臃肿。
Java
代码中字符串的嵌入式
HTML
标记是一个看起来简单的代码,这是因为有很多可行的工具比其更有效。虽然字符串可以移入一个字符串资
源中,但如果在运行时需要对实际颜色值进行确定,这种情况则通常不会发生。
我们最后必须执行甚至更为臃肿的字符串连接,或我们需要使用格式化的字符串资源来生成适当的
HTML
字符串,之后我们使用
Html
类别对其进行解析。还有一个更大的麻烦就是
Html
的
fromHtml()
方法可使我们在标记内指定颜色资源,包括颜色状态列表。我们只能在
‘android’
包中
找到它,所以我们无法介入系统资源,并不能使用我们自己的资源。
如果我们在上层
TextView
更改的情况下需要改变文本颜色,这就会给我们带来问题。
我已经对一个
Html
为什么不好的用例进行了阐述,那么
应
如何解决这个问题呢?
使用
Span
来更改文本的通常的方法是使用
TextAppearanceSpan
,但是这常常需要我们指定其他的因
素,比如文本外观或字体等。
在上述列子中,我们仅关注了改变一部分字符串字体颜色的问题,所以大可不必使用
TextAppearanceSpan
。其他的选择还有
ForegroundColorSpan
,但这个
方法同样存在着不支持颜色状态列表的问题。
但是我们可以轻松地创建我们自己的自定义
Span
,它可以准确地完成我们需要做的工作(而且我们可以在此之后在整个应用程序中再次使用这个自定义
Span
)。
让我们首先建立一个简单的自定义
Span,
其将仅仅改变文本的颜色。我们在这里可以改用
ForegroundColorSpan
(而且我肯定会使用生产代码),但是我选择创建一个自定义的代码,仅
此用来帮助解释如何编写我们自己的
Span
:
class StaticColourSpan extends CharacterStyle {
private final int colour;
public StaticColourSpan(int colour) {
super();
this.colour = colour;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(colour);
}
}
很简单吧?
构造函数使用了一个颜色值,因为我们扩展了
CharacterStyle
,所以我们需要使用
updateDrawState()
。这种方法将在文本的
onDraw()
前调用,并允许我们调整
Paint
绘画对象,
以此给文本上色。所以,我们所有需要做的是设置绘画对象的颜色,之后就全部搞定了。
现在一些人可能已经意识到这种方法并不能解决
TextView
状态变化下文本更改颜色用例的问题,但我们可以创建另一种可以精确完成这个工作的
Span
。
class ColourStateListSpan extends CharacterStyle {
private final ColorStateList colorStateList;
public ColourStateListSpan(ColorStateList colorStateList) {
super();
this.colorStateList = colorStateList;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(colorStateList.getColorForState(tp.drawableState, 0));
}
}
这是非常相似的,差异是构造函数采用了
ColorStateList
而不是原始的颜色值,而且在
updateDrawSate()
,我们在
ColorStateList
(颜色状态列表)中查找了合适的颜色,这些颜色依赖我们
从
TextPaint
对象获得的状态。
updateDrawState()
将在
TextView
刷新后调出,所以我们在此时可获知该控制状态。
下一个考虑的事情就是我们真的不希望加载
ColorStateList
对象将其调出。
但是我们可以轻松地创建一个工厂方法,这个方法将采取资源标识符并依靠资源类型加载适合的
Span
。
public abstract class TextColourSpan extends CharacterStyle {
public static TextColourSpan newInstance(Context context, int resourceId) {
Resources resources = context.getResources();
ColorStateList colorStateList = resources.getColorStateList(resourceId);
if (colorStateList != null) {
return new ColourStateListSpan(colorStateList);
}
int colour = resources.getColor(resourceId);
if (colour >= 0) {
return new StaticColourSpan(colour);
}
return null;
}
}
如果我们改变
StaticColourSpan
和
ColourStateListSpan
类别,从而扩展基础类别,而不是直接扩展
CharacterStyle
,那么我们有了一个多态的
newInstance()
方法,这个方法将依靠加载资源
的类型而返回合适的对象。
最后一件值得考虑的事就是如何确定
Span
应用在字符串的范围。字符串模式匹配的一个显而易见的选择是使用正则表达式,研究一个实用类别是如何为我们完成这个工作的。
public final class SpanUtils {
private SpanUtils() {
}
public static CharSequence createSpannable(Context context, int stringId, Pattern pattern, CharacterStyle... styles) {
String string = context.getString(stringId);
return createSpannable(string, pattern, styles);
}
public static CharSequence createSpannable(CharSequence source, Pattern pattern, CharacterStyle... styles) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
Matcher matcher = pattern.matcher(source);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
applyStylesToSpannable(spannableStringBuilder, start, end, styles);
}
return spannableStringBuilder;
}
private static SpannableStringBuilder applyStylesToSpannable(SpannableStringBuilder source, int start, int end, CharacterStyle... styles) {
for (CharacterStyle style : styles) {
source.setSpan(CharacterStyle.wrap(style), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
return source;
}
}
我们现在可以使用字符串将其调出,在出现匹配时应用我们希望匹配的正则表达式和
Span
对象列表。
这种方法可轻松地随时进行应用,而且比
Html
实施机制更为简洁。然而,可以肯
定的是其在理解和维护上更为轻松。而且这个方法用起来很有效
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CharacterStyle redText = TextColourSpan.newInstance(this, R.color.bright_red);
CharacterStyle changeText = TextColourSpan.newInstance(this, R.color.pressable_string);
Pattern redPattern = Pattern.compile(getString(R.string.simple_string_pattern));
Pattern changePattern = Pattern.compile(getString(R.string.pressable_string_pattern));
final TextView text2 = (TextView) findViewById(R.id.text2);
final TextView text3 = (TextView) findViewById(R.id.text3);
formatUsingSpans(text2, R.string.simple_string, redPattern, redText);
formatUsingSpans(text3, R.string.pressable_string, redPattern, redText);
formatUsingSpans(text3, changePattern, changeText);
}
private void formatUsingSpans(TextView textView, int stringId, Pattern pattern, CharacterStyle... styles) {
CharSequence text = SpanUtils.createSpannable(this, stringId, pattern, styles);
textView.setText(text);
}
}
附带的源文件还有一些进一步的例子,指出这种技术是如何应用到这几个简单类别的。
所以,总而言之:如果我承继了你使用
Html
走了捷径编写的代码,那么我将把你找出来并报仇雪恨。
然而,更有可能发生的事你将必须维护这个代码,而且最后自怨自艾。
不要使用
Html
类别来创建技术债务,应该多用点心使用
Spans
来正确地完成任务。
本文中的源代码在
此
可见。
在
Android Lollipop
上使用
JobScheduler API
在本教程中,您将学习如何使用
JobScheduler
API
适用于
Android Lollipop
。当满足一定的条件时,该
JobScheduler
API
允许开发者创建后台执行的工作。
介绍
当使用
Android
进行工作时,会遇到这样的情况
——
你会想在将来的某个时间或在一定条件下运行任务,例如当一个设备接入电源或连接到
Wi-Fi
网络。值得庆幸的是有
API21
,因为
Android Lollipop
而被大多数人所知,谷歌已经提供了被称为
JobScheduler
API
的新组件来处理这样的情况。
当一组预定义的条件得到满足时,
JobScheduler
API
的应用程序执行一项操作。不像
AlarmManager
类,该时间测定时不准确的。此外,该
JobScheduler
API
能够一同批处理各种
工作。这允许应用程序执行特定的任务,同时考虑设备的电池在定时控制上的成本。
在这篇文章中,通过使用
JobScheduler
API
和
jobservice
类在一个
Android
应用程序中运行一个简单的后台任务,您将学习更多关于
JobScheduler
API
和
jobservice
类方面的知
识。在本教程中的代码可以在
GitHub
上获得。
1
、创建工作服务
开始,你会想使用最小所需的
API 21
创建一个新的
Android
项目,因为在
Android
的最新版本中已经增加了
JobScheduler
API
,在写作的时候,
它没有通过支持库向后兼容。
假设你使用的是
Android Studio
,在你点击完新项目按钮后,你会看到一个基本的
“Hello World”
应用。你要进行这个项目的第一步是创建一个新的
Java
类。为了让事情简单化,命名它为
JobSchedulerService
,并扩展
jobservice
类,这就需要创建两个方法:
onStartJob(JobParameters params)
和
onStopJob(JobParameters params)
。
public class JobSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
当你开始你的任务时,
onstartjob
(
jobparameters params
)
是你必须使用的方法,因为它是系统用来触发已经安排的工作的。正如你可以看到,该方法返回一个布尔值。如果返回
值是
false
,该系统假定任何任务运行不需要很长时间并且到方法返回时已经完成。如果返回值是
true
,那么系统假设任务是需要一些时间并且负担落到你(开发者)的身上,当给
定的任务完成时通过调用
jobFinished(JobParameters params, boolean needsRescheduled)
告知系统。
当收到取消请求时,
onStopJob(JobParameters params)
是系统用来取消挂起的任务的。重要的是要注意到,如果
onStartJob(JobParameters params)
返回
false
,当取消请求
被接收时,该系统假定没有目前运行的工作。换句话说,它根本就不调用
onStopJob(JobParameters params)
。
有一点要注意的是,工作服务在你的应用程序的主线程上运行。这意味着,你必须使用另一个线程处理程序,或运行时间更长的任务异步任务以不阻塞主线程。由于多线程技术超出本
教程的范围,让我们保持简单和实现处理程序来运行在
JobSchedulerService
类中的任务。
private Handler mJobHandler = new Handler( new Handler.Callback() {
@Override
public boolean handleMessage( Message msg ) {
Toast.makeText( getApplicationContext(),
"JobService task running", Toast.LENGTH_SHORT )
.show();
jobFinished( (JobParameters) msg.obj, false );
return true;
}
} );
在处理程序中,你执行
handleMessage(Message msg)
方法(
Handler
实例的一部分),并使用它运行你的任务的逻辑。在此情况下,让事情简单化,从应用程序发送一个
Toast
信
息,虽然这是在那里你可以像同步数据一样给出你的逻辑。
当任务完成时,你需要调用
jobFinished(JobParameters params, boolean needsRescheduled)
让系统知道你完成了那项任务,它可以开始排队接下来的操作。如果你不这样做,
你的工作将只运行一次,你的应用程序将不被允许执行额外的工作。
在
onStartJob(JobParameters params)
方法中,
jobFinished(JobParameters params, boolean needsRescheduled)
的两个参数值是
JobParameters
传递到
JobService
类,一个布尔
值让系统知道是否需要根据工作的最初要求重新编排工作。这个布尔值对于理解是非常有用的,因为它帮助你了解如何处理由于其他问题(如一个失败的网络电话)而导致你的任务无
法完成的情况。
随着
Handler
程序实例的创建,你可以继续并开始执行
onStartJob(JobParameters params)
和
onStopJob(JobParameters params)
方法来控制你的任务。你会发现,在下面的代码片
段中,
onStartJob(JobParameters params)
方法的返回值为
true
。这是因为,你要使用一个
Handler
程序实例来控制你的操作,这意味着相比
onStartJob(JobParameters params)
方法它可能需要更长的时间来完成。通过返回
true
,你让应用程序知道,你将手动调用
jobFinished(JobParameters params, boolean needsRescheduled)
方法。你也会注意到,
数字
1
被传递到
Handler
示例。这是相关引用工作中你将用到的标识符。
@Override
public boolean onStartJob(JobParameters params) {
mJobHandler.sendMessage( Message.obtain( mJobHandler, 1, params ) );
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
mJobHandler.removeMessages( 1 );
return false;
}
一旦你使用
Java
部分的
JobSchedulerService
类完成,你需要研究
AndroidManifest.xml
,并增加服务节点,因此,你的应用程序有权限绑定和使用
JobService
类。
<service android:name=".JobSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE" />
2
、创建工作计划
随着
JobSchedulerService
类完成,我们可以开始考虑你的应用程序将如何与
JobScheduler API
相互作用。你需要做的第一件事就是创建一个
JobScheduler
对象,在示例代码中调
用
mJobScheduler
,通过获得一个系统服务的实例
JOB_SCHEDULER_SERVICE
来初始化它。在示例应用程序中,这是在
MainActivity
类中完成的。
mJobScheduler = (JobScheduler)
getSystemService( Context.JOB_SCHEDULER_SERVICE );
当你想创建你的计划任务时,你可以使用来
JobInfo.Builder
创建一个
JobInfo
对象,字符指针传递到你的服务中。为了创建一个
JobInfo
对象,
JobInfo.Builder
接受两个参数。
第一个参数是你将运行工作的标识符,第二个参数是你将与
JobScheduler API
.
一同使用的服务的组件名称。
JobInfo.Builder builder = new JobInfo.Builder( 1,
new ComponentName( getPackageName(),
JobSchedulerService.class.getName() ) );
当你的工作将执行时,这个编辑器可以让你设置许多不同的控制选项。下面的代码片段显示了你如何设置你的任务每三秒定期运行一次。
builder.setPeriodic( 3000 );
其他方法包括:
•
setMinimumLatency(long minLatencyMillis)
:这会使你的工作不启动直到规定的毫秒数已经过去了。这是与
setPeriodic(long time)
不兼容的,并且如果同时使用这两个函数将
会导致抛出异常。
•
setOverrideDeadline(long maxExecutionDelayMillis)
:这将设置你的工作期限。即使是无法满足其他要求,你的任务将约在规定的时间已经过去时开始执行。类似于
setMinimumLatency(long time)
,这个函数是与
setPeriodic(long time)
互相排斥的,并且如果同时使用这两个函数,将会导致抛出异常。
setPersisted(boolean isPersisted)
:这个函数告诉系统,在设备重新启动后,你的任务是否应该继续存在。
•
setRequiredNetworkType(int networkType)
:这个函数会告诉你工作,只有在设备处于一种特定的网络中时,它才启动。它的默认值是
JobInfo.NETWORK_TYPE_NONE
,这就
意味着,无论是否有网络连接,该任务均可以运行。另外两个可用的类型是
JobInfo.NETWORK_TYPE_ANY
,这需要某种类型的网络连接可用,工作才可以运行;以及
JobInfo.NETWORK_TYPE_UNMETERED
,这就要求设备在非蜂窝网络中。
•
setRequiresCharging(boolean requiresCharging)
:使用这个函数会告诉你的应用程序,除非设备开始充电,否则工作不会启动。
•
setRequiresDeviceIdle(boolean requiresDeviceIdle)
:这会告知你的工作不会启动,除非用户不使用他们的设备,并且他们已经有一段时间没有使用它。
重要的是要注意到,
setRequiredNetworkType(int networkType)
、
setRequiresCharging(boolean requireCharging)
和
setRequiresDeviceIdle(boolean requireIdle)
可能
会导致你的工作永远不启动,除非
setOverrideDeadline(long time)
还设置允许即使不符合条件的情况下,你的工作也可以运行。一旦规定优先条件,你可以创建
JobInfo
对象,并
发送它到你的
JobScheduler
对象,如下所示。
if( mJobScheduler.schedule( builder.build() ) <= 0 ) {
//If something goes wrong
}
你会注意到,
schedule
操作返回一个整数。如果
schedule
失败,它将返回一个为零或更少的值,对应于一个错误代码。否则,它将返回在
JobInfo.Builder
中定义的作业标识符。
如果你的应用程序需要你停止特定或所有工作,你可以通过对
JobScheduler
对象调用
cancel(int jobId)
或
cancelAll()
实现。
mJobScheduler.cancelAll();
你现在应该能够使用
JobScheduler API
以及你自己的应用程序进行批处理工作和运行后台操作。
结论
在这篇文章中,你已经学会了如何实现一个子类,使用一个
Handler
对象来运行你的应用程序的后台任务。你也学会了如何使用
JobInfo.Builder
来设置你的服务应该运行时的要求。通过
这些,你应该能够在留意功率消耗的同时,提高你自己的应用程序操作。
分析清单
:
测量和寻找哪些方面
当你发布一个应用程序或更新时
,
你经常希望获得新用户和
/
或更多的收益
。
Google Play Developer Console
一直为你追踪这些指标,但无论你是看到你喜欢的数字还是不喜欢的数字,但是你仍
然不知道是什么驱使的他们。
Analytics
,一般情况下,我使用它收集使用和错误数据,
它能帮助你了解痛点和你的应用程序的用户
,让您专注于清除障碍和更好地服务于用户的需求。反过来,这也会提高用户满意
度,产生更高的使用率和新用户,以及每个用户的更高收入。
特别是,
Google Analytics
配备了一个
Android SDK
,网络接口已经被许多企业用户所熟知,使它成为一个
实现分析策略的强大工具
。
在
Android
开发者网站上有一节专门讲述了它的
特点
。这是一款非常强大的工具,它可以给你关于你的用户的
“
开箱即用的
”
数据。让我们先暂时撇开实施细节,先关注如何衡量和寻找
……
识别功能问题
•
崩溃
——
如果你没有存在这个问题,定期在
Google Play Developer Console
上检查异常和
ANRS
报告。
•
捕捉异常
——
有一些异常是预期要发生的,但是你应该了解它们,因为可能会在你的应用程序中显示一个潜在的问题。
•
服务器通信
——
对于提供一个快速,平稳,高效的应用程序,背景同步是至关重要的。你调用服务器需要多久呢?你的调用以
IO
异常结束或响应时间超过
10
秒的情况占多数比例
呢?你的用户在使用
WiFi
,
4G
,
3G
和
2G
的比例有多大呢?关系到本地缓存和服务器同步发生具体异常(例如
SQLExceptions
)的频率有多少呢?
•
定位
、
定位
、
定位
——
对于你捕捉到的异常,他们是否是特别针对对于一个给定的时区和语言而发生?
Android
是当今世界上最流行的移动平台,所以你不能假设你的用户生活都
处在你所在的国家,说着你所说的语言。
•
搜索
——
用户是根据你所期望的方式搜索吗?在他们查询过程中,他们做出错误的拼写时,适当的建议可以减少错误拼写吗?
识别
UI
问题
至少,你应该追踪你的用户的交互活动,他们在每个画面花费多长时间,以及每个活动之间的流程,以确定以下问题。
•
画面
UI
层次结构
——
你
ActionBar
真的需要这些图标吗?你是否应该或高或低地移动屏幕上的一个按钮(特别是需要较长滚动的屏幕)?
•
UX
流程
——
如果很多用户需要经过屏幕
A -> B -> C -> D
,是否需要为他们设定一个
A -> D
的屏幕过渡方式?此外,你可以考虑具体的追踪来帮助你发现潜在的问题。
•
慢画面初始化
——
你在
onCreate
中加载的一些数据,是否应移出主线程?
•
滚动
——
如果你有很长的屏幕,你知道你的用户需要滚动多少吗?你为你的用户设计的数据加载速度够快吗?
理解你的用户
收集了解
UI
问题的数据也将帮助你了解你的用户。
•
未发现的功能
——
你可以通过着眼于结合用户评论和点击数据实现评估用户未发现的功能。
•
未使用的功能
——
你可以通过着眼于结合点击数据和一个特定的画面时间实现评估未使用的功能。
下一步,根据基本的理解通过
分组划分用户段
,例如重度使用者,中度使用者和罕见的用户。对于每一段,你需要自问的基本问题是:
•
你的用户使用该应用程序有多频繁?
•
他们在哪里花费了时间?
•
最常见的界面流程是什么?
•
是否有某些功能未被发现?
•
是否有某些功能未被使用?
•
您的用户如何与你的推送通知互动?
核心原则是为你的用户创建一个丰富
、
有意义的体验
,
在正确的时间提供正确的信息
。一旦你了解了你的用户,你可以考虑围绕广告、内容和个性化方面设计有针对性的策略。例如,你可以:
•
专注于某一段和你的应用程序广告等。
•
当进行了某些特定的行为时,在你的主屏幕上提供上下文帮助,这是游戏在玩家低等级时经常采用的策略。
•
个性化你的主屏幕,例如,通过根据用户类型适应不同截面的位置。
在应用程序中创建和维护强大分析的技巧
•
写下你的假设
——
用你的详细的路线图,史诗或简单的长期目标以呈现你对你的用户的假设。
•
为每个假设分配风险
——
你的依赖于这一假设的长期目标中有多少是真实存在的?
•
考虑你如何能确定每个假设的有效性
——
开始于你的最危险的假设,考虑从你的应用程序中收集的数据以帮助你确定或消除每个假设
•
从现在开始
,
设身处地为你自己考虑六个月
——
想象一下,到那时,你的应用程序是完全无
bug
,有足够的用户来证明你的投资将产生一轮又一轮的大发展。你的各种选项是什么?你
需要知道哪一个是最好的?
•
部分功能的实现分析
——
由于太容易对功能急于求成,而忘记分析。然而,你怎么才能衡量你的新功能的影响?
•
重复你的分析
——
当你获得对当前用户的相关认知后,毫无疑问你会问自己很多关于他们的问题。就像你重复使用你的应用程序,同时重复你的分析。
用户的隐私数据
是的,收集数据是为了更好地服务于你的用户,但不要忘记你的用户的隐私,所以匿名化你收集到的数据。特别是,请确保你遵守
开发者分发协议
的第
4.3
节。
一些数据,如用户的位置是特别敏感的,所以你可能想把你的数据集分成两组。第一组不需要用户的同意就可以收集,它将使用
匿名的
IP
,并且是一般的事物信息,如一般用户细分、
点击事件和屏幕。第二组需要用户的同意,它不使用匿名的
IPs
,并且可能包括诸如用户位置等信息。
实施建议
•
定义一个接口
——
为了获得最大的灵活性,为你的分析方法创建一个接口,一个一般的实现类(从你的代码中调用),一个实现类用于你所选择使用的提供程序。通过这种方
式,你应该决定改变分析提供程序,你可以简单地为新的提供程序添加一个实现类,并改变你的一般实现类的代码用于替代这个提供程序。
•
为屏幕和事件定义枚举
——
每个枚举分别为画面或事件发送完整的数据到分析服务,使你的应用程序中的分析代码非常简洁和易于维护。
•
开发设置
——
分析也需要被测试,所以,为你的开发构建使用不同的分析
ID
。你一旦在你的应用程序中完成分析,确保你的业务团队能够访问开发分析数据,以确保他们能理解
它。
•
A/B
测试
——
完成这项测试的一种方式是在毫秒系统中对当前时间使用模数函数,以确定用户测试组
ID
,然后将它保存在一个共享的首选字段里。这应该在你的应用程序类
onCreate
方法中完成,仅进行一次。例如,如果你要发送
90
%的用户到
ID1
的组和
10
%的用户到
ID2
组,你会如下编写代码:
```
int userTestingGroupId = System.currentTimeMillis()%10 != 0? 1:2;
###
请记得
- `
亲自做可用性测试
`——
这可以通过你的应用程序的一个原型完成。亲自进行可用性测试是你的第一个调用端口
,
用以识别
UI
问题。
- `
做手工和自动化测试
`——
应用程序测试是你的第一个调用端口
,
用于识别函数问题。
- `
在
Google Play Developer Console
上使用
α/β
发行渠道
`——
在你的所有用户体验它们之前
,
最好能捕捉功能和
UI
问题。你可能会考虑分割你的
alpha/beta
渠道以匹配特定的用户细分
,
例如只
有重度使用者可以成为
alpha
测试者
,
只有中度使用者可以成为
beta
测试者。
## MVC / MVP
中的
M -
模型
你好
,
亲爱的读者
,
希望你也是
Android
开发者。
去年
,
Android Fragments
遇到了很多麻烦
,
越来越多的开发者谈论他们遇到的问题
,
来自
Square
(
一如既往
)
的伙计有一个解决方案
——Flow
和
Mortar
。今天我找到了我之前的评论
“[Advocating
Against Android Fragments](https://corner.squareup.com/2014/10/advocating-against-android-fragments.html)”
。

我找到这篇评论是因为我看了
Android
周报的专栏
:
“[
一项有关
Flow
和
Mortar
的调查
](https://www.bignerdranch.com/blog/an-investigation-into-flow-and-mortar/)”
。
<p>
我不想谈论
[Flow](https://github.com/square/flow)
和
[Mortar](https://github.com/square/mortar)
(
只是读了手册
、
专栏并尝试使用它们
)
。我想大家能注意到一个
`
非常小的事情
`
。
这是来自
Android
周报专栏评论的图片
:

不幸的是
,
很多开发者确实看到这样的
`
模态
`
(
Model
)
层。
`Model
不是
JSON
或
XML
或
SQL`
(
顺便说一下
,
它是查询语言
)
。
JSON
、
XML
等只是数据格式
,
`
而不是
Model`
。它是你的
`entities`
的一个代表。
`Model`
是一个操纵这些实体的层
、
类
、
对象。
>
问
:
在
`
许多
`
应用程序中我们能看到什么
?
>
答
:
`Spaghetti code`
,
当所有的带有
300
多行代码的
“
业务逻辑
”
被置入画面中和
/
或
Fragments
(
碎片
)
或现在被植入
Flow & Mortar
的呈现者。类似于
`RxJava`
的一个流行解决方案将反馈代码
!(
实
际上
,
这比
callbackhelled
代码更好。
)
###
我想说什么呢
?
请创建模态。
####
步骤如下
:
`
例如
:
你需要从你的
API
中获得用户列表
`
。
当你看到
A/F/P
,
它是画面
/ Fragment
(
碎片
)
/
呈现者。
1
、
创建一些用户实体表示
,
类或接口
(
在
Java
中
)
——
用户
2
、
根据要求的数据格式写
(
或使用自动生成的
)
解析器
,
例如
JSON
或
XML
等等
3
、
在你的
A/F/P
(
画面
/
碎片
/
呈现者
)
中写一个
API
调用。
4
、
现在从你的
A/F/P
中
`
删除
`
所有的
API
调用。
5
、
思考步骤
3
中的有关问题。
如果你经常写类似于
API
调用的东西
,
直接在你的
A/F/P
中访问
Database / ContentProvider / SharedPreferences
,
请停止这样做。
####
为什么说在
A/F/P
中写这类代码是不好的习惯
,
这里有很多原因
:
1
、
`
难以测试
`
,
因为你必须实例化
A/ F / P
实例
,
模仿它的状态
,
只是执行一些有用的代码。当然
Android
开发者中很少有人
(
2%
)
的写测试
…
2
、
迟早
`
你会打破一个最重要的规则
:
“
不要写重复的代码
”`
,
因为之后你需要在应用程序的另一个位置做同样的事情。我敢保证。
3
、
当你需要切换到其他
REST
框架
(
Retrofit
?)
、
HTTP
客户端
(
OkHttp
?)
、
数据库
(
Realm?
)
、
Parser
(
Gson?
)
或另一个流行的框架
,
你将不得不改变很多的
A / F / P
类
,
然后测试它们
,
再次测试它们
,
再次测试
……
(
现在你会考虑测试单元
)
。
4
、
`
阅读超过
300
行的代码
,
尤其是在其中变更类是很困难的
`
,
消耗了大量可以用在更有用的事情上的时间
,
比如生活。没有人喜欢意大利面条式的代码
,
但很多人都写这样的代码。
不幸的是
,
即使来自谷歌的人也会用这样不好的习惯方式编写应用程序
,
只是从
d.android.com
或
AOSP
或
Google I/O
应用程序源代码中就可以看到这样的代码样本。
####
解决方案是模态层
从
API
获得用户列表的更好的变体
:
1
、
创建一些用户实体表示
,
类或接口
(
在
Java
中
)
——
用户
2
、
根据要求的数据格式写
(
或使用自动生成的
)
解析器
,
例如
JSON
或
XML
等等。
3
、
`
创建模态类
`
,
例如
UserModel
4
、
在
UserModel
中创建一个方法
,
类似于
“getUsers()”
,
执行
API
调用和解析并返回结果
,
我建议使用
RxJava
的
Observable
模式
,
因为它是非常灵活的
、
漂亮的
,
但是你可以使用回调机制
,
或者其他你想
要使用的。
5
、
现在只需要在你的
A/F/P
中创建一个实例
,
并向它要一个用户列表
!
6
、
祝你今天过得开心
☺
####
为什么它比
A / F / P
中所做的同样事情要好
?
1
、
`
你可以很容易地添加或改变行为
`
:
添加缓冲层
(
例如通过数据库
),
使额外的数据转换
(
当
API
改变时
,
等等
)
或在某一个地方的任何其他额外的行为。
2
、
`
你可以在应用程序中的许多地方很容易地使用相同的模型
`
,
而不需要重复写代码。
3
、
`
你的
A/F/P
将会比较小
`
,
大约平均需要
150–200
的代码。
4
、
`
您可以为模型创建测试单元
`
。
5
、
`
你可以使用
Dependency Injection`
注入模型到你的
A/F/P
中
,
使代码更好
、
更简洁。
6
、
代码整洁。
这就是全部内容
,
希望你的生活会变得更好。
注
:
本专栏可应用于任何类型的开发
,
如客户端
:
iOS
、
Windows Phone
或
Backend
等等。但我想谈论的是
Android
的开发
,
因为它是我们工作中遇到的一个非常现实的问题。
##
使用安卓
Wear API
创建
watchface—
第
2
部分
在该教程的
[
第一部分
](http://catinean.com/2015/03/07/creating-a-watch-face-with-android-wear-api/)
,
已经涉及到创建一个数字的
Android Wear watchface
的基础知识。在这一
部分中
,
将看到如何在
Android Wear
的移动应用中为其
watchface
添加一个面板设置。在学习完本文后
,
你将学会在你的移动设备上从
Android Wear
应用程序内部控制背景颜色
、
时间
、
日期和你的
watchface
的颜色。
为了建立腕表和移动设备之间的通信通道
,
你必须使用
Google Play services
中的
[Wearable Data Layer API](https://developer.android.com/training/wearables/data-
layer/index.html)
。官方文件说明如下
:
>
因为这些
API
是专为手持设备和可穿戴设备之间的通信而设计的
,
这是仅有的一些
API
,
你应该使用它们设置这些设备之间的通信。例如
,
不要尝试打开底层套接字创建一个通信信道。
在可穿戴数据层
API
的帮助下
,
你可以将将多种对象类型发送到你的设备上
:
- [DataItems](https://developers.google.com/android/reference/com/google/android/gms/wearable/DataItem) ——
提供数据存储和自动同步
- [Messages]——
主要用于远程过程调用和单向请求
- [Assets] ——
用于发送二进制数据
对于通信渠道
,
我们将使用
DataApi
来发送从移动设备到数据层的
DataItems
,
而
watchface
将接受任何修改。
###
创建移动设置界面
前面已经看到
,
我们将要使用的
API
是
Google Play services
的一部分
,
所以你必须在你的
mobile/src/main/AndroidManifest.xml
文件中
`<application>`
标签之下指定一个
meta-data
实体
:
...
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
下一步
,
在
mobile/src/main/java/your_package
中创建一个空的
activity
组件
,
这将作为你的
watchface
的
activity
组件设置。我们调用它的
`SimpleWatchFaceConfigurationActivity`
。为了让你的
watchface
认为该
activity
组件为设置
activity
组件
,
你必须在你的
mobile AndroidManifest.xml
文件中指定一个
`<intent-
filter>`
:
...
<activity
android:name="com.catinean.simpleandroidwatchface.SimpleWatchFaceConfigurationActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="com.catinean.simpleandroidwatchface.CONFIG_DIGITAL" />
<category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
正如你看到的
,
你必须为你的
intent filter
和类别指定一个自定义的
< action >
。在我们的例子中
,
动作名称是由封装名称和配置字符串
:
com.catinean.simpleandroidwatchface.CONFIG_DIGITAL
组成的。
在穿戴模块方面
,
你必须在
AndroidManifest.xml
文件中为你之前创建的
`<service>`
实体增加一个额外的
`<meta-data>`
字段
:
...
<service
android:name=".SimpleWatchFaceService"
android:label="@string/app_name"
android:permission="android.permission.BIND_WALLPAPER">
...
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.catinean.simpleandroidwatchface.CONFIG_DIGITAL" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
让我们看一下
,
元数据
`
值
`
如何对应于
activity
组件的动作
`
名称
`
。
现在
,
activity
组件是空的。为了用户能够配置
watchface
背景颜色
、
日期和时间的颜色
,
我们用两个实体填充它。它的布局很简单
,
形成一个带有两个元素的
`LinearLayout`
(
一个用于背景颜色
,
另一个
用于日期和时间的颜色
)
。该
mobile/src/main/res/layout/activity_configuration.xml
文件将会是如下的形式
:
<android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="?attr/actionBarSize"
style="@style/MyActionBarStyle" />
<RelativeLayout
android:id="@+id/configuration_background_colour"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_preference_background"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:textColor="@android:color/black"
android:textSize="18sp"
android:text="@string/background_colour" />
<View
android:id="@+id/configuration_background_colour_preview"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentEnd="true" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/configuration_time_colour"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_preference_background"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:textSize="18sp"
android:textColor="@android:color/black"
android:text="@string/date_and_time_colour" />
<View
android:id="@+id/configuration_date_and_time_colour_preview"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentEnd="true" />
</RelativeLayout>
再回到
activity
组件
,
我们希望在对话框中显示各元素颜色的名称。为此
,
我们将创建一个简单的
ColourChooserDialog
,
其将包含一个简单的颜色列表
,
当用户点击一个
activity
组件的元素时
,
它将进
行显示。
该
`ColourChooserDialog`
将会是这样的
:
package com.catinean.simpleandroidwatchface;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
public class ColourChooserDialog extends DialogFragment {
private static final String ARG_TITLE = "ARG_TITLE";
private Listener colourSelectedListener;
public static ColourChooserDialog newInstance(String dialogTitle) {
Bundle arguments = new Bundle();
arguments.putString(ARG_TITLE, dialogTitle);
ColourChooserDialog dialog = new ColourChooserDialog();
dialog.setArguments(arguments);
return dialog;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
colourSelectedListener = (Listener) activity;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String title = getArguments().getString(ARG_TITLE);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(title)
.setItems(R.array.colors_array, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
String[] colours = getResources().getStringArray(R.array.colors_array);
colourSelectedListener.onColourSelected(colours[which], getTag());
}
});
return builder.create();
}
interface Listener {
void onColourSelected(String colour, String tag);
}
}
你可以看到
,
这是一个简单的
`DialogFragment`
,
其通过一个字符串数组
`R.array.colors_array`
显示
`AlertDialog`
列表
(
获取更多关于对话框的一般信息
,
你可以在这里
[
阅读
]
(http://developer.android.com/guide/topics/ui/dialogs.html)
官方文档
)
。
该字符串数组只是
mobile/src/main/res/values/arrays.xml
的内部资源
:
Black
White
Red
Green
Cyan
Magenta
为了通知所选择的颜色的活动
,
该对话框提供了一个接口。因为将有多个对话框的创建
(
一个用于背景颜色和一个用于日期和时间的颜色
),
而且必须要通过它们的
`tag`
来区分。
`SimpleWatchFaceConfigurationActivity`
会是这样的
:
public class SimpleWatchFaceConfigurationActivity extends ActionBarActivity implements ColourChooserDialog.Listener {
private static final String TAG_BACKGROUND_COLOUR_CHOOSER = "background_chooser";
private static final String TAG_DATE_AND_TIME_COLOUR_CHOOSER = "date_time_chooser";
private View backgroundColourImagePreview;
private View dateAndTimeColourImagePreview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_configuration);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
findViewById(R.id.configuration_background_colour).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ColourChooserDialog.newInstance(getString(R.string.pick_background_colour))
.show(getFragmentManager(), TAG_BACKGROUND_COLOUR_CHOOSER);
}
});
findViewById(R.id.configuration_time_colour).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ColourChooserDialog.newInstance(getString(R.string.pick_date_time_colour))
.show(getFragmentManager(), TAG_DATE_AND_TIME_COLOUR_CHOOSER);
}
});
backgroundColourImagePreview = findViewById(R.id.configuration_background_colour_preview);
dateAndTimeColourImagePreview = findViewById(R.id.configuration_date_and_time_colour_preview);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onColourSelected(String colour, String tag) {
if (TAG_BACKGROUND_COLOUR_CHOOSER.equals(tag)) {
backgroundColourImagePreview.setBackgroundColor(Color.parseColor(colour));
} else {
dateAndTimeColourImagePreview.setBackgroundColor(Color.parseColor(colour));
}
}
}
如果现在你就要在
`
穿戴
`
设备运行
wear
(
穿戴
)
`
模块
`
,
然后是移动设备上的
mobile
(
移动
)
模块
,
导航到
`Android Wear`
应用程序
,
你将能够访问
activity
组件的设置。

###
发送数据到
API
的数据层
现在
,
我们能够捕捉到选定的颜色
,
现在该去了解我们是如何将它发送到
API
的数据层的。为了连接和发送数据
,
你将不得不使用
`GoogleApiClient`
类。在
activity
组件中的
`onCreate()`
,
我们创建了
对象
,
使用
`onStart()`
我们连接到了数据层
,
使用
`onStop()`
断开连接。我们增强的
activity
组件将会是这样的
:
public class SimpleWatchFaceConfigurationActivity extends ActionBarActivity implements ColourChooserDialog.Listener,
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
...
private static final String TAG = "SimpleWatchface";
private GoogleApiClient googleApiClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
googleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Wearable.API)
.build();
...
}
@Override
protected void onStart() {
super.onStart();
googleApiClient.connect();
}
...
@Override
public void onConnected(Bundle bundle) {
Log.d(TAG, "onConnected");
}
@Override
public void onConnectionSuspended(int i) {
Log.e(TAG, "onConnectionSuspended");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "onConnectionFailed");
}
@Override
protected void onStop() {
if (googleApiClient != null && googleApiClient.isConnected()) {
googleApiClient.disconnect();
}
super.onStop();
}
}
为了确保连接
,
我加了一些相应的回调日志。一个同步数据层由两元素组成
:
- `payload`:
实际要发送的数据。
- `path`
:
你要发送数据项的一个唯一的标识符
(
`path
必须以正斜杠开头
`
,
例如
"/simple_watch_face_config"
)
。
我们将在
`PutDataMapRequest`
的帮助下发送数据
,
这为我们提供了一个类似于行为的关键值。
`onColourSelected(String colour, String tag)`
方法将会是这样的
:
@Override public void onColourSelected(String colour, String tag) {
PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/simple_watch_face_config");
if (TAG_BACKGROUND_COLOUR_CHOOSER.equals(tag)) {
backgroundColourImagePreview.setBackgroundColor(Color.parseColor(colour));
putDataMapReq.getDataMap().putString("KEY_BACKGROUND_COLOUR", colour);
} else {
dateAndTimeColourImagePreview.setBackgroundColor(Color.parseColor(colour));
putDataMapReq.getDataMap().putString("KEY_DATE_TIME_COLOUR", colour);
}
PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
Wearable.DataApi.putDataItem(googleApiClient, putDataReq);
}
你会看到在指定的
path
上如何创建一个
`PutDataRequest`
,
每次收到一个颜色
,
就用一个特定的
key
填充映射。最后
,
在
`Wearable.DataApi.putDataItem(googleApiClient, putDataReq)`
方
法的帮助下发送请求。
现在
,
我们能够发送颜色
,
必须在可穿戴模型中对它们进行处理
,
具体是在先前创建的
`SimpleWatchFaceService`
里处理。
###
在
SIMPLEWATCHFACESERVICE
中处理接收到的配置
回到可穿戴模型
,
我们必须处理
`SimpleWatchFaceService`
中之前通过
`SimpleWatchFaceConfigurationActivity`
发来的配置信息。
正如在配置
activity
组件中所做的那样
,
为了与数据层
API
同步
,
必须首先通过一个
`GoogleApiClient `
对象连接到它。当
watchface
是可见时
,
我们将开始连接到
API
;
当
watchface
是不可见的时
,
将端
口与
API
的连接。你的
`SimpleWatchFaceService`
将会是这样
:
public class SimpleWatchFaceService extends CanvasWatchFaceService {
...
private class SimpleEngine extends CanvasWatchFaceService.Engine implements
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
...
private GoogleApiClient googleApiClient;
@Override
public void onCreate(SurfaceHolder holder) {
...
googleApiClient = new GoogleApiClient.Builder(SimpleWatchFaceService.this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
...
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
...
googleApiClient.connect();
} else {
...
releaseGoogleApiClient();
}
...
}
private void releaseGoogleApiClient() {
if (googleApiClient != null && googleApiClient.isConnected()) {
googleApiClient.disconnect();
}
}
...
@Override
public void onConnected(Bundle bundle) {
Log.d(TAG, "connected GoogleAPI");
}
@Override
public void onConnectionSuspended(int i) {
Log.e(TAG, "suspended GoogleAPI");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "connectionFailed GoogleAPI");
}
@Override
public void onDestroy() {
...
releaseGoogleApiClient();
super.onDestroy();
}
}
}
我们在
`SimpleEngine`
的
`onCreate()`
方法中创建了
`GoogleApiClient`
对象
,
当
watchface
是可见的时进行连接
,
当
watchface
是不可见的时释放客户端。
下一步
,
当移动
activity
组件的背景颜色和日期
、
时间颜色的值改变以及每次
watch face
连接到数据层时
,
我们想要得到通知。为了实现这个功能
,
不得不使用
:
- `DataApi.DataListener`——
每次改变数据层的某些数据时
,
将会获得通知。
- `ResultCallback`——
每次
googleApiClient
连接时
,
它将会通知我们。
public class SimpleWatchFaceService extends CanvasWatchFaceService {
...
private class SimpleEngine extends CanvasWatchFaceService.Engine implements
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
...
private GoogleApiClient googleApiClient;
@Override
public void onCreate(SurfaceHolder holder) {
...
googleApiClient = new GoogleApiClient.Builder(SimpleWatchFaceService.this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
...
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
...
googleApiClient.connect();
} else {
...
releaseGoogleApiClient();
}
...
}
private void releaseGoogleApiClient() {
if (googleApiClient != null && googleApiClient.isConnected()) {
Wearable.DataApi.removeListener(googleApiClient, onDataChangedListener);
googleApiClient.disconnect();
}
}
...
@Override
public void onConnected(Bundle bundle) {
Log.d(TAG, "connected GoogleAPI");
Wearable.DataApi.addListener(googleApiClient, onDataChangedListener);
Wearable.DataApi.getDataItems(googleApiClient).setResultCallback(onConnectedResultCallback);
}
private final DataApi.DataListener onDataChangedListener = new DataApi.DataListener() {
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
DataItem item = event.getDataItem();
processConfigurationFor(item);
}
}
dataEvents.release();
invalidateIfNecessary();
}
};
private void processConfigurationFor(DataItem item) {
if ("/simple_watch_face_config".equals(item.getUri().getPath())) {
DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
if (dataMap.containsKey("KEY_BACKGROUND_COLOUR")) {
String backgroundColour = dataMap.getString("KEY_BACKGROUND_COLOUR");
watchFace.updateBackgroundColourTo(Color.parseColor(backgroundColour));
}
if (dataMap.containsKey("KEY_DATE_TIME_COLOUR")) {
String timeColour = dataMap.getString("KEY_DATE_TIME_COLOUR");
watchFace.updateDateAndTimeColourTo(Color.parseColor(timeColour));
}
}
}
private final ResultCallback<DataItemBuffer> onConnectedResultCallback = new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {
for (DataItem item : dataItems) {
processConfigurationFor(item);
}
dataItems.release();
invalidateIfNecessary();
}
};
@Override
public void onConnectionSuspended(int i) {
Log.e(TAG, "suspended GoogleAPI");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "connectionFailed GoogleAPI");
}
@Override
public void onDestroy() {
...
releaseGoogleApiClient();
super.onDestroy();
}
}
}
我们可以看到
,
每次
watch face
可见时
,
连接到数据层
,
一旦连接后
,
增加两个监听器。数据层每次发生变化时
,
`onDataChangedListener`
将得到通知
;
当服务连接时
,
`onConnectedResultCallback`
将得到通知。在这两种情况下
,
我们想处理接收到的
`DataItem - processConfigurationFor(DataItem)`
。在处理
Items
时
,
为了抓到发送的值
,
使用
path
(`/simple_watch_face_config`)
和
关联到每一项的
keys
(
在
`SimpleWatchFaceConfigurationActivity`
中定义了这些
keys
)
。一旦项目被识别
,
传递它们到
`SimpleWatchFace`
以更新颜
色
:
- `updateBackgroundColourTo(int colour)`
:
负责更新背景颜色
- `updateDateAndTimeColourTo(int colour)`
:
负责更新数据和时间的颜色
public class SimpleWatchFace {
...
private static final int DATE_AND_TIME_DEFAULT_COLOUR = Color.WHITE;
private static final int BACKGROUND_DEFAULT_COLOUR = Color.BLACK;
private int backgroundColour = BACKGROUND_DEFAULT_COLOUR;
private int dateAndTimeColour = DATE_AND_TIME_DEFAULT_COLOUR;
public static SimpleWatchFace newInstance(Context context) {
Paint timePaint = new Paint();
timePaint.setColor(DATE_AND_TIME_DEFAULT_COLOUR);
timePaint.setTextSize(context.getResources().getDimension(R.dimen.time_size));
timePaint.setAntiAlias(true);
Paint datePaint = new Paint();
datePaint.setColor(DATE_AND_TIME_DEFAULT_COLOUR);
datePaint.setTextSize(context.getResources().getDimension(R.dimen.date_size));
datePaint.setAntiAlias(true);
Paint backgroundPaint = new Paint();
backgroundPaint.setColor(BACKGROUND_DEFAULT_COLOUR);
return new SimpleWatchFace(timePaint, datePaint, backgroundPaint, new Time());
}
SimpleWatchFace(Paint timePaint, Paint datePaint, Paint backgroundPaint, Time time) {
this.timePaint = timePaint;
this.datePaint = datePaint;
this.backgroundPaint = backgroundPaint;
this.time = time;
}
public void draw(Canvas canvas, Rect bounds) {
time.setToNow();
canvas.drawRect(0, 0, bounds.width(), bounds.height(), backgroundPaint);
...
}
public void updateDateAndTimeColourTo(int colour) {
dateAndTimeColour = colour;
timePaint.setColor(colour);
datePaint.setColor(colour);
}
public void updateBackgroundColourTo(int colour) {
backgroundColour = colour;
backgroundPaint.setColor(colour);
}
...
}
最后剩下的是处理环境模式。在环境模式中
,
会默认设置为黑色和白色的颜色。
@Override public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode); watchFace.setAntiAlias(!inAmbientMode); watchFace.setShowSeconds(!isInAmbientMode());
if (inAmbientMode) {
watchFace.updateBackgroundColourToDefault();
watchFace.updateDateAndTimeColourToDefault();
} else {
watchFace.restoreBackgroundColour();
watchFace.restoreDateAndTimeColour();
}
invalidate();
startTimerIfNecessary();
}
每次进入环境模式我们更新默认的颜色
,
当它存在环境模式
,
我们必须恢复其选择的颜色
:
public class SimpleWatchFace {
...
public void updateBackgroundColourToDefault() {
backgroundPaint.setColor(BACKGROUND_DEFAULT_COLOUR);
}
public void updateDateAndTimeColourToDefault() {
timePaint.setColor(DATE_AND_TIME_DEFAULT_COLOUR);
datePaint.setColor(DATE_AND_TIME_DEFAULT_COLOUR);
}
public void restoreDateAndTimeColour() {
timePaint.setColor(dateAndTimeColour);
datePaint.setColor(dateAndTimeColour);
}
public void restoreBackgroundColour() {
backgroundPaint.setColor(backgroundColour);
}
}
现在你可以开始测试端到端的全部配置。首先在你的腕表上运行可穿戴模块,然后是手机上的移动模块。跳到你手机上的
Android Wear
应用程序,你可以选择安装腕表和访问它的设
置屏幕。从这里你可以改变背景以及日期和时间的颜色。
Android
性能案例追踪研究
两年前,我发表了一篇名为
Android
性能案例研究
的文章,帮助
Andriod
开发者理解哪些工具和技术可被用于识别、跟踪并解决性能问题。
这篇文章专注于一个推特客户端设计
Falcon Pro
,它是由
Joaquim Vergès
开发的。
Joaquim
非常好,他允许我在我的文章中使用他的应用程序,并迅速解决我发现的所有问题。一切进展
都非常顺利,直到
Joaquim
开始从头开始编写
Falcon Pro 3
。前不久
Joaquim
发行自己的新应用,他联系到我,因为他需要我帮助解决影响滚动的性能问题(再一次,我没有访问源代
码)。
Joaquim
使用了所有合适的工具,并能迅速排除那些没有产生问题的方面。例如,他发现过度拉大不是一个产生问题的原因。然而,他能够缩小问题到
ViewPager
的使用方面。他发给我
下面的截图:
alt text
图片
1.7
alt text
Joaquim
使用系统屏幕上的
GPU
分析工具来检测帧率下降。左边的屏幕截图显示了没有
ViewPager
的滚动时间线性能,右边的截图显示了带有一个
ViewPager
的性能(他使用
2014 Moto X
捕捉到这一数据)。产生问题的根本原因似乎很明显。
我的第一个想法是看看
ViewPager
是否不明原因地
滥用硬件层
。观察到的性能问题可能是由于列表滚动的每一帧引起硬件层更新造成的。系统的
硬件层更新调试工具
没有发现任何问
题。我仔细检查了
HierarchyViewer
,令人欣慰的是
ViewPager
的行为是正确的。
然后我准备使用另一个强大的、很少用的,叫做
Tracer for OpenGL
的工具。我
以前的文章
讲述了如何使用这款工具的更多细节。你所需要知道的是,这个工具会收集所有的
UI
工具包发
送到
GPU
的绘图指令。
Android 4.3
及以上版本:不幸的是,当我们在
Android 4.3
以上版本中引入绘图指令的重排和合并后,
Tracer
变得更加难以使用。这是一个非常有用的优化,但它可以防止
Tracer
获取分
组视图指令。你可以通过使用以下指令禁用显示列表的优化来恢复以前版本中绘图指令的行为:
读取
OpenGL
追踪:图中蓝色的指令是
GL
绘制像素到屏幕上。所有其他指令用于传输数据或设置状态,很容易被忽视。每次你点击一个蓝色的指令,
Tracer
将更新
Details
选项卡并在你
点击执行指令后显示你当前的渲染目标的内容。你可以通过一个接一个地点击每一个蓝色的指令重建一帧。这就是我如何使用
Tracer
分析性能问题的过程。通过查看每一帧是如何渲
染的,可以获得应用程序正在执行情况的很多见解。
在我仔细阅读了追踪收集到的
Falcon Pro I
滚动过程之后,我很惊讶地看到一系列块指令
SaveLayer/ComposeLayer
(点击图片看大图):
alt text
图片
1.8
alt text
这些块表明了临时硬件层的创建和组成。这些临时层是由
Canvas.saveLayer()
的不同变体创建的。当满足特定条件时,
UI
工具包使用
Canvas.saveLayer()
绘制
alpha < 1
(见
View.setAlpha()
)的
Views
:
•
getAlpha()
返回
< 1
的值
•
onSetAlpha()
返回
false
•
getLayerType()
返回
LAYER_TYPE_NONE
•
hasOverlappingRendering()
返回
true
Chet
和我在一些介绍中解释了
“
为什么你应该
慎重的使用
alpha
”
。每次
UI
工具包必须使用临时层时,绘图指令会被发送到不同的渲染目标,以及切换渲染目标操作对
GPU
来说消耗很大。
GPU
使用平铺
/
递延架构(
ImaginationTech
的
SGX
、
Qualcomm
的
Adreno
等等)明显地被这种行为所伤害。例如
Nvidia
的直接渲染架构会适用的好一些。由于
Joaquim
和我研究的
Moto X
2014
设备使用了
Qualcomm Adreno GPU
,使用多个临时的硬件层是出现我们所面临性能问题的最可能的根本原因。
因此,主要问题变成:是什么创造了这些临时层?
Tracer
给我们提供了答案。如果你看一下
Tracer
的截图就可以发现,
OpenGL
操作的
SaveLayer
组中唯一的绘图指令渲染的似乎是一个
小渲染目标中的圆(使用工具放大的结果)。现在,让我们来看看应用程序的截图:
alt text
图片
1.9
alt text
你看到顶部这些小圆圈了吗?那是一个
ViewPager
指针,用于显示用户的位置。
Joaquim
使用了第三方程序库(我不记得是哪一个了)来绘制这些指针。有趣的是这个程序库如何绘制
了该指针:当前页是一个白色的圆圈表示,在其他页面似乎是一个灰色的圆圈。我说
“
这似乎是一个灰色的
”
,是因为这些圆圈其实是半透明的白色圆圈。这个程序库为每一个圆圈使用
View
(这本身就是浪费),并调用
setAlpha()
改变它们的颜色。
这里有解决这个问题的几种解决方案:
•
使用一个自定义的
“
不活动的
”
颜色代替设置视图的透明度
•
从
hasOverlappingRendering()
返回
false
,该框架将设置适当的
alpha
为你绘制
•
从
onSetAlpha()
返回
true
,在绘制中设置一个
alpha
,用于绘制
“
灰色
”
圆圈
最简单的解决方法是第二个,但是它仅适用于
API16
以上。如果你必须支持老版本的
Android
,那么使用其他两种解决方案。我相信
Joaquim
会抛弃第三方程序库,改为使用他自己的指
针。
我希望这篇文章能明确指出那些似乎是无辜和无害操作所引起的性能问题。所以请记住:不要做假设,而是要采取措施!
针对
Jenkins
的谷歌商店安卓出版插件
优点
•
上传
APK
文件到
Google Play
•
这包括使用多个
APK
支持的应用程序
•
上传
APK
扩展(
.obb
)文件
•
从现有的应用程序中选择与使用扩展文件,例如补丁发布
•
安排应用程序的
alpha
、
beta
或产品发布
•
这包括将现有的应用程序移动到不同轨道的构建步骤
•
例如,你可以在一份工作上传一个
alpha
,然后后来又另一个工作推动它到
beta
•
生产应用程序的分级部署
•
为各种语言分发发布说明到上传的文件
•
如果由于某些原因配置不好,或上传应用程序失败,改变构建结果
•
每一个配置段都支持变量扩展,允许动态生成的发布说明
•
结合
Google OAuth Plugin
,所以凭证可以一次性地进入全局和工作之间的共享
•
很多
Google Play
账号也是通过这一机制支持
计划即将到来的特点
•
为应用程序上传截图和文本
需要
Jenkins
版本
1.565.1
或以上。
Google Play
发布账号
仅为了初始设置,你必须能够登入拥有
Google Play
发布账号的谷歌帐户。
注意:仅有管理员权限是不够的;你必须是帐户的所有者。
你可以在
Google Play
开发者控制台是通过
Settings → User accounts & rights
查看帐户的所有者是谁。
请注意
•
任何上传的
APK
将由
Google Play
立即发布;它们不会处在遴选或挂起状态
•
上传的应用程序必须已经在
Google Play
中存在;你不能使用
API
上传新的应用程序
•
尽管你只是上传
alpha
或
beta
版本的应用程序
,
你仍然需要为
Jenkins
提供
Manage Production APKs
许可
•
Google Play API
团队具有
“
指出这种型号(
2014
年
8
月)和仍在研究最安全和最有效的方式以解决使用
API
接入问题
”
(自
2015
年
4
月开始)
启动
以前的:设立
Google Play
凭据
下面的这个视频显示了初始安装过程:
https://www.youtube.com/watch?v=txdPSJF94RM
安装插件
通过
Jenkins
插件管理器安装这个插件。
如果进行手动安装,确保
Google OAuth Plugin
及其依赖项已经安装。
创建谷歌服务帐户
为了使自动访问你的
Google Play
帐户,你必须创建一个服务帐户:
1
、作为所有者登录到
Google Play
开发者控制台
2
、选择设置
→API
访问
3
、点击
“
创建新的项目
”
4
、建立新项目后,点击
“
创建服务帐户
”
5
、链接到谷歌开发者控制台
6
、创建一个新的
OAuth
客户端
ID
7
、选择服务帐户类型
8
、要注意到一个
.p12
文件已经下载,可能会命名为
“Google Play Android Developer-xxxxxxxxxxxx.p12”
9
、复制创建服务帐户的电子邮件地址
服务帐户权限分配
1
、返回
Google Play
开发者控制台页面
2
、在对话框上点击
“
完成
”
3
、请注意,服务帐户与
Google Play
发行帐户关联
4
、单击帐户的
“
授予访问权限
”
按钮
5
、至少确保下列权限启用:
•
编辑存储列表、定价和分销
•
管理制作的
APK
•
管理
Alpha
和
Beta APK
•
管理
Alpha
和
Beta
用户
6
、点击
“
添加用户
”
7
、现在你可以登录
Google Play
发行帐户了
添加服务账号到
Jenkins
:
1
、导航到你的
Jenkins
实例
2
、从
Jenkins
的侧边栏选择
“
证书
”
3
、选择一个证书域并点击
“
添加证书
”
4
、从
“
类
”
下拉列表,选择
“
谷歌服务帐户密钥
”
5
、输入证书名称
——
实际值是不重要的
6
、选择
“P12
密钥
”
类型
7
、上传由谷歌开发者控制台下载的
.p12
文件
8
、点击
“OK”
创建证书
为了将应用程序发布到
Google Play
,
Jenkins
需要证书和权限。
Per-job config
上传一个
APK
以下设置过程在这个视频中演示:
https://www.youtube.com/watch?v=iu-bLY9-jkc
1
、创建一个新的自由形式的软件项目
2
、通过你的需求建立步骤,确保你想上传的这个
APK
在建立的工作空间是可用的
3
、编译后添加
“
上传的
Android APK
到
Google Play”
4
、从下拉列表中选择证书名称
-
证书必须属于拥有应用程序上传权限的
Google Play
账户
5
、输入路径和
/
或通配符指向上传的
APK
或多个
APK
•
这可能是一个
Ant
式的
*/-release.apk
,或者一个相对于工作空间根的逗号分隔的文件名列表
6
、选择跟踪到应当配置的
APK
•
如果你使用
Jenkins
配置作品的发布,你可以选择推出的百分比
7
、选择
“
添加语言
”
到将发布的说明以及上传的
APK
•
你可以根据需要添加或多或少的你支持的语言,但是每一种语言都必须已经添加到你的应用程序中,位于
Google Play
开发者控制台的
“
商店列表
”
下。
APK
扩展文件
你可以选择为上传的每个
APK
增加两个
扩展文件
。
扩展文件列表可以以与的应用程序相同的方式指定,但注意,它们必须以
[main|patch].
.
.obb.
格式命名。你可以从在线帮助获取更多细节。
移动现有的
APK
到另一个版本的轨道
如果你已经上传一个应用程序到
alpha
轨道(例如),你可以在以后使用
Jenkins
重新分配该版本为
beta
版或产品发布轨道。
根据工作配置的
“
构建
”
章节,增加了
“
移动
AndroidAPK
到不同发布轨道
”
的设置步骤,和配置新的发布轨道。
你可以通过告诉
Jenkins
哪一个
APK
需要移动或直接输入
APK
版本代码,或通过提供
APK
文件,该插件会为你读取申请
ID
和版本代码。
想要的反馈
如果你是扩展文件的用户,特别是如果你使用多个
APK
情况下,这将有助于了解你如何正常地使用扩展文件,且无论什么样的插件都提供了足够的信息。
你通常会为每个应用程序的
扩展做独立的主
/
补丁文件吗?或者它是更常见的所有
APK
共享相同的扩展文件吗?
目前来说,当试图从现有的应用程序中重新利用扩展文件时(没有明确地指定那些来自于旧
APK
的扩展文件需要与新上传的
APK
一同重新使用),前者的情况下是不合适的。
通过在页面顶部的电子邮件告诉我们!
疑难解答
来自于插件的错误信息(其中许多都直接来自于
Google Play API
)应当一目了然。
如果你获得特定配置工作中遇到了麻烦,请尝试手动上传相同的
APK
到
Google Play
。这样你可能就会看到失败的原因,例如版本代码冲突或类似。
或者,请给我们发送错误报告或包含有相信信息的邮件,包括构建控制台日志输出;在页面顶部的信息框内查看我们的联系信息。
常见问题
如果我想上传多个、不同应用
ID
的
APK
程序(即构建风格),该怎么办?
使用
Android Gradle
构建系统的构建风格特点,它可能是有一个单一
Android
构建产生多个
APK
,每一个
APK
有一个不同的应用
ID
。
例如,你可以有应用程序
ID“com.example.app”
和
“com.example.app.pro”
分别为免费和付费版本。
这些往往是建立在一个单一的
Jenkins
的工作中,有人想知道为什么这个插件将拒绝上传的在一个单一的工作中具有不同应用
ID
的
APK
。
然而,就
Google Play
所考虑的方面而言,这些都是完全独立的应用程序。这是正确的,因此应该在单独的
Jenkins
构建(每一个应用
ID
)上传。
如果您想尝试在一个
Jenkins
构建中上传完全不同的
APK
(比如三个),这就需要用
Google Play API
开放和提交三个独立的
“
编辑会话
”
。如果其中任何一个失败
——
也许是因为一个无效
的
APK
,或由于一个
API
失败(不幸的是,使用
Google Play API
过程中这种情况并不少见)
——
你将使你的
Google Play
帐户处于不一致的状态而结束。您的构建将被标记为失败,但有
一个或多个应用程序实际上已经上传并发布到
Google Play
,所以你就必须手动修复这种情况。此外,你将不能仅进行重新运行构建,因为这将由于已经存在的
APK
而失败。
在这种情况下的最佳方法是有一项工作构建不同的风格(即
APK
使用不同的应用程序
ID
),然后,如果构建成功,这将存档的
APK
和启动多个
“
下游
”Jenkins
构建,这将单独发布的每个
应用程序。
即
“
上传
”
的工作可能是通用的,可以通过参数接收
APK
的信息。
待办事项:
提供更多关于如何设置的信息。
2
Issue #145
>
原文链接:
http://androidweekly.net/issues/issue-145
文章和教程
Android Material
支持库的提示与技巧
(code.hootsuite.com)
这篇文章的重点是帮你的设计增加一些亮点,并且让它更接近
Google Material
设计指南。最好的部分是:这些设计不需要设计师或新的资产。
Android UI
自动化测试
(googletesting.blogspot.com)
这篇文章回顾了
Android UI
测试的四个策略,目的是创建快速、可靠和容易调试的
UI
测试。
RecyclerView FastScroll –
第二部分
(blog.stylingandroid.com)
我们学会了
FastScroller
控制框架。在这一结束篇中,我们将添加触摸和滚动行为。
根据
Material
设计导航制图工具样式
(medium.com)
在这篇文章中,作者讨论了类似于
Google apps
的导航制图工具控制样式。
Android
的模糊视图
(developers.500px.com)
模糊效果可以生动的表达内容分层的含义。它允许用户保留上下文,同时专注于当前的特色内容,即使在模糊表面下以视差方式变换或动态的更改。
附加
Android
工件和
Gradle
的档案
(wiebe-elsinga.com)
当构建
Android
应用程序或库时,常见的做法是将你的工件保存到本地文件储存或回购。
映射与包的神秘关系
(medium.com)
因为把
Maps
放在一个包中比看起来要困难的多。
响应式编程
(gist.github.com)
很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的,特别是它的变体,包括
Rx
,
Bacon.js
,
RAC
等等。
延期的共享元素转换
(3b)
(www.androiddesignpatterns.com)
这篇文章我们通过讨论
Lollipop Transition API
:延期的共享元素转换,来继续深入分析共享元素转换。这是这一系列文章的第四部分,我会通过以下观点展开:
用
Robolectric
进行参数化测试
(www.jayway.com)
最近,作者需要编写一个测试用例,一个操作多次执行,但每次执行要用不同的测试数据。原来
Robolectric
有一个参数化
Robolectric
测试运行器。
欢迎为
Android
和
iOS
嵌入
API
(android-developers.blogspot.com)
为
Android(
和
iOS)
设置
Places APIs
连通了简单的经纬度表示的地理位置之间的差距,以及人们如何与一个已知的位置相关联。
赞助
免费的
Android
应用程序测试
+
优化
(software.intel.com)
发现免费软件测试服务可用来测试您的基于英特尔处理器的
Android
设备创建的应用程序。帮助您的
app
得到最好的应用。点击查看详情。
库和代码
Jackdaw
(github.com)
Jackdaw
是一个注解处理器,它允许简化
Java/Android
开发,防止编写枯燥的代码。
Jackdaw
的灵感来源于
Lombok
项目,但与
Lombok
相比:它在
IDE
上不需要额外的插件,并且它
不修改现有的源代码。
Open-Source-Android-Apps
(github.com)
这是一个开源的
Android
应用程序集合。
新闻
在谷歌市场上创造更好的用户体验
(android-developers.blogspot.com)
谷歌正在为
Google Play
中的应用和游戏引入一种新的基于年龄的评分系统。在应用和游戏发表在
Google Play
之前他们也开始审查这些应用程序,使得能更好地保护社区和提高应用程
序目录。
工具
Victor
(github.com)
有了这个
Gradle
插件,您可以为
SVGs
定义源文件夹,它们将自动的栅格
/
包含在您的构建中,且不会干扰您的源代码。
特别
AnDevCon
,
July 29-31
,
Boston
(www.andevcon.com)
AnDevCon
是构建
Android
应用程序的软件开发人员的最主要的技术会议。从超过
75
个类和教程中选择,有
40
多个顶级参展商互相交流,全部
100%
集中在
Android
开发上。使用比
普遍价格低
200
美元的
ANDROID
代码。
Android Material
支持库:
Electric Boogaloo
的提示与技巧
如果你之前阅读过我
先前的博客文章
和链接的教程,那么你应该有这样一个应用程序,它使用
Material
支持库,并且看起来非常得体。这篇文章的重点是帮你的设计增加一些亮点,并
且让它更接近
Google Material
设计指南。最好的部分是:这些设计不需要设计师或新的资产。
Rr32ynHS1.png
图片
2.1
Rr32ynHS1.png
连锁反应
每个人都喜欢按钮上的以及使一切事物拥有反馈的令人满意的连锁反应。我将向您展示如何添加这个效果。注意连锁反应只会出现在运行
Lollipop
的设备中,而在以前的版本中呈现为
一个静态的高亮区。
按钮
大多数按钮是由几个画板组成的。一般来说有一个按键,正常版本中有用的代码如下:
/drawable/button.xml:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/button_pressed"/>
<item android:drawable="@drawable/button_normal"/>
</selector>
为了得到你的连锁反应,在
drawable-21
上重写你的按钮,在你的正常按钮上添加一个连锁反应效果。使用
?android:colorControlHighlight
可以使你添加的连锁反应与应用程序中
内置的连锁反应颜色一致。
/drawable-21/button.xml:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:drawable="@drawable/button_normal" />
</ripple>
johnybot
图片
2.2
johnybot
如果你不喜欢默认的灰色,你可以指定在你的主题中
colorControlHighlight
的颜色。但是,对于这点我提出警示,因为它会偏离你应用程序的内容,并且几乎没有
设计的应
用程序会这样做。
可点击的视图
如果你有一个视图,想给它添加一个连锁反应,那么应该怎么做呢
?
一种常见的情况是有几个可点击的项目的
LinearLayout
。你可以用
<ripple>
元素创建自己的画板,但是还有一种更
简单的方法。只需要给
?attr/selectableItemBackground
设置背景。
In XML:
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" />
In code:
int[] attrs = new int[]{R.attr.selectableItemBackground};
TypedArray typedArray = getActivity().obtainStyledAttributes(attrs);
int backgroundResource = typedArray.getResourceId(0, 0);
view.setBackgroundResource(backgroundResource);
hootsuite
图片
2.3
hootsuite
如果你想使连锁反应来扩展之前的视图的边界,那么你就可以使用
?attr/selectableItemBackgroundBorderless
。它与
ImageButtons
和部分大视图的较小的
Buttons
能很好的兼
容。
Material
图片
2.4
Material
带有风格的对话框
当
AlertDialogs
和
ProgressDialogs
在
Lollipop
设备中运行时,
AlertDialogs
和
ProgressDialogs
与
material
设计应该自动显示不幸的是
,
除非你手动改变它们,否则它们就会呈现为默认的蓝
绿色。如果你喜欢蓝绿色,那么就这样设置即可
——
但是我们可以很容易地使对话框与应用程序的主题相匹配。
Material
图片
2.5
Material
Material
图片
2.6
Material
全部对话框
想让你的按钮适合你的主题非常简单,只需要创建一个风格,然后将它添加到你的主题中。在
AlertDialog
中的按钮以及
ProgressDialog
中的
ProgressBar
的颜色是由
colorAccent
属性决
定的。
styles.xml:
<style name="AlertDialogCustom" parent="Theme.AppCompat.Light.Dialog">
<item name="colorAccent">@color/primary</item>
</style>
themes.xml:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:alertDialogTheme">@style/AlertDialogCustom</item>
</style>
Material
图片
2.7
Material
Material
图片
2.8
Material
破坏性的对话框
当
Material
设计指南
首次发布时,其中包括一个为破坏性对话框的单独的设计。这个设计有一个红色的按钮,它是用来强调正在执行的操作具有破坏性的。要想实现这一功能,你可以
在你的对话框主题中重写
buttonBarPositiveButtonStyle
属性:
styles.xml:
<style name="AlertDialogCustom.Destructive">
<item name="android:buttonBarPositiveButtonStyle">@style/DestructiveButton</item>
</style>
style name="DestructiveButton"
parent="android:Widget.DeviceDefault.Button.Borderless">
<item name="android:textColor">@color/red</item>
</style>
想要为特定的
AlertDialog
使用这一风格,你需要在
AlertDialog.Builder
构造函数中指定一个主题:
Material
图片
2.9
Material
在每个平台上的
Material
对话框
如果你想让你的对话框拥有跨所有
Android
版本的
Material
外观,你需要自定义对话框风格或库。我不喜欢白费力气做重复工作,所以我建议使用一个可用的库:
•
https://github.com/afollestad/material-dialogs
•
https://github.com/fengdai/AlertDialogPro/
•
https://github.com/drakeet/MaterialDialog
导航制图工具
浏览
Navigation Drawers
的
Material
设计部分,会为你设计
DrawerLayout
提供一个好的指南。它为得到一个基本的
Material
风格的制图工具提供了所需要的图纸填充、文本大小以及颜
色。
文本颜色
指南指示,应为你选定的制图工具项目使用主要颜色或者黑色。你可以检测在代码中哪个项目被选中了或者哪个项目改变了文本颜色,但是还有一种更简单的方式。通过使用
ColorStateList
,就可以指定当文本被选中或没被选中时的颜色。
color/menu_text.xml:
and in your layout file:
<TextView
android:id="@+id/menu_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Text.Title"
android:textColor="@color/menu_selector" />
图标颜色
你也可以改变菜单图标的颜色使之与文本匹配。如果你使用
v-21
或更高的版本,就可以简单的设置
“android:tint”
属性,使它与之前的
ColorStateList
相同。但是,当你使用
Material
支
持库时,你就需要更广泛的方案来解决这个问题。
一个解决方案是每个图像有两个属性,一个是选中,一个是未选中。这是一个不错的解决方案,但是如果你想对图片编辑软件改变颜色或者得到一些使用经验,那么这两个属性都需要
改变。一种编程的方法是通过
ColorFilter
来为你的图标上色:
iconView.setColorFilter(getResources().getColor(R.color.primary_dark));
This
Stack Overflow
向你展示了一种很好的方式来改变图标颜色,它是基于其状态,通过创建
ImageView
的一个子类实现的。你也可以重用你的
ColorStateList
,如使用
v-21 tint
:
Material
图片
2.10
Material
总结
让你的应用程序遵循
的
Material
设计指南基本方针并不是特别困难的。许多事情甚至都不需要设计师。有了这些基础知识,你就可以学习更加复杂的概念,如
animation and
elevation
。
浏览
Hootsuite
的新的
Android
设计,现在在
Play Store
中可用。
Android UI
自动化测试
概述
这篇文章回顾了
Android UI
测试的四个策略,目的是创建快速、可靠和容易调试的
UI
测试。
开始之前,请不要忘记导入的规则:可以单元测试的要单元测试。
Robolectric
和
gradle unit tests support
就是
Android
单元测试框架的很好的例子。
UI
测试,从另一方面来说,是用来
验证你的应用程序能否返回与设备中一系列的用户动作相呼应的正确的
UI
输出。
Espresso
是一个很好的框架,用来在同一进程中运行
UI
操作和验证。更多关于
Espresso
和
UI
Automator
工具,请参阅:
test support libraries
。
Google+
团队实现许多
UI
测试的迭代。接下来我们讨论在每一个
UI
测试的策略中的经验教训。请继续关注带有更多的细节和代码示例的文章。
策略
1
:使用端对端的测试作为
UI
测试
让我们从一些定义开始。
UI
测试确保你的应用程序会返回与设备中一系列的用户动作相呼应的正确的
UI
输出。端对端
(E2E)
测试提出了应用程序的全系统服务,包括所有后端服务器和
客户端应用程序。
E2E
测试将保证数据正确地发送到客户端应用程序
,
保证整个系统正确地运行。
通常,为了使应用程序
UI
功能化,你需要来自后端服务器的数据,所以
U I
测试需要模拟数据,但不一定必需从后端服务器模拟。在许多情况下,
U I
测试与
E2E
测试混淆,这是因为
E2E
测试非常类似于手动测试场景。然而,由于许多变量的存在,如网络薄片,真实服务器的认证,系统的大小等等,使调试
E2E
测试及使
E2E
测试稳定非常困难。
Android UI
图片
2.11
Android UI
当你将
UI
测试用作
E2E
测试时,你将面临以下的问题:
•
非常庞大以及缓慢的测试。
•
由于超时和内存问题导致的高片状率。
•
认证问题
(
如:自动化测试的认证是非常棘手的
)
让我们看看使用以下策略是怎样修复这些问题的。
策略
2
:密封的
UI
测试使用假的服务器
在这个策略中,你避免了使用网络电话和外部依赖,但是你需要提供你的应用程序和驱动
UI
的数据。更新你的应用程序使它与本地服务器而不是外地服务器来通信,并创建一个假的
服务器,它能够为你的应用程序提供数据。接下来,你需要一种机制通来生成你的应用程序所需的数据。这可以通过使用不同的方法来实现,你使用的方法取决于你的系统设计。一种
方法是记录服务器响应,并在假的服务器中回放这些服务器响应。
一旦你将密封的
UI
测试与本地假的服务器相连,那么你也应该进行
server hermetic tests
。这样你把你的
E2E
测试划分为一个服务器端测试,一个客户端测试和一个集成测试,来验证
服务器和客户是否同步
(
关于集成测试更多的细节,请参阅
blog
的后端测试部分
)
。
现在,客户端测试流看起来如下所示:
Android UI
图片
2.12
Android UI
虽然这种方法大大减小了测试规模和片状率,但是你仍然为你的测试保留一个单独的假服务器。调试仍然不容易,因为有两个移动的部分:测试和本地服务器。虽然通过这种方法,测
试稳定性会大大的提高,但是本地服务期会导致一些薄片。
让我们看看这一问题如何改善
…
策略
3
:应用程序的依赖注入设计
为了删除运行在
Android
中假服务器的额外的依赖,你需要在应用程序中使用依赖注入,用真实的模块实现来交换假的。例如
Dagger
,或者如果需要的话,你可以创建你自己的依赖注
入。
这将提高你的应用程序的单元测试和
UI
测试的可测试性。为你提供有模拟依赖关系的能力的测试。在
instrumentation testing
中,测试程序和被测试的应用程序被装载在同一进程中,
所以测试代码运行时访问应用程序的代码。不仅如此,你还可以使用类路径覆盖
(
事实是测试类路径优先于被测试的应用程序
)
来覆盖某一类或在那里注入虚伪。例如,为了使你的测试
密封,你的应用程序应该支持网络实现的注入。在测试过程中,测试在你的应用程序中注入一个假的网络实现,这个假的实现将提供成熟的数据,而不是与后端服务器进行通信。
Android UI
图片
2.13
Android UI
策略
4
:在更小的库中构建应用程序
如果你想将你的应用程序扩展到许多模块和视图中,并计划添加更多的功能,同时保持稳定性和快速构建
/
测试,那么你应该在小组件
/
库中构建应用程序。每个库应该有自己的
UI
资源
和用户依赖关系管理。这种策略不仅使为密封测试而构建的库的模拟依赖关系成为可能,并且能够作为你应用程序的各种组件的实验平台来服务。
一旦你有依赖注入支持的小组件,你就可以为每个组件构建一个测试应用程序。
测试应用程序提供了库的实际
UI
,需要的假数据,以及模拟依赖关系。
Espresso
测试将针对这些测试应用程序运行。这使得在隔离条件下测试更小的库成为可能。
例如,我们可以考虑为登录和应用程序的设置构建更小的库。
Android UI
图片
2.14
Android UI
现在,设置组件测试看起来如下所示:
Android UI
图片
2.15
Android UI
总结
为
Android
中大量应用程序进行
UI
测试是非常具有挑战性的。关于
Google+
团队,这里有一些
UI
测试的经验教训:
1.
不要用
E2E
测试来代替
UI
测试。除了
UI
测试,还可以写入单元测试和集成测试。
2.
可以采用密封测试的方式。
3.
在设计你的应用程序时,使用依赖注入。
4.
在小库
/
模块中构建你的应用程序,并单独进行测试。然后你可以得到一些集成测试来验证组件间的集成是否正确。
5.
组件化的
UI
测试被证明比
E2E
快的多,并且比它稳定
99%+
。快速性和稳定性测试证明大大提高开发人员的生产力。
RecyclerView FastScroll – Part 2
请留言
在
之前的文章
中,我们学会了
FastScroller
控制框架。在这一结束篇中,我们将添加触摸和滚动行为。
Recycler
图片
2.16
Recycler
首先我们需要的是一种内部方法,当由于
FastScroller
的触摸事件或者
用户滚动了
RecyclerView
,滚动的位置变化时,为了设置
bubble
和
handle
的位置,该方法会被调用:
FastScroller.java
public class FastScroller extends LinearLayout {
.
.
.
private void setPosition(float y) {
float position = y / height;
int bubbleHeight = bubble.getHeight();
bubble.setY(getValueInRange(0, height - bubbleHeight, (int) ((height - bubbleHeight) * position)));
int handleHeight = handle.getHeight();
handle.setY(getValueInRange(0, height - handleHeight, (int) ((height - handleHeight) * position)));
}
private int getValueInRange(int min, int max, int value) {
int minimum = Math.max(min, value);
return Math.min(minimum, max);
}
.
.
.
}
这里需要一些数学知识,因为
handle
和
bubble
是不同高度的,且我们需要单独的处理每一个。当滚动的时候,我们想让每一个都有自己的上边缘。
列表中的项目时可见的。
getValueInRange()
是一个实用程序方法,确保
bubble
和
handle
在它们的追踪中。
我们的
FastScroller
控制与
RecyclerView
相关联,所以下一个任务是提供一种机制,使用一个简单的设值函数来实现关联:
FastScroller.java
public class FastScroller extends LinearLayout {
private final ScrollListener scrollListener = new ScrollListener();
public void setRecyclerView(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
recyclerView.setOnScrollListener(scrollListener);
}
private class ScrollListener extends OnScrollListener {
@Override
public void onScrolled(RecyclerView rv, int dx, int dy) {
View firstVisibleView = recyclerView.getChildAt(0);
int firstVisiblePosition = recyclerView.getChildPosition(firstVisibleView);
int visibleRange = recyclerView.getChildCount();
int lastVisiblePosition = firstVisiblePosition + visibleRange;
int itemCount = recyclerView.getAdapter().getItemCount();
int position;
if (firstVisiblePosition == 0) {
position = 0;
} else if (lastVisiblePosition == itemCount - 1) {
position = itemCount - 1;
} else {
position = firstVisiblePosition;
}
float proportion = (float) position / (float) itemCount;
setPosition(height * proportion);
}
}
}
当调用设值函数时,设置一个
OnScrollListener
实例,当用户直接
scroll
RecyclerView
时,该实例会被调用,由此我们可以调整
handle
和
buddle
的位置。为了在列表顶部和底部提供
正确的位置,需要一些逻辑知识。
接下来我们需要看
FastScroller
控制中处理触摸事件。我们希望实现的行为是:当用户在控制中轻触时,
handle
会出现。用户可以上下拖动来改变当前位置。当用户释放时,在
handle
隐藏之前会有一个短暂的延迟。这是通过覆盖
onTouchEvent()
实现的:
FastScroller.java
public class FastScroller extends LinearLayout {
.
.
.
private static final int HANDLE_HIDE_DELAY = 1000;
private static final int TRACK_SNAP_RANGE = 5;
private final HandleHider handleHider = new HandleHider();
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
setPosition(event.getY());
if (currentAnimator != null) {
currentAnimator.cancel();
}
getHandler().removeCallbacks(handleHider);
if (handle.getVisibility() == INVISIBLE) {
showHandle();
}
setRecyclerViewPosition(event.getY());
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
getHandler().postDelayed(handleHider, HANDLE_HIDE_DELAY);
return true;
}
return super.onTouchEvent(event);
}
private class HandleHider implements Runnable {
@Override
public void run() {
hideHandle();
}
}
private void setRecyclerViewPosition(float y) {
if (recyclerView != null) {
int itemCount = recyclerView.getAdapter().getItemCount();
float proportion;
if (bubble.getY() == 0) {
proportion = 0f;
} else if (bubble.getY() + bubble.getHeight() >= height - TRACK_SNAP_RANGE) {
proportion = 1f;
} else {
proportion = y / (float) height;
}
int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount));
recyclerView.scrollToPosition(targetPos);
}
}
.
.
.
}
当接收一个向下或移动的动作时,我们设置当前的位置来与当前的
Y
位置匹配,取消可能运行的动画,取消延迟的处理程序回调
(
关于这点在第二部分中有更多描述
)
。如果
handle
不可
见,那我们调用之前创建的方法使它显现。最后在重调
true
之前,我们设置
RecyclerView
的当前位置来使用触发事件。
当接收一个向上的动作时,在一个短暂的延迟后,我们使用一个
Handler
发布一个延迟动作来隐藏
handle
。
当我们设置
RecyclerView
的位置时,如果我们在底部一定距离内来与底部对齐,或若第一项可见时与顶部对齐,那么我们要明白其逻辑关系,否则,如果在中间的某个位置,那就需要
计算出正确的比例值。
注意在这里我们只使用
scrollToPosition()
,而不用
smoothScrollToPosition()
,所以在
之前的系列
中不会出现问题。
这是我们控制完成的。剩下的就是连接。首先我们将它添加到包含
RecyclerView
的布局:
res/layout/activity_main.xml
<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">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:scrollbars="none"
tools:context=".MainActivity" />
<com.stylingandroid.smoothscrolling.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true" />
</RelativeLayout>
最后我们需要在
RecyclerView
和
FastScroller
之间创建联系:
MainActivity.java
public class MainActivity extends Activity {
.
.
.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.setAdapter(LargeAdapter.newInstance(this));
int duration = getResources().getInteger(R.integer.scroll_duration);
recyclerView.setLayoutManager(new ScrollingLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false, duration));
FastScroller fastScroller = (FastScroller) findViewById(R.id.fastscroller);
fastScroller.setRecyclerView(recyclerView);
}
.
.
.
}
就这样。现在我们可以看到
fas
滚动行为:
最后一点:在联系人应用程序中,
FastScroller
处理包含一个字母表示的列表中的当前位置。这是使用了一个比我们的示例稍微复杂的适配器,但是添加这个不应该是繁琐的工作。也
许这是我们在以后的部分中要讨论的。
可用的源代码
here
。
Mark Allison
.
版权所有。这篇文章最开始出现在
Styling Android
。
这个页面的部分内容是基于
创建和共享的工作修改的。
根据
Material
设计导航制图工具样式
Material
图片
2.17
Material
简介
导航制图工具似乎应该在
这里
介绍。除了一些
criticism
我也喜欢模式,所以我决定在我研究的几个应用程序上实现它。你在这里可以阅读到的仅仅是我对于它感兴趣的想法,希望能够
帮助互相帮助互相学习。
这是三个出版物的第二版。看看第一章和第三章:
•
Material
设计下的导航制图工具选型
•
Material
设计下的导航制图工具行为
(
很快出版
)
你可以从这里浏览关于导航制图工具的
Material
设计指南:
•
导航制图工具模式
•
Material
设计的指标和关键路线
•
Toolbar
方法
初始
制图工具总是热门话题。当
Material
设计概念开始出现时,人们有一些
困惑
,甚至
后来指南产生
,概念也没有完全清晰。
现在有
很棒的库
出现,甚至一些
代码
来查看
•••
但是如果你查看这些,有可能是因为你觉得
代码好玩
。
在这里我将要谈论制图工具的样式。它并没有覆盖全部你可以在指南中看到的样式,只是一些我观点中指出的东西。
准备好了吗?
位置
过去,
Navigation Drawer
和
ActionBar
处于相同层次的视图中。
Material
图片
2.18
Material
随着模式演化,开始有一些不一致。当
Material
设计出现时,可以很清楚的看到这两部分不再处于视图的同一层次中。关于这点有
许多讨论
,
甚至更多
,
但是
没有给出太多有
用的东西
,而现在导航制图工具的位置定义如下:
左侧导航制图工具跨屏幕的整个高度,制图工具处于状态栏下方。所有制图工具下的东西都被幕布遮住变暗。幕布下的内容仍然是可见的。
Material
图片
2.19
Material
注意图片中右侧的制图工具有些不同。
注意:我假定你使用的是
AppCompat
工具栏
。
由于工具栏是另一个视图层次结构,所以就像你处理其他视图一样,只是定位制图工具布局下的工具栏。
如果你在一些
应用程序中看到如下所示的东西,不用担心:
Material
图片
2.20
Material
Google+
照片可能是最后一个制图工具没有在
actionBar/Toolbar
上的
应用程序,我相信下次更新会修复这个问题。
自旋条
你还记得当打开制图工具时,在
actionBar/toolbar
出现的别致的图标动画吗?在
Holo
中,动画并不别致,但是
Material
设计使得动画看起来非常棒。
Material
图片
2.21
Material
我认为它首先出现在
的
Play Store
中,然后多数开发人员和设计师都喜欢它。
但是不久之后,在
动画
上出现了制图工具,如前部分解释的那样。在当时是非常奇怪的,因为它也出现在
的
Material
设计视频和商品推销功能中。
Material
图片
2.22
Material
我记得当迁移到
material
设计时,大多数人表现的像第一件要做的事是
burger/arrow
的事。在第一个
实现中,制图工具不在工具栏的上方,且你也不会看到华丽的动画效果。指
南出现之后,有一段
奇怪
的时刻,
应用程序做的正好与
Material
设计指南告诉我们的相反。而现在我写这篇文章时,
应用程序是遵循指南的,并且我希望不久以后看到
全部应用程序中制图工具都在工具栏的上方。
在我看来,那个设计决定实在是晚了。图标的动画已经默认的由
SDK
释放和激活。
出于某种原因,即使布局之后,制图工具处于一切工具的上方,大多数
应用程序仍然有动画
(
注意:在写这一条目的过程中,
Gmail
和
Inbox
已经停用了
)
,即使你只能偶尔见到
(
如果你注意并且缓慢的移动制图工具就可以看到它
)
。这点让我难以接受,一旦你见到它,也不能视而不见。所以我决定把它关掉。
当你第一次在
DrawerArrowStyle
中看到如下的选项时,看起来非常容易:
item name=”spinBars”>true
它在
Android
开发者中定义如下
:
*
在过渡期间条是否应该旋转。必须是一个布尔值,
“true”
或者
“false”
。
*
问题是它不起作用。如果你将它设置为
“false”
,那么条会一种奇怪的方式旋转。
我发现禁用它的方法是覆盖
onDrawerSlide
方法。看看下面的
Gist linked
。
由于图标动画隐约可见,因此没有理由继续保留它。如果你不留意就不会看见它,如果你留意并且看到了它,却不理解正在发生什么。
资料图片
资料图片是圆形的。得到一个圆形的图像有不同的方法,但我始终记得
Romain Guy
在
这篇文章
中谈到的方法。所以我决定使用
CircleImageView
,它是基于
Romain Guy
的技术的,不
会有错。我还没有检查
Google IO
应用程序上使用的方法
,但是可能值得去看一看。
在
Google Play Movies
和
Google Play Books
中的图像有一个白色的边框。其他
应用程序中没有这个边框。
Google+
和
Hangouts
在工具栏中有资料图片,虽然它也有白色的边
框。
Material
图片
2.23
Material
Material
图片
2.24
Material
注意:
检查一下资料图片的大小
这个资料图片是圆形的,通常没有边框。确保你的带有库的圆形图片是遵循
Romain
推荐的技术得到的。
封面图片
封面图片
(
不同于资料图片
)
,是
account/header
部分
(
这是上一部分,通常你可以在你的账户之间改变,看到你的显示名称,电子邮箱和你的资料图片
)
的背景。
这个区域的文本是白色的,并且为了确保它可见,你可以应用前景或半透明的黑色覆盖封面图片。我尝试了不同的透明度,发现透明度为
40-50%
的黑色是不错的。你不想看不见照
片,也不想使文本不可读。
我所做的是在
FrameLayout
中应用前景。我不知道这是不是最好的方法,希望得到一些反馈。我没有可以切换的账户,对我来说,整个布局
/
部分是可点击的,带有触感反馈或
Lollipop
的连锁反应或全部。确保你使用
centerCrop
的
scaleType
来使它看起来更好。
Material
图片
2.25
Material
如果你查看上面图片,制图工具是可见的,且在状态栏的下方。此刻我写这点时,
应用程序仍然处于迁移到这种模式的过程中。
Gmail, Inbox, Keep, Play Store
和
Hangouts
已经
有这种布局,而其他的应用程序正在进行中。此刻,这只发生在运行在
Lollipop
和以上版本中的应用程序中。
Material
图片
2.26
Material
Material
图片
2.27
Material
尽管当前在
Play Store
的
Google IO
应用程序中,导航制图工具是完全错误的,但是它们有更好的代码,并且为下一版本的发布
(
看起来还需要一段时间
...
从现在开始到几个月后的
Google IO
会议之前可能一直更新
)
做好了准备。
神奇的地方在
Google ScrimInsets layout
中。复制,粘贴,完成。你可以试着做。我觉得
的员工会比我做的更好。看看下面的代码部分。看看要点中的代码,因为它也需要用到
主题
/
样式。
ScrimInsets
布局能否应用于低于
Lollipop
的版本中,我也不清楚。然而我知道将它用于
Kit Kat
中是可能的,但是
不这样做。的确,
“
入侵
”
状态栏和
/
或导航栏不是
Lollipop
下的
事,并且可能这就是其原因。
注意:
查看封面图片的大小
状态栏下的导航制图工具只会在
Lollipop
中出现。入侵状态栏或导航栏不
Lollipop
下的事,因此这可能就是不会发生的原因。
Material
图片
2.28
Material
当谈论到为制图工具中的主要行制订样式时,我们需要为每一行的每一个单元解决
3
个元素
(
背景,图标和文本
)
和
3
种不同的状态
(
默认,选中和按下
)
。并不是所有的东西都按照
说明
来
写,但是我们必须阅读说明,看看
应用程序和其他看起来不错的应用程序,试图找出怎样能使它以
Material
设计的方式看起来不错的方法。
好的,现在让我们看看一些
应用程序来收集一些线索。在接下来的图片中,第一行是默认的状态,第二行是选中的状态,第三行是按下的状态。
Material
图片
2.29
Material
Material
图片
2.30
Material
Material
图片
2.31
Material
Material
图片
2.32
Material
Material
图片
2.33
Material
Material
图片
2.34
Material
Material
图片
2.35
Material
Material
图片
2.36
Material
Material
图片
2.37
Material
Material
图片
2.38
Material
Material
图片
2.39
Material
以上所有图片看起来相似但是它们都不相同。总结:
Material
图片
2.40
Material
Material
图片
2.41
Material
应用程序看上去很一致,但是当你注意细节你会发现此刻就在我写这篇文章时,超过
10
个应用程序在为导航制图工具中可选的行项目制订样式。
•
经验法则
经过一些尝试,以及查看指南和
应用程序,这就是我提出的。
Material
图片
2.42
Material
Material
图片
2.43
Material
如何实现
我真的很想知道其他人是怎样实现的,这是我实现的方法。
首先我为背景使用两个画板。一个用于
Lollipop
的
res/drawable-v21
及以上版本,另一个用于较低版本的
res/drawable
。原因是连锁反应不低于
5.0
。
*
只在
Lollipop
及以上版本中使用连锁反应。低于
5.0
的版本不支持连锁反应,更重要的是,不希望出现连锁反应。
*
你每次按下一行,无论是被选中的还是未被选中的,连锁反应就会出现。所以在
res/drawable-v21
中,你实际上有的是一个选择器,它带有几个包含连锁反应的项目。这是因为当按下
被选中或未被选中的行时,我们想显示相同的连锁反应,但是未被选中的行的背景是白色,而被选中的行的背景是
grey_200
。
在
res/drawable
,你所需的是一个选择器,它不带有取决于布局装态的连锁反应。
图标
在上个月我尝试的是避免同一图标有不同选颜色。所以我想到的是我所有图标都为白色。然后我应用了一个带有想使用的颜色的
tint
,然后你就能得到你的有颜色的图标。这样做的好
处是当你决定改变颜色时,你不需要每次都创建新的图标,
你可以从
得到不同大小和不同颜色的图标
,并且你可以在几秒钟之内为相同的图标设置不同的颜色,以此来找出哪
一个是最好的。如果你为相同的图标使用不同的颜色,你还可以节省空间。如果你需要根据状态
(
按下,选中
...)
决定图标的颜色变化,你只需要在你的
tint
上设置一个
颜色状态列表
。
我是在
自定义视图中以编程的方式扩展
ImageView
,因为颜色状态栏在
Kit Kat
及以下版本中不起作用
(android
:
tint
在颜色下起作用,但是在颜色状态下不起作用
)
。
看看下面联系的要点我是怎么做的。如果你有更好的选择或者发现了错误,请说明。
页眉页脚行为
这里的指导方针是相当灵活的。它基本上由你的设计决定。
页眉部分
(
又名账户部分
)
有时固定,有时会出现一些可滚动。就你的设计而言,在我看来这应该是被固定的。制图工具看起来更好,它是一致的,是获取你的资料的最好方法。
页脚部分
(
即设置和支持
)
可以被固定或者不被固定。如果你看看
应用程序,有些是固定的,有些是在滚动条的底部。如果你不知道它们不能到达制图工具的底部,那么页脚只出
现在主行之后。
Material
图片
2.44
Material
Material
图片
2.45
Material
在我看来
,
固定是最好的方法
,
但你可以做一些例外。例如当你固定页眉时
,
一些条目也就固定了
,
然后在制图工具中有一个可滚动的部分。如果滚动部分的空间很少以至于它看起来有些蠢
笨
(1
或
2
行
),
那么你可能就想获得更多的空间,而取消固定页脚是一个不错的选择。
页眉和页脚应该被固定,除非制图工具结构需要空间来达到良好的外观和行为。
由于制图工具上大量的选项,路径、结构
...
这里没有真正的经验法则。
资源
•
官方
Material
设计图标
•
Material
设计颜色定义
代码
•
Github
上的项目
总结
这是关于制图工具的样式设置。知道你想要什么比做到需要的时间更多。
如果你想知道怎么为制图工具设置样式,看看其他两篇文章。
我希望收到一些评论,反馈或者其他什么都好。我写这篇文章是希望大家互相帮助互相学习。
过得愉快!
Android
的模糊视图
模糊效果
模糊效果可以生动的表达内容分层的含义。它允许用户保留上下文,同时专注于当前的特色内容,即使在模糊表面下以视差方式变换或动态的更改。
在
iOS
中,我们可以通过首先构建一个
UIVisualEffectView
来得到这种模糊:
UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]
然后添加
visualEffectView
到视图层次中,在视图层次中它会动态的模糊它下面的东西。
Android
当前发展状况
当事情并不像
Android
上那么简单时,我们确实看到了模糊效果的很好的例子,如
Yahoo
天气应用程序。根据
Nicholas Pomepuy’s blog post
,然而,这里的模糊是通过缓存背景图像的
pre-render
模糊版本得到的。
虽然这种方法非常有效,但是它并不完全适合我们的需要。
500
像素
的图像通常是重点内容,而不仅仅用于提供背景。那就意味着图像可能会改变很多,变化很快,即使它们在模糊层
下。
Android
应用程序
中的
tour
就是一个很好的例子。在这里,随着用户滑动页面,一排排的图片向相反的方向转变,并淡出,这使得为组合所需的模糊效果而恰当的管理多个预渲染
图像变得非常困难。
Android
的模糊视图
图片
2.46
Android
的模糊视图
试图绘制方法
Tour
需要的是一个模糊视图,它能够动态的实时的模糊它下面的视图。我们最终到达的接口非常简单,就如首次给定模糊视图一个引用那样:
blurringView.setBlurredView(blurredView);
然后每当模糊视图变化时
——
无论由于内容变化
(
如显示一个新的照片
)
,视图转换,或动画的一个步骤,模糊视图都将无效:
blurringView.invalidate();
为了实现模糊视图,我们可以生成
View
类的子类,并覆盖
onDraw()
方法来呈现模糊效果:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Use the blurred view’s draw() method to draw on a private canvas.
mBlurredView.draw(mBlurringCanvas);
// Blur the bitmap backing the private canvas into mBlurredBitmap
blur();
// Draw mBlurredBitmap with transformations on the blurring view’s main canvas.
canvas.save();
canvas.translate(mBlurredView.getX() - getX(), mBlurredView.getY() - getY());
canvas.scale(DOWNSAMPLE_FACTOR, DOWNSAMPLE_FACTOR);
canvas.drawBitmap(mBlurredBitmap, 0, 0, null);
canvas.restore();
}
这里的关键是当模糊视图重绘时,它使用模糊视图的
draw()
方法,它有一个参考,但是会画到一个私有的,
bitmap-backed
画布上:
mBlurredView.draw(mBlurringCanvas);
(
值得注意的是,这种调用另一种视图的
draw()
方法的方式也适用于构建一个
magnifier
或
signature UI
,其中
magnifier
的内容或
signature
的面积是扩大的,而不是模糊的。
)
按照在
Nicholas Pomepuy’s post
中讨论的观点,我们使用二次抽样的组合以及
RenderScript
来快速处理。当我们初始化模糊视图的私有画布
mBlurringCanvas
时,二次抽样的设置就
完成了:
int scaledWidth = mBlurredView.getWidth() / DOWNSAMPLE_FACTOR;
int scaledHeight = mBlurredView.getHeight() / DOWNSAMPLE_FACTOR;
mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
mBlurringCanvas = new Canvas(mBitmapToBlur);
鉴于对
RenderScript
的设置和适当地初始化,用于
onDraw()
中的
blur()
方法非常简单,如下所示:
mBlurInput.copyFrom(mBitmapToBlur);
mBlurScript.setInput(mBlurInput);
mBlurScript.forEach(mBlurOutput);
mBlurOutput.copyTo(mBlurredBitmap);
现在
mBlurredBitmap
已经准备好了,剩下的
onDraw()
方法负责使用适当平移和缩放的方式,把它画入模糊视图的画布中。
实现细节
对于一个完整的实现,我们需要注意几个技术点。首先,我们发现
8
倍的
downsampling
缩放和
15
的模糊半径对我们更有利。适合于你的参数可能会不同。
第二,我们在模糊位图的边缘发现了一些
RenderScript
工件。为了辨识这些,我们将
scaled
的宽度和高度提高到最近的
4
的倍数:
// The rounding-off here is for suppressing RenderScript artifacts at the edge.
scaledWidth = scaledWidth - (scaledWidth % 4) + 4;
scaledHeight = scaledHeight - (scaledHeight % 4) + 4;
第三,为了进一步确保良好的性能,我们创建了两个位图
mBitmapToBlur
,支持私有画布
mBlurringCanvas
,
和
mBlurredBitmap
需求,并且只有当模糊视图的大小发生变化时,才
需要重建它们。同样地,只有在模糊视图的大小发生变化时,我们才创建
RenderScript
的
Allocation
对象
mBlurInput
和
mBlurOutput
。
第四,我们也用
PorterDuff.Mode.OVERLAY
在模糊图像的顶部画一层均匀的,半透明的白色来突出我们的设计需求。
最后,因为
RenderScript
只在
API level 17
及更高的版本上可用,我们需要在
Android
旧版本上完全降低。不幸的是,正如
Nicholas Pomepuy’s post
中提到的,
Java
中的位图模糊解决方
案,虽然适合于预呈现一个缓存副本,但是实时渲染不够快。我们作出的决定是简单地使用一个具有高透明度的半透明视图作为候选。
(2015
年
3
月
23
日更新:通过使用
RenderScript
支持库,可以更低水平的
API
下使用我们的解决方案。以下提到的库和样例已经更新,以反映这一点。感谢
GitHub
用户
panzerdev
指出这一点,并发送了请求。
)
优点和缺点
我们喜欢这个视图绘图方法因为它是实时模糊的,容易使用,它允许模糊视图内容的不可知性,它还允许模糊和模糊视图之间关系的灵活性,最重要的是,它适合我们的需要。
然而
,
这种方法确实期望模糊视图参与适当的坐标变换的模糊视图的下落。相应地
,
在模糊视图不能是模糊视图的子视图
,
否则你会从相互嵌套调用得到一个堆栈溢出。这种限制下一个简
单的原则是确保这个模糊视图是
z
值在它前面的模糊视图的同层。
我们已经注意到的另一个限制是矢量图和文本,如果我们使用默认的位图
downsampling
,它们不能很好的起作用。
库和样例
为了看到我们的解决方案,你可以查看
Android
应用程序
中的
tour
。我们也在
GitHub
中
建立一个小型的开源库,以及详细演示
,
展示了内容变化,动画以及视图转换的使用方法。
Android
的模糊视图
图片
2.47
Android
的模糊视图
附加
Android
工件和
Gradle
的档案
当构建
Android
应用程序或库时,常见的做法是将你的工件保存到本地文件储存或回购。
除了你的
APK
,还有一些你想要
/
需要保存的附加的工件,并且你想要这样做。最常见的一个是
Javadoc
,或许是你的
proguard
生成文件,如映射文件。
我当然希望
Gradle
任务能够处理这个问题。所以让我们看看你可能想用的一些任务。
添加
Javadoc
归档任务
下面将添加任务来为每一个构建类型生成
Javadocs
,并组装成
jar
存档。
android.applicationVariants.all { variant ->
project.task("${variant.name.capitalize()}Javadoc", type: Javadoc) {
destinationDir = new File("$project.buildDir/javadoc/$variant.name")
source = variant.javaCompile.source
ext.androidJar = "${project.android.sdkDirectory}/platforms/${project.android.compileSdkVersion}/android.jar"
classpath = project.files(variant.javaCompile.classpath.files) + project.files(ext.androidJar)
options {
linksOffline("http://d.android.com/reference", "${project.android.sdkDirectory}/docs/reference")
links("http://docs.oracle.com/javase/7/docs/api/");
setMemberLevel(JavadocMemberLevel.PACKAGE)
docEncoding = 'UTF-8'
encoding = 'UTF-8'
charSet = 'UTF-8'
}
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
project.task("generate${variant.name.capitalize()}JavadocJar", type: Jar, dependsOn: "${variant.name.capitalize()}Javadoc") {
classifier 'javadoc'
description = 'Assembles a jar archive containing the generated Javadoc API documentation of $variant.name.'
destinationDir = new File("$project.buildDir/libs/")
exclude '**/BuildConfig.class'
exclude '**/R.class'
from "$project.buildDir/javadoc/$variant.name"
}
}
当添加到
build.gradle
文件时,会从
gradle generateReleaseSourcesJar
命令行生成
Javadoc jar
档案。
添加源存档任务
以下将添加任务来组装一个包含
Java
源的
jar
档案。
android.applicationVariants.all { variant ->
project.task("generate${variant.name.capitalize()}SourcesJar", type: Jar) {
classifier = 'sources'
description = 'Assembles a jar archive containing the main sources of $variant.name..'
destinationDir = new File("$project.buildDir/libs/")
// exclude generated files
exclude '**/BuildConfig.java'
exclude '**/R.java'
from variant.javaCompile.source
}
}
当添加到你的
build.gradle
文件时,会从
gradle generateReleaseSourcesJar
的命令行生成你的源
jar
档案。
添加混淆器归档任务
以下将添加任务来组装一个包含生成混淆文件的
zip
档案。
android.applicationVariants.all { variant ->
project.task("generate${variant.name.capitalize()}ProguardFilesJar", type: Zip) {
classifier 'proguard'
description = 'Assembles a jar archive containing the Proguard files of $variant.name..'
destinationDir = new File("$project.buildDir/libs/")
from "$project.buildDir/outputs/mapping"
}
}
当添加到你的
build.gradle
文件时,会从
gradle generateReleaseProguardFilesJar
的命令行生成混淆
zip
档案。
映射与包的神秘关系
[
这篇文章是在欧金尼奥
@workingkills Marletti
的帮助下完成的。
]
警告
——
这是一个
很长
的帖子。
当边缘情况没有被覆盖
假设你需要
传递一个映射的值作为额外的意图
。这可能不是一个常见的情况,不可否认,但它也可能会发生。它的确发生在
Eugenio
。
如果你正在使用一个哈希映射
,这是映射中最常见的类型,你没有创建一个包含
“
额外
”
信息的自定义类,
那么你是幸运的
。你可以写入:
intent.putExtra("map", myHashMap);
在你接收的活动中,你会得到很好的映射,消除了额外的意图:
HashMap map = (HashMap) getIntent().getSerializableExtra("map");
如果你需要
以额外的意图传递另外一种映射
——
比如说,一个树形映射(或任何自定义实现
),
该怎么办呢?好吧,当你找到它:
TreeMap map = (TreeMap) getIntent().getSerializableExtra("map");
然后,你可以得到:
java.lang.ClassCastException: java.util.HashMap cannot be cast to java.util.TreeMap
是的,一个很好的
ClassCastException
异常
,因为你的映射已经变成
…
一个哈希映射。
我们将看到,为什么我们稍后会使用
getSerializableExtra()
,现在我们足以说,那是因为所有的默认映射的实现是可序列化的并且没有范围过窄的
putExtra()/get*Extra()
可以接受他们。
在我们继续之前,
让我们来了解一下这个过程中所有的参与者。
[
tl:dr;
如果你想要一个解决方案,请直接跳到最后的
“
解决方法
”!
]
包
你们中的很多人都知道(但也许有些人不知道),在
Android
框架下的
所有
IPC
通信
都是
基于
Binders
的概念。并且希望你们很多人都知道,主要的机制是让
数据基于
包
在进程之间
进行编组
。
包是
Android
为
IPC
使用的一个
优化的、非通用的接口机制
。与接口现象相反,你不应该
以任何一种持久性的形式使用包
,因为它没有提供用于处理不同版本的数据。
每当你看到一
个包,说明你正在引擎盖下处理一个包。
添加额外意图?包
在一个片段中设置参数?包
等等
包知道如何处理一大堆
箱外类型,包括原生类型、字符串、数组、映射、稀疏阵列、打包和接口。
打包是一种你必须可以读写数据到任意一个包的机制
,除非你真的,真的组要使用
接口。
打包相对于接口的优势主要是关于性能
,在大多数情况下,这应该是一个足够更倾向于前者的理由,并且接口有一定的开销。
让我们一步一步来分析吧
所以,让我们试着去了解
**
什么让我们得到一个
ClassCastException
**
。从我们所使用的代码开始,我们可以看到,我们对
Intent#putExtras()
的调用解析需要一个字符串和接口。
正如我们前面所说的,这是所预料到的,映射的实现是可序列化的,它们没有被打包。此外,没有一个
putExtras()
明确需要使用
映射
。
步骤
1
:找到所述的第一个薄弱环节
让我们来看看在
Intent.putExtra(String,Serializable)
会发生什么:
Intent.java
public Intent putExtra(String name, Serializable value) {
// ...
mExtras.putSerializable(name, value);
return this;
}
在这里,
mExtras
显然是一个
包
。那么好吧,
意图代表将所有的额外都打到一个包,就如同我们所预期的
,并调用
Bundle#putSerializable()
。让我们看看这个方法:
Bundle.java
@Override
public void putSerializable(String key, Serializable value) {
super.putSerializable(key, value);
}
事实证明,这恰恰仅代表了超级实现,那就是:
BaseBundle.java
void putSerializable(String key, Serializable value) {
unparcel();
mMap.put(key, value);
}
好,到最后
我们尝到了一些甜头
。
首先,让我们忽略
unparcel()
。我们可以看到,
mMap
是一个
array<String,Object>
。这告诉我们,
我们正在失去任何一种曾经拥有过的类型的信息
,也就是说,在这一点上,不管我们
将值放入包中使用的方法类型多么强大,
一切都会在包含对象值的一个大的映射上结束
。
我们的蜘蛛意识开始发麻
......
映射与包的神秘关系
图片
2.48
映射与包的神秘关系
步骤
2
:编写映射
当我们真正开始写
包
的内容时,才是真正有趣的时候。在此之前,如果我们检查额外的类型,我们仍可以得到正确的类型:
Intent intent = new Intent(this, ReceiverActivity.class);
intent.putExtra("map", treeMap);
Serializable map = intent.getSerializableExtra("map");
Log.i("MAP TYPE", map.getClass().getSimpleName());
这样的输出正如我们所预料,由
TreeMap
到
LogCat
。所以这样的转变必须发生在包被写入到所述包中并被再次读取的时候。
如果我们看一下
如何写一个包时
,我们可以看到
BaseBundle#writeToParcelInner
下的细节问题
:
BaseBundle.java
void writeToParcelInner(Parcel parcel, int flags) {
if (mParcelledData != null) {
// ...
} else {
// ...
int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(mMap);
int endPos = parcel.dataPosition();
// ...
}
跳过所有对我们无关紧要的代码,我们可以看到,
大部分工作都是由
Parcel#writeArrayMapInternal()
(记住是
mMap
是一个数组映射)执行的
:
Parcel.java
/* package */ void writeArrayMapInternal(
ArrayMap<String, Object> val) {
// ...
int startPos;
for (int i=0; i<N; i++) {
// ...
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
// ...
}
}
基本上做的就是
把在
BaseBundle
的映射里写入的每一个键
-
值对根据值的大小
形成连续的字符串(这里的值是字符串)。到目前为止后者似乎没有考虑到值的类型。
让我们更深一层!
映射与包的神秘关系
图片
2.49
映射与包的神秘关系
步骤
3
:编写映射值
那么,你问,
Parcel#writeValue()
看起来怎么样
?在这里,在它的
if-elseif-else
体现:
Parcel.java
public final void writeValue(Object v) {
if (v == null) {
writeInt(VAL_NULL);
} else if (v instanceof String) {
writeInt(VAL_STRING);
writeString((String) v);
} else if (v instanceof Integer) {
writeInt(VAL_INTEGER);
writeInt((Integer) v);
} else if (v instanceof Map) {
writeInt(VAL_MAP);
writeMap((Map) v);
} else if (/* you get the idea, this goes on and on */) {
// ...
} else {
Class<?> clazz = v.getClass();
if (clazz.isArray() &&
clazz.getComponentType() == Object.class) {
// Only pure Object[] are written here, Other arrays of non-primitive types are
// handled by serialization as this does not record the component type.
writeInt(VAL_OBJECTARRAY);
writeArray((Object[]) v);
} else if (v instanceof Serializable) {
// Must be last
writeInt(VAL_SERIALIZABLE);
writeSerializable((Serializable) v);
} else {
throw new RuntimeException("Parcel: unable to marshal value "+ v);
}
}
}
啊哈!明白了!即使我们把
TreeMap
作为一个借口放进包里,
writeValue()
方法实际上也会把它放进映射分支的一个实例
V
中
,这(原因很明显)在
else…if
(
V
是接口的实例)分支
之前。
在这一点上,感觉变得越来越强烈。
我现在想知道,对于映射他们是否使用了一些完全非法的捷径,不知怎的就将他们变成了哈希映射?
映射与包的神秘关系
图片
2.50
映射与包的神秘关系
步骤
4
:将映射写到包中
那么,事实上,就其本身而言,除了后续我们将强化映射的类型,
writeMap()
自身做的事情并不多
:
Parcel.java
public final void writeMap(Map val) {
writeMapInternal((Map<String, Object>) val);
}
该方法的
JavaDoc
是很清楚的:
“
映射的关键字必须是字符串对象。
”
类型擦除
确保我们在这里不会有运行时间的错误
,即使我们可能在传递一个关键字不是字符串类型的映射(再一次,这完全是较高水平的非法行为
…
)。
事实上,只要
我们看一下
writeMapIntent()
,我们就会想到:
Parcle.java
/* package */ void writeMapInternal(Map<String,Object> val) {
// ...
Set<Map.Entry<String,Object>> entries = val.entrySet();
writeInt(entries.size());
for (Map.Entry<String,Object> e : entries) {
writeValue(e.getKey());
writeValue(e.getValue());
}
}
再次,在这里类型擦除让
那些计算在运行时变得一文不值
。事实是我们将之前对
键和值
进行类型检查的
writeValue()
作为我们
“
解压缩
”
映射,把一切都放进包里。正如我们所看见的,
writeValue()
完全可以处理非字符串类型的键
。
也许文档和代码在某些点上有点同步,但事实上,
在包中放置和检索一个
TreeMap<Integer,Object>
是相当容易的
。
那么,理所当然,
树形映射
成为一个
哈希映射
是一种例外。
黑洞与启示
映射与包的神秘关系
图片
2.51
映射与包的神秘关系
好吧,这里的
图片已经很清楚
了。当映射被写进一个包中时已经完全失去他们的类型了,所以
当他们进行回读的时候没有办法对信息进行恢复
。
步骤
5
:对映射进行回读
作为最后一步,快速检查我们的理论,
让我们去检查一下
readValue()
,它是与
writeValue()
相对应的:
Parcel.java
public final Object readValue(ClassLoader loader) {
int type = readInt();
switch (type) {
case VAL_NULL:
return null;
case VAL_STRING:
return readString();
case VAL_INTEGER:
return readInt();
case VAL_MAP:
return readHashMap(loader);
// ...
}
}
当写入数据时包工作的方式
,每一项的内容如下:
1.
它
定义了数据类型
int
(
VAL_*
常量之一)
2.
对
数据本身进行转储
(可以包括其他元数据如非固定大小的数据类型,例如字符串长度)
3.
对数据类型进行递归嵌套(非原始)
在这里,我们看到
readValue()
读取数据的类型为
int
,这使得我们的
TreeMap
通过
writeValue()
被设置为
VAL_MAP
,然后根据相应的选择情况来
调用
readHashMap()
来检索数据
本
身:
Parcel.java
public final HashMap readHashMap(ClassLoader loader)
{
int N = readInt();
if (N < 0) {
return null;
}
HashMap m = new HashMap(N);
readMapInternal(m, N, loader);
return m;
}
(the C#-style opening curly brace is actually in AOSP, it’s not my fault)
你几乎可以想象,
readMapInternal()
简单的打包所有的映射条目,这些条目是从我们传递给映射的包中读取的
。
是的。
这就是为什么你总是从一个包中得到一个哈希映射
。如何你创建一个自定义的映射,通过接口进行实现结果也是如此。但
绝对不是我们所希望的
!
如果这是一个预期的效果或者只是一个疏忽
那就很难说了。这是无可否认的一个边缘情况,因为你有几个真正的理由来将一个映射传递到一个
Intent
中,并且你应该只是有很少的好理
由去传递接口而不是包。但是缺乏文档让我觉得它实际上可能就只是一个疏忽而不是设计决策(从其他的设计决策中派生)。
解决方法(又名
tl;dr
)
好吧,我们要
深一层的了解我们的问题
,然而现在我们已经确定了被我们打乱了的关键路径。
我们需要确保我们的
TreeMap
不被抓到由
writeValue()
映射检查的实例
V
中
。
当谈论到
Eugenio
时我的大脑中想到的第一个解决方案是很单一的但却很有效:
将映射包装到一个接口容器中
。
Eugenio
迅速投入到了这个普通的包装中并确认它解决了这个问题。
MapWrapper.java
public class MapWrapper<T extends Map & Serializable> implements Serializable {
private final T map;
public MapWrapper(T map) {
this.map = map;
}
public T getMap() {
return map;
}
public static <T extends Map & Serializable> Intent
putMapExtra(Intent intent, String name, T map) {
return intent.putExtra(name, new MapWrapper<>(map));
}
public static <T extends Map & Serializable> T
getMapExtra(Intent intent, String name)
throws ClassCastException {
Serializable s = intent.getSerializableExtra(name);
return s == null ? null : ((MapWrapper<T>)s).getMap();
}
}
请注意,你在
gist
上找到的
完整代码
是使用
Android
的
@NonNull
注释强制执行的。如果你想单纯的在
Java
中使用这些代码,你可以用
JetBrain
的
@NonNull
取代它,或者你也可以选
择脱离这些注释。
另一个可能的解决方案
另一个解决方法是把它作为一个
Intent extra
之前,自己
先把映射提前序列化成一个字节数组
,然后用
getByteArrayExtra()
对它进行检索,但你必须手动处理序列化和反序列化。
如果你不怕麻烦想选择其他的解决方案来代替,
Eugenio
已经为你提供了
单独
Gist
的代码
。
当你无法控制不断增多的
Intent
时
最后,也许出于某种原因,
你无法控制
Bundle
创建的代码
——
例如,因为它在某些第三方库中。
在这种情况下,请记住,
许多映射的实现需要有一个构造函数
,该构造函数是以映射作为输入的,比如新建一个
TreeMap(Map)
。如果有需要的话,你可以使用构造函数
将你检索的哈
希映射从
Bundle
中
“
变回
”
你之前使用的映射类型
。
请记住,在这种情况下,在映射上的
任何一个
“extra”
的属性都将会丢失
,并且只有键
/
值对会被保留下来。
结论
作为一个
Android
开发人员,意味着你几乎可以轻而易举的用你的方式去完成任何事情
,尤其是小的、微不足道的事情。
我们从中可以学到些什么?
当事情的发展不像我们所预期的那样,
不要只盯着
JavaDoc
不动。
因为那样只会浪费时间。
或者是因为
JavaDoc
的作者不了解有关于你的具体情况。
答案可能就在
AOSP
代码中。
我们可以随意的访问
AOSP
代码。这在动态领域中几乎是独一无二的。我们可以而且应该知道这是为什么。
尽管有时候它看起来像
WTF-land
,
当你了解了你工作的内部运行平台,你就可以成为一名很好的开发人员了。
并且记住:没有打败你的事情只会让你变得更强,或者更疯狂。
映射与包的神秘关系
图片
2.52
映射与包的神秘关系
响应式编程(
Reactive Programming
)介绍
很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的。
学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下。刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它们都流于表面,从没有围绕响应
式编程构建起一个完整的知识体系。库的文档往往也无法帮助你去了解它的函数。不信的话可以看一下这个:
通过合并元素的指针,将每一个可观察的元素序列放射到一个新的可观察的序列中,然后将多个可观察的序列中的一个转换成一个只从最近的可观察序列中产生值得可观察的序列。
天啊。
我看过两本书,一本只是讲述了一些概念,而另一本则纠结于如何使用响应式编程库。我最终放弃了这种痛苦的学习方式,决定在开发中一边使用响应式编程,一边理解它。在
Futurice
工作期间,我尝试在真实项目中使用响应式编程,并且当我遇到困难时,得到了同事们的帮助。
在学习过程中最困难的一部分是
以
响应式编程的方式思考
。这意味着要放弃命令式且带状态的编程习惯,并且要强迫你的大脑以一种不同的方式去工作。在互联网上我找不到任何关
于这方面的教程,而我觉得这世界需要一份关于怎么以响应式编程的方式思考的实用教程,这样你就有足够的资料去起步。库的文档无法为你的学习提供指引,而我希望这篇文章可
以。
“
什么是响应式编程
?”
在互联网上有着一大堆糟糕的解释与定义。
Wikipedia
一如既往的空泛与理论化。
Stackoverflow
的权威答案明显不适合初学者。
Reactive Manifesto
看起来是你展示给你公司的项目经理
或者老板们看的东西。微软的
Rx terminology
"Rx = Observables + LINQ + Schedulers"
过于重量级且微软味十足,只会让大部分人困惑。相对于你所使用的
MV*
框架以及钟爱的编程语
言,
"Reactive"
和
"Propagation of change"
这些术语并没有传达任何有意义的概念。框架的
Views
层当然要对
Models
层作出反应,改变当然会传播。如果没有这些,就没有东西会被渲
染了。
所以不要再扯这些废话了。
响应式编程是使用异步数据流进行编程
一方面,这并不是什么新东西。
Event buses
或者
Click events
本质上就是异步事件流,你可以监听并处理这些事件。响应式编程的思路大概如下:你可以用包括
Click
和
Hover
事件在
内的任何东西创建
Data stream
。
Stream
廉价且常见,任何东西都可以是一个
Stream
:变量、用户输入、属性、
Cache
、数据结构等等。举个例子,想像一下你的
Twitter feed
就像是
Click events
那样的
Data stream
,你可以监听它并相应的作出响应。
在这个基础上,你还有令人惊艳的函数去组合、创建、过滤这些
Streams
。
这就是函数式魔法的用武之地。
Stream
能接受一个,甚至多个
Stream
为输入。你可以融合两个
Stream
,也
可以从一个
Stream
中过滤出你感兴趣的
Events
以生成一个新的
Stream
,还可以把一个
Stream
中的数据值
映射到一个新的
Stream
中。
既然
Stream
在响应式编程中如此重要,那么我们就应该好好的了解它们,就从我们熟悉的
"Clicks on a button" Event stream
开始。
响应式编程
图片
2.53
响应式编程
Stream
就是一个
按时间排序的
Events
序列
,
它可以放射三种不同的
Events
:
(
某种类型的
)Value
、
Error
或者一个
" Completed" Signal
。考虑一下
"Completed"
发生的时机,例如,当包含这
个按钮的窗口或者视图被关闭时。
通过分别为
Value
、
Error
、
"Completed"
定义事件处理函数,我们将会
异步地
捕获这些
Events
。有时可以忽略
Error
与
"Completed"
,你只需要定义
Value
的事件处理函数就行。监听一个
Stream
也被称作是订阅
,而我们所定义的函数就是观察者,
Stream
则是被观察者,其实就是
Observer Design Pattern
。
上面的示意图也可以使用
ASCII
重画为下图,在下面的部分教程中我们会使用这幅图:
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
既然已经开始对响应式编程感到熟悉,为了不让你觉得无聊,我们可以尝试做一些新东西:我们将会把一个
Click event stream
转为新的
Click event stream
。
首先,让我们做一个能记录一个按钮点击了多少次的计数器
Stream
。在常见的响应式编程库中,每个
Stream
都会有多个方法,如
map
,
filter
,
scan
,
等等。当你调用其中一个方法
时,例如
clickStream.map(f)
,它就会基于原来的
Click stream
返回一个
新的
Stream
。它不会对原来的
Click steam
作任何修改。这个特性称为
不可变性
,
它对于响应式编程
Stream
,
就如果汁对于薄煎饼。我们也可以对方法进行链式调用,如
clickStream.map(f).scan(g)
:
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f)
会根据你提供的
f
函数把原
Stream
中的
Value
分别映射到新的
Stream
中。在我们的例子中,我们把每一次
Click
都映射为数字
1
。
scan(g)
会根据你提供的
g
函数把
Stream
中的所有
Value
聚合成一个
Value
x = g(accumulated, current)
,这个示例中
g
只是一个简单的添加函数。然后,每
Click
一次,
counterStream
就会把点击的总次数发给它的观
察者。
为了展示响应式编程真正的实力,让我们假设你想得到一个包含
“
双击
”
事件的
Stream
。为了让它更加有趣,假设我们想要的这个
Stream
要同时考虑三击
(Triple clicks)
,或者更加宽泛,
连击
(
两次或更多
)
。深呼吸一下,然后想像一下在传统的命令式且带状态的方式中你会怎么实现。我敢打赌代码会像一堆乱麻,并且会使用一些变量保存状态,同时也有一些计算时间
间隔的代码。
而在响应式编程中,这个功能的实现就非常简单。事实上,这逻辑只有
4
行代码
。但现在我们先不管那些代码。用图表的方式思考是理解怎样构建
Stream
的最好方法,无论你是初学者
还是专家。
响应式编程
图片
2.54
响应式编程
灰色的方框是用来转换
Stream
函数的。首先,简而言之,我们把连续
250 ms
内的
Click
都积累到一个列表中(就是
buffer(stream.throttle(250ms)
做的事。不要在意这些细节,
我们只是展示一下响应式编程而已
)
。结果是一个列表的
Stream
,然后我们使用
map()
把每个列表映射为一个整数,即它的长度。最终,我们使用
filter(x >= 2)
把整数
1
给过滤
掉。就这样,
3
个操作就生成了我们想要的
Stream
。然后我们就可以订阅
(“
监听
”)
这个
Stream
,并以我们所希望的方式作出反应。
我希望你能感受到这个示例的优美之处。这个示例只是冰山一角:你可以把同样的操作应用到不同种类的
Stream
上,例如,一个
API
响应的
Stream
;另一方面,还有很多其它可用的
函数。
“
为什么我要使用响应式编程
(RP)
?
”
响应式编程提高了代码的抽象层级,所以你可以只关注定义了业务逻辑的那些相互依赖的事件,而非纠缠于大量的实现细节。
RP
的代码往往会更加简明。
特别是在开发现在这些有着大量与数据事件相关的
UI events
的高互动性
Webapps
、手机
apps
的时候,
RP
的优势就更加明显。
10
年前,网页的交互就只是提交一个很长的表单到后端,
而在前端只产生简单的渲染。
Apps
就表现得更加的实时了:修改一个表单域就能自动地把修改后的值保存到后端,为一些内容
"
点赞
"
时,会实时的反应到其它在线用户那里等等。
现在的
Apps
有着大量各种各样的实时
Events
,以给用户提供一个交互性较高的体验。我们需要工具去应对这个变化,而响应式编程就是一个答案。
以
RP
方式思考的例子
让我们做一些实践。一个真实的例子一步一步的指导我们以
RP
的方式思考。不是虚构的例子,也没有只解释了一半的概念。学完教程之后,我们将写出真实可用的代码,并做到知其
然,知其所以然。
在这个教程中,我将会使用
JavaScript
和
RxJS
作为工具
,因为
JavaScript
是现在最多人会的语言,而
Rx* library family
有多种语言版本,并支持多种平台
(
.NET
,
Java
, Scala, Clojure,
JavaScript
,
Ruby
,
Python
,
C++
,
Objective-C/Cocoa
, Groovy
等等
)
。所以,无论你用的是什么工具,你都能从下面这个教程中受益。
实现
"Who to follow"
推荐界面
在
上,这个表明其他账户的
UI
元素看起来是这样的:
响应式编程
图片
2.55
响应式编程
我们将会重点模拟它的核心功能,如下:
•
启动时从
API
那里加载帐户数据,并显示
3
个推荐
•
点击
"Refresh"
时,加载另外
3
个推荐用户到这三行中
•
点击帐户所在行的
'x'
按钮时,只清除那一个推荐然后显示一个新的推荐
•
每行都会显示帐户的头像,以及他们主页的链接
我们可以忽略其它的特性和按钮,因为它们是次要的。同时,因为
最近关闭了对非授权用户的
API
,我们将会为
Github
实现这个推荐界面,而非
。这是
Github
获取用户
的
API
。
如果你想先看一下最终效果,这里有完成后的代码
http://jsfiddle.net/staltz/8jFJH/48/
。
请求和响应
在
Rx
中你该怎么处理这个问题呢?
好吧,首先,
(
几乎
)
所有的东西都可以转为一个
Stream
。这就是
Rx
的咒语。让我们先从最简单的特性开始:
"
在启动时,从
API
加载
3
个帐户的数
据
"
。这并没有什么特别,就只是简单的
(1)
发出一个请求,
(2)
收到一个响应,
(3)
渲染这个响应。所以,让我们继续,并用
Stream
代表我们的请求。一开始可能会觉得杀鸡用牛刀,但我
们应当从最基本的开始,对吧?
在启动的时候,我们只需要发出一个请求,所以如果我们把它转为一个
Data stream
的话,那就是一个只有一个
Value
的
Stream
。稍后,我们知道将会有多个请求发生,但现在,就只有一
个请求。
--a------|->
Where a is the string 'https://api.github.com/users'
这是一个我们想向其发出请求的
URL
的
Stream
。每当一个请求事件发生时,它会告诉我们两件事:
"
什么时候
"
与
"
什么东西
"
。
"
什么时候
"
这个请求会被执行,就是什么时候这个
Event
会被映射。
"
什么东西
"
会被请求,就是这个映射出来的值:一个包含
URL
的
String
。
在
RX*
中,创建只有一个值的
Stream
是非常简单的。官方把一个
Stream
称作
“Observable”
,因为它可以被观察,但是我发现那是个很愚蠢的名子,所以我把它叫做
Stream
。
var requestStream = Rx.Observable.just('https://api.github.com/users');
但是现在,那只是一个包含了
String
的
Stream
,并没有其他操作,所以我们需要以某种方式使那个值被映射。就是通过
[subscribinghref="https://github.com/Reactive-
Extensions/RxJS/blob/master/doc/api/core/observable.md")
这个
Stream
。
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}
留意一下我们使用了
jQuery
的
Ajax
函数
(
我们假设你已经知道
should know already
)
去处理异步请求操作。但先等等,
Rx
可以用来处理
异步
Data stream
。那这个请求的响应就不能当作
一个包含了将会到达的数据的
Stream
吗?当然,从理论上来讲,应该是可以的,所以我们尝试一下。
requestStream.subscribe(function(requestUrl) {
// execute the request
var responseStream = Rx.Observable.create(function (observer) {
jQuery.getJSON(requestUrl)
.done(function(response) { observer.onNext(response); })
.fail(function(jqXHR, status, error) { observer.onError(error); })
.always(function() { observer.onCompleted(); });
});
responseStream.subscribe(function(response) {
// do something with the response
});
}
[
Rx.Observable.create()
href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
所做的事就是通过显式的通知每一个
Observer (
或者说
是
“Subscriber”) Data events(
onNext()
)
或者
Errors (
onError()
)
来创建你自己的
Stream
。而我们所做的就只是把
jQuery Ajax Promise
包装起来而已。
打扰一下,这意味者
Promise
本
质上就是一个
Observable
?
响应式编程
图片
2.56
响应式编程
是的。
Observable
就是
Promise++
。在
Rx
中,你可以用
var stream = Rx.Observable.fromPromise(promise)
轻易的把一个
Promise
转为
Observable
,所以我们就这样子做吧。唯一的不
同就是
Observable
并不遵循
Promises/A+
,
但概念上没有冲突。
Promise
就是只有一个映射值的
Observable
。
Rx Stream
比
Promise
更进一步的是允许返回多个值。
这样非常不错,并展现了
Observables
至少有
Promise
那么强大。所以如果你相信
Promise
宣传的那些东西,那么也请留意一下
Rx Observables
能胜任些什么。
现在回到我们的例子,如果你已经注意到了我们在
subscribe()
内又调用了另外一个
subscribe()
,这类似于
Callback hell
。同样,你应该也注意到
responseStream
是建立在
requestStream
之上的。就像你之前了解到的那样,在
Rx
内有简单的机制可以从其它
Stream
中转换并创建出新的
Stream
,所以我们也应该这样子做。
你现在需要知道的一个基本的函数是
[map(f)href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
,它分别把
f()
应用到
Stream A
中的每一个值中,并把返回的值放进
Stream B
里。如果我们也对请求
Stream
与响应
Stream
进行同样的处理,我们可以把
Request URL
映射为响应
Promise(
而
Promise
可以转
为
Streams)
。
var responseMetastream = requestStream
.map(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
然后,我们将会创造一个叫做
" Metastream "
的怪物:包含
Stream
的
Stream
。暂时不需要害怕。
Metastream
就是一个
Stream
,其中映射的值还是另外一个
Stream
。你可以把它想像为
pointers
:每个映射的值都是一个指向其它
Stream
的指针。在我们的例子里,每个请求
URL
都会被映射一个指向包含响应
Promise stream
的指针。
响应式编程
图片
2.57
响应式编程
Response
的
Metastream
看起来会让人困惑,并且看起来也没有帮到我们什么。我们只想要一个简单的响应
stream
,其中每个映射的值应该是
JSON
对象,而不是一个
JSON
对象
的
'Promise'
。是时候介绍
(Mr. Flatmap)(https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypeflatmapselector-resultselector)
了:它是
map()
的一个版本,通过把应用到
"trunk" Stream
上的所有操作都应用到
"branch" Stream
上,可以
"flatten" Metastream
。
Flatmap
并不是用来
"
修复
" Metastream
的,因为
Metastream
也不
是一个漏洞,这只是一些用来处理
Rx
中的异步响应的工具。
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
响应式编程
图片
2.58
响应式编程
很好。因为响应
stream
是根据请求
stream
定义的,所以
如果
我们后面在请求
stream
上发起更多的请求的话,在响应
stream
上我们将会得到相应的响应事件,就像预期的那样:
requestStream: --a-----b--c------------|->
responseStream: -----A--------B-----C---|->
(lowercase is a request, uppercase is its response)
现在,我们终于有了一个响应
stream
,所以可以把收到的数据渲染出来了:
responseStream.subscribe(function(response) {
// render `response` to the DOM however you wish
});
把目前为止所有的代码放到一起就是这样:
var requestStream = Rx.Observable.just('https://api.github.com/users');
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
responseStream.subscribe(function(response) {
// render `response` to the DOM however you wish
});
刷新按钮
我之前并没有提到返回的
JSON
是一个有着
100
个用户数据的列表。因为这个
API
只允许我们设置偏移量,而无法设置返回的用户数,所以我们现在是只用了
3
个用户的数据而浪费了
另外
97
个的数据。这个问题暂时可以忽略,稍后我们会学习怎么缓存这些数据。
每点击一次刷新按钮,请求
stream
就会映射一个新的
URL
,同时我们也能得到一个新的响应。我们需要两样东西:一个是刷新按钮上
Click events
组成的
Stream(
咒语:一切都能是
Stream)
,同时我们需要根据刷新
click stream
而改变请求
stream
。幸运的是,
RxJS
提供了从
Event listener
生成
Observable
的函数。
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
既然刷新
click event
本身并没有提供任何要请求的
API URL
,我们需要把每一次的
Click
都映射为一个实际的
URL
。现在,我们把刷新
click stream
改为新的请求
stream
,其中每一个
Click
都分别映射为带有随机偏移量的
API
端点。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
因为我比较笨并且也没有使用自动化测试,所以我刚把之前做好的一个特性毁掉了。现在在启动时不会再发出任何的请求,而只有在点击刷新按钮时才会。额
...
这两个行为我都需要:
无论是点击刷新按钮时还是刚打开页面时都该发出一个请求。
我们知道怎么分别为这两种情况生成
Stream
:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
但我们怎样才能把这两个
"
融合
"
为一个呢?好吧,有
[
merge()
href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
函数。这就是它做的事的图解:
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
这样就简单了:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
var requestStream = Rx.Observable.merge(
requestOnRefreshStream, startupRequestStream
);
还有一个更加简洁的可选方案,不需要使用中间变量。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.merge(Rx.Observable.just('https://api.github.com/users'));
甚至可以更简短,更具有可读性:
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.startWith('https://api.github.com/users');
[
startWith()
href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
函数做的事和你预期的完全一样。无论你输入的
Stream
是怎样,
startWith(x)
输出的
Stream
一开始都是
x
。但是还不够
DRY
,我重复了
API
终端
string
。一种修复的方法是去掉
refreshClickStream
最后的
startWith()
,并在一开始的时候
"
模拟
"
一次刷新
Click
。
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
很好。如果你把之前我
"
毁掉了的版本
"
的代码和现在的相比,就会发现唯一的不同是加了
startWith()
函数。
用
Stream
构建三个推荐
到现在为止,我们只是谈及了这个
推荐
UI
元素在
responeStream
的
subscribe()
内执行的渲染步骤。对于刷新按钮,我们还有一个问题:当你点击
‘
刷新
’
时,当前存在的三个推荐并
不会被清除。新的推荐会在响应到达后出现,为了让
UI
看起来舒服一些,当点击刷新时,我们需要清理掉当前的推荐。
refreshClickStream.subscribe(function() {
// clear the 3 suggestion DOM elements
});
不,别那么快,朋友。这样不好,我们现在有
两个
订阅者会影响到推荐的
DOM
元素
(
另外一个是
responseStream.subscribe()
)
,而且这样完全不符合
Separation of concerns
。还记
得响应式编程的咒语么?
响应式编程
图片
2.59
响应式编程
所以让我们把显示的推荐设计成一个
stream
,其中每一个映射的值都是包含了推荐内容的
JSON
对象。我们以此把三个推荐内容分开来。现在第一个推荐看起来是这样子的:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
});
其他的,
suggestion2Stream
和
suggestion3Stream
可以简单的拷贝
suggestion1Stream
的代码来使用。这不是
DRY
,它会让我们的例子变得更加简单一些,加之我觉得这是一
个可以帮助考虑如何减少重复的良好实践。
我们不在
responseStream
的
subscribe()
中处理渲染了,我们这么处理:
suggestion1Stream.subscribe(function(suggestion) {
// render the 1st suggestion to the DOM
});
回到
"
当刷新时,清理掉当前的推荐
"
,我们可以很简单的把刷新点击映射为
null
,并且在
suggestion1Stream
中包含进来,如下:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
);
当渲染时,
null
解释为
"
没有数据
"
,所以把
UI
元素隐藏起来。
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});
现在的示意图:
refreshClickStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestion1Stream: ----s-----N---s----N-s-->
suggestion2Stream: ----q-----N---q----N-q-->
suggestion3Stream: ----t-----N---t----N-t-->
其中,
N
代表了
null
作为一种补充,我们也可以在一开始的时候就渲染
“
空的
”
推荐内容。这通过把
startWith(null)
添加到
Suggestion stream
就完成了:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
现在结果是:
refreshClickStream: ----------o---------o---->
requestStream: -r--------r---------r---->
responseStream: ----R----------R------R-->
suggestion1Stream: -N--s-----N----s----N-s-->
suggestion2Stream: -N--q-----N----q----N-q-->
suggestion3Stream: -N--t-----N----t----N-t-->
关闭推荐并使用缓存的响应
还有一个功能需要实现。每一个推荐,都该有自己的
"X"
按钮以关闭它,然后在该位置加载另一个推荐。最初的想法,点击任何关闭按钮时都需要发起一个新的请求:
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button
var requestStream = refreshClickStream.startWith('startup click')
.merge(close1ClickStream) // we added this
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
这个没有效果。这将会关闭并且重新加载
所有
的推荐,而不是仅仅处理我们点击的那一个。有一些不一样的方法可以解决,并且让它变得更加有趣,我们可以通过复用之前的请求来
解决它。
API
的响应页面有
100
个用户,而我们仅仅使用其中的三个,所以还有很多的新数据可以使用,无须重新发起请求。
同样的,我们用
Stream
的方式来思考。当点击
'close1'
时,我们想要用
responseStream
最近的映射从响应列表中获取一个随机的用户,如:
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
在
Rx*
中,
叫做连接符函数的
[
combineLatest
href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
似乎实现了我们想要的功能。它接受两个
Stream
,
A
和
B
作为输入,当其中一个
Stream
发射一个值时,
combineLatest
把最近两个发射的值
a
和
b
从各自的
Stream
中取出并且返回一个
c = f(x,y)
,其中
f
为你定义的函
数。用图来表示更好:
stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
vvvvvvvv combineLatest(f) vvvvvvv
----AB---AC--EC---ED--ID--IQ---->
where f is the uppercase function
我们可以在
close1ClickStream
和
responseStream
上使用
combineLatest()
,所以无论什么时候当一个按钮被点击时,我们可以获得最新的响应发射值,并且在
suggestion1Stream
上产生一个新的值。另一方面,
combineLatest()
是对称的,当一个新的响应在
responseStream
发射时,它将会把最后的
'
关闭
1'
的点击事件一起合并来产生一个
新的推荐。这是有趣的,因为它允许我们把之前的
suggestion1Stream
代码简化成下边这个样子:
var suggestion1Stream = close1ClickStream
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
还有一个问题需要解决。
combineLatest()
使用最近的两个数据源,但是当其中一个来源没发起任何事件时,
combineLatest()
无法在
Output stream
中产生一个
Data event
。从上边的
ASCII
图中,你可以看到,当第一个
Stream
发射值
a
时,这个值时并没有任何输出产生,只有当第二个
Stream
发射值
b
时才有值输出。
有多种方法可以解决这个问题,我们选择最简单的一种,一开始在
'close 1'
按钮上模拟一个点击事件:
var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
.combineLatest(responseStream,
function(click, listUsers) {l
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
结束
终于完成了,所有的代码合在一起是这样子:
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var responseStream = requestStream
.flatMap(function (requestUrl) {
return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
});
var suggestion1Stream = close1ClickStream.startWith('startup click')
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});
你可以查看这个最终效果
http://jsfiddle.net/staltz/8jFJH/48/
这段代码虽然短小,但实现了不少功能:它适当的使用
Separation of concerns
实现了对
Multiple events
的管理,甚至缓存了响应。函数式的风格让代码看起来更加
Declarative
而非
Imperative
:我们并非给出一组指令去执行,而是通过定义
Stream
之间的关系
定义这是什么
。举个例子,我们使用
Rx
告诉计算机
*suggestion1Stream*
是
由
'close 1' Stream
与最新
响应中的一个用户合并而来,在程序刚运行或者刷新时则是
null
。
留意一下代码中并没有出现如
if
、
for
、
while
这样的控制语句,或者一般
JavaScript
应用中典型的基于回调的控制流。如果你想使用
filter()
,上面的
subscribe()
中甚至
可以不用
if
、
else
(
实现细节留给读者作为练习
)
。在
Rx
中,我们有着像
map
、
filter
、
scan
、
merge
、
combineLatest
、
startWith
这样的
Stream
函数,甚至更多类似
的函数去控制一个事件驱动
(Event-driven)
的程序。这个工具集让你可以用更少的代码实现更多的功能。
接下来会发生什么
如果你觉得
Rx*
会成为你首选的响应式编程库,花点时间去熟悉这个
[big list of functionshref="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md")
,它包
括了如何转换、合并、以及创建
Observable
。如果你想通过图表去理解这些函数,请看
RxJava's very useful documentation with marble diagrams
。无论什么时候你遇到问题,画一下这些
图,思考一下,看一下这一大串函数,然后继续思考。以我个人经验,这样效果很明显。
一旦你开始使用
Rx*
去编程,很有必要去理解
[Cold vs Hot Observableshref="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md")
中的概念。如果忽略
了这些,你一不小心就会被它坑了。我提醒过你了。通过学习真正的函数式编程去提升自己的技能,并熟悉那些会影响到
Rx*
的问题,比如副作用。
但是响应式编程不仅仅是
Rx
。还有相对容易理解的
Bacon.js
,它没有
Rx
那些怪癖。
Elm Language
则以它自己的方式支持
RP
:它是一门会编译成
Javascript + HTML + CSS
的响应式编
程语言
,并有一个
time travelling debugger
。非常厉害。
Rx
在需要处理大量事件的
Frontend
和
Apps
中非常有用。但它不仅仅能用在客户端,在后端或者与数据库交互时也非常有用。事实上,
RxJava
是实现
Netflix's API
服务器端并发的一个
重要组件
。
Rx
并不是一个只能在某种应用或者语言中使用的
Framework
。它本质上是一个在开发任何
Event-driven
软件中都能使用的编程范式。
如果教程帮到你了,
请支持
。
延期的共享元素转换
(3b)
这篇文章我们通过讨论
Lollipop Transition API
:延期的共享元素转换,来继续深入分析共享元素转换。这是这一系列文章的第四部分,我会通过以下观点展开:
•
第一部分:
以
Activity & Fragment Transitions
开始
•
第二部分:
深入内容转换
•
第三部分
a
:
深入共享元素转换
•
第三部分
b
:
延期共享元素转换
•
第三部分
c
:实现共享元素回调
(
很快出现!
)
•
第四部分:
Activity & Fragment Transitions
示例
(
很快出现!
)
首先我们讨论由于常见问题导致推迟共享元素转换的需要。
理解问题
当处理共享元素转换时,一个常见的问题源于这样的事实:它们很早就出现在
Activity
生命周期的框架中。回顾
第
1
部分
,
转换
必须捕获目标视图的开始和结束状态,来构建一个正常
运转的动画。因此,如果框架在它的共享元素给出最后的大小,位置以及被调用的
Activity
内的大小之前,开始共享元素转换,那么转换会为它的共享元素捕获错误的结束值,由此产
生的动画将完全错误
(
错误的输入转换示例,见
视频
3.3
)
。
在转换开始之前,共享元素的结束值是否被计算出来主要取决于两个因素:
(1)
被调用的
activity
的布局的复杂性和深度,
(2)
调用的
activity
加载所需数据的时间。布局越复杂,那么决
定共享元素在屏幕上显示的位置和大小所花费的时间就越长。同样地,如果
activity
内共享元素的最终外观取决于异步加载数据,那么在数据传递回主线程之前,框架有可能自动的启
动共享元素转换。下面列出的是一些你可能会遇到的常见的问题:
•
共享元素在一个
Fragment
中,它寄存在被调用的
activity
中。
•
FragmentTransactions
在被命令后不是立即执行
;当主线程的工作结束后,它们稍后执行。所以,如果共享元素在
Fragment
的视图层的内部,并且
FragmentTransaction
执行
的不够迅速,那么框架有可能在共享元素正确衡量之前就启动共享元素转换,并在屏幕上布局。
1
•
共享元素是一个高分辨率的图像
。设置一个超过
ImageView
最初的范围的高分辨率图像,可能会
引发额外的布局
甚至视图层次
,
因此在共享元素准备好之前就开始转换的可能性
会增加。流行的位图加载
/
扩展库的异步性质,如
Volley
和
Picasso
,不会可靠的解决这个问题:框架没有图像被下载,扩展,和
/
或从后台线程的磁盘获取的这些先验知识,所以
无论图像是否正在被处理,框架都会启动共享元素转换。
•
共享元素依赖与异步加载数据。
如果在共享元素最终出现在被调用的
activity
之前,需要从
AsyncTask
,
AsyncQueryHandler
,
Loader
或类似的东西上下载数据,框架可能会在
数据传回主线程之前启动转换。
postponeEnterTransition()
和
startPostponedEnterTransition()
在这一点上你可能会想
,“
要是有办法暂时延迟转换
,
直到我们确定共享元素已正确测量和布局。
”
嗯
,
你很幸运
,
因为
Activity Transitions API
2
给了我们一个这样做的方法
!
想要暂时性的阻止共享元素转换启动,在你调用的
activity
的
onCreate()
方法中调用
postponeEnterTransition()
。之后,当你确定共享元素已经正确的定位和定形后,调用
startPostponedEnterTransition()
来恢复转换。你会发现一个有用的常见的模式,在
OnPreDrawListener
中启动延期转换,当共享元素被测量并布局后会被调用:
3
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Postpone the shared element enter transition.
postponeEnterTransition();
// TODO: Call the "scheduleStartPostponedTransition()" method
// below when you know for certain that the shared element is
// ready for the transition to begin.
}
/**
* Schedules the shared element transition to be started immediately
* after the shared element has been measured and laid out within the
* activity's view hierarchy. Some common places where it might make
* sense to call this method are:
*
* (1) Inside a Fragment's onCreateView() method (if the shared element
* lives inside a Fragment hosted by the called Activity).
*
* (2) Inside a Picasso Callback object (if you need to wait for Picasso to
* asynchronously load/scale a bitmap before the transition can begin).
*
* (3) Inside a LoaderCallback's onLoadFinished() method (if the shared
* element depends on data queried by a Loader).
*/
private void scheduleStartPostponedTransition(final View sharedElement) {
sharedElement.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
sharedElement.getViewTreeObserver().removeOnPreDrawListener(this);
startPostponedEnterTransition();
return true;
}
});
}
忽略名字,这两种方法也可以用来延迟共享元素返回转换。简单的推迟返回转换,调用
activity
的
onActivityReenter()
方法:
4
/**
* Don't forget to call setResult(Activity.RESULT_OK) in the returning
* activity or else this method won't be called!
*/
@Override
public void onActivityReenter(int resultCode, Intent data) {
super.onActivityReenter(resultCode, data);
// Postpone the shared element return transition.
postponeEnterTransition();
// TODO: Call the "scheduleStartPostponedTransition()" method
// above when you know for certain that the shared element is
// ready for the transition to begin.
}
尽管共享元素转换更流利可靠,但是将延期共享元素添加到你的应用程序中有可能会引起潜在地副作用,注意到这点也很重要:
•
在调用
postponeEnterTransition
之后
****
不要忘记调用
startPostponedEnterTransition()
:忘记调用会使你的应用程序处于死锁的状态,阻止用户进入下一个
Activity
屏幕。
•
不要推迟转换超过几分之一秒。推迟甚至几分之一秒的转换可能会给应用程序中引入不必要的延迟,恼人的用户,以此减慢了用户的体验。
一如往常,感谢阅读!如果你有任何问题,请随意留下评论,如果你觉得文章对你有帮助,不要忘记
+1
和
/
或分享这篇文章。
1
当然,大多数应用程序可以通过调用
FragmentManager#executePendingTransactions()
来解决这个问题,该方法会强迫待定的
FragmentTransactions
立即执行而不是异步执行。
2
注意
postponeEnterTransition()
和
startPostponedEnterTransition()
方法只位
Activity Transitions
工作,不为
Fragment Transitions
工作。关于解释及可能的解决方案,见
this
StackOverflow answer
和
this Google+ post
.
3
专家提示:在调用
View#isLayoutRequested()
之前,是否需要分配
OnPreDrawListener
,如果是必须的,
View#isLaidOut()
在某些情况下可能会派上用场。
4
一个测试你的共享元素返回
/
重新输入转换的行为的好方法是通过进入
Developer Options
和启用
"Don't keep activities"
设置。这将有助于测试最坏的情况
,
即在返回转换开始之前,调用
的
activity
需要重建其布局
,
重新查询必要的数据等。
用
Robolectric
进行参数化测试
在我们目前的项目中,我们使用
Robolectric
为
Android
应用程序编写单元测试,并且它一直都很好用。最近我需要编写一个操作需要执行几次并且需要用到不同的测试数据的一个测试
用例,还要断言正确的行为发生取决于数据。
对于所谓的
参数化测试
,
Junit
具有一个易于使用的选项,其中一项里面定义了测试数据,然后可以使用
参数化测试运行器
来执行测试。这将为测试数据的每一个元素创建测试类的一
个实例,其中测试数据被传递给构造函数。
事实证明,
Robolectric
有一个完全一样的
参数化
Robolectric
测试运行器
(但略微有些调整,以符合
Robolectric
),并且在验证应用程序接收来自外部服务提供商的不同的错误代码的行
为测试时工作的非常好。
@RunWith(ParameterizedRobolectricTestRunner.class)
public class ContactServiceTest {
@ParameterizedRobolectricTestRunner.Parameters(name = "ErrorCode = {0}")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{105, 105_ERROR_MSG},
{113, 113_ERROR_MSG},
{114, 114_ERROR_MSG},
{134, 134_ERROR_MSG},
{137, 137_ERROR_MSG},
{999, DEFAULT_ERROR_MSG} // Bogus error code
});
}
private int errorCode;
private String expectedErrorMsg;
public ContactServiceTest(int errorCode, String errorMsg) {
this.errorCode = errorCode;
this.expectedErrorMsg = errorMsg;
}
@Test
public void when_known_error_code_is_received_from_service_correct_error_msg_is_displayed_to_user() {
// HTTP response from service contains defined error code
Robolectric.addPendingHttpResponse(HttpStatus.SC_OK, buildFakeServiceResponse(errorCode));
// Contact the service
mService.contactService();
// Use awaitility to wait until error message is displayed to user
// then assert that the error message is correct
await().until(getDisplayedErrorMsg(), is(expectedErrorMsg));
}
代码将执行测试用例六次,依次为测试数据的每个元素进行测试,并将显示的错误消息看成是特定错误代码定义的错误信息。每个测试运行将被视为自己的测试用例并作为报告进行创
建。在参数注释中添加名称参数,这样可以重新整理测试结果。正如显示的测试运行的结果,测试用例的名字在这种情况下将是
when_known_error_code_is_received_from_service_correct_error_msg_is_displayed_to_user[ErrorCode = 105]
when_known_error_code_is_received_from_service_correct_error_msg_is_displayed_to_user[ErrorCode = 113]
when_known_error_code_is_received_from_service_correct_error_msg_is_displayed_to_user[ErrorCode = 114]
...
对于进一步检验
Junit
理论启示
或者说是
Junit
快速检查
,一种很好的方法是在你的
Junit
(也就是
Robolectric
)测试中生成测试数据,而不是你自己去定义它。
欢迎为
Android
和
iOS
嵌入
API
由
Jen Kovnats Harrington
发布,谷歌地图
API
的产品经理
最初
发布
到谷歌地图开发人员的博客
人们不认为他们的位置是在地图的坐标上。他们想象的环境是他们在什么商店或者餐馆,周围有些什么。为了帮助你的应用程序可以讲出用户所使用的语言,我们正针对
Places API
for Android
和
Places API for iOS
开放一个测试程序。
Places API
的
web
服务
和
JavaScript
库
可以使用已经有一段时间了。通过对
Android
和
iOS
设备提供本地支持,您可以通过新的
APIs
利用设备的位置信号来优化移动体验。
Android
和
iOS
的
Places API
为简单表示地理位置的经度和纬度缩短了距离,以及人们如何与一个已经的地方进行定位。例如,你不会告诉别人你出生在
25.7918359
,
-80.2127959
。你
只需要简单地说,
“
我出生在迈阿密的杰克逊纪念医院,位于佛洛里达州。
”Places API
将谷歌全球的位置数据库加载到你的应用程序中,提供了超过
1
亿个地方,如餐馆、当地的商
业、酒店、博物馆和其他景点。
主要功能包括:
•
添加一个
地点选择器
:一个下拉式的
UI
控件,允许你的用户指定地方。
•
获取
用户现在所在的
位置
•
显示详细的位置信息
,包括地方名称、地址、电话号码和网站
•
使用
自动
完成来保存用户的时间和拼写的地点名称,根据它们的类型自动完成。
•
通过
添加
与用户相关的
新的地方
并在谷歌位置数据库中显示这些地方来使你的应用程序脱颖而出。
•
通过
报告
设备在某一个特定的地点来改善你周围的地图。
为了在
Android
上开始使用
Places API
,查看
DevByte
,
检查
开发文档
,并演示播放。为了在
iOS
测试程序上申请应用
Places API
,看
这里
。
嵌入
API
图片
2.60
嵌入
API
用户体验
图片
2.61
用
户体验
在谷歌市场上创造更好的用户体验
不管它是一种
跟踪训练的方法
,
chart the nighttime stars
,或是
build a new reality
,争夺世界霸权,谷歌市场为开发人员提供了一个创建引人入胜的应用程序和游戏的平台,从而打造一
个成功的企业。关键的任务是用户在谷歌市场上搜索应用程序和游戏的时候可以享受到一种积极地体验。现在我们利用两个更新来提升开发人员和用户的体验。
一种基于行业标准的全球内容分级系统
今天,在谷歌市场上针对应用程序和游戏,我们要介绍一种新的基于年龄的分级系统。我们都知道,对于什么样的内容适合孩子、青少年和成年人,来自不同国家的人们都有不同的想
法。所以今天的这个声明将帮助开发人员针对不同的使用者来更好地标记应用程序。与行业最佳标准相一致,这种改变不仅将为开发人员和他们的用户进行沟通熟悉以及对本地内容进
行分级提供一种简便的方法,并且通过让用户选择适合他们的内容,有助于改善应用程序的发现的参与。
从现在开始,开发人员可以对他们的每一个应用程序和游戏完成一份内容分级问卷调查来获取客观的内容分级结果。谷歌市场的新分级系统分级联盟(
IARC
)及其参
与机构的官方分级,包括娱乐软件分级委员会(
ESRB
),全欧洲的游戏信息组织(
PEGI
),澳大利亚分类委员会,娱乐软件协会(
USK
),分级系统(
ClassInd
)。不受特定权威分
级覆盖的区域将显示一个机遇年龄的一般的分级。这个过程对开发人员是快速、自动而且免费的。在未来的几周里,来自全球的消费者将可以在他们本地的市场里开始看到这些新的分
级系统。
在谷歌市场里为了帮助维护你的应用程序的可用性,可以
登录到开发人员控制台
并为你的每个应用程序完成一份新的分级问卷调查。没有一份完整的分级问卷调查的应用程序将被标记
为
“
未分级
”
,并且可能会在某些区域被封锁或者被用于特定用户。从五月份开始,所有新的应用程序以及现有应用程序中的更新在谷歌市场里可以被发布之前都将需要一份完成的问卷
调查。
一个更好的保护用户的应用程序审查过程
几个月前,为了更好的保护社区和改善应用程序的目录,应用程序在谷歌市场里被发布之前,我们开始对他们进行审查。这个新的过程设计一个专家小组,他们主
要主责识别哪些违反开发政策的行为,这些
developer policies
比应用程序的生命周期还要早。我们看重的是谷歌市场里唯一快速的创新和更迭,并且将继续帮助开
发人员将他们的产品在被提交之后几个小时之内推向市场,而不是等到几天或者几周之后。实际上,在部署的过程中对于开发人员并没有明显的变化。
为了协助这项工作,并未开发人员提供更大的透明度,我们还推出了在处理发布
status
上的改进方法。开发人员可以更深入的了解为什么应用程序会被拒绝或暂停,并且如果轻微的违
反政策,他们可以很容易的修复并重新提交他们的应用程序。
在过去的一年中,我们已经为开发人员支付了超过
70
亿美元,并且很高兴看到生态系统的发展与创新。我们将继续开发新的工具和服务来促进这一增长,并帮助开发者社区构建成功
的企业。
更多信息请访问
http://wiki.jikexueyuan.com/project/android-weekly/




