自定义viewgroup的方法有什么(自定义viewgroup教程)

两种显示方式:

  1. 主播全屏,其他游客悬浮在右侧。下面简称大小屏模式。
  2. 所有人等分屏幕。下面简称等分模式。

自定义viewgroup的方法有什么(自定义viewgroup教程)
自定义viewgroup的方法有什么(自定义viewgroup教程)分析

  • 最多4人连麦,明确这点方便定制坐标算法。
  • 自定义的 ViewGroup 最好分别提供等分模式和大小屏模式的边距设置接口,便于修改。
  • SDK 自己管理了 TextureView 的绘制和测量,所以 ViewGroup 需要复写 onMeasure 方法以通知 TextureView 测量和绘制。
  • 一个计算 0.0f ~ 1.0f 逐渐减速的函数,给动画过程做支撑。
  • 一个记录坐标的数据模型。和一个根据现有 Child View 的数量计算两种布局模式下,每个 View 摆放位置的函数。

实现

1.定义坐标数据模型

private data class ViewLayoutInfo(
    var originalLeft: Int = 0,// original开头的为动画开始前的起始值
    var originalTop: Int = 0,
    var originalRight: Int = 0,
    var originalBottom: Int = 0,
    var left: Float = 0.0f,// 无前缀的为动画过程中的临时值
    var top: Float = 0.0f,
    var right: Float = 0.0f,
    var bottom: Float = 0.0f,
    var toLeft: Int = 0,// to开头的为动画目标值
    var toTop: Int = 0,
    var toRight: Int = 0,
    var toBottom: Int = 0,
    var progress: Float = 0.0f,// 进度 0.0f ~ 1.0f,用于控制 Alpha 动画
    var isAlpha: Boolean = false,// 透明动画,新添加的执行此动画
    var isConverted: Boolean = false,// 控制 progress 反转的标记
    var waitingDestroy: Boolean = false,// 结束后销毁 View 的标记
    var pos: Int = 0// 记录自己索引,以便销毁
) {
    init {
        left = originalLeft.toFloat()
        top = originalTop.toFloat()
        right = originalRight.toFloat()
        bottom = originalBottom.toFloat()
    }
}

以上,记录了执行动画和销毁View所需的数据。(于源码中第352行)

2.计算不同展示模式下View坐标的函数

if (layoutTopicMode) {
    var index = 0
    for (i in 1 until childCount) if (i != position) (getChildAt(i).tag as ViewLayoutInfo).run {
        toLeft = measuredWidth - maxWidgetPadding - smallViewWidth
        toTop = defMultipleVideosTopPadding + index * smallViewHeight + index * maxWidgetPadding
        toRight = measuredWidth - maxWidgetPadding
        toBottom = toTop + smallViewHeight
        index++
    }
} else {
    var posOffset = 0
    var pos = 0
    if (childCount == 4) {
        posOffset = 2
        pos++
                                                                                                               
        (getChildAt(0).tag as ViewLayoutInfo).run {
            toLeft = measuredWidth.shr(1) - multiViewWidth.shr(1)
            toTop = defMultipleVideosTopPadding
            toRight = measuredWidth.shr(1) + multiViewWidth.shr(1)
            toBottom = defMultipleVideosTopPadding + multiViewHeight
        }
    }
                                                                                                               
    for (i in pos until childCount) if (i != position) {
        val topFloor = posOffset / 2
        val leftFloor = posOffset % 2
        (getChildAt(i).tag as ViewLayoutInfo).run {
            toLeft = leftFloor * measuredWidth.shr(1) + leftFloor * multipleWidgetPadding
            toTop = topFloor * multiViewHeight + topFloor * multipleWidgetPadding + defMultipleVideosTopPadding
            toRight = toLeft + multiViewWidth
            toBottom = toTop + multiViewHeight
        }
        posOffset++
    }
}

post(AnimThread(
    (0 until childCount).map { getChildAt(it).tag as ViewLayoutInfo }.toTypedArray()
))

Demo源码中的add、remove、toggle方法重复代码过多,未来得及优化。这里只附上 addVideoView 中的计算部分(于源代码中第141行),只需稍微修改即可适用add、remove和toggle。(也可参考 CDNLiveVM 中的 calcPosition 方法,为经过优化的版本)layoutTopicMode = true 时,为大小屏模式。

由于是定制算法,只能适用这一种布局,故不写注释。只需明确一点,此方法最终目的是为了计算出每个View当前应该出现的位置,保存到上面定义的数据模型中并开启动画(最后一行 post AnimThread 为开启动画的代码,我这里是通过 post 一个线程来更新每一帧)。

可根据不同的需求写不同的实现,最终符合定义的数据模型即可。

3.逐渐减速的算法,使动画效果看起来更自然。

