a亚洲精品_精品国产91乱码一区二区三区_亚洲精品在线免费观看视频_欧美日韩亚洲国产综合_久久久久久久久久久成人_在线区

首頁 > 系統 > Android > 正文

從源碼解析Android中View的容器ViewGroup

2019-12-12 06:41:03
字體:
來源:轉載
供稿:網友

 這回我們是深入到ViewGroup內部/,了解ViewGroup的工作,同時會闡述更多有關于View的相關知識。以便為以后能靈活的使用自定義空間打更近一步的基礎。希望有志同道合的朋友一起來探討,深入Android內部,深入理解Android。

一、ViewGroup是什么?
       一個ViewGroup是一個可以包含子View的容器,是布局文件和View容器的基類。在這個類里定義了ViewGroup.LayoutParams類,這個類是布局參數的子類。

       其實ViewGroup也就是View的容器。通過ViewGroup.LayoutParams來指定子View的參數。

ViewGroup作為一個容器,為了制定這個容器應有的標準所以為其指定了接口

public abstract class ViewGroup extends View implements ViewParent, ViewManager 
       這兩個接口這里不研究,如果涉及到的話會帶一下。ViewGroup有小4000行代碼,下面我們一個模塊一個模塊分析。

二、ViewGroup這個容器
       ViewGroup是一個容器,其采用一個數組來存儲這些子View:

// Child views of this ViewGroup  private View[] mChildren; 

       由于是通過一個數組來存儲View數據的,所以對于ViewGroup來說其必須實現增、刪、查的算法。下面我們就來看看其內部實現。

2.1 添加View的算法

  protected boolean addViewInLayout(View child, int index, LayoutParams params) {      return addViewInLayout(child, index, params, false);    }  protected boolean addViewInLayout(View child, int index, LayoutParams params,        boolean preventRequestLayout) {      child.mParent = null;      addViewInner(child, index, params, preventRequestLayout);      child.mPrivateFlags = (child.mPrivateFlags & ~DIRTY_MASK) | DRAWN;      return true;    }  private void addViewInner(View child, int index, LayoutParams params,        boolean preventRequestLayout) {      ...      addInArray(child, index);      ...    }  private void addInArray(View child, int index) {    ...    } 

       上面四個方法就是添加View的核心算法的封裝,它們是層層調用的關系。而我們通常調用的addView就是最終通過上面那個來最終達到添加到ViewGroup中的。

   2.1.1 我們先來分析addViewInner方法:
首先是對子View是否已經包含到一個父容器中,主要的防止添加一個已經有父容器的View,因為添加一個擁有父容器的View時會碰到各種問題。比如記錄本身父容器算法的問題、本身被多個父容器包含時更新的處理等等一系列的問題都會出現。

if (child.getParent() != null) {        throw new IllegalStateException("The specified child already has a parent. " +            "You must call removeView() on the child's parent first.");      } 

然后就是對子View布局參數的處理。

調用addInArray來添加View

父View為當前的ViewGroup

焦點的處理。

當前View的AttachInfo信息,這個信息是用來在窗口處理中用的。Android的窗口系統就是用過AttachInfo來判斷View的所屬窗口的,這個了解下就行。詳細信息設計到Android框架層的一些東西。

AttachInfo ai = mAttachInfo;      if (ai != null) {        boolean lastKeepOn = ai.mKeepScreenOn;        ai.mKeepScreenOn = false;        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));        if (ai.mKeepScreenOn) {          needGlobalAttributesUpdate(true);        }        ai.mKeepScreenOn = lastKeepOn;      } 

View樹改變的監聽

if (mOnHierarchyChangeListener != null) {        mOnHierarchyChangeListener.onChildViewAdded(this, child);      } 

子View中的mViewFlags的設置:

if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {        mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;      } 

2.1.2 addInArray
       這個里面的實現主要是有個知識點,以前也沒用過arraycopy,這里具體實現就不多加描述了。

System.arraycopy(children, 0, mChildren, 0, index);  System.arraycopy(children, index, mChildren, index + 1, count - index); 

2.2 移除View
       移除View的幾種方式:

(1)移除指定的View。

(2)移除從指定位置的View

(3)移除從指定位置開始的多個View

(4)移除所有的View

       其中具體涉及到的方法就有好多了,不過最終對要刪除的子View中所做的無非就是下列的事情:

如果擁有焦點則清楚焦點

將要刪除的View從當前的window中解除關系。

設置View樹改變的事件監聽,我們可以通過監聽OnHierarchyChangeListener事件來進行一些相應的處理。

