1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15 */
16 package com.android.example.text.styling.roundedbg
17 
18 import android.graphics.Canvas
19 import android.graphics.drawable.Drawable
20 import android.text.Layout
21 import kotlin.math.max
22 import kotlin.math.min
23 
24 /**
25  * Base class for single and multi line rounded background renderers.
26  *
27  * @param horizontalPadding the padding to be applied to left & right of the background
28  * @param verticalPadding the padding to be applied to top & bottom of the background
29  */
30 internal abstract class TextRoundedBgRenderer(
31         val horizontalPadding: Int,
32         val verticalPadding: Int
33 ) {
34 
35     /**
36      * Draw the background that starts at the {@code startOffset} and ends at {@code endOffset}.
37      *
38      * @param canvas Canvas to draw onto
39      * @param layout Layout that contains the text
40      * @param startLine the start line for the background
41      * @param endLine the end line for the background
42      * @param startOffset the character offset that the background should start at
43      * @param endOffset the character offset that the background should end at
44      */
drawnull45     abstract fun draw(
46         canvas: Canvas,
47         layout: Layout,
48         startLine: Int,
49         endLine: Int,
50         startOffset: Int,
51         endOffset: Int
52     )
53 
54     /**
55      * Get the top offset of the line and add padding into account so that there is a gap between
56      * top of the background and top of the text.
57      *
58      * @param layout Layout object that contains the text
59      * @param line line number
60      */
61     protected fun getLineTop(layout: Layout, line: Int): Int {
62         return layout.getLineTopWithoutPadding(line) - verticalPadding
63     }
64 
65     /**
66      * Get the bottom offset of the line and add padding into account so that there is a gap between
67      * bottom of the background and bottom of the text.
68      *
69      * @param layout Layout object that contains the text
70      * @param line line number
71      */
getLineBottomnull72     protected fun getLineBottom(layout: Layout, line: Int): Int {
73         return layout.getLineBottomWithoutPadding(line) + verticalPadding
74     }
75 }
76 
77 /**
78  * Draws the background for text that starts and ends on the same line.
79  *
80  * @param horizontalPadding the padding to be applied to left & right of the background
81  * @param verticalPadding the padding to be applied to top & bottom of the background
82  * @param drawable the drawable used to draw the background
83  */
84 internal class SingleLineRenderer(
85     horizontalPadding: Int,
86     verticalPadding: Int,
87     val drawable: Drawable
88 ) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
89 
drawnull90     override fun draw(
91         canvas: Canvas,
92         layout: Layout,
93         startLine: Int,
94         endLine: Int,
95         startOffset: Int,
96         endOffset: Int
97     ) {
98         val lineTop = getLineTop(layout, startLine)
99         val lineBottom = getLineBottom(layout, startLine)
100         // get min of start/end for left, and max of start/end for right since we don't
101         // the language direction
102         val left = min(startOffset, endOffset)
103         val right = max(startOffset, endOffset)
104         drawable.setBounds(left, lineTop, right, lineBottom)
105         drawable.draw(canvas)
106     }
107 }
108 
109 /**
110  * Draws the background for text that starts and ends on different lines.
111  *
112  * @param horizontalPadding the padding to be applied to left & right of the background
113  * @param verticalPadding the padding to be applied to top & bottom of the background
114  * @param drawableLeft the drawable used to draw left edge of the background
115  * @param drawableMid the drawable used to draw for whole line
116  * @param drawableRight the drawable used to draw right edge of the background
117  */
118 internal class MultiLineRenderer(
119     horizontalPadding: Int,
120     verticalPadding: Int,
121     val drawableLeft: Drawable,
122     val drawableMid: Drawable,
123     val drawableRight: Drawable
124 ) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
125 
drawnull126     override fun draw(
127         canvas: Canvas,
128         layout: Layout,
129         startLine: Int,
130         endLine: Int,
131         startOffset: Int,
132         endOffset: Int
133     ) {
134         // draw the first line
135         val paragDir = layout.getParagraphDirection(startLine)
136         val lineEndOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
137             layout.getLineLeft(startLine) - horizontalPadding
138         } else {
139             layout.getLineRight(startLine) + horizontalPadding
140         }.toInt()
141 
142         var lineBottom = getLineBottom(layout, startLine)
143         var lineTop = getLineTop(layout, startLine)
144         drawStart(canvas, startOffset, lineTop, lineEndOffset, lineBottom)
145 
146         // for the lines in the middle draw the mid drawable
147         for (line in startLine + 1 until endLine) {
148             lineTop = getLineTop(layout, line)
149             lineBottom = getLineBottom(layout, line)
150             drawableMid.setBounds(
151                 (layout.getLineLeft(line).toInt() - horizontalPadding),
152                 lineTop,
153                 (layout.getLineRight(line).toInt() + horizontalPadding),
154                 lineBottom
155             )
156             drawableMid.draw(canvas)
157         }
158 
159         val lineStartOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
160             layout.getLineRight(startLine) + horizontalPadding
161         } else {
162             layout.getLineLeft(startLine) - horizontalPadding
163         }.toInt()
164 
165         // draw the last line
166         lineBottom = getLineBottom(layout, endLine)
167         lineTop = getLineTop(layout, endLine)
168 
169         drawEnd(canvas, lineStartOffset, lineTop, endOffset, lineBottom)
170     }
171 
172     /**
173      * Draw the first line of a multiline annotation. Handles LTR/RTL.
174      *
175      * @param canvas Canvas to draw onto
176      * @param start start coordinate for the background
177      * @param top top coordinate for the background
178      * @param end end coordinate for the background
179      * @param bottom bottom coordinate for the background
180      */
drawStartnull181     private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
182         if (start > end) {
183             drawableRight.setBounds(end, top, start, bottom)
184             drawableRight.draw(canvas)
185         } else {
186             drawableLeft.setBounds(start, top, end, bottom)
187             drawableLeft.draw(canvas)
188         }
189     }
190 
191     /**
192      * Draw the last line of a multiline annotation. Handles LTR/RTL.
193      *
194      * @param canvas Canvas to draw onto
195      * @param start start coordinate for the background
196      * @param top top position for the background
197      * @param end end coordinate for the background
198      * @param bottom bottom coordinate for the background
199      */
drawEndnull200     private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
201         if (start > end) {
202             drawableLeft.setBounds(end, top, start, bottom)
203             drawableLeft.draw(canvas)
204         } else {
205             drawableRight.setBounds(start, top, end, bottom)
206             drawableRight.draw(canvas)
207         }
208     }
209 }