杨辉的个人博客
翻译自 http://developer.android.com/intl/zh-cn/tools/data-binding/guide.html 这个文档用于解释如何使用 Data Binding Library 编写声明式的布局,减少应用中逻辑以及布局所需要的“胶水代码”。 Data …
翻译自 http://developer.android.com/intl/zh-cn/tools/data-binding/guide.html 这个文档用于解释如何使用 Data Binding Library 编写声明式的布局,减少应用中逻辑以及布局所需要的“胶水代码”。 Data Binding Library 提供了灵活性与通用性 - 它是一个 support library,可以在 Android 2.1(API level 7+)以上的平台使用。 为了使用 data binding,gradle plugin的版本必须是 1.5.0-alpha1以上。 编译环境 为了使用 Data Binding,首先在 Android SDK manager 中下载最新的 Support repository。 然后在 build.gradle 中添加 dataBinding 段。 使用以下代码段配置 data binding: android { .... dataBinding { enabled = true } } 如果你的 app module 依赖了一个使用 data binding 的库,那么你的 app module 的 build.gradle 也必须配置 data binding。 同时,确定使用了支持该特性的 Android Studio。在 Android Studio 1.3 以及之后的版本提供了 data binding 的支持,详见 Android Studio Support for Data Binding。 Data Binding 布局文件 编写你的第一个 data binding 表达式 Data binding 布局文件与普通布局文件有一点不同。它以一个 layout 标签作为根节点,里面是 data 标签与 view 标签。view 标签的内容就是不使用 data binding 时的普通布局文件内容。以下是一个例子: 在 data 标签中的 user 变量 描述了一个布局中会用到的属性。 布局文件中的表达式使用 “@{}” 的语法。在这里,TextView 的文本被设置为 user中的 firstName 属性。 数据对象 假设你有一个 plain-old Java object(POJO) 的 User 对象。 public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } 这种类型的对象拥有不可改变的数据。在应用中,读一次且不变动数据的对象非常常见。也可以使用 JavaBeans 对象: public class User { private final String firstName; private final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } } 从 data binding 的角度看,这两个类是一样的。用于 TextView 的 android:text 属性的表达式@{user.firstName},会读取 POJO 对象的 firstName 域以及 JavaBeans 对象的 getFirstName() 方法。此外,如果 firstName() 方法存在的话也同样可用。 绑定数据 在默认情况下,会基于布局文件生成一个 Binding 类,将它转换成帕斯卡命名并在名字后面接上”Binding”。上面的那个布局文件叫 main_activity.xml,所以会生成一个 MainActivityBinding 类。这个类包含了布局文件中所有的绑定关系(user变量),会根据绑定表达式给布局文件赋值。在 inflate 的时候创建 binding 的方法如下: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity); User user = new User("Test", "User"); binding.setUser(user); } 就这么简单!运行应用,你会发现测试用户已经显示在界面中了。你也可以通过以下这种方式: MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater()); 如果你在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,你可以这样写: ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false); //or ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false); 绑定事件 事件可以直接与 handler 函数绑定,类似于 android:onClick 可以指定 Activity 中的一个函数一样。事件属性的命名由 listener 的函数命名决定。举个例子,View.OnLongClickListener 中有一个 onLongClick() 函数,所以这个事件的对应属性就是 android:onLongClick。 为了将事件分配给 handler,只需要使用一个 binding 表达式,值为要调用的函数名。举个例子,如果你的数据对象有两个函数: public class MyHandlers { public void onClickFriend(View view) { ... } public void onClickEnemy(View view) { ... } } 分配点击事件的 binding 表达式如下: 也有一个特殊的点击事件 handler,他们有一些不同于 android:onClick 的属性来避免冲突。下面是一些用来避免冲突的属性: Class Listener Setter Attribute SearchView setOnSearchClickListener(View.OnClickListener)) android:onSearchClick ZoomControls setOnZoomInClickListener(View.OnClickListener)) android:onZoomIn ZoomControls setOnZoomOutClickListener(View.OnClickListener)) android:onZoomOut 布局细节 导入 data标签内可以有多个 import 标签。你可以在布局文件中像使用 Java 一样导入引用。 现在 View 可以被这样引用: 当类名发生冲突时,可以使用 alias: 现在,Vista 可以用来引用 com.example.real.estate.View ,与 View 在布局文件中同时使用。导入的类型也可以用于变量的类型引用和表达式中: "/> 注意:Android Studio 还没有对导入提供自动补全的支持。你的应用还是可以被正常编译,要解决这个问题,你可以在变量定义中使用完整的包名。 导入也可以用于在表达式中使用静态域/方法: … 和 Java 一样,java.lang.* 会被自动导入。 变量 data 标签中可以有任意数量的 variable 标签。每个 variable 标签描述了会在 binding 表达式中使用的属性。 变量类型会在编译时被检查,所以如果变量声明了 Observable 接口或者是一个可观察容器类,那它会被反射使用。如果变量是一个没有声明 Observable* 接口的基类或借口,变量的变动则不会引起 UI 的变化! 当针对不同配置编写不同的布局文件时(比如横屏竖屏的布局),变量会被合并。所以这些不同配置的布局文件之间不能存在冲突。 自动生成的 binding 类会为每一个变量生产 getter/setter 函数。这些变量会使用 Java 的默认赋值,直到 setter 函数被调用。默认赋值有 null,0(int),false(boolean)等。 binding 类也会生一个一个命名为 context 的特殊变量,这个变量被用于表达式中。context 变量其实就是 rootView 的 getContext()) 的返回值。context 变量会被同名的显式变量覆盖。 自定义 Binding 类名 默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app,binding 类会被放在 com.example.my.app.databinding 中。 通过修改 data标签中的class 属性,可以修改 Binding 类的命名与位置。举个例子: ... 以上会在 databinding 包中生成名为 ContactItem 的binding 类。如果需要放置在不同的包下,可以在前面加 “.”: ... 这样的话,ContactItem 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中: ... Includes 在使用应用命名空间的布局中,变量可以传递到任何 include 布局中。 需要注意,name.xml 与 contact.xml 中都需要声明 user 变量。 Data binding 不支持直接包含 merge 节点。举个例子,以下的代码就不能正常运行: 表达式语言 通用特性 表达式语言与 Java 表达式有很多相似之处。下面是相同之处: 数学计算 + - / * % 字符串连接 + 逻辑 && || 二进制 & | ^ 一元 + - ! ~ 位移 >> >>> = Null合并运算符 Null合并运算符(??)会在非 null 的时候选择左边的操作,反之选择右边。 android:text="@{user.displayName ?? user.lastName}" 等同于 android:text="@{user.displayName != null ? user.displayName : user.lastName}" 属性引用 首先是先前编写你的第一个 data binding 表达式中所提到的:JavaBean 引用。当表达式引用了一个类内的属性时,他会尝试直接调用域,getter,还有 ObservableFields。 android:text="@{user.lastName}" 避免NullPointerException 自动生成的 data binding 代码会自动检查和避免 null pointer exceptions。举个例子,在表达式 @{user.name} 中,如果 user 是 null,user.name 会赋予默认值 null。如果你引用了 user.age,因为 age 是 int 类型,所以默认赋值为 0。 容器类 通用的容器类:数组,lists,sparse lists,和 map,可以用 [] 操作符来存取 "/> "/> "/> … android:text="@{list[index]}" … android:text="@{sparse[index]}" … android:text="@{map[key]}" 字符串字面量 使用单引号把属性包起来,就可以很简单地在表达式中使用双引号: android:text='@{map["firstName"]}' 也可以用双引号将属性包起来。这样的话,字符串字面量就可以用"或者反引号(`) 来调用 android:text="@{map[`firstName`}" android:text="@{map["firstName"]}" 资源 也可以在表达式中使用普通的语法来引用资源: android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" 字符串格式化和复数形式可以这样实现: android:text="@{@string/nameFormat(firstName, lastName)}" android:text="@{@plurals/banana(bananaCount)}" 当复数形式有多个参数时,应该这样写: Have an orange Have %d oranges android:text="@{@plurals/orange(orangeCount, orangeCount)}" 一些资源需要显示类型调用。 Class Listener Setter Attribute String[] @array @stringArray int[] @array @intArray TypedArray @array @typedArray Animator @animator @animator StateListAnimator @animator @stateListAnimator color int @color @color ColorStateList @color @colorStateList 数据对象 任何 POJO 都能用在 data binding 中,但是更改 POJO 并不会同步更新 UI。data binding 的强大之处就在于它可以让你的数据拥有更新通知的能力。这里有三种不同的数据变动通知机制,Observable 对象,observable 域,与 observable 容器类。 当以上的 observable 对象绑定在 UI 上,数据发生变化时,UI 就会同步更新。 Observable 对象 当一个类声明了 Observable 接口时,data binding 会设置一个 listener 在绑定的对象上,以便监听对象域的变动。 Observable 接口有一个添加/移除 listener 的机制,但通知取决于开发者。为了简化开发,我们创建了一个基类 BaseObservable,来实现 listener 注册机制。这个类也实现了域变动的通知,你只需要在 getter 上使用 Bindable 注解,并在 setter 中实现通知。 private static class User extends BaseObservable { private String firstName; private String lastName; @Bindable public String getFirstName() { return this.firstName; } @Bindable public String getLastName() { return this.lastName; } public void setFirstName(String firstName) { this.firstName = firstName; notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } } Bindable 注解会在编译时在 BR 类内生成一个元素。而 BR 类会生成在 module 的 package 下。如果数据基类不可修改,Observable 接口的存储和 listener 通知可以用 PropertyChangeRegistry 来实现。 Observable域 创建 Observable 类还是需要花费一点时间的,如果开发者想要省时,或者数据类的域很少的话,可以使用 ObservableField 以及它的派生 ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble,ObservableParcelable。ObservableFields 是单一域的自包含 observable 对象。原始版本避免了在存取过程中做打包/解包操作。要使用它,在数据类中创建一个 public final 域: private static class User { public final ObservableField firstName = new ObservableField(); public final ObservableField lastName = new ObservableField(); public final ObservableInt age = new ObservableInt(); } 就这么简单!要存取数据,只需要使用 get set 方法: user.firstName.set("Google"); int age = user.age.get(); Observable 容器类 一些应用会使用更加灵活的结构来保持数据。Observable 容器类允许使用 key 来获取这类数据。当 key 是类似 String 的一类引用类型时,使用 ObservableArrayMap 会非常方便。 ObservableArrayMap user = new ObservableArrayMap(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17); 在布局中,可以用 String key 来获取 map 中的数据: "/> … 当 key 是整数类型时,可以使用 ObservableArrayList: ObservableArrayList user = new ObservableArrayList(); user.add("Google"); user.add("Inc."); user.add(17); 在布局文件中,使用下标获取列表数据: "/> … 生成Binding 生成的 binding 类将布局中的 View 与变量绑定在一起。就像先前提到过的,类名和包名可以自定义。生成的 binding 类会继承 ViewDataBinding。 创建 binding 应该在 inflate 之后创建,确保 View 的层次结构不会在绑定前被干扰。绑定布局有好几种方式。最常见的是使用 binding 类中的静态方法。inflate 函数会 inflate View 并将 View 绑定到 binding 类上。此外有更加简单的函数,只需要一个 LayoutInflater 或一个 ViewGroup: MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false); 如果布局使用不同的机制来 inflate,则可以独立做绑定操作: MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot); 有时绑定关系是不能提前确定的。这种情况下,可以使用 DataBindingUtil : ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId); 带有 ID 的 View 布局中每一个带有 ID 的 View,都会生成一个 public final 域。binding过程会做一个简单的赋值,在 binding 类中保存对应 ID 的 View。这种机制相比调用 findViewById 效率更高。举个例子: 将会在 binding 类内生成: public final TextView firstName; public final TextView lastName; ID 在 data binding 中并不是必需的,但是在某些情况下还有有必要对 View 进行操作。 变量 每一个变量会有相应的存取函数: 并在 binding 类中生成对应的 getter setter: public abstract com.example.User getUser(); public abstract void setUser(com.example.User user); public abstract Drawable getImage(); public abstract void setImage(Drawable image); public abstract String getNote(); public abstract void setNote(String note); ViewStub ViewStub 相比普通 View 有一些不同。ViewStub 一开始是不可见的,当它们被设置为可见,或者调用 inflate 方法时,ViewStub 会被替换成另外一个布局。 因为 ViewStub 实际上不存在于 View 结构中,binding 类中的类也得移除掉,以便系统回收。因为 binding 类中的 View 都是 final 的,所以我们使用了一个叫 ViewStubProxy 的类来代替 ViewStub。开发者可以使用它来操作 ViewStub,获取 ViewStub inflate 时得到的视图。 但 inflate 一个新的布局时,必须为新的布局创建一个 binding。因此,ViewStubProxy 必须监听 ViewStub 的 ViewStub.OnInflateListener,并及时建立 binding。由于 ViewStub 只能有一个 OnInflateListener,你可以将你自己的 listener 设置在 ViewStubProxy 上,在 binding 建立之后, listener 就会被触发。 高级 binding 动态变量 有时候,有一些不可知的 binding 类。例如,RecyclerView.Adapter 可以用来处理不同布局,这样的话它就不知道应该使用哪一个 binding 类。而在 onBindViewHolder(VH, int)) 的时候,binding 类必须被赋值。 在这种情况下,RecyclerView 的布局内置了一个 item 变量。BindingHolder 有一个 getBinding 方法,返回一个 ViewDataBinding 基类。 public void onBindViewHolder(BindingHolder holder, int position) { final T item = mItems.get(position); holder.getBinding().setVariable(BR.item, item); holder.getBinding().executePendingBindings(); } 直接 binding 当变量或者 observable 发生变动时,会在下一帧触发 binding。有时候 binding 需要马上执行,这时候可以使用 executePendingBindings())。 后台线程 只要数据不是容器类,你可以直接在后台线程做数据变动。Data binding 会将变量/域转为局部量,避免同步问题。 属性 Setter 当绑定数据发生变动时,生成的 binding 类必须根据 binding 表达式调用 View 的 setter 函数。Data binding 框架内置了几种自定义赋值的方法。 自动 Setter 对一个 attribute 来说,data binding 会尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。 举个例子,针对一个与 TextView 的 android:text 绑定的表达式,data binding会自动寻找 setText(String) 函数。如果表达式返回值为 int 类型, data binding则会寻找 setText(int) 函数。所以需要小心处理函数的返回值类型,必要的时候使用强制类型转换。需要注意的是,data binding 在对应名称的属性不存在的时候也能继续工作。你可以轻而易举地使用 data binding 为任何 setter “创建” 属性。举个例子,support 库中的 DrawerLayout 并没有任何属性,但是有很多 setter,所以你可以使用自动 setter 的特性来调用这些函数。 重命名 Setter 一些属性的命名与 setter 不对应。针对这些函数,可以用 BindingMethods 注解来将属性与 setter 绑定在一起。举个例子,android:tint 属性可以这样与 setImageTintList(ColorStateList)) 绑定,而不是 setTint: @BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), }) Android 框架中的 setter 重命名已经在库中实现了,开发者只需要专注于自己的 setter。 自定义 Setter 一些属性需要自定义 setter 逻辑。例如,目前没有与 android:paddingLeft 对应的 setter,只有一个 setPadding(left, top, right, bottom) 函数。结合静态 binding adapter 函数与 BindingAdapter 注解可以让开发者自定义属性 setter。 Android 属性已经内置一些 BindingAdapter。例如,这是一个 paddingLeft 的自定义 setter: @BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } Binding adapter 在其他自定义类型上也很好用。举个例子,一个 loader 可以在非主线程加载图片。 当存在冲突时,开发者创建的 binding adapter 会覆盖 data binding 的默认 adapter。 你也可以创建多个参数的 adapter: 1 2 3 4 @BindingAdapter({"bind:imageUrl", "bind:error"}) public static void loadImage(ImageView view, String url, Drawable error) { Picasso.with(view.getContext()).load(url).error(error).into(view); } 1 2 当 imageUrl 与 error 存在时这个 adapter 会被调用。imageUrl 是一个 String,error 是一个 Drawable。 在匹配时自定义命名空间会被忽略 你可以为 android 命名空间编写 adapter Binding adapter 方法可以获取旧的赋值。只需要将旧值放置在前,新值放置在后: @BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int oldPadding, int newPadding) { if (oldPadding != newPadding) { view.setPadding(newPadding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } } 事件 handler 仅可用于只拥有一个抽象方法的接口或者抽象类。例如: @BindingAdapter("android:onLayoutChange") public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue, View.OnLayoutChangeListener newValue) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (oldValue != null) { view.removeOnLayoutChangeListener(oldValue); } if (newValue != null) { view.addOnLayoutChangeListener(newValue); } } } 当 listener 内置多个函数时,必须分割成多个 listener。例如,View.OnAttachStateChangeListener 内置两个函数:onViewAttachedToWindow()) 与 onViewDetachedFromWindow())。在这里必须为两个不同的属性创建不同的接口。 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewDetachedFromWindow { void onViewDetachedFromWindow(View v); } @TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewAttachedToWindow { void onViewAttachedToWindow(View v); } 因为改变一个 listener 会影响到另外一个,我们必须编写三个不同的 adapter,包括修改一个属性的,和修改两个属性的。 @BindingAdapter("android:onViewAttachedToWindow") public static void setListener(View view, OnViewAttachedToWindow attached) { setListener(view, null, attached); } @BindingAdapter("android:onViewDetachedFromWindow") public static void setListener(View view, OnViewDetachedFromWindow detached) { setListener(view, detached, null); } @BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}) public static void setListener(View view, final OnViewDetachedFromWindow detach, final OnViewAttachedToWindow attach) { if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) { final OnAttachStateChangeListener newListener; if (detach == null && attach == null) { newListener = null; } else { newListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (attach != null) { attach.onViewAttachedToWindow(v); } } @Override public void onViewDetachedFromWindow(View v) { if (detach != null) { detach.onViewDetachedFromWindow(v); } } }; } final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener); if (oldListener != null) { view.removeOnAttachStateChangeListener(oldListener); } if (newListener != null) { view.addOnAttachStateChangeListener(newListener); } } } 上面的例子比普通情况下复杂,因为 View 是 add/remove View.OnAttachStateChangeListener 而不是 set。android.databinding.adapters.ListenerUtil 可以用来辅助跟踪旧的 listener 并移除它。 对应 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)) 支持的 api 版本,通过向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,data binding 代码生成器会知道这些 listener 只会在 Honeycomb MR1 或更新的设备上使用。 转换器 对象转换 当 binding 表达式返回对象时,会选择一个 setter(自动 Setter,重命名 Setter,自定义 Setter),将返回对象强制转换成 setter 需要的类型。 下面是一个使用 ObservableMap 保存数据的例子: 在这里,userMap 会返回 Object 类型的值,而返回值会被自动转换成 setText(CharSequence) 所需要的类型。当对参数类型存在疑惑时,开发者需要手动做类型转换。 自定义转换 有时候会自动在特定类型直接做类型转换。例如,当设置背景的时候: 在这里,背景需要的是 Drawable,但是 color 是一个整数。当需要 Drawable 却返回了一个整数时,int 会自动转换成 ColorDrawable。这个转换是在一个 BindingConversation 注解的静态函数中实现: @BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); } 需要注意的是,这个转换只能在 setter 阶段生效,所以 不允许 混合类型: Android Studio 对 Data binding 的支持 Android Studio 支持 data binding 表达式的高亮,并会在编辑器中标出表达式中的语法错误。 在预览窗口显示的是 data binding 表达式的默认值。下面是一个设置默认值的例子,TextView 的 text 默认值为 PLACEHOLDER。 如果你需要在设计阶段显示默认值,你可以使用 tools 属性代替默认值表达式,详见设计阶段布局属性。
除了常见的 Activity - Fragment 模式,还有 Activity - ViewPager - Fragment 模式,这种情况又略有不同。 有时候调试 activity recreate 的时候,会发现 ViewPager 变成一片空白,没有 Fragment 显示出来。该有的 Fragment recreate 哪里去了? 查看 FragmentAdapter 的源码中的instantiateItem()函数,代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { mCurTransaction.attach(fragment); } else { fragment = getItem(position); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } //... return fragment; } 可以看出,这里的逻辑为,如果 FragmentManager 中有可用的 Fragment 实例,就直接使用该实例,避免重复创建。否则才通过getItem()函数创建新的 fragment 实例。这里有一个细节,就是 Fragment 的 tag 是通过 makeFragmentName() 函数获取。具体代码如下: 1 2 3 private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } 这里的 viewId 就是 containerId,也就是 ViewPager 的 id;id 就是 getItemId(),也就是 position。所以以后可以通过构造这个 tag 直接将 ViewPager 的 Fragment find 出来(你可以在你的代码中这样做,注意类型检测与 NullPointer)。这里也是第一个关键点:ViewPager 必须拥有 id,否则 ViewPager 中的 Fragment 可能无法恢复。 回到刚刚的instantiateItem()函数,两条路径都只是 beginTransaction 却没有 commit,commit 操作在finishUpdate()中完成。代码如下: 1 2 3 4 5 6 7 public void finishUpdate(ViewGroup container) { if (mCurTransaction != null) { mCurTransaction.commitAllowingStateLoss(); mCurTransaction = null; //为了gc? mFragmentManager.executePendingTransactions(); } } 可以看到,这里用的是 commitAllowingStateLoss(),而不是 commit,而且紧随一个 executePendingTransactions 使之生效。我个人的猜测是为了防止 activity 在后台的时候 commit 导致 crash(系统不允许 activity 在onSaveInstanceState()之后做 fragment transaction操作,以防止状态丢失) 。有失必有得,避免了crash,就可能导致 state 丢失。所以这里就引来了第二个关键点:避免 activity 在后台的时候更换当前 fragment,你可以检测 activity 状态,有必要的时候将操作封装在 Runnable 中,activity resume 之后执行这个 Runnable。只要在 resume 之后做 commit,那么 fragment state 就能被正确地保存。 在上一篇 blog 中,我提到过,Activity,View/ViewGroup,Fragment都能自动保存 state 并事后恢复,这里可不包括PagerAdapter。实际测试发现,recreate之后,viewPager.getAdapter() 为 null。既然没有 adapter,那么也就不会有 instantiateItem 啦。所以第三个要点是:recreate 之后必须调用 setAdapter,并且getItemId()必须返回和先前一样的值,以便 adapter 将 fragment item恢复出来。 还有一个细节就是,如果你的结构是 Activity : Fragment : ViewPager : Fragment 的话,那么在第一个 Fragment 中 new adapter 的时候,必须使用getChildFragmentManager(),而不是getFragmentManager()。childFramentManager 会给每一个 fragment 赋予正确的 parentFragment,以便后面恢复 fragment的时候恢复正确的 mWho 值(内部标识)。 总结以上,要 ViewPager 中的 Fragment 能正确回复,需要注意: ViewPager 要有 id 避免 Activity 在后台的更换当前 Fragment(commit操作) Activity recreate 之后必须调用 setAdapter 并保障 itemId 一致。
说明 一般都是用v4的Fragment实现,可以有getChildFragmentManager()的支持,这里以 v4 版本为例。 命名 FragmentActivity 源码中的变量命名其实很乱,比如一个 FragmentManagerImpl 的实例,叫做 mFragments,后面需要注意。 保存与恢复 FragmentActivity.onCreate() 中有这样一个函数:getLastNonConfigurationInstance() 与之相对应的有onRetainNonConfigurationInstance()。 onRetainNonConfigurationInstance():将要保存的实例保存起来,在 recreate 之后恢复。需要注意的是,函数的返回类型是 Object,说明这里保存的是任何实例(Activity,Fragment,etc),而不是 SaveState。与利用 SaveState 新建实例恢复状态不同的方式不同。这个方法在 api level 13 中取消使用。FragmentActivity 中覆盖了Activity 的这个方法,加入了一些额外的 Object,比如一个 Fragment 组和一个 Loader 组。现在要用原来onRetainNonConfigurationInstance()方法来保存 Object 的话,请使用onRetainCustomNonConfigurationInstance()方法。 getLastNonConfigurationInstance():这个方法返回了一个 NonConfigurationInstances 实例,里面包含了上面所说保存的 Fragment 实例组和 Loader 实例组。 FragmentActivity 利用上面两个函数来还原部分 Fragment 和 Loader。为什么是部分 Fragment 呢?因为这个是有选择的筛选,判断原则就是 fragment 是不是mRetainInstance等于 true。还记得有一个 hold 住 fragment 实例的例子吗?一个包含 AsyncTask 的 fragment,在屏幕旋转 Activity recreate 之后,fragment 中的 AsyncTask 并没有重新跑,而是继续执行。那个例子的关键,就是 fragment 初始化之后,需要调用setRetainIntance(true)方法,将 mRetainInstance 赋值为 true。所以 FragmentActivity 会将所有这种类型的 Fragment 都用 onRetainCustomNonConfigurationInstance() 方法保存起来,以便 AsyncTask 继续执行。 同理,Loader 本来的作用就是,在 Activity recreate 之后,避免重新 load 的过程。所以 FragmentActivity 也会利用这个方法将所有 loader 保存起来,recreate 之后复用。 具体代码如下: public final Object onRetainNonConfigurationInstance() { //... Object custom = onRetainCustomNonConfigurationInstance(); //覆盖这个函数来保存自己的Object ArrayList fragments = mFragments.retainNonConfig(); //... if (fragments == null && !retainLoaders && custom == null) { return null; } NonConfigurationInstances nci = new NonConfigurationInstances(); nci.activity = null; nci.custom = custom; nci.children = null; nci.fragments = fragments; nci.loaders = mAllLoaderManagers; return nci; } retainNonConfig()的实现如下(这函数的命名我也没懂): ArrayList retainNonConfig() { ArrayList fragments = null; if (mActive != null) { for (int i=0; i(); } fragments.add(f); f.mRetaining = true; f.mTargetIndex = f.mTarget != null ? f.mTarget.mIndex : -1; if (DEBUG) Log.v(TAG, "retainNonConfig: keeping retained " + f); } } } return fragments; } 那剩下部分的 fragment 如何恢复呢?这就要靠常见的 create + restore state 模式了。 android 源码中很多地方都用到这种 create + restore 的模式,比如 Activity,View/ViewGroup,Fragment 等。如果 fragment.mRetainInstance 等于 false,则采用下面的代码来保存(FragmentActivity.onSaveInstanceState()): protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); } } 这里的 mFragments 其实是一个 FragmentManagerImpl 实例,前面说过了。他的 onSaveInstanceState 方法中用了一个类,叫做 FragmentState,用来保存 Fragment 的状态。其中包含的关键元素有: ClassName:fragment 的具体类名 Index:fragment 在 Activity 中的下标,一个 Activity 含有多个 Fragment FragmentId:如果 Fragment 是在 xml 中初始化的话,则为 xml 中的ID,否则和下面的 containerId 一致 ContainerId:add(R.id.container, mFragment)中的container id Tag:add 的时候用的 tag Detached:fragment 状态 Arguments:fragment.setArguments(args)的 Bundle 类型参数 观察上面的元素,可以发现足以重建一个完整的 fragment,类型,参数,状态,id,tag 都有了。而 fragment 内部 View 的状态则用 View 自带的状态恢复机制去恢复。当然这里要求你用标准的模式去初始化一个fragment,new + setArguments(),如果你用new + 自定义setXXX方法的话,那这里就不能正常恢复了。 两种保存机制对应的恢复代码在 FragmentManager.restoreAllState()中,内容太长这里就不贴了。大致逻辑为:恢复 retain fragments -> 恢复 state fragments -> 指定 target fragment -> 重建 added fragment 组 -> 重建 backStack 正因为 fragment 自带这种简单的 fragment 恢复机制,我们经常看见很多源码的 activity 的 onCreate 中,如果 savedInstanceState 为 null,才做 fragment traction 操作,避免重复 add。
知识来源PPT Tools 命名空间 tools 命名空间是在 Android Studio 中引入的 编辑预览特性,可以生成一些只在 IDE 预览界面生效的特性。 属性预览 1 2 3 这个属性应该很多人都用过,设置只在 IDE 预览界面生效的属性。可以方便地对照设计稿调 UI 而避免将预览属性编译进 apk 中(是否会编译进 apk 我也没具体验证过,不过 android 在遇到不支持的 xml 属性会直接忽略,所以无伤大碍)。而且这些属性在被include的时候能够保持,所以推荐在 headerView,footerView,adapterItem,activity,fragment 等xml中使用,在 CustemView 中配合 isInEditMode() 函数使用更佳!不过没有代码提示功能有点遗憾。 Lint提示忽略 对于有洁癖的人来说,XML Editor 有很多小黄点实在无法忍受。比如布局的 RTL 支持,ImageView 的 description 定义等。这些属性虽然官方建议添加,但是在实际开发环境做支持实在有点困难。这时候就可以用 tools:ignore 属性将小黄点去处,我常用的 ignore 属性如下: 1 tools:ignore="ContentDescription, RtlHardcoded" 有时候在 Style 中需要对原生属性和 appcompat 自定义属性同时设置,但是原生属性在低版本上不支持,当然就如我前面说的,android 在遇到不支持的属性会直接忽略,但是如果强迫发作,可以用下面的方法忽略: 1 @drawable/ic_abc_share 风格套用 这个是我比较少用的属性,举例如下,大家可以试试: 1 2 3 4 5 6 1 2 3 ListView 预览 正常情况下,大家的 ListView 预览都是千篇一律的固定样式。其实可以套用 HeaderView/FooterView/ItemView 到预览界面中,需要注意的是,itemView 预览的 tools:attribute 只在第一个 item 中生效,但是我想应该不碍事吧 :) 1 2 3 4 5 总结:以上设置可能大家觉得很繁琐,很麻烦,是否有必要花费时间做这些事情呢?我个人觉得,任何一个有追求的 Android 开发者都应该在学习在 xml 中设置 tools attributes,他可以让你还没运行就能发现更多UI上的问题,在多人协作的时候,也能让协作者更加方便地理解你的代码,避免部分属性的二次渲染(xml渲染一次,java修改渲染一次),希望 google 能添加更多 tools attributes 特性,例如定义 merge 节点外层预览等。 Support Library Annotations Support Library Annotations 相信大家都用过,相比 tools attributes,他所带来的好处更加明显,能够根据代码逻辑需要添加限制,并在编写/编译时对错误进行提示。在Library项目,多人协作中起到很好的辅导作用。 @Nullable 与 @NonNull 顾名思义,声明返回值/全局变量/参数可能为 null 或者不允许为 null,从而对潜在的 NullPointerException 进行警告, 例如访问一个@Nullable 的成员变量/函数,将 @Nullable 变量设置为 @NonNull 的参数等。我一般会结合 Gson 使用,最新的 ButterKnife 也用这个 annotation 替换了老版本内置的 @Optional。由于过于常用,这里就不贴 sample 了。 资源ID限制 限制参数只能为资源 ID 而非普通 int 数值,可用 annotation 类型如下: Name Description @AnimatorRes R.animator.xxx @AnimRes R.anim.xxx @AnyRes 任意类型的资源 ID @ArrayRes R.array.xxx @AttrRes R.attr.xxx @BoolRes R.bool.xxx @ColorRes R.color.xxx @DimenRes R.dimen.xxx @DrawableRes R.drawable.xxx @FractionRes 3.14159 或者 10%p 之类的值,具体请查看 getResources().getFraction() 函数 @IdRes R.id.xxx @IntegerRes R.integer.xxx @InterpolatorRes R.interpolator.xxx @LayoutRes R.layout.xxx @MenuRes R.menu.xxx @PluralsRes 复数资源,具体案例下面会解释 @RawRes R.raw.xxx @StringRes R.string.xxx @StyleableRes R.styleable.xxx @StyleRes R.style.xxx @XmlRes R.xml.xxx 大部分都很好理解,其中可能比较陌生的就是 @FractionRes 与 @ PluralsRes 了。 @FractionRes FractionRes 类型的资源在 Animation Xml 中比较常见,比如100%p,p代表parent,也就是占 parent 的 100%,例如在 TranslateAnimation 中就很常见(除非你能将 parent 大小 hardcode) @PluralsRes PluralsRes 没用过也是正常,英语的名字复数一般在后面加 s 或者 es,单数不用,所以在 String 格式化的时候需要用一种叫做 Plurals 的资源类型,sample 如下,简单易懂: 1 2 3 4 5 6 7 8 no Tutorial one Tutorial %d Tutorials 顺便附上 java 调用 1 2 String quantityString = getResources().getQuantityString(R.plurals.tutorials, number); 资源 Annotation 强烈建议在 CustomView 中使用。 参数范围限制 @FloatRange @IntRange Sample: 1 public void setAlpha(@IntRange(from=0, to=255) int alpha) { ... } 容器长度限制 @Size 常见容器类都可使用,例如: 1 public void setLocation(@Size(2) int[] location) { ... } 枚举限制 @IntDef @StringDef 由于 Android 不推荐使用枚举,所以一般会用 int 或者 String 代替枚举,但是这样就缺少了取值限制,所以需要结合这两个 annotation 来使用。 Sample: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Retention(RetentionPolicy.SOURCE) @IntDef({ExpertHelpAdapter.TYPE_EMPTY, ExpertHelpAdapter.TYPE_LISTVIEW, ExpertHelpAdapter.TYPE_WEBVIEW}) public @interface AdapterType {} @AdapterType private int mType = TYPE_WEBVIEW; @AdapterType public int getType() { return mType; } public void setType(@AdapterType int type) { mType = type; notifyDataSetChanged(); } 注意添加 @Retention(RetentionPolicy.SOURCE) ,指定此 annotation 只在代码中生效(非运行时),建议在所有替代枚举的地方使用。 线程限制 Name Description @MainThread 只能在主线程线程运行 @UiThread 只能在UI线程运行 @BinderThread 只能在Binder线程运行 @WorkerThread 只能在自定义线程中运行 这几个 annotation 我也没用过,查询了相关文档,作出解释如下: MainThread 与 UiThread 在大部分情况下可混用,可能是Application.onCreate 与 Activity.onCreate 的区别?只能等待大神解答了。 BinderThread: ContentProvider 做增删改查的线程。 WorkerThread: 自开线程,一般就是非UI线程。 以上 Annotation 建议在多线程模块中使用,特别涉及 UI 回调与非 UI 回调。 架构注解? @CallSuper:一般添加在函数声明处,要求覆盖时必须调用super实现。例如 1 2 @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { ... } @CheckResult:确保函数返回值被使用,没有被忽略 @VisibleForTesting:暴露测试接口用 权限限制 @RequiresPermission:检查相关权限是否已申请,建议在 library 项目中使用。 混淆限制 @Keep:混淆时保持变量或方法不被混淆,但该特性目前还未被支持。 调试用注解 @ViewDebug.ExportedProperty:添加在getXXX方法前,可让CustomView的内部参数在 Hierarchy Viewer 中查询,使用方法如下: 1 2 3 4 5 6 7 @ExportedProperty(category = "layout") int x = 1; @ExportedProperty(category = "layout") public boolean isFocused() { return true; } 其中 layout 为 Hierarchy Viewer 中的属性目录。 但是我觉得这个注解实际意义不大,首先Hierarchy Viewer使用限制很多,需要 debug 版本的 ROM 才能正常开启,虽然有 ViewServer 这个 Library 可以尝试开启,但是成功率不高。其次是 Hierarchy Viewer dump 数据实在过慢,UI界面卡顿明显,稍微复杂一点的视图定位会耗费很多时间,而且很容易手机端 app crash,电脑 dump 数据丢失。所以,还是老老实实 log 或者断点调试吧……
由于墙越建越高,现在想要访问国外的服务越来越难了。在购买了自己的VPS之后,可以基于各种方案建立对应的翻墙方案: Windows/Linux/Mac/Android: shadowsocks ( PAC ) iOS: AnyConnect ( 路由表 ) Openvpn 由于特征过于明显已经不能使用。 以上方案暂时能够满足各种Desktop环境的需求,但是在使用Terminal的时候,会发现以上方案并不好用。这时候就要祭出大杀器 ProxyChains-ng 了。 安装好 proxychains4 之后,建立默认配置,指向本地 shadowsocks 建立的socks5代理。 1 2 3 4 5 6 7 8 9 10 11 12 # /etc/proxychains.conf strict_chain proxy_dns remote_dns_subnet 224 tcp_read_time_out 15000 tcp_connect_time_out 8000 [ProxyList] socks5 127.0.0.1 1080 #define your local socks proxy here 这时可以测试你的代理有没有生效了 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 ➜ cnbeta git:(develop) proxychains4 git pull origin develop [proxychains] config file found: /etc/proxychains.conf [proxychains] preloading /usr/lib/libproxychains4.dylib [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] Strict chain ... 127.0.0.1:1080 ... github.com:22 ... OK [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda From github.com:kyze8439690/cnbeta * branch develop -> FETCH_HEAD [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda [proxychains] DLL init: proxychains-ng 4.8.1-git-4-gea51cda Already up-to-date. 如果看到一堆 [proxychains] 就说明代理成功了,以后想要运行的命令通过代理访问网络,就在运行的命令前增加 “proxychains4 “前缀即可(注意空格) 当然 proxychains4 可能太长,加个alias: 1 2 # ~/.zshrc alias pc='proxychains4' 当然 pc git pull 还是太麻烦,而且会丢失补全的功能,如果你使用的是 Mac + iTerm 的组合的话,可以在 iTerm -> Preferences -> Profiles -> Keys 中,新建一个快捷键,例如 ⌥+↩︎ ,Action 选择 Send Hex Code,键值为 0x1 0x70 0x63 0x20 0xd,保存生效。 这样的话,以后命令要代理就直接敲命令,然后 ⌥+↩︎ 即可,这样命令补全也能保留了。 附上 Hex Code 对应表,获取工具为 keycodes Hex Code Key 0x1 ⌃ + a 0x70 p 0x63 c 0x20 [space] 0xd ↩︎ Thanks to: http://everet.org Chenye
使用 ListView 的时候,根据需求需要动态将HeaderView/FooterView隐藏掉,这时你会发现 setVisibility(View.GONE) 根本没有效果,两个折衷的方案是: 动态将HeaderView/FooterView remove掉,要显示的时候再add回去。 在HeaderView/FooterView外面包一个Container ViewGroup(例如 FrameLayout),再把这个Container作为HeaderView/FooterView add 到ListView 中。 以上两个方案都能实现隐藏 HeaderView/FooterView 的效果。下面我从源码介绍以下为何 View.GONE 不生效,以及为何以上 workaround 能够生效的原因。 总所周知,一个 View 能在屏幕上显示出来,需要经历 measure / layout / draw 三个步骤,measure 步骤负责测量View的大小,layout 步骤负责布局,draw 步骤负责绘制。一个 View 占屏幕多大位置,一般是由 measure 步骤决定。 对于 ListView 来说,不论是 Header 还是 Footer 还是普通的 ItemView,对他来说,都是普通的子 View。Header/Footer 和 ItemView 的区别,在 HeaderViewListAdapter 中体现。 ListView HeaderView/FooterView 设置隐藏不生效,表现为仍然占据原有的位置空间。所以我们先从 ListView 的 onMeasure() 函数入手。 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 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets up mListPadding super.onMeasure(widthMeasureSpec, heightMeasureSpec); //... int childWidth = 0; int childHeight = 0; int childState = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) { final View child = obtainView(0, mIsScrap); //测量 itemView measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); //... } //... if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; } if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } //设置 ListView dimension setMeasuredDimension(widthSize , heightSize); mWidthMeasureSpec = widthMeasureSpec; } 从这里可以看出每一个 itemView 的测量,都是由 measureScrapChild() 这个函数完成的,所以我们再来看看这个函数的源码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void measureScrapChild(View child, int position, int widthMeasureSpec) { LayoutParams p = (LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(position); p.forceAdd = true; int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, mListPadding.left + mListPadding.right, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } 从这段源码可以看出,每个 itemView 的测量,要先判断 view 是否存在 AbsListView.LayoutParams,如果不存在则new一个新的。generateDefaultLayoutParams() 源码如下。 1 2 3 4 5 @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } WRAP_CONTENT 值为 -2,MATCH_PARENT 为 -1,所以默认情况下 itemView 的大小由他自身决定(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))。 在以上的源码中可以总结出以下几点: ListView 的源码中并没有针对 Visibility 做特殊处理,一般 ViewGroup 都会跳过 Visibility 为 View.GONE 的 childView, 让他们大小为0。所以设置 HeaderView/FooterView 为 View.GONE 是无效的。 ListView 中对 itemView 的测量取决于 LayoutParams,想通过设置 LayoutParams 来隐藏 ListView 的某一项是行不通的,因为 0,-1,-2都会变成 ItemView 自己来决定自己的大小。你可以这样做一个测试:给 HeaderView/FooterView 设置一个 height 为 1 的 AbsListView.LayoutParams,你会发现 HeaderView/FooterView终于能够自我收缩了! 那为何给 HeaderView/FooterView 包一层 Container 就可以实现隐藏的效果呢?分析如下: 默认结构是这样的: ListView -> HeaderView/FooterView(View.GONE) View.VISIBLE 一样去 measure,隐藏失败。 Container Workaround 是这样的:ListView -> Container(WRAP_CONTENT) -> HeaderView/FooterView(View.GONE) WRAP_CONTENT,继续看下一层,HeaderView/FooterView 是 View.GONE,从而导致 Container measure 出来的 measureHeight 是 0,所以 HeaderView/FooterView 被隐藏。 当然还有第三种 Workarround:就是覆盖 HeaderView/FooterView的 getMeasuredHeight() 函数,让它有选择地按照实际情况返回 0 或者 super.getMeasuredHeight()。
Begin from ViewRoot’s performTrasversals() -> performMeasure() -> performLayout() -> performDraw() Measure: pass measurespec to all children and ask them to measure themselves base on the measurespec value Measure is a traversal procedure, from parent to child. MeasureSpec is a integer conbination of a size and a mode: size: size provided by parent to calculate measured size mode: limit provided by parent to calculate measured size, possible values as below: AT_MOST: the measured size can not be larger than specified size. EXACTLY: the measured size must be equal to specified size. UNSPECIFIED: the child view can be whatever size it wants. Why setting width/height to 0dp when child view in LinearLayout has weight? getMeasuredWidth/height will always return 0 until measure finish. Layout: Based on the result of measure, now we know the child views’s approximate dimensions, the next procedure is decide the location of a view. Just like drawing a rect on a white paper, we must know the size of this rect, the distance between the rect’s edge and the pager’s edge, then we can draw it down. The layout procedure need four parameters: left top right bottom These four parameters indicate the distance between view edge and parent’s left top point. In View’s onLayout(), do nothing. In View’s layout(), call setFrame(), then mLeft, mRight, mTop, mBottom is assigned value, then getWidth()/getHeigth() will not return 0 but the correct value. That is why we can not call getWidth()/getHeight() in activity’s onCreate() method, but in view.getViewTreeObserver().addOnGlobalLayoutListener(), because only then the layout procedure is finished. In layout procedure, we can also decide the size of a view, by changing the left/top/right/bottom value, but generally, we don’t do this. Draw: after measure and layout, the exactly visble area of the view is determined, now we will draw the view/viewgroup. When drawing, don’t determine drawing location base on canvas.getWidth()/getHeight(), the canvas size won’t be equal to the view size, use view.getWidth()/getHeight() instead(On android 2.3, the canvas size will be equal to the screen size in some case). In View’s onDraw(), do nothing, you can define you own drawing mechanism here. In View’s draw(), draw background -> draw content(onDraw) -> draw children(dispatchDraw) Draw children by calling dispatchDraw(), in View’s dispatchDraw(), do nothing. In ViewGroup’s dispatchDraw(), use a for loop to call child’s draw() method. If clipToPadding is set to true(by default is true), the canvas pass to child will be clip base on ViewGroup’ size and scrolling position. That is why children can not draw outside its parent if not call setClipToPadding(false). In View’s source code, there is two draw() method, draw(Canvas) and draw(Canvas, ViewGroup, long), the second one is call by parent ViewGroup’s dispatchDraw(), and in this method, handle with the canvas(clip, translate and so on) and call the draw(Canvas). measure() -> onMeasure() layout() -> onLayout() draw() -> onDraw() Do not override xxx(), override onXXX() instead. ViewGroup ’s onLayout is abstract. ViewGroup will not call onDraw until you call setWillNotDraw(false). measure/layout/draw spend time butter measure/layout/draw spend time > 16ms -> janky Improve performance: measure: FrameLayout -> RelativeLayout -> LinearLayout layout: reduce the number of layout level. ImageView + TextView -> TextView Multi LinearLayout -> RelativeLayout … draw: in customView, try your best to reduce draw time(overdraw), clip canvas and just draw the necessary area. If necessary, you can try to draw content instead of using different view to make your UI(bypass measure layout, but will make coding harder).
周末跑去深圳参加了一场阿里主持的技术沙龙,主题是《如何构建高可用的APP》,沙龙中相关的ppt和视频可以在他们的微博中找到。沙龙中收获比较大的是有关UC的何杰分享的Android应用性能优化实践,和手Q web业务优化的解析。 Android应用性能优化实践中,提及了比较多性能优化的干货。一般我们对于性能的调试多是依赖开发者工具,而uc对性能优化做了额外有趣而且有创意的事情。 流量消耗(感谢zqjia大神指出) 性能 稳定性 内存占用 电量消耗 apk大小 应该是这六点,希望我没有记错。其中性能是一个比较重要而又比较难的难点。性能问题造成的因素很多,产品功能因素,代码原因,设备配置,设备运行软件数量,是否安装安全软件,rom适配,等等。有很多问题,在开发测试甚至灰度测试的时候难以发现,依赖用户反馈时,对问题描述量化等也比较难。 用户反馈分类:按照使用功能,发生频率,用户类型分类 StrictMode ANR:更加严格,会暴露更多可能的潜在问题 Looper Hook:在UI线程以外开启一条线程,定时向UI线程post一个runnable,并记下post时间。runnable的内容是将执行时间同步回发送线程,如果UI线程被阻塞,那么post过去的runnable不能被准时执行,那么同步回去的执行时间会与post时间有这较大的差值,设定几个阀值(1000ms或5000ms),用来评测不同情况下卡UI线程的情况,并可以通过log Message.what的值和Message.callback的类型来判断发生场景。对于这里,建议大家看下Looper,Message,Handler,MessageQueue的源码实现。 而在优化的细节上,uc也总结了很多方案(200+)。有一些关键点有: 分段加载 延迟加载:耗时操作延后执行,执行时机通过post runnable到UIThread触发,表明UIThread已经idle,可以尝试进行耗时操作 缓存复用:不只是数据,View也可以复用,在Activity rotate的时候,在View被destory前保存View实例,在Activity recreate之后将View重新add到layout中,只需要重新measure layout draw,省略new View的过程 运行时线程管理防止抢占资源,造成UIThread运行卡顿 引入异步dns及cache防止获取网络代理卡顿 解决start第三方Activity外部crash导致app卡死:采用外壳Activity方案解决,即start第三方Activity时采用:Activity -> ShieldActivity -> Third party Activity 的调用方法。 尝试开启GPU加速(难点,代价比较大) SharedPreference:commit()是UIThread线程执行,如果保存数据过大,可能卡UIThread,换用apply()。 安全软件拦截:沟通反馈 在举出案例之后,也给出了一些总结: 培养异步化的思维,不只是开发,产品也需要多考虑这一方面 不要尝试假定用户可能的使用场景在一个小的范围内,实际情况可能很极限(举了一个用户下载列表8k多条目的例子) 预加载 + 闲时加载 + 按需加载 线程限制管理(控制并发) + 任务队列 压力测试 防御式编程(Wiki) 禁止UIThread做如下操作:文件IO读写,耗CPU操作,IPC同步 洋洋洒洒居然写了这么多,由此可以看出这个分享的干货之多。真心十分感谢阿里的这次技术分享,真心学到东西了。沙龙的ppt和视频应该近期在他们的微博就会放出,大家可以关注一下。
在做Android开发的过程中,不可避免地需要使用到自带的android developer tools(开发人员工具),这是一个强大的开发辅助工具,随着android版本的更新,developer tools也集成了越来越多十分方便的调试功能,这里以android 4.4.4版本为例子,说说其中一部分我常用工具的使用(恕我才疏学浅没能全部懂用)。 显示布局边界 这个工具用于显示普通view布局的size,margin等属性,实际使用场景为:查看view的实际位置,检查界面是由普通view拼装而成或用surfaceView(WebView)实现。 强制使用从右到左的布局 也就是RTL布局(Right-to-Left),由于一些国家地区的语言习惯,书写阅读是从右到左(类似中国古代的书写习惯)。有些开发者可能很奇怪,在2.3或以上的android版本,padding,margin,gravity等,是left,right,top,bottom组合,到了3.0或以后,就多了个start和end,这个也是为了RTL布局而添加的。一般开发者都会无视这个选项,沿用常见的左右布局,但是如果你做的是国际化应用的话,就需要考虑RTL布局了。而这个开发者选项,就是让开发者可以在不切换语言区域的情况下,调试RTL布局。 显示GPU视图更新 随着android版本的更新,越来越多的绘制操作能使用GPU来完成,详见http://developer.android.com/guide/topics/graphics/hardware-accel.html,而这个工具打开之后,使用GPU绘制的区域会用红色来标注,而没有红色标注的区域,则是使用CPU绘制的。这个选项也可以用来查看redraw的区域大小。 调试GPU过度绘制 这个就是经典了Overdraw问题(过度绘制)了。我们知道,动画是通过一帧一帧不断重绘,连续播放实现的。而在绘制完成的时候,每一个像素点可能已经不可避免地被绘制了一次以上,由于每个像素点最终显示出来的只有一种颜色(假设透明度100%),所以先前绘制的操作都是无用的,这就是overdraw了。Overdraw会造成什么问题呢?为了达到60FPS的绘制速率,每一次绘制,都需要在16ms内完成(1000ms / 60),如果绘制的时间过长,就会导致绘制占用的时间过长,用户在使用时就会有卡顿的感觉。需要注意的一点是:Overdraw是无法避免的,我们只能尽量通过优化减少他。打开调试GPU过度绘制选项(显示过度绘制区域),屏幕会显示一些从浅绿到深红的色块,这些色块指示出了overdraw的程度,颜色越往深红靠,说明overdraw越严重。由于overdraw无法避免,要整个界面都显示白色几乎不可能,我们只能尽量让红色的区域减少。如果你发现你的app几乎整个都是深红色的,那说明你需要好好优化一下布局了。 GPU呈现模式分析 这个选项用于显示绘制速率。打开时屏幕下部会显示绘制速率条形图和一条横线。该横线表示在它以下的绘制时间少于16ms,绘制是流畅的。如果条形超出了横线,说明当前发生了绘制的卡滞,需要根据当前操作优化程序,提供一致的流畅体验。 不保留活动 这个选项用于调试activity被销毁的情况。当系统可用内存不足,新程序要求分配内存的时候,除了gc以外,系统可能会把后台的activity destroy掉以回收内存,为了能恢复被destory activity的状态,系统在destory时会调用onSaveInstanceState(Bundle)方法,将activity状态保存在bundle中;在activity重启的onCreate(Bundle)方法中,将保存的状态恢复。 回到主题,不保留活动选项,正是用来调试这个情况的。由于你无法预知系统什么时候会回收activity释放内存空间,我们需要手动触发他。打开这个选项之后,每一个不是visible的activity,都会直接被调用onSavedInstance并destory掉。所以,你只需要打开不保留活动,打开你要测试的activity,按home键回到桌面,再返回这个activity并观察现象尽可。如果状态恢复有问题,这时候应该会发生一个NPE(NullPointerException),或者视图上的数据显示有问题。这时再根据情况修改代码即可。 最后附上cyrilmottier大大有关android状态恢复的ppt,网络情况的好的直接看原链接。
在一个ViewGroup中处理touch events需要格外注意。因为在ViewGroup里面有着各种要处理不同touch events的子view,这是很常见的。为了确保每个view能正确地获取到属于他的touch events,我们必须覆盖onInterceptTouchEvent()函数。 ViewGroup中的Touch Events 当在一个ViewGroup表面,或者里面的子view表面检测到一个touch event的时候,会调用ViewGroup的onInterceptTouchEvent()函数。如果onInterceptTouchEvent()函数返回true时,这个MotionEvent将会被截断,也就意味着,不会传递给子view,而是交给父层(ViewGroup)的onTouchEvent函数处理。 onInterceptTouchEvent()方法提供了一个让父层在所以子view之前先获取到touch event的途径。如果你在onInterceptTouchEvent()中返回了true的话,先前获取到touch events的子view会接收到一个ACTION_CANCEL,而先前的events会传到父层的onTouchEvent做处理。onInterceptTouchEvent()也能直接返回false去简单地观察touch events按view hierarchy传递给他们原先的目标(子view),用子view的onTouchEvent去处理这些events。 在下面这些代码段中,MyViewGroup继承于ViewGroup,并包含了多个子view。如果你在子view上横向拖动,子view不再会获取到touch events,而MyViewGroup会处理这些touch events来滚动他的内容。但是,如果你按下子view中的button,或者纵向滚动子view,父层不会截获这些touch events,因为子view才是传递的目标。在这种情况中,onInterceptTouchEvent()应该返回false,这样MyViewGroup的onTouchEvent才不会被调用。 public class MyViewGroup extends ViewGroup { private int mTouchSlop; ... ViewConfiguration vc = ViewConfiguration.get(view.getContext()); mTouchSlop = vc.getScaledTouchSlop(); ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * 这个方法只是决定我们是否要截获这个手势. * 如果我们返回true,那么onTouchEvent 就会被调用,然后我们就开始进行滚动的操作 */ final int action = MotionEventCompat.getActionMasked(ev); // 先处理触摸手势完成的情况 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // 释放滚动. mIsScrolling = false; return false; // 不会截获touch event,让子view处理他 } switch (action) { case MotionEvent.ACTION_MOVE: { if (mIsScrolling) { // 处于正在滚动的状态,因此要截获touch event! return true; } // 如果用户手指横向移动量超过了阀值,则开始滚动 // 作为读者的练习:) final int xDiff = calculateDistanceX(ev); // 触摸阀值应该用ViewConfiguration常量来计算 if (xDiff > mTouchSlop) { // 开始滚动! mIsScrolling = true; return true; } break; } ... } // 通常情况下,我们不会想要截获touch event,它们应该让子view来处理 return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // 这里我们开始处理touch event (e.g. 如果action是ACTION_MOVE,滚动container)。 // 这个方法只有在touch event在onInterceptTouchEvent中被截获时调用 ... } } 需要注意的是ViewGroup也提供了一个requestDisallowInterceptTouchEvent()函数。ViewGroup可以通过调用这个函数,让子view阻止所有父层利用onInterceptTouchEvent()来截取touch event。 使用ViewConfiguration常量 上面的代码中使用了ViewConfiguration去初始化一个叫mTouchSlop的变量。你可以使用ViewConfiguration类去获取Android系统内置的距离,速度,次数等。 “Touch slop” 可以解释为一个用户手势被判断为滑动的距离。Touch slop通常用来防止用户的其他触摸行为(如触摸屏幕上的元素)被判断为滑动。 另外两个最常使用的ViewConfiguration方法是getScaledMinimumFlingVelocity()和getScaledMaximumFlingVelocity()。这两个方法会返回初始化滑动的最大速度值和最小速度值,以像素/秒为单位。例如: ViewConfiguration vc = ViewConfiguration.get(view.getContext()); private int mSlop = vc.getScaledTouchSlop(); private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); ... case MotionEvent.ACTION_MOVE: { ... float deltaX = motionEvent.getRawX() - mDownX; if (Math.abs(deltaX) > mSlop) { // 发生了滑动 } ... case MotionEvent.ACTION_UP: { ... } if (mMinFlingVelocity 下面的代码段会完成下面的事情: 获取父view并post一个Runnable到UI线程。这会确保父类在调用getHitRect())方法前先勾画出他的子类。getHitRect())方法的作用是在父类的坐标系中获取子view的hit rectangle(触摸区域)。 找到ImageButton子view并调用getHitRect())去获取子类触摸区域范围。 扩展ImageButton的hit rectangle范围。 初始化TouchDelegate对象,参数是扩展后的hit rectangle和ImageButton子view。 在父view中设置TouchDelegate,这样在这个触摸范围内的touch event都会传给ImageButton 在ImageButton子view的触摸范围容量内,父view会接收所有的touch events,如果touch event发生在子类的hit rectangle内,父类会将touch event传给子类做处理。 public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取父view View parentView = findViewById(R.id.parent_layout); parentView.post(new Runnable() { // post到父类的消息队列中,确保在调用getHitRect()前勾画出子类 @Override public void run() { // 实例view的区域范围(ImageButton) Rect delegateArea = new Rect(); ImageButton myButton = (ImageButton) findViewById(R.id.button); myButton.setEnabled(true); myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT).show(); } }); // ImageButton的hit rectangle myButton.getHitRect(delegateArea); // 在ImageButton边框的右边和底边扩展触摸区域 delegateArea.right += 100; delegateArea.bottom += 100; // 初始化TouchDelegate. // "delegateArea" is the bounds in local coordinates of // the containing view to be mapped to the delegate view. // "myButton" is the child view that should receive motion // events. TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton); // Sets the TouchDelegate on the parent view, such that touches // within the touch delegate bounds are routed to the child. if (View.class.isInstance(myButton.getParent())) { ((View) myButton.getParent()).setTouchDelegate(touchDelegate); } } }); } }
在android 2.3,解析bitmap的时候,不能用RGBA8888格式解析图片成bitmap,只能解析成RGB565,就算加了Config也一样。 在android google code project上的解释是这样的:https://code.google.com/p/android/issues/detail?id=13038 public static Bitmap convert(Bitmap bitmap, Bitmap.Config config) { Bitmap convertedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), config); Canvas canvas = new Canvas(convertedBitmap); Paint paint = new Paint(); paint.setColor(Color.BLACK); canvas.drawBitmap(bitmap, 0, 0, paint); return convertedBitmap; } 这个函数是最近在研究zxing解析本地图片时候发现的,突然发现这个函数貌似能用与解决不能解析为RGBA8888的bug,在测试之后,果然生效了。使用方法如下: Bitmap bitmap = convert(bitmap, Bitmap.Config.ARGB_8888); 这个函数用的也是很曲线救国的方法了,用canvas将原来的bitmap画到属性为RGBA8888的空bitmap上,成功绕过了android 2.3不能直接解析为RGBA8888的bug。 (吐槽:做android开发越久越觉得android坑啊……)
您可以订阅此RSS以获取更多信息