diff --git a/core-ui/build.gradle b/core-ui/build.gradle index b50804a0f5..25d26d6845 100644 --- a/core-ui/build.gradle +++ b/core-ui/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation applicationDependencies.appcompat implementation applicationDependencies.kotlin implementation applicationDependencies.coroutines + implementation applicationDependencies.androidxCore implementation applicationDependencies.design implementation applicationDependencies.recyclerView diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/common/Markup.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/common/Markup.kt index b62b3adbc1..9938a4ed92 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/common/Markup.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/common/Markup.kt @@ -2,10 +2,13 @@ package com.agileburo.anytype.core_ui.common import android.graphics.Color import android.graphics.Typeface +import android.text.Annotation import android.text.Editable import android.text.Spannable import android.text.SpannableString import android.text.style.* +import com.agileburo.anytype.core_ui.widgets.text.KEY_ROUNDED +import com.agileburo.anytype.core_ui.widgets.text.VALUE_ROUNDED /** * Classes implementing this interface should support markup rendering. @@ -47,11 +50,13 @@ interface Markup { STRIKETHROUGH, TEXT_COLOR, BACKGROUND_COLOR, - LINK + LINK, + KEYBOARD } companion object { const val DEFAULT_SPANNABLE_FLAG = Spannable.SPAN_EXCLUSIVE_INCLUSIVE + const val SPAN_MONOSPACE = "monospace" } } @@ -94,6 +99,19 @@ fun Markup.toSpannable() = SpannableString(body).apply { mark.to, Markup.DEFAULT_SPANNABLE_FLAG ) + Markup.Type.KEYBOARD -> setSpan( + TypefaceSpan(Markup.SPAN_MONOSPACE), + mark.from, + mark.to, + Markup.DEFAULT_SPANNABLE_FLAG + ).also { + setSpan( + Annotation(KEY_ROUNDED, VALUE_ROUNDED), + mark.from, + mark.to, + Markup.DEFAULT_SPANNABLE_FLAG + ) + } } } } @@ -102,6 +120,9 @@ fun Editable.setMarkup(markup: Markup) { getSpans(0, length, CharacterStyle::class.java).forEach { span -> removeSpan(span) } + getSpans(0, length, Annotation::class.java).forEach { span -> + removeSpan(span) + } markup.marks.forEach { mark -> when (mark.type) { Markup.Type.ITALIC -> setSpan( @@ -142,6 +163,21 @@ fun Editable.setMarkup(markup: Markup) { Markup.DEFAULT_SPANNABLE_FLAG ) } + Markup.Type.KEYBOARD -> { + setSpan( + TypefaceSpan(Markup.SPAN_MONOSPACE), + mark.from, + mark.to, + Markup.DEFAULT_SPANNABLE_FLAG + ).also { + setSpan( + Annotation(KEY_ROUNDED, VALUE_ROUNDED), + mark.from, + mark.to, + Markup.DEFAULT_SPANNABLE_FLAG + ) + } + } } } } diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/LayoutExtensions.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/LayoutExtensions.kt new file mode 100644 index 0000000000..c968c7f82c --- /dev/null +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/LayoutExtensions.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 com.agileburo.anytype.core_ui.widgets.text + +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/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt index c206e4aa2f..abac057314 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt @@ -1,10 +1,13 @@ package com.agileburo.anytype.core_ui.widgets.text import android.content.Context +import android.graphics.Canvas +import android.text.Spanned import android.text.TextWatcher import android.text.util.Linkify import android.util.AttributeSet import androidx.appcompat.widget.AppCompatEditText +import androidx.core.graphics.withTranslation import com.agileburo.anytype.core_ui.extensions.toast import me.saket.bettermovementmethod.BetterLinkMovementMethod import timber.log.Timber @@ -12,16 +15,29 @@ import timber.log.Timber class TextInputWidget : AppCompatEditText { private val watchers: MutableList = mutableListOf() + private val textRoundedBgHelper: TextRoundedBgHelper var selectionDetector: ((IntRange) -> Unit)? = null - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyle: Int = android.R.attr.textViewStyle) : super( context, attrs, defStyle - ) + ) { + val attributeReader = TextRoundedBgAttributeReader(context, attrs) + textRoundedBgHelper = TextRoundedBgHelper( + horizontalPadding = attributeReader.horizontalPadding, + verticalPadding = attributeReader.verticalPadding, + drawable = attributeReader.drawable, + drawableLeft = attributeReader.drawableLeft, + drawableMid = attributeReader.drawableMid, + drawableRight = attributeReader.drawableRight + ) + } override fun addTextChangedListener(watcher: TextWatcher) { watchers.add(watcher) @@ -44,6 +60,16 @@ class TextInputWidget : AppCompatEditText { super.onSelectionChanged(selStart, selEnd) } + 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) + } + fun setLinksClickable() { //makeLinksActive() } diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgAttributeReader.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgAttributeReader.kt new file mode 100644 index 0000000000..40223dc2b1 --- /dev/null +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgAttributeReader.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 com.agileburo.anytype.core_ui.widgets.text + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.core.content.res.getDrawableOrThrow +import com.agileburo.anytype.core_ui.R + +/** + * Reads default attributes that [TextRoundedBgHelper] needs from resources. The attributes read + * are: + * + * - chHorizontalPadding: the padding to be applied to left & right of the background + * - chVerticalPadding: the padding to be applied to top & bottom of the background + * - chDrawable: the drawable used to draw the background + * - chDrawableLeft: the drawable used to draw left edge of the background + * - chDrawableMid: the drawable used to draw for whole line + * - chDrawableRight: the drawable used to draw right edge of the background + */ +class TextRoundedBgAttributeReader(context: Context, attrs: AttributeSet?) { + + val horizontalPadding: Int + val verticalPadding: Int + val drawable: Drawable + val drawableLeft: Drawable + val drawableMid: Drawable + val drawableRight: Drawable + + init { + val typedArray = context.obtainStyledAttributes( + attrs, + R.styleable.TextRoundedBgHelper, + 0, + R.style.RoundedBgTextView + ) + horizontalPadding = typedArray.getDimensionPixelSize( + R.styleable.TextRoundedBgHelper_roundedTextHorizontalPadding, + 0 + ) + verticalPadding = typedArray.getDimensionPixelSize( + R.styleable.TextRoundedBgHelper_roundedTextVerticalPadding, + 0 + ) + drawable = typedArray.getDrawableOrThrow( + R.styleable.TextRoundedBgHelper_roundedTextDrawable + ) + drawableLeft = typedArray.getDrawableOrThrow( + R.styleable.TextRoundedBgHelper_roundedTextDrawableLeft + ) + drawableMid = typedArray.getDrawableOrThrow( + R.styleable.TextRoundedBgHelper_roundedTextDrawableMid + ) + drawableRight = typedArray.getDrawableOrThrow( + R.styleable.TextRoundedBgHelper_roundedTextDrawableRight + ) + typedArray.recycle() + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgHelper.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgHelper.kt new file mode 100644 index 0000000000..bd4304f875 --- /dev/null +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgHelper.kt @@ -0,0 +1,90 @@ +package com.agileburo.anytype.core_ui.widgets.text + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.text.Annotation +import android.text.Layout +import android.text.Spanned + +/** + * Helper class to draw multi-line rounded background to certain parts of a text. The start/end + * positions of the backgrounds are annotated with [android.text.Annotation] class. Each annotation + * should have the annotation key set to **rounded**. + * + * i.e.: + * ``` + * + * "this is a regular paragraph." + * ``` + * + * **Note:** BiDi text is not supported. + * + * @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 + * @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 + */ +const val KEY_ROUNDED = "key" +const val VALUE_ROUNDED = "rounded" + +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) + spans.forEach { span -> + if (span.value == VALUE_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/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgRenderer.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgRenderer.kt new file mode 100644 index 0000000000..f532fa5961 --- /dev/null +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextRoundedBgRenderer.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 com.agileburo.anytype.core_ui.widgets.text + +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/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MarkupToolbarWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MarkupToolbarWidget.kt index fcdf8bae9e..528c3a8557 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MarkupToolbarWidget.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MarkupToolbarWidget.kt @@ -40,11 +40,12 @@ class MarkupToolbarWidget : ConstraintLayout { LayoutInflater.from(context).inflate(R.layout.widget_markup_toolbar, this) } - fun markupClicks() = flowOf(bold(), italic(), strike(), link()).flattenMerge() + fun markupClicks() = flowOf(bold(), italic(), strike(), link(), code()).flattenMerge() private fun bold() = bold.clicks().map { Markup.Type.BOLD } private fun italic() = italic.clicks().map { Markup.Type.ITALIC } private fun strike() = strike.clicks().map { Markup.Type.STRIKETHROUGH } private fun link() = link.clicks().map { Markup.Type.LINK } + private fun code() = code.clicks().map { Markup.Type.KEYBOARD } fun colorClicks() = color.clicks() fun hideKeyboardClicks() = keyboard.clicks() diff --git a/core-ui/src/main/res/drawable/rounded_text_bg.xml b/core-ui/src/main/res/drawable/rounded_text_bg.xml new file mode 100644 index 0000000000..9e3c93a827 --- /dev/null +++ b/core-ui/src/main/res/drawable/rounded_text_bg.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/rounded_text_bg_left.xml b/core-ui/src/main/res/drawable/rounded_text_bg_left.xml new file mode 100644 index 0000000000..707da90a2c --- /dev/null +++ b/core-ui/src/main/res/drawable/rounded_text_bg_left.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/rounded_text_bg_mid.xml b/core-ui/src/main/res/drawable/rounded_text_bg_mid.xml new file mode 100644 index 0000000000..c1c0afbdfd --- /dev/null +++ b/core-ui/src/main/res/drawable/rounded_text_bg_mid.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/rounded_text_bg_right.xml b/core-ui/src/main/res/drawable/rounded_text_bg_right.xml new file mode 100644 index 0000000000..7e074af745 --- /dev/null +++ b/core-ui/src/main/res/drawable/rounded_text_bg_right.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/values/attrs.xml b/core-ui/src/main/res/values/attrs.xml index b73bc04515..2264b4bf95 100644 --- a/core-ui/src/main/res/values/attrs.xml +++ b/core-ui/src/main/res/values/attrs.xml @@ -6,4 +6,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/values/colors.xml b/core-ui/src/main/res/values/colors.xml index bdeda845e8..e28338121c 100644 --- a/core-ui/src/main/res/values/colors.xml +++ b/core-ui/src/main/res/values/colors.xml @@ -24,6 +24,8 @@ #BDBDBD #ACA996 + + #F3F2EC #2C2B27 diff --git a/core-ui/src/main/res/values/dimens.xml b/core-ui/src/main/res/values/dimens.xml index 9218308764..d273e31d9c 100644 --- a/core-ui/src/main/res/values/dimens.xml +++ b/core-ui/src/main/res/values/dimens.xml @@ -75,4 +75,7 @@ 6dp 8dp + 4dp + 1dp + \ No newline at end of file diff --git a/core-ui/src/main/res/values/styles.xml b/core-ui/src/main/res/values/styles.xml index b7d508f371..9ca2b37f9d 100644 --- a/core-ui/src/main/res/values/styles.xml +++ b/core-ui/src/main/res/values/styles.xml @@ -212,4 +212,13 @@ bold + + \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt index a677e42ea6..b8a861d3a8 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt @@ -65,13 +65,20 @@ fun BlockEntity.Content.Text.Mark.toMiddleware(): Block.Content.Text.Mark { .build() } BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR -> { - Models.Block.Content.Text.Mark + Block.Content.Text.Mark .newBuilder() - .setType(Models.Block.Content.Text.Mark.Type.BackgroundColor) + .setType(Block.Content.Text.Mark.Type.BackgroundColor) .setRange(rangeModel) .setParam(param as String) .build() } + BlockEntity.Content.Text.Mark.Type.KEYBOARD -> { + Block.Content.Text.Mark + .newBuilder() + .setType(Block.Content.Text.Mark.Type.Keyboard) + .setRange(rangeModel) + .build() + } else -> throw IllegalStateException("Unsupported mark type: ${type.name}") } } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt index 6819056593..ef0c7999b9 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt @@ -129,6 +129,13 @@ private fun mapMarks(content: Block.Content.Text): List = param = checkNotNull(mark.param) ) } + Block.Content.Text.Mark.Type.KEYBOARD -> { + Markup.Mark( + from = mark.range.first, + to = mark.range.last, + type = Markup.Type.KEYBOARD + ) + } else -> null } } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt index eb05fae9c2..5e162b7351 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt @@ -235,6 +235,7 @@ class PageViewModel( Markup.Type.TEXT_COLOR -> Block.Content.Text.Mark.Type.TEXT_COLOR Markup.Type.LINK -> Block.Content.Text.Mark.Type.LINK Markup.Type.BACKGROUND_COLOR -> Block.Content.Text.Mark.Type.BACKGROUND_COLOR + Markup.Type.KEYBOARD -> Block.Content.Text.Mark.Type.KEYBOARD }, param = action.param ) @@ -287,11 +288,11 @@ class PageViewModel( Timber.d("Preparing views for focus: $focus") models.asMap().asRender(pageId).mapNotNull { block -> - when { - block.content is Block.Content.Text -> { + when (block.content) { + is Block.Content.Text -> { block.toView(focused = block.id == focus) } - block.content is Block.Content.Image -> { + is Block.Content.Image -> { block.toView() } else -> null diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000000..51cd844076 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + + defaultConfig { + applicationId "com.agileburo.anytype.sample" + minSdkVersion 26 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + + implementation project(':core-utils') + implementation project(':core-ui') + + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/sample/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 diff --git a/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt b/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..b54bfc5b5a --- /dev/null +++ b/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.agileburo.anytype.sample + +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("com.agileburo.anytype.sample", appContext.packageName) + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a9ce5f8191 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/agileburo/anytype/sample/MainActivity.kt b/sample/src/main/java/com/agileburo/anytype/sample/MainActivity.kt new file mode 100644 index 0000000000..7d16f870ec --- /dev/null +++ b/sample/src/main/java/com/agileburo/anytype/sample/MainActivity.kt @@ -0,0 +1,90 @@ +package com.agileburo.anytype.sample + +import android.os.Bundle +import android.text.Annotation +import android.text.Spannable +import android.text.SpannableString +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.item_test.view.* + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val annotation = Annotation("key", "rounded") + val list = mutableListOf() + + val range = IntRange(0, 50) + range.forEach { + + var t = "I am block number $it" + val spannable = SpannableString(t) + if (it == 10) { + spannable.setSpan(annotation, 0, t.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + list.add(Item(text = spannable)) + } else { + list.add(Item(text = spannable)) + } + } + + with(recyclerView) { + layoutManager = LinearLayoutManager(this@MainActivity) + addItemDecoration( + DividerItemDecoration( + this@MainActivity, + DividerItemDecoration.VERTICAL + ) + ) + adapter = MarkupAdapter(list) { pos: Int -> + (this.adapter as MarkupAdapter).setData( + Item(SpannableString("I am block number $pos")), + pos + ) + + } + } + + } + + class MarkupAdapter(private val data: MutableList, private val listener: (Int) -> Unit) : + RecyclerView.Adapter() { + + fun setData(item: Item, position: Int) { + data[position] = item + notifyItemChanged(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MarkupViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false) + return MarkupViewHolder(view) + } + + override fun getItemCount(): Int = data.size + + override fun onBindViewHolder(holder: MarkupViewHolder, position: Int) { + holder.bind(data[position].text, listener) + } + + class MarkupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + fun bind(text: SpannableString, listener: (Int) -> Unit) { + itemView.item.text = text + itemView.setOnClickListener { + listener.invoke(adapterPosition) + } + } + } + } +} + +data class Item(val text: SpannableString) diff --git a/sample/src/main/java/com/agileburo/anytype/sample/RoundedBgTextView.kt b/sample/src/main/java/com/agileburo/anytype/sample/RoundedBgTextView.kt new file mode 100644 index 0000000000..81180db374 --- /dev/null +++ b/sample/src/main/java/com/agileburo/anytype/sample/RoundedBgTextView.kt @@ -0,0 +1,42 @@ +package com.agileburo.anytype.sample + +import android.content.Context +import android.graphics.Canvas +import android.text.Spanned +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.graphics.withTranslation +import com.agileburo.anytype.core_ui.widgets.text.TextRoundedBgAttributeReader +import com.agileburo.anytype.core_ui.widgets.text.TextRoundedBgHelper + +class RoundedBgTextView : AppCompatTextView { + + private val textRoundedBgHelper: TextRoundedBgHelper + + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle + ) : super(context, attrs, defStyleAttr) { + val attributeReader = TextRoundedBgAttributeReader(context, attrs) + textRoundedBgHelper = TextRoundedBgHelper( + horizontalPadding = attributeReader.horizontalPadding, + verticalPadding = attributeReader.verticalPadding, + drawable = attributeReader.drawable, + drawableLeft = attributeReader.drawableLeft, + drawableMid = attributeReader.drawableMid, + drawableRight = attributeReader.drawableRight + ) + } + + 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/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..0d025f9bf6 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..9c6e51f16c --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/item_test.xml b/sample/src/main/res/layout/item_test.xml new file mode 100644 index 0000000000..a99b4e36ce --- /dev/null +++ b/sample/src/main/res/layout/item_test.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..eca70cfe52 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..eca70cfe52 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..898f3ed59a Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..dffca3601e Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..64ba76f75e Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..dae5e08234 Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..e5ed46597e Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..14ed0af350 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..b0907cac3b Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..d8ae031549 Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..2c18de9e66 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..beed3cdd2c Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000000..69b22338c6 --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000000..1ad8c299e9 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Sample + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000000..5885930df6 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt b/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt new file mode 100644 index 0000000000..df82d91f98 --- /dev/null +++ b/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.agileburo.anytype.sample + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle b/settings.gradle index cb98617c9e..2308e75a99 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ':app', + ':sample', ':core-utils', ':middleware', ':protobuf',