美文网首页Android-CoordinatorLayout.……
CoordinatorLayout,嵌套滑动,自定义Behavi

CoordinatorLayout,嵌套滑动,自定义Behavi

作者: 暴走的小青春 | 来源:发表于2018-11-26 20:52 被阅读907次

怎么自定义behavior?
自定义behavior的几个重载方法的参数有何意义(何为消耗)?
什么是嵌套滑动?Behavior里有dependency这个依赖和嵌套滑动有关系
么?
CoordinatorLayout内一定要有appbarlayout?亦或是CollapsingToolbarLayout?

这几个问题相信很多人都觉得似懂非懂,如果你对事件分发,view的机制冥然于心的话,那分析出coordinatorLayout自然这几个问题也就引刃而解了,首先我们从CoordinatorLayout的onMeasure开始说起(基于android-27)的源码:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ...
...}

我们可以看到首先调用了这两个很重要的方法,首先看prepareChildren

private void prepareChildren() {
       mDependencySortedChildren.clear();
       mChildDag.clear();

       for (int i = 0, count = getChildCount(); i < count; i++) {
           final View view = getChildAt(i);
      
           final LayoutParams lp = getResolvedLayoutParams(view);
          
           lp.findAnchorView(this, view);

           mChildDag.addNode(view);

           // Now iterate again over the other children, adding any dependencies to the graph
           for (int j = 0; j < count; j++) {
               if (j == i) {
                   continue;
               }
               final View other = getChildAt(j);
               if (lp.dependsOn(this, view, other)) {
                   if (!mChildDag.contains(other)) {
                       // Make sure that the other node is added
                       mChildDag.addNode(other);
                   }
                   // Now add the dependency to the graph
                   mChildDag.addEdge(other, view);
               }
           }
       }

       // Finally add the sorted graph list to our list
       mDependencySortedChildren.addAll(mChildDag.getSortedList());
       // We also need to reverse the result since we want the start of the list to contain
       // Views which have no dependencies, then dependent views after that
       Collections.reverse(mDependencySortedChildren);
   }

很明显关键方法是

LayoutParams getResolvedLayoutParams(View child) {
       final LayoutParams result = (LayoutParams) child.getLayoutParams();
       if (!result.mBehaviorResolved) {
           if (child instanceof AttachedBehavior) {
               Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
               if (attachedBehavior == null) {
                   Log.e(TAG, "Attached behavior class is null");
               }
               result.setBehavior(attachedBehavior);
               result.mBehaviorResolved = true;
           } else {
               // The deprecated path that looks up the attached behavior based on annotation
               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().getDeclaredConstructor().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;
   }

这个是啥意思呢,很明显就是在onMeasure时通过注解获取view的对应的behavior,前提是mBehaviorResolved为空,引出了第一个概念
behavior,其实啊如果看过你必须了解的LayoutParams的那些事儿就知道在addview的时候
就会创建layoutParams,这里我们看到CoordinatorLayout它的LayoutParams

LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_Layout);

            this.gravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                    Gravity.NO_GRAVITY);
            mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                    View.NO_ID);
            this.anchorGravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                    Gravity.NO_GRAVITY);

            this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                    -1);

            insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
            dodgeInsetEdges = a.getInt(
                    R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }

截取部分代码看到是通过一个parseBehavior方法把一个String类型的
layout_behavior,变成了一个类,parseBehavior如下所示:

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>) context.getClassLoader()
                        .loadClass(fullName);
                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个参数。

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[]{
           Context.class,
           AttributeSet.class
   };
所以自定义behavior必须要两个参数的构造

那我们知道了创建behavior第一种是在xml里,第二种在onMeasure通过注解,而且xml的优先级显然比注解的优先级高。
那我们在回到一开始的prepareChildren方法,我们可以看到下面又出现了一个 if (lp.dependsOn(this, view, other))这个方法.

behavior里的依赖关系

这里我们举个最简单的例子

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:id="@+id/main_content"

   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <android.support.design.widget.AppBarLayout

       android:id="@+id/appbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

       <android.support.v7.widget.Toolbar

           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           android:background="?attr/colorPrimary"
           app:layout_scrollFlags="scroll|enterAlways" />


   </android.support.design.widget.AppBarLayout>

   <android.support.v4.widget.NestedScrollView
       android:id="@+id/recyclerView"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">

       <TextView
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:text="@string/app_text" />
   </android.support.v4.widget.NestedScrollView>


</android.support.design.widget.CoordinatorLayout>

这个布局相信大家很熟悉,我们可以看到NestedScrollView有个app:layout_behavior是appBarLayout的ScrollingViewBehavior,AppBarLayout也有behavior(用注解表示)是

public static class Behavior extends HeaderBehavior<AppBarLayout> {
       private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
       private static final int INVALID_POSITION = -1;
...
}

我们在ScrollingViewBehavior发现了依赖的代码

 @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            // We depend on any AppBarLayouts
            return dependency instanceof AppBarLayout;
        }

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                View dependency) {
            offsetChildAsNeeded(parent, child, dependency);
            return false;
        }

