深入探讨并实现Unity图像轮播

深入探讨并实现Unity图像轮播

最近接了一个展厅的小项目,需求中有一个图像轮播的小功能,本来以为这是一碟小菜,心想瞬间有了N中解决方案,然而,现实是pia pia打脸,幸好最终完美实现了效果,才保住了作为公司中几个妹子心目中“大神”的老脸。
先看看最终效果:

注意哦,它两边是有一个渐隐效果的。
说一说制作历程和思路吧:

思路一

一看到我们美监妹子的效果图,第一个想到的是自带的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图像轮播_第1张图片
当然了,如果不想要中间大,两边小的效果,可以吧贝塞尔曲线的Z值全设置为0,这样曲线就退化为一条直线了,效果如下:

甚至是这样子的效果哦:
深入探讨并实现Unity图像轮播_第2张图片


最后,当然这个组件还可以继续再完善,继续再研发其他的效果,比如自动缓缓播放等等,这些都不难,这里就不再继续探讨了。


最后附上下载链接

点此下载源码

你可能感兴趣的:(Unity,Unity,轮播,carousel,透明掩码,图像轮播)