Unity_扇形图(饼状图)+ UI动画

GitHub项目链接点这里(求一个star⭐)

效果

效果

介绍

实现饼状图的大体思路:先求出每一项数据在所有数据占的比例,得出该比例在一个圆中占的角度,第一个扇形使用默认的旋转角度,其次根据上一条数据所绘制的扇形角度计算下一条数据的旋转角度,最终拼成一整个圆形。最后上色,添加颜色注解。
和鼠标的交互用到了多边形碰撞器 PolygonCollider2D,根据不同的弧度设置不同精度的碰撞器,最后根据Image继承的ICanvasRaycastFilter接口检测鼠标是否悬浮在当前碰撞器上。

关键字

1. 对象池
2. DoTween 动画
3. 多边形碰撞器 PolygonCollider2D
4. UI射线检测接口 ICanvasRaycastFilter

分析及代码

  • 绘图数据

定义绘图数据的结构GraphData,包含数据的描述信息_desc,数据的值_value。在需要绘图时传递一组GraphData的类型的数据给此模块就可以实现图形的绘制。

[System.Serializable]
public struct GraphData
{
    public string _desc;
    [Range(0, 100)]
    public float _value;

    public float Rate
    {
        get
        {
            return _value / 100;
        }
    }

    public GraphData(string desc, float value)
    {
        _desc = desc;
        _value = value;
    }
}
  • 扇形元素 UIPieImage

在绘制图形之前准备好每一个扇形所需要的功能。为了实现与鼠标的交互,这里使用到了PolygonCollider2D组件。该组件可以通过脚本设置多边形碰撞体的顶点,通过一个有序的Vector2类型的数组绘制出碰撞体的边缘。也可以在Inspector面板上调整(但这不符和本功能模块需要不停刷新数据的需求)。
注意在设置顶点数组时的顶点顺序,下面的代码中有【找规律】标签,是最原始的代码,比较容易理解。我习惯了去精简一堆有规律的代码,这样可以使我的代码看起来很简洁。下面是UIPieImage脚本,有注释,我就不分开分析了。

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(PolygonCollider2D))]
public class UIPieImage : Image
{
    private PolygonCollider2D _polygon = null;
    private PolygonCollider2D Polygon
    {
        get
        {
            if (_polygon == null) _polygon = GetComponent<PolygonCollider2D>();
            return _polygon;
        }
    }

    /// <summary>
    /// 重置Collider
    /// </summary>
    public void ResetCollider()
    {
        float radius = rectTransform.sizeDelta.y / 2;
        float angle = fillAmount * 360;
        SetColliderPath(radius, angle);
    }

    /// <summary>
    /// 设置Collider路径
    /// </summary>
    /// <param name="radius"></param>
    /// <param name="radian"></param>
    private void SetColliderPath(float radius, float angle)
    {
        Polygon.SetPath(0, GetPathes(radius, angle));
    }

    /// <summary>
    /// 计算路径点
    /// </summary>
    /// <param name="radius"></param>
    /// <param name="angle"></param>
    /// <returns></returns>
    private Vector2[] GetPathes(float radius, float angle)
    {
        //注意数组的顺序
        Vector2[] pathes = null;
        float rate = angle / 60;
        float radian = angle * Mathf.Deg2Rad;
        #region 找规律
        //if (1 >= rate)
        //{
        //    pathes = new Vector2[3];
        //}
        //else if (1 < rate && 2 >= rate)
        //{
        //    pathes = new Vector2[4];
        //    pathes[2] = new Vector2(radius * Mathf.Sin(radian / 2), radius * Mathf.Cos(radian / 2));
        //}
        //else if (2 < rate && 3 >= rate)
        //{
        //    pathes = new Vector2[5];
        //    pathes[2] = new Vector2(radius * Mathf.Sin(radian / 3), radius * Mathf.Cos(radian / 3));
        //    pathes[3] = new Vector2(radius * Mathf.Sin(radian / 3 * 2), radius * Mathf.Cos(radian / 3 * 2));
        //}
        //else if (3 < rate && 4 >= rate)
        //{
        //    pathes = new Vector2[6];
        //    pathes[2] = new Vector2(radius * Mathf.Sin(radian / 4), radius * Mathf.Cos(radian / 4));
        //    pathes[3] = new Vector2(radius * Mathf.Sin(radian / 2), radius * Mathf.Cos(radian / 2));
        //    pathes[4] = new Vector2(radius * Mathf.Sin(radian / 4 * 3), radius * Mathf.Cos(radian / 4 * 3));
        //}
        //else if (4 < rate && 5 >= rate)
        //{
        //    pathes = new Vector2[7];
        //    pathes[2] = new Vector2(radius * Mathf.Sin(radian / 5), radius * Mathf.Cos(radian / 5));
        //    pathes[3] = new Vector2(radius * Mathf.Sin(radian / 5 * 2), radius * Mathf.Cos(radian / 5 * 2));
        //    pathes[4] = new Vector2(radius * Mathf.Sin(radian / 5 * 3), radius * Mathf.Cos(radian / 5 * 3));
        //    pathes[5] = new Vector2(radius * Mathf.Sin(radian / 5 * 4), radius * Mathf.Cos(radian / 5 * 4));
        //}
        //else
        //{
        //    pathes = new Vector2[8];
        //    pathes[2] = new Vector2(radius * Mathf.Sin(angle / 6), radius * Mathf.Cos(angle / 6));
        //    pathes[3] = new Vector2(radius * Mathf.Sin(angle / 6 * 2), radius * Mathf.Cos(angle / 6 * 2));
        //    pathes[4] = new Vector2(radius * Mathf.Sin(angle / 6 * 3), radius * Mathf.Cos(angle / 6 * 3));
        //    pathes[5] = new Vector2(radius * Mathf.Sin(angle / 6 * 4), radius * Mathf.Cos(angle / 6 * 4));
        //    pathes[6] = new Vector2(radius * Mathf.Sin(angle / 6 * 5), radius * Mathf.Cos(angle / 6 * 5));
        //}
        #endregion
        for (int i = 1; i <= 6; i++)
        {
            if (i - 1 < rate && i >= rate)
            {
                pathes = new Vector2[i + 2];
                Debug.Log(i);
                for (int j = 2; j < pathes.Length - 1; j++)
                {
                    pathes[j] = new Vector2(radius * Mathf.Sin(radian / i * (j - 1)), radius * Mathf.Cos(radian / i * (j - 1)));
                }
                break;
            }
        }
        pathes[0] = Vector2.zero;
        pathes[1] = new Vector2(0, radius);
        pathes[pathes.Length - 1] = new Vector2(radius * Mathf.Sin(radian), radius * Mathf.Cos(radian));
        return pathes;
    }

