iOS雷达图

uwzzf4535 5年前
   <p>最近公司要做一个医学考试类的App,里面有一个能力分析的雷达图,leader让我来封装这个雷达图。这个模块是在做测心率模块期间完成的,也是使用的 CoreGraphics 来实现的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c93552974b0971a4ca4ecafeaf4a8f64.jpg"></p>    <p>之前并不知道雷达图长什么样,这个是我在百度上下载的。大概就长这个样子,我们要用的和这个不太一样,没有文字和介绍。</p>    <h2><strong>思路</strong></h2>    <p>写这个的时候,没有查阅什么资料,所以不太清楚别人是怎么做的。</p>    <p>我是从角度出发的,例如上面的图中,有5个能力模块,5个能力模块的分支(后面就叫它 主干 了,我也不知道叫什么)两两的夹角都是相同的。</p>    <ol>     <li>首先把一个主干放在 y轴 上,这个主干绕 原点 O 一周是 2π ,被5个 主干 5等分,那么每一个夹角都是 2π/5 。</li>     <li>主干的长是相同的,可以通过 正弦 和 余弦 来求出每个主干落点的坐标,再从 原点O 到 落点 画线。</li>     <li>之后的网格和能力分布的画法也是这个思路,同样是从 y轴 上的主干取点,不同的是 斜边 的长度会根据 网格间距 和 能力百分比 变化,如下图:</li>    </ol>    <p><img src="https://simg.open-open.com/show/6f50f86a6190afe53b68091d46bf5f50.jpg"></p>    <p>不知道大家还记不记得正弦和余弦是啥了,我是已经还给数学老师了(虽然初中数学也是年组第一的),刚好写这个的前一天在看高数。</p>    <pre>  <code class="language-objectivec">正弦: sin∂ = 对边/斜边    余弦: cos∂ = 临边/斜边</code></pre>    <h2><strong>实现</strong></h2>    <p>1. 初始化,定义几个属性后面使用</p>    <pre>  <code class="language-objectivec">// 元素数组  @property (strong, nonatomic) NSArray <Element *>*elements;</code></pre>    <p>这是一个放Model的数组,用来储存能力模块信息</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/89a1256f0e27235ba46f9f95303f28a4.jpg"></p>    <pre>  <code class="language-objectivec">// 每一个主干的长度  static float element_w = 0;  // 主干到View预留的空白部分宽度  static float border_w  = 5;  // 中心点的横坐标  static float center_w  = 0;  // 中心点的纵坐标  static float center_h  = 0;</code></pre>    <pre>  <code class="language-objectivec">- (instancetype)initWithFrame:(CGRect)frame Elements:(NSArray <Element *>*)elements {      if (self = [super initWithFrame:frame]) {          self.backgroundColor = [UIColor whiteColor];          self.elements = elements;          if (self.frame.size.width>self.frame.size.height)              element_w = (self.frame.size.height-border_w*2)/2;          else              element_w = (self.frame.size.width-border_w*2)/2;            center_w = self.frame.size.width/2;          center_h = self.frame.size.height/2;        }      return self;  }</code></pre>    <p>2.画主干</p>    <pre>  <code class="language-objectivec">- (void)buildRadarMap {      if (self.elements.count<3) return;        // 获取画布      CGContextRef context = UIGraphicsGetCurrentContext();      // 划线颜色      if (self.trunkLineColor)          CGContextSetStrokeColorWithColor(context, self.trunkLineColor.CGColor);      else          CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);      // 划线宽度      if (self.lineWidth)          CGContextSetLineWidth(context, self.lineWidth);      else          CGContextSetLineWidth(context, 1);      // 起点坐标      CGContextMoveToPoint(context, center_w, center_h);      // 第一条线      CGContextAddLineToPoint(context, center_w, center_h - element_w);        //画元素主干      for (int i = 1; i <self.elements.count; i++) {          float x   = 0;          float y   = 0;          double pi = (M_PI*2.0/(self.elements.count))*i;            // 计算主干落点坐标          Coordinate(pi, element_w, center_w, center_h,&x, &y);          // 设置每次的初始点坐标          CGContextMoveToPoint(context, center_w, center_h);          // 设置终点坐标          CGContextAddLineToPoint(context, x, y);      }      CGContextStrokePath(context);  }</code></pre>    <p>注: self.trunkLineColor 是我设置的一个外部可控主干的颜色,如果尾部没有设置就给默认值。 self.lineWidth 就是先的宽度了。</p>    <pre>  <code class="language-objectivec">/**   *  雷达图分成几等份 默认1等份(最外面一圈的框框)   */  @property (assign, nonatomic) float part;    /**   *  雷达线的宽度 默认1   */  @property (assign, nonatomic) float lineWidth;    /**   *  主干线颜色 默认黑色   */  @property (strong, nonatomic) UIColor *trunkLineColor;    /**   *  分等份线颜色 默认黑色   */  @property (strong, nonatomic) UIColor *partLineColor;    /**   *  比例线颜色 默认绿色   */  @property (strong, nonatomic) UIColor *percentLineColor;    - (void)setTrunkLineColor:(UIColor *)trunkLineColor {      _trunkLineColor =trunkLineColor;      [self setNeedsDisplay];  }    - (void)setPartLineColor:(UIColor *)partLineColor {      _partLineColor = partLineColor;      [self setNeedsDisplay];  }    - (void)setPercentLineColor:(UIColor *)percentLineColor {      _percentLineColor = percentLineColor;      [self setNeedsDisplay];  }</code></pre>    <p>设置set方法调用 setNeedsDisplay 外部设置颜色的时候会调用 drawRect:</p>    <p>3.计算落点的方法</p>    <pre>  <code class="language-objectivec">#pragma mark - 算落点坐标    void Coordinate (double pi, float l, float c_w , float c_h, float *x, float *y) {        if (pi < M_PI_2) {          *x = c_w + sin(pi)*l;          *y = c_h - cos(pi)*l;      } else if (pi == M_PI_2) {          *x = c_w + sin(pi)*l;          *y = c_h + cos(pi)*l;      } else if (pi < M_PI) {          *x = c_w + sin(pi)*l;          *y = c_h - cos(pi)*l;      } else if (pi == M_PI) {          *x = c_w + sin(pi)*l;          *y = c_h + cos(pi)*l;      } else if (pi < M_PI_2*3) {          *x = c_w + sin(pi)*l;          *y = c_h - cos(pi)*l;      } else if (pi == M_PI_2*3) {          *x = c_w + sin(pi)*l;          *y = c_h + cos(pi)*l;      } else {          *x = c_w + sin(pi)*l;          *y = c_h - cos(pi)*l;      }  }</code></pre>    <table>     <thead>      <tr>       <th>变量</th>       <th>用途</th>      </tr>     </thead>     <tbody>      <tr>       <td>pi</td>       <td>角度</td>      </tr>      <tr>       <td>l</td>       <td>主干长度</td>      </tr>      <tr>       <td>c_w</td>       <td>原点横坐标</td>      </tr>      <tr>       <td>c_h</td>       <td>原点纵坐标</td>      </tr>      <tr>       <td>x</td>       <td>落点横坐标</td>      </tr>      <tr>       <td>y</td>       <td>落点纵坐标</td>      </tr>     </tbody>    </table>    <p>坐标系和View的Rect还不太一样,比如在坐标系第二、三象限的 y 是负值,而我们要的坐标的 y 是越往下越大,所以我们要 c_h+(-y) , x 同理。</p>    <p>4.画网格,也就是要把主干分成几等分,为了更灵活我也设置了可以从外部设置。</p>    <pre>  <code class="language-objectivec">/**   *  雷达图分成几等份 默认1等份(最外面一圈的框框)   */  @property (assign, nonatomic) float part;    - (void)setPart:(float)part {      _part = part;      [self setNeedsDisplay];  }</code></pre>    <pre>  <code class="language-objectivec">#pragma mark - 画雷达分等分图    - (void)buildPart {        float r = self.part<=1?1:self.part;        // 获取画布      CGContextRef context = UIGraphicsGetCurrentContext();      // 划线颜色      if (self.partLineColor)          CGContextSetStrokeColorWithColor(context, self.partLineColor.CGColor);      else          CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);      // 划线宽度      if (self.lineWidth)          CGContextSetLineWidth(context, self.lineWidth);      else          CGContextSetLineWidth(context, 1);      // 话分割线      for (int j = 0; j<r; j++) {          // 设置每次的初始点坐标          CGContextMoveToPoint(context, center_w, border_w);          // 画百分比分部          for (int i = 1; i<=self.elements.count; i++) {              float x   = 0;              float y   = 0;              double pi = (M_PI*2.0/(self.elements.count))*i;              Coordinate(pi,element_w*(r-j)/r, center_w, center_h,&x, &y);                if (i == 1) {                  CGContextMoveToPoint(context, center_w, border_w + element_w*j/r);              }              if (i == self.elements.count) {                  CGContextAddLineToPoint(context, center_w, border_w + element_w*j/r);              } else {                  CGContextAddLineToPoint(context, x, y);              }          }        }      CGContextStrokePath(context);  }</code></pre>    <p>原理和画主干是一样的,只不过是每次的 起点 变成了上一个点的 落点 。把 主干 的长度进行了分割</p>    <p>5.画能力占比线</p>    <pre>  <code class="language-objectivec">#pragma mark - 画百分比占比线    - (void)buildPercent {        // 获取画布      CGContextRef context = UIGraphicsGetCurrentContext();      // 划线颜色      if (self.percentLineColor)          CGContextSetStrokeColorWithColor(context, self.percentLineColor.CGColor);      else          CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);      // 划线宽度      if (self.lineWidth)          CGContextSetLineWidth(context, self.lineWidth);      else          CGContextSetLineWidth(context, 1);        Element *ele = self.elements[0];      CGContextMoveToPoint(context, center_w, border_w +element_w*(1-ele.percent));      for (int i = 1; i<=self.elements.count; i++) {          float x   = 0;          float y   = 0;            if (i == self.elements.count) {              //终点,最终回到开始点坐标              Element *ele = self.elements[0];              CGContextAddLineToPoint(context, center_w, border_w +element_w*(1-ele.percent));          } else {              Element *ele = self.elements[i];              double pi = (M_PI*2.0/(self.elements.count))*i;              Coordinate(pi,element_w*ele.percent, center_w, center_h,&x, &y);              CGContextAddLineToPoint(context, x, y);          }      }      CGContextStrokePath(context);  }</code></pre>    <p>这里和画网格不一样的就是每次的把主干的长度按照这个 能力值 的 百分比 分割</p>    <h2><strong>总结</strong></h2>    <p>因为有之前的心率那得基础,这里完成的很快,然后做了一个相对比较灵活的封装,还没有加能力描述和百分比值,后续加上。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/e6d65460a6c2</p>    <p> </p>