Untitled

 avatar
unknown
plain_text
9 months ago
39 kB
11
Indexable
package com.example.testcanva

import android.app.Activity
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream


@Suppress("Deprecation")
class Template27(
    activity: Activity, cv: Cv, applicationScope: CoroutineScope
) {
    init {
        createTemplate(activity, cv, applicationScope)
    }

    private fun createTemplate(activity: Activity, cv: Cv, scope: CoroutineScope) {
        scope.launch(Dispatchers.IO) {
            val pdf = PdfDocument()
            val pageWidth = 700
            val pageHeight = 1000

            // Keep your current page size for consistency with other templates
            val pageInfo = PdfDocument.PageInfo.Builder(700, 1000, 1).create()
            val page = pdf.startPage(pageInfo)
            val c = page.canvas
            c.drawColor(Color.WHITE)

            // ----------------  Point  --------------------
            var xCoordinate = 30f
            var yCoordinate = 10f
            val hGap = 10f

            val card1Width = pageWidth - 250f
            val card1Height = 120f
            val strokeWidth: Float = 2f


            // ----------------  Text  --------------------
            val regular = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
            val medium =
                Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)   // use bold as medium stand-in
            val bold = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
            val italic = Typeface.create(Typeface.SANS_SERIF, Typeface.ITALIC)

            // ----------------  Typography --------------------
            val TITLE = 20f          // "Liam Parker"
            val SUBTITLE = 16f       // "Senior Web Developer"
            val H1 = 18f             // Section titles
            val H2 = 14f             // Item titles (company, role)
            val H3 = 12f             // Minor headings / labels
            val BODY = 11f           // Paragraphs, dates
            val SMALL = 9f           // Chips, meta text

            // ----------------  Color  --------------------
            val INK = "#0F172A".toColorInt()   // main text on light
            val SUB_INK = "#5D5959".toColorInt()   // secondary text
            val PANEL_BG = "#2C3943".toColorInt()   // right dark card
            val PANEL_INK = "#E5F2F0".toColorInt()   // text over dark
            val ACCENT = "#32C5B6".toColorInt()   // teal accent (icons, bullets, bars)
            val ACCENT_DARK = "#1A9286".toColorInt()
            val BULLET = ACCENT                   // left bullets
            val LINK = "#0EA5E9".toColorInt()
            val RULE = "#4F5755".toColorInt()
            val MUTED = "#6B7280".toColorInt()
            val LINE_LIGHT = "#E5E7EB".toColorInt()
            val CHIP_STROKE = "#94A3B8".toColorInt()
            val sidebarBackground = "#E5E5E5".toColorInt()
            val primaryClr = "#283034".toColorInt()

            // ----------------  Paint  --------------------
            fun createTextPaint(
                color: Int, size: Float, typeface: Typeface, isAntiAlias: Boolean = true
            ): TextPaint {
                return TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                    this.color = color
                    this.textSize = size
                    this.typeface = typeface
                    this.isAntiAlias = isAntiAlias
                }
            }

            fun createStrokePaint(
                color: Int,
                strokeWidth: Float,
                style: Paint.Style = Paint.Style.STROKE,
                isAntiAlias: Boolean = true
            ): Paint {
                return Paint(Paint.ANTI_ALIAS_FLAG).apply {
                    this.color = color
                    this.style = style
                    this.strokeWidth = strokeWidth
                    this.isAntiAlias = isAntiAlias
                }
            }


            val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                color = BULLET; style = Paint.Style.FILL
            }


            // ----------------  Drawing  --------------------
            yCoordinate = c.drawNameHeader(
                rect = RectF(xCoordinate, yCoordinate, card1Width, card1Height),
                cornerRadius = 12f,
                barWidth = 6f,
                barCornerRadius = 6f,
                backgroundColor = sidebarBackground,
                barColor = primaryClr,
                name = "${cv.personalInfo.firstName} ${cv.personalInfo.lastName}",
                role = cv.personalInfo.profession,
                namePaint = createTextPaint(
                    color = INK, size = TITLE, typeface = bold, isAntiAlias = true
                ),
                rolePaint = createTextPaint(
                    color = BULLET, size = SUBTITLE, typeface = bold, isAntiAlias = true
                ),
            )

            if (cv.academicRecords.size >= 1) {
                yCoordinate += 10f
                xCoordinate -= 3f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "ACADEMIC RECORD",
                    iconRes = R.drawable.ic_academic_record,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 10f
                yCoordinate += 10f
                yCoordinate = c.drawAcademicRecords(
                    context = activity,
                    width = 360,
                    records = cv.academicRecords,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    uniPaint = createTextPaint(INK, H2, bold, true),
                    degreePaint = createTextPaint(INK, H3, regular, true),
                    marksPaint = createTextPaint(BULLET, H3, regular, true),
                    datePaint = createTextPaint(BULLET, SMALL, italic, true),
                    dotPaint = dotPaint
                )
            }

            if (cv.labourExperience.jobRole.isNotEmpty()) {
                xCoordinate -= 10f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "EXPERIENCE",
                    iconRes = R.drawable.ic_experience, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                yCoordinate += 10f
                xCoordinate += 10f
                yCoordinate = c.drawProfessionalExperiences(
                    context = activity,
                    records = arrayListOf(cv.labourExperience),
                    startX = xCoordinate,
                    startY = yCoordinate,
                    dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                        color = BULLET
                        style = Paint.Style.FILL
                    },
                    uniPaint = createTextPaint(INK, 16f, bold, true),
                    degreePaint = createTextPaint(INK, 14f, regular, true),
                    datePaint = createTextPaint(ACCENT, 10f, regular, true),
                    marksPaint = createTextPaint(INK, 10f, regular, true),
                )
            }

            if (cv.achievements.size >= 1) {
                xCoordinate -= 10f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "ACHIEVEMENTS",
                    iconRes = R.drawable.ic_achievements, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 25f
                yCoordinate += 10f
                yCoordinate = c.drawBulletList(
                    items = cv.achievements,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    lineGap = 20f,
                    maxWidthPx = 250f

                )
            }

            if (cv.personalInfo.socialMediaLink.isNotEmpty()) {
                xCoordinate -= 25f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "SOCIAL LINKS",
                    iconRes = R.drawable.ic_template_08_linkedin, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 25f
                yCoordinate += 10f
                val arr: ArrayList<AdditionalField> = arrayListOf(
                    AdditionalField(
                        field = cv.personalInfo.socialMediaLink
                    )
                )
                c.drawBulletList(
                    items = arr,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    lineGap = 20f,
                    maxWidthPx = 250f

                )
            }

            xCoordinate = 470f
            yCoordinate = 10f

            var sidebarX = xCoordinate
            val sidebarTopY = yCoordinate

            val strokePaint = createStrokePaint(
                color = ACCENT, strokeWidth = 2f,
                style = Paint.Style.STROKE, isAntiAlias = true
            )
            val textPaint = createTextPaint(ACCENT, SMALL, regular, true)


