| /* |
| * 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 androidx.ui.geometry.Size |
| import androidx.ui.graphics.BlendMode |
| import androidx.ui.graphics.Brush |
| import androidx.ui.graphics.Color |
| import androidx.ui.graphics.ColorFilter |
| import androidx.ui.graphics.Path |
| import androidx.ui.graphics.StrokeCap |
| import androidx.ui.graphics.StrokeJoin |
| import androidx.ui.graphics.drawscope.DrawScope |
| import androidx.ui.graphics.drawscope.Stroke |
| import androidx.ui.graphics.drawscope.withTransform |
| import androidx.ui.graphics.vectormath.Matrix4 |
| import androidx.ui.unit.IntSize |
| import androidx.ui.util.fastForEach |
| import androidx.ui.util.toRadians |
| 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>() |
| |
| inline fun PathData(block: PathBuilder.() -> Unit): List<PathNode> = |
| with(PathBuilder()) { |
| block() |
| getNodes() |
| } |
| |
| const val DefaultPathName = "" |
| 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 |
| */ |
| internal open var invalidateListener: (() -> Unit)? = null |
| |
| fun invalidate() { |
| invalidateListener?.invoke() |
| } |
| |
| abstract fun DrawScope.draw() |
| } |
| |
| internal class VectorComponent( |
| viewportWidth: Float, |
| viewportHeight: Float, |
| val name: String = "" |
| ) : VNode() { |
| |
| val root = GroupComponent(this@VectorComponent.name).apply { |
| pivotX = 0.0f |
| pivotY = 0.0f |
| invalidateListener = { |
| isDirty = true |
| } |
| } |
| |
| private var isDirty: Boolean = true |
| |
| private val cacheDrawScope = DrawCache() |
| |
| var viewportWidth: Float = viewportWidth |
| set(value) { |
| if (field != value) { |
| field = value |
| isDirty = true |
| } |
| } |
| |
| var viewportHeight: Float = viewportHeight |
| set(value) { |
| if (field != value) { |
| field = value |
| isDirty = true |
| } |
| } |
| |
| private var previousDrawSize: Size = Size.UnspecifiedSize |
| |
| /** |
| * Cached lambda used to avoid allocating the lambda on each draw invocation |
| */ |
| private val drawVectorBlock: DrawScope.() -> Unit = { |
| with (root) { draw() } |
| } |
| |
| fun DrawScope.draw(alpha: Float, colorFilter: ColorFilter?) { |
| // If the content of the vector has changed, or we are drawing a different size |
| // update the cached image to ensure we are scaling the vector appropriately |
| if (isDirty || previousDrawSize != size) { |
| root.scaleX = size.width / viewportWidth |
| root.scaleY = size.height / viewportHeight |
| cacheDrawScope.drawCachedImage( |
| IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()), |
| this@draw, |
| layoutDirection, |
| drawVectorBlock |
| ) |
| isDirty = false |
| previousDrawSize = size |
| } |
| cacheDrawScope.drawInto(this, alpha, colorFilter) |
| } |
| |
| override fun DrawScope.draw() { |
| draw(1.0f, null) |
| } |
| |
| override fun toString(): String { |
| return buildString { |
| append("Params: ") |
| append("\tname: ").append(name).append("\n") |
| append("\tviewportWidth: ").append(viewportWidth).append("\n") |
| append("\tviewportHeight: ").append(viewportHeight).append("\n") |
| } |
| } |
| } |
| |
| internal data class PathComponent(val name: String) : VNode() { |
| |
| var fill: Brush? = null |
| set(value) { |
| if (field != value) { |
| field = value |
| invalidate() |
| } |
| } |
| |
| var fillAlpha: Float = 1.0f |
| set(value) { |
| if (field != value) { |
| field = value |
| invalidate() |
| } |
| } |
| |
| var pathData: List<PathNode> = EmptyPath |
| set(value) { |
| if (field != value) { |
| field = value |
| isPathDirty = true |
| invalidate() |
| } |
| } |
| |
| var strokeAlpha: Float = 1.0f |
| set(value) { |
| if (field != value) { |
| field = value |
| invalidate() |
| } |
| } |
| |
| var strokeLineWidth: Float = DefaultStrokeLineWidth |
| set(value) { |
| if (field != value) { |
| field = value |
| invalidate() |
| } |
| } |
| |
| var stroke: Brush? = null |
| set(value) { |
| if (field != value) { |
| field = value |
| invalidate() |
| } |
| } |
| |
| var strokeLineCap: StrokeCap = DefaultStrokeLineCap |
| set(value) { |
| if (field != value) { |
| field = value |
| isStrokeDirty = true |
| invalidate() |
| } |
| } |
| |
| var strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin |
| set(value) { |
| if (field != value) { |
| field = value |
| isStrokeDirty = true |
| invalidate() |
| } |
| } |
| |
| var strokeLineMiter: Float = DefaultStrokeLineMiter |
| set(value) { |
| if (field != value) { |
| field = value |
| isStrokeDirty = true |
| invalidate() |
| } |
| } |
| |
| private var isPathDirty = true |
| private var isStrokeDirty = true |
| |
| private var strokeStyle: Stroke? = null |
| |
| private val path = Path() |
| |
| private val parser = PathParser() |
| |
| private fun updatePath() { |
| parser.clear() |
| path.reset() |
| parser.addPathNodes(pathData).toPath(path) |
| } |
| |
| override fun DrawScope.draw() { |
| if (isPathDirty) { |
| updatePath() |
| isPathDirty = false |
| } |
| |
| fill?.let { drawPath(path, brush = it, alpha = fillAlpha) } |
| stroke?.let { |
| var targetStroke = strokeStyle |
| if (isStrokeDirty || targetStroke == null) { |
| targetStroke = |
| Stroke(strokeLineWidth, strokeLineMiter, strokeLineCap, strokeLineJoin) |
| strokeStyle = targetStroke |
| isStrokeDirty = false |
| } |
| drawPath(path, brush = it, alpha = strokeAlpha, style = targetStroke) |
| } |
| } |
| |
| override fun toString(): String { |
| return path.toString() |
| } |
| } |
| |
| internal data class GroupComponent(val name: String = DefaultGroupName) : VNode() { |
| |
| private var groupMatrix: Matrix4? = 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 |
| children.fastForEach { child -> |
| 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: Matrix4 |
| val target = groupMatrix |
| if (target == null) { |
| matrix = Matrix4() |
| groupMatrix = matrix |
| } else { |
| matrix = target |
| } |
| matrix.assignColumns(Matrix4.identity()) |
| // M = T(translationX + pivotX, translationY + pivotY) * |
| // R(rotation) * S(scaleX, scaleY) * |
| // T(-pivotX, -pivotY) |
| matrix.translate(translationX + pivotX, translationY + pivotY) |
| matrix.rotateZ(radians = rotation.toRadians()) |
| matrix.scale(scaleX, scaleY, 1f) |
| matrix.translate(-pivotX, -pivotY) |
| } |
| |
| fun insertAt(index: Int, instance: VNode) { |
| if (index < numChildren) { |
| 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 DrawScope.draw() { |
| if (isMatrixDirty) { |
| updateMatrix() |
| isMatrixDirty = false |
| } |
| |
| if (isClipPathDirty) { |
| updateClipPath() |
| isClipPathDirty = false |
| } |
| |
| withTransform({ |
| groupMatrix?.let { transform(it) } |
| val targetClip = clipPath |
| if (willClipPath && targetClip != null) { |
| clipPath(targetClip) |
| } |
| }) { |
| children.fastForEach { node -> |
| with(node) { |
| this@draw.draw() |
| } |
| } |
| } |
| } |
| |
| val numChildren: Int |
| get() = children.size |
| |
| override fun toString(): String { |
| val sb = StringBuilder().append("VGroup: ").append(name) |
| children.fastForEach { node -> |
| sb.append("\t").append(node.toString()).append("\n") |
| } |
| return sb.toString() |
| } |
| } |