main

square/leakcanary

Last updated at: 29/12/2023 09:39

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)
  }
}