统一富媒体动效控件

一种与RecycleView搭配实现高效复用的统一富媒体动效控件

Posted by Tristan on August 9, 2021

背景

富媒体当下各种样式的控件庞乱繁杂,接口方法不统一,开发者经常使用富媒体控件开发时需要注意的细节较多,比如要事先了解多种控件的api请求方法。
另外,富媒体当下发展得很快,快速迭代滋生了媒体控件的多和杂(工程中图片和视频类控件有很多种)。
当下环境的缺口是,整个市场没有一个统一封装的富媒体动效控件,整个应用级别目前没有一个整体的富媒体资源缓存机制,也没有一个与列表类控件形成高效复用的综合富媒体控件。

目标

设计统一富媒体动效控件旨在满足两个场景:

  1. 解决单个运营类场景的复杂媒体高效切换,一个控件可以满足多种效果
  2. 解决类似feed流列表形式的模板统一和高效复用,减少模板量和视图数

multiview_templet

收益

  1. 单个控件职能丰富,将简化Feed流模板的设计和开发工作
  2. 统一API调用后,会大幅降低对富媒体控件的开发学习成本

调研

京东

  1. 基本库调研

    图片加载库:fresco
    动效合成库:lottie、ValueAnimator
    视频播放库:ijkplayer

  2. feed流调研

    com.jingdong.app.mall.home.floor.view.linefloor.widget.VideoSkuView / IjkVideoView

  3. 动效基本没有封装,属性动画使用较多 image

淘宝

  1. 基本库调研

    图片加载库:ImageLoadFeature
    动效合成库:lottie、HGifView
    视频播放库:ijkplayer

  2. feed流调研

    com.taobao.homepage.view.widgets.MultiLayout / HGifView、HVideoView

  3. 视频和GIF封装为一个富布局MultiLayout
    HGifView和HImageView集成同一个父组件TUrlImageView使用同一个资源加载器ImageLoadFeature image

调研结果

两大电商在以下四方面的总结可以归纳为:

  1. 静图上,淘宝对图片组件进行了封装,京东直接使用了fresco库提供的图片组件;
  2. 动效上,淘宝基于图片组件做了封装,京东多使用属性动画的方式产生动画效果;
  3. 两者对lottie的处理是直接使用了L库提供的LottieAnimationView,没有二次封装;
  4. 视频上,两者统一使用了开源的ijkplayer库进行定制自己的视频加载和播放组件;

初步产出

基于对两大电商的调研和自己的业务,我们产出结果如下:

  • 静图使用MultiImageView,三方库选用Fresco或Glide
  • GIF使用MultiGifView,三方库选用Fresco
  • 动效使用MultiLottieView,三方库选用Lottie
  • Banner使用MultiPagerView,结合ViewPager产出banner效果
  • 视频使用MultiVideoView,一方库选用LeadingVideoView支持加载本地资源
  • 直播使用MultiLiveView,二方库选用WPlayer
  • 整体统一组装为WubaMultiView,产出一个统一动效控件

设计

需求明确后,设计上考虑要解决三个问题:

  • 统一性:不同媒体类型调用的接口和模板统一化问题;
  • 复用性:富媒体控件在整个APP级别上的缓存复用问题;
  • 高效性:如何搭配RecycleView进一步深入到控件级别实现高效复用。

1 统一性

首先,高度封装并统一了接口调用方法,使外部调用者对原本多样化动效的请求方式实现成统一的调用方法,控件自身能够按需加载且是高内聚的;
另外,支持扩展新的样式。
控件通过统一的接口封装和灵活切换媒体形式,实现资源请求、播放接口到列表模板的全链统一化。

2 复用性

给予富媒体的多种item缓存和复用能力,通过一个全局静态媒体控件缓存池,自定义大小,实现了富媒体控件在APP级别上的复用。

3 高效性

通过与RecycleView的缓存策略搭配使用,满足富媒体多种动效样式在列表视图展示中,完成了从一级模板布局到二级动效控件的高效双重复用;
既不需要创建和维护多余的富媒体动效模板,也不需要为不展示的动效控件开销额外的内存空间。

整体控件设计流程图:

multiview

实现

1 统一性

1、该控件已枚举了单图(支持Webp格式)、GIF图、Lottie图、Pager定制动效和Video视频,支持扩展能力,开发者还可以继续接入自定义的其它媒体类动效。

public final enum class MediaType private constructor() : kotlin.Enum<MediaType> {
    IMAGE,

    GIF,

    LOTTIE,

    PAGER,

    VIDEO;
}

2、该控件对这些不同类型的动效媒体进行了统一的接口封装,实现请求加载和播放控制等能力的高度统一,对外只提供了流程图中可见的四种基础方法调用:

1)设置当前媒体类型
2)设置URL请求
3)播放控制
4)设置缓存开关

    通过简单的方法封装和内部统一的接口实现完成了控件的高内聚。

3、对外提供的API比起其它单媒体控件设计更简单:

// 设置媒体类型
fun setMediaType(mediaType: MediaType)

// 获取当前的媒体接口
fun getIView() : IView?


// 设置请求的地址url…
fun setUrl(vararg url: String)

// 设置裁剪类型
fun setActualImageScaleType(scaleType: ScalingUtils.ScaleType)

// 设置默认图及其裁剪类型
fun setPlaceholderImage(resourceId: Int, scaleType: ScalingUtils.ScaleType?)