// 1) Collect rows (non-empty only)
            val rows = mutableListOf<Pair<String, Int>>().apply {
                if (cv.personalInfo.phone.isNotEmpty()) add(cv.personalInfo.phone to R.drawable.ic_phone)
                if (cv.personalInfo.email.isNotEmpty()) add(cv.personalInfo.email to R.drawable.ic_email)
                if (cv.personalInfo.dateOfBirth.isNotEmpty()) add(cv.personalInfo.dateOfBirth to R.drawable.dob)
                if (cv.personalInfo.address.isNotEmpty()) add(cv.personalInfo.address to R.drawable.ic_contact_address)
                if (cv.personalInfo.gender.name.isNotEmpty()) add(cv.personalInfo.gender.name to R.drawable.ic_gender)
                if (cv.personalInfo.nationality.isNotEmpty()) add(cv.personalInfo.nationality to R.drawable.ic_nationality)
                if (cv.personalInfo.maritalStatus.name.isNotEmpty())
                    add(cv.personalInfo.maritalStatus.name to R.drawable.ic_contact_martial_status)
            }

// If nothing to draw, skip
            if (rows.isNotEmpty()) {

                // 2) Measure total height (matches drawCircledIconHeader geometry)
                val rowTopGap = 10f                 // you add +10f before each row
                val circleRadius = 14f
                val circleDia = circleRadius * 2f
                val fixedTextWidthPx = 165          // same as drawCircledIconHeader
                val padTop = 12f
                val padBottom = 12f
                val sidebarWidth = 220f
                val cornerRadius = 20f

                val measurePaint = createTextPaint(ACCENT, SMALL, regular, true)

                var measured = 0f
                rows.forEach { (text, _) ->
                    measured += rowTopGap

                    val textLayout =
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            StaticLayout.Builder
                                .obtain(text, 0, text.length, measurePaint, fixedTextWidthPx)
                                .setAlignment(Layout.Alignment.ALIGN_NORMAL)
                                .setIncludePad(false)
                                .build()
                        } else {
                            StaticLayout(
                                text,
                                measurePaint,
                                fixedTextWidthPx,
                                Layout.Alignment.ALIGN_NORMAL,
                                1f,
                                0f,
                                false
                            )
                        }

                    // each row height = max(circle diameter, text layout height)
                    measured += maxOf(circleDia, textLayout.height.toFloat())
                }

                val sidebarHeight = padTop + measured + padBottom

                // 3) Draw background FIRST (no PorterDuff, so it actually renders)
                c.drawSidebar(
                    startX = sidebarX,
                    startY = sidebarTopY,
                    width = sidebarWidth,
                    height = sidebarHeight,
                    cornerRadius = cornerRadius,
                    backgroundColor = "#2E3C46".toColorInt()
                )

                yCoordinate = sidebarTopY // reset to top of sidebar
                sidebarX += +10f
                rows.forEach { (text, iconRes) ->
                    yCoordinate += rowTopGap
                    yCoordinate = c.drawCircledIconHeader(
                        context = activity,
                        text = text,
                        iconRes = iconRes,
                        startX = sidebarX,
                        startY = yCoordinate,
                        strokePaint = strokePaint,
                        h1Paint = textPaint
                    )
                }
            }

            val startY = yCoordinate + 40f
            c.drawSidebar(
                startX = 465f,
                startY = startY,
                width = 220f,
                height = 700f,
                cornerRadius = 0f,
                backgroundColor = "#ECEDEF".toColorInt()
            )


            if (cv.skills.size >= 1) {
                xCoordinate += 10f
                yCoordinate = 350f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "SKILLS",
                    iconRes = R.drawable.ic_skills, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 23f
                yCoordinate += 10f
                yCoordinate = c.drawBulletList(
                    items = cv.skills,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    maxWidthPx = 170f,
                    lineGap = 10f
                )
            }

            if (cv.interests.size >= 1) {
                xCoordinate -= 23f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "INTERESTS",
                    iconRes = R.drawable.ic_interests, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 23f
                yCoordinate += 10f
                yCoordinate = c.drawBulletList(
                    items = cv.interests,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    maxWidthPx = 170f,
                    lineGap = 10f
                )
            }

            if (cv.hobbies.size >= 1) {
                xCoordinate -= 23f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "HOBBIES",
                    iconRes = R.drawable.ic_interests, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 23f
                yCoordinate += 10f
                yCoordinate = c.drawBulletList(
                    items = cv.hobbies,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    maxWidthPx = 170f,
                    lineGap = 10f
                )
            }

            if (cv.hobbies.size >= 1) {
                xCoordinate -= 23f
                yCoordinate = c.drawCircledIconHeader(
                    context = activity,
                    text = "LANGUAGES",
                    iconRes = R.drawable.ic_languages, // your cap icon
                    startX = xCoordinate,
                    startY = yCoordinate,
                    strokePaint = createStrokePaint(
                        color = INK,
                        strokeWidth = 2f,
                        style = Paint.Style.STROKE,
                        isAntiAlias = true
                    ),
                    h1Paint = createTextPaint(INK, H1, bold, true)
                )

                xCoordinate += 23f
                yCoordinate += 10f
                yCoordinate = c.drawBulletList(
                    items = cv.languages,
                    startX = xCoordinate,
                    startY = yCoordinate,
                    bulletColor = BULLET,
                    maxWidthPx = 170f,
                    lineGap = 10f
                )
            }


            // Done
            pdf.finishPage(page)

            val dir = File(activity.filesDir.absolutePath, "Offline CV Maker")
            dir.mkdirs()
            val file = File(dir, "${cv.personalInfo.firstName} ${cv.personalInfo.lastName}.pdf")
            try {
                withContext(Dispatchers.IO) {
                    pdf.writeTo(FileOutputStream(file))
                    withContext(Dispatchers.Main) { Util.isTemplateGenerated.postValue(true) }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    Toast.makeText(activity, "oops_something_went_wrong", Toast.LENGTH_SHORT).show()
                }
                e.printStackTrace()
            } finally {
                pdf.close()
            }
        }

    }
}


