feat: EmojiEditText

This commit is contained in:
cgspine
2022-04-08 17:23:58 +08:00
parent fe826dad42
commit de5cde7074
5 changed files with 467 additions and 3 deletions
@@ -15,7 +15,7 @@ object Dep {
const val group = "com.qmuiteam"
const val qmuiVer = "2.1.0.1"
const val archVer = "2.1.0.2"
const val typeVer = "0.1.0"
const val typeVer = "0.1.0.1"
const val composeCoreVer = "0.0.1"
const val composeVer = "0.0.1.2"
const val photoVer = "0.0.1.3"
@@ -0,0 +1,107 @@
package com.qmuiteam.qmuidemo.fragment.lab
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatEditText
import androidx.constraintlayout.widget.ConstraintLayout
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord
import com.qmuiteam.qmui.kotlin.*
import com.qmuiteam.qmui.type.parser.EmojiTextParser
import com.qmuiteam.qmui.type.view.EmojiEditText
import com.qmuiteam.qmui.widget.QMUITopBarLayout
import com.qmuiteam.qmuidemo.QDQQFaceManager
import com.qmuiteam.qmuidemo.R
import com.qmuiteam.qmuidemo.base.BaseFragment
import com.qmuiteam.qmuidemo.lib.annotation.Widget
@Widget(name = "EmojiEditText", iconRes = R.mipmap.icon_grid_in_progress)
@LatestVisitRecord
class QDEmojiInputFragment : BaseFragment() {
override fun onCreateView(): View {
return EmojiLayout(requireContext())
}
}
class EmojiLayout(context: Context): ConstraintLayout(context){
val topBarLayout = QMUITopBarLayout(context).apply {
fitsSystemWindows = true
id = View.generateViewId()
}
val editText = EmojiEditText(context).apply {
gravity = Gravity.TOP or Gravity.LEFT
textParser = EmojiTextParser(QDQQFaceManager.getInstance()) { true }
}
val se = TextView(context).apply {
text = "[色]"
setPadding(0, dip(20), 0, dip(20))
onClick {
editText.replaceSelection("[色]")
}
}
val weixiao = TextView(context).apply {
text = "[微笑]"
setPadding(0, dip(20), 0, dip(20))
onClick {
editText.replaceSelection("[微笑]")
}
}
val daku = TextView(context).apply {
text = "[大哭]"
setPadding(0, dip(20), 0, dip(20))
onClick {
editText.replaceSelection("[大哭]")
}
}
val delete = TextView(context).apply {
text = "delete"
setPadding(0, dip(20), 0, dip(20))
onClick {
editText.delete()
}
}
val toolBar = LinearLayout(context).apply {
id = View.generateViewId()
orientation = LinearLayout.HORIZONTAL
addView(se, LinearLayout.LayoutParams(0, wrapContent, 1f))
addView(weixiao, LinearLayout.LayoutParams(0, wrapContent, 1f))
addView(daku, LinearLayout.LayoutParams(0, wrapContent, 1f))
addView(delete, LinearLayout.LayoutParams(0, wrapContent, 1f))
}
init {
addView(topBarLayout, LayoutParams(0, wrapContent).apply {
alignParentHor()
topToTop = constraintParentId
})
addView(toolBar, LayoutParams(0, wrapContent).apply {
alignParentHor()
bottomToBottom = constraintParentId
})
addView(editText, LayoutParams(0, 0).apply {
alignParentHor()
topToBottom = topBarLayout.id
bottomToTop = toolBar.id
})
editText.setText("反反复复[微笑][色]发发发方法")
}
fun handleClick(text: String){
val origin = editText.text
if(origin == null){
editText.setText(text)
}else{
origin.append(text)
}
}
}
@@ -47,6 +47,7 @@ import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDComposeTipFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScrollFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDEditorFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDEmojiInputFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoClipFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoFragment;
import com.qmuiteam.qmuidemo.fragment.lab.QDSchemeFragment;
@@ -153,6 +154,7 @@ public class QDDataManager {
mLabNames.add(QDPhotoFragment.class);
mLabNames.add(QDPhotoClipFragment.class);
mLabNames.add(QDEditorFragment.class);
mLabNames.add(QDEmojiInputFragment.class);
}
public QDItemDescription getDescription(Class<? extends BaseFragment> cls) {
+3 -2
View File
@@ -36,6 +36,7 @@ android {
}
dependencies {
api(Dep.AndroidX.annotation)
api(Dep.AndroidX.coreKtx)
implementation(Dep.AndroidX.appcompat)
implementation(Dep.AndroidX.annotation)
implementation(Dep.AndroidX.coreKtx)
}
@@ -0,0 +1,354 @@
package com.qmuiteam.qmui.type.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.style.ImageSpan
import android.util.AttributeSet
import android.util.SparseArray
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.util.putAll
import androidx.core.util.valueIterator
import com.qmuiteam.qmui.type.TypeModel
import com.qmuiteam.qmui.type.element.Element
import com.qmuiteam.qmui.type.element.EmojiElement
import com.qmuiteam.qmui.type.parser.EmojiTextParser
open class EmojiEditText(
context: Context,
attributeSet: AttributeSet? = null
) : AppCompatEditText(context, attributeSet) {
private var emojiModel: EmojiModel? = null
private var isTextFirstSet: Boolean = false
private var isTextManualSetting: Boolean = false
var textParser: EmojiTextParser? = null
set(value) {
if (field != value) {
field = value
if (isTextFirstSet) {
setText(text, BufferType.EDITABLE)
}
}
}
private val textWatcher = object : TextWatcher {
private val pendingRemoveRange = mutableListOf<Pair<Int, Int>>()
private val pendingRemoveEmojis = mutableListOf<Emoji>()
private val pendingAddEmojis = mutableListOf<Emoji>()
private var isPendingRemoving = false
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
if (isPendingRemoving || isTextManualSetting) {
return
}
val model = emojiModel
if (s != null && model != null && count > 0) {
val end = start + count
var current = start
while (current < end) {
val emoji = model.getEmoji(current)
if (emoji != null) {
pendingRemoveEmojis.add(emoji)
if (emoji.start < start) {
pendingRemoveRange.add(emoji.start to start)
}
val emojiEnd = emoji.start + emoji.text.length
if (emojiEnd >= end) {
val offset = count - after
pendingRemoveRange.add((end - offset) to (emojiEnd - offset))
}
current = emoji.start + emoji.text.length
} else {
current++
}
}
}
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (isPendingRemoving || isTextManualSetting) {
return
}
val model = emojiModel
if (model != null) {
var cur: Emoji? = model.end
val offset = count - before
val correctStartIndex = start + before
while (cur != null && cur.start >= correctStartIndex) {
cur.start += offset
cur = cur.prev
}
}
if (count > 0) {
val insertModel = textParser?.parse(s!!.subSequence(start, start + count))?.toEmojiModel(start)
if (insertModel != null) {
if (model != null) {
model.merge(insertModel)
} else {
emojiModel = insertModel
}
insertModel.map.valueIterator().asSequence().filterTo(pendingAddEmojis) { true }
}
}
}
override fun afterTextChanged(s: Editable?) {
if (isPendingRemoving || isTextManualSetting) {
return
}
isPendingRemoving = true
pendingRemoveRange.forEach {
s?.removeRange(it.first, it.second)
}
pendingRemoveRange.clear()
pendingRemoveEmojis.forEach {
s?.removeSpan(it.span)
}
pendingRemoveEmojis.clear()
pendingAddEmojis.forEach {
s?.setSpan(it.span, it.start, it.start + it.text.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
pendingAddEmojis.clear()
fixSelection()
isPendingRemoving = false
}
}
init {
addTextChangedListener(textWatcher)
}
fun replaceSelection(toInsert: CharSequence) {
val origin = text
if (origin == null) {
setText(toInsert)
} else {
origin.replace(selectionStart, selectionEnd, toInsert)
}
}
fun delete() {
val origin = text ?: return
if (selectionStart != selectionEnd) {
origin.replace(selectionStart, selectionEnd, "")
} else if (selectionStart > 0) {
origin.delete(selectionStart - 1, selectionEnd)
}
}
override fun setText(text: CharSequence?, type: BufferType?) {
isTextFirstSet = true
val model = textParser?.parse(text)?.toEmojiModel(0)
emojiModel = model
val spannable = if (text is Spannable) {
val spans = text.getSpans(0, text.length, EmojiSpan::class.java)
for (span in spans) {
text.removeSpan(span)
}
text
} else {
SpannableString(text ?: "")
}
model?.map?.valueIterator()?.forEach {
spannable.setSpan(it.span, it.start, it.start + it.text.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
isTextManualSetting = true
super.setText(spannable, type)
fixSelection()
isTextManualSetting = false
}
private fun fixSelection() {
val model = emojiModel ?: return
if (selectionStart == selectionEnd) {
val emoji = model.getEmoji(selectionStart) ?: return
if (selectionStart > emoji.start && selectionStart < (emoji.start + emoji.text.length)) {
setSelection(emoji.start + emoji.text.length)
}
} else {
var fixStart = selectionStart
var fixEnd = selectionEnd
val start = model.getEmoji(selectionStart)
if (start != null && selectionStart > start.start && selectionStart < (start.start + start.text.length)) {
fixStart = start.start
}
val end = model.getEmoji(selectionEnd)
if (end != null && selectionEnd > end.start && selectionEnd < (end.start + end.text.length)) {
fixEnd = end.start + end.text.length
}
setSelection(fixStart, fixEnd)
}
}
}
class Emoji(val span: EmojiSpan, val text: CharSequence, var start: Int) {
var prev: Emoji? = null
var next: Emoji? = null
}
class EmojiModel(
val map: SparseArray<Emoji>,
var begin: Emoji,
var end: Emoji
) {
fun merge(other: EmojiModel) {
map.putAll(other.map)
when {
other.begin.start > end.start -> {
end.next = other.begin
other.begin.prev = end
end = other.end
}
other.end.start < begin.start -> {
other.end.next = begin
begin.prev = other.end
begin = other.begin
}
else -> {
var next: Emoji?
var otherNext: Emoji?
val newBegin: Emoji = if (begin.start < other.begin.start) {
next = begin.next
otherNext = other.begin
begin
} else {
otherNext = other.begin.next
next = begin
other.begin
}
var cur = newBegin
while (next != null || otherNext != null) {
when {
next == null -> {
cur.next = otherNext
otherNext!!.prev = cur
begin = newBegin
end = other.end
return
}
otherNext == null -> {
cur.next = next
next.prev = cur
begin = newBegin
return
}
next.start < otherNext.start -> {
cur.next = next
next.prev = cur
cur = next
next = next.next
}
else -> {
cur.next = otherNext
otherNext.prev = cur
cur = otherNext
otherNext = otherNext.next
}
}
}
}
}
}
fun getEmoji(pos: Int): Emoji? {
if (begin.start > pos) {
return null
}
if (end.start + end.text.length <= pos) {
return null
}
var lo = 0
var hi: Int = map.size() - 1
while (lo <= hi) {
val mid = lo + hi ushr 1
val midVal = map.valueAt(mid)
when {
midVal.start < pos -> {
lo = mid + 1
}
midVal.start + midVal.text.length >= pos -> {
hi = mid - 1
}
else -> {
return midVal
}
}
}
return null
}
}
fun TypeModel.toEmojiModel(offset: Int): EmojiModel? {
var node: Element? = firstElement()
val map = SparseArray<Emoji>()
var begin: Emoji? = null
var end: Emoji? = null
while (node != null) {
if (node is EmojiElement) {
val next = Emoji(EmojiSpan(node.drawable.apply {
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}), node.text, offset + node.start)
map.put(offset + node.start, next)
if (begin == null) {
begin = next
end = next
} else {
next.prev = end
end!!.next = next
end = next
}
}
node = node.next
}
if (begin == null) {
return null
}
return EmojiModel(map, begin, end!!)
}
class EmojiSpan(drawable: Drawable) : ImageSpan(drawable) {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val rect = drawable.bounds
if (fm != null) {
val iconHeight = rect.height()
val fontMetricsHeight = fm.descent - fm.ascent
if (iconHeight != fontMetricsHeight) {
val scale = fontMetricsHeight.toFloat() / iconHeight
rect.left = 0
rect.top = 0
rect.right = (rect.width() * scale).toInt()
rect.bottom = (iconHeight * scale).toInt()
drawable.bounds = rect
}
}
return rect.width()
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
canvas.save()
val rect = drawable.bounds
val fontMetricsInt = paint.fontMetricsInt
val fontTop = y + fontMetricsInt.top
val fontMetricsHeight = fontMetricsInt.bottom - fontMetricsInt.top
val iconHeight = rect.height()
val iconTop = fontTop + (fontMetricsHeight - iconHeight) / 2
canvas.translate(x, iconTop.toFloat())
drawable.draw(canvas)
canvas.restore()
}
}