移动周分享-第51期

Java方法参数过多 - 吴明

  • 示例方法
1
2
3
4
5
6
7
8
9
10
11
public void getNews(Context context,
Callback callback,
String uuid,
String uid,
String from,
String token,
String uid,
String .....){

......
}
  • 示例构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Person {
public String lastName;
public String firstName;
public String middleName;
public String salutation;
public String suffix;
public String streetAddress;
public String city;
public String state;
public boolean isFemale;
public boolean isEmployed;
public boolean isHomeOwner;
public Person(String lastName, String firstName, String middleName, String salutation,
String suffix, String streetAddress, String city, String state,
boolean isFemale, boolean isEmployed, boolean isHomeOwner) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
this.salutation = salutation;
this.suffix = suffix;
this.streetAddress = streetAddress;
this.city = city;
this.state = state;
this.isFemale = isFemale;
this.isEmployed = isEmployed;
this.isHomeOwner = isHomeOwner;
}
}
  • 问题:
    • 添加大量参数理解难度
    • 易参数位置不正确且运行正常
    • 不易维护
    • 那一个方法或者构造方法多少个参数才好了—没有答案
1
函数参数的理想个数是零,其次是一,紧随其后的是二,应该尽可能避免三个参数的情况。参数如果多于三个则需要特殊的理由,而且无论如何都不应该再使用。
解决方法
  • 引入参数对象

    • 参数关系紧密合并到一个对象中

      • Person.class

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        	public class Person {
        public FullName fullName;
        public Address address;
        public boolean isFemale;
        public boolean isEmployed;
        public boolean isHomeOwner;
        public Person(FullName fullName, Address address, boolean isFemale, boolean isEmployed, boolean isHomeOwner) {
        this.fullName = fullName;
        this.address = address;
        this.isFemale = isFemale;
        this.isEmployed = isEmployed;
        this.isHomeOwner = isHomeOwner;
        }
        }
- FullName.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	public class FullName {
public String lastName;
public String firstName;
public String middleName;
public String salutation;
public String suffix;
public FullName(String lastName, String firstName, String middleName, String salutation, String suffix) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
this.salutation = salutation;
this.suffix = suffix;
}
}
- Address.class
1
2
3
4
5
6
7
8
9
10
	public class Address {
public String streetAddress;
public String city;
public String state;
public Address(String streetAddress, String city, String state) {
this.streetAddress = streetAddress;
this.city = city;
this.state = state;
}
}
- 问题:参数对象可能被滥用。如果一个开发者纯粹为了减少参数数量,把联系不紧的几个参数强捆在一个类中这肯定是行不通的,在可读性上甚至适得其反。
  • Builder模式:

    • 需求:当一个对象需要不同的参数构造方法?不能写5*5满足所有的需求吧
    • 适用范围:构建对象时,如果碰到类有很多参数——其中很多参数类型相同而且很多参数可以为空时,使用Builder模式来完成。当参数数量不多、类型不同而且都是必须出现时,通过增加代码实现Builder往往无法体现它的优势。在这种情况下,理想的方法是调用传统的构造函数。再者,如果不需要保持不变,那么就使用无参构造函数调用相应的set方法吧。
    • 代码

      • Person.class
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      public class Person {
      public FullName fullName;
      public Address address;
      public boolean isFemale;
      public boolean isEmployed;
      public boolean isHomeOwner;

      public Person(FullName fullName, Address address, boolean isFemale, boolean isEmployed, boolean isHomeOwner) {
      this.fullName = fullName;
      this.address = address;
      this.isFemale = isFemale;
      this.isEmployed = isEmployed;
      this.isHomeOwner = isHomeOwner;
      }

      public static class Builder {
      private FullName fullName;
      private Address address;
      private boolean isFemale;
      private boolean isEmployed;
      private boolean isHomeOwner;

      /**
      * 如果有必填参数这里可以构造必填构造方法
      */

      public Builder() {
      }

      public Builder setFullName(FullName fullName) {
      this.fullName = fullName;
      return this;
      }

      public Builder setAddress(Address address) {
      this.address = address;
      return this;
      }

      public Builder setFemale(boolean female) {
      isFemale = female;
      return this;
      }

      public Builder setEmployed(boolean employed) {
      isEmployed = employed;
      return this;
      }

      public Builder setHomeOwner(boolean homeOwner) {
      isHomeOwner = homeOwner;
      return this;
      }

      public Person create() {
      return new Person(fullName, address, isFemale, isEmployed, isHomeOwner);
      }
      }
      }
- FullName.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class FullName {
public String lastName;
public String firstName;
public String middleName;
public String salutation;
public String suffix;

public FullName(String lastName, String firstName, String middleName, String salutation, String suffix) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
this.salutation = salutation;
this.suffix = suffix;
}

public static class Builder {
private String lastName;
private String firstName;
private String middleName;
private String salutation;
private String suffix;

public Builder() {
}

public Builder setLastName(String lastName) {
this.lastName = lastName;
return this;
}

public Builder setFirstName(String firstName) {
this.firstName = firstName;
return this;
}

public Builder setMiddleName(String middleName) {
this.middleName = middleName;
return this;
}

public Builder setSalutation(String salutation) {
this.salutation = salutation;
return this;
}

public Builder setSuffix(String suffix) {
this.suffix = suffix;
return this;
}

public FullName create() {
return new FullName(lastName, firstName, middleName, salutation, suffix);
}
}
}
- Address.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Address {
public String streetAddress;
public String city;
public String state;
public Address(String streetAddress, String city, String state) {
this.streetAddress = streetAddress;
this.city = city;
this.state = state;
}
public static class Builder {
private String streetAddress;
private String city;
private String state;

public Builder() {
}

public Builder setStreetAddress(String streetAddress) {
this.streetAddress = streetAddress;
return this;
}

public Builder setCity(String city) {
this.city = city;
return this;
}

public Builder setState(String state) {
this.state = state;
return this;
}

public Address create() {
return new Address(streetAddress, city, state);
}
}
}
调用的地方
1
2
3
4
5
6
7
8
 public static void main(String[] args) {
FullName fullName = new FullName.Builder().setFirstName("yes")
.setLastName("no").create();
Address address = new Address.Builder().setCity("china").setState("12")
.create();
Person person = new Person.Builder().setAddress(address)
.setFullName(fullName).create();
}
- 优点:客户端代码的可用性和可读性得到了大大提高,构造函数的参数数量明显减少调用起来非常直观。单个builder构建多个对象时Builder参数可在创建期间进行调整,还可以根据对象不同而进行改变,有效的避免重载构造函数。 - 缺点:增加代码量,代码变得更加冗长(相比较参数数量的增加,相同类型的参数混在一起,可选参数的增加而言,改善代码可读性更有价值)
  • 重载

    • 适用范围:方法中参数可选参数或者参数中指定参数相同
    • 代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public String name(String name,int year) {
      return name+year;
      }
      /**
      * 重载
      * @param name
      * @return
      */
      public String name(String name) {
      return name+"null";
      }
- 优点:遇到可选参数或者默认参数时,使用方法重载会十分有效。

Swift与JavaScript交互 - 刘康

概述

iOS原生应用和web页面的交互大致上有这几种方法:

  • iOS7之后的JavaScriptCore
  • 拦截协议
  • 第三方框架WebViewJavaScriptBridge : 是基于拦截协议进行的封装,学习成本相对JavaScriptCore较高
  • iOS8之后的WKWebView : iOS8之后推出的,还没有成为主流使用。

关于JavaScriptCore

涉及到的几种类型:

  • JSContext: JSContext是代表JS的执行环境,通过-evaluateScript:方法就可以执行JS代码
  • JSValue: JSValue封装了JS与ObjC中的对应的类型,以及调用JS的API等
  • JSExport: JSExport是一个协议,遵守此协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来,才能调用

Swift与JS交互方式

两种调用JS代码的方法:

1、直接调用JS代码

2、在Swift中通过JSContext注入模型,然后调用模型的方法

直接调用JS代码

我们可以不通过模型来调用方法,也可以直接调用方法

1
2
3
4
5
6
7
8
9
10
11
// 直接JS方法
let context = JSContext()
context.evaluateScript("var num = 8")
context.evaluateScript("function double(value) {return value * 2}")
// 调用并打印结果
let result = context.evaluateScript("double(num)")
print("result = \(result)")
// 可通过下标来获取JS方法
let doubleFunc = context.objectForKeyedSubscript("double")
let double10Result = doubleFunc.callWithArguments(["10"])
print("doubleFunc(10), result = \(double10Result.toString())")

这种方式是没有注入模型到JS中的。这种方式使用起来不太合适,通常在JS中有很多全局的函数,为了防止名字重名,使用模型的方式是最好不过了。通过我们协商好的模型名称,在JS中直接通过模型来调用我们在Swift中所定义的模型所公开的API。
注入模型的交互

