Untitled
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