// Helper __________________________________________________________________________________________

private fun Canvas.drawBulletList(
    items: ArrayList<AdditionalField>,
    startX: Float,
    startY: Float,
    bulletColor: Int = "#EF4444".toColorInt(),
    textColor: Int = Color.BLACK,
    textSize: Float = 14f,
    bulletRadius: Float = 3f,
    lineGap: Float = 18f,          // gap between items
    limitTextSize: Int = 60,       // soft char cap per item ("rest to 60")
    maxWidthPx: Float = 320f,      // available width for the bullet text
    innerLineGap: Float = 2f       // small gap between the wrapped lines
): Float {
    var y = startY

    val bulletPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = bulletColor
        style = Paint.Style.FILL
    }
    val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
        color = textColor
        this.textSize = textSize
        typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
    }

    fun ellipsizeToWidth(text: String, paint: TextPaint, maxWidth: Float): String {
        if (!maxWidth.isFinite() || maxWidth <= 0f) return text
        if (paint.measureText(text) <= maxWidth) return text
        val ellipsis = "…"
        val ellW = paint.measureText(ellipsis)
        var lo = 0;
        var hi = text.length;
        var best = 0
        while (lo <= hi) {
            val mid = (lo + hi) ushr 1
            val w = paint.measureText(text, 0, mid)
            if (w + ellW <= maxWidth) {
                best = mid; lo = mid + 1
            } else hi = mid - 1
        }
        return text.substring(0, best).trimEnd() + ellipsis
    }

    /** Up to 2 lines; second line ellipsized if still overflowing. */
    fun split2(text: String, paint: TextPaint, maxWidth: Float, cap: Int): List<String> {
        val capped = if (text.length > cap) text.substring(0, cap) else text
        if (!maxWidth.isFinite() || maxWidth <= 0f) return listOf(capped)

        val c1 = paint.breakText(capped, true, maxWidth, null)
        if (c1 >= capped.length) return listOf(capped)

        val l1 = capped.substring(0, c1).trimEnd()
        val rest = capped.substring(c1).trimStart()
        val c2 = paint.breakText(rest, true, maxWidth, null)
        val l2 = if (c2 >= rest.length) rest else ellipsizeToWidth(rest, paint, maxWidth)
        return listOf(l1, l2)
    }

    val fm = textPaint.fontMetrics
    val lineH = (fm.descent - fm.ascent)

    items.forEach { item ->
        val raw = item.field
        if (raw.isNotBlank()) {
            val lines = split2(raw, textPaint, maxWidthPx, limitTextSize)

            // Baseline for line 1
            val baseline1 = y - fm.ascent

            // Bullet centered to line 1
            val bulletY = baseline1 + (fm.ascent + fm.descent) / 2f
            drawCircle(startX - 10f, bulletY, bulletRadius, bulletPaint)

            // Draw line 1
            drawText(lines[0], startX, baseline1, textPaint)

            var blockBottom = baseline1 + fm.descent

            // Draw line 2 if present
            if (lines.size > 1) {
                val baseline2 = baseline1 + lineH + innerLineGap
                drawText(lines[1], startX, baseline2, textPaint)
                blockBottom = baseline2 + fm.descent
            }

            // Advance to next item
            y = blockBottom + lineGap
        }
    }

    return y
}


