《代码里的世界》 —UI篇
用文字札记描绘自己 android学习之路
转载请保留出处 by Qiao
http://blog.youkuaiyun.com/qiaoidea/article/details/72943797
CoordinatorLayout出自android.support.design库,是一个控制接管子View之间动画交互的一个强大的库。通过一系列封装,使得界面在滑动交互方面更加方便。
CoordinatorLayout的几个概念
• CoordinatorLayout 一个类似FramLayout的容器,负责处理滑动事件和维护子View的交互。
• CoordinatorLayout.Behavior 子View之间交互响应的接口,由CoordinatorLayout统一管理控制
• CoordinatorLayout.LayoutParams 集成了Behavior的布局参数
1.Behaivior附加到子View
由于Behavior是基于子View的动画交互扩展,View本身并没有set/get对应交互表现的方法,所以CoordinatorLayout对于子View的管理控制主要是通过两种方式给子View附加Behavior属性。
(1) layoutParams中定义Behavior.
使用布局参数在layout中追加layout_behavior字段,然后通过解析AttributeSet参数并反射成对应的Behavior来完成,最后附加在子View的LayoutParams中。其中代码实现概要
//解析AttributeSet,实在转换为LayoutParams时候进行
LayoutParams(Context context, AttributeSet attrs) {
//... other code
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
//详细反射解析过程,通过包名/类名来构造Behavior
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
(2).使用 @CoordinatorLayout.DefaultBehavior 注解
在子View的class类中添加注解,类声明的时候添加对应的behavior.
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton {
//....
}
解析过程:
//查找注解并构造
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null &&
(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(defaultBehavior.value().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
" could not be instantiated. Did you forget a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
return result;
}
2. 交互回调
拿到Behavior之后,在各种滑动或交互事件发生时通过Behavior接口将事件分发。以dispatchDependentViewsChanged举例
ublic void dispatchDependentViewsChanged(View view) {
final int childCount = mDependencySortedChildren.size();
boolean viewSeen = false;
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child == view) {
// We've seen our view, which means that any Views after this could be dependent
viewSeen = true;
continue;
}
if (viewSeen) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
child.getLayoutParams();
CoordinatorLayout.Behavior b = lp.getBehavior();
if (b != null && lp.dependsOn(this, child, view)) {
b.onDependentViewChanged(this, child, view);
}
}
}
}
通常,对于Behavior接口,我们只需要关注layoutDependsOn和onDependentViewChanged即可。其中
- layoutDependsOn方法在每次layout发生变化时都会调用,用于判断某个View是否是它所关注/依赖的目标对象dependency控件。为true时,当目标在屏幕上滑动或发生改变时,会通知我们做出相应的反应。
- onDependentViewChanged 一旦layoutDependsOn 返回为true,此方法就会被调用,我们可以在此方法中实现相应的动画效果。
仍以系统封装的FloatingActionButton为例,其响应也主要是实现上述两个方法
3. 使用范例
/** **依赖目标SnackbarLayout ,其中 SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; **/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
FloatingActionButton child, View dependency) {
// We're dependent on all SnackbarLayouts (if enabled)
return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
}
/** **当目标View发生位移等变化时,自己也做出相应改变,详细变化略 **/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
} else if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibility(parent, (AppBarLayout) dependency, child);
}
return false;
}
增加behavior前后对比:
延伸
1. AppBarLayout
是继承LinerLayout实现的一个ViewGroup容器组件,默认的AppBarLayout是垂直方向的, 可以管理其中的控件在内容滚动时的行为。
- ScrollingViewBehavior 一套默认的垂直滑动表现,它随着目标appbarLayout的垂直方向变化做相应的distanceY滑动。
- ScrollFlags 决定AppBarLayout子控件的表现行为。在layout中可以通过layout_scrollFlags来配置。其中参数及含义
- SCROLL_FLAG_SCROLL(对应xml scroll) 随着appbarlayout滚动而滚动,没有此属性则view位置保持不变
- SCROLL_FLAG_ENTER_ALWAYS(enterAlways) 当任何向下滚动事件发生时,都会触发子View进入显示
- SCROLL_FLAG_EXIT_UNTIL_COLLAPSED(exitUntilCollapsed) 当向上移出屏幕时,View会一直收缩到最小高度后,再移出屏幕
- SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED 当向下移动到子View的最小高度时候,View才会开始显示
- SCROLL_FLAG_SNAP(snap) 当滚动结束时,如果View只有部分可见,它将会自动滑动到最近的边界(完全可见或完全隐藏)
注:
想要实现顶部滑动隐藏的toolbar,前提是 CoordinatorLayout布局包含一个可以滑动的布局,由于事件拦截处理缘故,该可滑动布局可以是 NestedScrollView/ RecyclerView(实现了NestedScrollingChild
, *ScrollingView*
接口),对于传统的 ScollView 和 ListView无效。
2.CollapsingToolbarLayout
通过包含一个Toolbar来实现可折叠效果的布局样式,一般作为AppBarLayout的子View来使用。- CollapsingTitle. ToolBar的标题,通过setTitle(CharSequence)方法设置title。该title是随着CollapsingToolbarLayout折叠产生从大变小的效果。
- ContentScrim. CollapsingToolbarLayout的背景,通过setContentScrim(Drawable)方法或xml使用contentScrim来改变背景。
- StatusBarScrim. 状态栏的背景,方法setStatusBarScrim(Drawable)只能在Android L以上有用。
- Parallax scrolling children. CollapsingToolbarLayout滑动时子视图的视觉差,可以通过setParallaxMultiplier(float)或者 xml中使用layout_collapseParallaxMultiplier设置
- CollapseMode. 子视图的折叠模式。在xml中通过layout_collapseMode设置
COLLAPSE_MODE_OFF
无模式,同普通View表现一样COLLAPSE_MODE_PIN
(pin) 固定模式,在折叠的时候保持不变知道最后固定在顶端COLLAPSE_MODE_PARALLAX
(parallax)` 视差模式,在折叠的时候会有个视差折叠的效果
3. SwipeDismissBehavior
用于CoordinatorLayout中实现子View 的滑动删除,利用的是ViewDragHelper工具类,实现OnDismissListener接口即可
public interface OnDismissListener {
public void onDismiss(View view);
public void onDragStateChanged(int state);
}