Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 497, comments - 13, trackbacks - 0, articles - 1

          本文由“貓爸iYao”原創(chuàng)分享,感謝作者。

          1、引言

          最近有個需求:評論@人(沒錯,就是IM聊天或者微博APP里的@人功能),就像下圖這樣:

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

          網(wǎng)上已經(jīng)有一些文章分享了類似功能實現(xiàn)邏輯,但是幾乎都是擴展EditText類,這種實現(xiàn)方式肯定不能進入我的首發(fā)陣容。你以為是因為它不符合面向?qū)ο罅笤瓌t?錯,只因為它不夠優(yōu)雅!不夠優(yōu)雅!不夠優(yōu)雅!

          那么,只有飲水機代碼怎么辦?當然是:

          read the fuking source code

          功夫不負有心人,我讀了一遍EditText源碼,然后就造出了這個“優(yōu)雅的”輪子(開玩笑,EditText源碼怎么能叫fuking source code,他有一個爸爸叫TextView)。廢話不多說,上酸菜。

          在此之前,你需要記住一個跟文本相關的思想:一切皆Span!

          學習交流:

          - 即時通訊/推送技術(shù)開發(fā)交流4群:101279154 [推薦]

          - 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM

          (本文同步發(fā)布于:http://www.52im.net/thread-2165-1-1.html

          2、添加標簽文本樣式,并與標簽的業(yè)務數(shù)據(jù)綁定

          所有人都知道文本樣式與Spannable有關。

          這里同樣使用Spannable,我定義了一個DataBindingSpan<T>接口,主要有兩個功能:

          1)讓用戶提供一個CharSequence對象作為標簽,它決定了標簽文本的樣式和內(nèi)容;

          2)提供一個方法返回DataBindingSpan對象所綁定的業(yè)務數(shù)據(jù)。

          interfaceDataBindingSpan<T> {

              fun spannedText(): CharSequence

              fun bindingData(): T

          }

          示例代碼:

          class SpannableData(privateval spanned: String): DataBindingSpan<String> {

             override fun spannedText(): CharSequence {

                  return SpannableString(spanned).apply {

                      setSpan(ForegroundColorSpan(Color.RED), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

                  }

              }


              override fun bindingData(): String {

                  returnspanned

              }

          }

          這個類僅僅包裝了一個字符串,spannedText()返回一個改變標簽文本顏色為紅色的字符串,同時 bindingData()將該字符串作為業(yè)務數(shù)據(jù)返回。

          你也可以把它換成其他的,user對象不錯。spannedText()返回username,bindingData()返回userId,你就可以輕松實現(xiàn)@人功能業(yè)務數(shù)據(jù)綁定相關的邏輯了。

          3、保證文本上綁定的數(shù)據(jù)的安全可靠

          當我們把Span綁定到文本上以后,我們需要在文本發(fā)生變化時,保證文本和數(shù)據(jù)的安全性,可靠性,一致性。

          其實從DataBindingSpan開始,我們就在處理這個事情了。正如SpannableData所展現(xiàn)的一樣,當spannedText()返回的是一個Spannable對象時,使用Spanned.SPAN_EXCLUSIVE_EXCLUSIVE作為flag。它不能在頭部和尾部擴展Span的范圍,只允許中間插入。同時,當Span覆蓋的文本被刪除時,Span也會被刪除。也就是說,它天生具有一定數(shù)據(jù)安全可靠的屬性。這會為我們省掉很多事情。

          當然,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE并不具備完全的安全性。畢竟它不能阻止中間插入。這個事情得我們自己來做。那么,為了禁止中間插入,我們應該怎么做呢?

          這個需求又產(chǎn)生了兩個問題:

          1)當普通文本發(fā)生變化后,如何監(jiān)控一個Span起始位置發(fā)生變化?

          2)如何禁止Span內(nèi)部插入光標?

          對于第一個問題,我在網(wǎng)上看到過一種思路。維護一個Span起始位置管理器SpanRangeManager,然后利用TextWather監(jiān)聽文本變化,文本的任何變化都會導致SpanRangeManager重新測算Span的位置。

          當然,如果我使用這種方式,就不會有這篇博客了。其實Android SDK便有一個優(yōu)秀的Span管理器,那就是SpannableStringBuilder。同時SDK提供了一個偵聽器SpanWatcher偵聽SpannableStringBuilder中Span的變化。有興趣的同學可以去看一看他的源碼。

          第二個問題,我們要保證文本與數(shù)據(jù)的一致性,禁止光標插入到Span覆蓋的文本中間。

          有三種做法:

          1)普通文本,當標簽文本被破壞(刪除、插入、追加文本)時,讓綁定的數(shù)據(jù)失效,這就是微信的做法;

          2)普通文本,把標簽文本作為一個整體,不能對標簽內(nèi)部插入光標,杜絕數(shù)據(jù)被破壞的情況,這是微博的做法;

          3)占位符,使用不可分割的Span(如ImageSpan)替換,這是QQ的做法。

          微博、微信的方法都必須要對軟鍵盤刪除鍵、文本變化、光標活動、文本選中狀態(tài)以及span變化進行監(jiān)聽和處理。QQ就簡單多了,后面會講到。

          4、微博的做法

          4.1 偵聽并處理光標活動、選中狀態(tài)以及Span位置變化

          對于光標活動和選中狀態(tài)偵聽,如果采用繼承EditText的方式實現(xiàn)標簽文本功能,重寫onSelectionChanged(int selStart, int selEnd)方法便能夠偵聽光標活動。但是,這種方式怎么能算優(yōu)雅呢?

          要想“優(yōu)雅地”實現(xiàn)怎么辦?還是那句話:

          read the fuking source code

          兩個角色:

          Selection

          SpanWatcher

          如果有一篇文章叫做《Selection如何管理文本光標活動和選中狀態(tài)?》,那么它一定能回答這個問題。

          這里不會詳細講述Selection內(nèi)部實現(xiàn),你只需要知道兩點:

          1)選中狀態(tài)具有起點(start)和終點(end),而start與end反映在文本中,其實是兩個NoCopySpan: START, END;

          2)光標是一種特殊的選中狀態(tài),start與end在同一位置。

          既然選中狀態(tài)的實現(xiàn)是Span,它就是與View無關的,而與Spannable有關。也就是說,我們可以不使用EditText自身的API卻能夠管理它的光標活動和選中狀態(tài)(請注意這幾句話,他是“優(yōu)雅實現(xiàn)”的基石)。

          Selection管理光標活動。那么,SpanWatcher又是什么?前面說了,它是SpannableStringBuidler中用于偵聽Span變化的監(jiān)聽器。有個東西和它很像,TextWatcher。沒錯,他倆有同一個爹NoCopySpan。他倆一個偵聽文本變化,一個偵聽Span變化。

          下面是SpanWatcher的源碼:

          /**

           * When an object of this type is attached to a Spannable, its methods

           * will be called to notify it that other markup objects have been

           * added, changed, or removed.

           */

          public interface SpanWatcher extendsNoCopySpan {

              /**

               * This method is called to notify you that the specified object

               * has been attached to the specified range of the text.

               */

              public void onSpanAdded(Spannable text, Object what, intstart, intend);

              /**

               * This method is called to notify you that the specified object

               * has been detached from the specified range of the text.

               */

              public void onSpanRemoved(Spannable text, Object what, intstart, intend);

              /**

               * This method is called to notify you that the specified object

               * has been relocated from the range <code>ostart…oend</code>

               * to the new range <code>nstart…nend</code> of the text.

               */

              public void onSpanChanged(Spannable text, Object what, intostart, intoend, intnstart, intnend);

          }

          我們已經(jīng)知道光標是一種Span。也就是說,我們可以通過SpanWatcher偵聽光標活動,通過Selection實現(xiàn)當光標移動到Span內(nèi)部時,讓它重新移動到Span最近的邊緣位置,Span內(nèi)部永遠無法插入光標。這樣便能夠?qū)崿F(xiàn)把標簽文本(spanned text)看作一個整體的思路。

          下面是代碼實現(xiàn):

          package com.iyao

          import android.text.Selection

          import android.text.SpanWatcher

          import android.text.Spannable

          import kotlin.math.abs

          import kotlin.reflect.KClass

          class SelectionSpanWatcher<T: Any>(privateval kClass: KClass<T>): SpanWatcher {

              privatevar selStart = 0

              privatevar selEnd = 0

              override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int, nend: Int) {

                  if(what === Selection.SELECTION_END && selEnd != nstart) {

                      selEnd = nstart

                      text.getSpans(nstart, nend, kClass.java).firstOrNull()?.run {

                          val spanStart = text.getSpanStart(this)

                          val spanEnd = text.getSpanEnd(this)

                          val index = if(abs(selEnd - spanEnd) > abs(selEnd - spanStart)) spanStart elsespanEnd

                          Selection.setSelection(text, Selection.getSelectionStart(text), index)

                      }

                  }


                  if(what === Selection.SELECTION_START && selStart != nstart) {

                      selStart = nstart

                      text.getSpans(nstart, nend, kClass.java).firstOrNull()?.run {

                          val spanStart = text.getSpanStart(this)

                          val spanEnd = text.getSpanEnd(this)

                          val index = if(abs(selStart - spanEnd) > abs(selStart - spanStart)) spanStart elsespanEnd

                          Selection.setSelection(text, index, Selection.getSelectionEnd(text))

                      }

                  }

              }


              override fun onSpanRemoved(text: Spannable?, what: Any?, start: Int, end: Int) {

              }


              override fun onSpanAdded(text: Spannable?, what: Any?, start: Int, end: Int) {

              }

          }

          現(xiàn)在,我們只需要在setText()之前把這個Span添加到文本上就可以了。

          4.2 偵聽軟鍵盤刪除鍵并處理選中狀態(tài)

          現(xiàn)在已經(jīng)把Span覆蓋的文本作為一個整體,且無法插入光標,但是當我們從Span尾部刪除文本,仍是逐字刪除。我們的要求是刪除Span文本時,能夠整體刪除整個Span,這就需要監(jiān)聽鍵盤刪除鍵。

          package com.iyao

          import android.text.Selection

          import android.text.Spannable


          class KeyCodeDeleteHelper private constructor(){

              companion object {

                  fun onDelDown(text: Spannable): Boolean {

                      val selectionStart = Selection.getSelectionStart(text)

                      val selectionEnd = Selection.getSelectionEnd(text)

                      text.getSpans(selectionStart, selectionEnd, DataBindingSpan::class.java).firstOrNull { text.getSpanEnd(it) == selectionStart }?.run {

                          return(selectionStart == selectionEnd).also {

                              val spanStart = text.getSpanStart(this)

                              val spanEnd = text.getSpanEnd(this)

                              Selection.setSelection(text, spanStart, spanEnd)

                          }

                      }

                      returnfalse

                  }

              }

          }

          讓我們使用它:

          editText.setOnKeyListener { v, keyCode, event ->

              if(keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {

                  return @setOnKeyListenerKeyCodeDeleteHelper.onDelDown((v as EditText).text)

              }

              return @setOnKeyListenerfalse

          }


          //取數(shù)據(jù)

          val strings = editText.text.let {

              it.getSpans(0, it.length, DataBindingSpan::class.java)

          }.map { it.bindingData() }

          現(xiàn)在就可以實現(xiàn)微博一樣效果了。一切都那么順利。

          然而,當你運行起來會發(fā)現(xiàn),SelectionSpanWatcher完全沒有效果。輪子都造好了,你告訴我軸承斷了。

          并且,當你打印EditText文本上的Span時,你找不到SelectionSpanWatcher。這說明SelectionSpanWatcher在setText()過程中被清除掉了。那我們能不能把它放在setText()之后設置呢?如果你這么做,你會發(fā)現(xiàn)一個新問題。setText()添加的文本沒有效果。似乎我們不能通過setText()添加內(nèi)容,只能使用getText()追加內(nèi)容。不僅如此,我們必須完全禁用setText(),因為每一次調(diào)用,都會清除掉SelectionSpanWatcher。

          這種方式看起來還不錯,但是換一個不熟悉這個特性的人來使用怎么辦?告訴他不能用setText()方法?或者用內(nèi)聯(lián)方法或繼承的方式為EditText新增一個方法? 這些都可以,唯一的缺點是,它不是我想要的優(yōu)雅。我要讓它就像使用普通EditText一樣正常使用setText()方法。

          需要思考的問題是,SelectionSpanWatcher在哪里消失了?我要重新找回這個軸承。

          4.3 讓輪子優(yōu)雅實現(xiàn)的軸承:Editable.Factory

          SelectionSpanWatcher在setText()方法中消失了。我需要去閱讀它的源碼。

          EditText重寫了getText()、setText(CharSequence text, BufferType type)方法:

          @Override

          public Editable getText() {

              CharSequence text = super.getText();

              // This can only happen during construction.

              if(text == null) {

                  returnnull;

              }

              if(text instanceofEditable) {

                  return(Editable) super.getText();

              }

              super.setText(text, BufferType.EDITABLE);

              return(Editable) super.getText();

          }

           @Override

           public voidsetText(CharSequence text, BufferType type) {

               super.setText(text, BufferType.EDITABLE);

          }

          從源碼上看,重寫的唯一目的是將BufferType設置為BufferType.EDITABLE。

          我們都知道TextView有三種文本模式:

          1)BufferType.NORMAL 靜態(tài)文本模式,這種模式的文本無法編輯,也沒有富文本樣式;

          2)BufferType.SPANNABLE 帶文本樣式的模式,不可編輯。當TextView.isTextSelectable()返回true時,TextView的文本模式;

          3)BufferType.EDITABLE EditText的文本模式,可編輯,帶文本樣式。

          這里不具體講這三種模式相關的內(nèi)容。只需要知道EditText的模式是BufferType.EDITABLE。

          那么,BufferType.EDITABLE與“軸承”又有什么關系呢? 確實有關系。

          閱讀上面的源碼片段時,不知道有沒有人注意到setText(CharSequence)傳入一個CharSequence對象,TextView#getText()返回的是CharSequence對象, EditText#getText()卻返回一個Editable對象。它是在什么時候,如何完成的轉(zhuǎn)換呢?它會不會是一個突破口?

          從Editable getText()源碼看,它是在super.setText(text, BufferType.EDITABLE)中完成轉(zhuǎn)換的。

          在TextView源碼中,setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen)有這樣一個流程分支:

          private voidsetText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {

              if(type == BufferType.EDITABLE || getKeyListener() != null|| needEditableForNotification) {

                  ...

                  Editable t = mEditableFactory.newEditable(text);

                  text = t;

                  ...

              }

              ...

              mBufferType = type;

              setTextInternal(text);

              ...

          }

          由此可見,我們賦值給EditText的CharSequence對象先經(jīng)過mEditableFactory轉(zhuǎn)換為Editable對象,最終被真正賦值給EditText,mEditableFactory的類型正是Editable.Factory,這是一個靜態(tài)內(nèi)部類。

          我們看看Editable.Factory的具體實現(xiàn)是什么:

          /**

           * Factory used by TextView to create new {@link Editable Editables}. You can subclass

           * it to provide something other than {@link SpannableStringBuilder}.

           *

           * @see android.widget.TextView#setEditableFactory(Factory)

           */

           public static class Factory {

              private static Editable.Factory sInstance = newEditable.Factory();


              /**

               * Returns the standard Editable Factory.

               */

              public static Editable.Factory getInstance() {

                  returnsInstance;

              }


              /**

               * Returns a new SpannedStringBuilder from the specified

               * CharSequence.  You can override this to provide

               * a different kind of Spanned.

               */

              public Editable newEditable(CharSequence source) {

                  return new SpannableStringBuilder(source);

              }

          }

          很簡單的轉(zhuǎn)換,它將CharSequence對象轉(zhuǎn)換為Editable的子類SpannableStringBuilder的對象。

          我們看一看這個構(gòu)造器:

          public SpannableStringBuilder(CharSequence text, intstart, intend) {

              ...

              mText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(srclen));

              ...

              if(text instanceofSpanned) {

                  Spanned sp = (Spanned) text;

                  Object[] spans = sp.getSpans(start, end, Object.class);

                  for(intii = 0; ii < spans.length; ii++) {

                      if(spans[ii] instanceofNoCopySpan) {

                          continue;

                      }

                      ...

                      setSpan(false, spans[ii], st, en, fl, false);

                  }

                  restoreInvariants();

              }

          }

          這就是軸承斷掉的原因所在。

          前面提到SpanWatcher繼承自NoCopySpan,而NoCopySpan是一個標記接口。它的作用就是標記一個Span無法被拷貝。SpannableStringBuilder在構(gòu)造的時候,會忽略掉所有NoCopySpan及其子類。因此,SelectionSpanWatcher沒有被賦值給EditText的文本。

          既然NoCopySpan不被復制,那我們等SpannableStringBuilder構(gòu)造好后重新設置便好了。Editable.Factory的注釋讓我看到了希望。他可以被重寫,并被重新注入EditText。

          android.widget.TextView#setEditableFactory(Factory)

          下面是重寫的Editable.Factory,作用是重新把NoCopySpan設置到SpannableStringBuilder上:

          package com.iyao

          import android.text.Editable

          import android.text.NoCopySpan

          import android.text.SpannableStringBuilder

          import android.text.Spanned

          import android.text.style.BackgroundColorSpan


          class NoCopySpanEditableFactory(private var arg val spans: NoCopySpan): Editable.Factory() {

              override fun newEditable(source: CharSequence): Editable {

                  return SpannableStringBuilder.valueOf(source).apply {

                      spans.forEach {

                          setSpan(it, 0, source.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)

                      }

                  }

              }

          }

          沒錯,算空行一共17行代碼。它就是這個輪子的新軸承。現(xiàn)在我們重新使用它。

          通過editText.setEditableFactory()換上新的軸承,讓輪子跑起來:

          editText.setEditableFactory(NoCopySpanEditableFactory(SelectionSpanWatcher(DataBindingSpan::class)))

          editText.setOnKeyListener { v, keyCode, event ->

              if(keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {

                  return @setOnKeyListenerKeyCodeDeleteHelper.onDelDown((v as EditText).text)

              }

              return @setOnKeyListenerfalse

          }

          一個“優(yōu)雅的”實現(xiàn)誕生了,你可以像微博一樣在評論中使用@人了。

          運行效果:

          5、微信的做法

          微信的處理方式要簡單一些,他們不禁止在Span覆蓋的文本中插入光標,而是當Span覆蓋的文本改變后清除Span以及數(shù)據(jù)。他們同樣要監(jiān)聽刪除鍵實現(xiàn)Span整體刪除,只是表現(xiàn)上與微博稍有區(qū)別。

          微信的三部曲。

          首先,定義一個接口用來判斷Span是否失效:

          package com.iyao

          import android.text.Spannable

          interface RemoveOnDirtySpan {

              fun isDirty(text: Spannable): Boolean

          }

          其次,讓SpannableData實現(xiàn)此接口。當然,你也可以讓RemoveOnDirtySpan繼承DataBindingSpan,盡管我覺得這樣不符合“六大”。

          class SpannableData(privateval spanned: String): DataBindingSpan<String>, RemoveOnDirtySpan {

              override fun spannedText(): CharSequence {

                  return SpannableString(spanned).apply {

                      setSpan(ForegroundColorSpan(Color.RED), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

                  }

              }


              override fun bindingData(): String {

                  return spanned

              }


              override fun isDirty(text: Spannable): Boolean {

                  val spanStart = text.getSpanStart(this)

                  val spanEnd = text.getSpanEnd(this)

                  return spanStart >= 0&& spanEnd >= 0&& text.substring(spanStart, spanEnd) != spanned

              }

          }

          最后,重新寫一個DirtySpanWatcher用來刪除失效的Span:

          package com.iyao

          import android.text.SpanWatcher

          import android.text.Spannable

          class DirtySpanWatcher(private val removePredicate: (Any) -> Boolean) : SpanWatcher {

              override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int,

                                         nend: Int) {

                  if(what is RemoveOnDirtySpan && what.isDirty(text)) {

                      val spanStart = text.getSpanStart(what)

                      val spanEnd = text.getSpanEnd(what)

                      text.getSpans(spanStart, spanEnd, Any::class.java).filter {

                          removePredicate.invoke(it)

                      }.forEach {

                          text.removeSpan(it)

                      }

                  }

              }


              override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {

              }


              override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {

              }

          }

          現(xiàn)在,我們讓微信也跑起來:

          editText.setEditableFactory(NoCopySpanEditableFactory(DirtySpanWatcher{

              it is ForegroundColorSpan || it is RemoveOnDirtySpan

          }))

          editText.setOnKeyListener { v, keyCode, event ->

              if(keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {

                  KeyCodeDeleteHelper.onDelDown((v as EditText).text)

              }

              return @setOnKeyListenerfalse

          }

          需要注意,微信和微博有一點小區(qū)別,微博有二次確認刪除選中,微信沒有。代碼上的差別僅僅是微信少了一個return@setOnKeyListener。

          運行效果:

          6、QQ的做法

          QQ的做法太簡單,我不太想講它。這里寫一個簡單的Demo演示一下。

          QQ同樣需要用到DataBindingSpan<T>,甚至你也可以不用。它的核心是ImageSpan:

          class SpannableData(privateval spanned: String): DataBindingSpan<String> {


              override fun spannedText(): CharSequence {

                  returnSpannableString("@$spanned ").apply {

                      setSpan(ImageSpan(LabelDrawable("@$spanned", color = Color.LTGRAY), spanned), 0, length-1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

                  }

              }

              override fun bindingData(): String {

                  return spanned

              }

          }

          現(xiàn)在只需要實現(xiàn)一個繪制文字的Drawable,這里我取名叫LabelDrawable,也許并不準確:

          class LabelDrawable(val text: CharSequence, private val textPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {

              textSize = 42f

              this.color = Color.DKGRAY

              textAlign = Paint.Align.CENTER

          }, color: Int): ColorDrawable(color) {

              init {

                  calculateBounds()

              }

              override fun draw(canvas: Canvas) {

                  super.draw(canvas)

                  canvas.drawText(text, 0, text.length, bounds.centerX().toFloat(), bounds.centerY().toFloat() + getBaselineOffset(textPaint.fontMetrics), textPaint)

              }


              private fun calculateBounds() {

                  textPaint.getTextBounds(text.toString(), 0, text.length, bounds)

                  bounds.inset(-8, -4)

                  bounds.offset(8, 0)

              }


              private fun getBaselineOffset(fontMetrics: Paint.FontMetrics): Float {

                  return (fontMetrics.descent - fontMetrics.ascent) / 2- fontMetrics.descent

              }

          }

          就像普通的Span一樣使用他就行了。

          運行效果:

          如果想要做的更好一點,你需要處理多行文本measure、layout、draw等問題。給個小提示,TextView截屏也是一個Drawable。如果有一個View,即使它并未attach到Window上,我們也可以手動調(diào)用measure()、layout()、draw()方法獲取一個View的截圖Drawable用來添加到ImageSpan中使用,不過這樣無法響應觸摸事件。

          7、獲取文本中綁定的數(shù)據(jù)

          用下面的代碼就行了:

          val strings = editText.text.let {

              it.getSpans(0, it.length, DataBindingSpan::class.java)

          }.map { it.bindingData() }

          8、本文源碼附件下載

          (因無法上傳附件,請從鏈接:http://www.52im.net/thread-2165-1-1.html 處下載之!)

          9、題外話:本文代碼是Kotlin寫的,但我想要Java版的@人實現(xiàn),怎么辦?

          是的,Kotlin暫時還沒這么廣泛的使用,用不了。

          但,@這個看似很簡都的功能,實際上要不出bug的做好,還是有點難度,或者說代碼量還不算小。

          那么,哪里能找到靠譜的@人功能的Java版實現(xiàn)?

          答案在這里:可以下載網(wǎng)易云信官方開源的IM Demo,里面就有@功能完整代碼實現(xiàn):

          ▲ @人功能完整源碼位置  

          別跟我說這是違法的,他們自已說是開源。。。

          網(wǎng)易云信的IM Demo下載地址:點此進入

          網(wǎng)易云信的IM Demo的Github地址:https://github.com/netease-im/NIM_Android_Demo

          好了,我沒有收網(wǎng)易云信任何好處費,之所以推薦你去“扒”它的源碼,是因為我評估了主流的第3方IM開源的Demo代碼后,@人功能寫的還算不錯的,就只有網(wǎng)易云信了,木有辦法。

          附錄:更多精品資源下載

          [1] 精品源碼下載:

          Java NIO基礎視頻教程、MINA視頻教程、Netty快速入門視頻 [有源碼]

          輕量級即時通訊框架MobileIMSDK的iOS源碼(開源版)[附件下載]

          開源IM工程“蘑菇街TeamTalk”2015年5月前未刪減版完整代碼 [附件下載]

          微信本地數(shù)據(jù)庫破解版(含iOS、Android),僅供學習研究 [附件下載]

          NIO框架入門(四):Android與MINA2、Netty4的跨平臺UDP雙向通信實戰(zhàn) [附件下載]

          NIO框架入門(三):iOS與MINA2、Netty4的跨平臺UDP雙向通信實戰(zhàn) [附件下載]

          NIO框架入門(二):服務端基于MINA2的UDP雙向通信Demo演示 [附件下載]

          NIO框架入門(一):服務端基于Netty4的UDP雙向通信Demo演示 [附件下載]

          用于IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]

          高仿Android版手機QQ可拖拽未讀數(shù)小氣泡源碼 [附件下載]

          一個WebSocket實時聊天室Demo:基于node.js+socket.io [附件下載]

          Android聊天界面源碼:實現(xiàn)了聊天氣泡、表情圖標(可翻頁) [附件下載]

          高仿Android版手機QQ首頁側(cè)滑菜單源碼 [附件下載]

          開源libco庫:單機千萬連接、支撐微信8億用戶的后臺框架基石 [源碼下載]

          分享java AMR音頻文件合并源碼,全網(wǎng)最全

          微信團隊原創(chuàng)Android資源混淆工具:AndResGuard [有源碼]

          一個基于MQTT通信協(xié)議的完整Android推送Demo [附件下載]

          Android版高仿微信聊天界面源碼 [附件下載]

          高仿手機QQ的Android版鎖屏聊天消息提醒功能 [附件下載]

          高仿iOS版手機QQ錄音及振幅動畫完整實現(xiàn) [源碼下載]

          Android端社交應用中的評論和回復功能實戰(zhàn)分享[圖文+源碼]

          Android端IM應用中的@人功能實現(xiàn):仿微博、QQ、微信,零入侵、高可擴展[圖文+源碼]

          [2] 精品文檔和工具下載:

          計算機網(wǎng)絡通訊協(xié)議關系圖(中文珍藏版)[附件下載]

          史上最全即時通訊軟件簡史(精編大圖版)[附件下載]

          重磅發(fā)布:《阿里巴巴Android開發(fā)手冊(規(guī)約)》[附件下載]

          阿里技術(shù)結(jié)晶:《阿里巴巴Java開發(fā)手冊(規(guī)約)-終極版》[附件下載]

          基于RTMP協(xié)議的流媒體技術(shù)的原理與應用(技術(shù)論文)[附件下載]

          獨家發(fā)布《TCP/IP詳解 卷1:協(xié)議》CHM版 [附件下載]

          良心分享:WebRTC 零基礎開發(fā)者教程(中文)[附件下載]

          MQTT協(xié)議手冊(中文翻譯版)[附件下載]

          經(jīng)典書籍《UNIX網(wǎng)絡編程》最全下載(卷1+卷2、中文版+英文版)[附件下載]

          音視頻開發(fā)理論入門書籍之《視頻技術(shù)手冊(第5版)》[附件下載]

          國際電聯(lián)H.264視頻編碼標準官方技術(shù)手冊(中文版)[附件下載]

          Apache MINA2.0 開發(fā)指南(中文版)[附件下載]

          網(wǎng)絡通訊數(shù)據(jù)抓包和分析工具 Wireshark 使用教程(中文) [附件下載]

          最新收集NAT穿越(p2p打洞)免費STUN服務器列表 [附件下載]

          高性能網(wǎng)絡編程經(jīng)典:《The C10K problem(英文)》[附件下載]

          即時通訊系統(tǒng)的原理、技術(shù)和應用(技術(shù)論文)[附件下載]

          技術(shù)論文:微信對網(wǎng)絡影響的技術(shù)試驗及分析[附件下載]

          華為內(nèi)部3G網(wǎng)絡資料: WCDMA系統(tǒng)原理培訓手冊[附件下載]

          網(wǎng)絡測試:Android版多路ping命令工具EnterprisePing[附件下載]

          Android反編譯利器APKDB:沒有美工的日子里繼續(xù)堅強的擼

          一款用于P2P開發(fā)的NAT類型檢測工具 [附件下載]

          兩款增強型Ping工具:持續(xù)統(tǒng)計、圖形化展式網(wǎng)絡狀況 [附件下載]

          [3] 精選視頻、演講PPT下載:

          美圖海量用戶的IM架構(gòu)零基礎演進之路(PPT)[附件下載]

          開源實時音視頻工程WebRTC的架構(gòu)詳解與實踐總結(jié)(PPT+視頻)[附件下載]

          QQ空間百億級流量的社交廣告系統(tǒng)架構(gòu)實踐(視頻+PPT)[附件下載]

          海量實時消息的視頻直播系統(tǒng)架構(gòu)演進之路(視頻+PPT)[附件下載]

          YY直播在移動弱網(wǎng)環(huán)境下的深度優(yōu)化實踐分享(視頻+PPT)[附件下載]

          QQ空間移動端10億級視頻播放技術(shù)優(yōu)化揭秘(視頻+PPT)[附件下載]

          RTC實時互聯(lián)網(wǎng)2017年度大會精選演講PPT [附件下載]

          微信分享開源IM網(wǎng)絡層組件庫Mars的技術(shù)實現(xiàn)(視頻+PPT)[附件下載]

          微服務理念在微信海量用戶后臺架構(gòu)中的實踐(視頻+PPT)[附件下載]

          移動端IM開發(fā)和構(gòu)建中的技術(shù)難點實踐分享(視頻+PPT)[附件下載]

          網(wǎng)易云信的高品質(zhì)即時通訊技術(shù)實踐之路(視頻+PPT)[附件下載]

          騰訊音視頻實驗室:直面音視頻質(zhì)量評估之痛(視頻+PPT)[附件下載]

          騰訊QQ1.4億在線用戶的技術(shù)挑戰(zhàn)和架構(gòu)演進之路PPT[附件下載]

          微信朋友圈海量技術(shù)之道PPT[附件下載]

          手機淘寶消息推送系統(tǒng)的架構(gòu)與實踐(音頻+PPT)[附件下載]

          如何進行實時音視頻的質(zhì)量評估與監(jiān)控(視頻+PPT)[附件下載]

          Go語言構(gòu)建高并發(fā)消息推送系統(tǒng)實踐PPT(來自360公司)[附件下載]

          網(wǎng)易IM云千萬級并發(fā)消息處理能力的架構(gòu)設計與實踐PPT [附件下載]

          手機QQ的海量用戶移動化實踐分享(視頻+PPT)[附件下載]

          釘釘——基于IM技術(shù)的新一代企業(yè)OA平臺的技術(shù)挑戰(zhàn)(視頻+PPT)[附件下載]

          微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(PPT講稿)[附件下載]

          Netty的架構(gòu)剖析及應用案例介紹(視頻+PPT)[附件下載]

          聲網(wǎng)架構(gòu)師談實時音視頻云的實現(xiàn)難點(視頻采訪)

          滴滴打車架構(gòu)演變及應用實踐(PPT講稿)[附件下載]

          微信海量用戶背后的后臺系統(tǒng)存儲架構(gòu)(視頻+PPT)[附件下載]

          在線音視頻直播室服務端架構(gòu)最佳實踐(視頻+PPT)[附件下載]

          從0到1:萬人在線的實時音視頻直播技術(shù)實踐分享(視頻+PPT)[附件下載]

          微信移動端應對弱網(wǎng)絡情況的探索和實踐PPT[附件下載]

          Android版微信從300KB到30MB的技術(shù)演進(PPT講稿)[附件下載]

          從零開始搭建瓜子二手車IM系統(tǒng)(PPT)[附件下載]

          極光分享:高并發(fā)海量消息推送系統(tǒng)架構(gòu)演進(視頻+PPT)[附件下載]

          (本文同步發(fā)布于:http://www.52im.net/thread-2165-1-1.html



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 石景山区| 永德县| 宁蒗| 青神县| 于都县| 嘉禾县| 白玉县| 八宿县| 阿合奇县| 文化| 禄丰县| 南城县| 泰州市| 通渭县| 武陟县| 桐庐县| 龙里县| 汝阳县| 嘉义县| 衡阳县| 彰化县| 安岳县| 吉木萨尔县| 贺兰县| 扎兰屯市| 化州市| 梁平县| 正宁县| 台南市| 兰西县| 永定县| 通许县| 万载县| 平江县| 西乡县| 汝阳县| 涿州市| 海南省| 金寨县| 丰宁| 吴川市|