從父容器的子容器數組中刪除。

       具體的內容這里就不一一貼出來了,大家回頭看看源碼就哦了。

2.3 查詢
       這個就簡單了,就是直接從數組中取出就可以了:

public View getChildAt(int index) {    try {      return mChildren[index];    } catch (IndexOutOfBoundsException ex) {      return null;    }  } 

       分析到這兒,其實我們已經相當于分析了ViewGroup四分之一的代碼了,呵呵。

三、onFinishInflate
       我們一般使用View的流程是在onCreate中使用setContentView來設置要顯示Layout文件或直接創建一個View,在當設置了ContentView之后系統會對這個View進行解析,然后回調當前視圖View中的onFinishInflate方法。只有解析了這個View我們才能在這個View容器中獲取到擁有Id的組件,同樣因為系統解析完View之后才會調用onFinishInflate方法,所以我們自定義組件時可以onFinishInflate方法中獲取指定子View的引用。

四、測量組件
       在ViewGroup中提供了測量子組件的三個方法。

1、measureChild(View, int, int),為子組件添加Padding   

  protected void measureChild(View child, int parentWidthMeasureSpec,        int parentHeightMeasureSpec) {      final LayoutParams lp = child.getLayoutParams();         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,          mPaddingLeft + mPaddingRight, lp.width);      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,          mPaddingTop + mPaddingBottom, lp.height);         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    } 

2、measureChildren(int, int)根據指定的高和寬來測量所有子View中顯示參數非GONE的組件。   

  protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {      final int size = mChildrenCount;      final View[] children = mChildren;      for (int i = 0; i < size; ++i) {        final View child = children[i];        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {          measureChild(child, widthMeasureSpec, heightMeasureSpec);        }      }    } 

3、measureChildWithMargins(View, int, int, int, int)測量指定的子組件,為子組件添加Padding和Margin。   

  protected void measureChildWithMargins(View child,        int parentWidthMeasureSpec, int widthUsed,        int parentHeightMeasureSpec, int heightUsed) {      final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,          mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin              + widthUsed, lp.width);      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,          mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin              + heightUsed, lp.height);         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    } 

       上面三個方法都是為子組件設置了布局參數。最終調用的方法是子組件的measure方法。在View中我們知道這個調用實際上就是設置了子組件的布局參數并且調用onMeasure方法,最終設置了View測量后的高度和寬度。

五、onLayout
       這個函數是一個抽象函數,要求實現ViewGroup的函數必須實現這個函數,這也就是ViewGroup是一個抽象函數的原因。因為各種組件實現的布局方式不一樣,而onLayout是必須被重載的函數。

@Override protected abstract void onLayout(boolean changed,      int l, int t, int r, int b); 

 
來看View中layout方法:   