private inner class AnimThread(
    private val viewInfoList: Array,
    private var duration: Float = 180.0f,
    private var processing: Float = 0.0f
) : Runnable {
    private val waitingTime = 9L
                                                                                   
    override fun run() {
        var progress = processing / duration
        if (progress > 1.0f) {
            progress = 1.0f
        }
                                                                                   
        for (viewInfo in viewInfoList) {
            if (viewInfo.isAlpha) {
                viewInfo.progress = progress
            } else viewInfo.run {
                val diffLeft = (toLeft - originalLeft) * progress
                val diffTop = (toTop - originalTop) * progress
                val diffRight = (toRight - originalRight) * progress
                val diffBottom = (toBottom - originalBottom) * progress
                                                                                   
                left = originalLeft + diffLeft
                top = originalTop + diffTop
                right = originalRight + diffRight
                bottom = originalBottom + diffBottom
            }
        }
        requestLayout()
                                                                                   
        if (progress  0.8f) {
                var offset = ((progress - 0.7f) / 0.25f)
                if (offset > 1.0f)
                    offset = 1.0f
                processing += waitingTime - waitingTime * progress * 0.95f * offset
            } else {
                processing += waitingTime
            }
            postDelayed(this@AnimThread, waitingTime)
        } else {
            for (viewInfo in viewInfoList) {
                if (viewInfo.waitingDestroy) {
                    removeViewAt(viewInfo.pos)
                } else viewInfo.run {
                    processing = 0.0f
                    duration = 0.0f
                    originalLeft = left.toInt()
                    originalTop = top.toInt()
                    originalRight = right.toInt()
                    originalBottom = bottom.toInt()
                    isAlpha = false
                    isConverted = false
                }
            }
            animRunning = false
            processing = duration
            if (!taskLink.isEmpty()) {
                invokeLinkedTask()// 此方法执行正在等待中的任务,从源码中能看到,remove、add等函数需要依次执行,前一个动画未执行完毕就进行下一个动画可能会导致不可预知的错误。
            }
        }
    }
}

上述代码除了提供减速算法,还一并更新了对应View数据模型的中间值,也就是模型定义种的 left, top, right, bottom 。

本文转载自:https://www.gylmap.com

通过减速算法提供的进度值,乘以目标坐标与起始坐标的间距,得出中间值。

逐渐减速的算法关键代码为:

if (progress > 0.8f) {
    var offset = ((progress - 0.7f) / 0.25f)
    if (offset > 1.0f)
        offset = 1.0f
    processing += waitingTime - waitingTime * progress * 0.95f * offset
} else {
    processing += waitingTime
}

这个算法实现的有缺陷,因为它直接修改了进度时间,大概率会导致执行完毕的时间与设置的预期时间(如设置200ms执行完毕,实际可能超过200ms)不符。文末我会提供一个优化的减速算法。

变量 waitingTime 表示等待多久执行下一帧动画。用每秒1000ms计算即可,如果目标为60刷新率的动画,设置为1000 / 60 = 16.66667即可(近似值)。

计算并存储每个 View 的中间值后,调用 requestLayout() 通知系统的 onMeasure 和 onLayout 方法,重新摆放 View 。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    if (childCount == 0)
        return
                                                                         
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        val layoutInfo = child.tag as ViewLayoutInfo
        child.layout(
            layoutInfo.left.toInt(),
            layoutInfo.top.toInt(),
            layoutInfo.right.toInt(),
            layoutInfo.bottom.toInt()
        )
        if (layoutInfo.isAlpha) {
            val progress = if (layoutInfo.isConverted)
                1.0f - layoutInfo.progress
            else
                layoutInfo.progress
                                                                         
            child.alpha = progress
        }
    }
}

4.定义边距相关的变量,供简单的定制修改

/**
 * @param multipleWidgetPadding : 等分模式读取
 * @param maxWidgetPadding : 大小屏布局读取
 * @param defMultipleVideosTopPadding : 距离顶部变距
 */
private var multipleWidgetPadding = 0
private var maxWidgetPadding = 0
private var defMultipleVideosTopPadding = 0
                                                                                  
init {
    viewTreeObserver.addOnGlobalLayoutListener(this)
    attrs?.let {
        val typedArray = resources.obtainAttributes(it, R.styleable.AnyVideoGroup)
        multipleWidgetPadding = typedArray.getDimensionPixelOffset(
            R.styleable.AnyVideoGroup_between23viewsPadding, 0
        )
        maxWidgetPadding = typedArray.getDimensionPixelOffset(
            R.styleable.AnyVideoGroup_at4smallViewsPadding, 0
        )
        defMultipleVideosTopPadding = typedArray.getDimensionPixelOffset(
            R.styleable.AnyVideoGroup_defMultipleVideosTopPadding, 0
        )
        layoutTopicMode = typedArray.getBoolean(
            R.styleable.AnyVideoGroup_initTopicMode, layoutTopicMode
        )
        typedArray.recycle()
    }
}

取名时对这三个变量的职责定义,与编写逻辑时的定义有出入,所以有点词不达意,需参考注释。

由于这只是定制化的变量,并不重要,可根据业务逻辑自行随意修改。