// 设置请求结果回调
fun setOnRequestStatusListener(listener: WubaMultiView.OnRequestStatusListener)


// 动效播放
fun play()

// 动效停止
fun stop()

// 动效暂停
fun pause()

// 动效是否正在播放
fun isPlaying(): Boolean


// 设置缓存
fun setUseCache(useCache: Boolean)

同时,内部媒体类组件的接口实现方法既做到了统一又理解起来简单:

/**
 * 统一动效接口,抽象类
 */
interface IView {

    fun getMediaType() : MediaType

    fun getView(context: Context) : View

    fun setUrl(vararg url: String) {
        val list = ArrayList<String>()
        list.addAll(url)
        setUrl(list)
    }

    fun setUrl(urls: List<String>)

    fun setActualImageScaleType(scaleType: ScalingUtils.ScaleType)

    fun setPlaceholderImage(resourceId: Int, scaleType: ScalingUtils.ScaleType?)

    fun setFailureImage(resourceId: Int, scaleType: ScalingUtils.ScaleType?)

    fun play()

    fun pause()

    fun stop()

    fun isPlaying() : Boolean

    fun setOnRequestStatusListener(listener: WubaMultiView.OnRequestStatusListener)

}

    这种高内聚的接口设计和实现方式,对外部富媒体控件的使用者和内部富媒体样式的扩展者而言,都提供了极简的快速开发和定制入口。

2 复用性

1、该控件设置了全局缓存策略,缓存池是借助枚举类静态成员变量的方式基于整个APP出发设计的,可以实现APP级别的缓存,具有全局复用能力。

enum class MediaType {
    IMAGE {
        override fun getViewClass(): Class<IView> {
            return MultiImageView::class.java as Class<IView>
        }
        override fun getViewCacheSize(): Int {
            return 10
        }
    },
    ...

    companion object {
        private val CACHE_VIEWS = mutableMapOf<String, ArrayList<IView>>()
    }

    internal abstract fun getViewClass() : Class<IView>

    internal abstract fun getViewCacheSize() : Int

    internal fun getView(useCache: Boolean): IView {
        var v: IView? = null
        if (useCache && CACHE_VIEWS.containsKey(this.name) && (CACHE_VIEWS[this.name]?.size ?: 0) > 0) {
            v = CACHE_VIEWS[this.name]?.removeLast()
        }
        return v ?: getViewClass().newInstance()
    }

    internal fun addViewCache(view: IView) {
        if (!CACHE_VIEWS.containsKey(this.name)) {
            CACHE_VIEWS[this.name] = arrayListOf<IView>()
        }
        if ((CACHE_VIEWS[this.name]?.size?:0) < this.getViewCacheSize()) {
            CACHE_VIEWS[this.name]?.add(view)
        }
    }
    
}

2、同时又支持开启和关闭缓存,对一些特定场景或复用频次比较少的媒体控件可以暂时关闭缓存策略,支持设定各媒体自己控件的缓存池大小,灵活性很强。

    具体的缓存策略见《统一富媒体动效控件设计流程图》的左侧部分。

3 高效性

RecycleView通过多级缓存策略,实现了模板级别的高效复用,见下图。图中一个模板对应一个type值。

1、当列表划出手机屏幕时,划出的模板会进入缓存池: recycleview_out_screen

2、对于划入屏幕的模板,首先会考虑从缓存池中提取并复用: recycleview_in_screen

3、使用统一富媒体控件后,各个动效模板都会用这一个控件进行布局,那么对应type的值将会减少为1(模板仅剩一种)。RecycleView简化后的缓存策略如下图所示: recycleview_multi

4、然后通过设置不同的富媒体类型,用同一套模板展示不同的富媒体样式。统一富媒体控件结合RecycleView使用将形成四级缓存策略。 recycleview_multi2

    这里感觉是多了一个缓存层级,其实控件在之前已为我们减少了一层RecycleView的模板类型(即type值),实际上缓存层级并没有增加,只是逻辑上多出了一层而已。

使用

XML配置

唯一注意事项:XML中配置时需要设置媒体类型,即 media_type=”xxx”

<com.wuba.views.media.WubaMultiView
    android:id="@+id/iv_feed_nopic_avator"
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:layout_alignParentEnd="true"
    android:layout_marginTop="2.5dp"
    app:media_type="IMAGE"
    app:scaleType="centerCrop"
    app:placeholderImage="@drawable/xxx"
    app:placeholderImageScaleType="centerCrop"
    app:failureImage="@drawable/xxx"
    app:failureImageScaleType="centerCrop"/>

代码提示

image

预览效果

image

内存波动

优惠券悬浮窗改造

  1. 改造前内存出现了两次波动 image

  2. 改造后内存波动减少了一次 image

改造前的波动是因为场景中使用了两个媒体控件分别承载不同的效果,会出现同时加载静图控件和Lottie控件的情况;
而改造后的控件内部同一时间只会加载一种用户指定的媒体控件,这样表现在内存上波动最多只会出现一次。

总结

  1. 使用统一富媒体动效控件,可以简化RecycleView的模板,与其搭配可以实现item的高效复用,从而大大降低开发维护的成本。
  2. 使用统一富媒体动效控件,可以在APP级别对富媒体类控件整体产生复用,从而降低APP的内存消耗。
  3. 使用统一富媒体动效控件,暴露给开发者的接口极少,对富媒体相关业务开发上手很容易,简单的api设计产生了丰富的媒体动效。