暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

weekly

yBmZlQzJ 2024-01-13
213

前言

Android Weekly

相当于是

Android

开发社区的实时通讯录,每周报导

Android

最新讯息,包括新的库、工具和博客等,只要你有

Email

,就可以对其进行订阅,了解更多关于安卓的消息。

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)

最近

Facebook

发布了名为

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)

当应用程序由于许多大型位图而耗尽内存时会发生什么

?

程序会崩溃。

Facebook

通过创建称为

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

最近

Facebook

发布了一名为

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)”

 

![alt_text](../images/issue-146/7.1.png)

 

我找到这篇评论是因为我看了

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

周报专栏评论的图片

![alt_text](../images/issue-146/7.2.png)

 

不幸的是

很多开发者确实看到这样的

`

模态

`

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

组件的设置。

 

![alt text](../images/issue-146/8.1.gif)

 

###

发送数据到

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

的颜色。但是,对于这点我提出警示,因为它会偏离你应用程序的内容,并且几乎没有

Google

设计的应

用程序会这样做。

可点击的视图

如果你有一个视图,想给它添加一个连锁反应,那么应该怎么做呢

?

一种常见的情况是有几个可点击的项目的

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

总结

让你的应用程序遵循

Google

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

这个页面的部分内容是基于

Google

创建和共享的工作修改的。

根据

Material

设计导航制图工具样式

Material

图片

2.17

Material

简介

导航制图工具似乎应该在

这里

介绍。除了一些

criticism

我也喜欢模式,所以我决定在我研究的几个应用程序上实现它。你在这里可以阅读到的仅仅是我对于它感兴趣的想法,希望能够

帮助互相帮助互相学习。

这是三个出版物的第二版。看看第一章和第三章:

Material

设计下的导航制图工具选型

Material

设计下的导航制图工具行为

(

很快出版

)

你可以从这里浏览关于导航制图工具的

Material

设计指南:

导航制图工具模式

Material

设计的指标和关键路线

Toolbar

方法

初始

制图工具总是热门话题。当

Material

设计概念开始出现时,人们有一些

困惑

,甚至

后来指南产生

,概念也没有完全清晰。

现在有

很棒的库

出现,甚至一些

Google

代码

来查看

•••

但是如果你查看这些,有可能是因为你觉得

代码好玩

在这里我将要谈论制图工具的样式。它并没有覆盖全部你可以在指南中看到的样式,只是一些我观点中指出的东西。

准备好了吗?

位置

过去,

Navigation Drawer

ActionBar

处于相同层次的视图中。

Material

图片

2.18

Material

随着模式演化,开始有一些不一致。当

Material

设计出现时,可以很清楚的看到这两部分不再处于视图的同一层次中。关于这点有

许多讨论

甚至更多

但是

Google

没有给出太多有

用的东西

,而现在导航制图工具的位置定义如下:

左侧导航制图工具跨屏幕的整个高度,制图工具处于状态栏下方。所有制图工具下的东西都被幕布遮住变暗。幕布下的内容仍然是可见的。

Material

图片

2.19

Material

注意图片中右侧的制图工具有些不同。

注意:我假定你使用的是

AppCompat

工具栏

由于工具栏是另一个视图层次结构,所以就像你处理其他视图一样,只是定位制图工具布局下的工具栏。

如果你在一些

Google

应用程序中看到如下所示的东西,不用担心:

Material

图片

2.20

Material

Google+

照片可能是最后一个制图工具没有在

actionBar/Toolbar

上的

Google

应用程序,我相信下次更新会修复这个问题。

自旋条

你还记得当打开制图工具时,在

actionBar/toolbar

出现的别致的图标动画吗?在

Holo

中,动画并不别致,但是

Material

设计使得动画看起来非常棒。

Material

图片

2.21

Material

我认为它首先出现在

Google

Play Store

中,然后多数开发人员和设计师都喜欢它。

但是不久之后,在

动画

上出现了制图工具,如前部分解释的那样。在当时是非常奇怪的,因为它也出现在

Google

Material

设计视频和商品推销功能中。

Material

图片

2.22

Material

我记得当迁移到

material

设计时,大多数人表现的像第一件要做的事是

burger/arrow

的事。在第一个

Google

实现中,制图工具不在工具栏的上方,且你也不会看到华丽的动画效果。指

南出现之后,有一段

奇怪

的时刻,

Google

应用程序做的正好与

Material

设计指南告诉我们的相反。而现在我写这篇文章时,

Google

应用程序是遵循指南的,并且我希望不久以后看到

全部应用程序中制图工具都在工具栏的上方。

在我看来,那个设计决定实在是晚了。图标的动画已经默认的由

SDK

释放和激活。

出于某种原因,即使布局之后,制图工具处于一切工具的上方,大多数

Google

应用程序仍然有动画

