1 /* 2 * Copyright (C) 2015 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package leakcanary.internal 17 18 import android.content.Context 19 import android.graphics.Bitmap 20 import android.graphics.Bitmap.Config.ARGB_8888 21 import android.graphics.Canvas 22 import android.graphics.Color 23 import android.graphics.DashPathEffect 24 import android.graphics.Paint 25 import android.graphics.PorterDuff.Mode.CLEAR 26 import android.graphics.PorterDuffXfermode 27 import android.util.AttributeSet 28 import android.view.View 29 import com.squareup.leakcanary.core.R 30 import leakcanary.internal.DisplayLeakConnectorView.Type.END 31 import leakcanary.internal.DisplayLeakConnectorView.Type.END_FIRST_UNREACHABLE 32 import leakcanary.internal.DisplayLeakConnectorView.Type.GC_ROOT 33 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_FIRST_UNREACHABLE 34 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_LAST_REACHABLE 35 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_REACHABLE 36 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNKNOWN 37 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNREACHABLE 38 import leakcanary.internal.DisplayLeakConnectorView.Type.START 39 import leakcanary.internal.DisplayLeakConnectorView.Type.START_LAST_REACHABLE 40 import leakcanary.internal.navigation.getColorCompat 41 import kotlin.math.sqrt 42 43 internal class DisplayLeakConnectorView( 44 context: Context, 45 attrs: AttributeSet 46 ) : View(context, attrs) { 47 48 private val classNamePaint: Paint 49 private val leakPaint: Paint 50 private val clearPaint: Paint 51 private val referencePaint: Paint 52 private val strokeSize: Float 53 private val circleY: Float 54 55 private var type: Type? = null 56 private var cache: Bitmap? = null 57 58 enum class Type { 59 GC_ROOT, 60 START, 61 START_LAST_REACHABLE, 62 NODE_UNKNOWN, 63 NODE_FIRST_UNREACHABLE, 64 NODE_UNREACHABLE, 65 NODE_REACHABLE, 66 NODE_LAST_REACHABLE, 67 END, 68 END_FIRST_UNREACHABLE 69 } 70 71 init { 72 73 val resources = resources 74 75 type = NODE_UNKNOWN 76 circleY = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_center_y) 77 .toFloat() 78 strokeSize = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_stroke_size) 79 .toFloat() 80 81 classNamePaint = Paint(Paint.ANTI_ALIAS_FLAG) 82 classNamePaint.color = context.getColorCompat(R.color.leak_canary_class_name) 83 classNamePaint.strokeWidth = strokeSize 84 85 86 leakPaint = Paint(Paint.ANTI_ALIAS_FLAG) 87 leakPaint.color = context.getColorCompat(R.color.leak_canary_leak) 88 leakPaint.style = Paint.Style.STROKE 89 leakPaint.strokeWidth = strokeSize 90 91 val pathLines = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_line) 92 .toFloat() 93 94 val pathGaps = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_gap) 95 .toFloat() 96 leakPaint.pathEffect = DashPathEffect(floatArrayOf(pathLines, pathGaps), 0f) 97 98 clearPaint = Paint(Paint.ANTI_ALIAS_FLAG) 99 clearPaint.color = Color.TRANSPARENT 100 clearPaint.xfermode = CLEAR_XFER_MODE 101 102 referencePaint = Paint(Paint.ANTI_ALIAS_FLAG) 103 referencePaint.color = context.getColorCompat(R.color.leak_canary_reference) 104 referencePaint.strokeWidth = strokeSize 105 } 106 onDrawnull107 override fun onDraw(canvas: Canvas) { 108 val width = measuredWidth 109 val height = measuredHeight 110 111 if (cache != null && (cache!!.width != width || cache!!.height != height)) { 112 cache!!.recycle() 113 cache = null 114 } 115 116 if (cache == null) { 117 cache = Bitmap.createBitmap(width, height, ARGB_8888) 118 119 val cacheCanvas = Canvas(cache!!) 120 121 when (type) { 122 NODE_UNKNOWN -> drawItems(cacheCanvas, leakPaint, leakPaint) 123 NODE_UNREACHABLE, NODE_REACHABLE -> drawItems( 124 cacheCanvas, referencePaint, referencePaint 125 ) 126 NODE_FIRST_UNREACHABLE -> drawItems( 127 cacheCanvas, leakPaint, referencePaint 128 ) 129 NODE_LAST_REACHABLE -> drawItems( 130 cacheCanvas, referencePaint, leakPaint 131 ) 132 START -> { 133 drawStartLine(cacheCanvas) 134 drawItems(cacheCanvas, null, referencePaint) 135 } 136 START_LAST_REACHABLE -> { 137 drawStartLine(cacheCanvas) 138 drawItems(cacheCanvas, null, leakPaint) 139 } 140 END -> drawItems(cacheCanvas, referencePaint, null) 141 END_FIRST_UNREACHABLE -> drawItems( 142 cacheCanvas, leakPaint, null 143 ) 144 GC_ROOT -> drawGcRoot(cacheCanvas) 145 else -> throw UnsupportedOperationException("Unknown type " + type!!) 146 } 147 } 148 canvas.drawBitmap(cache!!, 0f, 0f, null) 149 } 150 drawStartLinenull151 private fun drawStartLine(cacheCanvas: Canvas) { 152 val width = measuredWidth 153 val halfWidth = width / 2f 154 cacheCanvas.drawLine(halfWidth, 0f, halfWidth, circleY, classNamePaint) 155 } 156 drawGcRootnull157 private fun drawGcRoot( 158 cacheCanvas: Canvas 159 ) { 160 val width = measuredWidth 161 val height = measuredHeight 162 val halfWidth = width / 2f 163 cacheCanvas.drawLine(halfWidth, 0f, halfWidth, height.toFloat(), classNamePaint) 164 } 165 drawItemsnull166 private fun drawItems( 167 cacheCanvas: Canvas, 168 arrowHeadPaint: Paint?, 169 nextArrowPaint: Paint? 170 ) { 171 if (arrowHeadPaint != null) { 172 drawArrowHead(cacheCanvas, arrowHeadPaint) 173 } 174 if (nextArrowPaint != null) { 175 drawNextArrowLine(cacheCanvas, nextArrowPaint) 176 } 177 drawInstanceCircle(cacheCanvas) 178 } 179 drawArrowHeadnull180 private fun drawArrowHead( 181 cacheCanvas: Canvas, 182 paint: Paint 183 ) { 184 // Circle center is at half height 185 val width = measuredWidth 186 val halfWidth = width / 2f 187 val circleRadius = width / 3f 188 // Splitting the arrow head in two makes an isosceles right triangle. 189 // It's hypotenuse is side * sqrt(2) 190 val arrowHeight = halfWidth / 2 * SQRT_TWO 191 val halfStrokeSize = strokeSize / 2 192 val translateY = circleY - arrowHeight - circleRadius * 2 - strokeSize 193 194 val lineYEnd = circleY - circleRadius - strokeSize / 2 195 cacheCanvas.drawLine(halfWidth, 0f, halfWidth, lineYEnd, paint) 196 cacheCanvas.translate(halfWidth, translateY) 197 cacheCanvas.rotate(45f) 198 cacheCanvas.drawLine( 199 0f, halfWidth, halfWidth + halfStrokeSize, halfWidth, 200 paint 201 ) 202 cacheCanvas.drawLine(halfWidth, 0f, halfWidth, halfWidth, paint) 203 cacheCanvas.rotate(-45f) 204 cacheCanvas.translate(-halfWidth, -translateY) 205 } 206 drawNextArrowLinenull207 private fun drawNextArrowLine( 208 cacheCanvas: Canvas, 209 paint: Paint 210 ) { 211 val height = measuredHeight 212 val width = measuredWidth 213 val centerX = width / 2f 214 cacheCanvas.drawLine(centerX, circleY, centerX, height.toFloat(), paint) 215 } 216 drawInstanceCirclenull217 private fun drawInstanceCircle(cacheCanvas: Canvas) { 218 val width = measuredWidth 219 val circleX = width / 2f 220 val circleRadius = width / 3f 221 cacheCanvas.drawCircle(circleX, circleY, circleRadius, classNamePaint) 222 } 223 setTypenull224 fun setType(type: Type) { 225 if (type != this.type) { 226 this.type = type 227 if (cache != null) { 228 cache!!.recycle() 229 cache = null 230 } 231 invalidate() 232 } 233 } 234 235 companion object { 236 237 private val SQRT_TWO = sqrt(2.0) 238 .toFloat() 239 private val CLEAR_XFER_MODE = PorterDuffXfermode(CLEAR) 240 } 241 } 242