feat: ring widget style (selectable bars/rings)
Build APK / build (pull_request) Successful in 1m34s
Build APK / build (pull_request) Successful in 1m34s
Add RingRenderer (circular gauge mirror of BarRenderer), a widget_layout_rings layout, a Bars/Rings preference + in-app toggle, and ring rendering branch in ClaudeUsageWidget. Full (4x2) widget honors the chosen style; compact size stays bars. Phase 1 of porting hamed-elfayome/Claude-Usage-Tracker features.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package me.khodak.claudeusage
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Draws a usage gauge as a circular ring Bitmap: rounded track ring + colored progress arc
|
||||
* (sweeping clockwise from 12 o'clock) + an optional radial pace tick + center percentage and
|
||||
* label. The ring counterpart to [BarRenderer] — same Bitmap contract, so it drops into both the
|
||||
* home-screen widget (setImageViewBitmap) and the in-app card and renders identically.
|
||||
*
|
||||
* Design mirrors hamed-elfayome/Claude-Usage-Tracker's ring gauges, using this app's existing
|
||||
* palette (SESSION_FILL clay / WEEKLY_FILL periwinkle, #252525 track, white pace marker).
|
||||
*/
|
||||
object RingRenderer {
|
||||
|
||||
private const val TRACK_COLOR = 0xFF252525.toInt()
|
||||
private const val CENTER_TEXT_COLOR = 0xFFFFFFFF.toInt()
|
||||
private const val LABEL_TEXT_COLOR = 0xFF999999.toInt()
|
||||
|
||||
/**
|
||||
* @param usedPct 0..100 fill of the ring
|
||||
* @param markerPct where you "should be" right now (pace), or null for no tick
|
||||
* @param fillColor ARGB progress-arc color (e.g. SESSION_FILL / WEEKLY_FILL)
|
||||
* @param markerColor ARGB pace-tick color, or null
|
||||
* @param centerText big text in the middle, e.g. "47%" (caller formats it)
|
||||
* @param labelText small caption under the number, e.g. "SESSION" (null hides it)
|
||||
* @param sizePx square bitmap edge; ring scales to fit
|
||||
* @param strokePx ring thickness
|
||||
*/
|
||||
fun render(
|
||||
usedPct: Int,
|
||||
markerPct: Int?,
|
||||
fillColor: Int,
|
||||
markerColor: Int?,
|
||||
centerText: String,
|
||||
labelText: String? = null,
|
||||
sizePx: Int = 240,
|
||||
strokePx: Float = 24f
|
||||
): Bitmap {
|
||||
val bmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bmp)
|
||||
val cx = sizePx / 2f
|
||||
val cy = sizePx / 2f
|
||||
val pad = strokePx / 2f + 2f
|
||||
val arcRect = RectF(pad, pad, sizePx - pad, sizePx - pad)
|
||||
val radius = (sizePx - strokePx) / 2f - 2f
|
||||
|
||||
// Track ring (full circle).
|
||||
val track = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokePx
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
color = TRACK_COLOR
|
||||
}
|
||||
canvas.drawArc(arcRect, 0f, 360f, false, track)
|
||||
|
||||
// Progress arc — clockwise from 12 o'clock (-90°).
|
||||
val pct = usedPct.coerceIn(0, 100)
|
||||
if (pct > 0) {
|
||||
val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokePx
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
color = fillColor
|
||||
}
|
||||
canvas.drawArc(arcRect, -90f, pct / 100f * 360f, false, fill)
|
||||
}
|
||||
|
||||
// Pace tick — a short radial mark across the ring at the "should be here" angle.
|
||||
if (markerPct != null && markerColor != null) {
|
||||
val m = markerPct.coerceIn(0, 100)
|
||||
val ang = Math.toRadians((-90f + m / 100f * 360f).toDouble())
|
||||
val rInner = radius - strokePx / 2f - 1f
|
||||
val rOuter = radius + strokePx / 2f + 1f
|
||||
val tick = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokePx * 0.18f
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
color = markerColor
|
||||
}
|
||||
canvas.drawLine(
|
||||
cx + (rInner * cos(ang)).toFloat(), cy + (rInner * sin(ang)).toFloat(),
|
||||
cx + (rOuter * cos(ang)).toFloat(), cy + (rOuter * sin(ang)).toFloat(),
|
||||
tick
|
||||
)
|
||||
}
|
||||
|
||||
// Center percentage.
|
||||
val hasLabel = !labelText.isNullOrEmpty()
|
||||
val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = CENTER_TEXT_COLOR
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = sizePx * 0.27f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
}
|
||||
val vfm = valuePaint.fontMetrics
|
||||
val valueBaselineShift = -(vfm.ascent + vfm.descent) / 2f
|
||||
val valueY = cy + valueBaselineShift - (if (hasLabel) sizePx * 0.07f else 0f)
|
||||
canvas.drawText(centerText, cx, valueY, valuePaint)
|
||||
|
||||
if (hasLabel) {
|
||||
val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = LABEL_TEXT_COLOR
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = sizePx * 0.105f
|
||||
letterSpacing = 0.12f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
}
|
||||
canvas.drawText(labelText!!.uppercase(), cx, cy + sizePx * 0.17f, labelPaint)
|
||||
}
|
||||
|
||||
return bmp
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user