前语

前不久,我写过一篇文章:迟来的续集–Drawable+Animator,将高雅进行到底 , 并在其间留下一个考虑题:” 用 动画Drawable 是否能够让 ImageSpan 直接动起来”

信任咱们也进行了尝试,而且不出意外地出现了意外!即使运用能够动起来的Drawable构建ImageSpan,也没有让他动起来!

好玩系列:听说你的ImageSpan没能动起来?

今天咱们将在一个愉快的氛围下,让ImageSpan动起来,并进行一些更深层次的探究,不出意外,这将是Drawable相关文章的终结篇。

别的,有几篇相关文章能够作为扩展阅览:

  • 三思系列:从头认识Drawable
  • 三思系列:为什么要自界说View

是否用得上

“学而时习之,不亦说乎” — 《论语-学而》

Span是Android中完结富文本的一种办法,读者诸君请留意,是一种办法而不是仅有办法! 除却Span机制,仍旧有其他办法展现富文本。

但不可否认:Span是十分轻量的一种办法,尽管这种 脱离展现容器的轻量 使得它的规划并不简略, 导致了简略运用它时很方便,重度运用它时 难以如指臂使 ,并会遭遇功用瓶颈。

以Juejin为例,只需我持续创造读者感兴趣的内容,我信任终有一天会解锁Lv10级作者,那么:

“在APP上给我颁发一个会闪烁的徽章,并追加在昵称后,以显现身份”,包含不限于:我的主页、文章作者栏、评论区、文章中 @我 的当地

这个功用好像很合情合理。

用ImageSpan计划完结这一需求也很合理,而且这一解析、展现计划能够多处复用,并不需求四处精心保护布局。尽管juejin并未这样做

简略策画后,今天的常识必定有运用的远景,稳赚不亏!

制造一个翻车现场

仍是凭借先前的项目:DrawableWorkShop ,先制作一个翻车现场。

在上一篇文章中,咱们现已完结了动画Drawable,正好利用它生成ImageSpan。咱们再增加DrawableStart 用作比照。

要害代码如下:

val tvSpan = findViewById<TextView>(R.id.tv_span)
val drawable = createADrawable()
val imgSpan = ImageSpan(drawable)
val ss = SpannableString("ImageSpan *")
ss.setSpan(imgSpan, 10, 11, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
tvSpan.text = ss
val drawableStart = createADrawable()
tvSpan.setCompoundDrawables(drawableStart, null, null, null)
tvSpan.setOnClickListener {
    drawable.start()
    drawableStart.start()
}
fun createADrawable(): AnimLetterDrawable2 {
    val drawable = AnimLetterDrawable2()
    drawable.textSize = 20f
    drawable.letters = "span"
    drawable.setBounds(0, 0, 100, 100)
    return drawable
}

点击TextView敞开动画,好,果然翻车了!ImageSpan并未动起来,而DrawableStart动起来了。

温故而知新 — 原因剖析

在前两篇相关文章中,咱们现已窥视到动画的原理:

依照特定的时刻序列制作对应帧,利用视觉暂留构成动画作用

无论是控件的特点动画、仍是Drawable动画,其本质均为此。在前两篇文章中,咱们分别用了两种办法驱动Drawable构成动画作用,稍作温习:

  • 根据 Drawable#scheduleSelf API,向宿主View Post 一个推迟履行的 Runnable 事务逻辑为从头制作。在Handler音讯机制的驱动下, Choreographer 完结了动画基本原理
  • 根据 ValueAnimator,依照时序履行回调,事务逻辑为从头制作。仍旧是凭借Handler音讯机制的驱动, Choreographer 完结动画基本原理。

此刻,能够做出大胆的假定:问题本质是没有正确从头制作

在前文中,咱们现已知道,Drawable从头制作的中心是 invalidateSelf()

class Drawable {
    public void invalidateSelf() {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }
}

而该API凭借了 Drawable.Callback 委托完结。

Debug之后能够发现,合作ImageSpan运用时,Drawable并未持有Callback实例

比照参阅TextView设置DrawableStart的相关中心代码,疏忽掉无关细节,其间调用了 Drawable#setCallback(this)

class TextView {
    private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
        boolean hasRelativeDrawables = (start != null) || (end != null);
        if (hasRelativeDrawables) {
            //ignore 
            if (start != null) {
                //ignore
                //要点
                start.setCallback(this);
                //ignore
            } else {
                dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
            }
            if (end != null) {
                //ignore
                end.setCallback(this);
                //ignore
            } else {
                dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
            }
            //ignore
        }
    }
}