注入模型的交互

首先,我们需要先定义一个协议,而且这个协议必须要遵守JSExport协议。

All methods that should apply in Javascript,should be in the following protocol.

注意,这里必须使用@objc,因为JavaScriptCore库是ObjectiveC版本的。如果不加@objc,则调用无效果。

1
2
3
4
5
6
objc protocol JavaScriptSwiftDelegate: JSExport {
func callSystemCamera();
func showAlert(title: String, msg: String);
func callWithDict(dict: [String: AnyObject])
func jsCallObjcAndObjcCallJsWithDict(dict: [String: AnyObject]);
}

接下来,我们还需要定义一个模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@objc class SwiftJSModel: NSObject, JavaScriptSwiftDelegate {
weak var controller: UIViewController?
weak var jsContext: JSContext?

func callSystemCamera() {
showAlert("JS调用Native", msg: "这里应该去调用摄像头");

let jsFunc = self.jsContext?.objectForKeyedSubscript("jsFunc");
jsFunc?.callWithArguments([]);
}

func showAlert(title: String, msg: String) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let alert = UIAlertController(title: title, message: msg, preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "ok", style: .Default, handler: nil))
self.controller?.presentViewController(alert, animated: true, completion: nil)
}
}

func callWithDict(dict: [String : AnyObject]) {
print("JS call objc method: callWithDict, args: %@", dict)
showAlert("JS调用Native方法:callWithDict", msg: "这里处理callWithDict")
}

func jsCallObjcAndObjcCallJsWithDict(dict: [String : AnyObject]) {
print("js call objc method: jsCallObjcAndObjcCallJsWithDict, args: %@", dict)
showAlert("JS调用Native方法:jsCallObjcAndObjcCallJsWithDict", msg: "dict:\(dict)")

let jsParamFunc = self.jsContext?.objectForKeyedSubscript("jsParamFunc");
let dict = NSDictionary(dictionary: ["age": 18, "height": 168, "name": "lili"])
jsParamFunc?.callWithArguments([dict])
}
}

接下来,我们在controller中在webview加载完成的代理中,给JS注入模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func webViewDidFinishLoad(webView: UIWebView) {
let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
let model = JSObjCModel()
model.controller = self
model.jsContext = context
self.jsContext = context

// 这一步是将OCModel这个模型注入到JS中,在JS就可以通过OCModel调用我们公暴露的方法了。
self.jsContext?.setObject(model, forKeyedSubscript: "OCModel")
let url = NSBundle.mainBundle().URLForResource("test", withExtension: "html")
self.jsContext?.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding));

self.jsContext?.exceptionHandler = {
(context, exception) in
print("exception @", exception)
}
}

JSContext是通过webView的valueForKeyPath获取的,其路径为documentView.webView.mainFrame.javaScriptContext。
这样就可以获取到JS的context,然后为这个context注入模型对象。
先写两个JS方法:

1
2
3
4
5
6
7
function jsFunc() {
alert('Objective-C call js to show alert');
}
// 注意哦,如果JS写错,可能在OC调用JS方法时,都会出错哦。
var jsParamFunc = function(argument) {
document.getElementById('jsParamFuncSpan').innerHTML = argument['name'];
}

这里定义了两个JS方法,一个是jsFunc,不带参数。
另一个是jsParamFunc,带一个参数。

当点击第一个按钮:Call ObjC system camera时,
通过OCModel.callSystemCamera(),就可以在HTML中通过JS调用OC的方法。
在Swift代码callSystemCamera方法体中,添加了以下两行代码,就是获取HTML中所定义的JS就去jsFunc,然后调用它。

1
2
let jsFunc = self.jsContext?.objectForKeyedSubscript("jsFunc");
jsFunc?.callWithArguments([]);

这样就可以在JS调用Siwft方法时,也让Swift反馈给JS。
注意:这里是通过objectForKeyedSubscript方法来获取变量jsFunc。
方法也是变量。看看下面传字典参数:

1
2
3
4
5
6
7
func jsCallObjcAndObjcCallJsWithDict(dict: [String : AnyObject]) {
print("js call objc method: jsCallObjcAndObjcCallJsWithDict, args: %@", dict)

let jsParamFunc = self.jsContext?.objectForKeyedSubscript("jsParamFunc");
let dict = NSDictionary(dictionary: ["age": 18, "height": 168, "name": "lili"])
jsParamFunc?.callWithArguments([dict])
}

获取HTML中定义的jsParamFunc方法,然后调用它并传了一个字典作为参数。

移动周分享-第50期

先看命令帮助的讲解 - 王胜

1
2
3
4
5
6
7
8
9
10
11
12
13
Victors-MPB:hello wangsheng$ git reset -h
usage: git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]
or: git reset [-q] <tree-ish> [--] <paths>...
or: git reset --patch [<tree-ish>] [--] [<paths>...]

-q, --quiet be quiet, only report errors
--mixed reset HEAD and index
--soft reset only HEAD
--hard reset HEAD, index and working tree
--merge reset HEAD, index and working tree
--keep reset HEAD but keep local changes
-p, --patch select hunks interactively
-N, --intent-to-add record only the fact that removed paths will be added later

各种模式对比

模式 HEAD是否重置 Index是否重置 working区是否重置
—mixed『缺省模式』
—soft
—hard
—merge
—keep

SourceTree 重置分析

重置 -> 丢弃文件变更

  • 操作截图

    git-reset-mixed-partial

  • 实际执行的命令

1
2
3
git -c diff.mnemonicprefix=false -c core.quotepath=false -c credential.helper=sourcetree reset -q HEAD -- a.txt c.txt 	
git -c diff.mnemonicprefix=false -c core.quotepath=false -c credential.helper=sourcetree checkout HEAD -- a.txt
Completed successfully
  • 结果
1
2
3
4
5
6
7
8
9
10
11
Victors-MPB:hello wangsheng$ git st
On branch feature2
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)

Untracked files:
(use "git add <file>..." to include in what will be committed)

c.txt

nothing added to commit but untracked files present (use "git add" to track)

可以看出这样操作,git的上下文环境还在merge状态,这种情况一不小心就容易当做重置干净了,继续干自己的事情了,殊不知把别人的改动丢弃了。

重置 -> 重置所有

  • 操作截图

    git-reset-hard-all

  • 实际执行的命令

1
2
3
4
git -c diff.mnemonicprefix=false -c core.quotepath=false -c credential.helper=sourcetree reset -q --hard HEAD -- 

git -c diff.mnemonicprefix=false -c core.quotepath=false -c credential.helper=sourcetree submodule update --init --recursive
Completed successfully
  • 结果
1
2
3
Victors-MPB:hello wangsheng$ git st
On branch feature2
nothing to commit, working directory clean

快速集成TouchID - 杨志平

iPhone 5s,iOS 8,iOS7时未开放

依赖 LocalAuthentication.framework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 判断设备支持状态
LAContext *context = [[LAContext alloc] init];
[context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil];

// 如果可以
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"reason" reply:^(BOOL success, NSError *error) {
if (success) {
//验证成功,主线程处理UI
}
else
{
// 重点:错误处理,分为多种错误
// 有验证失败、取消、手动输入密码等等相对应的逻辑处理
// 详见demo
}
}];

错误类型

  • LAErrorAuthenticationFailed
  • LAErrorUserCancel
  • LAErrorUserFallback
  • LAErrorSystemCancel
  • LAErrorPasscodeNotSet
  • LAErrorTouchIDNotAvailable
  • LAErrorTouchIDNotEnrolled
  • LAErrorTouchIDLockout NS_ENUM_AVAILABLE(10_11, 9_0)
  • LAErrorAppCancel NS_ENUM_AVAILABLE(10_11, 9_0)
  • LAErrorInvalidContext NS_ENUM_AVAILABLE(10_11, 9_0)

dispatch_sync的坑 - 潘君

起因

dispatch_sync自己创建的队列 仍旧有可能在main thread上执行block

  • GCD只是管理一个线程池 不保证一定在某一个thread或不在某一个thread执行

  • 非main程队列也有可能调用主线程

  • 为了优化 有可能在当前线程中执行block

问题

如何不让dispatch_sync卡主线程呢

答: 别在主线程调用此函数 哈哈哈

用途

  • 锁的功能

  • 确保在主线程执行block

AFNetworking在调用前判断是否是主线程

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

if (![[NSThread currentThread] isMainThread]) {

dispatch_sync(dispatch_get_main_queue(), ^{

[self transitionToNextPhase];

});

return YES;

}

参考

移动周分享-第49期

Android-mvp 吴明

  • mvc与mvp对比
  • mvp

    • model:依然是业务逻辑和实体模型
    • view:对应于Activity,负责View的绘制以及与用户交互
    • presenter:负责完成View于Model间的交互
    • 关系图
      mvp关系图
  • View定义定义包括:Activity,xml,Fragment,adapter

  • Google定义MVP

