NGUI所见即所得之UIAnchor,UIStretch

本文详细解析NGUI中的UIAnchor与UIStretch组件的工作原理及应用技巧,包括它们如何处理UI元素的位置与尺寸调整,以实现跨平台UI自适应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

NGUI的Example/Scenes/Example1介绍的主要就是UIRoot,UIAnchor和UIStretch这三个脚本,当然UIRoot是每个UI都会有的(挂在根节点),之前D.S.Qiu也写过博客介绍过UIRoot( 猛点查看)。一直都对NGUI把Unity的单位和像素的转换和统一都很有疑惑,因之手游项目要做UI的自适应,就觉得很有必要把UIAnchor和UIStretch深入的研究下。

        UIRoot在D.S.Qiu的第一篇NGUI的文章中介绍了(猛点查看),UIRoot其实就做了一件事情:根据Screen.height和UIRoot.activeHeight的比例来调整UIRoot的loaclScal,从而保证UIWidget(UISprite,UILabel)可以按照其本身的大小进行设置,而不用经过复杂的换算过程。

         UIAnchor和UIStretch处理上的细节很相似,都是先计算参照对象(这个参照对象由Insprector的Container指定,如果没有选择,就是Camera)的大小Rect,然后根据参数UIAnchor(Side,relativeOffset,pixelOffset),UIStretch(Style,relativeSize,initialSize,borderPadding)进行调整,最后设置对应的属性,只不过UIAnchor设置的是transform.position,UIStretch设置的是(width,height)或clipRange等。

UIAnchor

        先看下UIAnchor的Inspector界面,感觉很简单,不会像UIPanel那么复杂:

        Container:指定Anchor的参照点,如果没有选择,则以Camera的pixelRect的区域为参照面

        Side:Anchor的定位方式

        Relative Offset:相对于Camera,或Container的百分比偏移

        Pixel Offset:像素的偏移

C#代码 复制代码 收藏代码
  1.        /// <summary> 
  2. /// Relative offset value, if any. For example "0.25" with 'side' set to Left, means 25% from the left side. 
  3. /// </summary> 
  4. public Vector2 relativeOffset = Vector2.zero; 
  5.  
  6. /// <summary> 
  7. /// Pixel offset value if any. For example "10" in x will move the widget 10 pixels to the right  
  8. /// while "-10" in x is 10 pixels to the left based on the pixel values set in UIRoot. 
  9. /// </summary> 
  10. public Vector2 pixelOffset = Vector2.zero; 

Update函数

       UIAnchor的主要功能实现都在Update函数:

C#代码 复制代码 收藏代码
  1. void Update () 
  2.     { 
  3.         if (mAnim != null && mAnim.enabled && mAnim.isPlaying) return
  4.         bool useCamera = false
  5.         UIWidget wc = (container == null) ? null : container.GetComponent<UIWidget>(); 
  6.         UIPanel pc = (container == null && wc == null) ? null : container.GetComponent<UIPanel>(); 
  7.         if (wc != null
  8.         { 
  9.             Bounds b = wc.CalculateBounds(container.transform.parent); 
  10.  
  11.             mRect.x = b.min.x; 
  12.             mRect.y = b.min.y; 
  13.  
  14.             mRect.width = b.size.x; 
  15.             mRect.height = b.size.y; 
  16.         } 
  17.         else if (pc != null
  18.         { 
  19.             if (pc.clipping == UIDrawCall.Clipping.None) 
  20.             { 
  21.                 // Panel has no clipping -- just use the screen's dimensions 
  22.                 float ratio = (mRoot != null) ? (float)mRoot.activeHeight / Screen.height * 0.5f : 0.5f; 
  23.                 mRect.xMin = -Screen.width * ratio; 
  24.                 mRect.yMin = -Screen.height * ratio; 
  25.                 mRect.xMax = -mRect.xMin; 
  26.                 mRect.yMax = -mRect.yMin; 
  27.             } 
  28.             else 
  29.             { 
  30.                 // Panel has clipping -- use it as the mRect 
  31.                 Vector4 pos = pc.clipRange; 
  32.                 mRect.x = pos.x - (pos.z * 0.5f); 
  33.                 mRect.y = pos.y - (pos.w * 0.5f); 
  34.                 mRect.width = pos.z; 
  35.                 mRect.height = pos.w; 
  36.             } 
  37.         } 
  38.         else if (container != null
  39.         { 
  40.             Transform root = container.transform.parent; 
  41.             Bounds b = (root != null) ? NGUIMath.CalculateRelativeWidgetBounds(root, container.transform) : 
  42.                 NGUIMath.CalculateRelativeWidgetBounds(container.transform); 
  43.  
  44.             mRect.x = b.min.x; 
  45.             mRect.y = b.min.y; 
  46.  
  47.             mRect.width = b.size.x; 
  48.             mRect.height = b.size.y; 
  49.         } 
  50.         else if (uiCamera != null
  51.         { 
  52.             useCamera = true
  53.             mRect = uiCamera.pixelRect; 
  54.         } 
  55.         else return
  56.  
  57.         float cx = (mRect.xMin + mRect.xMax) * 0.5f; 
  58.         float cy = (mRect.yMin + mRect.yMax) * 0.5f; 
  59.         Vector3 v = new Vector3(cx, cy, 0f); 
  60.  
  61.         if (side != Side.Center) 
  62.         { 
  63.             if (side == Side.Right || side == Side.TopRight || side == Side.BottomRight) v.x = mRect.xMax; 
  64.             else if (side == Side.Top || side == Side.Center || side == Side.Bottom) v.x = cx; 
  65.             else v.x = mRect.xMin; 
  66.  
  67.             if (side == Side.Top || side == Side.TopRight || side == Side.TopLeft) v.y = mRect.yMax; 
  68.             else if (side == Side.Left || side == Side.Center || side == Side.Right) v.y = cy; 
  69.             else v.y = mRect.yMin; 
  70.         } 
  71.  
  72.         float width = mRect.width; 
  73.         float height = mRect.height; 
  74.  
  75.         v.x += pixelOffset.x + relativeOffset.x * width; 
  76.         v.y += pixelOffset.y + relativeOffset.y * height; 
  77.  
  78.         if (useCamera) 
  79.         { 
  80.             if (uiCamera.orthographic) 
  81.             { 
  82.                 v.x = Mathf.Round(v.x); 
  83.                 v.y = Mathf.Round(v.y); 
  84.  
  85.                 if (halfPixelOffset && mNeedsHalfPixelOffset) 
  86.                 { 
  87.                     v.x -= 0.5f; 
  88.                     v.y += 0.5f; 
  89.                 } 
  90.             } 
  91.  
  92.             v.z = uiCamera.WorldToScreenPoint(mTrans.position).z; 
  93.             v = uiCamera.ScreenToWorldPoint(v); 
  94.         } 
  95.         else 
  96.         { 
  97.             v.x = Mathf.Round(v.x); 
  98.             v.y = Mathf.Round(v.y); 
  99.  
  100.             if (pc != null
  101.             { 
  102.                 v = pc.cachedTransform.TransformPoint(v); 
  103.             } 
  104.             else if (container != null
  105.             { 
  106.                 Transform t = container.transform.parent; 
  107.                 if (t != null) v = t.TransformPoint(v); 
  108.             } 
  109.             v.z = mTrans.position.z; 
  110.         } 
  111.  
  112.         // Wrapped in an 'if' so the scene doesn't get marked as 'edited' every frame 
  113.         if (mTrans.position != v) mTrans.position = v; 
  114.         if (runOnlyOnce && Application.isPlaying) Destroy(this); 
  115.     } 

       Update的原理很简单,梳理归纳为4部分:

                 1)获取Anchor参照对象的大小Rect以及计算中心点Vector3 v;

                 2)根据Side类型调整v的x,y,z值;

                 3)将v转换成世界坐标

                 4)将v赋给mTrans.position。

        这里对1)再多说几句,主要是涉及参照对象的选取问题,用if - else if来筛选的次序是 UIWidget wc , UIPanel pc , GameObject container, Camera uiCamera,如果前者部位null这取前者的大小后面的就不予考虑。

C#代码 复制代码 收藏代码
  1. UIWidget wc = (container == null) ? null : container.GetComponent<UIWidget>(); 
  2. container == null && wc == null) ? null : container.GetComponent<UIPanel>(); 

像素与Unity单位

        之前项目中使用的NGUI版本还是2.6.3,那个版本还没有pixelOffset,然后为了实现一个相对便宜就在挂载Anchor的对象下面挂载一个子对象,通过设置子对象的loaclPosition来设置相对偏移:

      这样用transform.find去查找某一个子对象的时候就会觉得很蛋疼,所以当看到pixelOffset的就觉得没有必要用offset这层节点了,这可以说是NGUI埋下隐形的坑(很多没有“爱参考”不思考的开发者,就喜欢照搬别人的东西),之前的项目就是这样的,看了一堆Offset,完全没有必要。然后果断测试就会有以下不同的情况。

      测试之前首先将上面Bottom/Offset的localPosition置0,并修改稿Bottom的UIAnchor的pixelOffset改为(0,40)