fun Canvas.drawSidebar(
    startX: Float = 0f,
    startY: Float = 0f,
    width: Float = 443f,
    height: Float,
    cornerRadius: Float = 40f,
    backgroundColor: Int = Color.parseColor("#2E3C46"),
    circleColor: Int = Color.parseColor("#10B7A7"),
    circleRadius: Float = 12f,
    strokeColor: Int = Color.WHITE,
    strokeWidth: Float = 4f,
    circleOffset: Float = 10f // positive => circle extends below the sidebar bottom
): Float {
    // === Sidebar shape ===
    val rect = RectF(startX, startY, startX + width, startY + height)
    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = backgroundColor
        style = Paint.Style.FILL
    }
    val path = Path().apply {
        val radii = floatArrayOf(
            0f, 0f,                     // top-left (square)
            cornerRadius, cornerRadius, // top-right
            cornerRadius, cornerRadius, // bottom-right
            0f, 0f                      // bottom-left (square)
        )
        addRoundRect(rect, radii, Path.Direction.CW)
    }
    drawPath(path, paint)

    // === Circle (attached near bottom-left edge) ===
    val cx = startX
    val cy = startY + height - circleRadius + circleOffset

    val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = circleColor
        style = Paint.Style.FILL
    }
    drawCircle(cx, cy, circleRadius, circlePaint)

    // Stroke ring (outer radius remains circleRadius)
    val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = strokeColor
        style = Paint.Style.STROKE
        this.strokeWidth = strokeWidth
    }
    drawCircle(cx, cy, circleRadius - strokeWidth / 2f, strokePaint)

    // --- Return the next free Y (visual bottom) ---
    // If circleOffset > 0, the circle extends below the rect by exactly circleOffset.
    val visualBottom = startY + height + maxOf(0f, circleOffset)
    return visualBottom
}