Google-mvp

Google-MVP-demo

开源工具SWIG - 杨俊构

什么是SWIG

  • 官方网站 http://www.swig.org/
  • GitHub开源 https://github.com/swig/swig

  • SWIG是个帮助使用C或者C++编写的软件能与其它各种高级编程语言进行嵌入联接的开发工具。SWIG能应用于各种不同类型的语言包括常用脚本编译语言例如Perl, PHP, Python, Tcl, Ruby and PHP。支持语言列表中 也包括非脚本编译语言,例如C#, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), Java, Modula-3, OCAML以及R,甚至是编译器或者汇编的计划应用(Guile, MzScheme, Chicken)。

  • SWIG普遍应用于创建高级语言解析或汇编程序环境,用户接口,作为一种用来测试C/C++或进行原型设计的工具。SWIG还能够导出 XML或Lisp s-expressions格式的解析树。SWIG可以被自由使用,发布,修改用于商业或非商业中。

快速开发

参考 http://www.swig.org/tutorial.html

  • eg: 将 C语言转化成java (Windows下)

    1. 下载Swig工具包

      SWIG在三大主力平台都可以安装,下载到的是一个zip压缩包,解压然后配置好环境变量(SWIG_HOME,Path)即可使用了。【真的一直在更新

    2. 新建example.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/* File : example.c */

#include <time.h>
double My_variable = 3.0;

int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}

int my_mod(int x, int y) {
return (x%y);
}

char *get_time()
{
time_t ltime;
time(&ltime);
return ctime(&ltime);
}
  1. 编写接口文件example.i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        /* example.i */
%module example
%{
/* Put header files here or function declarations like below */
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
%}

extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
```
4. 建立Java模块
> SWIG也会产生JNI代码以便Jave代码进入C/CC++。以下是建立一个Jave模块的事例(cygwin):
$ swig -java example.i

$ gcc -c example.c example_wrap.c -I/c/jdk1.3.1/include -I/c/jdk1.3.1/include/win32

$ gcc -shared example.o  example_wrap.o -mno-cygwin -Wl,--add-stdcall-alias  -o example.dll

$ cat main.java
public class main {
public static void main(String argv[]) {
    System.loadLibrary("example");
    System.out.println(example.getMy_variable());
    System.out.println(example.fact(5));
    System.out.println(example.get_time());
}
}
$ javac main.java
$ java main
3.0
120
Mon Mar  4 18:20:31  2002
$
1
2
3
4
5
6
7
8
9
10
11
      
> (1)swig -java example.i 执行后会生成

> >符合JNI语法的C文件: example_wrap.c
> >生成Java类文件:exampleJNI.java,example.java

> (2)SWIG创建的wrapper文件可直接用来编译连接产生共享库,不需要再对生成文件进行编辑。
> (3)对Java程序员来说,SWIG是把C/C++代码嵌入到Java脚本的理想工具。SWIG理解C/C++申明,因此它可以解析C/C++头文件并产生Java模型并对其进行编码,编译之后,你就可以在Java中使用。

- SWIG 懒人方法
> 并非总是需要写一个专门的接口文件。如果你有一个头文件,你可以直接在其中包含SWIG接口,如例:

%module example
%{
/ Includes the header in the wrapper code /

include “header.h”

%}

/ Parse the header file to generate wrappers /
%include “header.h”

1
2

> 另外,有些人可能只包括SWIG条件指令在头文件中。例如:

ifdef SWIG

%module example
%{

include “header.h”

%}

endif

extern int fact(int n);

```

编写JNI封装器的代码,还有处理数据类型的转换耗时且费力,所以要用到 SWIG是java程序的福利。

总结

  • SWIG是一个运用于编译环节的软件开发工具,它能生成出代码来使得用 C/C++ 编写的代码可以同其它编程语言连接在一起。
  • SWIG 不只是能被用在 Android 和 Java 开发领域,它是一个扩展性方面很强大的工具,也可以用来自动生成其它编程语言的代码。
  • 总之,安装和使用swig都是非常简单的事情。使用swig能够大大减轻程序员的痛苦程度,帮助你更快的完成你想要的功能。

参考 NDK学习笔记(三)SWIG:自动生成JNI代码

移动周分享-第48期

