NGUI源码分析(二)之 自定义 UIScrollView 01版本

文章说明

本篇文章基于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 一个简单总结,以后也许会出一个比较正规的终极版(如果我记得的话)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值