view系列源码分析之CoordinatorLayout,嵌套滑动,自定义Behavior分析(1)

怎么自定义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第一个调用的地方。
依赖的产生原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容