@Suppress("DEPRECATION")
private fun Canvas.drawProfessionalExperiences(
    context: Context,
    records: ArrayList<LaborExperience>,
    startX: Float,
    startY: Float,
    uniPaint: TextPaint,      // title paint (bold)
    degreePaint: TextPaint,   // company paint
    datePaint: TextPaint,     // date paint (accent)
    marksPaint: TextPaint,    // location paint
    dotPaint: Paint           // bullet
): Float {

    // --- layout constants (tweak if needed) ---
    val rightEdgeX = 410f
    val dotRadius = 3f
    val gapAfterDot = 10f
    val gapLeftTextVsDate = 12f
    val lineGap = 4f
    val rowGap = 18f

    // local builder (inline, no helper fn asked earlier)
    fun makeLayout(
        text: CharSequence,
        p: TextPaint,
        width: Int,
        align: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL
    ): StaticLayout {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            StaticLayout.Builder.obtain(text, 0, text.length, p, width).setAlignment(align)
                .setIncludePad(false).setLineSpacing(0f, 1f).build()
        } else {
            StaticLayout(text, p, width, align, 1f, 0f, false)
        }
    }

    var y = startY
    val bulletX = startX + dotRadius
    val textX = startX + dotRadius * 2 + gapAfterDot

    for (rec in records) {
        // --- derive strings safely ---
        val title = (rec.jobRole ?: "").trim()
        val company = (rec.companyName ?: "").trim()
        val location =
            listOfNotNull(rec.city?.trim(), rec.country?.trim()).filter { it.isNotEmpty() }
                .joinToString(", ")
        val dateText = buildString {
            append(rec.joiningDate ?: "")
            append(" – ")
            append(if (rec.isAdded == true) "Present" else (rec.leavingDate ?: ""))
        }.trim()

        // --- date layout (right column) ---
        val maxRightWidth = (rightEdgeX - textX).toInt().coerceAtLeast(0)
        val dateLayout =
            makeLayout(dateText, datePaint, maxRightWidth, Layout.Alignment.ALIGN_OPPOSITE)
        val dateW = dateLayout.width

        // --- available width for left block after reserving date space ---
        val leftW = (rightEdgeX - textX - dateW - gapLeftTextVsDate).coerceAtLeast(0f).toInt()

        // --- build left layouts ---
        val titleLayout = makeLayout(title, uniPaint, 200)
        val companyLayout = makeLayout(company, degreePaint, 240)
        val locationLayout = makeLayout(location, marksPaint, 240)

        // --- bullet aligned to first line baseline of the title block ---
        val firstBaseline = titleLayout.getLineBaseline(0).toFloat()
        val bulletCenterY =
            y + firstBaseline - uniPaint.ascent() * 0.65f  // visually centered near first line
        drawCircle(bulletX, bulletCenterY, dotRadius, dotPaint)

        // --- draw date (right aligned) ---
        save()
        translate(rightEdgeX - dateW, y)
        dateLayout.draw(this)
        restore()

        // --- draw left stacked text ---
        save(); translate(textX, y); titleLayout.draw(this); restore()
        var innerY = y + titleLayout.height + lineGap
        save(); translate(textX, innerY); companyLayout.draw(this); restore()
        innerY += companyLayout.height + lineGap
        save(); translate(textX, innerY); locationLayout.draw(this); restore()
        innerY += locationLayout.height

        // --- compute total row height and advance ---
        val textBlockHeight =
            titleLayout.height + lineGap + companyLayout.height + lineGap + locationLayout.height
        val rowHeight = maxOf(textBlockHeight, dateLayout.height.toFloat())
        y += rowHeight + rowGap
    }

    return y
}


