1. 程式人生 > >對 UIRect 的一點理解

對 UIRect 的一點理解

mat wan function end height 默認 標記 targe 影響

  UIRect,一個繼承 MonoBehaviour 的抽象類,主要實現了錨點功能。

2.1 UIRect 簡單介紹

  UIRect 內實現了類 AnchorPoint,它保存了具體的位置信息,有三個成員變量需要講一下:

  • target,設置偏移的對照節點,參照物;
  • relative,相對參照物的位置,比如相對參照物的左邊(值為0)或者右邊(值為1),下邊(值為0)或者上邊(值為1),中點(值為0.5),也可以自定義值;
  • absolute,具體的偏移像素值。

  UIRect 實現的錨點其實是用 AnchorPoint 保存了自己上下左右四個邊的位置信息,UIRect 中的代碼不太多,不仔細介紹了,我們就具體看下下面兩段代碼:

  代碼一展示的 cameraRayDistance 屬性,註釋很清楚的說明了它的功能是獲取攝像機距離當前面板的長度值,原理也不難,它在自身的位置上創建了一個與自己平行的 Plane,然後通過從攝像機發射一條射線獲取攝像機到 Plane 的距離來得到攝像機距離自己的長度值。

    /// UIRect.cs
    /// <summary>
    /// Helper function that returns the distance to the camera‘s directional vector hitting the panel‘s plane.
    /// </summary>

    protected float cameraRayDistance
    {
        get
        {
            if (anchorCamera == null) return 0f;

#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7
            if (!mCam.isOrthoGraphic)
#else
            if (!mCam.orthographic)
#endif
            {
                Transform t = cachedTransform;
                Transform ct = mCam.transform;
                Plane p = new Plane(t.rotation * Vector3.back, t.position);
                Ray ray = new Ray(ct.position, ct.rotation * Vector3.forward);
                float dist;
                if (p.Raycast(ray, out dist)) return dist;
            }
            return Mathf.Lerp(mCam.nearClipPlane, mCam.farClipPlane, 0.5f);
        }
    }

代碼一,cameraRayDistance屬性

  創建一個和自己平行的面,我們只需要一條法線和一個點。點比較容易,自己的位置就行了,那麽為什麽 t.rotation * Vector3.back 就是自己的法線呢?其實對於 NGUI 來說 Vector3.back 或者 Vector3.forward 就是默認情況下面板的法線(面板沒有做任何的旋轉操作),如果面板經歷過一系列旋轉操作,只需要讓 Vector3.back 或者 Vector3.forward 經歷同樣的旋轉操作就能獲取到此時自己的法線向量(t.rotation * Vector3.back就是這個意思)。

  代碼二展示了計算上下左右四個邊位置的錨點值的一個相對比較復雜的情況,代碼有點多,原理還是比較簡單的,就是先計算出上下左右四個邊的世界坐標,再轉換成相對參照物 relativeTo 的局部坐標。

    /// NGUITools.cs
    /// <summary>
    /// Get sides relative to the specified camera. The order is left, top, right, bottom.
    /// </summary>

    static public Vector3[] GetSides (this Camera cam, float depth, Transform relativeTo)
    {
#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7
        if (cam.isOrthoGraphic)
#else
        if (cam.orthographic)
#endif
        {
            float os = cam.orthographicSize;
            //攝像機可視區域左邊位置
            float x0 = -os;
            //攝像機可視區域右邊位置
            float x1 = os;
            //攝像機可視區域頂部位置
            float y0 = -os;
            //攝像機可視區域底部位置
            float y1 = os;

            Rect rect = cam.rect;
            Vector2 size = screenSize;

            float aspect = size.x / size.y;
            aspect *= rect.width / rect.height;
            //通過設備真實分辨率和攝像機視口區域大小來矯正攝像機可視區域左邊和右邊的位置
            x0 *= aspect;
            x1 *= aspect;

            // We want to ignore the scale, as scale doesn‘t affect the camera‘s view region in Unity
            Transform t = cam.transform;
            Quaternion rot = t.rotation;
            Vector3 pos = t.position;

            int w = Mathf.RoundToInt(size.x);
            int h = Mathf.RoundToInt(size.y);

            if ((w & 1) == 1) pos.x -= 1f / size.x;
            if ((h & 1) == 1) pos.y += 1f / size.y;

            //計算上下左右四個邊的世界坐標
            mSides[0] = rot * (new Vector3(x0, 0f, depth)) + pos;
            mSides[1] = rot * (new Vector3(0f, y1, depth)) + pos;
            mSides[2] = rot * (new Vector3(x1, 0f, depth)) + pos;
            mSides[3] = rot * (new Vector3(0f, y0, depth)) + pos;
        }
        else
        {
            //暫時不研究這段代碼
            mSides[0] = cam.ViewportToWorldPoint(new Vector3(0f, 0.5f, depth));
            mSides[1] = cam.ViewportToWorldPoint(new Vector3(0.5f, 1f, depth));
            mSides[2] = cam.ViewportToWorldPoint(new Vector3(1f, 0.5f, depth));
            mSides[3] = cam.ViewportToWorldPoint(new Vector3(0.5f, 0f, depth));
        }
        
        //有參照物的話,計算上下左右四個邊相對參照物的的局部坐標
        if (relativeTo != null)
        {
            for (int i = 0; i < 4; ++i)
                mSides[i] = relativeTo.InverseTransformPoint(mSides[i]);
        }
        return mSides;
    }