    /// <summary>
    /// 检测Recast是否有效
    /// ICanvasRaycastFilter接口函数
    /// </summary>
    /// <param name="sp"></param>
    /// <param name="eventCamera"></param>
    /// <returns></returns>
    public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        bool inCollider = Polygon.OverlapPoint(Input.mousePosition);
        if (inCollider) transform.localScale = Vector3.one * 1.2f;
        else transform.localScale = Vector3.one;
        return inCollider;
    }
}
  • 1.定义参数

下图是Unity的布局,为了方便管理,在PieGraphPanel中分别设置了PieContent(扇形),NoteContent(颜色注解)两个空对象存放相应的子元素。
其次提供了是否根据数据值排序的参数_sort,所有的扇形颜色管理_colors。

布局

    public GraphData[] _datas;//数据
    public bool _sort;//排序
    public float _pieRadius = 100;//扇形半径
    public RectTransform _noteContent;//注解Content
    public RectTransform _pieContent;//扇形Content
    public Text _notePrefab;//注解Prefab
    public UIPieImage _piePrefab;//扇形Prefab
    public float _tweenTime = 1f;//动画时间

    //注解、扇形、颜色 管理
    private Text[] _notes;
    private UIPieImage[] _pies;
    private Color[] _colors;
  • 2.颜色注解

颜色注解使用了颜色的渐变,修改颜色的g(green)值,注意要先画分配好颜色注解,下面绘制圆形时会用到上色。

    /// <summary>
    /// 颜色注解
    /// </summary>
    private void DrawNote()
    {
        _notes = new Text[_datas.Length];
        _colors = new Color[_datas.Length];
        for (int i = 0; i < _datas.Length; i++)
        {
            Text note = ObjectPool.Instance.GetObject(_notePrefab.name, _noteContent).GetComponent<Text>();
            _colors[i] = new Color(0, (float)i / _datas.Length, 0);
            note.text = _datas[i]._desc;
            note.GetComponentInChildren<Image>().color = _colors[i];
            note.gameObject.SetActive(true);
            _notes[i] = note;
        }
    }
  • 3.画扇形

先求出每一项数据在所有数据占的比例,得出该比例在一个圆中占的角度,第一个扇形使用默认的旋转角度,其次根据上一条数据所绘制的扇形角度计算下一条数据的旋转角度。使用到了对象池的优化(链接)

 	/// <summary>
    /// 画扇形
    /// </summary>
    private void DrawPie()
    {
        float sum = 0;
        for (int i = 0; i < _datas.Length; i++)
        {
            sum += _datas[i]._value;
        }
        _pies = new UIPieImage[_datas.Length];
        Vector3 _curAngle = Vector3.zero;
        for (int i = 0; i < _datas.Length; i++)
        {
            UIPieImage pie = ObjectPool.Instance.GetObject(_piePrefab.name, _pieContent).GetComponent<UIPieImage>();
            pie.rectTransform.sizeDelta = Vector2.one * _pieRadius * 2;
            float rate = _datas[i]._value / sum;
            pie.fillAmount = 0;
            pie.color = _colors[i];
            _curAngle += Vector3.forward * 360 * rate;
            pie.rectTransform.localEulerAngles = _curAngle;
            //pie.rectTransform.localScale = Vector3.zero;
            pie.gameObject.SetActive(true);
            //pie.rectTransform.DOScale(Vector3.one, _tweenTime);
            pie.DOFillAmount(rate, _tweenTime).OnComplete(() => pie.ResetCollider());
            _pies[i] = pie;
        }
    }
  • 4.排序