@Suppress("DEPRECATION")
private fun Canvas.drawAcademicRecords(
    context: Context,
    records: List<AcademicRecord>,
    width: Int,                 // total available text width
    startX: Float,
    startY: Float,
    uniPaint: TextPaint,        // institute text style
    degreePaint: TextPaint,     // degree text style
    marksPaint: TextPaint,      // marks text style
    datePaint: TextPaint,       // date text style
    dotPaint: Paint,            // dot style (fill)
    marksColWidth: Float = 70f, // fixed right column for marks
    gapDegreeToMarks: Float = 8f
): Float {

    // --- constants ---
    val dotRadius = 3f
    val gapAfterDot = 10f
    val lineGap = 3f
    val rowGap = 16f

    var yTop = startY
    val dotX = startX
    val textX = startX + gapAfterDot

    records.forEach { rec ->
        // --- date text ---
        val dateText = buildString {
            append(rec.startDate)
            append(" – ")
            append(if (rec.currentlyStudying || rec.endDate.isBlank()) "Present" else rec.endDate)
        }

        // ===== A) INSTITUTE =====
        val instLayout = StaticLayout(
            rec.instituteName, uniPaint, width, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
        )

        val instFm = uniPaint.fontMetrics
        val firstLineH = (instFm.descent - instFm.ascent)
        val cy = yTop + firstLineH / 2f
        drawCircle(dotX, cy, dotRadius, dotPaint)

        save()
        translate(textX, yTop)
        instLayout.draw(this)
        restore()

        yTop += instLayout.height + lineGap

        // ===== B) DEGREE + MARKS =====
        val hasMarks = rec.marks.isNotBlank() && !rec.marks.equals("NA", ignoreCase = true)
        val degreeAvailW =
            if (hasMarks) (width - marksColWidth - gapDegreeToMarks).coerceAtLeast(10f) else width.toFloat()

        // --- degree layout ---
        val degreeLayout = StaticLayout(
            rec.degreeName,
            degreePaint,
            degreeAvailW.toInt(),
            Layout.Alignment.ALIGN_NORMAL,
            1f,
            0f,
            false
        )

        // --- draw degree text ---
        save()
        translate(textX, yTop)
        degreeLayout.draw(this)
        restore()

        // --- draw marks with StaticLayout (right aligned inside fixed column) ---
        if (hasMarks) {
            val lastLine = (degreeLayout.lineCount - 1).coerceAtLeast(0)
            val lastBaseline = yTop + degreeLayout.getLineBaseline(lastLine)

            val colRight = textX + width
            val marksLeft = colRight - marksColWidth

            val marksLayout = StaticLayout(
                rec.marks.trim(),
                marksPaint,
                marksColWidth.toInt(),
                Layout.Alignment.ALIGN_OPPOSITE, // right-align within column
                1f,
                0f,
                false
            )

            // Align vertically with last degree line
            val marksY = lastBaseline - marksLayout.height + marksPaint.textSize
            save()
            translate(marksLeft, marksY)
            marksLayout.draw(this)
            restore()
        }

        yTop += degreeLayout.height + lineGap

        // ===== C) DATE =====
        val dfm = datePaint.fontMetrics
        val dateBaseline = yTop - dfm.ascent
        drawText(dateText, textX, dateBaseline, datePaint)

        yTop = dateBaseline + dfm.descent + rowGap
    }

    return yTop
}


