[go: nahoru, domu]

blob: f61c4ae8cdd35a6ac137a8f0364c03657c876a3b [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.graphics.vector
import android.graphics.Matrix
import androidx.ui.geometry.Offset
import androidx.ui.graphics.BlendMode
import androidx.ui.graphics.Brush
import androidx.ui.graphics.Canvas
import androidx.ui.graphics.Color
import androidx.ui.graphics.ColorFilter
import androidx.ui.graphics.ImageAsset
import androidx.ui.graphics.Paint
import androidx.ui.graphics.PaintingStyle
import androidx.ui.graphics.Path
import androidx.ui.graphics.StrokeCap
import androidx.ui.graphics.StrokeJoin
import androidx.ui.graphics.withSave
import androidx.ui.unit.Px
import kotlin.math.ceil
const val DefaultGroupName = ""
const val DefaultRotation = 0.0f
const val DefaultPivotX = 0.0f
const val DefaultPivotY = 0.0f
const val DefaultScaleX = 1.0f
const val DefaultScaleY = 1.0f
const val DefaultTranslationX = 0.0f
const val DefaultTranslationY = 0.0f
val EmptyPath = emptyList<PathNode>()
/**
* paint used to draw the cached vector graphic to the provided canvas
*/
// TODO (njawad) Can we update the Compose Canvas API to make this paint optional?
internal val EmptyPaint = Paint()
inline fun PathData(block: PathBuilder.() -> Unit): List<PathNode> =
with(PathBuilder()) {
block()
getNodes()
}
const val DefaultPathName = ""
const val DefaultAlpha = 1.0f
const val DefaultStrokeLineWidth = 0.0f
const val DefaultStrokeLineMiter = 4.0f
val DefaultStrokeLineCap = StrokeCap.butt
val DefaultStrokeLineJoin = StrokeJoin.miter
val DefaultTintBlendMode = BlendMode.srcIn
val DefaultTintColor = Color.Transparent
fun addPathNodes(pathStr: String?): List<PathNode> =
if (pathStr == null) {
EmptyPath
} else {
PathParser().parsePathString(pathStr).toNodes()
}
sealed class VNode {
/**
* Callback invoked whenever the node in the vector tree is modified in a way that would
* change the output of the Vector
*/
// TODO (b/144849567) don't make this publicly accessible after the vector graphics modules
// are merged
open var invalidateListener: (() -> Unit)? = null
fun invalidate() {
invalidateListener?.invoke()
}
abstract fun draw(canvas: Canvas)
}
class VectorComponent(
var viewportWidth: Float,
var viewportHeight: Float,
var defaultWidth: Px,
var defaultHeight: Px,
val name: String = ""
) : VNode() {
val root = GroupComponent(this@VectorComponent.name).apply {
pivotX = 0.0f
pivotY = 0.0f
scaleX = defaultWidth.value / viewportWidth
scaleY = defaultHeight.value / viewportHeight
invalidateListener = {
isDirty = true
}
}
private var isDirty: Boolean = true
private var vectorPaint: Paint? = null
/**
* Cached Image of the Vector Graphic to be re-used across draw calls
* if the Vector graphic is not dirty
*/
// TODO (njawad) add invalidation logic to re-draw into the offscreen Image
private var cachedImage: ImageAsset? = null
val size: Int
get() = root.size
fun draw(canvas: Canvas, alpha: Float, colorFilter: ColorFilter?) {
var targetImage = cachedImage
if (targetImage == null) {
targetImage = ImageAsset(
ceil(defaultWidth.value).toInt(),
ceil(defaultHeight.value).toInt()
)
cachedImage = targetImage
}
if (isDirty) {
root.draw(Canvas(targetImage))
isDirty = false
}
canvas.drawImage(targetImage, Offset.zero, obtainVectorPaint(alpha, colorFilter))
}
override fun draw(canvas: Canvas) {
draw(canvas, DefaultAlpha, null)
}
override fun toString(): String {
return buildString {
append("Params: ")
append("\tname: ").append(name).append("\n")
append("\twidth: ").append(defaultWidth).append("\n")
append("\theight: ").append(defaultHeight).append("\n")
append("\tviewportWidth: ").append(viewportWidth).append("\n")
append("\tviewportHeight: ").append(viewportHeight).append("\n")
}
}
private fun obtainVectorPaint(alpha: Float, colorFilter: ColorFilter?): Paint {
return if (colorFilter == null && alpha == DefaultAlpha) {
EmptyPaint
} else {
val targetPaint = vectorPaint ?: Paint().also { vectorPaint = it }
val currentColorFilter = targetPaint.colorFilter
if (currentColorFilter != colorFilter) {
targetPaint.colorFilter = colorFilter
}
if (targetPaint.alpha != alpha) {
targetPaint.alpha = alpha
}
targetPaint
}
}
}
data class PathComponent(val name: String) : VNode() {
var fill: Brush? = null
set(value) {
if (field != value) {
field = value
updateFillPaint {
field?.applyTo(this)
}
invalidate()
}
}
var fillAlpha: Float = DefaultAlpha
set(value) {
if (field != value) {
field = value
updateFillPaint {
alpha = field
}
invalidate()
}
}
var pathData: List<PathNode> = EmptyPath
set(value) {
if (field != value) {
field = value
isPathDirty = true
invalidate()
}
}
var strokeAlpha: Float = DefaultAlpha
set(value) {
if (field != value) {
field = value
updateStrokePaint {
alpha = field
}
invalidate()
}
}
var strokeLineWidth: Float = DefaultStrokeLineWidth
set(value) {
if (field != value) {
field = value
updateStrokePaint {
strokeWidth = field
}
invalidate()
}
}
var stroke: Brush? = null
set(value) {
if (field != value) {
field = value
updateStrokePaint {
field?.applyTo(this)
}
invalidate()
}
}
var strokeLineCap: StrokeCap = DefaultStrokeLineCap
set(value) {
if (field != value) {
field = value
updateStrokePaint {
strokeCap = field
}
invalidate()
}
}
var strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin
set(value) {
if (field != value) {
field = value
updateStrokePaint {
strokeJoin = field
}
invalidate()
}
}
var strokeLineMiter: Float = DefaultStrokeLineMiter
set(value) {
if (field != value) {
field = value
updateStrokePaint {
strokeMiterLimit = field
}
invalidate()
}
}
private var isPathDirty = true
private val path = Path()
private var fillPaint: Paint? = null
private var strokePaint: Paint? = null
private val parser = PathParser()
private inline fun updateStrokePaint(strokePaintUpdater: Paint.() -> Unit) {
if (strokePaint == null) {
strokePaint = createStrokePaint()
} else {
strokePaint?.strokePaintUpdater()
}
}
private fun createStrokePaint(): Paint = Paint().apply {
isAntiAlias = true
style = PaintingStyle.stroke
alpha = strokeAlpha
strokeWidth = strokeLineWidth
strokeCap = strokeLineCap
strokeJoin = strokeLineJoin
strokeMiterLimit = strokeLineMiter
stroke?.applyTo(this)
}
private fun updateFillPaint(fillPaintUpdater: Paint.() -> Unit) {
if (fillPaint == null) {
fillPaint = createFillPaint()
} else {
fillPaint?.fillPaintUpdater()
}
}
private fun createFillPaint(): Paint = Paint().apply {
isAntiAlias = true
alpha = fillAlpha
style = PaintingStyle.fill
fill?.applyTo(this)
}
private fun updatePath() {
parser.clear()
path.reset()
parser.addPathNodes(pathData).toPath(path)
}
override fun draw(canvas: Canvas) {
if (isPathDirty) {
updatePath()
isPathDirty = false
}
val fillBrush = fill
if (fillBrush != null) {
var targetFillPaint = fillPaint
if (targetFillPaint == null) {
targetFillPaint = createFillPaint()
fillPaint = targetFillPaint
}
canvas.drawPath(path, targetFillPaint)
}
val strokeBrush = stroke
if (strokeBrush != null) {
var targetStrokePaint = strokePaint
if (targetStrokePaint == null) {
targetStrokePaint = createStrokePaint()
strokePaint = targetStrokePaint
}
canvas.drawPath(path, targetStrokePaint)
}
}
override fun toString(): String {
return path.toString()
}
}
data class GroupComponent(val name: String = DefaultGroupName) : VNode() {
private var groupMatrix: Matrix? = null
private val children = mutableListOf<VNode>()
var clipPathData: List<PathNode> = EmptyPath
set(value) {
if (field != value) {
field = value
isClipPathDirty = true
invalidate()
}
}
private val willClipPath: Boolean
get() = clipPathData.isNotEmpty()
private var isClipPathDirty = true
private var clipPath: Path? = null
private var parser: PathParser? = null
override var invalidateListener: (() -> Unit)? = null
set(value) {
field = value
for (child in children) {
child.invalidateListener = value
}
}
private fun updateClipPath() {
if (willClipPath) {
var targetParser = parser
if (targetParser == null) {
targetParser = PathParser()
parser = targetParser
} else {
targetParser.clear()
}
var targetClip = clipPath
if (targetClip == null) {
targetClip = Path()
clipPath = targetClip
} else {
targetClip.reset()
}
targetParser.addPathNodes(clipPathData).toPath(targetClip)
}
}
var rotation: Float = DefaultRotation
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var pivotX: Float = DefaultPivotX
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var pivotY: Float = DefaultPivotY
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var scaleX: Float = DefaultScaleX
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var scaleY: Float = DefaultScaleY
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var translationX: Float = DefaultTranslationX
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
var translationY: Float = DefaultTranslationY
set(value) {
if (field != value) {
field = value
isMatrixDirty = true
invalidate()
}
}
private var isMatrixDirty = true
private fun updateMatrix() {
val matrix: Matrix
val target = groupMatrix
if (target == null) {
matrix = Matrix()
groupMatrix = matrix
} else {
matrix = target
}
with(matrix) {
reset()
postTranslate(-pivotX, -pivotY)
postScale(scaleX, scaleY)
postRotate(rotation, 0f, 0f)
postTranslate(translationX + pivotX,
translationY + pivotY)
}
}
fun insertAt(index: Int, instance: VNode) {
if (index < size) {
children[index] = instance
} else {
children.add(instance)
}
instance.invalidateListener = invalidateListener
invalidate()
}
fun move(from: Int, to: Int, count: Int) {
if (from > to) {
var current = to
repeat(count) {
val node = children[from]
children.removeAt(from)
children.add(current, node)
current++
}
} else {
repeat(count) {
val node = children[from]
children.removeAt(from)
children.add(to - 1, node)
}
}
invalidate()
}
fun remove(index: Int, count: Int) {
repeat(count) {
children[index].invalidateListener = null
children.removeAt(index)
}
invalidate()
}
override fun draw(canvas: Canvas) {
if (isMatrixDirty) {
updateMatrix()
isMatrixDirty = false
}
if (isClipPathDirty) {
updateClipPath()
isClipPathDirty = false
}
canvas.withSave {
val targetClip = clipPath
if (willClipPath && targetClip != null) {
canvas.clipPath(targetClip)
}
val matrix = groupMatrix
if (matrix != null) {
// TODO (njawad) add concat support to matrix
canvas.nativeCanvas.concat(matrix)
}
for (node in children) {
node.draw(canvas)
}
}
}
val size: Int
get() = children.size
override fun toString(): String {
val sb = StringBuilder().append("VGroup: ").append(name)
for (node in children) {
sb.append("\t").append(node.toString()).append("\n")
}
return sb.toString()
}
}