Android Weekly Issue #221
September 4th, 2016
Android Weekly Issue #221
ARTICLES & TUTORIALS
Android ImageView ScaleType: A Visual Guide
回想一下, 你是不是总是记不住ImageView的不同ScaleType的区别, 每次都要各种尝试来找到自己适合的.
这篇文章的作者也有这样的烦恼, 于是他把各种ScaleType都截了图:
如果用了CENTER_INSIDE, FIT_CENTER, FIT_START,或者FIT_END, 而实际View的大小比图像的大, 可以使用android:adjustViewBounds
属性为true, 就会调整View的大小.
官方文档: ImageView.ScaleType
5 steps to creating frustration-free Android test devices
作者讲了如何统一管理测试机.
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应用: Nova Launcher.
确实, 因为每个机器的launcher和组织方式不一样, 所以有时候换个机器就会很难找到你想要的东西.
用了这个Nova Launcher之后, 你可以设置好你的home, dock, drawer, 然后多个机器分享设置, 这样当你拿起另一个机器的时候, 所有的应用都在同样的位置.
5. 设置每个机器的系统设置
最后一件事就是一些系统上的设置, 包括:
- 所有地点的Wi-Fi;
- DND/total silence: 关声音;
- Developer options和USB debugging开关打开;
- 当插线时仍然保持屏幕唤醒;
- 亮度设置.
最后作者建议开发者平时生活中可以多玩玩各种Android应用.
Security issues with Android Accessibility
看这篇文章之前, 让我们了解一下Accessibility是什么, 搜了一下Android相关文档:
- Guides of Accessibility
- Training for Implementing Accessibility
- Design Guidelines for Accessibility
- Material Design Accessibility
Accessibility是为了扩展访问和利用应用的形式, 基本出发点是为了辅助老年人或者是有障碍的人, 增加一些听觉或触觉反馈, 也可以用来辅助一些特殊场合下的用户, 比如正在开车或照顾孩子, 或者处于非常嘈杂的环境下的情形.
可以结合Google的TalkBack, 也可以自己开发相关的服务.
好了, 话题收敛回来, 看看作者说的安全问题指的是什么.
作者一开篇以一个印度很流行的应用Voodoo为例指出, 把屏幕上的文字读出来这个功能是有安全漏洞的.
首先Voodoo向用户请求accessibility的权限, 这个权限使得应用可以从屏幕上读取文字, 但是用户会认为所有的敏感字段应该不在这个范围之内, 这就是开发者需要认真对待的了.
最近有一个新的登录设计, 已经被应用开来, 就是用户可以选择显示或者隐藏密码字段.
当我们把输入框的input type设置为密码, 那么它是不会被读取到的, 但是有一些应用为了支持显示密码的功能, 可能会把input type设置为其他类型, 这样就会导致密码暴露, 有accessibility权限的恶意应用就会借此盗用用户的敏感信息.
这样当然是不好的啦, 用户开启权限的时候还认为敏感字段总会受到保护呢, 所以我们开发者应该小心地对待用户的敏感信息, 很简单:
ViewCompat.setImportantForAccessibility(your_view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
新的TextInputLayout
的API可以实现密码显示的toggle功能, 希望这个问题在TextInputLayout中已经解决了, 但是在用这个View之前, 上面的hotfix也算一种解决办法.
How to fix horizontal scrolling in your Android app
在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))) {...}
这是他们的完整代码: BetterRecyclerView
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在这里: manidesto/scrolling-demo.
Update Dependencies. Code. Repeat.
每一个Android开发可能都需要对他们的项目进行(更新依赖, 编码, 重复)这样的循环工作, 如果你想要你的所有项目都有同样的版本号, 这样是很浪费时间的.
原文作者就经历了这样的情景, 他想要把他这个目录Android-Examples下的所有项目都更新一下. 这个目录里全是那种很小的简单例子, 但都是可独立运行的工程.
每当gradle-plugin, support library或者google play services要更新版本号, 保持这些工程全部都updated是一项很难的工作.
所以原作者想要放弃原先逐个更新的土办法, 更有效率地来更新依赖版本号.
首先想到的就是在gradle里定义一个变量, 然后双引号加$引用这个变量.
为了让所有的module都采用同一变量, 可以在根项目的build.gradle文件里定义变量, 即使用ext块.
但是到此, 只能统一管理在同一个project下的各个modules的依赖版本.
如果跨projects呢?
首先, 作者在存放这些projects的根目录下建了一个gradle文件, 然后把变量都定义在那里.
然后如何应用到各个project呢?于是原作者找啊找, 找到了这块: gradle Subproject configuration
他给每个工程的根build文件加了个这个:
// This is added to apply the gradle file to each module under the project
subprojects {
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"
..
}
}
好啦, 至此, 所有的依赖配置问题就解决了, 以后改版本号只需要改一个地方就可以应用到所有项目.
DI 101 - Part 2
作者上一篇的文章里介绍了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) { ... }
Inject interfaces without provide methods on Dagger 2
用dagger2注入接口, 返回实现类的对象, 比较常规的方法是在Module里面写一个@Provides
标注的providesXXX()方法, 返回值类型是接口, 实际返回的是实现类的对象, 比如:
@Module
public class HomeModule {
@Provides
public HomePresenter providesHomePresenter(){
return new HomePresenterImp();
}
}
但是如果我们想给实现类加一个依赖UserService呢?
我们当然可以把UserService作为参数传给这个provide方法, 然后传到实现类的构造函数中, 在里面存一个字段.
又或者, 我们可以使用@Binds
注解, 像这样:
@Module
public abstract class HomeModule {
@Binds
public abstract HomePresenter bindHomePresenter(HomePresenterImp
homePresenterImp);
}
这是一个抽象类中的抽象方法, 这个方法的签名意思是告诉dagger, 注入HomePresenter
接口(返回值)时, 使用HomePresenterImpl
(方法参数)实现.
然后, 在HomePresenterImpl
类的构造函数上加一个@Inject
就可以了.
这样我们就不需要在provide方法上加依赖参数了.
Introducing ExpandableRecyclerView
本文介绍expandable-recycler-view, 一个开源的库, 可以展开和折叠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地址: expandable-recycler-view
Creating Custom Annotations in Android
注解是什么
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
}
最后作者还附上了另一个他的文章, 推荐他的网络请求库: Fast Android Networking
It's parfetti time!
作者介绍他的库: confetti.
这个库实现了一个粒子系统, 来发射出随机的纸屑, 并且可以被定制化, 比如发射源的形状(点或者线), 初始的物理约束(速度, 加速度, 旋转等), 还可以定义消失或者拖拽行为, 感觉效果还挺好的.
关于性能, 纸屑对象是循环利用的, 每一个bitmap也只被分配一次地址, 动画参数也做了一些预计算, 所以作者说不用担心丢帧, 除非你一次性出现的片儿实在是太多了.
Converting callback async calls to RxJava
作者他们在自己Android应用里开始使用RxJava以后, 经常会遇到由于API没有follow reactive model, 导致他们必须做一些转换工作, 将它们和其他的RxJava Observable链连接起来.
API对于很重的操作通常提供这两种方式之一
- 1.同步阻塞方法调用, 通常需要后台线程调用.
- 2.异步非阻塞方法调用, 结合callback, listener, 或者broadcast receiver等.
把同步方法变为Observable:
比如:
// wrapping synchronous operation in an RxJava Observable
Observable<Boolean> wipeContents(final SharedPreferences sharedPreferences) {
return Observable.fromCallable(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return sharedPreferences.edit().clear().commit();
}
});
}
把异步方法变为Observable:
变异步没那么简单了, 之前有一些模式是用工厂方法Observable.create()
把它们包起来, 比如here, here, here, 但是这种方法存在一些缺点.
作者举了一个传感器监听的例子.
用create()转换之后, 需要处理一些问题, 比如注销listener, 错误处理, 检查subscriber等, 这几个都可能办到, 但是还有一个backpressure的问题, 不好办.
这个backpressure是什么捏: backpressure
当生产者发射值的速率比消费者可以处理的速率快的时候, 有一个内置的buffer size, 当超出的时候就会抛出MissingBackpressureException
.
幸运的是, RxJava v1.1.7推出了Observable.fromAsync()
, 在v1.2.0改名为Observable.fromEmitter()
.
这个里面对于backpressure的处理定义了好几种策略, 你只需要选一种模式就行.
然后作者给出了采用这个新方法的例子, 这里不再赘述, 可以看原文.
Sample代码在: AndroidRxFromAsyncSample
LIBRARIES & CODE
ItemTouchHelper Extension
ItemTouchHelper
的扩展, 加滑动settling和恢复. Sample的效果是给单个item加了滑动后出现删除和refresh两个按钮.
Fresco Image Viewer
为Fresco库加的全屏查看图像的工具, 支持双手指的zoom和滑动关闭手势.
ABTestGen
一个生成简单A/B test的库, 使用注解.
RecyclerViewHelper v24.2.0
一个RecyclerView的辅助类, 提供滑动删除, 拖动, divider, 选中和非选中事件等的支持.
Paginize
一个轻量级的Android应用framework.
SPECIALS
Tips and tricks for Android Development
一个很长的README, 包含了各种快捷键, 编码建议, 工具, 插件, 还有有一些推荐的网站等, 其中有mock api和新闻网站及其他有用的工具等.