Android Weekly Issue #221
September 4th, 2016
ARTICLES & TUTORIALS
回想一下, 你是不是总是记不住ImageView的不同ScaleType的区别, 每次都要各种尝试来找到自己适合的.
这篇文章的作者也有这样的烦恼, 于是他把各种ScaleType都截了图:如果用了CENTER_INSIDE, FIT_CENTER, FIT_START,或者FIT_END, 而实际View的大小比图像的大, 可以使用android:adjustViewBounds
属性为true, 就会调整View的大小.
官方文档:
作者讲了如何统一管理测试机.
1.根据你要支持的API level来安装系统.
理想情况下你应该每一个API都有一个对应的机器, 更进一步可以统计一下你的用户用什么的最多来进行调整.
作者列举了他当前的五个机器, 一般来讲, 你至少需要高中低API版本的, 也需要Samsung的机器来测试一些可能会被定制的地方, (当然作者是在国外了, 国内估计需要测试的定制机型就更多了), 另外还需要一个大屏幕的, 来查看UI的适配情况.
幸运的是除了品牌定制机, 其他都可以用模拟器来补救, 在此推荐一下genymotion, 传说中最快的模拟器.
2. 安装并配置测试所需的应用
为了测试你的app, 你可能需要一系列的工具app, 所以第二步你需要安装它们, 登录及设置等等.
原作者常用的工具app有:- 1Password: 管理密码.
- AZ Screen Recorder: 截屏, 制作gif.
- Chrome Beta: 因为原作者做WebView相关的工作, 所以需要看这个.
- Dropbox: 自动上传截图, 从电脑可以方便拿, 也可以用来做一些文件相关的测试.
- Flesky / Swiftkey / Google Keyboard: 也是作者应用相关, 需要测试各种键盘.
- Keep: 很好用的笔记应用, 可以存一些小notes, url等, 跨设备同步.
- Solid Explorer: 文件管理器, 可以在系统中方便地移动文件.
3. 在各处都登录
需要登录的账号都登录.
4. 为了统一体验装个Nova Launcher
为了让每个机器都看起来一样, 原作者装了个launcher应用: .
确实, 因为每个机器的launcher和组织方式不一样, 所以有时候换个机器就会很难找到你想要的东西.
用了这个Nova Launcher之后, 你可以设置好你的home, dock, drawer, 然后多个机器分享设置, 这样当你拿起另一个机器的时候, 所有的应用都在同样的位置.
5. 设置每个机器的系统设置
最后一件事就是一些系统上的设置, 包括:
- 所有地点的Wi-Fi;
- DND/total silence: 关声音;
- Developer options和USB debugging开关打开;
- 当插线时仍然保持屏幕唤醒;
- 亮度设置.
最后作者建议开发者平时生活中可以多玩玩各种Android应用.
看这篇文章之前, 让我们了解一下Accessibility是什么, 搜了一下Android相关文档:
Accessibility是为了扩展访问和利用应用的形式, 基本出发点是为了辅助老年人或者是有障碍的人, 增加一些听觉或触觉反馈, 也可以用来辅助一些特殊场合下的用户, 比如正在开车或照顾孩子, 或者处于非常嘈杂的环境下的情形.
可以结合Google的, 也可以自己开发相关的服务.
好了, 话题收敛回来, 看看作者说的安全问题指的是什么.
作者一开篇以一个印度很流行的应用Voodoo为例指出, 把屏幕上的文字读出来这个功能是有安全漏洞的.
首先Voodoo向用户请求accessibility的权限, 这个权限使得应用可以从屏幕上读取文字, 但是用户会认为所有的敏感字段应该不在这个范围之内, 这就是开发者需要认真对待的了.
最近有一个新的登录设计, 已经被应用开来, 就是用户可以选择显示或者隐藏密码字段.
当我们把输入框的input type设置为密码, 那么它是不会被读取到的, 但是有一些应用为了支持显示密码的功能, 可能会把input type设置为其他类型, 这样就会导致密码暴露, 有accessibility权限的恶意应用就会借此盗用用户的敏感信息.
这样当然是不好的啦, 用户开启权限的时候还认为敏感字段总会受到保护呢, 所以我们开发者应该小心地对待用户的敏感信息, 很简单:
ViewCompat.setImportantForAccessibility(your_view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
新的TextInputLayout
的API可以实现密码显示的toggle功能, 希望这个问题在TextInputLayout中已经解决了, 但是在用这个View之前, 上面的hotfix也算一种解决办法.
在Android中垂直的滚动很常见, 但是如果在垂直滚动的View里嵌套一个水平滚动的View, 那滑动的体验将会非常不好.
Problem: 垂直滚动和内嵌的水平滚动打架了, 滚动体验不佳.
What's happening inside:
例子里根view是一个RecyclerView
加垂直 LinearLayoutManager
, 里面的child是一个RecyclerView
加水平LinearLayoutManager
.
但是当用户做水平滚动的时候, touch事件首先被外面的父View给拦截了.
看RecyclerView
的代码可知, 在onInterceptTouchEvent()
方法里, 在垂直滚动使能的情况下, 只要垂直移动的距离(dy)大于一定程度(Math.abs(dy) > mTouchSlop
), 就会被认为是垂直滚动.
所以作者他们的解决方案是继承了RecyclerView, 覆写了这个方法, 把条件改成:
if(canScrollVertically && Math.abs(dy) > mTouchSlop && (canScrollHorizontally || Math.abs(dy) > Math.abs(dx))) {...}
这是他们的完整代码:
Bonus
还有一个跟fling相关的问题:RecyclerView
在fling之后需要挺长的一段时间来稳定(settle)下来, 当child还在这个稳定过程中时, 如果用户尝试竖直滚动, touch事件实际上是被child吃掉的. 还是从onInterceptTouchEvent()
的代码可以看出:
if (mScrollState == SCROLL_STATE_SETTLING) { getParent().requestDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING);}
当child处于SETTLING状态时, child会要求它的parent不要拦截touch事件.
这在通常情况下是好的.
但是在作者的使用场景里, 他们的root中没有其他竖直滚动和拖拽的child, 所以他们又继承了刚才那个BetterRecyclerView, 写了一个requestDisallowInterceptTouchEvent()
为空实现的View作为root. 他们的sample demo在这里: .
每一个Android开发可能都需要对他们的项目进行(更新依赖, 编码, 重复)这样的循环工作, 如果你想要你的所有项目都有同样的版本号, 这样是很浪费时间的.
原文作者就经历了这样的情景, 他想要把他这个目录下的所有项目都更新一下. 这个目录里全是那种很小的简单例子, 但都是可独立运行的工程.
每当gradle-plugin, support library或者google play services要更新版本号, 保持这些工程全部都updated是一项很难的工作.所以原作者想要放弃原先逐个更新的土办法, 更有效率地来更新依赖版本号.
首先想到的就是在gradle里定义一个变量, 然后双引号加$引用这个变量. 为了让所有的module都采用同一变量, 可以在根项目的build.gradle文件里定义变量, 即使用ext块. 但是到此, 只能统一管理在同一个project下的各个modules的依赖版本.如果跨projects呢?
首先, 作者在存放这些projects的根目录下建了一个gradle文件, 然后把变量都定义在那里. 然后如何应用到各个project呢?于是原作者找啊找, 找到了这块: 他给每个工程的根build文件加了个这个:// This is added to apply the gradle file to each module under the projectsubprojects { apply from: '../../dependencies.gradle'}
这样以后在每个工程的module里面都可以直接引用变量了.
但是, 这对于android-gradle-plugin的版本是不管用的.
这是因为上面应用的配置只对subproject起作用, 对每一个root project是没有应用到的.
所以作者在每一个项目的root build.gradle中, 在buildscript块又加了它的依赖配置文件:buildscript { // This is added to apply the gradle file to facilitate providing variable values to root build.gradle of the project apply from: '../dependencies.gradle' .. dependencies { classpath "com.android.tools.build:gradle:$androidPluginVer" .. }}
好啦, 至此, 所有的依赖配置问题就解决了, 以后改版本号只需要改一个地方就可以应用到所有项目.
作者上一篇的文章里介绍了Dagger的基本使用.
这篇还是教程类文章, 讲:多个Modules:
写了一个Retrofit 2的ApiModule, 和一个Realm的DatabaseModule.
多个对象:
有时候我们需要提供一个类的不同对象, 我们可以用@Named
注解, 然后用不同的字符串来区分它们.
文中的例子是这样:
@Provides@Named(IMAGE_URL)String provideImageUrl() { return ImageApiService.ENDPOINT;}@Provides@Named(URL)String provideBaseUrl() { return RestApiService.ENDPOINT;}@Provides @Singleton @Named(REST_API_RETROFIT)Retrofit provideRetrofit(@Named(URL) String baseUrl, OkHttpClient client) { ... }@Provides @Singleton @Named(IMAGE_API_RETROFIT)Retrofit provideImageRetrofit(@Named(IMAGE_URL) String baseUrl, OkHttpClient client) { ... }
用dagger2注入接口, 返回实现类的对象, 比较常规的方法是在Module里面写一个@Provides
标注的providesXXX()方法, 返回值类型是接口, 实际返回的是实现类的对象, 比如:
@Modulepublic class HomeModule { @Provides public HomePresenter providesHomePresenter(){ return new HomePresenterImp(); }}
但是如果我们想给实现类加一个依赖UserService呢?
我们当然可以把UserService作为参数传给这个provide方法, 然后传到实现类的构造函数中, 在里面存一个字段.
又或者, 我们可以使用@Binds
注解, 像这样:
@Modulepublic abstract class HomeModule { @Binds public abstract HomePresenter bindHomePresenter(HomePresenterImp homePresenterImp);}
这是一个抽象类中的抽象方法, 这个方法的签名意思是告诉dagger, 注入HomePresenter
接口(返回值)时, 使用HomePresenterImpl
(方法参数)实现.
然后, 在HomePresenterImpl
类的构造函数上加一个@Inject
就可以了.
这样我们就不需要在provide方法上加依赖参数了.
本文介绍, 一个开源的库, 可以展开和折叠RecyclerView中的组.
RecyclerView
作为ListView
的升级版, 却也减少了一些功能比如OnItemClickListener
, ChoiseModes
, 还有扩展版的ExpandableListView
.
ExpandableRecyclerView
, 用自定义的RecyclerView.Adapter
来实现展开关闭分组的功能. 首先明白一下Adapter的功能, 其实adapter就是一个中间人, 将一些数据按照index翻译给View, 然后显示.
当显示的list是单维度的时候, 这样的翻译很简单, 数据的index就直接对应了屏幕上view的index.
当时当你显示二维数据时, 翻译就变得有点复杂,数据和view的index可能对应, 也可能不对应.
RecyclerView.Adapter
就只能处理一维数据的情况, 这就是为什么要对其进行一些扩展, 才能实现ExpandableRecyclerView. 后来作者简单讲了实现的原理, 用到了ExpandableListPosition
, 是Android SDK中就有的类, 只不过有包限制, 所以拷贝到了这个库里.
最后附上repo地址:
注解是什么
Annotations are Metadata.
注解是元数据, 而元数据是一些关于其他数据的信息.
所以说, 注解是关于代码的信息.比如@Override
注解, 即便你不在方法上标注它, 程序依然能够正常工作. 那么它是用来干什么的呢?
@Override
是用来告诉编译器, 这个方法覆写了一个方法, 如果父类没有这个方法, 则会报一个编译错误.
如果你不加这个注解, 有可能你方法名不小心拼错了却仍然编译通过了.
创建自定义注解:
比如, 创建一个:@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@interface Status { public enum Priority {LOW, MEDIUM, HIGH} Priority priority() default Priority.LOW; String author() default “Amit”; int completion() default 0;}
其中@Target
指定了这个注解可以放在哪里. 如果你不设置, 这个注解可以放在任何地方.
ElementType.TYPE
(class, interface, enum)ElementType.FIELD
(instance variable)ElementType.METHOD
ElementType.PARAMETER
ElementType.CONSTRUCTOR
ElementType.LOCAL_VARIABLE
@Retention
定义这个注解可以被保存多久.
RetentionPolicy.SOURCE
- 到编译结束, 不会被编进.class, 只会留在源文件中.@Override
,@SuppressWarnings
都是这种.RetentionPolicy.CLASS
- 到类加载丢弃, 注解将存储在.class文件中, 这是默认值.RetentionPolicy.RUNTIME
- 不被丢弃. .class文件中有, 并且可由VM读入, 在运行时可以通过反射的方式读取到.
上面的注解使用时:
// in Foo.java@Status(priority = STATUS.Priority.MEDIUM, author = “Amit Shekhar”, completion = 0)public void methodOne() { //no code}// get the annotation information Class foo = Foo.class; for(Method method : foo.getMethods()) { Status statusAnnotation = (Status)method.getAnnotation(Status.class); if(statusAnnotation != null) { System.out.println(" Method Name : " + method.getName()); System.out.println(" Author : " + statusAnnotation.author()); System.out.println(" Priority : " + statusAnnotation.priority()); System.out.println(" Completion Status : " + statusAnnotation.completion()); } }
如果你的注解中仅有一个属性, 它应该叫value, 并且使用的时候不用指定属性名.
@interface Status{ int value();} @Status(50) public void someMethod() { //few codes }
最后作者还附上了另一个他的文章, 推荐他的网络请求库:
作者介绍他的库: .
这个库实现了一个粒子系统, 来发射出随机的纸屑, 并且可以被定制化, 比如发射源的形状(点或者线), 初始的物理约束(速度, 加速度, 旋转等), 还可以定义消失或者拖拽行为, 感觉效果还挺好的.
关于性能, 纸屑对象是循环利用的, 每一个bitmap也只被分配一次地址, 动画参数也做了一些预计算, 所以作者说不用担心丢帧, 除非你一次性出现的片儿实在是太多了.
作者他们在自己Android应用里开始使用RxJava以后, 经常会遇到由于API没有follow reactive model, 导致他们必须做一些转换工作, 将它们和其他的RxJava Observable链连接起来.
API对于很重的操作通常提供这两种方式之一
- 1.同步阻塞方法调用, 通常需要后台线程调用.
- 2.异步非阻塞方法调用, 结合callback, listener, 或者broadcast receiver等.
把同步方法变为Observable:
用这个
比如:
// wrapping synchronous operation in an RxJava ObservableObservablewipeContents(final SharedPreferences sharedPreferences) { return Observable.fromCallable(new Callable () { @Override public Boolean call() throws Exception { return sharedPreferences.edit().clear().commit(); } }); }
把异步方法变为Observable:
变异步没那么简单了, 之前有一些模式是用工厂方法Observable.create()
把它们包起来, 比如, , , 但是这种方法存在一些缺点.
作者举了一个传感器监听的例子.
用create()转换之后, 需要处理一些问题, 比如注销listener, 错误处理, 检查subscriber等, 这几个都可能办到, 但是还有一个backpressure的问题, 不好办. 这个backpressure是什么捏: 当生产者发射值的速率比消费者可以处理的速率快的时候, 有一个内置的buffer size, 当超出的时候就会抛出MissingBackpressureException
. 幸运的是, RxJava v1.1.7推出了Observable.fromAsync()
, 在v1.2.0改名为Observable.fromEmitter()
.
然后作者给出了采用这个新方法的例子, 这里不再赘述, 可以看原文.
Sample代码在:LIBRARIES & CODE
ItemTouchHelper
的扩展, 加滑动settling和恢复. Sample的效果是给单个item加了滑动后出现删除和refresh两个按钮.
为Fresco库加的全屏查看图像的工具, 支持双手指的zoom和滑动关闭手势.
一个生成简单A/B test的库, 使用注解.
一个RecyclerView的辅助类, 提供滑动删除, 拖动, divider, 选中和非选中事件等的支持.
一个轻量级的Android应用framework.
SPECIALS
一个很长的README, 包含了各种快捷键, 编码建议, 工具, 插件, 还有有一些推荐的网站等, 其中有mock api和新闻网站及其他有用的工具等.