5.复写 onMeasure 方法,这里主要是通知 TextureView 更新大小。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)
                                                                                               
    multiViewWidth = widthSize.shr(1)
    multiViewHeight = (multiViewWidth.toFloat() * 1.33334f).toInt()
    smallViewWidth = (widthSize * 0.3125f).toInt()
    smallViewHeight = (smallViewWidth.toFloat() * 1.33334f).toInt()
                                                                                               
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        val info = child.tag as ViewLayoutInfo
        child.measure(
            MeasureSpec.makeMeasureSpec((info.right - info.left).toInt(), MeasureSpec.EXACTLY),
            MeasureSpec.makeMeasureSpec((info.bottom - info.top).toInt(), MeasureSpec.EXACTLY)
        )
    }
                                                                                               
    setMeasuredDimension(
        MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
        MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)
    )
}

总结

1.明确数据模型,一般情况下记录起始上下左右坐标、目标上下左右坐标、和进度百分比就足够了。

2.根据需求明确动画算法,这里补充一下优化的减速算法:

factor = 1.0
if (factor == 1.0)
    (1.0 - (1.0 - x) * (1.0 - x))
else
    (1.0 - pow((1.0 - x), 2 * factor))
// x = time.

3.根据算法计算出来的值更新 layout 布局即可。

此类 ViewGroup 实现简单方便,只涉及到几个基本系统API。如不想写 onMeasure 方法可继承 FrameLayout 等已写好 onMeasure 实现的 ViewGroup 。

秒鲨号所有文章资讯、展示的图片素材等内容均为注册用户上传(部分报媒/平媒内容转载自网络合作媒体),仅供学习参考。用户通过本站上传、发布的任何内容的知识产权归属用户或原始著作权人所有。如有侵犯您的版权,请联系我们反馈!本站将在三个工作日内改正。
(0)

大家都在看

  • 封面怎么设计(设计封面图片)

    来源:公众号 PingMianDesigner(平面设计) 正封是封面中最为重要的一个部分,除了要传达图书的内容外,还有着在第一时间抓住卖者目光的作用,一个优秀的封面作品对图书的销…

    2022年4月22日 专栏投稿
  • 苹果解压缩软件哪个好(好用的解压软件推荐)

    苹果电脑专用解压缩软件,这几款App值得推荐。 The Unarchiver – Unzip RAR ZIP for mac(解压缩软件) Unarchiver-Unzip RAR…

    2022年3月14日 专栏投稿
  • 海带怎么吃(为什么晚上不能吃海带)

    海带是大家耳熟能详的一种海鲜蔬菜,因为营养价值高、有养生功效,又被称为长寿菜,但你知道海带不能乱吃吗? 甲亢病人要慎吃海带 甲状腺功能亢进症(甲亢)是一种促进人体活动亢进和新陈代谢…

    2022年4月7日
  • 缩阴怎么缩(缩阴入户)

    现在很多女性朋友,为了追求自己更加完美,也别让夫妻双方在生活上更加快乐,很多女性都希望做一些缩阴运动,也有一些人围着自己身体送健康,每天练习缩肛运动追求健康,我们请养生专家给我们讲…

    2022年4月3日
  • 怎么转档案(档案丢失补办申请书)

    在我们发现个人档案遗失并且无法找回时,要及时找相关部门机构补办遗失的档案。 档案遗失补办 01 我们补办档案时通常要先准备一份档案丢失证明,这种证明通常是学校或单位开的,开好后,让…

    2022年4月25日
  • dnf关羽是什么职业(dnf2021关羽神话装备排名)

    DNF魔枪士征战者(关羽)加点,这个加点适合搬砖,日常,养猪,养狗,养鸡,肝号 但是前期低等级的加点方案不像现在这样的加法,低等级就不说了 仁者见仁智者见智,加点没有绝对的对错 最…

    2022年5月17日 专栏投稿
  • 童颜神器效果怎么样(Tripollar射频美容仪童颜机测评)

    TriPollar 太火了,各平台美妆博主都在大力推荐,它的RF多极射频,射频黄金三角,能量可以高效、均匀、稳定的直达真皮层,并刺激胶原蛋白重建、新生,从而达到紧致肌肤,淡化细纹和…

    2022年3月17日 专栏投稿
  • 个人任务管理工具(工作任务管理工具)

    编辑导语:作为一名B端产品经理,每天需要做的事情有很多,如何提高自己的任务管理能力?作者分享了一个任务管理工具的实践与应用,并思考如何将这款工具的价值最大化。 任务管理工具是B端产…

    2022年6月25日 专栏投稿
  • 手机维修怎么样(全国手机维修培训学校10强)

    先来回答标题中的问题,借用大家熟知的一句话:学习技术任何时候都不晚!当然,手机维修也不例外,毕竟是一门技术行业,它的入局是有一定门槛的,一些赚快钱的人是不愿意在这个行业花费时间去深…

    2022年4月24日 专栏投稿
  • 七彩虹怎么样(七彩虹的主机质量怎么样)

    今年初对于很多游戏玩家来说,市场的环境看起来相当不友好。主要是因为芯片缺货加上挖矿兴起,一下子让硬件大规模缺货涨价,甚至出现买到就是赚到的状况。其实上游芯片厂商也拿出了解决方法,比…

    2022年4月21日 专栏投稿
品牌推广 在线咨询
返回顶部