至此,估测得到验证。

直面问题 — 以最简略的代码让ImageSpan动起来

才思敏捷的读者或许现已想到,给ImageSpan内部的Drawable设置Callback不就能够了吗?就像这样:

 tvSpan.setOnClickListener {
    //设置Callback
    drawable.callback = it
    drawable.start()
    //屏蔽掉DrawableStart的干扰
    // drawableStart.start()
}
好玩系列:听说你的ImageSpan没能动起来?

当你自信满满的尝试了一下,哎呀,又TM翻车了!!!

好玩系列:听说你的ImageSpan没能动起来?

翻车原因

让咱们再温习一下:

public class View {
    public void invalidateDrawable(@NonNull Drawable drawable) {
        //看这儿的校验
        if (verifyDrawable(drawable)) {
            final Rect dirty = drawable.getDirtyBounds();
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            //这儿是改写
            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
            rebuildOutline();
        }
    }
    protected boolean verifyDrawable(@NonNull Drawable who) {
        // Avoid verifying the scroll bar drawable so that we don't end up in
        // an invalidation loop. This effectively prevents the scroll bar
        // drawable from triggering invalidations and scheduling runnables.
        return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who)
                || (mDefaultFocusHighlight == who);
    }
}

很明显,ImageSpan内包含的Drawable和TextView之间并无直接相关!

留意,TextView的判别逻辑存在重载,仅布景、聚集作用、DrawableStart、DrawableTop、DrawableEnd、DrawableBottom 是有相关的:

class TextView {
    protected boolean verifyDrawable(@NonNull Drawable who) {
        final boolean verified = super.verifyDrawable(who);
        if (!verified && mDrawables != null) {
            for (Drawable dr : mDrawables.mShowing) {
                if (who == dr) {
                    return true;
                }
            }
        }
        return verified;
    }
}

暗度陈仓,绕过校验

才思敏捷的读者朋友必定想到了:”既然是校验出的问题,那我绕过校验不就好了”,很快掏出了代码V2:

tvSpan.setOnClickListener {
//            drawable.callback = it //这种办法无效,Drawable和TextView之间无相关
    drawable.callback = object : Drawable.Callback {
        override fun invalidateDrawable(who: Drawable) {
            //直接刷,绕过校验
            it.invalidate()
        }
        override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
            it.scheduleDrawable(who, what, `when`)
        }
        override fun unscheduleDrawable(who: Drawable, what: Runnable) {
            it.unscheduleDrawable(who, what)
        }
    }
    drawable.start()
//            drawableStart.start()
}
好玩系列:听说你的ImageSpan没能动起来?

果然,它动起来了!

作者按:让ImageSpan动起来的中心常识,到此现已完毕,但常识的探究还未完毕,下面将打开Span世界的大门

高雅,永恒的寻求

我一向寻求编程中的一种高雅:

  • 杂乱度按必要程度分层展开,防止 没有必要的详细导致难以理解的杂乱
  • 有条有理,当代码不得不做出改动时,不因代码间没必要的耦合,加大变化难度

而咱们目前面临的问题,恰恰便是一个好机会,能够顺势研讨源码,汲取常识,在此基础上,封装代码,更高雅地处理问题;而且在研讨的过程中,能够摸索到其它常识模块。

这刚好能够通往第三境 对于 人生寻求三境 ,我会在下一个杂篇和读者们沟通一下心得

直接运用会带来的问题

1.尽管有用,但耦合过重,事务代码中不得不露出过多无关代码

咱们现已使ImageSpan动起来了,那我多搞几个动态徽章没有问题吧。

将中心代码复刻,很快就得到了以下代码

