1. 程式人生 > >NGUI核心大剖析(一) UIRect

NGUI核心大剖析(一) UIRect

UIRect是UIPanel和UIWidget的基類,研究好前者,才能更方便的研究後兩者,所以我將它做為本系列的第一篇。

(注:本系列討論的NGUI版本為3.10.0)

首先要知道的一點是,UIRect是一個abstract class,也就是抽象類,它不能被例項化。它為UIPanel和UIWidget提供了一些公用的方法,這些方法通過AnchorPoint設定後兩者的矩形區域。在UIRect的最開始,我們看到有一個Inner Class叫做AnchorPoint。結構很簡單,類函式的演算法也都很簡單,具體的用途我們暫時不講,後面會提到。唯一要注意的是,在set方法裡,我們看到:
this.absolute = Mathf.FloorToInt(absolute + 0.5f);

這其實就等同於
this.absolute = Mathf.RoundToInt(absolute);

接著往下看
	/// <summary>
	/// Left side anchor.
	/// </summary>

	public AnchorPoint leftAnchor = new AnchorPoint();

	/// <summary>
	/// Right side anchor.
	/// </summary>

	public AnchorPoint rightAnchor = new AnchorPoint(1f);

	/// <summary>
	/// Bottom side anchor.
	/// </summary>

	public AnchorPoint bottomAnchor = new AnchorPoint();

	/// <summary>
	/// Top side anchor.
	/// </summary>

	public AnchorPoint topAnchor = new AnchorPoint(1f);



定義了四個AnchorPoint型別的變數,分別對應左右下上四個位置的Anchor Point,也就是錨點。具體是什麼,我們就要結合在Unity Editor裡面的應用來看了。新建一個widget,把Anchors裡的Type改成Unified或者Advanced,我們就可以自行調節Anchor Point了。Unified和Advanced區別在於,Unified統一指定一個Transform為target,而Advanced可以為左右下上每一個Anchor Point指定一個Transform為target。我們這裡以Unified為例:如圖所示,以UI Root為Target,所謂Target其實就是參考系的載體,真正的參考系是圖中的Target‘s Center也就是UI Root的中心位置。可以這麼理解,UIWidget的左邊在UI Root(水平)中心點的x座標-50的位置,右邊在UI Root(水平)中心點的x座標+50的位置,Bottom和Top與之類似。
通過調節Left,Right,Bottom,Top後面的值,我們可以看到UIWidget的Position和Size在變化,而Scene裡面所示的UIWidget的區域也隨之增大減小。至於參考系下拉選單裡的其他選項,其實都是取Target的不同位置(Set To Current Position是一個快捷方法,把參考系拉倒和UIWidget當前位置重疊的UIRoot的位置上),以達到UI可以根據螢幕大小自適應變化的目的。當然,前面說“位置”,其實並不準確,準確的來講其實是位置除以尺寸,我們姑且稱之為參考系比例關於螢幕自適應,有機會的話,我們會在後續的文章中講到。回到程式碼中,看SetAnchor及其幾個過載的方法,我們拿引數列表最長的為例
	/// <summary>
	/// Anchor this rectangle to the specified transform.
	/// </summary>

	public void SetAnchor (GameObject go,
		float left, int leftOffset,
		float bottom, int bottomOffset,
		float right, int rightOffset,
		float top, int topOffset)
	{
		Transform t = (go != null) ? go.transform : null;

		leftAnchor.target = t;
		rightAnchor.target = t;
		topAnchor.target = t;
		bottomAnchor.target = t;

		leftAnchor.relative = left;
		rightAnchor.relative = right;
		bottomAnchor.relative = bottom;
		topAnchor.relative = top;

		leftAnchor.absolute = leftOffset;
		rightAnchor.absolute = rightOffset;
		bottomAnchor.absolute = bottomOffset;
		topAnchor.absolute = topOffset;

		ResetAnchors();
		UpdateAnchors();
	}



target對應的就是Editor中的target,relative對應的是參考系比例(center為0.5f,left和bottom為0,right和top為1),absolute對應的就是相對座標。設定完引數之後,就重置並更新Anchors。

我們回頭看AnchorPoint這個Inner Class裡的幾個方法。Set不必多說就是設定參考系比例和相對座標。SetToNearest實在UIRectEditor裡用到,給出相對於三個參考系比例(預設0,0.5f,1)的相對座標,取最小值所對應的參考系比例。SetHorizontal根據某個transform計算出新的橫向偏移量(參考系比例不變)。SetVertical同理。GetSide獲取相對於某個transform的座標。一般會呼叫UIRect的GetSides,後面我們會講到。回到之前的編輯器截圖
我們還差了一個Execute的引數沒有講,這個對應了下面這段程式碼
	public enum AnchorUpdate
	{
		OnEnable,
		OnUpdate,
		OnStart,
	}

	/// <summary>
	/// Whether anchors will be recalculated on every update.
	/// </summary>

	public AnchorUpdate updateAnchors = AnchorUpdate.OnUpdate;


