これは何
Androidで一番使われているであろうグラフ描画ライブラリMPAndroidChartでチップをいい感じに表示させようとした時に細かいところで難儀したので、その覚書です
書かないこと
MPAndroidChart本体の使い方、記述の仕方は先人たちがいるので割愛します
やったこと
チップを出す位置の調整
折れ線グラフを使用していたのですが、それぞれのデータ選択時に表示されるチップの位置を調整する必要がありました。
何も指定しなければ折れ線の上にチップが表示されますが、グラフの表示領域に限りがあったため、折れ線の上にチップが表示し切れない場合は下に表示するように調整が必要でした。左右も同様です。
チップの見た目編集(MarkerView)
MarkerView
を使って、表示時の細かいレイアウト調整ができます。
実際に調整が可能なメソッドを2つ紹介します
1. refreshContent
メソッドがチップを表示する度に呼び出されます。ここでは表示する対象のデータにアクセスできるので、データを元にチップの表示を調整することができます。
チップ本体の位置を調整することはできませんが、例えばチップが吹き出し型だった場合の足(?)の向きを調整することができます。
メリット:
データを元に調整ができる
デメリット:
表示する座標データを持っていないので、実際にグラフ上のどこに表示するかは制御できない
2. getOffsetForDrawingAtPoint
こちらもチップを表示する度に呼び出されます。ここでは表示する対象のデータにアクセスすることはできません。
ですが、チップを表示する座標データを持っているので、グラフ上のチップ表示位置を調整することができます。
チップが見切れる問題はこちらで解決できます。
メリット:
座標を元にチップの表示位置を調整できる
デメリット:
実際のデータを持っていないので、データに基づく制御はできない
それぞれチップを表示するタイミングで1 → 2の順で呼び出されます。
今回私は吹き出し型のチップ
をグラフの範囲内で
表示したかったので、それぞれを組み合わせて使用しました。
実際の使い方
MarkerView.xml
を作成し、lineChart
に登録して使用しました。
今回は吹き出しの位置をチップの表示位置によって上下出しわけしたかったので、あらかじめ上下に足となる三角形のパーツをつけ、表示位置によってGONE/VISIBLEを切り替えるようにしました。
チップ本体の位置調整は前述の通りgetOffsetForDrawingAtPoint
で行いました。
override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF {
val widthHalf = width / 2.0f
// 縦の見切れを判断するための調整値
// 画面サイズによって変更
val heightAdjust = if (resources.displayMetrics.heightPixels > 2000) {
17
} else {
20
}
// 横の見切れを判断する値の調整値
val widthAdjust = -93
val heightMargin = 20f
val viewWidth = resources.displayMetrics.widthPixels - 16
val xOffset = if (posX < widthHalf) {
// 左側見切れ
-posX
} else if (posX > viewWidth - widthHalf + widthAdjust) {
// 右側見切れ
-(width.toFloat() - 50)
} else {
// 左右見切れなし
-widthHalf
}
val yOffset = if (posY < height + heightAdjust) {
// 上側見切れ
heightMargin
} else {
// 上下見切れなし
-height - heightMargin
}
return MPPointF(xOffset, yOffset)
}
Android初心者なのでもっと良い計算方法があったのではと思うのですが、画面サイズによって見切れ具合が変わったりするため、値を微調整してこの処理に落ち着きました。
難しかった原因
- 表示するグラフの値が2桁/3桁で差分があり、グラフの表示範囲にバリエーションがあった
- グラフの表示範囲は共通だったため、Y軸の値が3桁の場合はその分グラフの表示範囲が小さくなってしまい、2桁の場合より見切れ幅が大きくなってしまっていた
吹き出しの位置調整はrefreshContent
で行いました。
override fun refreshContent(entry: Entry?, highlight: Highlight?) {
if (entry == null) {
super.refreshContent(null, highlight)
return
}
val data = entry.data as? GrowthCurveGraph
data?.let { graphData ->
val dropDownView = findViewById<ImageView>(R.id.dropdown_icon)
val dropUpView = findViewById<ImageView>(R.id.drop_up_icon)
val dropDownViewMargin = dropDownView.layoutParams as MarginLayoutParams
val dropUpViewMargin = dropUpView.layoutParams as MarginLayoutParams
val value = deleteString(graphData.value)
findViewById<TextView>(R.id.valueText).text = graphData.value
findViewById<TextView>(R.id.ageText).text = graphData.age
findViewById<TextView>(R.id.dateText).text = graphData.date
// チップの足を調整する単位
val adjustWidth = calcAdjustWidth()
// 足をずらす必要がある(左)
val leftSideCount = if (isYearRound) {
10
} else {
2
}
// 足をずらす必要がある(右)
val rightSideCount = if (isYearRound) {
if (resources.displayMetrics.heightPixels > 2000) {
58
} else {
52
}
} else {
count * 2 / 3
}
// 足の表示(上下)を切り替える必要があるかどうかを判断するための値を設定する
val topMargin = calcTopMargin()
val rightMargin =
// 左端
if (entry.x < leftSideCount) {
(adjustWidth * (leftSideCount - entry.x)).toInt()
// 右端
} else if (entry.x > rightSideCount) {
if (isHeight) {
-(adjustWidth * 2.3).toInt()
} else {
-(adjustWidth * 2)
}
} else {
// その他
0
}
// データの上に表示し切れない場合
if (topMargin < value) {
dropDownView.visibility = GONE
dropUpView.visibility = VISIBLE
dropUpView.layoutParams = dropUpViewMargin.apply {
setMargins(0, 0, rightMargin, 0)
}
} else {
dropDownView.visibility = VISIBLE
dropUpView.visibility = GONE
dropDownView.layoutParams = dropDownViewMargin.apply {
setMargins(0, 0, rightMargin, 0)
}
}
}
super.refreshContent(entry, highlight)
}
足の位置調整はデータが取得できたのと、X軸の数は2パターンしかなかったため、固定で左右両端の場合に足の位置をスライドさせるようにしました。
逆に言えば座標が取得できなかったため、データを元にするしかなかったとも言えます…
グラフの上限/下限を取得するようにして、表示する範囲を算出し、それを元に見切れるか否かを判断するようにしています。
当然ですが座標を元に計算した値と一致しないので、整合性が取れるように細かい調整値を入れています。