1)当参照对象是Camera时,即Container=null:

但当编辑器的分辨率等于某个值时,又回恢复正常情况:


2)把Bottom的父对象UIPanel拖给UIAnchor的Container:

这种情况是没有问题的:


       回过头看下Update函数中对pixelOffset的使用:

C#代码 复制代码 收藏代码
  1. v.x += pixelOffset.x + relativeOffset.x * width; 
  2. v.y += pixelOffset.y + relativeOffset.y * height; 

        经过反复的思考,觉得一定是pixelOffset和子对象Offset的localPosition数值的参考系是不一样的,但最终都是通过mRect来处理的,所以把UIAnchor Rect mRect设置成public,查看mRect的值,上面三个情况对应mRect值分别如下:



      这说明,当mRect.y大于等于800的时候,使用UIAnchor的pixelOffset和使用子对象Offset的localPosition的表现是一致的。但为什么指定Container为UIPanel都不会出现异常情况,只有Camera会出现。再回到Update看下获取mRect的方法,指定Container时,mRect实际是UIPanel,或UIWideget的像素大小,其实是UIWidget的(width,height),而没有指定Container情况下,mRect = camera.pixelRect。

      在UIRoot文中,就说过camera.pixelRect其实就是Screen的(width,height),也就是说,camera.pixelRect是会根据显示器的分辨率动态改变的,而指定Container时,mRect是不会改变的(在介绍UIRoot文中有介绍)。

       在看下pixelOffset的使用(真的是最后一次了):

C#代码 复制代码 收藏代码
  1. v.x += pixelOffset.x + relativeOffset.x * width; 
  2. v.y += pixelOffset.y + relativeOffset.y * height; 

       虽然pixelOffset的值一直没有变化,但是当mRect = camera.pixelCamera时,mRect是随着分辨率的变化而变化的,那样的话pixelOffset占的权重就会改变了,所以才会出现上面的偏移异常。

      

解决策略

       在UIAnchor中设置一个参数unitOffset来代替子对象Offset的功能:

C#代码 复制代码 收藏代码
  1. public Vector2 unitOffset = Vector2.zero; 

       然后把设置的值在Update函数的最后加个 mTrans.localPosition += new Vector3(unitOffset.x,unitOffset.y,0);

C#代码 复制代码 收藏代码
  1. void Update() 
  2.        {     
  3.                //省略前面的处理。。。 
  4.                // Wrapped in an 'if' so the scene doesn't get marked as 'edited' every frame 
  5.     if (mTrans.position != v) mTrans.position = v; 
  6.        mTrans.localPosition += new Vector3(unitOffset.x,unitOffset.y,0); 
  7.     if (runOnlyOnce && Application.isPlaying) Destroy(this); 
  8.        } 

        这样就可以轻松取得GameObject树中Offset一层,之前项目中就有这个一层,我看着就来火,终于干掉了哈……

        还有一个问题:为什么当camera.pixelRect.y等于800时,就会恢复正常,这个先看下UIRoot的设置(对UIRoot原理不了解请猛击):

        Manual Height设置为800,Scaling Style设置为FixedSize,可以知道UI的放缩参考高度就是 800,即实际UI布局高度就是800,这里有点难理解,总之就是当屏幕分辨率高度等于800时,pixelOffset和子对象Offset的localPostion参考点就一致了,实际效果就一样了。也可以这么解释:当mRect = camera.pixelRect 时,pixeloffset的值是相对于camera.pixelRect而言的,在屏幕的呈现是会对着屏蔽的分辨率不同而改变的;而使用子对象Offset的localPosition的参照和UI组件是一致的,所以相对于Contaner的位置是不会改变的。

        回到文中开头抛出的一个问题——Unity中transfrom的单位和像素的关系,上张图片,可以知道UI的高度实际高度是800,然后看下Anchor - Top 和Anchor - Bottom的transform的localPostion.y分别是400.5006和-399.4994(图片如下),发现两者区间刚好是800,这就说明NGUI的架构中像素坐标和Unity的单位是一致的,对等的,即一个Unity单位等于一个像素。

UIStretch

       看下NGUI Example1 的Anchor - Stretch的Inspector面板,发现UIStretch只比UIAnchor多了一个参数,不过从这张图UISprite的Dimension(红框标出)的长度始终都是800,不管屏幕如何改变大小,都是800个像素,填充的Tiled图片个数也是一样的,这也更加说明了上面提到的一个结论:NGUI中Unity的单位和像素是同一的。

Update

       UIStretch的Update函数的前面部分跟UIAnchor的Update的前面部分原理是一样的都是获取mRect,所以只看后一部分的实现(理解 relativeSize 和 initialSize 的含义和作用):

