原始 HTML 报告:通用按钮延迟增强执行链路

通用按钮延迟增强执行链路

这篇笔记梳理通用按钮延迟增强逻辑:数据如何进入按钮、动画队列如何生成、播放进度和外部状态如何触发动画,以及按钮显隐性由哪一层控制。
核心链路:

数据来源 -> 对象创建 -> 初始化/绑定 -> 队列/状态准备 -> 外部触发 -> 分发 -> 具体执行 -> 回调/副作用

数据与 transition

延迟增强阶段配置最终进入 NadGeneralButtonModel.transitions。每个阶段由 TransitionItem 表示,核心字段是 styledelayduration

class TransitionItem {
    var style = 0
    var delay: Long = 0
    var duration: Long = 0
 
    companion object {
        const val INITITAL_STATE = 0
        const val BTN_SHOW_STATE = 1
        const val COLOR_CHANGE_STATE = 2
    }
}

在融合流场景中,老的 EnhanceModel.transition 会通过 NadGeneralButtonUtil.toSdkTransition() 转成 nadcore 的 TransitionItem,再填到 NadGeneralButtonModel.transitions

父容器创建按钮并提供驱动入口

NadSummaryViewContainer 负责创建 NadGeneralBtnManagerView,并把它添加到延迟增强按钮容器中。父容器不实现动画细节。

val model = data.generalButton ?: NadGeneralButtonUtil.createGeneralBtnModel(context, data, enhanceModel)
if (mGeneralButtonView == null) {
    mGeneralButtonView = NadGeneralBtnManagerView(
        context,
        NadFlowSummaryButtonHelper(store, model, mOnEnhanceBtnShowCallBack)
    )
} else {
    mGeneralButtonView?.updateData(model)
}
mTransitionButtonContainer.addView(mGeneralButtonView)

驱动入口来自 NadSummaryComponent

showButton.observe(this) { state ->
    summaryView.getGeneralButtonView()?.updateTransitionState(state)
}
 
private fun showAdOperateButton(playerProgress: Int) {
    summaryView.getGeneralButtonView()?.updateTransition(playerProgress)
}

Manager 选择真实按钮并转发调用

NadGeneralBtnManagerView 根据 buttonType 管理两个真实按钮 View:普通按钮 NadGeneralButtonView 和下载按钮 NadDownloadGeneralButtonView

open fun updateData(data: NadGeneralButtonModel) {
    helper.updateModel(data)
    val targetButtonType = helper.getButtonType()
    if (currentButtonType?.value == targetButtonType) {
        getCurrentButtonView()?.updateData(data)
        return
    }
    switchButtonView(targetButtonType)
}

Manager 不直接执行动画,只负责转发进度和状态:

open fun updateTransition(progress: Int) {
    getCurrentButtonView()?.updateTransition(progress)
}
 
open fun updateTransitionState(style: Int) {
    getCurrentButtonView()?.updateTransitionState(style)
}

真实按钮绑定数据并进入 TransitionManager

真实按钮初始化或更新时都会走 bindData()。延迟增强绑定发生在最后一步 bindTransition()

internal open fun bindData() {
    bindTextData()
    bindContainerData()
    bindIconData()
    bindLottieData()
    bindPagData()
    bindListener()
    bindTransition()
}
 
protected open fun bindTransition() {
    transitionManager.bindTransitions(
        helper.getBackgroundPagUrl() != null,
        false,
        true
    )
}

这里仍然是准备阶段,不是播放阶段。

TransitionManager 根据配置生成动画队列

bindTransitions() 负责清理旧队列、处理直接最终态,并把每个 TransitionItem 转成一个 BaseTransitionItemRunnable 放入 queue

fun bindTransitions(usePag: Boolean, isDowngrade: Boolean, shouldPlayEntrance: Boolean) {
    reset()
    if (helper.shouldApplyFinalState()) {
        animator.applyFinalColor()
        return
    }
    helper.getTransitions()?.forEach { item ->
        when (item?.style) {
            TransitionItem.INITITAL_STATE -> animator.animateInit(item)
            TransitionItem.BTN_SHOW_STATE -> animator.animateShow(item)
            TransitionItem.COLOR_CHANGE_STATE -> animator.animateColorChange(item, usePag, isDowngrade, shouldPlayEntrance)
        }
    }
}

animateShow()animateColorChange() 的名字容易误解。它们在这里主要是创建任务并入队,真正播放要等 onStartAnim() 被触发。

Runnable 的具体实现来自匿名子类

BaseTransitionItemRunnable 是抽象模板,本身没有具体动画。

abstract class BaseTransitionItemRunnable(delay: Long, duration: Long) : Runnable {
    override fun run() {
        onStartAnim()
    }
 
    abstract fun onStartAnim()
    abstract fun onCancelAnim()
}

具体逻辑由 NadButtonTransitionManager 里的匿名子类实现。创建 show 任务时,onStartAnim() 负责展示按钮并启动高度/淡入动画:

queue.add(object : BaseTransitionItemRunnable(item.delay, item.duration) {
    override fun onStartAnim() {
        helper.onBtnStateChange(TransitionItem.BTN_SHOW_STATE)
        buttonView.visibility = VISIBLE
        buttonLottie.visibility = VISIBLE
        heightAnim?.start()
        buttonView.startAnimation(fadeInAnimation)
    }
})

创建 color 任务时,onStartAnim() 负责播放背景 PAG 或启动颜色动画:

addTransition(object : BaseTransitionItemRunnable(item.delay, item.duration) {
    override fun onStartAnim() {
        if (usePag && bgPagView.isReady()) {
            bgPagView.visibility = VISIBLE
            bgPagView.play()
        } else {
            colorAnim?.start()
            bgPagView.visibility = GONE
        }
        helper.onBtnStateChange(TransitionItem.COLOR_CHANGE_STATE)
        buttonLottie.playAnimation()
    }
})

所以 queue 里虽然存的类型都是 BaseTransitionItemRunnable,但实际对象分别是 init/show/color 的匿名子类。调用 item.onStartAnim() 时,会动态分发到该对象自己的实现。

两种触发入口共用同一套队列

播放进度触发

update(progress)currentIndex 顺序取当前任务。当播放进度达到 nextStartTime 时,执行当前任务的 onStartAnim(),然后推进索引。

fun update(progress: Int) {
    val item = queue[currentIndex]
    if (progress * 1000 >= nextStartTime) {
        item.onStartAnim()
        nextStartTime += (item.mDuration + item.mDelay).toInt()
        currentIndex++
    }
}

外部状态触发

updateTransitionState(style) 按外部传入的 style 直接触发指定阶段,并手动修正 currentIndex,避免后续重复触发同一阶段。

fun updateTransitionState(style: Int) {
    when (style) {
        TransitionItem.BTN_SHOW_STATE -> {
            if (currentIndex <= 1) {
                queue.getOrNull(style)?.let {
                    it.onStartAnim()
                    currentIndex = 2
                }
            }
        }
    }
}

两种触发入口不是互斥关系,而是共用同一套 queue/currentIndex

updateTransition(progress)
  按播放器进度顺序推进 queue
 
updateTransitionState(state)
  按外部状态直接推动到某个阶段
 
currentIndex
  记录已经走到哪个阶段,用来降低重复触发风险

显隐规则

按钮显隐由两层控制:Manager 层负责切换真实按钮时隐藏旧 View;TransitionManager 层负责延迟展示动画中的真实显隐。

Manager 层

切换按钮类型时,Manager 会先隐藏所有真实按钮:

private fun hideAllButtonViews() {
    generalButtonView?.visibility = GONE
    downloadButtonView?.visibility = GONE
}

如果没有展示动画,复用旧 View 时需要直接设置为 VISIBLE,否则切换后可能一直保持 GONE

TransitionManager 层

BTN_SHOW_STATE 的展示动画在绑定时会先隐藏真实按钮;当 show runnable 被触发时再显示按钮,并启动高度动画和淡入动画。

fun animateShow(item: TransitionItem) {
    buttonView.layoutParams?.height = 0
    buttonView.visibility = GONE
    buttonLottie.visibility = GONE
 
    queue.add(object : BaseTransitionItemRunnable(item.delay, item.duration) {
        override fun onStartAnim() {
            buttonView.visibility = VISIBLE
            buttonLottie.visibility = VISIBLE
            heightAnim?.start()
            buttonView.startAnimation(fadeInAnimation)
        }
    })
}

⚠️ height = 0 依赖 layoutParams 已存在。首次创建时外层 layoutParams 可能尚未挂上,因此稳定的隐藏依据是 buttonView.visibility = GONE

完整链路

1. 数据进入
EnhanceModel.transition / data.generalButton

NadGeneralButtonModel.transitions

buttonType = command / download
 
2. 父容器创建
NadSummaryViewContainer

NadGeneralBtnManagerView(context, helper)

addView 到 ad_video_enhance_btn
 
3. Manager 选择真实按钮
NadGeneralBtnManagerView.updateData()

buttonType 相同:currentButtonView.updateData(data)
buttonType 不同:switchButtonView(type)
 
4. 真实按钮绑定
NadGeneralButtonView.updateData()

bindData()

bindTransition()

transitionManager.bindTransitions(...)
 
5. 动画队列创建
bindTransitions()

INITITAL_STATE     -> animateInit()        -> queue.add(initItem)
BTN_SHOW_STATE     -> animateShow()        -> queue.add(showItem)
COLOR_CHANGE_STATE -> animateColorChange() -> queue.add(colorItem)
 
6. 动画触发
进度触发:updateTransition(progress) -> transitionManager.update(progress)
状态触发:updateTransitionState(state) -> transitionManager.updateTransitionState(state)

item.onStartAnim()
 
7. 具体动画执行
showItem.onStartAnim()  -> VISIBLE + heightAnim + fadeIn
colorItem.onStartAnim() -> PAG / colorAnim + buttonLottie

相关笔记