天天看點

深入探讨并實作Unity圖像輪播深入探讨并實作Unity圖像輪播

深入探讨并實作Unity圖像輪播

最近接了一個展廳的小項目,需求中有一個圖像輪播的小功能,本來以為這是一碟小菜,心想瞬間有了N中解決方案,然而,現實是pia pia打臉,幸好最終完美實作了效果,才保住了作為公司中幾個妹子心目中“大神”的老臉。

先看看最終效果:

深入探讨并實作Unity圖像輪播深入探讨并實作Unity圖像輪播

注意哦,它兩邊是有一個漸隐效果的。

說一說制作曆程和思路吧:

思路一

一看到我們美監妹子的效果圖,第一個想到的是自帶的ScrollView(ScrollRect元件),然而确認了需求後,說是這個輪播是要無限循環的,就是說滾動是無窮無盡的,朝左邊拖動,最左邊的要能自動跑到最右邊,朝右邊拖動,最右邊的要能自動跑到最左邊,如果用ScrollRect做,勢必要有比較複雜的位置計算,果斷PASS掉了。

然後有了第二個思路:

還是用UGUI,所有的圖放到一個具有Mask元件的Image下,然後所有的圖用anchoredPosition求得與中心點(0,0)的距離,然後用這個距離做一個反比系數,用這個系數去控制圖像的尺寸,即:如果距離如果為0,則圖像的Size最大,否則,就越小。

k = 1 − ( a n c h o r e d P o s i t i o n − ( 0 , 0 ) ) . m a g n i t u d e 輪 播 組 件 寬 度 / 2 k = 1 - {{(anchoredPosition-(0,0)).magnitude}\over{輪播元件寬度/2}} k=1−輪播元件寬度/2(anchoredPosition−(0,0)).magnitude​

public void OnDrag(PointerEventData eventData)
{
   Vector2 delta = eventData.delta;
   delta.y = 0; // 排除縱向的拖動
   foreach( CarouselImage image in AllImages )
   {
       image.anchoredPosition += delta;
       float k = 1 - Mathf.Abs(image.anchoredPostion.x) / HalfWidth;
       image.sizeDelta *= k;
   }
}
           

嗯,看上去不錯,然而,這樣做有一點沒有考慮,那就是中間那個圖應該是位于最上層,兩邊的圖應該處于底層,這就需要動态的對他們排序了。很顯然,可以用上面的k值進行排序,K越大SiblingIndex就應該越大,這樣可以保證中間的圖位于頂層。然而,試了幾種修改SiblingIndex的方法,都覺得不夠優雅,而且設定SiblingIndex的時機總是不對。哎,抓耳撓腮之際,突然想到,幹嘛非要局限于一個平面呢。咱們unity可是3D的。

第三個思路

用SpriteRenderer(後來改為了World模式下的Canvas,原因是輪播并不是直接播放原圖,而是進行了特定的排版,比如加上邊框,圖像上添加說明文字等等),圍繞一個點去旋轉,這些圖像設定為廣告闆,一切問題迎刃而解。

public void LoadImages( CarouselImage [] images )
{
	float deltaAngle = 2f * Mathf.PI / images.Length;
	for( int i = 0; i < images.Length; ++i)
	{
		images[i].localPosition = new Vector3(
			-Mathf.Sin(i * deltaAngle) * Radius,
			0,
			Mathf.Cos(i * deltaAngle ) * Radius);
	}
}
           

嗯。這樣的話,所有的圖像都平均分布在這個圓的周圍了,想要輪播,隻需要繞y軸轉動這個頂層的物體就行了。

嗯,看上去不錯了,但是。。因為這個是圓,所有的圖檔圍繞這個圓平均分布,但有一個問題是,當圖檔數量不同時,圖檔的稀疏程度就會不同,但我們美監要求的是,除非圖檔數量少于N,否則無論多少圖檔,可見部分的圖始終是N張。那就意味着,這個圓上分布的圖并不是均勻分布的。而且圓的半徑也很難調整。額。。這個思路又放棄了。

終極思路

怎麼辦,怎麼才能優雅的實作這個呢?受上一個思路的啟發,實際上,圓本身是不需要的,本質上我隻需要一個圓弧,然後在這段弧上,分布N張圖檔(圖檔總量大于N),看不見的圖應當可以disable掉。關鍵是這個弧了。然後,我就想到了貝塞爾曲線。

// Class CarsoulManager
//貝塞爾曲線頂點
[SerializeField]
private Vector3 [] BezierPos = new Vector3 []
{
	new Vector3[ -160, 0, 80 ],
	new Vector3[ 0, 0, 0 ],
	new Vector3[ 160, 0, 80 ]
};
// 支援四階、三階、二階貝塞爾曲線
public Vector3 GetBezierPosition( float lerp )
{
    int count = BezierPos.Length;
    if (count >= 4)
    {
        Vector3 p1 = Vector3.Lerp(BezierPos[0], BezierPos[1], lerp);
        Vector3 p2 = Vector3.Lerp(BezierPos[1], BezierPos[2], lerp);
        Vector3 p3 = Vector3.Lerp(BezierPos[2], BezierPos[3], lerp);
        Vector3 p4 = Vector3.Lerp(p1, p2, lerp);
        Vector3 p5 = Vector3.Lerp(p2, p3, lerp);
        return Vector3.Lerp(p4, p5, lerp);
    }
    else if (count == 3)
    {
        Vector3 p1 = Vector3.Lerp(BezierPos[0], BezierPos[1], lerp);
        Vector3 p2 = Vector3.Lerp(BezierPos[1], BezierPos[2], lerp);
        return Vector3.Lerp(p1, p2, lerp);
    }
    else if (count == 2)
    {
        return Vector3.Lerp(BezierPos[0], BezierPos[1], lerp);
    }
    else if (count == 1)
        return BezierPos[0];
    else
        return Vector3.zero;
}
           