(

注意:在写这一条目的过程中,

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

应用程序中没有这个边框。

Google+

Hangouts

在工具栏中有资料图片,虽然它也有白色的边

框。

Material

图片

2.23

Material

Material

图片

2.24

Material

注意:

检查一下资料图片的大小

这个资料图片是圆形的,通常没有边框。确保你的带有库的圆形图片是遵循

Romain

推荐的技术得到的。

封面图片

封面图片

(

不同于资料图片

)

,是

account/header

部分

(

这是上一部分,通常你可以在你的账户之间改变,看到你的显示名称,电子邮箱和你的资料图片

)

的背景。

这个区域的文本是白色的,并且为了确保它可见,你可以应用前景或半透明的黑色覆盖封面图片。我尝试了不同的透明度,发现透明度为

40-50%

的黑色是不错的。你不想看不见照

片,也不想使文本不可读。

我所做的是在

FrameLayout

中应用前景。我不知道这是不是最好的方法,希望得到一些反馈。我没有可以切换的账户,对我来说,整个布局

/

部分是可点击的,带有触感反馈或

Lollipop

的连锁反应或全部。确保你使用

centerCrop

scaleType

来使它看起来更好。

Material

图片

2.25

Material

如果你查看上面图片,制图工具是可见的,且在状态栏的下方。此刻我写这点时,

Google

应用程序仍然处于迁移到这种模式的过程中。

Gmail, Inbox, Keep, Play Store

Hangouts

已经

有这种布局,而其他的应用程序正在进行中。此刻,这只发生在运行在

Lollipop

和以上版本中的应用程序中。

Material

图片

2.26

Material

Material

图片

2.27

Material

尽管当前在

Play Store

Google IO

应用程序中,导航制图工具是完全错误的,但是它们有更好的代码,并且为下一版本的发布

(

看起来还需要一段时间

...

从现在开始到几个月后的

Google IO

会议之前可能一直更新

)

做好了准备。

神奇的地方在

Google ScrimInsets layout

中。复制,粘贴,完成。你可以试着做。我觉得

Google

的员工会比我做的更好。看看下面的代码部分。看看要点中的代码,因为它也需要用到

主题

/

样式。

ScrimInsets

布局能否应用于低于

Lollipop

的版本中,我也不清楚。然而我知道将它用于

Kit Kat

中是可能的,但是

Google

不这样做。的确,

入侵

状态栏和

/

或导航栏不是

Lollipop

下的

事,并且可能这就是其原因。

注意:

查看封面图片的大小

状态栏下的导航制图工具只会在

Lollipop

中出现。入侵状态栏或导航栏不

Lollipop

下的事,因此这可能就是不会发生的原因。

Material

图片

2.28

Material

当谈论到为制图工具中的主要行制订样式时,我们需要为每一行的每一个单元解决

3

个元素

(

背景,图标和文本

)

3

种不同的状态

(

默认,选中和按下

)

。并不是所有的东西都按照

说明

写,但是我们必须阅读说明,看看

Google

应用程序和其他看起来不错的应用程序,试图找出怎样能使它以

Material

设计的方式看起来不错的方法。

好的,现在让我们看看一些

Google

应用程序来收集一些线索。在接下来的图片中,第一行是默认的状态,第二行是选中的状态,第三行是按下的状态。

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

Google

应用程序看上去很一致,但是当你注意细节你会发现此刻就在我写这篇文章时,超过

10

个应用程序在为导航制图工具中可选的行项目制订样式。

经验法则

经过一些尝试,以及查看指南和

Google

应用程序,这就是我提出的。

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

,然后你就能得到你的有颜色的图标。这样做的好

处是当你决定改变颜色时,你不需要每次都创建新的图标,

你可以从

Google

得到不同大小和不同颜色的图标

,并且你可以在几秒钟之内为相同的图标设置不同的颜色,以此来找出哪

一个是最好的。如果你为相同的图标使用不同的颜色,你还可以节省空间。如果你需要根据状态

(

按下,选中

...)

决定图标的颜色变化,你只需要在你的

tint

上设置一个

颜色状态列表

我是在

自定义视图中以编程的方式扩展

ImageView

,因为颜色状态栏在

Kit Kat

及以下版本中不起作用

(android

tint

在颜色下起作用,但是在颜色状态下不起作用

)

看看下面联系的要点我是怎么做的。如果你有更好的选择或者发现了错误,请说明。

页眉页脚行为

这里的指导方针是相当灵活的。它基本上由你的设计决定。

页眉部分

(

又名账户部分

)

有时固定,有时会出现一些可滚动。就你的设计而言,在我看来这应该是被固定的。制图工具看起来更好,它是一致的,是获取你的资料的最好方法。

页脚部分

(

即设置和支持

)

可以被固定或者不被固定。如果你看看

Google

应用程序,有些是固定的,有些是在滚动条的底部。如果你不知道它们不能到达制图工具的底部,那么页脚只出

现在主行之后。

Material

图片

2.44

Material

Material

图片

2.45

Material

在我看来

,

固定是最好的方法

,

但你可以做一些例外。例如当你固定页眉时

,

一些条目也就固定了

,

然后在制图工具中有一个可滚动的部分。如果滚动部分的空间很少以至于它看起来有些蠢

(1

2

),

那么你可能就想获得更多的空间,而取消固定页脚是一个不错的选择。

页眉和页脚应该被固定,除非制图工具结构需要空间来达到良好的外观和行为。

由于制图工具上大量的选项,路径、结构

...

这里没有真正的经验法则。

资源

Google

官方

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"

推荐界面

Twitter

上,这个表明其他账户的

UI

元素看起来是这样的:

响应式编程

图片

2.55

响应式编程

我们将会重点模拟它的核心功能,如下:

启动时从

API

那里加载帐户数据,并显示

3

个推荐

点击

"Refresh"

时,加载另外

3

个推荐用户到这三行中

点击帐户所在行的

'x'

按钮时,只清除那一个推荐然后显示一个新的推荐

每行都会显示帐户的头像,以及他们主页的链接

我们可以忽略其它的特性和按钮,因为它们是次要的。同时,因为

Twitter

最近关闭了对非授权用户的

API

,我们将会为

Github

实现这个推荐界面,而非

Twitter

。这是

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/

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论