package com.opensource.svgaplayer.drawer import android.graphics.* import android.os.Build import android.text.StaticLayout import android.text.TextUtils import android.widget.ImageView import com.opensource.svgaplayer.SVGADynamicEntity import com.opensource.svgaplayer.SVGASoundManager import com.opensource.svgaplayer.SVGAVideoEntity import com.opensource.svgaplayer.entities.SVGAVideoShapeEntity /** * Created by cuiminghui on 2017/3/29. */ internal class SVGACanvasDrawer(videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity) : SGVADrawer(videoItem) { private val sharedValues = ShareValues() private val drawTextCache: HashMap = hashMapOf() private val pathCache = PathCache() private var beginIndexList: Array? = null private var endIndexList: Array? = null override fun drawFrame(canvas: Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) { super.drawFrame(canvas, frameIndex, scaleType) playAudio(frameIndex) this.pathCache.onSizeChanged(canvas) val sprites = requestFrameSprites(frameIndex) // Filter null sprites if (sprites.count() <= 0) return val matteSprites = mutableMapOf() var saveID = -1 beginIndexList = null endIndexList = null // Filter no matte layer var hasMatteLayer = false sprites.get(0).imageKey?.let { if (it.endsWith(".matte")) { hasMatteLayer = true } } sprites.forEachIndexed { index, svgaDrawerSprite -> // Save matte sprite svgaDrawerSprite.imageKey?.let { /// No matte layer included or VERSION Unsopport matte if (!hasMatteLayer || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // Normal sprite drawSprite(svgaDrawerSprite, canvas, frameIndex) // Continue return@forEachIndexed } /// Cache matte sprite if (it.endsWith(".matte")) { matteSprites.put(it, svgaDrawerSprite) // Continue return@forEachIndexed } } /// Is matte begin if (isMatteBegin(index, sprites)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { saveID = canvas.saveLayer(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), null) } else { canvas.save() } } /// Normal matte drawSprite(svgaDrawerSprite, canvas, frameIndex) /// Is matte end if (isMatteEnd(index, sprites)) { matteSprites.get(svgaDrawerSprite.matteKey)?.let { drawSprite(it, this.sharedValues.shareMatteCanvas(canvas.width, canvas.height), frameIndex) canvas.drawBitmap(this.sharedValues.sharedMatteBitmap(), 0f, 0f, this.sharedValues.shareMattePaint()) if (saveID != -1) { canvas.restoreToCount(saveID) } else { canvas.restore() } // Continue return@forEachIndexed } } } releaseFrameSprites(sprites) } private fun isMatteBegin(spriteIndex: Int, sprites: List): Boolean { if (beginIndexList == null) { val boolArray = Array(sprites.count()) { false } sprites.forEachIndexed { index, svgaDrawerSprite -> svgaDrawerSprite.imageKey?.let { /// Filter matte sprite if (it.endsWith(".matte")) { // Continue return@forEachIndexed } } svgaDrawerSprite.matteKey?.let { if (it.length > 0) { sprites.get(index - 1)?.let { lastSprite -> if (lastSprite.matteKey.isNullOrEmpty()) { boolArray[index] = true } else { if (lastSprite.matteKey != svgaDrawerSprite.matteKey) { boolArray[index] = true } } } } } } beginIndexList = boolArray } return beginIndexList?.get(spriteIndex) ?: false } private fun isMatteEnd(spriteIndex: Int, sprites: List): Boolean { if (endIndexList == null) { val boolArray = Array(sprites.count()) { false } sprites.forEachIndexed { index, svgaDrawerSprite -> svgaDrawerSprite.imageKey?.let { /// Filter matte sprite if (it.endsWith(".matte")) { // Continue return@forEachIndexed } } svgaDrawerSprite.matteKey?.let { if (it.length > 0) { // Last one if (index == sprites.count() - 1) { boolArray[index] = true } else { sprites.get(index + 1)?.let { nextSprite -> if (nextSprite.matteKey.isNullOrEmpty()) { boolArray[index] = true } else { if (nextSprite.matteKey != svgaDrawerSprite.matteKey) { boolArray[index] = true } } } } } } } endIndexList = boolArray } return endIndexList?.get(spriteIndex) ?: false } private fun playAudio(frameIndex: Int) { this.videoItem.audioList.forEach { audio -> if (audio.startFrame == frameIndex) { if (SVGASoundManager.isInit()) { audio.soundID?.let { soundID -> audio.playID = SVGASoundManager.play(soundID) } } else { this.videoItem.soundPool?.let { soundPool -> audio.soundID?.let { soundID -> audio.playID = soundPool.play(soundID, 1.0f, 1.0f, 1, 0, 1.0f) } } } } if (audio.endFrame <= frameIndex) { audio.playID?.let { if (SVGASoundManager.isInit()) { SVGASoundManager.stop(it) } else { this.videoItem.soundPool?.stop(it) } } audio.playID = null } } } private fun shareFrameMatrix(transform: Matrix): Matrix { val matrix = this.sharedValues.sharedMatrix() matrix.postScale(scaleInfo.scaleFx, scaleInfo.scaleFy) matrix.postTranslate(scaleInfo.tranFx, scaleInfo.tranFy) matrix.preConcat(transform) return matrix } private fun drawSprite(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { drawImage(sprite, canvas) drawShape(sprite, canvas) drawDynamic(sprite, canvas, frameIndex) } private fun drawImage(sprite: SVGADrawerSprite, canvas: Canvas) { val imageKey = sprite.imageKey ?: return val isHidden = dynamicItem.dynamicHidden[imageKey] == true if (isHidden) { return } val bitmapKey = if (imageKey.endsWith(".matte")) imageKey.substring(0, imageKey.length - 6) else imageKey val drawingBitmap = (dynamicItem.dynamicImage[bitmapKey] ?: videoItem.imageMap[bitmapKey]) ?: return val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) val paint = this.sharedValues.sharedPaint() paint.isAntiAlias = videoItem.antiAlias paint.isFilterBitmap = videoItem.antiAlias paint.alpha = (sprite.frameEntity.alpha * 255).toInt() if (sprite.frameEntity.maskPath != null) { val maskPath = sprite.frameEntity.maskPath ?: return canvas.save() val path = this.sharedValues.sharedPath() maskPath.buildPath(path) path.transform(frameMatrix) canvas.clipPath(path) frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) if (!drawingBitmap.isRecycled) { canvas.drawBitmap(drawingBitmap, frameMatrix, paint) } canvas.restore() } else { frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) if (!drawingBitmap.isRecycled) { canvas.drawBitmap(drawingBitmap, frameMatrix, paint) } } dynamicItem.dynamicIClickArea.let { it.get(imageKey)?.let { listener -> val matrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) frameMatrix.getValues(matrixArray) listener.onResponseArea(imageKey, matrixArray[2].toInt() , matrixArray[5].toInt() , (drawingBitmap.width * matrixArray[0] + matrixArray[2]).toInt() , (drawingBitmap.height * matrixArray[4] + matrixArray[5]).toInt()) } } drawTextOnBitmap(canvas, drawingBitmap, sprite, frameMatrix) } private fun drawTextOnBitmap(canvas: Canvas, drawingBitmap: Bitmap, sprite: SVGADrawerSprite, frameMatrix: Matrix) { if (dynamicItem.isTextDirty) { this.drawTextCache.clear() dynamicItem.isTextDirty = false } val imageKey = sprite.imageKey ?: return var textBitmap: Bitmap? = null dynamicItem.dynamicText[imageKey]?.let { drawingText -> dynamicItem.dynamicTextPaint[imageKey]?.let { drawingTextPaint -> drawTextCache[imageKey]?.let { textBitmap = it } ?: kotlin.run { textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) val drawRect = Rect(0, 0, drawingBitmap.width, drawingBitmap.height) val textCanvas = Canvas(textBitmap) drawingTextPaint.isAntiAlias = true val fontMetrics = drawingTextPaint.getFontMetrics(); val top = fontMetrics.top val bottom = fontMetrics.bottom val baseLineY = drawRect.centerY() - top / 2 - bottom / 2 textCanvas.drawText(drawingText, drawRect.centerX().toFloat(), baseLineY, drawingTextPaint); drawTextCache.put(imageKey, textBitmap as Bitmap) } } } dynamicItem.dynamicBoringLayoutText[imageKey]?.let { drawTextCache[imageKey]?.let { textBitmap = it } ?: kotlin.run { it.paint.isAntiAlias = true textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) val textCanvas = Canvas(textBitmap) textCanvas.translate(0f, ((drawingBitmap.height - it.height) / 2).toFloat()) it.draw(textCanvas) drawTextCache.put(imageKey, textBitmap as Bitmap) } } dynamicItem.dynamicStaticLayoutText[imageKey]?.let { drawTextCache[imageKey]?.let { textBitmap = it } ?: kotlin.run { it.paint.isAntiAlias = true var layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { var lineMax = try { val field = StaticLayout::class.java.getDeclaredField("mMaximumVisibleLineCount") field.isAccessible = true field.getInt(it) } catch (e: Exception) { Int.MAX_VALUE } StaticLayout.Builder .obtain(it.text, 0, it.text.length, it.paint, drawingBitmap.width) .setAlignment(it.alignment) .setMaxLines(lineMax) .setEllipsize(TextUtils.TruncateAt.END) .build() } else { StaticLayout(it.text, 0, it.text.length, it.paint, drawingBitmap.width, it.alignment, it.spacingMultiplier, it.spacingAdd, false) } textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) val textCanvas = Canvas(textBitmap) textCanvas.translate(0f, ((drawingBitmap.height - layout.height) / 2).toFloat()) layout.draw(textCanvas) drawTextCache.put(imageKey, textBitmap as Bitmap) } } textBitmap?.let { textBitmap -> val paint = this.sharedValues.sharedPaint() paint.isAntiAlias = videoItem.antiAlias paint.alpha = (sprite.frameEntity.alpha * 255).toInt() if (sprite.frameEntity.maskPath != null) { val maskPath = sprite.frameEntity.maskPath ?: return@let canvas.save() canvas.concat(frameMatrix) canvas.clipRect(0, 0, drawingBitmap.width, drawingBitmap.height) val bitmapShader = BitmapShader(textBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) paint.shader = bitmapShader val path = this.sharedValues.sharedPath() maskPath.buildPath(path) canvas.drawPath(path, paint) canvas.restore() } else { paint.isFilterBitmap = videoItem.antiAlias canvas.drawBitmap(textBitmap, frameMatrix, paint) } } } private fun drawShape(sprite: SVGADrawerSprite, canvas: Canvas) { val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) sprite.frameEntity.shapes.forEach { shape -> shape.buildPath() shape.shapePath?.let { val paint = this.sharedValues.sharedPaint() paint.reset() paint.isAntiAlias = videoItem.antiAlias paint.alpha = (sprite.frameEntity.alpha * 255).toInt() val path = this.sharedValues.sharedPath() path.reset() path.addPath(this.pathCache.buildPath(shape)) val shapeMatrix = this.sharedValues.sharedMatrix2() shapeMatrix.reset() shape.transform?.let { shapeMatrix.postConcat(it) } shapeMatrix.postConcat(frameMatrix) path.transform(shapeMatrix) shape.styles?.fill?.let { if (it != 0x00000000) { paint.style = Paint.Style.FILL paint.color = it val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) if (alpha != 255) { paint.alpha = alpha } if (sprite.frameEntity.maskPath !== null) canvas.save() sprite.frameEntity.maskPath?.let { maskPath -> val path2 = this.sharedValues.sharedPath2() maskPath.buildPath(path2) path2.transform(frameMatrix) canvas.clipPath(path2) } canvas.drawPath(path, paint) if (sprite.frameEntity.maskPath !== null) canvas.restore() } } shape.styles?.strokeWidth?.let { if (it > 0) { paint.alpha = (sprite.frameEntity.alpha * 255).toInt() paint.style = Paint.Style.STROKE shape.styles?.stroke?.let { paint.color = it val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) if (alpha != 255) { paint.alpha = alpha } } val scale = matrixScale(frameMatrix) shape.styles?.strokeWidth?.let { paint.strokeWidth = it * scale } shape.styles?.lineCap?.let { when { it.equals("butt", true) -> paint.strokeCap = Paint.Cap.BUTT it.equals("round", true) -> paint.strokeCap = Paint.Cap.ROUND it.equals("square", true) -> paint.strokeCap = Paint.Cap.SQUARE } } shape.styles?.lineJoin?.let { when { it.equals("miter", true) -> paint.strokeJoin = Paint.Join.MITER it.equals("round", true) -> paint.strokeJoin = Paint.Join.ROUND it.equals("bevel", true) -> paint.strokeJoin = Paint.Join.BEVEL } } shape.styles?.miterLimit?.let { paint.strokeMiter = it.toFloat() * scale } shape.styles?.lineDash?.let { if (it.size == 3 && (it[0] > 0 || it[1] > 0)) { paint.pathEffect = DashPathEffect(floatArrayOf( (if (it[0] < 1.0f) 1.0f else it[0]) * scale, (if (it[1] < 0.1f) 0.1f else it[1]) * scale ), it[2] * scale) } } if (sprite.frameEntity.maskPath !== null) canvas.save() sprite.frameEntity.maskPath?.let { maskPath -> val path2 = this.sharedValues.sharedPath2() maskPath.buildPath(path2) path2.transform(frameMatrix) canvas.clipPath(path2) } canvas.drawPath(path, paint) if (sprite.frameEntity.maskPath !== null) canvas.restore() } } } } } private val matrixScaleTempValues = FloatArray(16) private fun matrixScale(matrix: Matrix): Float { matrix.getValues(matrixScaleTempValues) if (matrixScaleTempValues[0] == 0f) { return 0f } var A = matrixScaleTempValues[0].toDouble() var B = matrixScaleTempValues[3].toDouble() var C = matrixScaleTempValues[1].toDouble() var D = matrixScaleTempValues[4].toDouble() if (A * D == B * C) return 0f var scaleX = Math.sqrt(A * A + B * B) A /= scaleX B /= scaleX var skew = A * C + B * D C -= A * skew D -= B * skew var scaleY = Math.sqrt(C * C + D * D) C /= scaleY D /= scaleY skew /= scaleY if (A * D < B * C) { scaleX = -scaleX } return if (scaleInfo.ratioX) Math.abs(scaleX.toFloat()) else Math.abs(scaleY.toFloat()) } private fun drawDynamic(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { val imageKey = sprite.imageKey ?: return dynamicItem.dynamicDrawer[imageKey]?.let { val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) canvas.save() canvas.concat(frameMatrix) it.invoke(canvas, frameIndex) canvas.restore() } dynamicItem.dynamicDrawerSized[imageKey]?.let { val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) canvas.save() canvas.concat(frameMatrix) it.invoke(canvas, frameIndex, sprite.frameEntity.layout.width.toInt(), sprite.frameEntity.layout.height.toInt()) canvas.restore() } } class ShareValues { private val sharedPaint = Paint() private val sharedPath = Path() private val sharedPath2 = Path() private val sharedMatrix = Matrix() private val sharedMatrix2 = Matrix() private val shareMattePaint = Paint() private var shareMatteCanvas: Canvas? = null private var sharedMatteBitmap: Bitmap? = null fun sharedPaint(): Paint { sharedPaint.reset() return sharedPaint } fun sharedPath(): Path { sharedPath.reset() return sharedPath } fun sharedPath2(): Path { sharedPath2.reset() return sharedPath2 } fun sharedMatrix(): Matrix { sharedMatrix.reset() return sharedMatrix } fun sharedMatrix2(): Matrix { sharedMatrix2.reset() return sharedMatrix2 } fun shareMattePaint(): Paint { shareMattePaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_IN)) return shareMattePaint } fun sharedMatteBitmap(): Bitmap { return sharedMatteBitmap as Bitmap } fun shareMatteCanvas(width: Int, height: Int): Canvas { if (shareMatteCanvas == null) { sharedMatteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8) // shareMatteCanvas = Canvas(sharedMatteBitmap) } // val matteCanvas = shareMatteCanvas as Canvas // matteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) // return matteCanvas return Canvas(sharedMatteBitmap) } } class PathCache { private var canvasWidth: Int = 0 private var canvasHeight: Int = 0 private val cache = HashMap() fun onSizeChanged(canvas: Canvas) { if (this.canvasWidth != canvas.width || this.canvasHeight != canvas.height) { this.cache.clear() } this.canvasWidth = canvas.width this.canvasHeight = canvas.height } fun buildPath(shape: SVGAVideoShapeEntity): Path { if (!this.cache.containsKey(shape)) { val path = Path() path.set(shape.shapePath) this.cache[shape] = path } return this.cache[shape]!! } } }