DisplayLeakConnectorView.kt
TLDR
The DisplayLeakConnectorView
class is responsible for drawing a custom view that displays a connection between different types of nodes. The view can be customized based on the provided Type
. It handles different types of drawing operations, such as drawing lines, circles, and arrow heads.
Classes
DisplayLeakConnectorView
This class extends the View
class and provides custom drawing functionality for displaying a connection between different types of nodes. It contains various properties and methods for handling the drawing operations.
Methods
None
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 leakcanary.internal
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.PorterDuff.Mode.CLEAR
import android.graphics.PorterDuffXfermode
import android.util.AttributeSet
import android.view.View
import com.squareup.leakcanary.core.R
import leakcanary.internal.DisplayLeakConnectorView.Type.END
import leakcanary.internal.DisplayLeakConnectorView.Type.END_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.GC_ROOT
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_LAST_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNKNOWN
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.START
import leakcanary.internal.DisplayLeakConnectorView.Type.START_LAST_REACHABLE
import leakcanary.internal.navigation.getColorCompat
import kotlin.math.sqrt
internal class DisplayLeakConnectorView(
context: Context,
attrs: AttributeSet
) : View(context, attrs) {
private val classNamePaint: Paint
private val leakPaint: Paint
private val clearPaint: Paint
private val referencePaint: Paint
private val strokeSize: Float
private val circleY: Float
private var type: Type? = null
private var cache: Bitmap? = null
enum class Type {
GC_ROOT,
START,
START_LAST_REACHABLE,
NODE_UNKNOWN,
NODE_FIRST_UNREACHABLE,
NODE_UNREACHABLE,
NODE_REACHABLE,
NODE_LAST_REACHABLE,
END,
END_FIRST_UNREACHABLE
}
init {
val resources = resources
type = NODE_UNKNOWN
circleY = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_center_y)
.toFloat()
strokeSize = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_stroke_size)
.toFloat()
classNamePaint = Paint(Paint.ANTI_ALIAS_FLAG)
classNamePaint.color = context.getColorCompat(R.color.leak_canary_class_name)
classNamePaint.strokeWidth = strokeSize
leakPaint = Paint(Paint.ANTI_ALIAS_FLAG)
leakPaint.color = context.getColorCompat(R.color.leak_canary_leak)
leakPaint.style = Paint.Style.STROKE
leakPaint.strokeWidth = strokeSize
val pathLines = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_line)
.toFloat()
val pathGaps = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_gap)
.toFloat()
leakPaint.pathEffect = DashPathEffect(floatArrayOf(pathLines, pathGaps), 0f)
clearPaint = Paint(Paint.ANTI_ALIAS_FLAG)
clearPaint.color = Color.TRANSPARENT
clearPaint.xfermode = CLEAR_XFER_MODE
referencePaint = Paint(Paint.ANTI_ALIAS_FLAG)
referencePaint.color = context.getColorCompat(R.color.leak_canary_reference)
referencePaint.strokeWidth = strokeSize
}
override fun onDraw(canvas: Canvas) {
val width = measuredWidth
val height = measuredHeight
if (cache != null && (cache!!.width != width || cache!!.height != height)) {
cache!!.recycle()
cache = null
}
if (cache == null) {
cache = Bitmap.createBitmap(width, height, ARGB_8888)
val cacheCanvas = Canvas(cache!!)
when (type) {
NODE_UNKNOWN -> drawItems(cacheCanvas, leakPaint, leakPaint)
NODE_UNREACHABLE, NODE_REACHABLE -> drawItems(
cacheCanvas, referencePaint, referencePaint
)
NODE_FIRST_UNREACHABLE -> drawItems(
cacheCanvas, leakPaint, referencePaint
)
NODE_LAST_REACHABLE -> drawItems(
cacheCanvas, referencePaint, leakPaint
)
START -> {
drawStartLine(cacheCanvas)
drawItems(cacheCanvas, null, referencePaint)
}
START_LAST_REACHABLE -> {
drawStartLine(cacheCanvas)
drawItems(cacheCanvas, null, leakPaint)
}
END -> drawItems(cacheCanvas, referencePaint, null)
END_FIRST_UNREACHABLE -> drawItems(
cacheCanvas, leakPaint, null
)
GC_ROOT -> drawGcRoot(cacheCanvas)
else -> throw UnsupportedOperationException("Unknown type " + type!!)
}
}
canvas.drawBitmap(cache!!, 0f, 0f, null)
}
private fun drawStartLine(cacheCanvas: Canvas) {
val width = measuredWidth
val halfWidth = width / 2f
cacheCanvas.drawLine(halfWidth, 0f, halfWidth, circleY, classNamePaint)
}
private fun drawGcRoot(
cacheCanvas: Canvas
) {
val width = measuredWidth
val height = measuredHeight
val halfWidth = width / 2f
cacheCanvas.drawLine(halfWidth, 0f, halfWidth, height.toFloat(), classNamePaint)
}
private fun drawItems(
cacheCanvas: Canvas,
arrowHeadPaint: Paint?,
nextArrowPaint: Paint?
) {
if (arrowHeadPaint != null) {
drawArrowHead(cacheCanvas, arrowHeadPaint)
}
if (nextArrowPaint != null) {
drawNextArrowLine(cacheCanvas, nextArrowPaint)
}
drawInstanceCircle(cacheCanvas)
}
private fun drawArrowHead(
cacheCanvas: Canvas,
paint: Paint
) {
// Circle center is at half height
val width = measuredWidth
val halfWidth = width / 2f
val circleRadius = width / 3f
// Splitting the arrow head in two makes an isosceles right triangle.
// It's hypotenuse is side * sqrt(2)
val arrowHeight = halfWidth / 2 * SQRT_TWO
val halfStrokeSize = strokeSize / 2
val translateY = circleY - arrowHeight - circleRadius * 2 - strokeSize
val lineYEnd = circleY - circleRadius - strokeSize / 2
cacheCanvas.drawLine(halfWidth, 0f, halfWidth, lineYEnd, paint)
cacheCanvas.translate(halfWidth, translateY)
cacheCanvas.rotate(45f)
cacheCanvas.drawLine(
0f, halfWidth, halfWidth + halfStrokeSize, halfWidth,
paint
)
cacheCanvas.drawLine(halfWidth, 0f, halfWidth, halfWidth, paint)
cacheCanvas.rotate(-45f)
cacheCanvas.translate(-halfWidth, -translateY)
}
private fun drawNextArrowLine(
cacheCanvas: Canvas,
paint: Paint
) {
val height = measuredHeight
val width = measuredWidth
val centerX = width / 2f
cacheCanvas.drawLine(centerX, circleY, centerX, height.toFloat(), paint)
}
private fun drawInstanceCircle(cacheCanvas: Canvas) {
val width = measuredWidth
val circleX = width / 2f
val circleRadius = width / 3f
cacheCanvas.drawCircle(circleX, circleY, circleRadius, classNamePaint)
}
fun setType(type: Type) {
if (type != this.type) {
this.type = type
if (cache != null) {
cache!!.recycle()
cache = null
}
invalidate()
}
}
companion object {
private val SQRT_TWO = sqrt(2.0)
.toFloat()
private val CLEAR_XFER_MODE = PorterDuffXfermode(CLEAR)
}
}