From e76ac6abbd2e758e97fdddd43acba03984499d81 Mon Sep 17 00:00:00 2001 From: cgspine Date: Mon, 21 Mar 2022 15:15:25 +0800 Subject: [PATCH] feat: ul and header. --- .../com/qmuiteam/editor/EditorBehavior.kt | 38 +- .../java/com/qmuiteam/editor/QMUIEditor.kt | 360 +++++++++++++----- .../main/java/com/qmuiteam/editor/WordEdit.kt | 4 +- .../src/main/java/com/qmuiteam/plugin/Dep.kt | 2 +- .../qmuidemo/fragment/lab/QDEditorFragment.kt | 16 +- 5 files changed, 311 insertions(+), 109 deletions(-) diff --git a/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt b/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt index 1164baeb..041b175c 100644 --- a/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt +++ b/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt @@ -1,12 +1,16 @@ package com.qmuiteam.editor -import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp interface EditorBehavior { fun apply(value: TextFieldValue): TextFieldValue } +internal fun String.isHeaderTag(): Boolean{ + return HeaderLevel.values().find { it.tag == this } != null +} class Bold(val weight: Int) : EditorBehavior { override fun apply(value: TextFieldValue): TextFieldValue { @@ -14,8 +18,36 @@ class Bold(val weight: Int) : EditorBehavior { } } -class Quote : EditorBehavior { +object Quote : EditorBehavior { + + const val tag = "quote" + override fun apply(value: TextFieldValue): TextFieldValue { return value.quote() } -} \ No newline at end of file +} + + +object UnOrderList : EditorBehavior { + + const val tag = "ul" + + override fun apply(value: TextFieldValue): TextFieldValue { + return value.unOrder() + } +} + +class Header(val level: HeaderLevel): EditorBehavior { + + override fun apply(value: TextFieldValue): TextFieldValue { + return value.header(level) + } +} + +enum class HeaderLevel(val tag: String, val fontSize: TextUnit) { + h1("h1", 24.sp), + h2("h2", 22.sp), + h3("h3", 20.sp), + h4("h4", 18.sp), + h5("h5", 16.sp) +} diff --git a/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt b/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt index 37970e9e..3bba8928 100644 --- a/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt +++ b/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt @@ -3,10 +3,13 @@ package com.qmuiteam.editor import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Rect @@ -14,10 +17,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextIndent @@ -28,7 +28,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch - interface EditorDecoration { @Composable fun Compose() @@ -43,9 +42,9 @@ class QuoteDecoration(val rect: Rect) : EditorDecoration { } Box( Modifier - .offset(dpRect.left, dpRect.top) + .offset(dpRect.left, dpRect.top - 6.dp) .width(dpRect.width) - .height(dpRect.height) + .height(dpRect.height + 12.dp) .background(Color.LightGray) ) { Box( @@ -59,11 +58,33 @@ class QuoteDecoration(val rect: Rect) : EditorDecoration { } } + +class UnOrderedDecoration(val rect: Rect) : EditorDecoration { + @Composable + override fun Compose() { + key(this) { + val dpRect = with(LocalDensity.current) { + DpRect(rect.left.toDp(), rect.top.toDp(), rect.right.toDp(), rect.bottom.toDp()) + } + Box( + Modifier + .offset(dpRect.left, dpRect.top + dpRect.height / 2 - 2.dp) + .width(4.dp) + .height(4.dp) + .clip(CircleShape) + .background(Color.Black) + ) + } + } +} + @Composable fun QMUIEditor( modifier: Modifier = Modifier, value: TextFieldValue, channel: Channel, + hint: AnnotatedString = AnnotatedString(""), + hintStyle: TextStyle = TextStyle.Default.copy(color = Color.Gray), textStyle: TextStyle = TextStyle.Default, focusRequester: FocusRequester = remember { FocusRequester() @@ -102,20 +123,29 @@ fun QMUIEditor( onTextLayout = { val list = mutableListOf() it.layoutInput.text.paragraphStyles.forEach { paragraph -> - if (paragraph.tag == "quote") { + val rect = if(paragraph.start == paragraph.end){ + val cursorRect = it.multiParagraph.getCursorRect(paragraph.start) + Rect( + 0f, + cursorRect.top, + it.multiParagraph.width, + cursorRect.bottom + ) + }else{ val start = it.multiParagraph.getBoundingBox(paragraph.start) val end = it.multiParagraph.getBoundingBox(paragraph.end - 1) - list.add( - QuoteDecoration( - Rect( - 0f, - start.top, - it.multiParagraph.width, - end.bottom - ) - ) + Rect( + 0f, + start.top, + it.multiParagraph.width, + end.bottom ) } + if (paragraph.tag == Quote.tag) { + list.add(QuoteDecoration(rect)) + } else if (paragraph.tag == UnOrderList.tag) { + list.add(UnOrderedDecoration(rect)) + } } editorDecorations = list }, @@ -135,6 +165,9 @@ fun QMUIEditor( it.Compose() } } + if(textFieldValue.text.isEmpty()){ + Text(text = hint, style = hintStyle) + } innerTextField() } ) @@ -142,35 +175,35 @@ fun QMUIEditor( } } -private fun TextFieldValue.check(): TextFieldValue{ +private fun TextFieldValue.check(): TextFieldValue { val paragraphs = mutableListOf>() var currentIndex = 0 var nextIndex = text.indexOf('\n') - while (nextIndex >= 0){ - val exist = annotatedString.paragraphStyles.find { it.start == 0 && it.end == nextIndex+1} - if(exist == null){ - paragraphs.add(AnnotatedString.Range(ParagraphStyle(), currentIndex, nextIndex+1, "p")) - }else{ + while (nextIndex >= 0) { + val exist = annotatedString.paragraphStyles.find { it.start == 0 && it.end == nextIndex + 1 } + if (exist == null) { + paragraphs.add(AnnotatedString.Range(ParagraphStyle(), currentIndex, nextIndex + 1, "p")) + } else { paragraphs.add(exist) } - currentIndex = nextIndex+1 + currentIndex = nextIndex + 1 nextIndex = text.indexOf('\n', currentIndex) } - if(currentIndex < text.length){ - val exist = annotatedString.paragraphStyles.find { it.start == 0 && it.end == text.length} - if(exist == null){ + if (currentIndex < text.length) { + val exist = annotatedString.paragraphStyles.find { it.start == 0 && it.end == text.length } + if (exist == null) { paragraphs.add(AnnotatedString.Range(ParagraphStyle(), currentIndex, text.length, "p")) - }else{ + } else { paragraphs.add(exist) } } - if(text.isEmpty() || (selection.collapsed && selection.end == text.length)){ - val exist = annotatedString.paragraphStyles.find { it.start == text.length && it.end == text.length} - if(exist == null){ + if (text.isEmpty() || (selection.collapsed && selection.end == text.length)) { + val exist = annotatedString.paragraphStyles.find { it.start == text.length && it.end == text.length } + if (exist == null) { paragraphs.add(AnnotatedString.Range(ParagraphStyle(), text.length, text.length, "p")) - }else{ + } else { paragraphs.add(exist) } } @@ -181,39 +214,48 @@ private fun TextFieldValue.check(): TextFieldValue{ ) } -private fun TextFieldValue.modify( - block: (spans: MutableList>, paragraphs: MutableList>) -> Unit +private fun TextFieldValue.modifySpans( + block: (spans: MutableList>) -> Unit ): TextFieldValue { - val currentMutableStyles = mutableListOf>() - val currentMutableParagraph = mutableListOf>() + val mutableSpans = mutableListOf>() annotatedString.spanStyles.forEach { - if (it.start != it.end) { - currentMutableStyles.add(MutableRange(it.item, it.start, it.end, it.tag)) - } - + mutableSpans.add(MutableRange(it.item, it.start, it.end, it.tag)) } - annotatedString.paragraphStyles.forEach { - if (it.start != it.end) { - currentMutableParagraph.add(MutableRange(it.item, it.start, it.end, it.tag)) - } - } - block(currentMutableStyles, currentMutableParagraph) - val spanStyles = currentMutableStyles.map { + block(mutableSpans) + val spanStyles = mutableSpans.map { AnnotatedString.Range(it.item, it.start, it.end, it.tag) } - val paragraphStyles = currentMutableParagraph.map { - AnnotatedString.Range(it.item, it.start, it.end, it.tag) - } return TextFieldValue( - AnnotatedString(text, spanStyles, paragraphStyles), + AnnotatedString(text, spanStyles, annotatedString.paragraphStyles), + selection, + composition + ) +} + +private fun TextFieldValue.modifyParagraphs( + block: (paragraphs: MutableList>) -> Unit +): TextFieldValue { + val mutableParagraphs = mutableListOf>() + annotatedString.paragraphStyles.forEach { + mutableParagraphs.add(MutableRange(it.item, it.start, it.end, it.tag)) + } + + block(mutableParagraphs) + + val paragraphStyles = mutableParagraphs.map { + AnnotatedString.Range(it.item, it.start, it.end, it.tag) + } + + return TextFieldValue( + AnnotatedString(text, annotatedString.spanStyles, paragraphStyles), selection, composition ) } fun TextFieldValue.bold(weight: Int): TextFieldValue { - return modify { spans, _ -> + return modifySpans { spans -> if (selection.collapsed) { val contained = spans.find { it.tag == "bold" && it.end >= selection.start && it.start <= selection.start @@ -255,30 +297,101 @@ fun TextFieldValue.bold(weight: Int): TextFieldValue { } } -fun TextFieldValue.quote(): TextFieldValue { - return modify { _, paragraphs -> - val selectionEnd = selection.end - val prev = text.lastIndexOf('\n', selectionEnd) + 1 - var next = text.indexOf('\n', selectionEnd) - if (next < 0) { - next = text.length +private fun MutableRange.updateStyleIfNeeded( + value: TextFieldValue, tag: String, style: ParagraphStyle +): Boolean{ + if (value.selection.collapsed) { + val shouldModify = if(start == end){ + start == start + }else if(value.selection.start in start until end){ + true + } else if(value.selection.end == end){ + value.text[end - 1] != '\n' + }else false + if (shouldModify) { + item = style + this.tag = tag + return true } - val toRemoveList = arrayListOf>() + } else { + if (start < value.selection.end && end > value.selection.start) { + item = style + this.tag = tag + return true + } + } + return false +} + +internal fun TextFieldValue.paragraphStyle(tag: String, style: ParagraphStyle): TextFieldValue { + return modifyParagraphs { paragraphs -> paragraphs.forEach { - if (it.end > prev && it.start < next) { - toRemoveList.add(it) + it.updateStyleIfNeeded(this, tag, style) + } + }.modifySpans { spans -> + spans.removeAll { + if (selection.collapsed) { + it.tag == "h" && it.start <= selection.end && it.end >= selection.start + } else { + it.tag == "h" && it.start < selection.end && it.end > selection.start + } + + } + } +} + +internal fun TextFieldValue.quote(): TextFieldValue { + return paragraphStyle( + Quote.tag, ParagraphStyle( + textIndent = TextIndent(10.sp, 10.sp) + ) + ) +} + +internal fun TextFieldValue.unOrder(): TextFieldValue { + return paragraphStyle( + UnOrderList.tag, ParagraphStyle( + textIndent = TextIndent(10.sp, 10.sp) + ) + ) +} + +internal fun TextFieldValue.header(level: HeaderLevel): TextFieldValue { + var start = Int.MAX_VALUE + var end = Int.MIN_VALUE + val ret = modifyParagraphs { paragraphs -> + val style = ParagraphStyle() + paragraphs.forEach { + if(it.updateStyleIfNeeded(this, level.tag, style)){ + start = it.start.coerceAtMost(start) + end = it.end.coerceAtLeast(end) } } - toRemoveList.forEach { - paragraphs.remove(it) + } + if (start == Int.MAX_VALUE || end == Int.MIN_VALUE) { + return ret + } + return ret.modifySpans { + var i = 0 + while (i < it.size) { + val span = it[i] + if (span.end > start && (span.start < end || (span.start == end && span.start == span.end))) { + when { + span.start < start -> { + span.end = start + } + span.end > end -> { + span.start = end + } + else -> { + it.removeAt(i) + i-- + } + } + } + i++ } - paragraphs.add( - MutableRange( - ParagraphStyle( - textIndent = TextIndent(5.sp, 5.sp) - ), prev, next, "quote" - ) - ) + it.add(MutableRange(SpanStyle(fontSize = level.fontSize), start, end, "h")) } } @@ -290,43 +403,86 @@ private fun updateTextFieldValue( return TextFieldValue(current.annotatedString, next.selection, next.composition) } if (next.text.isBlank()) { - return TextFieldValue(AnnotatedString(""), next.selection, next.composition) + return TextFieldValue(AnnotatedString(""), next.selection, next.composition).check() } - val currentMutableStyles = mutableListOf>() - val currentMutableParagraph = mutableListOf>() + val mutableSpan = mutableListOf>() + val mutableParagraph = mutableListOf>() current.annotatedString.spanStyles.forEach { - currentMutableStyles.add(MutableRange(it.item, it.start, it.end, it.tag)) + mutableSpan.add(MutableRange(it.item, it.start, it.end, it.tag)) } current.annotatedString.paragraphStyles.forEach { - currentMutableParagraph.add(MutableRange(it.item, it.start, it.end, it.tag)) + mutableParagraph.add(MutableRange(it.item, it.start, it.end, it.tag)) } var indexCorrect = 0 wordEdit(current, next).list.forEach { point -> val lastIndex = point.oldIndex + indexCorrect if (point.action == WordEditAction.insert) { - currentMutableStyles.forEach { - it.modifyByInsert(next.text, lastIndex) + mutableParagraph.forEachIndexed { index, item -> + item.modifyByInsert(lastIndex, index == mutableParagraph.size - 1) } - currentMutableParagraph.forEach { - it.modifyByInsert(next.text, lastIndex) + mutableSpan.forEach { item -> + item.modifyByInsert(lastIndex, true) + if(item.tag=="h"){ + mutableParagraph.find { it.start == item.start }?.let { + item.end = it.end + } + } + } + if (next.text[point.newIndex] == '\n') { + for (i in 0 until mutableParagraph.size) { + val paragraph = mutableParagraph[i] + if (paragraph.start <= point.newIndex && paragraph.end > point.newIndex) { + if (!paragraph.tag.isHeaderTag()) { + mutableParagraph.add(i + 1, MutableRange(paragraph.item, point.newIndex + 1, paragraph.end, paragraph.tag)) + } else { + mutableParagraph.add(i + 1, MutableRange(ParagraphStyle(), point.newIndex + 1, paragraph.end, "p")) + mutableSpan.find { + it.start == paragraph.start && it.end == paragraph.end && it.tag == "h" + }?.let { it.end = point.newIndex + 1 } + } + + paragraph.end = point.newIndex + 1 + break + } + } } indexCorrect++ } else if (point.action == WordEditAction.delete) { - currentMutableStyles.forEach { - it.modifyByDelete(lastIndex) + var i = 0 + while (i < mutableSpan.size) { + val span = mutableSpan[i] + val shouldRemove = span.modifyByDelete(lastIndex) + if (shouldRemove) { + mutableSpan.removeAt(i) + i -= 1 + } + i++ } - currentMutableParagraph.forEach { - it.modifyByDelete(lastIndex) + i = 0 + while (i < mutableParagraph.size) { + val paragraph = mutableParagraph[i] + val shouldRemove = paragraph.modifyByDelete(lastIndex) + if (shouldRemove) { + mutableParagraph.removeAt(i) + i -= 1 + } + i++ } indexCorrect-- } } - val spanStyles = currentMutableStyles.map { + mutableSpan.removeAll { + it.start == it.end && (it.end < next.selection.start || it.start > next.selection.end) + } + mutableParagraph.removeAll { + it.start == it.end && (it.end < next.selection.start || it.start > next.selection.end) + } + val spanStyles = mutableSpan.map { AnnotatedString.Range(it.item, it.start, it.end, it.tag) } - val paragraphStyles = currentMutableParagraph.map { + val paragraphStyles = mutableParagraph.map { AnnotatedString.Range(it.item, it.start, it.end, it.tag) } return TextFieldValue( @@ -336,28 +492,27 @@ private fun updateTextFieldValue( ) } -private class MutableRange(val item: T, var start: Int, var end: Int, val tag: String) { - fun modifyByInsert(text: String, insertPos: Int) { +private class MutableRange(var item: T, var start: Int, var end: Int, var tag: String) { + fun modifyByInsert(insertPos: Int, appendIfAtEnd: Boolean) { + val toInsertPos = insertPos + 1 if (start == end) { - if (insertPos < start - 1) { + if (toInsertPos < start) { start++ end++ - } else if (insertPos == start - 1) { + } else if (toInsertPos == start) { end++ } } else { - if (insertPos < start) { + if (toInsertPos < start) { start++ end++ - } else if (insertPos == end - 1) { - end++ - } else if (insertPos < end) { + } else if (toInsertPos < end || (appendIfAtEnd && toInsertPos == end)) { end++ } } } - fun modifyByDelete(deletePos: Int) { + fun modifyByDelete(deletePos: Int): Boolean { if (start == end) { if (deletePos < start - 1) { start-- @@ -365,14 +520,15 @@ private class MutableRange(val item: T, var start: Int, var end: Int, val tag } else if (deletePos == start - 1) { start-- end-- - } - } else { - if (deletePos < start) { - start-- - end-- - } else if (deletePos < end) { - end-- + return true } } + if (deletePos < start) { + start-- + end-- + } else if (deletePos < end) { + end-- + } + return false } } \ No newline at end of file diff --git a/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt b/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt index 78005f5d..7dbccb06 100644 --- a/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt +++ b/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt @@ -79,7 +79,7 @@ fun wordEdit(oldText: String, newText: String, shift: Int = 0): WordEditResult { WordEditPoint( WordEditAction.delete, shift + j - 1, - shift - 1 + -1 ) ).apply { prev = array[j - 1]!!.node @@ -110,7 +110,7 @@ fun wordEdit(oldText: String, newText: String, shift: Int = 0): WordEditResult { val rowLast = next[j - 1]!! val path2 = WordEditRecord(rowLast.dis + 1).apply { node = WordEditRecordNode( - WordEditPoint(WordEditAction.delete, shift + j - 1, shift + i) + WordEditPoint(WordEditAction.delete, shift + j - 1, -1) ).apply { prev = rowLast.node } diff --git a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt index 92f341ec..5d66811a 100644 --- a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -18,7 +18,7 @@ object Dep { const val typeVer = "0.1.0" const val composeCoreVer = "0.0.1" const val composeVer = "0.0.1" - const val photoVer = "0.0.1.1" + const val photoVer = "0.0.1.2" const val editorVer = "0.0.1" } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt index 3eba53bb..fa301a8e 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.qmuiteam.compose.core.ui.QMUITopBar @@ -44,6 +45,7 @@ class QDEditorFragment : ComposeBaseFragment() { .weight(1f) .padding(16.dp), value = TextFieldValue(""), + hint = AnnotatedString("写下这一刻的想法"), channel = channel ) { @@ -61,7 +63,19 @@ class QDEditorFragment : ComposeBaseFragment() { TextButton("引用"){ scope.launch { - channel.send(Quote()) + channel.send(Quote) + } + } + + TextButton("无序列表"){ + scope.launch { + channel.send(UnOrderList) + } + } + + TextButton("Header"){ + scope.launch { + channel.send(Header(HeaderLevel.h2)) } } }