好玩系列:听说你的ImageSpan没能动起来?
val tvSpan2 = findViewById<TextView>(R.id.tv_span2)
val infoBuilder = SpannableStringBuilder().append("Leobert")
val madels = arrayListOf<String>("Lv.10", "持续创造", "笔耕不追", "夜以继日")
val drawables: List<AnimLetterDrawable2> = madels.map { madel ->
    appendMadel(infoBuilder, madel).let { drawable ->
        drawable.callback = object : Drawable.Callback {
            override fun invalidateDrawable(who: Drawable) {
                tvSpan2.invalidate()
            }
            //ignore
        }
        drawable
    }
}
tvSpan2.text = infoBuilder
tvSpan2.setOnClickListener {
    drawables.forEach {
        it.start()
    }
}
fun appendMadel(builder: SpannableStringBuilder, madel: String): AnimLetterDrawable2 {
    val drawable = AnimLetterDrawable2()
    //ignore
    val imgSpan = ImageSpan(drawable)
    val ss = SpannableString(" *")
    ss.setSpan(imgSpan, 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    builder.append(ss)
    //ignore,追加了一处ClickSpan,能够直观观测点击触发
    return drawable
}

您现已发现,为了让功用有用,咱们不得不小心的设置回调,一旦有所遗漏,就会带来Bug。

必须留意,仅用于显现时,咱们尚能够压服自己不负责任地疏忽 “不移除回调” 带来的负面影响,比如无效改写、内存走漏。

而在修改时(如EditText中运用,并删去图片)、以及RecycleView中TextView复用(推行到可替换出现内容的状况) 则不得不移除回调。不然轻则造成功用损耗和内存走漏,重则立现 UI bug。

能够幻想,这样的代码太过于丑恶,过多的无关代码露出在事务完结中

2.影响复用

Callback的仅有性导致ImageSpan不能被有用复用,还需求进行必定的改造。

尽管在理论上现已推断出这种做法会影响Drawable复用(以及Span的复用、进一步推行到Spannable的复用),仍旧以代码验证一下推论:

val tvSpan3 = findViewById<TextView>(R.id.tv_span3)
// 沿袭上文中构建的Spannable,它现现已过Callback和tvSpan2高度耦合,
// 咱们期望这样的代码就能够完结方针,但明显目前无法完结
tvSpan3.text = infoBuilder
tvSpan3.setOnClickListener {
    drawables.forEach {
        it.start()
    }
}
tvSpan3.movementMethod = LinkMovementMethod.getInstance()

读者诸君可运用WorkShop自行尝试,不出意外的翻车了吧。

1.处理Callback对复用的约束

明显,咱们无法修改SDK的内容,但能够运用组合形式。

先界说一个回调接口,依靠笼统以解耦。

interface OnRefreshListener {
    /**
     * will be called when a inner drawable of the span want to invalidate.
     *
     * @return true if the listener want to be called in future.
     * false otherwise
     */
    fun onRefresh(): Boolean
}

界说组合,仍旧以接口界说,以提高灵敏度:

interface OnRefreshListeners : OnRefreshListener {
    fun addRefreshListener(callback: OnRefreshListener)
    fun removeRefreshListener(callback: OnRefreshListener)
}

咱们能够顺从其美的界说一个组合Callback完结如下:

class DrawableCallbackComposer : OnRefreshListeners, Drawable.Callback {
    private val mRefreshListeners: MutableCollection<OnRefreshListener> = mutableListOf()
    override fun addRefreshListener(callback: OnRefreshListener) {
        mRefreshListeners.add(callback)
    }
    override fun removeRefreshListener(callback: OnRefreshListener) {
        mRefreshListeners.remove(callback)
    }
    override fun onRefresh(): Boolean {
        val stillActivatedAfterRefresh = mRefreshListeners.filter {
            it.onRefresh()
        }
        mRefreshListeners.clear()
        mRefreshListeners.addAll(stillActivatedAfterRefresh)
        return true
    }
    override fun unscheduleDrawable(who: Drawable, what: Runnable) {
    }
    override fun invalidateDrawable(who: Drawable) {
        onRefresh()
    }
    override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
    }
}

接下来就能够运用 DrawableCallbackComposer 来扩展复用度,但明显,咱们并不乐意到处暴露 DrawableCallbackComposer 的操作,持续考虑封装和躲藏。