可选项。根据值的大小排序。

    /// <summary>
    /// 排序
    /// </summary>
    private void Sort()
    {
        for (int i = 0; i < _datas.Length; i++)
        {
            for (int j = 0; j < _datas.Length - i - 1; j++)
            {
                if (_datas[j]._value > _datas[j + 1]._value)
                {
                    _tempData = _datas[j];
                    _datas[j] = _datas[j + 1];
                    _datas[j + 1] = _tempData;
                }
            }
        }
    }
  • 5.整体代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

/// <summary>
/// 扇形图
/// </summary>
public class UIPieGraphManager : MonoBehaviour
{
    public GraphData[] _datas;//数据
    public bool _sort;//排序
    public float _pieRadius = 100;//扇形半径
    public RectTransform _noteContent;//注解Content
    public RectTransform _pieContent;//扇形Content
    public Text _notePrefab;//注解Prefab
    public UIPieImage _piePrefab;//扇形Prefab
    public float _tweenTime = 1f;//动画时间

    //注解、扇形、颜色 管理
    private Text[] _notes;
    private UIPieImage[] _pies;
    private Color[] _colors;

    private void Awake()
    {
        ObjectPool.Instance.SetPrefab(_notePrefab.gameObject);
        ObjectPool.Instance.SetPrefab(_piePrefab.gameObject);
    }

    /// <summary>
    /// 初始化扇形图
    /// </summary>
    public void InitPieGraph(GraphData[] data)
    {
        RefeshPieGraph(data);
    }

    /// <summary>
    /// 刷新扇形图
    /// </summary>
    public void RefeshPieGraph(GraphData[] data)
    {
        _datas = data;
        if (_sort) Sort();
        ClearTransform(_noteContent);
        ClearTransform(_pieContent);
        DrawNote();
        DrawPie();
    }

    private GraphData _tempData;
    /// <summary>
    /// 排序
    /// </summary>
    private void Sort()
    {
        for (int i = 0; i < _datas.Length; i++)
        {
            for (int j = 0; j < _datas.Length - i - 1; j++)
            {
                if (_datas[j]._value > _datas[j + 1]._value)
                {
                    _tempData = _datas[j];
                    _datas[j] = _datas[j + 1];
                    _datas[j + 1] = _tempData;
                }
            }
        }
    }

    /// <summary>
    /// 颜色注解
    /// </summary>
    private void DrawNote()
    {
        _notes = new Text[_datas.Length];
        _colors = new Color[_datas.Length];
        for (int i = 0; i < _datas.Length; i++)
        {
            Text note = ObjectPool.Instance.GetObject(_notePrefab.name, _noteContent).GetComponent<Text>();
            _colors[i] = new Color(0, (float)i / _datas.Length, 0);
            note.text = _datas[i]._desc;
            note.GetComponentInChildren<Image>().color = _colors[i];
            note.gameObject.SetActive(true);
            _notes[i] = note;
        }
    }

    /// <summary>
    /// 画扇形
    /// </summary>
    private void DrawPie()
    {
        float sum = 0;
        for (int i = 0; i < _datas.Length; i++)
        {
            sum += _datas[i]._value;
        }
        _pies = new UIPieImage[_datas.Length];
        Vector3 _curAngle = Vector3.zero;
        for (int i = 0; i < _datas.Length; i++)
        {
            UIPieImage pie = ObjectPool.Instance.GetObject(_piePrefab.name, _pieContent).GetComponent<UIPieImage>();
            pie.rectTransform.sizeDelta = Vector2.one * _pieRadius * 2;
            float rate = _datas[i]._value / sum;
            pie.fillAmount = 0;
            pie.color = _colors[i];
            _curAngle += Vector3.forward * 360 * rate;
            pie.rectTransform.localEulerAngles = _curAngle;
            //pie.rectTransform.localScale = Vector3.zero;
            pie.gameObject.SetActive(true);
            //pie.rectTransform.DOScale(Vector3.one, _tweenTime);
            pie.DOFillAmount(rate, _tweenTime).OnComplete(() => pie.ResetCollider());
            _pies[i] = pie;
        }
    }

    /// <summary>
    /// 入池
    /// </summary>
    /// <param name="trans"></param>
    /// <param name="pool"></param>
    private void ClearTransform(Transform parent)
    {
        for (int i = 1; i < parent.childCount; i++)
        {
            ObjectPool.Instance.RecycleObj(parent.GetChild(i).gameObject, parent);
        }
    }

}
  • 8
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值