代碼二,獲取錨點上下左右四個邊的具體位置

  這裏做一些講解:

  • cam.orthographicSize,這在介紹 UIRoot 子節點的位置時有講過,cam.orthographicSize 等於攝像機可視區域的 height / 2;
  • Unity 坐標系,中心點是<0, 0, 0>,頂部是 <0, -cam.orthographicSize, 0>,底部是 <0, -cam.orthographicSize, 0>;
  • 計算攝像機可視區域的左右兩側的具體數值,這在介紹 UIRoot 子節點的位置時也有講過,因為攝像機可視區域是數值和設備分辨率一樣的長方形,既然 height / 2 = cam.orthographicSize 的話,那 width / 2 就是 (screenSize.x / screenSize.y) * cam.orthographicSize,這裏的 cam.rect 長和寬都是1,基本不會修改,這裏就不介紹了(具體可以自己改改看看,也可以看看文檔裏如何介紹攝像機的 Viewport Rect 的);
  • 計算上下左右各邊的世界坐標,這個就是先計算默認情況下(攝像機沒有旋轉操作和位移操作)的位置,然後和攝像機做同樣的旋轉操作和位移操作。

2.2,NGUI 官方利用錨點功能自適應 UILabel 的示例:

  圖一展示了 NGUI 官方示例 Tutorial 7,圖一中的標記1代表父節點 Label - Content,標記2代表子節點 Label - Title,標記3代表子節點 Sprite - Background,標記4顯示 Label - Content 的設置,長和寬都居中設置,寬度固定而高度可變,表示如果 UILabel 中的內容改變導致 UILabel 的高度產生了變化,那麽 UILabel 會上下兩邊均勻放縮,作為背景的 Sprite - Background 會跟著放縮,而作為標題的 Label - Title 也會跟著調整自己的位置,這主要是使用錨點來實現的。

技術分享圖片

圖一,NGUI 官方示例 Tutorial 7

圖二展示了 Label - Title 的組件設置,Widget 欄有對齊方式,高度寬度信息,Anchors 欄有錨點信息,NGUI 的錨點是設置節點上下左右四個邊相對於父節點的位置,譬如 Label - Title 就設置了它的底邊距離 Label - Content 的頂邊 40px,它的頂邊距離 Label - Content 的頂邊 80px(40px + 自己的高度),若是 Label - Content 的內容變化導致了高度變化, 自動向上下兩邊放縮,Label - Content 頂部位置產生變化,Label - Title 也就跟著調整自己的位置了。

技術分享圖片

圖二,Label - Title 組件設置

  圖二展示了 Sprite - Background 的組件設置,可以看出它設置了自己的上下左右四個邊分別距離 Label - Title 上下左右四個邊 40px。

技術分享圖片

圖三,Sprite - Background的組件設置

  其實我一度認為錨點只是一個點而已,按照對齊方式它可以是中心點,左側中點,左上角等等,設置也只是設置它相對父節點的局部位置。當然我也認為 NGUI 的錨點寫的有些繁雜,不過 NGUI 這樣實現自然有它的理由(不然也沒法實現上面說的類似功能了),我們就不談優點了,來談下這樣實現有什麽問題,其實是有一個不可避免的小問題的,那就是錨點設置的是節點四個邊的位置,它們的值會與節點高度值和寬度值互相影響,雖然這是實現自適應必須的,卻也不是所有情況都需要。譬如 Label - Title,如果我設置它的底部距離 Label - Content 的頂部 40px,它的頂部距離 Label - Content 的頂部 70px,這樣 Label - Title 的高度就自動變成 30px;如果我直接修改了 Label - Title 的高度為 50px,對齊方式是居中對齊,那麽它會向上下兩邊各延伸 5px,那麽它的底部距離 Label - Content 的頂部就自動變成了 35px,它的頂部距離 Label - Content 的頂部就自動變成了 85px。

對 UIRect 的一點理解