2.李代桃僵,利用署理

如果咱们具有一个署理层,它帮助 Drawable 处理 DrawableCallbackComposer子节点 OnRefreshListener 注册与解注册,而且终究体现为一个 Drawable,那么 暴露的操控代码 将替换为简略的 依靠注入中的实例供给

根据接口的灵敏性,咱们能够顺从其美的界说如下 DrawableProxy 类。

它能够直接署理原Drawable关于制作的全部内容,又追加了 运用 DrawableCallbackComposer 和 回调注册解注册的必要操控逻辑

class DrawableProxy
@JvmOverloads constructor(
    proxy: Drawable? = null,
    private val drawableCallbackComposer: DrawableCallbackComposer = DrawableCallbackComposer()
) : Drawable(),
    ResizeDrawable,
    Drawable.Callback by drawableCallbackComposer,
    OnRefreshListeners by drawableCallbackComposer {
    private var proxySafety: Drawable = proxy?.also { it.callback = drawableCallbackComposer } ?: this
        set(drawable) {
            field.callback = null
            drawable.callback = drawableCallbackComposer
            field = drawable
            needResize = true
            invalidateDrawable(this)
        }
    fun setProxy(drawable: Drawable) {
        this.proxySafety = drawable
    }
    fun clearProxy() {
        this.proxySafety = this
    }
    override var needResize: Boolean = false
    /*以下是署理完结,无需过度关心*/
    override fun getIntrinsicWidth(): Int {
        return proxySafety.intrinsicWidth
    }
    override fun getIntrinsicHeight(): Int {
        return proxySafety.intrinsicHeight
    }
    override fun draw(canvas: Canvas) {
        proxySafety.draw(canvas)
    }
    override fun setAlpha(alpha: Int) {
        proxySafety.alpha = alpha
    }
    override fun setColorFilter(cf: ColorFilter?) {
        proxySafety.colorFilter = cf
    }
    override fun getOpacity(): Int {
        return proxySafety.opacity
    }
    override fun onBoundsChange(bounds: Rect) {
        super.onBoundsChange(bounds)
        needResize = false
    }
    override fun setBounds(bounds: Rect) {
        super.setBounds(bounds)
        proxySafety.bounds = bounds
    }
    override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
        super.setBounds(left, top, right, bottom)
        proxySafety.setBounds(left, top, right, bottom)
    }
}

至此,咱们将运用DrawableProxy 代替原先的 AnimDrawable,用以构建Span实例。

接下来能够将留意力转移到:”完结、增加 OnRefreshListener”,经过调用宿主TextView的改写,显现动画帧。

持续运用SpanWatcher解放双手

行至此处,您必定不甘心再在事务代码中暴露此类操控代码:

drawable?.addRefreshListener(object : OnRefreshListener {
    override fun onRefresh(): Boolean {
        //ignore 比如生命周期断定 。。。
        view.invalidate()
        return true
    }
})

哪怕您将它封装成一个静态API以供事务代码中调用,也难以达到您对 寻求”高雅” 的要求底线了。

而SDK中现已确定了这位天选打工人 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.
 * */

翻译如下:

SpanWatcher 类型的实例被增加到 Spannable 之后,一旦发生 (Span)符号被 增加、改动、移除时, 实例相应的 API办法 会被调用,起到告诉作用。

藉此,咱们能够顺从其美地简化处理回调的注册与解注册

读者诸君,此刻还请再想一想,它能完美的处理问题吗?

class AnimImageSpanWatcher(view: View) : SpanWatcher, OnRefreshListener {
    private var mLastRefreshStamp: Long = 0
    private val mViewWeakReference: WeakReference<View> = WeakReference(view)
    override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {
        if (what is RefreshSpan) {
            val drawable = what.getInvalidateDrawable()
            drawable?.addRefreshListener(this)
        }
    }
    override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {
        if (what is RefreshSpan) {
            val drawable = what.getInvalidateDrawable()
            drawable?.removeRefreshListener(this)
        }
    }
    override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int, nend: Int) {
    }
    override fun onRefresh(): Boolean {
        val view = mViewWeakReference.get() ?: return false
        //ignore 生命周期有用性判别
        val currentTime = System.currentTimeMillis()
        //加一层过滤,防止改写过于频频
        if (currentTime - mLastRefreshStamp > REFRESH_INTERVAL) {
            mLastRefreshStamp = currentTime
            view.invalidate()
        }
        return true
    }
    companion object {
        private const val REFRESH_INTERVAL = 60
    }
}