Android单元测试框架 - 吴明

  • Android单元测试
    • 在Android项目中,单元测试的对象是组件状态、控件行为、界面元素和自定义函数。不推荐对每个函数进行一对一的测试,像onStart()、onDestroy()这些周期函数并不需要全部覆盖到。
  • 框架比较
    • JUnit
      • 原生单元测试
      • 不能让Activity执行到resume的状态
      • @王进已讲
    • AndroidTest
      • 运行在Android环境
    • Instrumentation
      • 运行在模拟器上
    • Robotium
      • 类似于Selenium的测试框架
      • 运行在模拟器上
      • 通过对模拟器的操作或者mock,来触发函数调用,进而对其结果进行验证
    • Robolectric+Mock(Mockito)
      • 运行在JVM上速度快
      • Jenkins周期性执行
      • 无需准备Android环境
      • 实现了Android中对XML的解析,模拟了View,Layout,以及资源的加载
      • 一些测试对象依赖度较高而需要解除依赖的场景(如网络)使用Mock框架
  • Robolectric

    • Android Studio配置

      • build.gradle添加库

        1
        testCompile "org.robolectric:robolectric:3.0"
      • Build Variants设置
        Unit Tests

      • Linux和Mac平台的需要配置下才能运行:在Android Studio中run边上有一个app选择,这里选择”Edit Configurations” 出现编辑界面
        Run/Debug Configurations
        在左边中新建JUnit,Default->JUnit
      • 测试代码
      • 中间出现问题
        • Android SDK目前只支持到22,5.1
  • 单元测试的目标函数主要有三种:

    1. 有明确的返回值,如上图的dosomething(Boolean param),做单元测试时,只需调用这个函数,然后验证函数的返回值是否符合预期结果。
    2. 这个函数只改变其对象内部的一些属性或者状态,函数本身没有返回值,就验证它所改变的属性和状态。
    3. 一些函数没有返回值,也没有直接改变哪个值的状态,这就需要验证其行为,比如点击事件。
    • 既没有返回值,也没有改变状态,又没有触发行为的函数是不可测试的,在项目中不应该存在。当存在同时具备上述多种特性时,本文建议采用多个case来真对每一种特性逐一验证,或者采用一个case,逐一执行目标函数并验证其影响。
    • 构造用例的原则是测试用例与函数一对一,实现条件覆盖与路径覆盖。Java单元测试中,良好的单元测试是需要保证所有函数执行正确的,即所有边界条件都验证过,一个用例只测一个函数,便于维护。在Android单元测试中,并不要求对所有函数都覆盖到,像Android SDK中的函数回调则不用测试。
  • 测试用例

    • 组件

      • Activity测试

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        		@Test
        public void testLifecycle() {
        ActivityController<MainActivity> activityController = Robolectric.buildActivity(MainActivity.class).create().start();
        Activity activity = activityController.get();
        Button action_next = (Button) activity.findViewById(R.id.next);
        Assert.assertEquals("next", action_next.getText().toString());
        // error
        // Assert.assertEquals("testLifecycle", action_next.getText().toString());
        }

        ````
        - Service
        - BroadcastReceiver
        - Fragment
        - 控件行为
        - 跳转
        - 界面元素
        - Toast

        @Test
        public void testToast() {
        Button action_toast = (Button) mActivity.findViewById(R.id.action_toast);
        action_toast.performClick();
        Assert.assertEquals(“hello”, ShadowToast.getTextOfLatestToast());
        }

        1
        2
        - Dialog
        - UI控件

        @Test
        public void testStartEndActivity() {
        Button action_next = (Button) mActivity.findViewById(R.id.next);
        action_next.performClick();

        Intent intent = new Intent(mActivity, EndActivity.class);

        Intent actionIntent = ShadowApplication.getInstance().getNextStartedActivity();
        Assert.assertEquals(intent, actionIntent);
        }

        1
        	- 自定义函数
        	- 网络请求
        - assertJ-android断言库
        - 资料
        	- [参考1](https://hkliya.gitbooks.io/unit-test-android-with-robolectric/content/0-introduction.html)
        	- [美团Android单元测试](http://tech.meituan.com/Android_unit_test.html)
        	- [测试用例](http://www.jianshu.com/p/9d988a2f8ff7)
        	
        	
        	
        ## 无主题 - 陈奎
        ###NodeJS 安装
        -	到 [nodejs 下载](https://nodejs.org/download/release/) 下载 `node-v5.9.0-linux-x64.tar.gz`,上传到 linux 服务器 `/usr/local/nodejs` 目录;
        -	在 nodejs 目录执行

tar zxvf node-v5.9.0-linux-x64.tar.gz

1
解压,进入解压后的bin目录下,执行ls会看到两个文件node,npm. 然后执行

./node –v

1
2
如果显示出版本号说明我们下载的程序包是没有问题的。 
- 配置环境变量,

sudo vi /etc/profile

1
export PATH的上一行添加如下内容

PATH=$PATH:/usr/local/nodejs/node-v5.9.0-linux-x64/bin

1
编辑完成后按Esc键 然后输入 :wq 按回车保存,退出vi ,执行

source /etc/profile

1
可以使变量立即生效,然后执行

echo $PATH

1
看输出内容是否包含自己添加的内容,然后到任意目录下去执行一次执行

node -v

1
2
```
npm -v

ok 搞定了,需要注意的是,通过 source /etc/profile,只是让变量临时生效了,如果此时重新开一个终端运行node会提示找不到命令,这个问题重启或者注销之后可以解决,如果不想重启的话,再次执行 source /etc/profile 即可。

newman 安装

  • 官网在这里newman
  • 执行
    1
    npm install -g newman@2

即可,安装成功后执行

1
newman –h

可以显示帮助信息。

mail 安装

  • 使用
    1
    yum search mail

发现有 mailx.x86_64 : Enhanced implementation of the mailx command 说明可以安装,执行

1
yum install mailx -y

搞定,执行如下命令即可发送邮件

1
echo "this is send content" | mail -s "send from linux" test@test.com

  • 默认情况下发送的邮件会被放到垃圾邮件里面,可以执行
    1
    sudo vi /etc/mail.rc

在其中添加如下配置

1
2
3
4
5
set from=test@163.com
set smtp=smtp.163.com
set smtp-auth-user=test
set smtp-auth-password=123456
set smtp-auth=login

这样发送的邮件就不会被放到垃圾邮件里面,也可以从文件中获取发送内容,如下

1
mail -s "mail title" 123456@163.com < outfile-1458185763.txt

参考:LINUX下通过外部SMTP发邮件 (直接抛弃sendmail和postfix)

插件推荐 - 王进

插件 简介
ParcelableGenerator Android中的序列化有两种方式, 分别是实现Serializable接口和Parcelable接口, 但在Android中是推荐使用Parcelable, 只不过我们这种方式要比Serializable方式要繁琐,那么有了这个插件一切就ok了
GsonFormat 现在大多数服务端api都以json数据格式返回, 而客户端需要根据api接口生成相应的实体类, 这个插件把这个过程自动化了, 赶紧使用起来吧
ADB WIFI 每天只需用USB线连接一次,就可以将USB连接切换成WIFI连接,去掉讨厌的数据线连接

移动周分享-第47期

Swift 高阶函数及参数的省略 - 杨志平

常见定义函数

1
2
3
4
5
6
let normalFunc = {
() -> Int in
return 10086
}
let normalResult = normalFunc()
// normalResult = 10086

函数参数的『省略』

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 以数组排序函数为例:

var numberArr = [1,7,4,6,3,2,5]

let res1 = numberArr.sort()

let res2 = numberArr.sort {
(num1: Int, num2: Int) -> Bool in
return num1>num2
}

let res3 = numberArr.sort {
(num1: Int, num2: Int) in
return num1>num2
}

let res4 = numberArr.sort {
(num1, num2) in
return num1>num2
}

let res5 = numberArr.sort {
return $0 > $1
}

let res6 = numberArr.sort {
$0 > $1
}

let res7 = numberArr.sort(>)

变化过程

{ (num1: Int, num2: Int) -> Bool in return num1>num2 }
{ (num1: Int, num2: Int) in return num1>num2 }
{ (num1, num2) in return num1>num2 }
{ return $0 > $1 }
{ $0 > $1 }

函数式编程

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func square(a:Float) -> Float {
return a * a
}
func cube(a:Float) -> Float {
return a * a * a
}
func averageSumOfSquares(a:Float,b:Float) -> Float {
return (square(a) + square(b)) / 2.0
}
func averageSumOfCubes(a:Float,b:Float) -> Float {
return (cube(a) + cube(b)) / 2.0
}

// 像潘君以前分享Swift的Currying(柯里化),可以灵活调配使用

func averageOfFunction(a a:Float,b:Float,f:(Float -> Float)) -> Float {
return (f(a) + f(b)) / 2
}

let squareResult1 = averageOfFunction(a: 2, b: 4, f: square)

let squareResult2 = averageOfFunction(a: 2, b: 4, f: {
(a: Float) -> Float in
return a * a
})
squareResult2

//{(x: Float) -> Float in return x * x}
//{x in return x * x}
//{x in x * x}
//{$0 * $0}

let squareResult3 = averageOfFunction(a: 2, b: 4, f: {$0 * $0})
squareResult3

案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 正统高阶函数样式
func sum1(value: Int) -> (Int -> Int) {
func adder(otherValue: Int) -> Int {
return otherValue + value
}
return adder
}

// 省略后
func sum2(value: Int) -> (Int -> Int) {
return { $0 + value }
}

let result1 = sum1(2)(3)
let result2 = sum2(5)(3)

// 改造函数完毕
func funcMathMethod1(first: Int -> Int, _/*起到变量匿名作用*/ second: Int -> Int) -> Int -> Int {
return { second(first($0)) }
}

let f1 = funcMathMethod1({$0 + 2}, {$0 / 3})
f1(7)

let f2 = funcMathMethod1({$0 * $0}, {$0 / 4})
f2(10)

// Tip: 使用函数式编程,要是用得不好容易造成可读性很差,那优化如下
typealias MathFunc = Int -> Int
func funcMathMethod2(f: MathFunc, _ s: MathFunc) -> MathFunc {
return { s(f($0)) }
}
let readability = funcMathMethod1({$0 + 2}, {$0 / 3})
readability(7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// map
var numberArr = [1,7,4,6,3,2,5]

let mapArray = numberArr.map { (num: Int) -> String in
return "第\(num)名"
}
let mapArray2 = numberArr.map({"第\($0)名"})
mapArray2

// 可选型随意解包 str 可以为 nil
let str: String? = "1234567890"
let mapStr = str.map({"第\($0)名"})
mapStr


// filter
let filArr = numberArr.filter { (num: Int) -> Bool in
return num > 4
}
let filArr2 = numberArr.filter({ $0 > 4 })
filArr2


// reduce
let sum = numberArr.reduce(0) { (presum:Int, num:Int) -> Int in
return presum + num
}
let sum2 = numberArr.reduce(10, combine: {$0 + $1})
sum2
let sumToStr = numberArr.reduce("") { (str: String, num: Int) -> String in
str + String(num)
}


// 求一个数组中偶数的平方和(一口气使用swift提供的三个高阶函数)
//[1,2,3,4,5,6,7] -> [2,4,6] -> 4+16+36 = 56
let result = numberArr.filter({$0%2==0}).map({$0*$0}).reduce(0, combine: {$0+$1})
result // result = 56

Swift高阶函数面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 面试题:用数组的 reduce 方法实现 map 的功能。
let arr = [1, 3, 2]
// map简易实现
let res = arr.map({$0*2})

// Array.reduce(<#T##initial: T##T#>, combine: <#T##(T, Int) throws -> T#>)
// 输出数据类型与初始化占位数据类型(initial)一致
let res2 = arr.reduce([]) {
(a: [Int], element: Int) -> [Int] in
var t = Array(a)
t.append(element * 2)
return t
}


// 面试题:用 reduce 方法一次求出数组中奇数的和、以及偶数乘积
// 使用元组,注意占位数据(0, 1),第一联合数据a :(Int, Int),函数输出数据(Int, Int) 三者类型一致

let arr2 = [1, 3, 2, 4]

let res3: (Int, Int) = arr2.reduce((0, 1)) {
(a :(Int, Int), element: Int) -> (Int, Int) in
if element % 2 == 0 {
return (a.0, a.1 * element)
} else {
return (a.0 + element, a.1)
}
}

移动周分享-第46期

Moto 360 Sport初体验 - 吴明

  • 前奏
    • 美版
    • 第一连接需要连接Android手机
    • 需要翻墙
  • 应用
    • Android Wear
    • WearADay
  • iPhone支持
    • 无需越狱,连接应用WearADay
      • 如图1
      • 如图2
      • 如图3
      • 如图4
      • 如图5
    • 包括来电,信息以及各种应用的通知,包括 QQ,微信等等在 iPhone 和 Moto360 配对成功之后都可以在 Moto360 上显示
      • 如图1
      • 如图2
      • 如图3
      • 如图4
      • 如图5
      • 如图6
  • Android支持
    • 微信:表情回复,语音回复,文字回复
    • QQ:未测
  • 缺陷
    • 分辨率不高
    • 目前测试没有好的输入法每次提示都是在连接手机输入但每次都是提示手机不在附近
  • 开发者模式安装应用

    1. 打开手机以及手表的开发者选项
      • 手机端:设置-关于手机,找到“版本号”,对着它连续点击7下即可开启,开机后进入“设置”-“开发者选项”,勾选“USB调试
      • 手机开启develp
      • 手表端:长按右侧按钮唤出设置界面-“About”,找到“Build number”,对着它连续点击7下即可开启,开启后进入设置-“Developer options”,按图所示开启“ADB debugging”和“Debug over Bluetooth”
      • Moto开启develp
    2. 打开Android Wear软件,勾选设置菜单里的“Debugging over Bluetooth”。
      • 你可以发现“Host”是disconnected未连接状态,这是代表手表端;“Target”是手机端,connected已连接状态;怎么让手表端的显示已连接呢?请继续往下看
      • Android Wear打开蓝牙调试
    3. 把手机连接上电脑,在adb工具的文件夹里调出命令行,依次输入以下命令

      1
      2
      adb forward tcp:4444 localabstract:/adb-hub
        adb connect localhost:4444

      分两次输入(电脑需支持adb命令,如果不支持请百度Mac配置adbWindow配置adb两个链接是未测试)

    • 输完以上两条命令后,留意手机,会弹出允许USB调试的窗口,勾选总是允许,点击OK即可,这时你就发现代表手表端的“Host”就变成connected已连接状态了,嗯,这就代表可以再电脑执行adb命令来控制手表了
    • 提示是否调试
    1. 相关命令

      • adb devices (显示已连接的设备,如图所示,会显示出2个设备名,分别对应手机和手表)
      • 如图

      • adb -s 手机ID号 install xxx.apk(给手机安装软件,要注意加上手机ID号,比如我的就是 adb -s TA26901ZLK install xxx.apk )

      • adb -s localhost:4444 install xxx.apk(给手表安装软件,MOTO 360的软件apk可以通过Android Wear软件专题下载,这里是通过手机的蓝牙给手表安装的,所以传输速度稍慢,要耐心等待,安装成功后会有提示)
      • adb -s localhost:4444 unistall xxx(卸载手表端的某个软件,比如adb -s localhost:4444unistall com.whirlscape.minuum)
      • 查看Moto 360所有app的packages

        1
        adb -s localhost:4444 shell pm list packages
      • 卸载指定包名的app

        1
        adb -s localhost:4444 uninstall [-k] <insert package name here>

Android NFC - 杨俊构

NFC 定义

  • From Android dev

    Near Field Communication (NFC) is a set of short-range wireless technologies, typically requiring a distance of 4cm or less to initiate a connection. NFC allows you to share small payloads of data between an NFC tag and an Android-powered device, or between two Android-powered devices.

    Tags can range in complexity. Simple tags offer just read and write semantics, sometimes with one-time-programmable areas to make the card read-only. More complex tags offer math operations, and have cryptographic hardware to authenticate access to a sector. The most sophisticated tags contain operating environments, allowing complex interactions with code executing on the tag. The data stored in the tag can also be written in a variety of formats, but many of the Android framework APIs are based around a NFC Forum standard called NDEF (NFC Data Exchange Format).

NFC应用

NFC通信技术原理

  • 支持NFC的设备可以在主动或被动模式下交换数据

NFC Android开发

  • NFC 感应事件分发

  • NFC 感应数据处理流程

  • NFC Card Readder Demo

参考 Android 开发 http://developer.android.com/guide/topics/connectivity/nfc/nfc.html

NFC标签种类

NFC 开发的建议

1.多了解芯片的功能和卡片的类型,

2.获得更多芯片厂商的资料,更多的接口信息

3.了解数据的解析方法

4.多打log,写更详细的log给自己,更简明的log给使用者

NFC展望

1.数据安全的问题日益严重

2.手机SIM卡的NFC功能会逐渐发展

3.卡片会更智能,点到点的应用会更多

4.应用更广,价格更便宜

Android Support Library 23.2 推出之后,增加了几个功能,例如支持Vector Drawables 和Animated Vector Drawables;增加AppCompat DayNight 主题;Design 库中增加Bottom Sheets,RecyclerView 支持 auto-measurement,之前的wrap_content ,match_parent 都将可以发挥作用等等

公司的App 之前使用过第三方的[BottomSheet] (https://github.com/soarcn/BottomSheet ),现在Android 有自己的BottomSheet 那还不赶紧换成原生的。然而好事多磨,Android 原生BottomSheet 资料太少,深研下去发现BottomSheet 就是个大坑!

BottomSheet 的使用 - 王进

BottomSheet 使用需要CoordinatorLayout作为父布局,BottomSheet 的布局作为CoordinatorLayout 的子布局,并且BottomSheetBehavior(比如加上app:layout_behavior=”android.support.design.widget.BottomSheetBehavior”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:fitsSystemWindows="true"

tools:context="android.com.bottomsheets.MainActivity">



<android.support.design.widget.AppBarLayout

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:theme="@style/AppTheme.AppBarOverlay">



<android.support.v7.widget.Toolbar

android:id="@+id/toolbar"

android:layout_width="match_parent"

android:layout_height="?attr/actionBarSize"

android:background="?attr/colorPrimary"

app:popupTheme="@style/AppTheme.PopupOverlay" />



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



<include layout="@layout/content_main" />



<include layout="@layout/bottom_sheet_main" />



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

实际使用过程中主要依靠BottomSheetBehavior来控制BottomSheet的展示及回调。

BottomSheetBehavior 具有五种状态:

  • STATE_COLLAPSED: 默认的折叠状态, bottom sheets只在底部显示一部分布局。显示高度可以通过 app:behavior_peekHeight 设置(默认是0)

  • STATE_DRAGGING : 过渡状态,此时用户正在向上或者向下拖动bottom sheet

  • STATE_SETTLING: 视图从脱离手指自由滑动到最终停下的这一小段时间

  • STATE_EXPANDED: bottom sheet 处于完全展开的状态:当bottom sheet的高度低于CoordinatorLayout容器时,整个bottom sheet都可见;或者CoordinatorLayout容器已经被bottom sheet填满。

  • STATE_HIDDEN : 默认无此状态(可通过app:behavior_hideable 启用此状态),启用后用户将能通过向下滑动完全隐藏 bottom sheet

设置状态:

bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);

回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

// The View with the BottomSheetBehavio

View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);

BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheet));



bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {

@Override

public void onStateChanged(@NonNull View bottomSheet, int newState) {

if (newState == BottomSheetBehavior.STATE_EXPANDED) {

text1.setText("Collapse Me!");

} else {

text1.setText("Expand Me!");

}

}



@Override

public void onSlide(@NonNull View bottomSheet, float slideOffset) {



}

});

}

强调:

  • BottomSheet 点击展示的默认是折叠状态,不是完全展开状况,所有如果需要完全展开,请设置展开状况

BottomSheetDialog

BottomSheetBehavior将能帮你实现 常驻bottom sheet( persistent bottom sheet)的场景, 但这个版本还提供了BottomSheetDialog 和 BottomSheetDialogFragment 来实现 modal bottom sheets的场景。只需要将AppCompatDialog 或者AppCompatDialogFragment分别替换成上述的两个控件,你就拥有了 bottom sheet 风格的对话框

坑1:

然而我们实际我们需要BottomSheetDialog 是展开的,而BottomSheetDialog只展示一部分

原因:BottomSheetDialog默认是STATE_COLLAPSED,所有BottomSheetDialog 依靠peekHight来设置高度,系统BottomSheetDialog 默认高度为256dp(查源码得知),那按理来说我们的BottomSheetDialog 高度该是256dp,但是我们实际发现BottomSheetDialog高度也不等于256dp。我们研究下BottomSheetBehavior的中控制BottomSheetDialog高度源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

@Override

public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {

// First let the parent lay it out

if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {

parent.onLayoutChild(child, layoutDirection);

}

// Offset the bottom sheet

mParentHeight = parent.getHeight();

mMinOffset = Math.max(0, mParentHeight - child.getHeight());

mMaxOffset = mParentHeight - mPeekHeight;

if (mState == STATE_EXPANDED) {

ViewCompat.offsetTopAndBottom(child, mMinOffset);

} else if (mHideable && mState == STATE_HIDDEN) {

ViewCompat.offsetTopAndBottom(child, mParentHeight);

} else if (mState == STATE_COLLAPSED) {

ViewCompat.offsetTopAndBottom(child, mMaxOffset);

}

if (mViewDragHelper == null) {

mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);

}

mViewRef = new WeakReference<>(child);

mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));

return true;

}

通过源码我们可以得知BottomSheetBehavior通过改变child的偏移量而控制BottomSheetDialog的高度,默认状态为STATE_COLLAPSED,child向下移动mMaxOffset高度,从而控制child显示高度为mPeekHeight,这就需要child与parent 顶部对齐,child的getTop 为0;

然而我们再去查看Android的BottomSheetDialog 内中布局R.layout.design_bottom_sheet_dialog,发现我们自定义的的BottomSheetDialog 的contentView 是放置在FrameLayout 中的,然而FrameLayout出于某些原因为垂直居中的,而不是顶部对齐,从而导致BottomSheetDialog在256dp的基础上向下偏移,只展示一部分。

所以我们可以通过下面方法解决BottomSheetDialog 的显示问题

解决方法如下:

  1. 通过bottomSheetDialog中contentView得到parentView,通过parentView 得到BottomSheetBehavior

  2. 测量bottomSheetDialog布局中content的高度,设置peekHight

  3. 设置bottomSheetDialog 的contentView 对应的父布局CoordinatorLayout的Grivity 为Gravity.TOP | Gravity.CENTER_HORIZONTAL;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

final BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(this);

View contentView = View.inflate(this, R.layout.bottom_sheet_avatar, null);



View.OnClickListener clickListener = new View.OnClickListener() {

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.avatar_open_photo:

openCamera();

break;

case R.id.avatar_open_picture:

openPicture();

break;

}

if (bottomSheetDialog != null && bottomSheetDialog.isShowing()) {

bottomSheetDialog.dismiss();

}

}

};



contentView.findViewById(R.id.avatar_open_photo).setOnClickListener(clickListener);

contentView.findViewById(R.id.avatar_open_picture).setOnClickListener(clickListener);

contentView.findViewById(R.id.avatar_cancel).setOnClickListener(clickListener);



bottomSheetDialog.setContentView(contentView);

View parent = (View) contentView.getParent();

BottomSheetBehavior behavior = BottomSheetBehavior.from(parent);

contentView.measure(0, 0);

behavior.setPeekHeight(contentView.getMeasuredHeight());

CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) parent.getLayoutParams();

params.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;

parent.setLayoutParams(params);

bottomSheetDialog.show();

坑2:

当我们设置bottomSheetDialog每次点击后不new,而是直接show的话,然而当我们会bottomSheetDialog 展开后,我们将BottomSheetDialog划下隐藏后, 再点击展示BottomSheetDialog后,会发现页面只是变暗,BottomsheetDialog未展开,这是由于之前我们划下收缩隐藏BottomSheetDialog后,bottomSheetDialogBehavior的状态为隐藏,再次show之后,系统未恢复bottomSheetDialogBehavior的状态,还是隐藏,所以再次点击后页面只是变暗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.showBottomSheetDialogButton:

if (bottomSheetDialog != null) {

bottomSheetDialog.show();

Log.i("0000", "behavior_state:" + bottomSheetDialogBehavior.getState());

Log.i("0000", "STATE_HIDDEN :" + BottomSheetBehavior.STATE_HIDDEN);

return;

}



bottomSheetDialog = new BottomSheetDialog(this);

bottomSheetDialog.show();

}

}

移动周分享-第45期

swift 的 guard & defer - 杨志平

guard 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 原始方法 (强解包可能会出现问题)
func tranIntToString1(x: Int?) -> String {
if x == nil || x! <= 0 {
// 不符合值的要求时,写点代码
return ""
}
// 使用x
return x!.description
}


// 改进
func tranIntToString2(x: Int?) -> String {
if let x = x where x>0 {
return x.description
}
return ""
}

// 保镖模式
// 和上面写法对比
func tranIntToString3(x: Int?) -> String {
guard let x = x where x > 0 else {
return ""
}
// 变量不符合条件判断时,执行下面代码
return x.description
}


// 非可选型
func tranIntToString4(x: Int) -> String {
guard x > 0 else {
return ""
}
return x.description
}


// 常常用于条件判断拦截
var view = UIView(frame: CGRectMake(0,0,80,80))
view.backgroundColor = UIColor.redColor()

UIView.animateWithDuration(0.3) { [weak view]() -> Void in
guard let view = view where view.alpha>0 else {return}
view.alpha = 0
}

懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1.分析 NSArray 是一个闭包的返回值,而这是一个没有参数的闭包 
lazy var dataArray:NSArray = { [] }()

//2.也可以写成这样
lazy var dataArray:NSArray = { return NSArray() }()

//3.从plist文件加载
lazy var dataArray:Array<XWWine> = {
let winePath = NSBundle.mainBundle().pathForResource("wine.plist", ofType: nil)!
let winesM = NSMutableArray(contentsOfFile: winePath);
var tmpArray:Array<XWWine>! = []
for tmpWineDict in winesM! {
var wine:XWWine = XWWine.wineWithDict(tmpWineDict as! NSDictionary)
tmpArray.append(wine)
}
print("我就运行一次")
return tmpArray }()

调用的时候再在家初始化方法(懒加载)

1
2
3
4
lazy private var underlineView: UIView = {
let view = UIView(frame: .zero)
return view
}()

defer 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
defer 关键字
*/

postfix func ++(inout x: Int) -> Int {
defer {
x = x/2
defer {
x += 100
}
}
return x
}
//
//postfix func ++(inout x: Int) -> Int {
// let current = x
// x += 2
// return current
//}


var num = 100
let num2 = num++
num //150
num2 //100

prefix func ++(inout x:Int) -> Int {
x += 2
return x
}

var number = 100
let number2 = ++number
number // 102
number2 // 102

嵌套枚举使用(简直就是动态Model)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public enum MenuScrollingMode {
case ScrollEnabled
case ScrollEnabledAndBouces
case PagingEnabled
}
public enum MenuItemWidthMode {
case Flexible
case Fixed(width: CGFloat)
}
public enum MenuDisplayMode {
case Standard(widthMode: MenuItemWidthMode, centerItem: Bool, scrollingMode: MenuScrollingMode)
case SegmentedControl
case Infinite(widthMode: MenuItemWidthMode)
}

// 初始化:用来携带信息很不错
public var menuDisplayMode = MenuDisplayMode.Standard(widthMode: PagingMenuOptions.MenuItemWidthMode.Fixed(width: 44), centerItem: false, scrollingMode: PagingMenuOptions.MenuScrollingMode.PagingEnabled)

// 实例
func labelWidth(size size: CGSize, widthMode: PagingMenuOptions.MenuItemWidthMode) -> CGFloat {

switch widthMode {
case .Flexible: return ceil(size.width)
case let .Fixed(width): return width
}
}

iOS事件传递 & 事件响应 - 张超耀

什么是响应链

  • 在我们点击屏幕的时候,iphone获取到了用户进行了“单击”这一行为,操作系统把包含这些点击事件的信息包装成UITouch和UIEvent形式的实例,然后找到当前运行的程序,逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链

什么是响应者

  • 在iOS中,能够响应事件的对象都是UIResponder的子类对象。UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用
(void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
1
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);
  • 我们可以看到方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象

事件对象

  • iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的

点击对象

  • UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中,我们通过集合中对象获取用户点击的位置。其中通过- (CGPoint)locationInView:(nullable UIView *)view获取当前点击坐标点,- (CGPoint)previousLocationInView:(nullable UIView *)view获取上个点击位置的坐标点。

  • 为了确认UIView确实是通过UIResponder的点击方法响应点击事件的,我创建了UIView的类别,并重写+ (void)load方法,使用method_swizzling的方式交换点击事件的实现 ——参照最下方demo

响应链传递

  • 上面已经介绍了某个控件在接收到点击事件时的处理,那么系统是怎么通过用户点击的位置找到处理点击事件的view的呢?
  • 存在着这么一个方法:- (nullable UIResponder *)nextResponder,通过方法名我们不难发现这是获取当前view的下一个响应者,逐级获取下一响应者,直到没有下一个响应者位置

源码地址

移动周分享-第44期

从右到左布局(RTL Layout)- 王进

从Android 4.2开始,Android SDK支持一种从右到左(RTL,Right-to-Left)UI布局的方式,尽管这种布局方式经常被使用在诸如阿拉伯语、希伯来语等环境中,中国用户很少使用。不过在某些特殊用途中还是很方便的。

使用:

  • 要在AndroidManifest.xml文件中将标签的android:supportsRtl属性值设为”true”,

  • 将布局中”left/right”改成”start/end”

例如

1
2
3
4
5
6
7
android:paddingStart  

android:layout_marginStart

android:paddingEnd

android:layout_marginEnd
  • 设置显示方向,改为从右向左

xml中

1
2
3
4
5
android:layoutDirection="rtl"

android:textDirection

android:textAlignment

Java中

  • getLayoutDirectionFromLocale

  • View.isLayoutRtl()

    1
    //  if  this  class is the subclass of View, then it can use  if (isLayoutRtl()) { }
  • Configuration.getLayoutDirection()

    1
    import android.content.res.Configuration; Configuration config = getResources().getConfiguratin(); if (config.getLayoutDirection() == View.LAYOUT_DIRECTIN_RTL) { }

    加载特定从右到左布局,图片等

    1
    res/ drawable-hdpi // Default drawable-ldltr-hdpi // LTR drawable-ldrtl-hpid // RTL

wKioJlKPZyjzCdeVAAAxAmZW9ww089.jpg (269×215)

玩《Ori and the Blind Forest》- 曾铭

先上百科:奥里与迷失森林 - 维基百科,自由的百科全书

Ori And The Blind Forest 封面

画面美

画面美是最容易直观感受的第一印象,随便上个图就能感受到。用朋友的话来说『随便一张截图就是精美的壁纸』

Ori And The Blind Forest 画面美

音乐燃

音乐一直是游戏容易被忽略的一块,特别是 ori 这类『小制作』很容易重视画面而忽视音乐。不过其已经拿下了 2015 年金摇杆奖的最佳游戏音效奖,这方面自然了得。
上手一会走过两个剧情你就会发现,音乐与音效与剧情衔接的十分自然。而洪水漫上来的那一段,我随着音乐不禁『燃』了起来,一段音乐已值回票价。
对音乐的分析见这里:如何评价《Ori and the Blind Forest 》的配乐水平? - 舟行的回答 - 知乎

Ori And The Blind Forest 音乐燃

剧情沉浸

奥瑞希望拯救『养母』,挣扎许久竟然发现悲剧都是自己酿成的,黑子的暴虐也有其苦衷。通关一刻,竟不知如何是好。
于剧情,我没能理解更深。而从序幕到最后一刻,过场动画所陈诉的剧情都如游戏一般令我沉浸其中。这是令人沉浸的好剧情。
想看大师的剧情分析看这里:对奥里这款游戏的细节、故事以及人物的一些看法。奥日与黑暗森林吧

Ori And The Blind Forest 剧情沉浸

地图赞

Ori 属于横版过关游戏,地图设计比较复杂。特别是随着剧情的进展,主角逐步有二段跳,滑翔,贴墙等技能。最难的点就这个『逐步』。

  • 主角能力未到时,有些地图是去不了的,随着剧情变化,能到的『自然』能到
  • 一条路线跑过去,活得了某项能力,同样路线跑回来,新能力在这条路线上神奇的延伸的新玩法。滑翔那段尤其明显。
  • 地图很大,游戏没设传送门。剧情整体路线不重复,而部分的路线重复,确因角色能力的不同有了新的『跑路方式』,这点地图和场景结合得特好

Ori And The Blind Forest 全地图

总结 - 系统平衡 - 神作

大学游戏大神魏巍有句话:『做游戏就是做平衡,你画面音乐剧情战斗任一方面做得完美都没有意义,只要有一点做得很不平衡,短板明显,就会毁掉这个游戏。』

Ori 难能可贵的就是这一点,角色能力设计、操控、地图、画面、音乐。每一点设计都很『完美』,更完美的是这些方面衔接的是如此之好,互为衬映提升游戏的整体感受。在我心中,他是又一款神作。

像不像古堡迷踪与 Yorda 一起吃西瓜 ; ) 懂的自然懂~

ClassyShark - 李仙鹏

引言

  • ClassyShark并非Google官方产品。
  • ClassyShark是针对Android开发者的独立工具。它能够可靠的浏览任何Android可执行文件,并且显示apk的重要信息,比如类、接口和成员,以及dex数量和依赖。它还支持多种格式,比如库(.dex,.aar,.so),可执行文件(.apk,.jar,.class)和AndroidManifest

使用

  • 打开APK(GUI展现)
    • java -jar ClassyShark.jar -open YOUR_APK.apk
  • 检测APK
    • java -jar ClassyShark.jar -inspect YOUR_APK.apk
  • 以文本形式存储APK中classes.dex的字符串表
    • java -jar ClassyShark.jar -stringdump YOUR_APK.apk

Google Protocol Buffer 介绍与使用

简介

  • 官网https://developers.google.com/protocol-buffers/

  • Google Protocol Buffer( 以下简称PB) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。PB是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

    “Protocol Buffers是一种以有效并可扩展的格式编码结构化数据的方式。”

    message Person {
      required int32 id = 1;
      required string name = 2;
      optional string email = 3;
    }
    

PB 的优点

  • 性能好,效率高

同 XML 相比, PB 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

Total Time 指一个对象操作的整个时间,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。从测试结果可以看到 Protobuf 的成绩很好,感兴趣的读者可以自行到网站上了解更详细的测试结果。

  • 代码生成机制,数据解析类自动生成

PB 语义更清晰,无需类似 XML 解析器的东西(因为 PB 编译器会将 .proto 文件编译生成对应的数据访问类以对 PB数据进行序列化、反序列化操作)。使用 PB无需学习复杂的文档对象模型,PB 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,PB 比其他的技术更加有吸引力

  • 支持向后兼容和向前兼容

它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

PB 使用(java)

  • step1: 下载protobuf

下载并编译获得proto.exe和protobuf-java.jar
也可以直接从网上下载proto.exe 下载和protobuf-java-2.4.1.jar(下载

  • step2:编写.proto文件,eg:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

option java_package = "com.jeson.roster.proto";
option java_outer_classname = "Roster";

message Student{
required int32 id = 1;
required string name = 2;

enum Sex{
MALE = 0;
FEMALE = 1;
}

required Sex sex = 3;

}

message StudentRoster{
repeated Student student = 1;
}
  • step3:编译.proto文件生成 java类文件:在proto.exe目录下:protoc —java_out=./src ./proto/msg.proto

在所使用的proto文件路径下打开cmd窗口执行以下命令

1
protoc -I=源地址 --java_out=目标地址  源地址/xxx.proto
  • step4:导入java类文件和依赖包直接使用

Android Studio eg : 完整demo地址

1 添加依赖包 compile ‘com.google.protobuf:protobuf-java:3.0.0-beta-2’

2 导入生成的 Roster 到对应的package下

3 使用Roster 序列化或反序列化数据

package com.jeson.testpb;

import com.jeson.roster.proto.Roster.Student;
import com.jeson.roster.proto.Roster.Student.Sex;
import com.jeson.roster.proto.Roster.StudentRoster;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class InfoHelper {


    public static final String FILE_NAME = "/mnt/sdcard/roster.dat";


    /**
     * 反序列化
     */

    public static List<StudentInfo> getStudentsFromFile() {

        ArrayList<StudentInfo> list = new ArrayList<StudentInfo>();
        FileInputStream fis = null;

        try {
            fis = new FileInputStream(FILE_NAME);
            StudentRoster roster = StudentRoster.parseFrom(fis);
            int student_count = roster.getStudentCount();

            for (int i = 0; i < student_count; i++) {
                Student student = roster.getStudent(i);
                StudentInfo info = new StudentInfo();
                info.setId(student.getId());
                info.setName(student.getName());
                info.setSex(student.getSex().toString());
                list.add(info);
            }

        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (null != fis) {
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        return list;
    }


    /**
     * 序列化
     */

    public static void saveStudentIntoFile(int id, String name, String sex) {
        FileOutputStream fos = null;

        StudentRoster.Builder rosterBuid = StudentRoster.newBuilder();


        Student.Builder student = Student.newBuilder();
        student.setId(id);
        student.setName(name);
        if ("MALE".equalsIgnoreCase(sex)) {
            student.setSex(Sex.MALE);
        } else {
            student.setSex(Sex.FEMALE);
        }

        rosterBuid.addStudent(student.build());

        try {
            fos = new FileOutputStream(FILE_NAME, true);
            rosterBuid.build().writeTo(fos);
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }

}

Andoid最近搜索记录

移动周分享-第43期

tablayout方法setupWithViewPager()坑 - 吴明

  • tallayout平常使用setupWithViewPager()
1
2
3
4
ViewPager viewPager=(ViewPager)findViewById(R.id.view_pager);
TabLayout tabContainView = (TabLayout) findViewById(R.id.pick_school_category_contain);
viewPager.setAdatper(new FragmentStatePagerAdapter(FragmentManager,fragments));
tabContainView. setupWithViewPager(viewPager);
  • 查看setupWithViewPager()方法源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void setupWithViewPager(@NonNull ViewPager viewPager) {
final PagerAdapter adapter = viewPager.getAdapter();
if (adapter == null) {
throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
}

// First we'll add Tabs, using the adapter's page titles
setTabsFromPagerAdapter(adapter);

// Now we'll add our page change listener to the ViewPager
viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(this));

// Now we'll add a tab selected listener to set ViewPager's current item
setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager));

// Make sure we reflect the currently set ViewPager item
if (adapter.getCount() > 0) {
final int curItem = viewPager.getCurrentItem();
if (getSelectedTabPosition() != curItem) {
selectTab(getTabAt(curItem));
}
}
}
  • 就会发现这里有三个坑

    • setTabsFromPagerAdapter

      1
      2
      3
      4
      5
      6
      public void setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) {
      removeAllTabs();
      for (int i = 0, count = adapter.getCount(); i < count; i++) {
      addTab(newTab().setText(adapter.getPageTitle(i)));
      }
      }

      removeAllTabs()这个就是说把前面所有tablayout添加的view都删掉。也就是说在之前不管怎么处理view都被干掉。然后设置为PagerAdapter返回的title

    • setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager));

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
      private final ViewPager mViewPager;

      public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
      mViewPager = viewPager;
      }

      @Override
      public void onTabSelected(TabLayout.Tab tab) {
      mViewPager.setCurrentItem(tab.getPosition());
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
      // No-op
      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {
      // No-op
      }
      }

      这里主要不算不算太坑,最主要的是设置了点击tablayot默认是viewpager是滚动的,自己可以设置这个时间监听。重写他的方法

    • selectTab(getTabAt(curItem));

      1
      2
      3
      4
      5
      6
      if (adapter.getCount() > 0) {
      final int curItem = viewPager.getCurrentItem();
      if (getSelectedTabPosition() != curItem) {
      selectTab(getTabAt(curItem));
      }
      }

      这里最主要是第一次默认选中第一个。相对应一些viewpager第一次就选中的不是第一个,这个就是一个很大的一个问题就是相当于viewpager点击了两次。

    • 分享这个坑主要是给大家提前填。以后还是不能偷懒就为了省代码而忘了看封装的方法的代码是不是有问题。

Chrome Custom Tabs 标签优点 - 王进

  • Chrome自定义标签页(Custom Tabs)将允许应用预加载,从而将网页的加载时间直接减半。

它的速度比WebView更快,预加载的效果甚至好于Chrome本身。

  • 持良好的用户体验,并且让用户感觉这个自定义 Tab 就是您应用的一部分。

  • 使用方法比WebView 简单,只需要一行代码,和直接调用系统浏览器显示网页没啥区别

  • 有大量自定义属性,改善用户体验

使用方法

需要导入Custom Tabs 的支持包:

compile ‘com.android.support:customtabs:23.1.0’

最简单的使用方式是只需要使用 CustomTabsIntent.Builder 对象来设置一些常用自定义选项,然后调用 CustomTabsIntent.launchUrl(Activity context, Uri url)) 函数即可。当然在具体使用过程中,您还需要判断用户手机是否支持 Custom Tabs。

配置属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder();

// 修改 ActionBar 的颜色
intentBuilder.setToolbarColor(Color.RED);

// 添加一个分享按钮
String shareLabel = "分享";
Bitmap icon = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_share);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, BlankActivity.class), 0);
intentBuilder.setActionButton(icon, shareLabel, pendingIntent);

//添加Menu 按钮
intentBuilder.addMenuItem("打开", pendingIntent);

//是否显示网页标题
intentBuilder.setShowTitle(true);

//隐藏网页Bar
intentBuilder.enableUrlBarHiding();

//自定义关闭 Custom tabs 的图标
intentBuilder.setCloseButtonIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));

//自定义 Activity 转场 动画
intentBuilder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left);
intentBuilder.setExitAnimations(this, android.R.anim.slide_in_left, android.R.anim.slide_out_right);

CustomTabsIntent customTabsIntent = intentBuilder.build();
// customTabsIntent.launchUrl(this, Uri.parse("http://www.51offer.com/"));

Android NDK 入门 - 杨俊构

什么是NDK?

  • NDK是一系列工具的集合

NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。
NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

  • NDK提供了一份稳定、功能有限的API头文件声

Google明确声明该API是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK中看出,这些API支持的功能非常有限,包含有:C标准库(libc)、标准数学库(libm)、压缩库(libz)、Log库(liblog)。

为什么使用NDK

  1. 代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
  2. 可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。
  3. 提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
  4. 便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用

如何使用NDK

  • Android JNI 原理

    Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的LIiraries都是C/C++编写的。所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现。

  • Android studio NDK 开发

  1. 解压NDK开发工具集
  2. 打开android studio,新建Android工程
  3. 点击:File ->ProjectStructure:如图:
  4. 在 local.properties 文件中设置ndk的路径
  5. 在 gradle.properties 添加”android.useDeprecatedNdk=true”
  6. 在 build.gradle 添加NDK 的配置
  7. 添加JAVA 接口类文件
1
2
3
4
5
6
7
8
package com.example.jungou.jnitest;
// Created by jungou on 2016/1/28 0028.
public class JniApi {
static {
System.loadLibrary("jesonlib");
}
private native String getTitle();
}
  1. 添加jni文件夹
  2. JAVA 接口类使用Alt+回车 自动修正,自动实现C代码
1
2
3
4
5
6
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_example_jungou_jnitest_JniApi_getTitle(JNIEnv *env, jobject instance) {

// TODO
return (*env)->NewStringUTF(env, "hello tile");
}
  1. 完成接口调用,测试完成

demo地址

移动周分享-第42期

软件推荐 suger & nvalt - 曾铭

surge

iOS 正常上网神器,68元,目前已下架

衍生品 Replica - Web Developer Tool

iOS 网络调试神器,iOS 设备中代理所有网络流量,方便查看所有网络请求。68 元,Surge 用户直接解锁。

nvalt

『惊的速记』,最快速的笔记方式,本地纯文本存储,优秀的快捷键配置,支持 tag, markdown, 预览等。开源。

Android 微信抢红包 - 吴明

  • 主要原理:监听微信APP,如果发送[微信红包]相关通知,则通过Android辅助类服务AccessibilityService实现抢红包点击一系列动作

  • 通过AccessibilityService服务监听微信

  • 主要监听配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
监听动作类型
android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeViewScrolled"
完成反馈动作类型
android:accessibilityFeedbackType="feedbackGeneric"
标识
android:accessibilityFlags=""
是否可以屏幕内容
android:canRetrieveWindowContent="true"
服务描述
android:description="@string/server_description"
通知反应时间
android:notificationTimeout="0"
监听应用包名
android:packageNames="com.tencent.mm"
  • AccessibilityService实现方法:
    • onAccessibilityEvent()时间监听回调方法
    • onServiceConnected启动服务
    • onUnbind服务器解绑
    • onInterrupt中断服务
  • 事件

    • AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:通知消息事件
    • AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:屏幕window变化时间
    • AccessibilityEvent.TYPE_VIEW_SCROLLED屏幕滚动监听事件
  • 1.微信后台抢红包:

    • 后台TYPE_NOTIFICATION_STATE_CHANGED事件监听有[微信红包]通知则点击通知,TYPE_WINDOW_STATE_CHANGED事件点击‘抢红包’,TYPE_WINDOW_STATE_CHANGED点击‘拆红包’,TYPE_WINDOW_STATE_CHANGED跳转红包查看详情
  • 2.微信当前聊天页面抢红包
    • 当前聊天页面,不能接受通知
    • AccessibilityEvent.TYPE_VIEW_SCROLLED监听屏幕view有新消息,则查看屏幕是否有’抢红包’view,有则点击‘抢红包’view,点击抢红包后则同上通过TYPE_WINDOW_STATE_CHANGED事件完成抢红包一系列动作
  • 代码地址

Android Ripple - 王进

ripple

使用效果如下:

B1是不设任何背景的按钮

B2设置了?android:attr/selectableItemBackground

B3设置了?android:attr/selectableItemBackgroundBorderless

设置波纹颜色:

android:colorControlHighlight:设置波纹颜色
android:colorAccent:设置checkbox等控件的选中颜色
ripple xml 中设置color 属性

设置自定义RippleDrawable

1
2
<!-- An unbounded red ripple. --/> 
<ripple android:color="#ffff0000" />

也可以更改主题的波纹样式

1
2
<item name="android:selectableItemBackground">@drawable/selectable_item_stroke_bg</item>
<item name="android:selectableItemBackgroundBorderless">@drawable/selectable_item_bg</item>

更改主题波纹颜色
android:colorControlHighlight:设置波纹颜色

更改样式波纹后,会对app系统内置具有波纹效果app造成影响,NavigationView,Toolbar 上Menu 等,特别是波纹的背景色

因为波纹是在background属性上设置的,所以如果控件需要其他背景要素,可以在ripple的默认 上设置

5.0 兼容

兼容方法有二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/common_action_button_h" />
<stroke android:width="1dp" android:color="@color/colorAccent" />
<size android:height="@dimen/common_action_button_h" />
<solid android:color="#E5E5E5" />
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/common_action_button_h" />
<stroke android:width="1dp" android:color="@color/colorAccent" />
<size android:height="@dimen/common_action_button_h" />
<solid android:color="#E5E5E5" />
</shape>
</item>
<item android:drawable="@drawable/shape_logout_btn"></item>
</selector>

drawable-v21

1
2
3
4
5
6
7
8
9
10
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#E5E5E5">

<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners android:radius="@dimen/common_action_button_h" />
<solid android:color="@android:color/white" />
</shape>
</item>
<item android:drawable="@drawable/shape_logout_btn"></item>
</ripple>