有了這個貝塞爾曲線,那麼加載好的圖像就可以平均配置設定到這條曲線上了。這很容易,使用Lerp就可以了,整個曲線從最左邊到最右邊,看作是從0到1,隻要給每張圖配置設定不同的Lerp值就好了。

// Class CarouselImage:
private float m_lerpVal = 0;
public float LerpValue
{
	get { return m_lerpVal; }
	set
	{
		m_lerpVal = value;
		if ((m_lerpVal < -Carousel.Spacing ) || ( m_lerpVal > 1+Carousel.Spacing))
		{
			if (gameObject.activeSelf)
				gameObject.SetActive(false);
		}
		else
		{
			if (!gameObject.activeSelf)
				gameObject.SetActive(true);
			transform.localPosition = Carousel.GetBezierPosition(m_lerpVal);
		}
	}
}
           

為了讓第0張圖在一開始時就位于中心位置,是以加載是按照如下方式進行的:

// Class CarouselManager:
public void LoadImages(CarouselImage [] items)
{
    // 計算每張圖的間隔
	float spacing = 1f / N;
	
	int len = items.Length;
	for( int i = 0; i < len; ++ i )
	{
		if( i >= m_Pictures.Count )
		{
			CarouselImage pic = Instantiate<CarouselImage >(PicturePrefab, transform);
			pic.WorldCamera = TheCamera;
			m_Pictures.Add(pic);
		}
		m_Pictures[i].Pictures = items[i];
		int k = ((i % 2 == 0) ? 1 : -1) * ((i + 1) / 2);
		m_Pictures[i].LerpValue = 0.5f + k * spacing;
	}

	for (int i = m_Pictures.Count - 1; i >= len; --i)
	{
		DestroyImmediate(m_Pictures[i].gameObject);
		m_Pictures.RemoveAt(i);
	}
}
           

稍微解釋一下,因為輪播元件是要頻繁更換圖檔的,是以可能會多次調用LoadImages方法,于是,為了節約一丢丢的性能,這裡就不删除上次加載的圖,而是重新賦予他們新的圖像,隻有上次加載的圖像數量不足時,才建立新的執行個體,當然,如果上次加載的圖像比這次還多,那最後剩餘的圖像還是要删除的。

那麼如何滑動互動呢?很簡單:

// Class CarouselTouch
public void OnTouchDrag(float delta)
{
	float min = float.MaxValue;
	float max = float.MinValue;
	float spacing = 1f / N;

	// 首先對所有圖像進行新的位置計算,并順便找到最左邊和最右邊的圖像
	foreach( CarouselImage pic in m_Pictures )
	{
		pic.LerpValue += delta;
		if( pic.LerpValue > max )
		{
			max = pic.LerpValue;
		}
		if( pic.LerpValue < min )
		{
			min = pic.LerpValue;
		}
	}

	// 如果是向左滑動的話,把最左邊的圖像挪到右邊去,反之亦然
	foreach( CarouselImage pic in m_Pictures)
	{
		if( delta > 0 )
		{
			if( pic.LerpValue > ( 1f +  spacing ))
			{
				pic.LerpValue = min - spacing;
				min = pic.LerpValue;
			}
		}
		else
		{
			if( pic.LerpValue < - spacing )
			{
				pic.LerpValue = max + spacing;
				max = pic.LerpValue;
			}
		}
	}
}
           

很完美了。隻剩下最後一個問題:美監要求兩邊要漸隐。其實這個實作起來也不是很難,兩種方法:

1、寫Shader,當圖像的lerp值超過一個門檻值時,比如小于0.1或者大于0.9時,就開始把兩邊透明化。

2、不想寫代碼的話,搞個錄影機,對準整個CarouselManager元件,并設定為隻渲染Carousel相關的圖像layer,渲染到一張RenderTexture中,對這個RenderTexture就可以做兩邊透明話的處理了,網上搜一下AlphaMask,一堆類似的插件,本質上也是個Mask,放到RawImage中即可。

最後,再看一下整體效果圖:

深入探讨并實作Unity圖像輪播深入探讨并實作Unity圖像輪播

當然了,如果不想要中間大,兩邊小的效果,可以吧貝塞爾曲線的Z值全設定為0,這樣曲線就退化為一條直線了,效果如下:

深入探讨并實作Unity圖像輪播深入探讨并實作Unity圖像輪播

甚至是這樣子的效果哦:

深入探讨并實作Unity圖像輪播深入探讨并實作Unity圖像輪播
最後,當然這個元件還可以繼續再完善,繼續再研發其他的效果,比如自動緩緩播放等等,這些都不難,這裡就不再繼續探讨了。

最後附上下載下傳連結

點此下載下傳源碼