作者按:感谢 长安皈故乡 的提醒,我遗漏了部分代码。

interface RefreshSpan {
    fun getInvalidateDrawable(): OnRefreshListeners?
}

行至此处,还剩下要害的一步:凭借TextView本身的机制,让这些类正常工作!

运用 Spannable.Factory 梦幻联动

不清楚读者诸君是否 精心研读TextView 的源码,当然,本篇并不计划展开剖析,TextView 中有一个API:

class TextView {
    /**
     * Sets the Factory used to create new Spannable
     */
    public final void setSpannableFactory(Spannable.Factory factory) {
        mSpannableFactory = factory;
        setText(mText);
    }
}

经过API完结,您能够发现,它会从头调用 setText API,并利用该 Factory 创立用于显现的 Spannable

Spannable.Factory

/**
 * Factory used by TextView to create new Spannables.
 * You can subclass it to provide something other than SpannableString.
 */
public static class Factory {
    //ignore
    /**
     * Returns a new SpannableString from the specified CharSequence.
     * You can override this to provide a different kind of Spannable.
     */
    public Spannable newSpannable(CharSequence source) {
        return new SpannableString(source);
    }
}

值得留意:

规划Spannable的复制时,理所当然存在一些Span不期望被复制,所以规划有该机制,完结 NoCopySpan 的符号(span)不会被复制

所以,可完结类似如下工厂类,完结最后的联动。

class CustomSpannableFactory(private val mNoCopySpans: List<NoCopySpan>) : Spannable.Factory() {
    override fun newSpannable(source: CharSequence): Spannable {
        val spannableStringBuilder = SpannableStringBuilder()
        mNoCopySpans.forEach {
            spannableStringBuilder.setSpan(
                it, 0, 0,
                Spanned.SPAN_INCLUSIVE_INCLUSIVE or Spanned.SPAN_PRIORITY
            )
        }
        spannableStringBuilder.append(source)
        return spannableStringBuilder
    }
}

终究,以伪代码完结展现调用细节如下:

val tvSpan = findViewById<TextView>(R.id.tv_span)
//榜首部分
//如上文,创立一个动画Drawable
val drawable = createADrawable()
//根据署理,利用它简化Drawable.Callback的处理
val proxyDrawable = DrawableProxy(drawable)
//您能够以为这便是一个完结了RefreshSpan的ImageSpan,
//内部做了等高处理等,这些和文章主题无关,扼要代码附于下文
val imgSpan = AnimIsohypseImageSpan(proxyDrawable)
//第二部分
//构建一个演示用的Spannable
val ss = SpannableString("ImageSpan *")
ss.setSpan(imgSpan, 10, 11, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
tvSpan.text = ss
//第三部分
//运用Watcher简化回调设置
val watchers = ArrayList<NoCopySpan>()
watchers.add(AnimImageSpanWatcher(tvSpan))
//运用自界说Factory完结联动
tvSpan.setSpannableFactory(CustomSpannableFactory(watchers))
//演示手动触发动画
tvSpan.setOnClickListener {
    drawable.start()
}

作者按:感谢 长安皈故乡 的提醒,我遗漏了部分代码。

别的还有一部分代码,仅用于参阅,读者诸君需求结合本身项目实际状况进行类簇的扩展

//DrawableProxy中用到
interface ResizeDrawable {
    var needResize: Boolean
}
interface IntegratedSpan
//等高
open class IsohypseImageSpan : ImageSpan, IntegratedSpan {
    //结构器和等高核算、制作等略
    open fun getResizedDrawable(): Drawable {
        val d = drawable
        if (drawableHeight == 0) {
            return d
        }
        if (!resized) {
            resized = true
            d.bounds = Rect(
                0, 0,
                (1f * drawableHeight.toFloat() * d.intrinsicWidth.toFloat() / d.intrinsicHeight).toInt(),
                drawableHeight
            )
        }
        return d
    }
}
//合作自界说的SpanWatcher
open class AnimIsohypseImageSpan : IsohypseImageSpan, RefreshSpan {
    //结构器略
    override fun getInvalidateDrawable(): OnRefreshListeners? {
        val d = getResizedDrawable()
        return if (d is OnRefreshListeners) {
            d
        } else {
            null
        }
    }
}
//用于有重核算高度的需求
class ResizeIsoheightImageSpan : AnimIsohypseImageSpan, RefreshSpan {
    //结构器略
    override fun getResizedDrawable(): Drawable {
        val d = drawable
        if (drawableHeight == 0) {
            return d
        }
        if (d is ResizeDrawable && (d.needResize || !resized)) {
            resizeSpan(d)
        } else if (!resized) {
            resizeSpan(d)
        }
        return d
    }
    private fun resizeSpan(d: Drawable) {
        resized = true
        d.bounds = Rect(0, 0,
                (1f * drawableHeight * d.intrinsicWidth / d.intrinsicHeight).toInt(),
                drawableHeight)
    }
}

实际上,在工程中应用时:

  • 榜首部分咱们会运用工厂类+依靠注入工具,在事务层屏蔽掉细节
  • 第二部分略,怎么构建Spannable并在事务层屏蔽该细节与本文无关
  • 第三部分也能够屏蔽掉一些细节

能够幻想的到,事务层变得朴实了,而组件的 Ability 层(这是笔者自行杜撰的命名,用于描绘自界说的控件、及其拓宽功用的类)也会十分干净,均能够做到依靠笼统,按需运用,简化耦合。

敲响警钟

一般来说,文章写到这,基本是完毕的节奏,或许您也憋了好长一口气才看到这儿,想着能够缓一口气了。

然而有个坏音讯不得不告诉您:其实问题并没有彻底处理

好玩系列:听说你的ImageSpan没能动起来?

回顾一下前文,有这样一段:

必须留意,仅用于显现时,咱们尚能够压服自己不负责任地疏忽 “不移除回调” 带来的负面影响,比如无效改写、内存走漏。

而在修改时(如EditText中运用,并删去图片)、以及RecycleView中TextView复用(推行到可替换出现内容的状况) 则不得不移除回调。不然轻则造成功用损耗和内存走漏,重则立现 UI bug。

咱们现已处理了一部分问题:

  • 简化注册回调,让Spannable能够被多个View有用复用,展现出动画。而且在工程性问题上做的不错。
  • 移除Span时,对回调进行解注册。可回看 AnimImageSpanWatcher 代码
  • 运用者以为本身不再需求被回调更新时,解除回调。可回看 OnRefreshListener 代码,留意,该状况彻底交给事务管控。
  • 弱引用,不妨碍运用者及宿主Activity被回收

但有一个重要的问题:RecycleView中TextView复用(推行到可替换出现内容的状况)没有考虑。

作者按:留意,EditText在修改时的特性、选中特性、Span机制本身的特性问题,没有在本篇中展开。这不代表它们不存在!

持续考虑&十分重要的结语

作者按:本篇的篇幅现已很长了,所以还请没有看过瘾的读者们见谅,考虑到大多数读者的阅览感触,咱们需求在此完毕了

但尚遗留3个问题需求持续考虑:

  1. RecycleView中,控件复用了,问题该怎么处理?

当然,咱们能够简化问题模型,用 RecycleView中TextView复用 来体现太重了,咱们能够简化为:

一个TextView展现 Spannable-A,随后展现 Spannable-B,

  1. 运用EditText修改时,是否会有新的问题,例如:删去时先选中的需求,删去完之后,又应当怎么处理
  2. 以上做法现已是最优了吗?还能不能持续优化

因为我以为以上代码尚存在优化空间,而且估测您的项目中现已运用了Span机制的扩展以完结某些需求;

而我尚不计划花费大量的时刻编写用于兼容用途的功用类。所以我并未将这部分代码传入仓库,防止您直接当做库项目运用,导致原功用模块不可用。

如果您很火急的需求将其移植到项目中,文中的代码现已包括所有要点,但请必须自行测试相关功用模块,做好兼容工作。