AndroidのCanvasでドーナツチャートを描く

ドーナツ型の円グラフを描くのに結構試行錯誤したので、メモしておく。

完成形

f:id:YaaMaa:20170216032855p:plain

f:id:YaaMaa:20170216032906p:plain

こういう二種類のグラフを作りたい。

clipPathを使う

最初は、グラフの色付きの部分を扇形で描き、その後にclipPathで円の中身をくり抜いていた。アンチエイリアスがかからんかったのは気に入らないけど、それ以外はいい感じだと思っていた。

けど、その円グラフをActivityの上のFragment上のViewPagerの1ページのFragmentの上…という複雑な境遇のCanvasで描くと、clipPathした部分が反転して見えてしまった。

f:id:YaaMaa:20170216033238p:plain

なので、別の方法を探した。

くり抜く

sukohi.blogspot.jp

↑こちらのブログに、よく似たことをやってはる方がいたので、真似させていただいた。

Pathを初めにadd○○を使って塗り、その上から反時計回りでくり抜きたい形をaddしたらできるらしい。

私の場合なら、初めにaddArcしてからaddCircle(CCW)でくり抜いたPathをdrawPathすればいいっていうこと。

これで解決したと思ったんだけど…

f:id:YaaMaa:20170216034549p:plain

こっちのグラフが大惨事になっていた。何事かと思ったら、addArcする際に中心を指定するのを忘れていた。

そして指定しようと思ったら…まず中心を指定するための引数がなかった。drawArcにはuseCenterっていう引数がちゃんと用意されてんのに、addArcにはない笑

なので、この方法も使えない。

他の解決法

stackoverflowで調べてみると、いろんなアドバイスがあった。

「初めにドーナツ型の画像用意しといて、それ切り取ったらいいよ」、「drawArcで代用しーよ」、「PorterDuffXfermode使えばいいよ」、「いっそのことAChartEngine使わせてもらいさ」など。

けど、画像用意するのは気に入らんし、drawArcではくり抜けへんし(上から背景色で塗りつぶすのはできるけど気持ち悪い)、ポーターダフは過去何度も使おうと思ってその度に挫折してきたし、ライブラリ使うのは何か悔しい(わがまま)。

自分で扇形をPathに追加するメソッドを作る

addArcでuseCenterが使えないなら、自分で扇形をPathに追加できるようにすればいいのでは。と思って、作ってみた。

public Path addSector(Path path, float x, float y, float radius, float startAngle, float sweepAngle) {
  path.reset();
  path.moveTo(x, y);
  path.lineTo(
    x + (float) (radius*Math.cos(Math.toRadians(startAngle))),
    y + (float) (radius*Math.sin(Math.toRadians(startAngle)))
  );
  path.arcTo(new RectF(x-radius, y-radius, x+radius, y+radius), startAngle, sweepAngle);
  path.close();
  return path;
}

扇形は英語でsectorっていうらしいので、addSectorって名前にした。まず中心に点を打って、それをstartAngleまで持って行って、そこからsweepAngle分arcToし、閉じる。

ラジアンとかいう言葉久しぶりに聞いて頭痛がした。

これでできたかな?と思ったが、

f:id:YaaMaa:20170216040423p:plain

刺さってる笑

そういえば、敵の敵は味方みたいな感じで、もともと描画してないところをさらにくり抜こうとしたら逆に描画されてしまうんか。

ドーナツ型のパスを自分で作る

よく考えれば、真ん中から円をくり抜くことに固執する必要はなかった。初めから真ん中が抜けているドーナツ形のパスを追加すればいいんやん!

public Path makeDonutPath(float x, float y, float radius, float startAngle, float sweepAngle, float holeProportion) {
  Path donutPath = new Path();
  donutPath.moveTo(
    x + (float) (radius*Math.cos(Math.toRadians(startAngle))),
    y + (float) (radius*Math.sin(Math.toRadians(startAngle)))
  );
  donutPath.arcTo(
    new RectF(x-radius, y-radius, x+radius, y+radius),
    startAngle,
    sweepAngle
  );
  donutPath.lineTo(
    x + (float) (radius*holeProportion*Math.cos(Math.toRadians(startAngle+sweepAngle))),
    y + (float) (radius*holeProportion*Math.sin(Math.toRadians(startAngle+sweepAngle)))
  );
  donutPath.arcTo(
    new RectF(x-radius*holeProportion, y-radius*holeProportion, x+radius*holeProportion, y+radius*holeProportion),
    startAngle+sweepAngle,
    -sweepAngle
  );
  donutPath.close();
  return donutPath;
}

やっとできた…!!( ;∀;)

ネーミングセンスがあれやけど、holeProportionっていうのはドーナツ全体の大きさに対するドーナツの穴の大きさの割合。

ちょっと修正

sweepAngleが0だと、弧の始まりと終わりが同じなので円全体が描画されてしまった。なので、new Path()作った直後に、sweepAngle==0なら即座にその空っぽのPathを持って帰ってもらうようにした。

また、sweepAngle >= 360fのときには、完全な輪っかを作って返すようにした。