1.介绍

 

 最近,有一个需求:评论@ people(是的,它是即时聊天或微博应用中的@ people功能),如下图所示:

 

 @安卓即时通讯应用中的人物功能实现:模仿微博、QQ、微信,零入侵,高可伸缩[图形+源代码] _ wechatimg43.jpg @安卓即时通讯应用中的人物功能实现:模仿微博、QQ、微信,零入侵,高可伸缩[图形+源代码]_微信

 ▲ @微信群聊界面中的人物功能▲@ QQ群聊界面中的人物功能

 

 互联网上的一些文章共享了类似功能的逻辑,但是几乎所有的文章都扩展了EditText类,所以这种实现方式肯定不能进入我的首发阵容。你认为这是因为它不符合面向对象的六个原则吗?错了,只是因为它不够优雅!不够优雅!不够优雅!

 

 那么,如果只有饮水机代码呢?当然是:

 阅读fuking源代码

 

 

 努力工作有回报。我读了一遍编辑文本的源代码,然后创造了这个“优雅”的轮子(开玩笑的,编辑文本的源代码怎么能被称为富金源代码,他有一个父亲叫TextView)。别废话了,上泡菜。

 

 在此之前,你需要记住一个与文本相关的想法:一切都是跨越!

 2.添加标签文本样式,并将其绑定到标签的业务数据

 

 每个人都知道文本风格与可展性有关。

 

 这里也使用可展性。我已经定义了一个DataBindingSpan接口,它有两个主要功能:

 

 1)让用户提供一个CharSequence对象作为标签,它决定了标签文本的样式和内容;

 2)提供一种方法来返回由DataBindingSpan对象绑定的业务数据。

 

 1234 interface DataBindingSpan { fun SpannedText():CharSequence fun BindingData():T }

 

 

 示例代码:

 010203040506070809101112 class SpanNableData(私有值跨度:字符串):DataBindingSpan { override fun SpanNedText():CharSequence { return SpanNableString(跨度)。应用{设置跨度(前背景颜色跨度(颜色。红色),0,长度,跨度。SPAN _ EXCLIVE _ EXCLIVE)} }覆盖有趣的绑定数据():字符串{返回范围} }

 

 

 这个类只包装一个字符串。spannedText()返回一个字符串,该字符串将标签文本的颜色更改为红色,而bindingData()将该字符串作为业务数据返回。

 

 你也可以把它变成别的东西。用户对象很好。SpannedText()返回用户名,bindingData()返回用户标识,这样就可以很容易地实现@ person函数的业务数据绑定的相关逻辑。

 3.确保绑定在文本上的数据的安全性和可靠性

 

 当我们将Span绑定到文本时,我们需要确保文本和数据在文本更改时的安全性、可靠性和一致性。

 

 事实上,自从数据库发布以来,我们一直在处理这件事。正如SpannableData所示,当spannedText()返回一个Spannable对象时,spanned。span _ exclusive _ exclusive用作标志。它不能在头部和尾部扩展跨度的范围,只能在中间插入。同时,当Span所涵盖的文本被删除时,Span也将被删除。也就是说,它天生具有一定的数据安全性和可靠性。这将为我们节省很多东西。

 

 当然,跨越。span _ exclusive _ exclusive没有完全的安全性。毕竟,它不能阻止中间插入。我们必须自己做这件事。那么,我们应该怎么做来禁止中间插入呢?

 

 这种需求产生了两个问题:

 

 1)当普通文本发生变化时,如何监控跨度起始位置的变化?

 2)如何禁止在跨度内插入光标?

 

 对于第一个问题,我在网上看到了一个想法。维护跨度起始位置管理器跨度管理器,然后使用文本查看器监控文本的变化。文本的任何更改都会导致spanrangmanager重新计算span的位置。

 

 当然,如果我使用这种方法,我就不会有这个博客。事实上,安卓系统有一个很好的跨度管理器,那就是跨度生成器。与此同时,SDK提供了一个监听程序SpanWatcher来监听SpannableStringBuilder中Span的变化。感兴趣的学生可以看看他的源代码。

 

 其次,我们需要确保文本和数据之间的一致性,并禁止将光标插入到跨度覆盖文本的中间。

 

 有三种方法:

 

 1)普通文本,当标签文本被破坏(删除、插入、追加)时,绑定数据失效,这是微信的做法;

 2)普通文本将标签文本作为一个整体,不能在标签中插入光标,防止数据被破坏。这是微博的做法;

 3)占位符被不可分割的范围(如图像范围)取代,这是QQ的惯例。

 

 微博和微信都必须监控和处理软键盘删除键、文本变化、光标活动、文本选择状态和跨度变化。QQ要简单得多,这将在后面讨论。

 4.微博的实践

 

 4.1倾听并处理光标活动、选定状态和量程位置变化

 

 对于游标活动和所选状态监听,如果标签文本功能是通过继承编辑文本来实现的,那么方法OnSelectonChanged(int Selstart,int Selend)可以监听游标活动。然而,这种方法怎么能被认为是优雅的呢?

 

 我应该怎样做才能“优雅地”实现它?还是那句话:

 阅读fuking源代码

 

 

 两个角色:

 

 选择

 SpanWatcher

 

 如果有一篇文章叫做“选择如何管理文本光标活动和选择状态?”,那么它一定能够回答这个问题。

 

 这里将不详细描述选择的内部实现,您只需要知道两件事:

 

 1)所选状态有一个起点(起点)和一个终点(终点),起点和终点反映在文本中,实际上是两个非点点:起点和终点;

 2)光标处于特殊选中状态,起点和终点在同一位置。

 

 因为所选状态的实现是跨度,所以它与视图无关,而是与跨度有关。也就是说,我们可以在不使用自己的API的情况下管理EditText的游标活动和选择状态(请注意这些词,他是“优雅实现”的基石)。

 

 选择管理光标活动。那么,什么是SpanWatcher?如前所述,它是一个监听器,用于监听SpannableStringBuidler中的跨度变化。有一些非常相似的东西,文本观察者。是的,他们有同一个父亲,诺科普斯潘。他们倾听文本变化和跨度变化。

 

 以下是SpanWatcher的源代码:

 0102030405060708091011121314151617181920212223/* * *当此类型的对象附加到Spannable时,将调用其方法*来通知它其他标记对象已经*添加、更改或移除。*/公共接口SpanWatcher扩展了NoCopySpan { /** *调用此方法是为了通知您指定的对象*已附加到指定的文本范围。*/public void on panadded(可扩展文本、对象内容、int开始、int结束);/** *调用此方法是为了通知您指定的对象*已从指定的文本范围中分离。*/公共空在panRemoved上(可展开文本、对象内容、int开始、int结束);/** *调用此方法是为了通知您指定的对象*已从文本的范围ostart…oend *重新定位到新的范围nstart…nend。*/ public void onSpanChanged(可扩展文本、对象内容、int ostart、int oend、int nstart、int nend);}

 

 

 我们已经知道光标是一种跨度。也就是说,我们可以通过SpanWatcher监听光标的活动,并意识到当光标在Span内移动时,它将再次移动到Span的最近边,并且光标永远不能插入Span内。这样,我们就可以实现将标注文本作为一个整体的想法。

 

 下面是代码实现:

 0102030405060708091011112131415161718192021222324252627282930313233343536373839 package com . iyao import Android . text . Selection import Android . text . SpanWat cher import Android . text . Spannaleimport kotlin . math . abimport kotlin . reflect . kClass class Selection .选择_结束&选择!= n start){ selEnd = n start text . GetSpans(n start,nend,kClass.java)。firstOrNull()?。运行{ val spanStart = text . GetSpanStart(this)val spanEnd = text . GetSpanEnd(this)val index = if(ABS(selEnd-spanEnd)> ABS(selEnd-spanStart))spanStart否则spanEnd Selection . set Selection(text,Selection . GetSelectionStart(text,index) } } if (what === Selection。选择_开始和选择开始!= n start){ SelStart = n start text . GetSpans(n start,nend,kClass.java)。firstOrNull()?。运行{ val spanStart = text . GetSpanStart(this)val spanEnd = text . GetSpanEnd(this)val index = if(ABS(SelStart-SpanEnd)> ABS(SelStart-SpanStart))spanStart否则spanEnd selection . set selection(text,index,selection . GetSelectionEnd(text))} }覆盖娱乐已覆盖已删除(text:Spanable?什么:有吗?,开始:Int,结束:Int) { }覆盖已添加的乐趣(文本:可扩展?什么:有吗?,开始:Int,结束:Int) { }}

 

 

 现在,我们只需要将这个跨度添加到setText()之前的文本中。

 

 4.2听软键盘的删除键并处理所选状态

 

 现在,Span覆盖的文本被视为一个整体,光标不能插入,但是当我们从Span的尾部删除文本时,它仍然被一个字一个字地删除。我们的要求是当删除Span文本时,整个Span都可以被删除,所以我们需要监控键盘的删除键。

 

 0102030405060708091011112131415161718192021 package com . iyao import Android . text . selection import Android . text . spanable class key codedeletehelper private constructor(){ companion object { fun OnDedown(text:Spanable):布尔值{ val selectionStart = selection . GetSelectionStart(text)val selectionId = selection . GetSelectionEnD(text)text . GetSpans(selectionStart,selectionEn)第一个或全部{ text.getSpanEnd(it) ==选择开始}?。运行{返回(选择开始==选择结束)。还有{ val spanStart = text . GetSpanStart(此)val spanEnd = text.getSpanEnd(此)选择.设置选择(text,SpanStart,spanEnd) } }返回false } }}

 

 

 让我们使用它:

 0102030405060708091011编辑文本. setOnKeyListener { v,键码,事件-> if(键码==键码事件。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ return @ SetOnKeyListener KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false }//Get数据valstring = edittext . text . let { it . getspans(0,it.length,databindingspan:: class.java)}。映射{it.bindingdata ()}

 

 

 现在你可以达到和微博一样的效果。一切都很顺利。

 

 然而,当你运行它的时候,你会发现选择panWatcher一点效果都没有。轮子已经造好了,你告诉我轴承坏了。

 

 而且,当您在编辑文本上打印跨度时,您找不到选择范围监视器。这表明选择面板监视器在设置文本()期间被清除。我们能在setText()之后设置它吗?如果你这样做,你会发现一个新问题。由setText()添加的文本无效。似乎我们不能通过setText()添加内容,我们只能使用getText()来添加内容。此外,我们必须完全禁用setText(),因为每次调用都会清除选择面板监视器。

 

 这个方法看起来不错,但是如果有人不熟悉这个特性呢?告诉他你不能用setText()?还是通过内联方法或继承为编辑文本添加新方法?所有这些都很好,但唯一的缺点是它不是我想要的优雅。我希望它像普通的编辑文本一样使用setText()方法。

 

 要考虑的问题是,选择“泛观察者”在哪里消失了?我要把这个方位拿回来。

 

 4.3优雅实现车轮的轴承:可编辑。工厂

 

 setText()方法中的SelectionSpanWatcher消失。我需要阅读它的源代码。

 

 编辑文本覆盖getText(),settext(字符序列文本,缓冲区类型)方法:

 0102030405060708091011121314151617 @ Override public可编辑GetText(){ CharSequence text = super . GetText();//这只能在施工期间发生。if (text == null) {返回null;}如果(可编辑的文本实例){返回(可编辑)super . GetText();} super.setText(文本,缓冲类型。可编辑);返回(可编辑)super . GetText();} @覆盖公共空设置文本(字符序列文本,缓冲类型){ super.setText(文本,缓冲类型。可编辑);}

 

 

 从源代码的角度来看,重写的唯一目的是将BufferType设置为BufferType.EDITABLE。

 

 我们都知道文本视图有三种文本模式:

 

 1)缓冲类型。正常静态文本模式,该模式下的文本不可编辑,也没有丰富的文本样式;

 2)缓冲类型。SPANNABLE是一种带有文本样式的模式,不能编辑。当TextView.isTextSelectable()返回true时,文本视图的文本模式;

 3)缓冲类型的文本模式。可编辑可编辑文本,可使用文本样式编辑。

 

 这里不具体提及这三种模式的相关内容。只需要知道编辑文本的模式是BufferType.EDITABLE。

 

 那么,缓冲类型之间是什么关系。可编辑和“方位”?这很重要。

 在阅读上面的源代码片段时,我想知道是否有人注意到设置文本(CharSequence)传入了一个CharSequence对象,文本视图#getText()返回了一个CharSequence对象,而编辑文本#getText()返回了一个可编辑对象。它是何时以及如何完成转换的?这将是一个突破吗?

 根据可编辑getText()的源代码,它被转换为super.settext(文本、缓冲类型、可编辑)。

 

 在TextView源代码中,settext(字符序列文本、缓冲区类型、布尔型通知前、整型)有这样一个流分支:

 010203040506070809101112私有void setText(CharSequence text,BufferType类型,布尔notifyBefore,int oldlen) { if (type == BufferType。可编辑|| getKeyListener()!= null | | NeedEditableForNotification){...可编辑t = mEditableFactory.newEditable可编辑(文本);text = t;...}...mBufferType =类型;setTextInternal(文本);...}

 

 

 因此,分配给编辑文本的字符序列对象首先由媒体工厂转换成可编辑对象,最后分配给编辑文本。媒体工厂的类型是可编辑的。工厂,它是一个静态的内部类。

 

 让我们看看可编辑的实现。工厂是:

 01020304050607080910111213141516171819202122232425/* * *工厂,由TextView用来创建新的{ @ link Editable。您可以对其进行子类化,以提供{@link SpannableStringBuilder}以外的功能。* * @参见安卓. widget . TextView # SeteditableFactory(工厂)*/公共静态类Factory {私有静态可编辑。工厂实例=新的可编辑。工厂();/** *返回标准的可编辑工厂。*/公共静态可编辑。工厂getInstance() {返回实例;} /** *从指定的*字符序列返回一个新的SpannedStringBuilder。您可以覆盖它以提供*一种不同类型的跨区表。*/ public可编辑的新可编辑的(字符序列源){返回新的SpannableStringBuilder(源);}}

 

 

 一个非常简单的转换,它将CharSequence对象转换为SpannableStringBuilder的对象,后者是可编辑的子类。

 

 让我们看看这个构造函数:

 0102030405060708091011121314151617 public SpannableStringBuilder(CharSequence text,int start,int end) {...mText = Arrayutils . NewUnpaddchararray(GrowingArrayutils . GrowSize(src len));...if(spanded的文本实例){ spanded sp =(spanded)文本;对象[]跨度= sp.getSpans(开始、结束、对象类);对于(int ii = 0;ii <跨度长度;ii++) { if(跨越[ii]个NoCopySpan实例){继续;}...setSpan(false,spans[ii],st,en,fl,false);}恢复投资者();}}

 

 

 这就是轴承损坏的原因。

 

 前面提到的SpanWatcher继承了NoCopySpan,NoCopySpan是一个标记接口。其功能是标记跨度不能被复制。在构造时,SpannableStringBuilder会忽略所有NoCopySpan及其子类。因此,选择panWatcher不会分配给编辑文本的文本。

 

 因为没有拷贝NoCopySpan,所以我们可以在构建SpannableStringBuilder之后重置它。可编辑的注释。工厂给了我希望。它可以被重写并重新注入到编辑文本中。

 1 ndroid . widget . TextView # SeteditableFactory(工厂)

 

 

 下面是重写的可编辑。工厂,用于将NoCopySpan重置为SpannableStringBuilder:

 0102030405060708091011121314151617 package com . iyao import Android . text . editableimport Android . text . nocopyspanimport Android . text . SpannableStringBuilder import Android . text . SpannedImport Android . text . style . BackgroundColorSpan类NoCopySpanEditableFactory(私有vararg val spans: NoCopySpan):可编辑。工厂(){覆盖有趣的新的可编辑的(来源:字符序列):可编辑的(返回SpannableStringBuilder.valueOf(来源)。应用{跨度. forEach {设置跨度(它,0,源.长度,跨度。SPAN_INCLUSIVE_INCLUSIVE) } } }

 

 

 没错,有17行代码。这是这个轮子的新轴承。现在让我们重复使用它。

 

 使用editText.setEditableFactory()安装新轴承并让车轮运转:

 1234567 edittext . seteditablefactory(nocopyspanediatablefactory(selectionPanwatcher(DataBindingSpan::class)))edittext . setonKeyListener { v,keyCode,事件-> if (keyCode == KeyEvent。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ return @ SetOnKeyListener KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false }

 

 

 一个“优雅”的实现诞生了,你可以在微博这样的评论中使用@ people。

 

 运行效果:

 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_1.gif

 5.微信的实践

 

 微信应该以更简单的方式处理。它们不禁止在覆盖范围的文本中插入光标,但是当覆盖范围的文本发生变化时,会清除范围和数据。他们还想通过监控删除按钮来实现Span的整体删除,但性能与微博略有不同。

 

 微信三部曲。

 

 首先,定义一个接口来判断Span是否无效:

 1234567 package com . iyao import Android . text . Spanable interface RemovedOnDirtysPan { fun IsDirty(text:Spanable):Boolean }

 

 

 第二,让SpannableData实现这个接口。当然,你也可以让RemoveOnDirtySpan继承DataBindingSpan,尽管我认为这不符合“六大”。

 

 010203040506070809101112131415161718 class SpanNableData(私有值范围:字符串):DataBindingSpan,RemoveOnDirtySpan { override fun SpanNedText():CharSequence { return SpanNableString(范围)。应用{设置跨度(前背景颜色跨度(颜色。红色),0,长度,跨度。SPAN_EXCLUSIVE_EXCLUSIVE) } }覆盖趣味绑定数据():字符串{返回跨度}覆盖趣味标识(文本:Spannable):布尔值{值spanStart =文本。getSpanStart(此)值spanEnd =文本。getSpanEnd(此)返回spanStart > = 0 & & spanEnd > = 0 & >文本。子字符串(spanStart,spanEnd)!= spanned } }

 

 

 最后,重写一个DirtySpanWatcher来删除无效的跨度:

 01020304050607080910111213141516171819202122232425262728 package com . iyao import Android . text . SpanWatcherImport Android . text . Spanable class DirtySpanWa tcher(private val Remove谓词:(Any) ->布尔值):SpanWatcher { override fun OnSpanchanged(text:Spanable,what: Any,ostart: Int,oend: Int,nstart筛选{ removePredicate.invoke(it) }。forEach { text.removeSpan(it) } }覆盖娱乐项目已覆盖(text:Spanable,what: Any,start: Int,end: Int) { }覆盖娱乐项目已添加(text:Spanable,what: Any,start: Int,end: Int) { } }

 

 

 现在,让微信运行起来:

 123456789 edittext . seteditablefactory(NocopysPanediatableFactory(DirtysPanwatcher { it is ForeGroundColorSpan | | it is RemoveOnDirtysPan }))edittext . SetOnKeyListener { v,keyCode,事件-> if (keyCode == KeyEvent。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false }

 

 

 应该注意的是,微信和微博有一点点不同。微博已经被确认和删除两次,但微信没有。代码上唯一的不同是微信没有return@setOnKeyListener。

 

 运行效果:

 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_2.gif

 6.QQ的实践

 

 QQ太简单了,我不想谈论它。在这里写一个简单的演示来演示。

 

 QQ还需要使用数据绑定服务,即使你不需要它。它的核心是图像跨度:

 010203040506070809101112 class SpanNableData(私有值跨度:字符串):DataBindingSpan { override fun SpanNedText():CharSequence { return SpanNableString(「@ $ spanded」)。应用{设置跨度(图像跨度(标签可绘制(@ $跨度),颜色=颜色。LTGRAY),跨区),0,长度-1,跨区。SPAN _ EXCLIVE _ EXCLIVE)} }覆盖有趣的绑定数据():字符串{返回范围} }

 

 

 现在,我们只需要实现一个可绘制的绘图文本。在这里,我的名字是LabelDrawable,这可能不准确:

 0102030405060708091011121314151617181920212223242526 class标签可绘制(val text: CharSequence,private val TextPaint:TextPaint = TextPaint(绘制)。反别名标志)。应用{ textSize = 42f this.color = Color。DKGRAY文本对齐=绘画。Align.CENTER},color:Int):ColorDrawable(color){ init { CalculateBounds()}覆盖有趣的绘图(画布:画布){ super.draw(画布)画布. drawText(文本,0,文本.长度,边界. CENTER()。toFloat(),bounds.centerY()。toFloat()+GetBaseline Offset(TextPaint . FontMetrics),textPaint) }私人趣味计算边界(){ TextPaint . GetTextBounds(text . ToString),0,text.length,Bounds)边界. inset(-8,-4)边界. offset(8,0) }私人趣味GetBaseline Offset(FontMetrics:Paint)。FontMetrics):浮动{返回(fontMetrics .下降- fontMetrics .上升)/ 2 - fontMetrics .下降}}

 

 

 像使用普通扳手一样使用他。

 

 运行效果:

 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_3.gif

 

 如果你想做得更好,你需要处理多行文本,如测量,布局和绘图。给点提示,文本视图截图也是可绘制的。如果有视图,即使它没有附加到窗口,我们也可以手动调用measure(),layout(),draw()方法来获取可绘制视图的屏幕截图,以便添加到图像Span,但是这不能响应触摸事件。

 7.获取文本中绑定的数据

 

 只需使用以下代码:

 123 val strings = EdItText . text . let { it . GetSpans(0,it.length,DataBindingSpan::class.java)}。映射{ it.bindingData() }

 

 

 8.下载本文的源代码附件

 

 轻松掌握(52 im.net)。zip (434 KB,下载次数:39次,价格:1枚金币)

 9.题外话:本文的代码是由Kotlin编写的,但是我希望Java版本由@ people实现。我该怎么办?

 

 是的,科特林还没有被广泛使用,所以它不能被使用。

 

 然而,@,一个看似简单的函数,在没有bug的情况下实际上有点困难,或者代码量并不太小。

 

 那么,我在哪里可以找到@ person函数的可靠的Java版本呢?

 

 答案就在这里:你可以下载网易云信的官方开源即时消息演示,它有@ function的完整代码实现:

 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_55.jpg

 ▲ @ person函数完整的源代码位置

 

 别告诉我这是违法的。他们说这是开源的。。。

 

 网易云信即时通讯演示下载地址:点击这里进入。

 网易云信即时通讯演示的Github地址:https://github.com/netease-im/NIM_Android_Demo

 

 好的,我没有向网易云信收取任何福利费用。我之所以建议你“撕掉”它的源代码,是因为在我评估了主流第三方即时通讯开源的演示代码之后,@ person函数写得相当好,只有网易云信没有选择。


哇谷im_im即时通讯_私有云_公有云-哇谷云科技官网-JM沟通

IM下载体验 - 哇谷IM-企业云办公IM即时聊天社交系统-JM 沟通下载

IM功能与价格 - 哇谷IM-提供即时通讯IM开发-APP搭建私有化-公有云-私有化云-海外云搭建

新闻动态 - 哇谷IM-即时通讯热门动态博客聊天JM沟通APP

哇谷IM-JM沟通热门动态博客短视频娱乐生活

关于哇谷-哇谷IM-提供企业即时通讯IM开发-语音通话-APP搭建私有化-公有云-私有化云-海外云搭建

联系我们 - 哇谷IM-即时通讯IM私有化搭建提供接口与SDK及哇谷云服务

IM云系统即时通讯公有云、私有云、企业云、海外云-哇谷IM团队

新的市场叫板环信、融云、腾讯云!开源版IM即使聊天工具

im即时通讯社交软件APP红包技术分析(五):微信红包、聊呗红包、诚信红包、高并发技术

公有云和私有云之间有什么区别?类似融云、环信云、网易云、哇谷云?