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