1 /*
2 * Copyright 2020 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
17 package androidx.compose.ui.text.platform
18
19 import android.text.TextPaint
20 import androidx.annotation.VisibleForTesting
21 import androidx.compose.runtime.State
22 import androidx.compose.runtime.derivedStateOf
23 import androidx.compose.ui.geometry.Size
24 import androidx.compose.ui.geometry.isSpecified
25 import androidx.compose.ui.graphics.BlendMode
26 import androidx.compose.ui.graphics.Brush
27 import androidx.compose.ui.graphics.Color
28 import androidx.compose.ui.graphics.Paint
29 import androidx.compose.ui.graphics.PaintingStyle
30 import androidx.compose.ui.graphics.Shader
31 import androidx.compose.ui.graphics.ShaderBrush
32 import androidx.compose.ui.graphics.Shadow
33 import androidx.compose.ui.graphics.SolidColor
34 import androidx.compose.ui.graphics.asComposePaint
35 import androidx.compose.ui.graphics.drawscope.DrawScope
36 import androidx.compose.ui.graphics.drawscope.DrawStyle
37 import androidx.compose.ui.graphics.drawscope.Fill
38 import androidx.compose.ui.graphics.drawscope.Stroke
39 import androidx.compose.ui.graphics.isSpecified
40 import androidx.compose.ui.graphics.toArgb
41 import androidx.compose.ui.text.platform.extensions.correctBlurRadius
42 import androidx.compose.ui.text.style.TextDecoration
43 import androidx.compose.ui.text.style.modulate
44 import androidx.compose.ui.util.fastCoerceIn
45 import androidx.compose.ui.util.fastRoundToInt
46
47 internal class AndroidTextPaint(flags: Int, density: Float) : TextPaint(flags) {
48 init {
49 this.density = density
50 }
51
52 // A wrapper to use Compose Paint APIs on this TextPaint
53 private var backingComposePaint: Paint? = null
54 private val composePaint: Paint
55 get() {
56 val finalBackingComposePaint = backingComposePaint
57 if (finalBackingComposePaint != null) return finalBackingComposePaint
<lambda>null58 return this.asComposePaint().also { backingComposePaint = it }
59 }
60
61 private var textDecoration: TextDecoration = TextDecoration.None
62
63 private var backingBlendMode: BlendMode = DrawScope.DefaultBlendMode
64
65 @VisibleForTesting internal var shadow: Shadow = Shadow.None
66
67 /**
68 * Different than other backing properties, this variable only exists to enable easy comparison
69 * between the last set color and the new color that is going to be set. Color conversion from
70 * Compose to platform color primitive integer is expensive, so it is more efficient to skip
71 * this conversion if the color is not going to change at all.
72 */
73 private var lastColor: Color? = null
74
75 @VisibleForTesting internal var brush: Brush? = null
76
77 internal var shaderState: State<Shader?>? = null
78
79 @VisibleForTesting internal var brushSize: Size? = null
80
81 private var drawStyle: DrawStyle? = null
82
setTextDecorationnull83 fun setTextDecoration(textDecoration: TextDecoration?) {
84 if (textDecoration == null) return
85 if (this.textDecoration != textDecoration) {
86 this.textDecoration = textDecoration
87 isUnderlineText = TextDecoration.Underline in this.textDecoration
88 isStrikeThruText = TextDecoration.LineThrough in this.textDecoration
89 }
90 }
91
setShadownull92 fun setShadow(shadow: Shadow?) {
93 if (shadow == null) return
94 if (this.shadow != shadow) {
95 this.shadow = shadow
96 if (this.shadow == Shadow.None) {
97 clearShadowLayer()
98 } else {
99 setShadowLayer(
100 correctBlurRadius(this.shadow.blurRadius),
101 this.shadow.offset.x,
102 this.shadow.offset.y,
103 this.shadow.color.toArgb()
104 )
105 }
106 }
107 }
108
setColornull109 fun setColor(color: Color) {
110 if (lastColor != color && color.isSpecified) {
111 this.lastColor = color
112 this.color = color.toArgb()
113 clearShader()
114 }
115 }
116
setBrushnull117 fun setBrush(brush: Brush?, size: Size, alpha: Float = Float.NaN) {
118 when (brush) {
119 // null brush should just clear the shader and leave `color` as the final decider
120 // while painting
121 null -> {
122 clearShader()
123 }
124 // SolidColor brush can be treated just like setting a color.
125 is SolidColor -> {
126 setColor(brush.value.modulate(alpha))
127 }
128 // This is the brush type that we mostly refer to when we talk about brush support.
129 // Below code is almost equivalent to;
130 // val this.shaderState = remember(brush, brushSize) {
131 // derivedStateOf {
132 // brush.createShader(size)
133 // }
134 // }
135 is ShaderBrush -> {
136 if (this.brush != brush || this.brushSize != size) {
137 if (size.isSpecified) {
138 this.brush = brush
139 this.brushSize = size
140 this.shaderState = derivedStateOf { brush.createShader(size) }
141 }
142 }
143 composePaint.shader = this.shaderState?.value
144 this.lastColor = null
145 setAlpha(alpha)
146 }
147 }
148 }
149
setDrawStylenull150 fun setDrawStyle(drawStyle: DrawStyle?) {
151 if (drawStyle == null) return
152 if (this.drawStyle != drawStyle) {
153 this.drawStyle = drawStyle
154 when (drawStyle) {
155 Fill -> {
156 // Stroke properties such as strokeWidth, strokeMiter are not re-set because
157 // Fill style should make those properties no-op. Next time the style is set
158 // as Stroke, stroke properties get re-set as well.
159
160 // avoid unnecessarily allocating a composePaint object in hot path.
161 this.style = Style.FILL
162 }
163 is Stroke -> {
164 composePaint.style = PaintingStyle.Stroke
165 composePaint.strokeWidth = drawStyle.width
166 composePaint.strokeMiterLimit = drawStyle.miter
167 composePaint.strokeJoin = drawStyle.join
168 composePaint.strokeCap = drawStyle.cap
169 composePaint.pathEffect = drawStyle.pathEffect
170 }
171 }
172 }
173 }
174
175 // BlendMode is only available to DrawScope.drawText.
176 // not intended to be used by TextStyle/SpanStyle.
177 var blendMode: BlendMode
178 get() {
179 return backingBlendMode
180 }
181 set(value) {
182 if (value == backingBlendMode) return
183 composePaint.blendMode = value
184 backingBlendMode = value
185 }
186
187 /** Clears all shader related cache parameters and native shader property. */
clearShadernull188 private fun clearShader() {
189 this.shaderState = null
190 this.brush = null
191 this.brushSize = null
192 this.shader = null
193 }
194 }
195
196 /** Accepts an alpha value in the range [0f, 1f] then maps to an integer value in [0, 255] range. */
setAlphanull197 internal fun TextPaint.setAlpha(alpha: Float) {
198 if (!alpha.isNaN()) {
199 val alphaInt = alpha.fastCoerceIn(0f, 1f).times(255).fastRoundToInt()
200 setAlpha(alphaInt)
201 }
202 }
203