CoordinatorLayout实现View动画交互的研究(16/04/18)

《代码里的世界》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*接口),对于传统的 ScollViewListView无效。

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);
    } 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值