Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2636 from EpicDima/epicdima/support_squiggly_mult…
…iline Support multiline squiggly
- Loading branch information
Showing
5 changed files
with
265 additions
and
97 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryTextView.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package leakcanary.internal | ||
|
||
import android.content.Context | ||
import android.graphics.Canvas | ||
import android.text.Layout | ||
import android.text.Spanned | ||
import android.util.AttributeSet | ||
import android.widget.TextView | ||
|
||
/** | ||
* Modified TextView to fully support SquigglySpan. | ||
*/ | ||
internal class LeakCanaryTextView( | ||
context: Context, | ||
attrs: AttributeSet, | ||
) : TextView(context, attrs) { | ||
private val singleLineRenderer: SquigglySpanRenderer by lazy { SingleLineRenderer(context) } | ||
private val multiLineRenderer: SquigglySpanRenderer by lazy { MultiLineRenderer(context) } | ||
|
||
override fun onDraw(canvas: Canvas) { | ||
if (text is Spanned && layout != null) { | ||
val checkpoint = canvas.save() | ||
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat()) | ||
try { | ||
drawSquigglyLine(canvas, text as Spanned, layout) | ||
} finally { | ||
canvas.restoreToCount(checkpoint) | ||
} | ||
} | ||
super.onDraw(canvas) | ||
} | ||
|
||
private fun drawSquigglyLine(canvas: Canvas, text: Spanned, layout: Layout) { | ||
// ideally the calculations here should be cached since they are not cheap. However, proper | ||
// invalidation of the cache is required whenever anything related to text has changed. | ||
val squigglySpans = text.getSpans(0, text.length, SquigglySpan::class.java) | ||
for (span in squigglySpans) { | ||
val spanStart = text.getSpanStart(span) | ||
val spanEnd = text.getSpanEnd(span) | ||
val startLine = layout.getLineForOffset(spanStart) | ||
val endLine = layout.getLineForOffset(spanEnd) | ||
|
||
// start can be on the left or on the right depending on the language direction. | ||
val startOffset = (layout.getPrimaryHorizontal(spanStart) | ||
+ -1 * layout.getParagraphDirection(startLine)).toInt() | ||
|
||
// end can be on the left or on the right depending on the language direction. | ||
val endOffset = (layout.getPrimaryHorizontal(spanEnd) | ||
+ layout.getParagraphDirection(endLine)).toInt() | ||
|
||
val renderer = if (startLine == endLine) singleLineRenderer else multiLineRenderer | ||
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/SquigglySpanRenderer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package leakcanary.internal | ||
|
||
import android.content.Context | ||
import android.graphics.Canvas | ||
import android.graphics.Paint | ||
import android.graphics.Path | ||
import android.os.Build | ||
import android.text.Layout | ||
import com.squareup.leakcanary.core.R | ||
import kotlin.math.max | ||
import kotlin.math.min | ||
import kotlin.math.sin | ||
import leakcanary.internal.navigation.getColorCompat | ||
|
||
/** | ||
* The idea with a multiline span from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin | ||
*/ | ||
internal abstract class SquigglySpanRenderer(context: Context) { | ||
private val squigglyPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) | ||
private val path: Path | ||
private val halfStrokeWidth: Float | ||
private val amplitude: Float | ||
private val halfWaveHeight: Float | ||
private val periodDegrees: Float | ||
|
||
init { | ||
val resources = context.resources | ||
squigglyPaint.style = Paint.Style.STROKE | ||
squigglyPaint.color = context.getColorCompat(R.color.leak_canary_leak) | ||
val strokeWidth = | ||
resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_stroke_width) | ||
.toFloat() | ||
squigglyPaint.strokeWidth = strokeWidth | ||
|
||
halfStrokeWidth = strokeWidth / 2 | ||
amplitude = resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_amplitude) | ||
.toFloat() | ||
periodDegrees = | ||
resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_period_degrees) | ||
.toFloat() | ||
path = Path() | ||
val waveHeight = 2 * amplitude + strokeWidth | ||
halfWaveHeight = waveHeight / 2 | ||
} | ||
|
||
abstract fun draw( | ||
canvas: Canvas, | ||
layout: Layout, | ||
startLine: Int, | ||
endLine: Int, | ||
startOffset: Int, | ||
endOffset: Int | ||
) | ||
|
||
protected fun Canvas.drawSquigglyHorizontalPath(left: Float, right: Float, bottom: Float) { | ||
squigglyHorizontalPath( | ||
path = path, | ||
left = left + halfStrokeWidth, | ||
right = right - halfStrokeWidth, | ||
centerY = bottom - halfWaveHeight, | ||
amplitude = amplitude, | ||
periodDegrees = periodDegrees | ||
) | ||
drawPath(path, squigglyPaint) | ||
} | ||
|
||
protected fun getLineBottom(layout: Layout, line: Int): Int { | ||
var lineBottom = layout.getLineBottomWithoutSpacing(line) | ||
if (line == layout.lineCount - 1) { | ||
lineBottom -= layout.bottomPadding | ||
} | ||
return lineBottom | ||
} | ||
|
||
companion object { | ||
/** | ||
* Android system default line spacing extra | ||
*/ | ||
private const val DEFAULT_LINESPACING_EXTRA = 0f | ||
|
||
/** | ||
* Android system default line spacing multiplier | ||
*/ | ||
private const val DEFAULT_LINESPACING_MULTIPLIER = 1f | ||
|
||
private fun squigglyHorizontalPath( | ||
path: Path, | ||
left: Float, | ||
right: Float, | ||
centerY: Float, | ||
amplitude: Float, | ||
periodDegrees: Float | ||
) { | ||
path.reset() | ||
|
||
var y: Float | ||
path.moveTo(left, centerY) | ||
val period = (2 * Math.PI / periodDegrees).toFloat() | ||
|
||
var x = 0f | ||
while (x <= right - left) { | ||
y = (amplitude * sin((40 + period * x).toDouble()) + centerY).toFloat() | ||
path.lineTo(left + x, y) | ||
x += 1f | ||
} | ||
} | ||
|
||
private fun Layout.getLineBottomWithoutSpacing(line: Int): Int { | ||
val lineBottom = getLineBottom(line) | ||
val lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= 19 | ||
val isLastLine = line == lineCount - 1 | ||
|
||
val lineBottomWithoutSpacing: Int | ||
val lineSpacingExtra = spacingAdd | ||
val lineSpacingMultiplier = spacingMultiplier | ||
val hasLineSpacing = lineSpacingExtra != DEFAULT_LINESPACING_EXTRA | ||
|| lineSpacingMultiplier != DEFAULT_LINESPACING_MULTIPLIER | ||
|
||
lineBottomWithoutSpacing = if (!hasLineSpacing || isLastLine && lastLineSpacingNotAdded) { | ||
lineBottom | ||
} else { | ||
val extra = if (lineSpacingMultiplier.compareTo(DEFAULT_LINESPACING_MULTIPLIER) != 0) { | ||
val lineHeight = getLineTop(line + 1) - getLineTop(line) | ||
lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier | ||
} else { | ||
lineSpacingExtra | ||
} | ||
|
||
(lineBottom - extra).toInt() | ||
} | ||
|
||
return lineBottomWithoutSpacing | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Draws the background for text that starts and ends on the same line. | ||
*/ | ||
internal class SingleLineRenderer(context: Context) : SquigglySpanRenderer(context) { | ||
override fun draw( | ||
canvas: Canvas, | ||
layout: Layout, | ||
startLine: Int, | ||
endLine: Int, | ||
startOffset: Int, | ||
endOffset: Int | ||
) { | ||
canvas.drawSquigglyHorizontalPath( | ||
left = min(startOffset, endOffset).toFloat(), | ||
right = max(startOffset, endOffset).toFloat(), | ||
bottom = getLineBottom(layout, startLine).toFloat(), | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Draws the background for text that starts and ends on different lines. | ||
*/ | ||
internal class MultiLineRenderer(context: Context) : SquigglySpanRenderer(context) { | ||
override fun draw( | ||
canvas: Canvas, | ||
layout: Layout, | ||
startLine: Int, | ||
endLine: Int, | ||
startOffset: Int, | ||
endOffset: Int | ||
) { | ||
val isRtl = layout.getParagraphDirection(startLine) == Layout.DIR_RIGHT_TO_LEFT | ||
val lineEndOffset = if (isRtl) { | ||
layout.getLineLeft(startLine) | ||
} else { | ||
layout.getLineRight(startLine) | ||
} | ||
|
||
canvas.drawSquigglyHorizontalPath( | ||
left = startOffset.toFloat(), | ||
right = lineEndOffset, | ||
bottom = getLineBottom(layout, startLine).toFloat(), | ||
) | ||
|
||
for (line in startLine + 1 until endLine) { | ||
canvas.drawSquigglyHorizontalPath( | ||
left = layout.getLineLeft(line), | ||
right = layout.getLineRight(line), | ||
bottom = getLineBottom(layout, line).toFloat(), | ||
) | ||
} | ||
|
||
val lineStartOffset = if (isRtl) { | ||
layout.getLineRight(startLine) | ||
} else { | ||
layout.getLineLeft(startLine) | ||
} | ||
|
||
canvas.drawSquigglyHorizontalPath( | ||
left = lineStartOffset, | ||
right = endOffset.toFloat(), | ||
bottom = getLineBottom(layout, endLine).toFloat(), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.