@Suppress("DEPRECATION")
fun Canvas.drawCircledIconHeader(
    context: Context,
    text: String,
    iconRes: Int,
    startX: Float,
    startY: Float,
    strokePaint: Paint,     // you configure: color, strokeWidth, etc.
    h1Paint: TextPaint      // you configure: size, bold, color, etc.
): Float {

    // ---- simple constants (tweak here if needed) ----
    val circleRadius = 14f
    val gapBetweenIconAndText = 10f
    val iconScale = 0.74f
    val fixedTextWidthPx = 165
    val strokeWidth = if (strokePaint.strokeWidth > 0f) strokePaint.strokeWidth else 2f

    // Ensure stroke style for circle outline
    val oldStyle = strokePaint.style
    strokePaint.style = Paint.Style.STROKE

    // ---- Circle geometry (from top-left) ----
    val cx = startX + circleRadius
    val cy = startY + circleRadius
    val strokeInnerRadius = (circleRadius - strokeWidth / 2f).coerceAtLeast(0f)
    drawCircle(cx, cy, strokeInnerRadius, strokePaint)

    // ---- Icon (centered inside the stroked circle) ----
    ContextCompat.getDrawable(context, iconRes)?.let { dr ->
        // Use stroke color for icon tint by default
        dr.setTint(strokePaint.color)

        val innerRadius = (circleRadius - strokeWidth).coerceAtLeast(0f)
        val iconRadius = (innerRadius * iconScale).coerceAtLeast(0f)

        val left = (cx - iconRadius).toInt()
        val top = (cy - iconRadius).toInt()
        val right = (cx + iconRadius).toInt()
        val bottom = (cy + iconRadius).toInt()

        dr.setBounds(left, top + 2, right, bottom) // slight down-shift for optical centering
        dr.draw(this)
    }

    // ---- Text block (StaticLayout with fixed width) ----
    val textX = startX + (circleRadius * 2f) + gapBetweenIconAndText
    val layout = StaticLayout(
        text, h1Paint, fixedTextWidthPx, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
    )

    save()
    translate(textX, startY + 5f)
    layout.draw(this)
    restore()

    // ---- Compute next Y ----
    val circleBottom = startY + (circleRadius * 2f)
    val textBottom = startY + layout.height

    // Restore original style (in case caller reuses paint)
    strokePaint.style = oldStyle

    return maxOf(circleBottom, textBottom)
}

@Suppress("DEPRECATION")
fun Canvas.drawNameHeader(
    name: String,
    role: String,
    namePaint: TextPaint,
    rolePaint: TextPaint,
    rect: RectF,
    cornerRadius: Float = 12f,
    barWidth: Float = 6f,
    barCornerRadius: Float = 6f,
    backgroundColor: Int,
    barColor: Int,
    horizontalPadding: Float = 18f,
    verticalPadding: Float = 14f,
    barGap: Float = 4f,           // kept and used
): Float {

    // --- Fixed text width inside the card (simple & consistent) ---
    val textLeft = rect.left + horizontalPadding
    val textWidth = (rect.right - horizontalPadding - textLeft).toInt().coerceAtLeast(1)

    // --- Build layouts (no ellipsize/measure loops, just draw what fits) ---
    val nameLayout = StaticLayout(
        name, namePaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
    )

    val roleLayout = StaticLayout(
        role, rolePaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
    )

    // --- Compute total height (padding + two blocks + padding) ---
    val contentHeight = verticalPadding + nameLayout.height + roleLayout.height + verticalPadding

    // --- Card rect (rounded only on top corners) ---
    val card = RectF(rect.left, rect.top, rect.right, rect.top + contentHeight)
    val radii = floatArrayOf(
        cornerRadius, cornerRadius,   // top-left
        cornerRadius, cornerRadius,   // top-right
        0f, 0f,                       // bottom-right
        0f, 0f                        // bottom-left
    )
    val path = Path().apply { addRoundRect(card, radii, Path.Direction.CW) }
    val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = backgroundColor
        style = Paint.Style.FILL
    }
    drawPath(path, bgPaint)

    // --- Vertical bar (simple rounded rect) ---
    val bar = RectF(
        card.left - (barWidth + barGap), card.top, card.left - barGap, card.bottom
    )
    val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = barColor
        style = Paint.Style.FILL
    }
    drawRoundRect(bar, barCornerRadius, barCornerRadius, barPaint)

    // --- Draw NAME block ---
    save()
    translate(textLeft, card.top + verticalPadding)
    nameLayout.draw(this)
    restore()

    // --- Draw ROLE block right after NAME block ---
    save()
    translate(textLeft, card.top + verticalPadding + nameLayout.height)
    roleLayout.draw(this)
    restore()

    // Return next Y
    return card.bottom
}


 
Editor is loading...
Leave a Comment