view系列源碼分析之CoordinatorLayout,嵌套滑動,自定義Behavior分析(1)

怎么自定義behavior?
自定義behavior的幾個重載方法的參數(shù)有何意義(何為消耗)?
什么是嵌套滑動雁芙?Behavior里有dependency這個依賴和嵌套滑動有關(guān)系
么?
CoordinatorLayout內(nèi)一定要有appbarlayout?亦或是CollapsingToolbarLayout钞螟?

這幾個問題相信很多人都覺得似懂非懂,如果你對事件分發(fā)兔甘,view的機(jī)制冥然于心的話,那分析出coordinatorLayout自然這幾個問題也就引刃而解了鳞滨,首先我們從CoordinatorLayout的onMeasure開始說起(基于android-27)的源碼:

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

我們可以看到首先調(diào)用了這兩個很重要的方法,首先看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);
   }

很明顯關(guān)鍵方法是

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的對應(yīng)的behavior,前提是mBehaviorResolved為空拯啦,引出了第一個概念
behavior,其實(shí)啊如果看過你必須了解的LayoutParams的那些事兒就知道在addview的時候
就會創(chuàng)建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);
        }
    }

我們可以看到它反射了構(gòu)造器來創(chuàng)建對象褒链,這里說一下唁情,這個構(gòu)造器
的參數(shù)必須是2個參數(shù)。

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[]{
           Context.class,
           AttributeSet.class
   };
所以自定義behavior必須要兩個參數(shù)的構(gòu)造

那我們知道了創(chuàng)建behavior第一種是在xml里甫匹,第二種在onMeasure通過注解甸鸟,而且xml的優(yōu)先級顯然比注解的優(yōu)先級高。
那我們在回到一開始的prepareChildren方法兵迅,我們可以看到下面又出現(xiàn)了一個 if (lp.dependsOn(this, view, other))這個方法.

behavior里的依賴關(guān)系

這里我們舉個最簡單的例子

<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發(fā)現(xiàn)了依賴的代碼

 @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的參數(shù)child是nestScrolledView
dependency如果是appBarLayout此方法就返回true,當(dāng)然在這個
prepareChildren里面調(diào)動是為了做排序讓被依賴的放在在這個列表的前面,比如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();
           }
       }
   }

這個方法最終會調(diào)用onChildViewsChanged()這里的參數(shù)是EVENT_PRE_DRAW同時會注冊一個preDraw的監(jiān)聽,具體那里調(diào)用可以看下淺談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扯夭,然后對應(yīng)的在每個view上畫出rect的大小鳍贾,然后每次改變都會改變rect的大小,從后文的for循環(huán)里知道j=i+1開始勉抓,舉個例子有a,b,c三個view贾漏,a在第一個,b和c都依賴于它藕筋,那當(dāng)在onMeasure時就會調(diào)用b,c的onDependentViewChanged方法纵散,因?yàn)楸灰蕾嚨挠肋h(yuǎn)在后面。

總結(jié):

了解了behavior的創(chuàng)建過程隐圾。
知道了onDependentViewChanged第一個調(diào)用的地方伍掀。
依賴的產(chǎn)生原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末暇藏,一起剝皮案震驚了整個濱河市蜜笤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盐碱,老刑警劉巖把兔,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪伙,死亡現(xiàn)場離奇詭異,居然都是意外死亡县好,警方通過查閱死者的電腦和手機(jī)围橡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缕贡,“玉大人翁授,你說我怎么就攤上這事×肋洌” “怎么了收擦?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谍倦。 經(jīng)常有香客問我塞赂,道長,這世上最難降的妖魔是什么昼蛀? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任减途,我火速辦了婚禮,結(jié)果婚禮上曹洽,老公的妹妹穿的比我還像新娘。我一直安慰自己辽剧,他們只是感情好送淆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怕轿,像睡著了一般偷崩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撞羽,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天阐斜,我揣著相機(jī)與錄音,去河邊找鬼诀紊。 笑死谒出,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邻奠。 我是一名探鬼主播笤喳,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼碌宴!你這毒婦竟也來了杀狡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤贰镣,失蹤者是張志新(化名)和其女友劉穎呜象,沒想到半個月后膳凝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恭陡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年蹬音,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片子姜。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡祟绊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哥捕,到底是詐尸還是另有隱情牧抽,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布遥赚,位于F島的核電站扬舒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏凫佛。R本人自食惡果不足惜讲坎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望愧薛。 院中可真熱鬧晨炕,春花似錦、人聲如沸毫炉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞄勾。三九已至费奸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間进陡,已是汗流浹背愿阐。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留趾疚,地道東北人缨历。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像盗蟆,于是被迫代替她去往敵國和親戈二。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容