文章说明
本篇文章基于NGUI (3.12.0)版本源码下的代码分析,如果代码和大家自己的不同,可能是版本不同。如果文章中分析有误希望大神看见指点迷津,大家好,我就是一个勤勤恳恳爱偷懒的码农,希望和大家一起学习研究。
自定义 Scrollivew
我觉得在学习别人源码的过程中,就单纯的阅读它也只能达到基本的理解,真正的掌握还是自己动手做一个小 demo 才能有更深刻的领悟,才可以真正的掌握他,所以边学习边实践是一个很不错的选择,今天我就自定义一个简单的 Scrollivew 小 demo,只模拟实现最常用的几个功能,代码比较粗糙,因为这不是我最终确定的版本,我后面会再出文章写一版我比较满意的版本,逻辑应该会大改。(如果我还记得的话)
功能目标
基本实现的功能是:
1.实现 Scrollivew 的水平 ,垂直滑动;
2.默认实现的效果是 MomentumAndSpring (因为这个比较常用);
3.通过代码实现动态添加 item ;
4.对 item 有做回收处理;
5.初始化的时候有对 Scrollivew 中的 item 做对齐处理;
代码
1.ScrollView 全局控制类
这个类主要负责处理 Scrollivew 相关逻辑,比如 拖动,计算边界等,Scrollivew 的全局控制类:
//Scrollview 类,控制整个scrollview系统
public enum ScrollViewDirection
{
None = 0,
Horizontal = 1,
Vertical = 2,
}
public delegate void ItemFunc(GameObject obj, int index);
public delegate void OpFunCB();
public class ScrollView : MonoBehaviour {
#region scrollview 基本属性
/// <summary>
/// scrollview 显示 panel
/// </summary>
public UIPanel _panel;
/// <summary>
/// item 管理控制器
/// </summary>
private ScrollViewItemManger _itemManger;
/// <summary>
/// 当前选中的item下标,默认是-1,表示没有选中,主要处理跳转问题
/// </summary>
private int _selectIndex = 0;
private bool _isPress = false;
private bool _isDrag = false;
/// <summary>
/// 如果全部显示就默认不移动
/// </summary>
private bool _fixIsDontMove = true;
public bool fixDontMove { set { _fixIsDontMove = value; } }
private bool _canMove = true;
private float _deltaTime = 0;
/// <summary>
/// 创建的一个用于辅助计算鼠标移动的平面
/// </summary>
private Plane _pointPlane;
/// <summary>
/// 保存上一个点的坐标
/// </summary>
private Vector3 _lastPos;
/// <summary>
/// 当前动量的大小
/// </summary>
private Vector3 _momentnum = Vector3.zero;
private int _dragID = -10;
private Bounds _bound;
private bool _caculateBounds = true;
public Bounds bounds
{
get
{
if (_caculateBounds)
{
_caculateBounds = false;
_bound = NGUIMath.CalculateRelativeWidgetBounds(_itemManger.parent, _itemManger.parent);
}
return _bound;
}
}
#endregion
#region 注册的回调方法
private OpFunCB _dragStartCB;
public OpFunCB RegisterDragStartCB { set{ _dragStartCB = value; } }
private OpFunCB _dragEndCB;
public OpFunCB RegisterDragEndCB { set{ _dragEndCB = value; } }
public ItemFunc RegisterInitItemFunc { set { _itemManger.SetInitItemFunc = value; } }
#endregion
private void Awake()
{
}
private void OnEnable()
{
_isPress = false;
_canMove = true;
_deltaTime = 0f;
}
// Use this for initialization
void Start () {
}
private void OnDisable()
{
_isPress = false;
_canMove = false;
_deltaTime = 0f;
}
void LateUpdate()
{
_deltaTime = RealTime.deltaTime;
if (!_canMove) return;
if (_isDrag)
{
//TODO
}
}
private void OnDestroy()
{
}
#region 外部接口
/// <summary>
/// 外部接口,动态初始化scrollview状态
/// </summary>
/// <param name="parent"></param>
/// <param name="direction"></param>
public void InitScrollView(GameObject parent, GameObject item, ScrollViewDirection direction = ScrollViewDirection.Vertical)
{
if (null == parent || null == item)
{
Debug.LogError(" scrollview error : not find parent or not find item");
return;
}
if (_panel == null)
{
_panel = parent.GetComponent<UIPanel>();
if (_panel == null)
{
_panel = parent.GetComponentInParent<UIPanel>();
if (_panel == null)
{
Debug.LogError(" scrollview error : parent not find UIPanel");
return;
}
}
}
if (_itemManger == null)
{
_itemManger = CommonTool.AddOrGetComponent<ScrollViewItemManger>(parent);
}
_itemManger.InitItemInfo(_panel, parent, item, direction);
_caculateBounds = true;
}
/// <summary>
/// 刷新scrollview
/// </summary>
/// <param name="num"> 刷新 scrollview 个数</param>
/// <param name="isRest"> 是否需要重置到起始点</param>
public void RefreshScrollveiw(int num, int select = 1, bool isRest = false)
{
if (num <= 0) {
return;
}
_selectIndex = select > num ? 1 : select;
_itemManger.RefreshItem(num);
SetMoveState();
_caculateBounds = true;
RestrictWithPanel();
}
public void PressScroll(bool isPress)
{
if (!enabled || isPress == _isPress || UICamera.currentScheme == UICamera.ControlScheme.Controller) return;
if (_itemManger == null || !NGUITools.GetActive(this))
{
return;
}
_isPress = isPress;
if (!_isPress)
{
if (_isDrag && _panel.clipping == UIDrawCall.Clipping.SoftClip)
{
RestrictWithPanel();
}
_isDrag = false;
if (_dragEndCB != null) { _dragEndCB(); }
if (_dragID == UICamera.currentTouchID) _dragID = -10;
}
else
{
DisableSpring();
_lastPos = UICamera.lastWorldPosition;
//创建一个检测平面
_pointPlane = new Plane(_itemManger.parent.rotation * Vector3.back, _lastPos);
//将初始位置的 panel 都归整数
Vector2 co = _panel.clipOffset;
co.x = Mathf.Round(co.x);
co.y = Mathf.Round(co.y);
_panel.clipOffset = co;
Vector3 pos = _itemManger.parent.localPosition;
pos.x = Mathf.Round(pos.x);
pos.y = Mathf.Round(pos.y);
_itemManger.parent.localPosition = pos;
if (_dragStartCB != null)
{
_dragStartCB();
}
}
}
public void DragScroll()
{
if (!enabled || UICamera.currentScheme == UICamera.ControlScheme.Controller) return;
if (_itemManger == null || !NGUITools.GetActive(this))
{
return;
}
_isDrag = true;
if (_canMove)
{
if (_dragID == -10) _dragID = UICamera.currentTouchID;
UICamera.currentTouch.clickNotification = UICamera.ClickNotification.BasedOnDelta;
Ray ray = UICamera.currentCamera.ScreenPointToRay(UICamera.currentTouch.pos);
float dist = 0f;
if (_pointPlane.Raycast(ray, out dist))
{
//获取当前这个点与平面碰撞后,在空间中的坐标
Vector3 currpos = ray.GetPoint(dist);
//计算两个的偏移
Vector3 offset = currpos - _lastPos;
_lastPos = currpos;
if (offset.x != 0f || offset.y != 0f || offset.z != 0f)
{
offset = _itemManger.parent.InverseTransformDirection(offset);
ScrollViewDirection direction = _itemManger.direction;
if (direction == ScrollViewDirection.Horizontal)
{
offset.y = 0f;
offset.z = 0f;
}
else
{
offset.x = 0f;
offset.z = 0f;
}
//将坐标再转回世界坐标的方向
offset = _itemManger.parent.TransformDirection(offset);
}
MovePanelPosition(offset);
}
}
}
/// <summary>
/// 添加一个外部的初始化item的回调函数
/// </summary>
/// <param name="func"></param>
public void AddInitItemFunc(ItemFunc func)
{
if (CheckCanSetFunc(func))
{
_itemManger.SetInitItemFunc = func;
}
}
#endregion
#region 内部逻辑
private bool CheckCanSetFunc(ItemFunc func)
{
return null != func && null != _panel && null != _itemManger;
}
/// <summary>
/// 设置移动状态
/// </summary>
private void SetMoveState()
{
if (_fixIsDontMove)
{
_canMove = _itemManger.GetRealNum() > _itemManger.GetShowMaxNum();
}
else
{
_canMove = true;
}
}
/// <summary>
/// 移动 panel view
/// </summary>
/// <param name="delta"></param>
private void MovePanelPosition(Vector3 delta)
{
//将世界坐标中的点,从世界坐标转化成局部坐标
Vector3 a = _itemManger.parent.InverseTransformPoint(delta);
Vector3 b = _itemManger.parent.InverseTransformPoint(Vector3.zero);
Vector3 relative = a - b;
_itemManger.parent.localPosition += relative;
Vector2 co = _panel.clipOffset;
co.x -= relative.x;
co.y -= relative.y;
_panel.clipOffset = co;
}
private void RestrictWithPanel()
{
Vector3 offset = CaculatePanelOffset(bounds.min, bounds.max);
if (offset.sqrMagnitude > 0.1f)
{
Vector3 pos = _itemManger.parent.localPosition + offset;
pos.x = Mathf.Round(pos.x);
pos.y = Mathf.Round(pos.y);
//第三个参数是移动的 步长,也可以理解为速度,不可以设置为负数
SpringPanel.Begin(_itemManger.parent.gameObject, pos, 8f);
}
}
private Vector3 CaculatePanelOffset(Vector2 min, Vector2 max)
{
Vector4 cr = _panel.finalClipRegion;
float half_width = cr.z * 0.5f;
float half_height = cr.w * 0.5f;
Vector2 minRect = new Vector2(min.x, min.y);
Vector2 maxRect = new Vector2(max.x, max.y);
Vector2 minArea = new Vector2(cr.x - half_width, cr.y - half_height);
Vector2 maxArea = new Vector2(cr.x + half_width, cr.y + half_height);
if (_panel.softBorderPadding && _panel.clipping == UIDrawCall.Clipping.SoftClip)
{
minArea.x += _panel.clipSoftness.x;
minArea.y += _panel.clipSoftness.y;
maxArea.x -= _panel.clipSoftness.x;
maxArea.y -= _panel.clipSoftness.y;
}
Vector3 offset = Vector3.zero;
if (minArea.x < minRect.x) offset.x -= minRect.x - minArea.x;
if (maxRect.y < maxArea.y) offset.y += maxArea.y - maxRect.y;
if (maxRect.x < minArea.x) offset.x += maxArea.x - maxRect.x;
if (minRect.y > minArea.y) offset.y -= minRect.y - minArea.y;
return offset;
}
private void DisableSpring()
{
SpringPanel sp = GetComponent<SpringPanel>();
if (sp != null) sp.enabled = false;
}
#endregion
}
2.item 管理类
这个类主要负责 item 的动态生成和销毁,关于 item 的相关信息都会放在这个类中去管控:
//scrollview item 管理类,用于模拟 uiwrapcontent
public class ScrollViewItemManger : MonoBehaviour {
#region 其他属性类
private class ScollViewAttrBase
{
public virtual void Dispose() { }
}
/// <summary>
/// scrollview panel 的基本信息
/// </summary>
private class ScollViewPanel : ScollViewAttrBase
{
private UIPanel _panel;
public UIPanel panel { get { return _panel; } }
private Vector2 _size;
public Vector2 size { get { return _size; } }
private Vector2 _softness;
public Vector2 softness { get { return _softness; } }
/// <summary>
/// 这个view 中可以容纳显示的最大 scrollivew item 个数, x表示列,y表示几行
/// </summary>
private Vector2 _showItemMax;
public Vector2 showItemMaxRange
{
get { return _showItemMax; }
set { _showItemMax = value; }
}
public int showItemMaxNum { get { return Mathf.CeilToInt(_showItemMax.x * _showItemMax.y); } }
public ScollViewPanel(UIPanel panel)
{
_panel = panel;
_size = _panel.GetViewSize();
_softness = _panel.clipSoftness;
_showItemMax = Vector2.one;
}
}
/// <summary>
/// scorllview item 的基本信息
/// </summary>
private class ScollViewItem : ScollViewAttrBase
{
private Vector2 _size;
public Vector2 size { get { return _size; } }
private GameObject _itemObj;
public GameObject itemObj { get { return _itemObj; } }
public ScollViewItem(GameObject obj)
{
_itemObj = obj;
Vector2 size = new Vector2(100, 100);
BoxCollider collider = obj.GetComponent<BoxCollider>();
if (null == collider)
{
UIWidget widget = obj.GetComponent<UIWidget>();
if (null != widget)
{
size.x = widget.width;
size.y = widget.height;
}
}
else
{
size.x = collider.size.x;
size.y = collider.size.y;
}
_size = size;
}
public GameObject CreateItem()
{
if (null != _itemObj)
{
return GameObject.Instantiate(_itemObj);
}
return null;
}
}
private class ScollViewItemRoot : ScollViewAttrBase
{
/// <summary>
/// 表示存放正在使用中item的root节点
/// </summary>
private GameObject _root;
public GameObject root { get { return _root; } }
public Transform rootTrans { get { return _root.transform; } }
/// <summary>
/// 表示存放未使用的item的root节点
/// </summary>
private GameObject _unUseRoot;
public GameObject unUseRoot { get { return _unUseRoot; } }
public Transform unUseRootTrans { get { return _unUseRoot.transform; } }
private UIGrid _grid;
public UIGrid grid { get { return _grid; } }
public ScollViewItemRoot(GameObject parent)
{
_root = new GameObject();
_root.name = "useRoot";
CommonTool.SetActive(_root, true);
CommonTool.SetParent(parent, _root);
_grid = _root.AddOrGetComponent<UIGrid>();
_grid.enabled = false;
_unUseRoot = new GameObject();
_unUseRoot.name = "unUseRoot";
CommonTool.SetParent(parent, _unUseRoot);
CommonTool.SetActive(_unUseRoot, false);
}
}
#endregion
#region 基本属性信息
/// <summary>
/// 整个scrollview的父节点
/// </summary>
private GameObject _parent;
public Transform parent { get { return _parent.transform; } }
/// <summary>
/// itemroot 信息,所有正在使用中的item的父节点节点
/// </summary>
private ScollViewItemRoot _itemRoot;
/// <summary>
/// 用于创建的item信息
/// </summary>
private ScollViewItem _itemModel;
/// <summary>
/// panel 信息
/// </summary>
private ScollViewPanel _scrollPanel;
/// <summary>
/// scrollview 方向
/// </summary>
private ScrollViewDirection _direction;
public ScrollViewDirection direction { get { return _direction; } }
/// <summary>
/// 真实需要创建的 item 个数
/// </summary>
private int _realNum = 0;
#endregion
#region 注册回调方法
private ItemFunc _InitItemFunc;
public ItemFunc SetInitItemFunc { set { _InitItemFunc = value; } }
#endregion
#region 外部接口
/// <summary>
/// 初始化 itemmanger 的接口,所有数据的入口代码
/// </summary>
/// <param name="panel"></param>
/// <param name="parent"></param>
/// <param name="item"></param>
/// <param name="direction"></param>
public void InitItemInfo(UIPanel panel, GameObject parent, GameObject item, ScrollViewDirection direction)
{
_parent = parent;
_direction = direction;
_scrollPanel = new ScollViewPanel(panel);
_itemModel = new ScollViewItem(item);
CommonTool.SetActive(item, false);
_itemRoot = new ScollViewItemRoot(parent);
InitGridInfo();
}
/// <summary>
/// 刷新需要显示的item
/// </summary>
/// <param name="num"></param>
public void RefreshItem(int num)
{
_realNum = num;
Transform trans = _itemRoot.rootTrans;
int oldNum = trans.childCount;
if (oldNum > num)
{
for (int i = oldNum - num; i > 0 ; i--)
{
RecycleItem(trans.GetChild(i));
}
}
else
{
//item不够,需要重新创建
int viewMax = _realNum;
int addNum = viewMax < num? viewMax - oldNum : num - oldNum;
for (int i = 0; i < addNum; i++)
{
GameObject obj = CreateItem();
CommonTool.SetActive(obj, true);
CommonTool.SetParent(_itemRoot.root, obj);
}
}
_itemRoot.grid.Reposition();
//调用item的初始化回调函数
if (_InitItemFunc != null)
{
for (int i = 0, len = trans.childCount; i < len; i++)
{
GameObject childObj = trans.GetChild(i).gameObject;
childObj.name = "item_" + i;
_InitItemFunc(childObj, i);
}
}
}
public int GetRealNum()
{
return _realNum;
}
public int GetShowMaxNum()
{
if (_scrollPanel != null)
{
return _scrollPanel.showItemMaxNum;
}
return 0;
}
#endregion
#region 内部接口
private void InitGridInfo()
{
UIGrid grid = _itemRoot.grid;
Vector2 maxView = Vector2.one;
if (_direction == ScrollViewDirection.Horizontal)
{
//水平滑动的scrollview,需要计算行有几个
grid.arrangement = UIGrid.Arrangement.Vertical;
grid.maxPerLine = Mathf.FloorToInt(_scrollPanel.size.y / _itemModel.size.y);
maxView.y = grid.maxPerLine;
maxView.x = Mathf.CeilToInt(_scrollPanel.size.x / _itemModel.size.x);
}
else
{
//垂直滑动,计算水平防线放几列
grid.arrangement = UIGrid.Arrangement.Horizontal;
grid.maxPerLine = Mathf.FloorToInt(_scrollPanel.size.x / _itemModel.size.x );
maxView.x = grid.maxPerLine; //设置显示界面中最大显示多少列
maxView.y = Mathf.CeilToInt(_scrollPanel.size.y / _itemModel.size.y);
}
grid.hideInactive = true;
grid.pivot = UIWidget.Pivot.TopLeft;
grid.cellWidth = _itemModel.size.x;
grid.cellHeight = _itemModel.size.y;
grid.enabled = true;
_scrollPanel.showItemMaxRange = maxView;
}
private GameObject CreateItem()
{
int recycleNum = _itemRoot.unUseRootTrans.childCount;
if (recycleNum > 0)
{
return _itemRoot.unUseRootTrans.GetChild(0).gameObject;
}
GameObject obj = _itemModel.CreateItem();
BoxCollider collider = obj.AddOrGetComponent<BoxCollider>();
collider.size = _itemModel.size;
UIDragScrollView drag = obj.AddOrGetComponent<CarrieUIDragScrollView_002>();
drag.InitDragInfo(_parent.transform);
return obj;
}
private void RecycleItem(Transform obj)
{
CommonTool.SetParent(_itemRoot.unUseRootTrans, obj);
}
/// <summary>
/// 检查是否再显示区域内 TODO
/// </summary>
private bool CheckIsInView(Vector3 pos)
{
return true;
}
#endregion
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
3.item 检测类
这个类是自动挂载在 动态生成的 item 身上的,同时 item 也应该挂载 collider ,负责监听 UICamera 的事件操作:
//scrollview item 拖拽等功能的类
public class UIDragScrollView : MonoBehaviour {
private ScrollView _scroll;
private bool _ispress = false;
// Use this for initialization
void Start() {
}
// Update is called once per frame
void Update() {
}
private void OnEnable()
{
InitDragInfo();
}
private void OnDisable()
{
_ispress = false;
}
public void InitDragInfo(Transform trans = null)
{
if (_scroll == null)
{
if (trans == null)
{
trans = transform;
}
_scroll = trans.GetComponentInParent<ScrollView>();
}
_ispress = false;
}
void OnPress(bool pressed)
{
if (!enabled || !NGUITools.GetActive(this) || _ispress == pressed) return;
_ispress = pressed;
if (_scroll)
{
_scroll.PressScroll(pressed);
}
}
void OnDrag(Vector2 delta)
{
if (!enabled || !NGUITools.GetActive(this)) return;
if (_scroll)
{
_scroll.DragScroll();
}
}
}
4.测试代码
public class TestScrollview002 : MonoBehaviour {
public GameObject panelObj;
public GameObject itemObj;
public ScrollViewDirection direction = ScrollViewDirection.None;
public int Count = 0;
public int NextCount = 0;
// Use this for initialization
void Start() {
RegisterScrollview();
}
// Update is called once per frame
void Update() {
}
private ScrollView _srcoll;
private void RegisterScrollview()
{
if (_srcoll == null)
{
_srcoll = CommonTool.AddOrGetComponent<ScrollView>(gameObject);
if (direction == ScrollViewDirection.None) { direction = ScrollViewDirection.Vertical; }
_srcoll.InitScrollView(panelObj, itemObj, direction);
_srcoll.RegisterDragStartCB = DragStartFunc;
_srcoll.RegisterDragEndCB = DragEndFunc;
_srcoll.RegisterInitItemFunc = InitItem;
}
if (Count > 0)
{
_srcoll.RefreshScrollveiw(Count);
}
}
[ContextMenu("RefreshNextCount")]
public void RefreshNextCount()
{
if (NextCount > 0)
{
_srcoll.RefreshScrollveiw(NextCount);
}
}
private void DragStartFunc()
{
//Debug.Log(" scrollview start drag 001 ");
}
private void DragEndFunc()
{
//Debug.Log(" scrollview start end 002 ");
}
private void InitItem(GameObject obj, int index)
{
//Debug.Log(" index obj init " + index);
}
}
5.测试实例
这里举一个例子,专门用于验证 Scrollview,操作如图:
给我们即将动态生成的 item 加上它需要显示的内容的组件(比如 texture,uisprite),和我们自定义的 “UIDragScrollView” 和 “box collider” 组件。
然后给 scrollivew 节点添加我们的测试脚本 “TestScrollview002”,并且将对应的参数拖拽到相应的位置,设置好参数。
同时,修改 Next Count 中的值,点击 TestScrollview 代码右侧的设置,选中 “RefreshNextCount” 可以直接执行 Next Count 中的数量。
6.辅助工具类
以上代码中有些使用到的工具类代码如下,个人工具类:
public static class CommonTool {
public static T AddOrGetComponent<T>(this GameObject obj) where T : Component
{
T component = obj.GetComponent<T>();
if (null == component)
{
component = obj.AddComponent<T>();
}
return component;
}
public static void SetActive(GameObject obj, bool show)
{
if (null != obj && obj.activeSelf != show)
{
obj.SetActive(show);
}
}
public static void SetActive(Transform trans, bool show)
{
if (null != trans)
{
SetActive(trans.gameObject, show);
}
}
public static void SetParent(GameObject parent, GameObject child)
{
if (parent == null || child == null)
{
return;
}
child.transform.SetParent(parent.transform);
child.transform.localPosition = Vector3.zero;
child.transform.localEulerAngles = Vector3.zero;
child.transform.localScale = Vector3.one;
}
public static void SetParent(Transform parent, Transform child)
{
if (null == parent || null == child)
{
return;
}
SetParent(parent.gameObject, child.gameObject);
}
}
结果
用了个免费软件做了个动图给大家康康。
垂直滑动的效果:
水平滑动的效果:
ps:如果你们发现代码过程中遇到什么bug,就自己修修呀,因为我写的比较草率,而且不会更新这个版本,这个只是我看完 scrollview 一个简单总结,以后也许会出一个比较正规的终极版(如果我记得的话)。