AnchorUpdate是個列舉類,定義了Anchor的三種更新方式,雖然是三種,但其實更新的程式碼都在Update中。通過布林值mUpdateAnchors或者updateAnchors == AnchorUpdate.OnUpdate來判斷是否需要更新。OnStart是隻更新一次,因為mUpdateAnchors預設值是true,OnEnable是在GameObject.setActive(true)或者這個元件enabled=true的時候呼叫OnEnable,令mUpdateAnchors = true 而OnUpdate是每個邏輯幀都會更新。
接下來是ResetAnchor
	/// <summary>
	/// Ensure that all rect references are set correctly on the anchors.
	/// </summary>

	public void ResetAnchors ()
	{
		mAnchorsCached = true;

		leftAnchor.rect		= (leftAnchor.target)	? leftAnchor.target.GetComponent<UIRect>()	 : null;
		bottomAnchor.rect	= (bottomAnchor.target) ? bottomAnchor.target.GetComponent<UIRect>() : null;
		rightAnchor.rect	= (rightAnchor.target)	? rightAnchor.target.GetComponent<UIRect>()	 : null;
		topAnchor.rect		= (topAnchor.target)	? topAnchor.target.GetComponent<UIRect>()	 : null;

		mCam = NGUITools.FindCameraForLayer(cachedGameObject.layer);

		FindCameraFor(leftAnchor);
		FindCameraFor(bottomAnchor);
		FindCameraFor(rightAnchor);
		FindCameraFor(topAnchor);

		mUpdateAnchors = true;
	}


ResetAnchors為每個Anchor找到target上的UIRect,並且找到本GameObject對應的Camera,最後為每個Anchor找到target對應的Camera(targetCamera)。rect我們會在UpdateAnchors裡講到。mCam在cameraRayDistance和GetSides裡用到(UIPanel也用到了,後面講到UIPanel的時候可能會講到它),程式碼不看,只看註釋:
/// Helper function that returns the distance to the camera's directional vector hitting the panel's plane.

(cameraRayDistance)簡單來講就是camera和panel之間的距離。
/// Get the sides of the rectangle relative to the specified transform.
/// The order is left, top, right, bottom.

(GetSides)簡單來講就是獲得矩形四條邊相對於某個transfrom的座標(x或y)targetCamera是為了GetLocalPos這個方法準備的,我們也只看註釋:
/// Helper function that gets the specified anchor's position relative to the chosen transform.

獲取某個anchor相對於某個transform的位置UpdateAnchors這個方法裡其實沒什麼,真正執行的是UpdateAnchorsInternal這個方法
	/// <summary>
	/// Update anchors.
	/// </summary>

	protected void UpdateAnchorsInternal (int frame)
	{
		mUpdateFrame = frame;
		mUpdateAnchors = false;

		bool anchored = false;

		if (leftAnchor.target)
		{
			anchored = true;
			if (leftAnchor.rect != null && leftAnchor.rect.mUpdateFrame != frame)
				leftAnchor.rect.Update();
		}

		if (bottomAnchor.target)
		{
			anchored = true;
			if (bottomAnchor.rect != null && bottomAnchor.rect.mUpdateFrame != frame)
				bottomAnchor.rect.Update();
		}

		if (rightAnchor.target)
		{
			anchored = true;
			if (rightAnchor.rect != null && rightAnchor.rect.mUpdateFrame != frame)
				rightAnchor.rect.Update();
		}

		if (topAnchor.target)
		{
			anchored = true;
			if (topAnchor.rect != null && topAnchor.rect.mUpdateFrame != frame)
				topAnchor.rect.Update();
		}

		// Update the dimensions using anchors
		if (anchored) OnAnchor();
	}


記錄mUpdateFrame是為了防止重複計算,在setAnchor或OnEnabled之後,同一幀的Update就不會再呼叫UpdateAnchorsInternal了。然後呼叫每個anchor的rect(在ResetAnchors中取到的)的Update方法。最後呼叫OnAnchor方法,OnAnchor是個abstract方法,具體實現在UIPanel和UIWidget(還有UIWidget的衍生類UILabel)裡面,根據參考系和相對位置,計算位置和尺寸。UIRect大概就講到這裡了,總結一下:UIRect是為UI元件描述其位置和尺寸資訊,通過AnchorPoint來實現,根據目標參考系和相對位置來計算矩形區域的每一個邊的位置。(⊙o⊙)…我好像不是很善於總結...最後可以加一個tips:OnEnable的時候會呼叫OnInit,UIRect的OnInit到沒有什麼可說的。UIPanel裡的OnInit在一堆初始化之中,有一句mRebuild = true,這會導致這個UIPanel裡的所有UIDrawCall重建(具體我們會在UIPanel裡講到)。UIWidget的OnInit有一句RemoveFromPanel(),並會呼叫OnUpdate(通過Update()),又會重新建立(尋找)panel(會導致這個Widget所屬的UIDrawCall會被重建,甚至所有UIDrawCall重建),並會呼叫Invalidate更新Visibility和FinalAlpha,並且這個方法是遞迴的,意思就是所有的子Widget都會更新一遍。所以GameObject.setActive還是慎重使用。