由此我们知道layoutDependsOn的参数child是nestScrolledView
dependency如果是appBarLayout此方法就返回true,当然在这个
prepareChildren里面调动是为了做排序让被依赖的放在在这个列表的前面,比如nestScrolledView依赖于appBarLayout,那么appBarLayout肯定在nestScrolledView的前边。也就是说布局文件里
你把nestScrolledView写在appBarLayout上面依然是appBarLayout在第一个view.

接下来看第二个方法ensurePreDrawListener

void ensurePreDrawListener() {
       boolean hasDependencies = false;
       final int childCount = getChildCount();
       for (int i = 0; i < childCount; i++) {
           final View child = getChildAt(i);
           if (hasDependencies(child)) {
               hasDependencies = true;
               break;
           }
       }

       if (hasDependencies != mNeedsPreDrawListener) {
           if (hasDependencies) {
               addPreDrawListener();
           } else {
               removePreDrawListener();
           }
       }
   }

这个方法最终会调用onChildViewsChanged()这里的参数是EVENT_PRE_DRAW同时会注册一个preDraw的监听,具体那里调用可以看下浅谈ondraw的前世今身

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
      final int layoutDirection = ViewCompat.getLayoutDirection(this);
      final int childCount = mDependencySortedChildren.size();
      final Rect inset = acquireTempRect();
      final Rect drawRect = acquireTempRect();
      final Rect lastDrawRect = acquireTempRect();

      for (int i = 0; i < childCount; i++) {
          final View child = mDependencySortedChildren.get(i);
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
              // Do not try to update GONE child views in pre draw updates.
              continue;
          }

          // Check child views before for anchor
          for (int j = 0; j < i; j++) {
              final View checkChild = mDependencySortedChildren.get(j);

              if (lp.mAnchorDirectChild == checkChild) {
                  offsetChildToAnchor(child, layoutDirection);
              }
          }

          // Get the current draw rect of the view
          getChildRect(child, true, drawRect);

          // Accumulate inset sizes
          if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
              final int absInsetEdge = GravityCompat.getAbsoluteGravity(
                      lp.insetEdge, layoutDirection);
              switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
                  case Gravity.TOP:
                      inset.top = Math.max(inset.top, drawRect.bottom);
                      break;
                  case Gravity.BOTTOM:
                      inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
                      break;
              }
              switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
                  case Gravity.LEFT:
                      inset.left = Math.max(inset.left, drawRect.right);
                      break;
                  case Gravity.RIGHT:
                      inset.right = Math.max(inset.right, getWidth() - drawRect.left);
                      break;
              }
          }

          // Dodge inset edges if necessary
          if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
              offsetChildByInset(child, inset, layoutDirection);
          }

          if (type != EVENT_VIEW_REMOVED) {
              // Did it change? if not continue
              getLastChildRect(child, lastDrawRect);
              if (lastDrawRect.equals(drawRect)) {
                  continue;
              }
              recordLastChildRect(child, drawRect);
          }
          // Update any behavior-dependent views for the change
          for (int j = i + 1; j < childCount; j++) {

              final View checkChild = mDependencySortedChildren.get(j);
              final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
              final Behavior b = checkLp.getBehavior();

              if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                  if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                      // If this is from a pre-draw and we have already been changed
                      // from a nested scroll, skip the dispatch and reset the flag
                      checkLp.resetChangedAfterNestedScroll();

                      continue;
                  }

                  final boolean handled;
                  switch (type) {
                      case EVENT_VIEW_REMOVED:
                          // EVENT_VIEW_REMOVED means that we need to dispatch
                          // onDependentViewRemoved() instead
                          b.onDependentViewRemoved(this, checkChild, child);
                          handled = true;
                          break;
                      default:
                          // Otherwise we dispatch onDependentViewChanged()
                          handled = b.onDependentViewChanged(this, checkChild, child);
                          break;
                  }

                  if (type == EVENT_NESTED_SCROLL) {
                      // If this is from a nested scroll, set the flag so that we may skip
                      // any resulting onPreDraw dispatch (if needed)

                      checkLp.setChangedAfterNestedScroll(handled);
                  }
              }
          }
      }

      releaseTempRect(inset);
      releaseTempRect(drawRect);
      releaseTempRect(lastDrawRect);
  }

这段代码首先新建了三个rect,然后对应的在每个view上画出rect的大小,然后每次改变都会改变rect的大小,从后文的for循环里知道j=i+1开始,举个例子有a,b,c三个view,a在第一个,b和c都依赖于它,那当在onMeasure时就会调用b,c的onDependentViewChanged方法,因为被依赖的永远在后面。

总结:

了解了behavior的创建过程。
知道了onDependentViewChanged第一个调用的地方。
依赖的产生原理。

相关文章

网友评论

    本文标题:CoordinatorLayout,嵌套滑动,自定义Behavi

    本文链接:https://www.haomeiwen.com/subject/bzliqqtx.html