diff --git a/markwon-round-textview/.gitignore b/markwon-round-textview/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/markwon-round-textview/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/markwon-round-textview/build.gradle b/markwon-round-textview/build.gradle
new file mode 100644
index 00000000..b255089c
--- /dev/null
+++ b/markwon-round-textview/build.gradle
@@ -0,0 +1,52 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ api project(':markwon-core')
+
+ deps.with {
+ // add a compileOnly dependency, so if this artifact is present
+ // we will try to obtain a SpanFactory for a Strikethrough node and use
+ // it to be consistent with markdown (please note that we do not use markwon plugin
+ // for that in case if different implementation is used)
+ compileOnly it['commonmark-strikethrough']
+
+ testImplementation it['ix-java']
+ }
+
+ deps.test.with {
+ testImplementation it['junit']
+ testImplementation it['robolectric']
+ }
+}
\ No newline at end of file
diff --git a/markwon-round-textview/consumer-rules.pro b/markwon-round-textview/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/markwon-round-textview/proguard-rules.pro b/markwon-round-textview/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/markwon-round-textview/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/markwon-round-textview/src/androidTest/java/io/noties/markdown/boundarytext/ExampleInstrumentedTest.kt b/markwon-round-textview/src/androidTest/java/io/noties/markdown/boundarytext/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..e9fc7c04
--- /dev/null
+++ b/markwon-round-textview/src/androidTest/java/io/noties/markdown/boundarytext/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package io.noties.markdown.boundarytext
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("io.noties.markdown.boundarytext.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/markwon-round-textview/src/main/AndroidManifest.xml b/markwon-round-textview/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..063beb55
--- /dev/null
+++ b/markwon-round-textview/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+ * author : NeXT + * time : 2018/12/11 + * desc : + * copy from: androidx.core.graphics.Canvas.kt + *+ */ + +inline fun Canvas.withTranslation( + x: Float = 0.0f, + y: Float = 0.0f, + block: Canvas.() -> Unit +) { + val checkpoint = save() + translate(x, y) + try { + block() + } finally { + restoreToCount(checkpoint) + } +} \ No newline at end of file diff --git a/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/LayoutExtensions.kt b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/LayoutExtensions.kt new file mode 100755 index 00000000..765132c8 --- /dev/null +++ b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/LayoutExtensions.kt @@ -0,0 +1,75 @@ +package io.noties.markdown.boundarytext +import android.os.Build +import android.text.Layout + +// Extension functions for Layout 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 + +/** + * Get the line bottom discarding the line spacing added. + */ +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 + + if (!hasLineSpacing || isLastLine && lastLineSpacingNotAdded) { + lineBottomWithoutSpacing = lineBottom + } else { + val extra: Float + if (lineSpacingMultiplier.compareTo(DEFAULT_LINESPACING_MULTIPLIER) != 0) { + val lineHeight = getLineHeight(line) + extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier + } else { + extra = lineSpacingExtra + } + + lineBottomWithoutSpacing = (lineBottom - extra).toInt() + } + + return lineBottomWithoutSpacing +} + +/** + * Get the line height of a line. + */ +fun Layout.getLineHeight(line: Int): Int { + return getLineTop(line + 1) - getLineTop(line) +} + +/** + * Returns the top of the Layout after removing the extra padding applied by the Layout. + */ +fun Layout.getLineTopWithoutPadding(line: Int): Int { + var lineTop = getLineTop(line) + if (line == 0) { + lineTop -= topPadding + } + return lineTop +} + +/** + * Returns the bottom of the Layout after removing the extra padding applied by the Layout. + */ +fun Layout.getLineBottomWithoutPadding(line: Int): Int { + var lineBottom = getLineBottomWithoutSpacing(line) + if (line == lineCount - 1) { + lineBottom -= bottomPadding + } + return lineBottom +} \ No newline at end of file diff --git a/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/RoundedBgTextView.kt b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/RoundedBgTextView.kt new file mode 100755 index 00000000..cdb9b468 --- /dev/null +++ b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/RoundedBgTextView.kt @@ -0,0 +1,70 @@ +package io.noties.markdown.boundarytext + +import android.content.Context +import android.graphics.Canvas +import android.text.Spanned +import android.util.AttributeSet +import android.widget.TextView + +/** + * A TextView that can draw rounded background to the portions of the text. See + * [TextRoundedBgHelper] for more information. + * + * See [TextRoundedBgAttributeReader] for supported attributes. + */ +class RoundedBgTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : TextView(context, attrs, defStyleAttr) { + + private lateinit var textRoundedBgHelper: TextRoundedBgHelper + private lateinit var attributeReader : TextRoundedBgAttributeReader + + private lateinit var mAttrs : AttributeSet + private var mTheme : String + + init { + mTheme = "light" + if (attrs != null) { + mAttrs = attrs + attributeReader = TextRoundedBgAttributeReader(context, mAttrs, mTheme) + textRoundedBgHelper = TextRoundedBgHelper( + horizontalPadding = attributeReader.horizontalPadding, + verticalPadding = attributeReader.verticalPadding, + drawable = attributeReader.drawable, + drawableLeft = attributeReader.drawableLeft, + drawableMid = attributeReader.drawableMid, + drawableRight = attributeReader.drawableRight + ) + } + + } + + fun setThemeChange(theme : String) { + mTheme = theme + attributeReader = TextRoundedBgAttributeReader(context, mAttrs, mTheme) + textRoundedBgHelper = TextRoundedBgHelper( + horizontalPadding = attributeReader.horizontalPadding, + verticalPadding = attributeReader.verticalPadding, + drawable = attributeReader.drawable, + drawableLeft = attributeReader.drawableLeft, + drawableMid = attributeReader.drawableMid, + drawableRight = attributeReader.drawableRight + ) + } + + fun setTextColorWith (textColor : Int){ + this.setTextColor(textColor) + } + + override fun onDraw(canvas: Canvas) { + // need to draw bg first so that text can be on top during super.onDraw() + if (text is Spanned && layout != null) { + canvas.withTranslation(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat()) { + textRoundedBgHelper.draw(canvas, text as Spanned, layout) + } + } + super.onDraw(canvas) + } +} \ No newline at end of file diff --git a/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgAttributeReader.kt b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgAttributeReader.kt new file mode 100755 index 00000000..bd9307c6 --- /dev/null +++ b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgAttributeReader.kt @@ -0,0 +1,53 @@ +package io.noties.markdown.boundarytext + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet + +class TextRoundedBgAttributeReader(context: Context, attrs: AttributeSet?, theme : String) { + + val horizontalPadding: Int + val verticalPadding: Int + val drawable: Drawable + val drawableLeft: Drawable + val drawableMid: Drawable + val drawableRight: Drawable + + init { + var typedArray = context.obtainStyledAttributes( + attrs, + R.styleable.TextRoundedBgHelper, + 0, + R.style.RoundedBgTextView + ) + if(theme.equals("dark")){ + typedArray = context.obtainStyledAttributes( + attrs, + R.styleable.TextRoundedBgHelper, + 0, + R.style.RoundedBgTextDarkView + ) + } + horizontalPadding = typedArray.getDimensionPixelSize( + R.styleable.TextRoundedBgHelper_roundedTextHorizontalPadding, + 0 + ) + verticalPadding = typedArray.getDimensionPixelSize( + R.styleable.TextRoundedBgHelper_roundedTextVerticalPadding, + 0 + ) + drawable = typedArray.getDrawable( + R.styleable.TextRoundedBgHelper_roundedTextDrawable + )!! + drawableLeft = typedArray.getDrawable( + R.styleable.TextRoundedBgHelper_roundedTextDrawableLeft + )!! + drawableMid = typedArray.getDrawable( + R.styleable.TextRoundedBgHelper_roundedTextDrawableMid + )!! + drawableRight = typedArray.getDrawable( + R.styleable.TextRoundedBgHelper_roundedTextDrawableRight + )!! + typedArray.recycle() + } +} diff --git a/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgHelper.kt b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgHelper.kt new file mode 100755 index 00000000..cb9b8fc5 --- /dev/null +++ b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgHelper.kt @@ -0,0 +1,68 @@ +package io.noties.markdown.boundarytext + + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.text.Layout +import android.text.Spanned +import io.noties.markwon.core.spans.CodeSpan + +class TextRoundedBgHelper( + val horizontalPadding: Int, + verticalPadding: Int, + drawable: Drawable, + drawableLeft: Drawable, + drawableMid: Drawable, + drawableRight: Drawable +) { + + private val singleLineRenderer: TextRoundedBgRenderer by lazy { + SingleLineRenderer( + horizontalPadding = horizontalPadding, + verticalPadding = verticalPadding, + drawable = drawable + ) + } + + private val multiLineRenderer: TextRoundedBgRenderer by lazy { + MultiLineRenderer( + horizontalPadding = horizontalPadding, + verticalPadding = verticalPadding, + drawableLeft = drawableLeft, + drawableMid = drawableMid, + drawableRight = drawableRight + ) + } + + /** + * Call this function during onDraw of another widget such as TextView. + * + * @param canvas Canvas to draw onto + * @param text + * @param layout Layout that contains the text + */ + fun draw(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 spans = text.getSpans(0, text.length, Annotation::class.java) + val spans = text.getSpans(0, text.length, CodeSpan::class.java) + spans.forEach { span -> +// if (span.value.equals("rounded")) { + 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) * horizontalPadding).toInt() + // end can be on the left or on the right depending on the language direction. + val endOffset = (layout.getPrimaryHorizontal(spanEnd) + + layout.getParagraphDirection(endLine) * horizontalPadding).toInt() + + val renderer = if (startLine == endLine) singleLineRenderer else multiLineRenderer + renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset) +// } + } + } +} \ No newline at end of file diff --git a/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgRenderer.kt b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgRenderer.kt new file mode 100755 index 00000000..8bbe84a8 --- /dev/null +++ b/markwon-round-textview/src/main/java/io/noties/markdown/boundarytext/TextRoundedBgRenderer.kt @@ -0,0 +1,194 @@ +package io.noties.markdown.boundarytext + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.text.Layout +import kotlin.math.max +import kotlin.math.min + +/** + * Base class for single and multi line rounded background renderers. + * + * @param horizontalPadding the padding to be applied to left & right of the background + * @param verticalPadding the padding to be applied to top & bottom of the background + */ +internal abstract class TextRoundedBgRenderer( + val horizontalPadding: Int, + val verticalPadding: Int +) { + + /** + * Draw the background that starts at the {@code startOffset} and ends at {@code endOffset}. + * + * @param canvas Canvas to draw onto + * @param layout Layout that contains the text + * @param startLine the start line for the background + * @param endLine the end line for the background + * @param startOffset the character offset that the background should start at + * @param endOffset the character offset that the background should end at + */ + abstract fun draw( + canvas: Canvas, + layout: Layout, + startLine: Int, + endLine: Int, + startOffset: Int, + endOffset: Int + ) + + /** + * Get the top offset of the line and add padding into account so that there is a gap between + * top of the background and top of the text. + * + * @param layout Layout object that contains the text + * @param line line number + */ + protected fun getLineTop(layout: Layout, line: Int): Int { + return layout.getLineTopWithoutPadding(line) - verticalPadding + } + + /** + * Get the bottom offset of the line and add padding into account so that there is a gap between + * bottom of the background and bottom of the text. + * + * @param layout Layout object that contains the text + * @param line line number + */ + protected fun getLineBottom(layout: Layout, line: Int): Int { + return layout.getLineBottomWithoutPadding(line) + verticalPadding + } +} + +/** + * Draws the background for text that starts and ends on the same line. + * + * @param horizontalPadding the padding to be applied to left & right of the background + * @param verticalPadding the padding to be applied to top & bottom of the background + * @param drawable the drawable used to draw the background + */ +internal class SingleLineRenderer( + horizontalPadding: Int, + verticalPadding: Int, + val drawable: Drawable +) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) { + + override fun draw( + canvas: Canvas, + layout: Layout, + startLine: Int, + endLine: Int, + startOffset: Int, + endOffset: Int + ) { + val lineTop = getLineTop(layout, startLine) + val lineBottom = getLineBottom(layout, startLine) + // get min of start/end for left, and max of start/end for right since we don't + // the language direction + val left = min(startOffset, endOffset) + val right = max(startOffset, endOffset) + drawable.setBounds(left, lineTop, right, lineBottom) + drawable.draw(canvas) + } +} + +/** + * Draws the background for text that starts and ends on different lines. + * + * @param horizontalPadding the padding to be applied to left & right of the background + * @param verticalPadding the padding to be applied to top & bottom of the background + * @param drawableLeft the drawable used to draw left edge of the background + * @param drawableMid the drawable used to draw for whole line + * @param drawableRight the drawable used to draw right edge of the background + */ +internal class MultiLineRenderer( + horizontalPadding: Int, + verticalPadding: Int, + val drawableLeft: Drawable, + val drawableMid: Drawable, + val drawableRight: Drawable +) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) { + + override fun draw( + canvas: Canvas, + layout: Layout, + startLine: Int, + endLine: Int, + startOffset: Int, + endOffset: Int + ) { + // draw the first line + val paragDir = layout.getParagraphDirection(startLine) + val lineEndOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) { + layout.getLineLeft(startLine) - horizontalPadding + } else { + layout.getLineRight(startLine) + horizontalPadding + }.toInt() + + var lineBottom = getLineBottom(layout, startLine) + var lineTop = getLineTop(layout, startLine) + drawStart(canvas, startOffset, lineTop, lineEndOffset, lineBottom) + + // for the lines in the middle draw the mid drawable + for (line in startLine + 1 until endLine) { + lineTop = getLineTop(layout, line) + lineBottom = getLineBottom(layout, line) + drawableMid.setBounds( + (layout.getLineLeft(line).toInt() - horizontalPadding), + lineTop, + (layout.getLineRight(line).toInt() + horizontalPadding), + lineBottom + ) + drawableMid.draw(canvas) + } + + val lineStartOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) { + layout.getLineRight(startLine) + horizontalPadding + } else { + layout.getLineLeft(startLine) - horizontalPadding + }.toInt() + + // draw the last line + lineBottom = getLineBottom(layout, endLine) + lineTop = getLineTop(layout, endLine) + + drawEnd(canvas, lineStartOffset, lineTop, endOffset, lineBottom) + } + + /** + * Draw the first line of a multiline annotation. Handles LTR/RTL. + * + * @param canvas Canvas to draw onto + * @param start start coordinate for the background + * @param top top coordinate for the background + * @param end end coordinate for the background + * @param bottom bottom coordinate for the background + */ + private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) { + if (start > end) { + drawableRight.setBounds(end, top, start, bottom) + drawableRight.draw(canvas) + } else { + drawableLeft.setBounds(start, top, end, bottom) + drawableLeft.draw(canvas) + } + } + + /** + * Draw the last line of a multiline annotation. Handles LTR/RTL. + * + * @param canvas Canvas to draw onto + * @param start start coordinate for the background + * @param top top position for the background + * @param end end coordinate for the background + * @param bottom bottom coordinate for the background + */ + private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) { + if (start > end) { + drawableLeft.setBounds(end, top, start, bottom) + drawableLeft.draw(canvas) + } else { + drawableRight.setBounds(start, top, end, bottom) + drawableRight.draw(canvas) + } + } +} \ No newline at end of file diff --git a/markwon-round-textview/src/main/res/drawable/rounded_text_bg.xml b/markwon-round-textview/src/main/res/drawable/rounded_text_bg.xml new file mode 100755 index 00000000..080efc4b --- /dev/null +++ b/markwon-round-textview/src/main/res/drawable/rounded_text_bg.xml @@ -0,0 +1,21 @@ + + +