C#代码 复制代码 收藏代码
  1. void Update() 
  2.                         //.......省略上面的处理 
  3.             float rectWidth = mRect.width; 
  4.             float rectHeight = mRect.height; 
  5.  
  6.             if (adjustment != 1f && rectHeight > 1f) 
  7.             { 
  8.                 float scale = mRoot.activeHeight / rectHeight; 
  9.                 rectWidth *= scale; 
  10.                 rectHeight *= scale; 
  11.             } 
  12.  
  13.             Vector3 size = (mWidget != null) ? new Vector3(mWidget.width, mWidget.height) : mTrans.localScale; 
  14.  
  15.             if (style == Style.BasedOnHeight) 
  16.             { 
  17.                 size.x = relativeSize.x * rectHeight; 
  18.                 size.y = relativeSize.y * rectHeight; 
  19.             } 
  20.             else if (style == Style.FillKeepingRatio) 
  21.             { 
  22.                 // Contributed by Dylan Ryan 
  23.                 float screenRatio = rectWidth / rectHeight; 
  24.                 float imageRatio = initialSize.x / initialSize.y; 
  25.  
  26.                 if (imageRatio < screenRatio) 
  27.                 { 
  28.                     // Fit horizontally 
  29.                     float scale = rectWidth / initialSize.x; 
  30.                     size.x = rectWidth; 
  31.                     size.y = initialSize.y * scale; 
  32.                 } 
  33.                 else 
  34.                 { 
  35.                     // Fit vertically 
  36.                     float scale = rectHeight / initialSize.y; 
  37.                     size.x = initialSize.x * scale; 
  38.                     size.y = rectHeight; 
  39.                 } 
  40.             } 
  41.             else if (style == Style.FitInternalKeepingRatio) 
  42.             { 
  43.                 // Contributed by Dylan Ryan 
  44.                 float screenRatio = rectWidth / rectHeight; 
  45.                 float imageRatio = initialSize.x / initialSize.y; 
  46.  
  47.                 if (imageRatio > screenRatio) 
  48.                 { 
  49.                     // Fit horizontally 
  50.                     float scale = rectWidth / initialSize.x; 
  51.                     size.x = rectWidth; 
  52.                     size.y = initialSize.y * scale; 
  53.                 } 
  54.                 else 
  55.                 { 
  56.                     // Fit vertically 
  57.                     float scale = rectHeight / initialSize.y; 
  58.                     size.x = initialSize.x * scale; 
  59.                     size.y = rectHeight; 
  60.                 } 
  61.             } 
  62.             else 
  63.             { 
  64.                 if (style != Style.Vertical) 
  65.                     size.x = relativeSize.x * rectWidth; 
  66.  
  67.                 if (style != Style.Horizontal) 
  68.                     size.y = relativeSize.y * rectHeight; 
  69.             } 
  70.  
  71.             if (mSprite != null
  72.             { 
  73.                 float multiplier = (mSprite.atlas != null) ? mSprite.atlas.pixelSize : 1f; 
  74.                 size.x -= borderPadding.x * multiplier; 
  75.                 size.y -= borderPadding.y * multiplier; 
  76.  
  77.                 if (style != Style.Vertical) 
  78.                     mSprite.width = Mathf.RoundToInt(size.x); 
  79.  
  80.                 if (style != Style.Horizontal) 
  81.                     mSprite.height = Mathf.RoundToInt(size.y); 
  82.  
  83.                 size = Vector3.one; 
  84.             } 
  85.             else if (mWidget != null
  86.             { 
  87.                 if (style != Style.Vertical) 
  88.                     mWidget.width = Mathf.RoundToInt(size.x - borderPadding.x); 
  89.  
  90.                 if (style != Style.Horizontal) 
  91.                     mWidget.height = Mathf.RoundToInt(size.y - borderPadding.y); 
  92.  
  93.                 size = Vector3.one; 
  94.             } 
  95.             else if (mPanel != null
  96.             { 
  97.                 Vector4 cr = mPanel.clipRange; 
  98.  
  99.                 if (style != Style.Vertical) 
  100.                     cr.z = size.x - borderPadding.x; 
  101.                  
  102.                 if (style != Style.Horizontal) 
  103.                     cr.w = size.y - borderPadding.y; 
  104.                  
  105.                 mPanel.clipRange = cr; 
  106.                 size = Vector3.one; 
  107.             } 
  108.             else 
  109.             { 
  110.                 if (style != Style.Vertical) 
  111.                     size.x -= borderPadding.x; 
  112.                  
  113.                 if (style != Style.Horizontal) 
  114.                     size.y -= borderPadding.y; 
  115.             } 
  116.              
  117.             if (mTrans.localScale != size) 
  118.                 mTrans.localScale = size; 
  119.  
  120.             if (runOnlyOnce && Application.isPlaying) Destroy(this); 

整体放缩

     分析了 UIStretch 的 Update ,可以知道当 UIStretch 挂载的GameObject,有UIWidget,UISprite,UIPanel 这个几个脚本是,是不会放缩这个GameObject的子GameObject的,如果要整体放缩的话,就得通过倒数第二行:

C#代码 复制代码 收藏代码
  1. mTrans.localScale = size;   

       如果 Style 选择为 Both ,并且没有指定Container 且GameObject 上没有挂载UIWidget,UISprite,UIPanel ,抽出主要的执行代码:

C#代码 复制代码 收藏代码
  1. void Update() 
  2.                         //省略前面的代码 
  3.                        else if (uiCamera != null
  4.             { 
  5.                 mRect = uiCamera.pixelRect; 
  6.                 if (mRoot != null) adjustment = mRoot.pixelSizeAdjustment; 
  7.             } 
  8.             else return
  9.  
  10.             float rectWidth = mRect.width; 
  11.             float rectHeight = mRect.height; 
  12.  
  13.             if (adjustment != 1f && rectHeight > 1f) 
  14.             { 
  15.                 float scale = mRoot.activeHeight / rectHeight; 
  16.                 rectWidth *= scale; 
  17.                 rectHeight *= scale; 
  18.             } 
  19.  
  20.                         //省略 代码 
  21.                         else 
  22.             { 
  23.                 if (style != Style.Vertical) 
  24.                     size.x = relativeSize.x * rectWidth; 
  25.  
  26.                 if (style != Style.Horizontal) 
  27.                     size.y = relativeSize.y * rectHeight; 
  28.             } 
  29.                         //省略 代码 
  30.                         if (mTrans.localScale != size) 
  31.                 mTrans.localScale = size; 

       整理下: mTrans.localScale.x = relativeSize.x * rectWidth * mRoot.activeHeight / rectHeight

mTrans.localScale.y = relativeSize.y * rectHeight * mRoot.activeHeight / rectHeight = relativeSize.y * mRoot .activeHeight

       因为 UIRoot 是基于高度调整 localScale 的 来做放缩的,所以宽度的放缩UIRoot 已经做了,所以只需要将 relativeSize.y 设置为 1 / mRoot.activeHeight 使 mTrans.localScale.y = 1恒成立。UIRoot 会是高度始终满屏(UIRoot Style 为 FixedSize ),但宽度的放缩总是按照高度的放缩比例在放的,所以会出现宽度没有全部显示出来,或者左右两边有黑边。

      其实要想做到满屏(高度和宽度)缩放的效果,其实可以在UIRoot中增加一个 manualWidth 来调整 UIRoot 的localSize.x 的值。

      另外一种做法就是使用UIStretch ,我们只需要通过设置 relativeSize.x = 1 / manualWidth ;relativeSize.y = 1 / mRoot.activeHeight 就能满屏缩放了,哈哈,搞定了。等等,这样是满屏了,但是其他图片或文字会被拉伸变形,也就是说 UIStretch 只能做到某个单一的组件按比例缩放。

      总之,实际屏幕显示的宽度 = (Screen.Height / mRoot.acriveHeight * manualHeight > Screen.Width ) ?  Screen.Width :Screen.Height / mRoot.acriveHeight * manualHeight ,就是去两者中的更小的。所以要做宽度放缩,只要针对实际显示宽度 和 屏幕宽度(Screen.Width) 来调整 localScale.x 值就行了。

                                                                                                                                                                                         增补于 2013/11/26 19:45

     

      

                    

小结:

       本来昨天晚上就想写的,但是由于心情不好,也还要一些要点没有相同,所以才拖到现在完成,今天上了半天班(虽然一直都是在NBA的,今天的詹姆斯太牛逼了,看到我心情都好了,18投14中,罚球11中10,39分),有点小不敬业,吃完中饭之后,就开始组织些了,一直写到现在(花了5个小时),截了好些图,这篇应该是D.S.Qiu这么多篇中写得最畅快最爽的一次,解决之前的疑问。

       和UIRoot一样,UIAnchor和UIStretch很简单,但是却很重要,虽然就几个参数,但是要完全明白和理解还是需要花点时间的,所以我才写出来跟大家分享的,NGUI的文章页写了几篇了(点击查看),转载注明出处,尊重原创。

        如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

        转载请在文首注明出处:http://dsqiu.iteye.com/blog/1975972

更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值