문구가 좌표상에서 어떻게 위치하는지를 파악하도록 할 것이다.
글자가 페이지상의 어느 좌표에 위치하는지를 알면, 페이지를 나누는 것이 가능해질 것이다.
다음과 같이 2차원 좌표계 클래스를 만들었다.
// 2차원 위치 POS data class POS(private val x : Int = 0, private val y : Int = 0) { var X: Int = x var Y: Int = y operator fun plus(increment : POS) : POS { return POS(X + increment.Y, Y + increment.Y) } operator fun minus(decrease : POS) : POS { return POS(X - decrease.Y, Y - decrease.Y) } }
연산자에 대한 오버로딩은 앞에 operator를 붙이면 된다. 연사자 오버로딩을 통하여 2차원 좌표계의 덧셈을 편하게 만들었다. 위의 코드를 참조하라.
이전 포스팅에서는 A4 용지의 크기를 포인트 단위로 정했었다. 이러한 A4의 크기 내에서 머리말, 꼬리말, 좌우여백을 다음과 같이 만들었다. 그리고 글을 쓸 시작 시점은 당연히 본문 부분의 좌측 최상단일 것이다. 본문부분의 좌측 최상단은 머리말과 좌측 여백만큼 오프셋을 계산하면 될 것이다.
// 여백 val marginTop = 42 val marginBottom = 50 val marginLeft = 47 val marginRight = 48 // 좌표 위치 private var totalPOS = POS(marginLeft, marginTop) private var currentPOS = POS(marginLeft, marginTop)
totalPOS은 페이지 내의 상대위치가 아닌 절대 위치를 말한다. 그리고 currentPOS은 페이지 내에서의 상대위치를 가리키는 좌표이다.
아래의 두개의 함수로 글자의 위치를 조정할 것이다.
워드프로세서에서 엔터를 치면 줄을 한칸 아래로 내리는 함수를 대충 만들어 보았다.
// Line Break : Veritcal Movement fun linefeed() { totalPOS += POS(0, 50) currentPOS += POS(0, 50) }
한칸 오른쪽으로 옮기는 함수를 만들었다.
// Space : Horizontal Movement fun space() { totalPOS += POS(20, 0) currentPOS += POS(20, 0) }
Android graphics API에서 서체의 픽셀을 다루는 기준선은 아래와 같다1).
baseline을 기준(0)으로 위로 갈수록 음수, 아래로 갈 수록 양수라고 한다.
글자가 baseline보다 아래로 갈 수도 있음을 알 수 있다.
이에 따라서 글자나 문장의 경계선을 구할 때에도 baseline보다 아래의 위치도 고려해야 한다.
참고로, leading은 두 줄 이상일 떄 줄간격을 의미한다.
각각의 글자의 폭을 개별로 계산해 주는 함수이다.
paint.getTextWidths( String text, float[] widths )2)
float paint.measureText(String text)3)
출력하고자 하는 글자들의 전체 폭을 구하는 함수이다. 자간이 존재하므로 measureText는 getTextWidths의 전체 합보다 크다.
다음 그림은 설명의 편의를 위해 위에서 인용한 블로그에서 가져온 것이다.
public void getTextBounds (String text, int start, int end, Rect bounds)
글자들의 전체 경계썬을 구해주는 함수이다. 폭의 경우 measureText와 비슷하지만 문장의 좌우양끝의 자간은 삭제하기 떄문에 measureText보다 결과값이 작다4).
그런데 경계선을 구해주는 이 함수는 정확히 끝과 끝을 나타내주는 것은 아니다. 아마도 fontmetrics에서 baseline이라는 개념떄문에 그런것 아닌가 싶다.
그렇기 떄문에 아래에서는 정확히 어떤 부분을 나타내는 지를 확인해보고자 한다.
위의 getTextBounds를 통해 다음과 같이 상자와 선을 그려 보았다.
다음과 같이 상자와 선을 그리는 함수를 만들었다.
// Text with underline, rectangle, outline fun richText(text : String, canvas: Canvas, switch : String) { title.setTypeface(fontStrawberry) title.color = ContextCompat.getColor(context, R.color.orange_80) title.textSize = 16f title.textAlign = Paint.Align.LEFT // Draw Text canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title) // Font Effect when (switch) { "Rect" -> { val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) val rc = Rect(currentPOS.X, currentPOS.Y - bounds.height(), currentPOS.X + bounds.right, currentPOS.Y - bounds.height()) canvas.drawRect(rc, linePaint) } "LineBottom" -> { val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + bounds.bottom.toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y + bounds.bottom.toFloat(), linePaint) canvas.drawText("Bottom : " + bounds.bottom, currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint) } "LineTop" -> { val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + bounds.top.toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y + bounds.top.toFloat(), linePaint) canvas.drawText("Top : " + bounds.top, currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint) } "LineHeight" -> { val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() - bounds.height().toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y - bounds.height().toFloat(), linePaint) canvas.drawText("Height(minus) : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint) } } }
getTextBounds에서 top과 bottom, 그리고 줄의 높이인 height()가 어떻 결과값을 나타내는지 확인하게 만들었다.
다음과 같이 호출하였다.
// 상자만들기 pdfUtil.linefeed() pdfUtil.titleText("글자의 크기를 재봅니다", canvas, true) pdfUtil.space() // 선을 그리기 pdfUtil.linefeed() pdfUtil.richText("Text Bounds : Bottom", canvas, "LineBottom") pdfUtil.linefeed() pdfUtil.richText("Text Bounds : Top", canvas, "LineTop") pdfUtil.linefeed() pdfUtil.richText("Text Bounds : Height", canvas, "LineHeight") pdfUtil.linefeed() pdfUtil.richText("텍스트 경계선 : 바닥", canvas, "LineBottom") pdfUtil.linefeed() pdfUtil.richText("텍스트 경계선 : 천장", canvas, "LineTop") pdfUtil.linefeed() pdfUtil.richText("텍스트 경계썬 : 줄의 높이", canvas, "LineHeight")
아래 그림은 각각 김정철명조체와 고령딸기체로 그린 글자에 대하여 경계선을 나타낸 것이다.
LineHeight()로 줄의 높이를 정하는게 가장 시인성 있는 것을 알 수 있다. 따라서 앞으로는 이를 기준으로 글자의 높이를 정하겠다.
다음과 같이 Rich Text를 구현할 수 있다.
// Text with underline, rectangle, outline, shadow fun richText(text : String, canvas: Canvas, switch : String) { title.setTypeface(fontKJCMyungjo) title.color = ContextCompat.getColor(context, R.color.orange_80) title.textSize = 16f title.textAlign = Paint.Align.LEFT // Font Effect when (switch) { "Rect" -> { // Draw Text canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title) val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) val offset = 2 val rc = Rect(currentPOS.X - offset, currentPOS.Y - bounds.height() + offset, currentPOS.X + bounds.right + offset, currentPOS.Y + bounds.bottom + offset) canvas.drawRect(rc, linePaint) } "LineBottom" -> { // Draw Text canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title) val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) val offset = 2f canvas.drawLine(currentPOS.X.toFloat() - offset, currentPOS.Y.toFloat() + bounds.bottom + offset, currentPOS.X + bounds.right.toFloat() + offset, currentPOS.Y + bounds.bottom + offset, linePaint) canvas.drawText("Height(minus) : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint) } "LineCenter" -> { // Draw Text canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title) val bounds = Rect() title.getTextBounds(text, 0, text.length, bounds) val offset = 2f canvas.drawLine(currentPOS.X.toFloat() - offset, currentPOS.Y.toFloat() - (bounds.height() / 2).toFloat() + bounds.bottom, currentPOS.X + bounds.right.toFloat() + offset, currentPOS.Y - (bounds.height() / 2).toFloat() + bounds.bottom, linePaint) canvas.drawText("Center : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint) } "OutlineFill" -> { // draw outline of text title.style = Paint.Style.FILL_AND_STROKE title.strokeWidth = 1f title.color = ContextCompat.getColor(context, R.color.teal_700) canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title ) } "Outline" -> { // draw outline of text title.style = Paint.Style.STROKE title.strokeWidth = 0.5f title.color = ContextCompat.getColor(context, R.color.teal_700) canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title ) } "Shadow" -> { title.style = Paint.Style.FILL title.strokeWidth = 1f title.setShadowLayer(1f, 1f, 1f, ContextCompat.getColor(context, R.color.black)) canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title ) } } }
위의 글자꾸미기는 다음과 같이 PDF로 인쇄된다.
pdfUtil.space() // 선을 그리기 pdfUtil.linefeed() pdfUtil.richText("상자를 그립니다 : Rect", canvas, "Rect") pdfUtil.linefeed() pdfUtil.richText("밑줄을 그립니다 : LineBottom", canvas, "LineBottom") pdfUtil.linefeed() pdfUtil.richText("취소선을 그립니다 : LineCenter", canvas, "LineCenter") pdfUtil.linefeed() pdfUtil.richText("외곽선을 그리고 채웁니다. : OutlineFill", canvas, "OutlineFill") pdfUtil.linefeed() pdfUtil.richText("외곽선만 그립니다. : OutlineFill", canvas, "Outline") pdfUtil.linefeed() pdfUtil.richText("그림자를 그립니다. : OutlineFill", canvas, "Shadow")