package com.shire.mobile import android.app.AlertDialog import android.content.Intent import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.net.Uri import android.os.Bundle import android.text.InputType import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Button import android.widget.CheckBox import android.widget.EditText import android.widget.LinearLayout import android.widget.RadioButton import android.widget.RadioGroup import android.widget.ScrollView import android.widget.Spinner import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import org.json.JSONArray import org.json.JSONObject import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale private const val LF_BACKGROUND = 0xFF090B10.toInt() private const val LF_SURFACE = 0xFF11151D.toInt() private const val LF_RAISED = 0xFF1A202C.toInt() private const val LF_TEXT = 0xFFF2F3F7.toInt() private const val LF_MUTED = 0xFF9EA6B5.toInt() private const val LF_GREEN = 0xFF75E6A4.toInt() private const val LF_BLUE = 0xFF73D7FF.toInt() private const val LF_AMBER = 0xFFFFC766.toInt() private const val LF_RED = 0xFFFF7D86.toInt() private const val LF_PURPLE = 0xFFA987FF.toInt() private data class MeasurementSpec( val name: String, val instruction: String, val visual: String ) class LimbForgeMeasurementActivity : ComponentActivity() { private val measurementSpecs = listOf( MeasurementSpec( "Cosmetic envelope length", "Measure between the specialist-agreed proximal reference " + "and the intended distal end of the cosmetic cover.", "length" ), MeasurementSpec( "Proximal circumference", "Mark the specialist-agreed proximal cover line, then measure " + "around the full cosmetic envelope at that line.", "circumference_high" ), MeasurementSpec( "Mid-envelope circumference", "Measure around the centre of the intended cosmetic cover. " + "Keep the tape level and do not compress soft tissue.", "circumference_mid" ), MeasurementSpec( "Distal circumference", "Measure around the specialist-agreed distal cover line. " + "Do not measure across an unprotected sensitive area.", "circumference_low" ), MeasurementSpec( "Maximum medial-lateral width", "Record the widest side-to-side cosmetic envelope dimension " + "at the marked reference level.", "width" ), MeasurementSpec( "Maximum anterior-posterior depth", "Record the widest front-to-back cosmetic envelope dimension " + "at the marked reference level.", "depth" ), MeasurementSpec( "Joint or hinge centre to distal end", "Measure from the confirmed joint or hinge centreline to the " + "planned distal end of the cosmetic cover.", "joint_length" ), MeasurementSpec( "Component clearance", "Record the smallest specialist-verified clearance required " + "around a component, connector, hinge or release mechanism.", "clearance" ), MeasurementSpec( "OI no-contact radius", "Osseointegration projects only: enter the no-contact radius " + "specified or verified by the prosthetic specialist.", "oi_radius" ), MeasurementSpec( "OI access opening length", "Osseointegration projects only: record the specialist-agreed " + "inspection, cleaning and emergency-access opening length.", "oi_opening" ), MeasurementSpec( "OI access opening width", "Osseointegration projects only: record the specialist-agreed " + "inspection, cleaning and emergency-access opening width.", "oi_opening" ) ) private lateinit var projectName: EditText private lateinit var sideSpinner: Spinner private lateinit var levelSpinner: Spinner private lateinit var oiGroup: RadioGroup private lateinit var oiYes: RadioButton private lateinit var oiNo: RadioButton private lateinit var measurementSpinner: Spinner private lateinit var measurementValue: EditText private lateinit var measurementInstruction: TextView private lateinit var measurementSummary: TextView private lateinit var guideView: LimbMeasurementGuideView private lateinit var exclusionZones: EditText private lateinit var componentNotes: EditText private lateinit var specialistName: EditText private lateinit var clinicName: EditText private lateinit var reviewNotes: EditText private lateinit var photoCount: TextView private lateinit var cosmeticOnlyCheck: CheckBox private lateinit var specialistReviewCheck: CheckBox private lateinit var accessCheck: CheckBox private val measurements = linkedMapOf() private val photoUris = mutableListOf() private val photoPicker = registerForActivityResult( ActivityResultContracts.OpenDocument() ) { uri: Uri? -> if (uri == null) { return@registerForActivityResult } try { contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) } catch (_: SecurityException) { // The selected image remains usable for this session. } if (uri.toString() !in photoUris) { photoUris += uri.toString() } updatePhotoCount() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val root = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setBackgroundColor(LF_BACKGROUND) setOnApplyWindowInsetsListener { view, insets -> view.setPadding( 0, insets.systemWindowInsetTop, 0, insets.systemWindowInsetBottom ) insets } } root.addView(buildHeader()) val scroll = ScrollView(this).apply { isFillViewport = true } val content = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setPadding(dp(16), dp(12), dp(16), dp(32)) } content.addView(buildSafetyBanner()) content.addView(sectionTitle("PROJECT")) content.addView(buildProjectSection()) content.addView(sectionTitle("GUIDED MEASUREMENTS")) content.addView(buildMeasurementSection()) content.addView(sectionTitle("SAFETY AND COMPONENT ENVELOPE")) content.addView(buildSafetySection()) content.addView(sectionTitle("SPECIALIST REVIEW")) content.addView(buildSpecialistSection()) content.addView(sectionTitle("REFERENCE PHOTOS")) content.addView(buildPhotoSection()) content.addView(buildActions()) scroll.addView( content, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) ) root.addView( scroll, LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f ) ) setContentView(root) root.requestApplyInsets() updateMeasurementGuide() updateMeasurementSummary() updatePhotoCount() loadCurrentDraft() } private fun buildHeader(): View { return LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL setPadding(dp(16), dp(14), dp(16), dp(12)) setBackgroundColor(LF_SURFACE) addView( Button(this@LimbForgeMeasurementActivity).apply { text = "BACK" setOnClickListener { finish() } }, LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) ) addView( LinearLayout(this@LimbForgeMeasurementActivity).apply { orientation = LinearLayout.VERTICAL setPadding(dp(12), 0, dp(8), 0) addView(label( "LIMBFORGE", 22f, LF_TEXT, true )) addView(label( "MEASUREMENT STUDIO • LOCAL-FIRST", 10f, LF_MUTED, true )) }, LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) ) addView(label( "DRAFT", 11f, LF_AMBER, true ).apply { setPadding(dp(10), dp(7), dp(10), dp(7)) setBackgroundColor(0xFF3A3020.toInt()) }) } } private fun buildSafetyBanner(): View { return card().apply { addView(label( "SPECIALIST REVIEW REQUIRED", 15f, LF_AMBER, true )) addView(label( "This records geometry for a non-load-bearing cosmetic " + "cover. It is not a socket-fit assessment, medical " + "prescription or approval to manufacture.", 13f, LF_TEXT, false )) } } private fun buildProjectSection(): View { projectName = field( "Project name, for example Ray — right transtibial cover" ) sideSpinner = spinner( listOf("Right", "Left", "Bilateral", "Not applicable") ) levelSpinner = spinner( listOf( "Transtibial / below-knee", "Knee disarticulation", "Transfemoral / above-knee", "Hip disarticulation", "Upper limb", "Custom cosmetic cover" ) ) oiYes = RadioButton(this).apply { id = View.generateViewId() text = "Yes — osseointegration" setTextColor(LF_TEXT) } oiNo = RadioButton(this).apply { id = View.generateViewId() text = "No — conventional prosthesis" setTextColor(LF_TEXT) } oiGroup = RadioGroup(this).apply { orientation = RadioGroup.VERTICAL addView(oiYes) addView(oiNo) } return card().apply { addView(prompt("Project name")) addView(projectName) addView(prompt("Side")) addView(sideSpinner) addView(prompt("Amputation or cover class")) addView(levelSpinner) addView(prompt("Osseointegration status — required")) addView(oiGroup) } } private fun buildMeasurementSection(): View { measurementSpinner = spinner( measurementSpecs.map { it.name } ).apply { onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long ) { updateMeasurementGuide() } override fun onNothingSelected( parent: android.widget.AdapterView<*>? ) = Unit } } guideView = LimbMeasurementGuideView(this).apply { layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(260) ) } measurementInstruction = label( "", 13f, LF_TEXT, false ) measurementValue = field("Measurement in millimetres").apply { inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL } measurementSummary = label( "", 13f, LF_TEXT, false ) return card().apply { addView(prompt("Select measurement")) addView(measurementSpinner) addView(guideView) addView(measurementInstruction) addView(measurementValue) addView(Button(this@LimbForgeMeasurementActivity).apply { text = "ADD OR UPDATE MEASUREMENT" setOnClickListener { addMeasurement() } }) addView(prompt("Recorded measurements")) addView(measurementSummary) } } private fun buildSafetySection(): View { exclusionZones = multilineField( "Describe specialist-defined no-contact zones, stoma or " + "abutment exclusion, drainage, cleaning and inspection access." ) componentNotes = multilineField( "Record component envelope, hinges, release mechanisms, " + "cables, charging access and emergency-release requirements." ) cosmeticOnlyCheck = safetyCheck( "I understand this project is non-load-bearing cosmetic work only." ) specialistReviewCheck = safetyCheck( "Measurements must be reviewed by my prosthetic specialist." ) accessCheck = safetyCheck( "The cover must preserve cleaning, inspection and emergency access." ) return card().apply { addView(prompt("No-contact and exclusion zones")) addView(exclusionZones) addView(prompt("Hardware and component notes")) addView(componentNotes) addView(cosmeticOnlyCheck) addView(specialistReviewCheck) addView(accessCheck) } } private fun buildSpecialistSection(): View { specialistName = field("Specialist name") clinicName = field("Clinic or service") reviewNotes = multilineField( "Questions, corrections, specialist instructions or review outcome." ) return card().apply { addView(specialistName) addView(clinicName) addView(reviewNotes) } } private fun buildPhotoSection(): View { photoCount = label( "", 13f, LF_MUTED, false ) return card().apply { addView(label( "Attach reference photos of the prosthesis, component " + "envelope, marked reference lines or specialist sketches. " + "Avoid including unrelated identifying information.", 13f, LF_TEXT, false )) addView(Button(this@LimbForgeMeasurementActivity).apply { text = "ADD REFERENCE PHOTO" setOnClickListener { photoPicker.launch(arrayOf("image/*")) } }) addView(photoCount) } } private fun buildActions(): View { val container = card() container.addView(Button(this).apply { text = "SAVE MEASUREMENT DRAFT" setOnClickListener { saveDraft(showConfirmation = true) } }) container.addView(Button(this).apply { text = "SPECIALIST REVIEW SUMMARY" setOnClickListener { showReviewSummary() } }) container.addView(Button(this).apply { text = "SHARE SUMMARY" setOnClickListener { if (validateDraft()) { saveDraft(showConfirmation = false) shareSummary() } } }) container.addView(Button(this).apply { text = "OPEN LIVE LIMBFORGE AGENT" setOnClickListener { startActivity( Intent( this@LimbForgeMeasurementActivity, AgentPortalActivity::class.java ).apply { putExtra( AgentPortalActivity.EXTRA_AGENT_TITLE, "LimbForge Runtime" ) putExtra( AgentPortalActivity.EXTRA_AGENT_URL, BuildConfig.LIMBFORGE_BASE_URL ) } ) } }) return container } private fun addMeasurement() { val spec = selectedMeasurement() val raw = measurementValue.text.toString().trim() val value = raw.toDoubleOrNull() if (value == null || value <= 0.0) { Toast.makeText( this, "Enter a valid measurement greater than zero.", Toast.LENGTH_LONG ).show() return } measurements[spec.name] = formatMeasurement(value) measurementValue.setText("") updateMeasurementSummary() Toast.makeText( this, "${spec.name} recorded.", Toast.LENGTH_SHORT ).show() } private fun updateMeasurementGuide() { if (!this::measurementSpinner.isInitialized) { return } val spec = selectedMeasurement() measurementInstruction.text = spec.instruction guideView.setGuide(spec.visual) val existing = measurements[spec.name] measurementValue.setText(existing ?: "") } private fun selectedMeasurement(): MeasurementSpec { return measurementSpecs[ measurementSpinner.selectedItemPosition.coerceAtLeast(0) ] } private fun updateMeasurementSummary() { measurementSummary.text = if (measurements.isEmpty()) { "No measurements recorded yet." } else { measurements.entries.joinToString("\n") { "• ${it.key}: ${it.value} mm" } } } private fun updatePhotoCount() { if (this::photoCount.isInitialized) { photoCount.text = "${photoUris.size} reference photo(s) attached" } } private fun validateDraft(): Boolean { if (projectName.text.toString().trim().isEmpty()) { showValidation("Enter a project name.") return false } if (oiGroup.checkedRadioButtonId == -1) { showValidation( "Select whether this is an osseointegration project." ) return false } if (measurements.isEmpty()) { showValidation("Record at least one measurement.") return false } if ( oiYes.isChecked && exclusionZones.text.toString().trim().isEmpty() ) { showValidation( "Osseointegration projects require a specialist-defined " + "no-contact and access-zone record." ) return false } if ( !cosmeticOnlyCheck.isChecked || !specialistReviewCheck.isChecked || !accessCheck.isChecked ) { showValidation( "Confirm all three safety conditions before saving." ) return false } return true } private fun showValidation(message: String) { AlertDialog.Builder(this) .setTitle("Draft incomplete") .setMessage(message) .setPositiveButton("OK", null) .show() } private fun saveDraft(showConfirmation: Boolean) { if (!validateDraft()) { return } val directory = File(filesDir, "limbforge/projects").apply { mkdirs() } val payload = buildJson() val current = File(directory, "current-project.json") val snapshot = File( directory, "snapshot-${System.currentTimeMillis()}.json" ) current.writeText(payload.toString(2)) snapshot.writeText(payload.toString(2)) if (showConfirmation) { Toast.makeText( this, "LimbForge draft saved locally.", Toast.LENGTH_LONG ).show() } } private fun buildJson(): JSONObject { val measurementJson = JSONObject() measurements.forEach { (name, value) -> measurementJson.put(name, value) } val photos = JSONArray() photoUris.forEach(photos::put) return JSONObject().apply { put("schema", "shire.limbforge.mobile.measurement.v1") put("updated_at", timestamp()) put("project_name", projectName.text.toString().trim()) put("side", sideSpinner.selectedItem.toString()) put("cover_class", levelSpinner.selectedItem.toString()) put("osseointegration", oiYes.isChecked) put("measurements_mm", measurementJson) put("exclusion_zones", exclusionZones.text.toString().trim()) put("component_notes", componentNotes.text.toString().trim()) put("specialist_name", specialistName.text.toString().trim()) put("clinic", clinicName.text.toString().trim()) put("review_notes", reviewNotes.text.toString().trim()) put("reference_photos", photos) put("specialist_review_required", true) put("non_load_bearing_cosmetic_only", true) } } private fun loadCurrentDraft() { val file = File( filesDir, "limbforge/projects/current-project.json" ) if (!file.exists()) { return } try { val payload = JSONObject(file.readText()) projectName.setText( payload.optString("project_name") ) selectSpinner( sideSpinner, payload.optString("side") ) selectSpinner( levelSpinner, payload.optString("cover_class") ) if (payload.optBoolean("osseointegration", false)) { oiYes.isChecked = true } else { oiNo.isChecked = true } val storedMeasurements = payload.optJSONObject("measurements_mm") if (storedMeasurements != null) { val keys = storedMeasurements.keys() while (keys.hasNext()) { val key = keys.next() measurements[key] = storedMeasurements.optString(key) } } exclusionZones.setText( payload.optString("exclusion_zones") ) componentNotes.setText( payload.optString("component_notes") ) specialistName.setText( payload.optString("specialist_name") ) clinicName.setText( payload.optString("clinic") ) reviewNotes.setText( payload.optString("review_notes") ) val photos = payload.optJSONArray("reference_photos") if (photos != null) { for (index in 0 until photos.length()) { val item = photos.optString(index) if (item.isNotBlank()) { photoUris += item } } } cosmeticOnlyCheck.isChecked = true specialistReviewCheck.isChecked = true accessCheck.isChecked = true updateMeasurementSummary() updatePhotoCount() updateMeasurementGuide() } catch (_: Exception) { Toast.makeText( this, "The previous LimbForge draft could not be loaded.", Toast.LENGTH_LONG ).show() } } private fun showReviewSummary() { if (!validateDraft()) { return } saveDraft(showConfirmation = false) AlertDialog.Builder(this) .setTitle("Specialist review summary") .setMessage(buildSummary()) .setNegativeButton("CLOSE", null) .setPositiveButton("SHARE") { _, _ -> shareSummary() } .show() } private fun shareSummary() { val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra( Intent.EXTRA_SUBJECT, "LimbForge measurement draft" ) putExtra(Intent.EXTRA_TEXT, buildSummary()) } startActivity( Intent.createChooser( shareIntent, "Share LimbForge specialist summary" ) ) } private fun buildSummary(): String { val measurementText = if (measurements.isEmpty()) { "None recorded" } else { measurements.entries.joinToString("\n") { "- ${it.key}: ${it.value} mm" } } val oiStatus = if (oiYes.isChecked) "YES" else "NO" return """ LIMBFORGE MEASUREMENT DRAFT SPECIALIST REVIEW REQUIRED Project: ${projectName.text.toString().trim()} Side: ${sideSpinner.selectedItem} Cover class: ${levelSpinner.selectedItem} Osseointegration: $oiStatus MEASUREMENTS $measurementText NO-CONTACT / EXCLUSION ZONES ${valueOrNone(exclusionZones)} HARDWARE / COMPONENT NOTES ${valueOrNone(componentNotes)} SPECIALIST Name: ${valueOrNone(specialistName)} Clinic: ${valueOrNone(clinicName)} REVIEW NOTES ${valueOrNone(reviewNotes)} Reference photos attached: ${photoUris.size} SAFETY STATUS - Non-load-bearing cosmetic cover only - Not a socket-fit assessment - Cleaning, inspection and emergency access must remain available - No design or manufacture approval until specialist review Generated: ${timestamp()} """.trimIndent() } private fun valueOrNone(field: EditText): String { return field.text.toString().trim().ifEmpty { "Not recorded" } } private fun timestamp(): String { return SimpleDateFormat( "yyyy-MM-dd HH:mm:ss Z", Locale.getDefault() ).format(Date()) } private fun formatMeasurement(value: Double): String { return if (value % 1.0 == 0.0) { value.toInt().toString() } else { String.format(Locale.US, "%.1f", value) } } private fun selectSpinner(spinner: Spinner, value: String) { for (index in 0 until spinner.count) { if (spinner.getItemAtPosition(index).toString() == value) { spinner.setSelection(index) return } } } private fun sectionTitle(text: String): View { return label(text, 13f, LF_GREEN, true).apply { setPadding(0, dp(22), 0, dp(8)) } } private fun card(): LinearLayout { return LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setPadding(dp(16), dp(16), dp(16), dp(16)) setBackgroundColor(LF_SURFACE) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { bottomMargin = dp(12) } } } private fun prompt(text: String): TextView { return label(text, 12f, LF_MUTED, true).apply { setPadding(0, dp(10), 0, dp(5)) } } private fun label( text: String, size: Float, colour: Int, bold: Boolean ): TextView { return TextView(this).apply { this.text = text textSize = size setTextColor(colour) if (bold) { setTypeface( typeface, android.graphics.Typeface.BOLD ) } } } private fun field(hint: String): EditText { return EditText(this).apply { this.hint = hint setHintTextColor(LF_MUTED) setTextColor(LF_TEXT) setBackgroundColor(LF_RAISED) setPadding(dp(12), dp(12), dp(12), dp(12)) inputType = InputType.TYPE_CLASS_TEXT layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { bottomMargin = dp(8) } } } private fun multilineField(hint: String): EditText { return field(hint).apply { minLines = 4 gravity = Gravity.TOP inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE } } private fun spinner(items: List): Spinner { return Spinner(this).apply { adapter = ArrayAdapter( this@LimbForgeMeasurementActivity, android.R.layout.simple_spinner_dropdown_item, items ) setBackgroundColor(LF_RAISED) } } private fun safetyCheck(text: String): CheckBox { return CheckBox(this).apply { this.text = text setTextColor(LF_TEXT) } } private fun dp(value: Int): Int { return ( value * resources.displayMetrics.density ).toInt() } } private class LimbMeasurementGuideView( context: android.content.Context ) : View(context) { private val outline = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = LF_MUTED style = Paint.Style.STROKE strokeWidth = 4f } private val highlight = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = LF_GREEN style = Paint.Style.STROKE strokeWidth = 8f strokeCap = Paint.Cap.ROUND } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = LF_TEXT textSize = 34f textAlign = Paint.Align.CENTER } private var guide = "length" fun setGuide(value: String) { guide = value invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawColor(LF_RAISED) val centre = width / 2f val top = height * 0.14f val bottom = height * 0.84f val envelope = RectF( centre - width * 0.18f, top, centre + width * 0.18f, bottom ) canvas.drawRoundRect( envelope, width * 0.12f, width * 0.12f, outline ) when (guide) { "length", "joint_length" -> { canvas.drawLine( centre + width * 0.27f, top, centre + width * 0.27f, bottom, highlight ) } "circumference_high" -> { drawBand(canvas, centre, height * 0.30f) } "circumference_mid" -> { drawBand(canvas, centre, height * 0.49f) } "circumference_low" -> { drawBand(canvas, centre, height * 0.68f) } "width" -> { canvas.drawLine( envelope.left, height * 0.49f, envelope.right, height * 0.49f, highlight ) } "depth" -> { canvas.drawOval( RectF( centre - width * 0.13f, height * 0.42f, centre + width * 0.13f, height * 0.56f ), highlight ) } "clearance" -> { canvas.drawRoundRect( RectF( envelope.left - width * 0.05f, envelope.top - height * 0.04f, envelope.right + width * 0.05f, envelope.bottom + height * 0.04f ), width * 0.14f, width * 0.14f, highlight ) } "oi_radius" -> { canvas.drawCircle( centre, height * 0.62f, width * 0.105f, highlight ) } "oi_opening" -> { canvas.drawRoundRect( RectF( centre - width * 0.11f, height * 0.54f, centre + width * 0.11f, height * 0.72f ), width * 0.04f, width * 0.04f, highlight ) } } canvas.drawText( "Highlighted line = current measurement", centre, height * 0.94f, textPaint ) } private fun drawBand( canvas: Canvas, centre: Float, y: Float ) { canvas.drawOval( RectF( centre - width * 0.22f, y - height * 0.045f, centre + width * 0.22f, y + height * 0.045f ), highlight ) } }