public final void layout(int l, int t, int r, int b) {    boolean changed = setFrame(l, t, r, b);    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {      if (ViewDebug.TRACE_HIERARCHY) {        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);      }       onLayout(changed, l, t, r, b);      mPrivateFlags &= ~LAYOUT_REQUIRED;    }    mPrivateFlags &= ~FORCE_LAYOUT;  } 

       在這個方法中調用了setFrame方法,這個方法是用來設置View中的上下左右邊距用的

  protected boolean setFrame(int left, int top, int right, int bottom) {      boolean changed = false;      //.......      if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {        changed = true;            // Remember our drawn bit        int drawn = mPrivateFlags & DRAWN;            // Invalidate our old position        invalidate();                int oldWidth = mRight - mLeft;        int oldHeight = mBottom - mTop;            mLeft = left;        mTop = top;        mRight = right;        mBottom = bottom;            mPrivateFlags |= HAS_BOUNDS;            int newWidth = right - left;        int newHeight = bottom - top;            if (newWidth != oldWidth || newHeight != oldHeight) {          onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);        }            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) {          // If we are visible, force the DRAWN bit to on so that          // this invalidate will go through (at least to our parent).          // This is because someone may have invalidated this view          // before this call to setFrame came in, therby clearing          // the DRAWN bit.          mPrivateFlags |= DRAWN;          invalidate();        }            // Reset drawn bit to original value (invalidate turns it off)        mPrivateFlags |= drawn;            mBackgroundSizeChanged = true;      }      return changed;    }  

我們可以看到如果新的高度和寬度改變之后會調用重新設置View的四個參數:   
(1)protected int mLeft;    
(2)protected int mRight;    
(3)protected int mTop;    
(4)protected int mBottom;   
這四個參數指定了View將要布局的位置。而繪制的時候是通過這四個參數來繪制,所以我們在View中調用layout方法可以實現指定子View中布局。 

六、ViewGroup的繪制。
       ViewGroup的繪制實際上是調用的dispatchDraw,繪制時需要考慮動畫問題,而動畫的實現實際上就通過dispatchDraw來實現的。

       我們不用理會太多的細節,直接看其繪制子組件調用的是drawChild方法,這個里面具體的東西就多了,涉及到動畫效果的處理,如果有機會的話再寫,我們只要知道這個方法的功能就行。

這里有個demo貼出其中的代碼大家可以測試下。

public ViewGroup01(Context context)  {    super(context);    Button mButton = new Button(context);    mButton.setText("測試");    addView(mButton);  }    @Override protected void onLayout(boolean changed, int l, int t, int r, int b)  {    View v = getChildAt(0);    if(v != null)      {      v.layout(120, 120, 250, 250);      }  }  @Override protected void dispatchDraw(Canvas canvas)  {    super.dispatchDraw(canvas);    View v = getChildAt(0);    if(v != null)      {      drawChild(canvas, v, getDrawingTime());      }  } 

效果圖片:

201645154246537.png (327×439)

七、ViewGroup的事件分發機制
我們用手指去觸摸Android手機屏幕,就會產生一個觸摸事件,但是這個觸摸事件在底層是怎么分發的呢?這個我還真不知道,這里涉及到操作硬件(手機屏幕)方面的知識,也就是Linux內核方面的知識,我也沒有了解過這方面的東西,所以我們可能就往上層來分析分析,我們知道Android中負責與用戶交互,與用戶操作緊密相關的四大組件之一是Activity, 所以我們有理由相信Activity中存在分發事件的方法,這個方法就是dispatchTouchEvent(),我們先看其源碼吧

public boolean dispatchTouchEvent(MotionEvent ev) {      //如果是按下狀態就調用onUserInteraction()方法,onUserInteraction()方法     //是個空的方法, 我們直接跳過這里看下面的實現     if (ev.getAction() == MotionEvent.ACTION_DOWN) {       onUserInteraction();     }          if (getWindow().superDispatchTouchEvent(ev)) {       return true;     }          //getWindow().superDispatchTouchEvent(ev)返回false,這個事件就交給Activity     //來處理, Activity的onTouchEvent()方法直接返回了false     return onTouchEvent(ev);   } 

這個方法中我們還是比較關心getWindow()的superDispatchTouchEvent()方法,getWindow()返回當前Activity的頂層窗口Window對象,我們直接看Window API的superDispatchTouchEvent()方法

/**    * Used by custom windows, such as Dialog, to pass the touch screen event    * further down the view hierarchy. Application developers should    * not need to implement or call this.    *    */   public abstract boolean superDispatchTouchEvent(MotionEvent event); 

這個是個抽象方法,所以我們直接找到其子類來看看superDispatchTouchEvent()方法的具體邏輯實現,Window的唯一子類是PhoneWindow,我們就看看PhoneWindow的superDispatchTouchEvent()方法

public boolean superDispatchTouchEvent(KeyEvent event) {     return mDecor.superDispatcTouchEvent(event);   } 

里面直接調用DecorView類的superDispatchTouchEvent()方法,或許很多人不了解DecorView這個類,DecorView是PhoneWindow的一個final的內部類并且繼承FrameLayout的,也是Window界面的最頂層的View對象,這是什么意思呢?別著急,我們接著往下看
 
我們先新建一個項目,取名AndroidTouchEvent,然后直接用模擬器運行項目, MainActivity的布局文件為

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   xmlns:tools="http://schemas.android.com/tools"   android:layout_width="match_parent"   android:layout_height="match_parent"   tools:context=".MainActivity" >    <TextView     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_centerHorizontal="true"     android:layout_centerVertical="true"     android:text="@string/hello_world" />  </RelativeLayout> 

 
利用hierarchyviewer工具來查看下MainActivity的View的層次結構,如下圖

201645154444322.png (1160×275)

我們看到最頂層就是PhoneWindow$DecorView,接著DecorView下面有一個LinearLayout, LinearLayout下面有兩個FrameLayout
上面那個FrameLayout是用來顯示標題欄的,這個Demo中是一個TextView,當然我們還可以定制我們的標題欄,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我們自定義標題欄的布局XML文件
下面的FrameLayout是用來裝載ContentView的,也就是我們在Activity中利用setContentView()方法設置的View,現在我們知道了,原來我們利用setContentView()設置Activity的View的外面還嵌套了這么多的東西
我們來理清下思路,Activity的最頂層窗體是PhoneWindow,而PhoneWindow的最頂層View是DecorView,接下來我們就看DecorView類的superDispatchTouchEvent()方法

public boolean superDispatchTouchEvent(MotionEvent event) {       return super.dispatchTouchEvent(event);     } 

在里面調用了父類FrameLayout的dispatchTouchEvent()方法,而FrameLayout中并沒有dispatchTouchEvent()方法,所以我們直接看ViewGroup的dispatchTouchEvent()方法

/**   * {@inheritDoc}   */   @Override   public boolean dispatchTouchEvent(MotionEvent ev) {     final int action = ev.getAction();     final float xf = ev.getX();     final float yf = ev.getY();     final float scrolledXFloat = xf + mScrollX;     final float scrolledYFloat = yf + mScrollY;     final Rect frame = mTempRect;      //這個值默認是false, 然后我們可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法     //來改變disallowIntercept的值     boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;      //這里是ACTION_DOWN的處理邏輯     if (action == MotionEvent.ACTION_DOWN) {     //清除mMotionTarget, 每次ACTION_DOWN都很設置mMotionTarget為null       if (mMotionTarget != null) {         mMotionTarget = null;       }        //disallowIntercept默認是false, 就看ViewGroup的onInterceptTouchEvent()方法       if (disallowIntercept || !onInterceptTouchEvent(ev)) {         ev.setAction(MotionEvent.ACTION_DOWN);         final int scrolledXInt = (int) scrolledXFloat;         final int scrolledYInt = (int) scrolledYFloat;         final View[] children = mChildren;         final int count = mChildrenCount;         //遍歷其子View         for (int i = count - 1; i >= 0; i--) {           final View child = children[i];                      //如果該子View是VISIBLE或者該子View正在執行動畫, 表示該View才           //可以接受到Touch事件           if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE               || child.getAnimation() != null) {           //獲取子View的位置范圍             child.getHitRect(frame);                          //如Touch到屏幕上的點在該子View上面             if (frame.contains(scrolledXInt, scrolledYInt)) {               // offset the event to the view's coordinate system               final float xc = scrolledXFloat - child.mLeft;               final float yc = scrolledYFloat - child.mTop;               ev.setLocation(xc, yc);               child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;                              //調用該子View的dispatchTouchEvent()方法               if (child.dispatchTouchEvent(ev)) {                 // 如果child.dispatchTouchEvent(ev)返回true表示               //該事件被消費了,設置mMotionTarget為該子View                 mMotionTarget = child;                 //直接返回true                 return true;               }               // The event didn't get handled, try the next view.               // Don't reset the event's location, it's not               // necessary here.             }           }         }       }     }      //判斷是否為ACTION_UP或者ACTION_CANCEL     boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||         (action == MotionEvent.ACTION_CANCEL);      if (isUpOrCancel) {       //如果是ACTION_UP或者ACTION_CANCEL, 將disallowIntercept設置為默認的false     //假如我們調用了requestDisallowInterceptTouchEvent()方法來設置disallowIntercept為true     //當我們抬起手指或者取消Touch事件的時候要將disallowIntercept重置為false     //所以說上面的disallowIntercept默認在我們每次ACTION_DOWN的時候都是false       mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;     }      // The event wasn't an ACTION_DOWN, dispatch it to our target if     // we have one.     final View target = mMotionTarget;     //mMotionTarget為null意味著沒有找到消費Touch事件的View, 所以我們需要調用ViewGroup父類的     //dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法     if (target == null) {       // We don't have a target, this means we're handling the       // event as a regular view.       ev.setLocation(xf, yf);       if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {         ev.setAction(MotionEvent.ACTION_CANCEL);         mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;       }       return super.dispatchTouchEvent(ev);     }      //這個if里面的代碼ACTION_DOWN不會執行,只有ACTION_MOVE     //ACTION_UP才會走到這里, 假如在ACTION_MOVE或者ACTION_UP攔截的     //Touch事件, 將ACTION_CANCEL派發給target,然后直接返回true     //表示消費了此Touch事件     if (!disallowIntercept && onInterceptTouchEvent(ev)) {       final float xc = scrolledXFloat - (float) target.mLeft;       final float yc = scrolledYFloat - (float) target.mTop;       mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;       ev.setAction(MotionEvent.ACTION_CANCEL);       ev.setLocation(xc, yc);              if (!target.dispatchTouchEvent(ev)) {       }       // clear the target       mMotionTarget = null;       // Don't dispatch this event to our own view, because we already       // saw it when intercepting; we just want to give the following       // event to the normal onTouchEvent().       return true;     }      if (isUpOrCancel) {       mMotionTarget = null;     }      // finally offset the event to the target's coordinate system and     // dispatch the event.     final float xc = scrolledXFloat - (float) target.mLeft;     final float yc = scrolledYFloat - (float) target.mTop;     ev.setLocation(xc, yc);      if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {       ev.setAction(MotionEvent.ACTION_CANCEL);       target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;       mMotionTarget = null;     }      //如果沒有攔截ACTION_MOVE, ACTION_DOWN的話,直接將Touch事件派發給target     return target.dispatchTouchEvent(ev);   } 

這個方法相對來說還是蠻長,不過所有的邏輯都寫在一起,看起來比較方便,接下來我們就具體來分析一下

我們點擊屏幕上面的TextView來看看Touch是如何分發的,先看看ACTION_DOWN
在DecorView這一層會直接調用ViewGroup的dispatchTouchEvent(), 先看18行,每次ACTION_DOWN都會將mMotionTarget設置為null, mMotionTarget是什么?我們先不管,繼續看代碼,走到25行,  disallowIntercept默認為false,我們再看ViewGroup的onInterceptTouchEvent()方法

public boolean onInterceptTouchEvent(MotionEvent ev) {    return false;  } 

直接返回false, 繼續往下看,循環遍歷DecorView里面的Child,從上面的MainActivity的層次結構圖我們可以看出,DecorView里面只有一個Child那就是LinearLayout, 第43行判斷Touch的位置在不在LinnearLayout上面,這是毫無疑問的,所以直接跳到51行, 調用LinearLayout的dispatchTouchEvent()方法,LinearLayout也沒有dispatchTouchEvent()這個方法,所以也是調用ViewGroup的dispatchTouchEvent()方法,所以這個方法卡在51行沒有繼續下去,而是去先執行LinearLayout的dispatchTouchEvent()
LinearLayout調用dispatchTouchEvent()的邏輯跟DecorView是一樣的,所以也是遍歷LinearLayout的兩個FrameLayout,判斷Touch的是哪個FrameLayout,很明顯是下面那個,調用下面那個FrameLayout的dispatchTouchEvent(),  所以LinearLayout的dispatchTouchEvent()卡在51也沒繼續下去
繼續調用FrameLayout的dispatchTouchEvent()方法,和上面一樣的邏輯,下面的FrameLayout也只有一個Child,就是RelativeLayout,FrameLayout的dispatchTouchEvent()繼續卡在51行,先執行RelativeLayout的dispatchTouchEvent()方法
執行RelativeLayout的dispatchTouchEvent()方法邏輯還是一樣的,循環遍歷 RelativeLayout里面的孩子,里面只有一個TextView, 所以這里就調用TextView的dispatchTouchEvent(), TextView并沒有dispatchTouchEvent()這個方法,于是找TextView的父類View,在看View的dispatchTouchEvent()的方法之前,我們先理清下上面這些ViewGroup執行dispatchTouchEvent()的思路,我畫了一張圖幫大家理清下(這里沒有畫出onInterceptTouchEvent()方法)

201645154529404.png (226×489)

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 日韩成人一区 | 91久久久久久 | 国产精品毛片一区二区三区 | 久久久久久久亚洲精品 | 欧美亚洲在线 | 国产精品夜间视频香蕉 | 成人午夜精品 | 在线免费中文字幕 | 欧美成人一区二区三区片免费 | 在线欧美视频 | 中文久久| 久久亚洲天堂 | 中文资源在线观看 | 看真人视频a级毛片 | 精品美女在线 | 伊人网av | 一级大片av | 秋霞a级毛片在线看 | 日本 国产 欧美 | 日本激情网 | 日韩一区二区在线播放 | 在线视频亚洲 | 成人日批 | 9uu在线观看 | 久久成人综合 | 看欧美黄色录像 | 精品黑人一区二区三区久久 | 成人精品一区二区三区中文字幕 | yy6080久久伦理一区二区 | 成人影院一区二区三区 | 国内精品视频一区二区三区 | 艹逼网| 99精品国产在热久久 | 亚洲精品九九 | 亚洲成a人v欧美综合天堂麻豆 | 精品久久久久久久久久久久久久 | 国产一区 | 国产一区二区在线看 | 午夜大片网 | 国产乱精品一区二区三区 | 免费一区二区三区 |