Source code: please click here

demand

Two display modes:

  1. The anchor is in full screen with other tourists floating on the right. Hereinafter referred to as small screen mode.

  1. Divide the screen equally for everyone. The bisection model is referred to below.

Analysis of the

  • Up to four people with a mic, so it’s easy to customize the coordinate algorithm.
  • A custom ViewGroup is best provided with an interface for margin setting in split mode and size screen mode for easy modification.
  • The SDK manages the drawing and measuring of TextureView itself, so the ViewGroup needs to copy the onMeasure method to notify TextureView of the measurement and drawing.
  • A function that calculates the gradual deceleration from 0.0f to 1.0f to support the animation process.
  • A data model for recording coordinates. And a function that calculates where each View should be placed in both layouts based on the number of existing Child Views.

implementation

1. Define the coordinate data model

private data class ViewLayoutInfo(
    var originalLeft: Int = 0,// Original is the starting value before the animation startsvar originalTop: Int = 0.var originalRight: Int = 0.var originalBottom: Int = 0.var left: Float = 0.0 f,// Unprefixed values are temporary values during the animationvar top: Float = 0.0 f.var right: Float = 0.0 f.var bottom: Float = 0.0 f.var toLeft: Int = 0,// To is the target value of the animationvar toTop: Int = 0.var toRight: Int = 0.var toBottom: Int = 0.var progress: Float = 0.0 f, / / in progress0.0 f ~ 1.0 f, used to control Alpha animationvar isAlpha: Boolean = false,// Transparent animation, newly added to execute this animationvar isConverted: Boolean = false,// controls the progress reversal flagvar waitingDestroy: Boolean = false,// Destroy the View tagvar pos: Int = 0// Record your own index for destruction) {
    init {
        left = originalLeft.toFloat()
        top = originalTop.toFloat()
        right = originalRight.toFloat()
        bottom = originalBottom.toFloat()
    }
}
Copy the code

Above, records the data needed to animate and destroy the View. (In the source code at line 352)

2. Calculate the function of View coordinates in different display modes

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()
))
Copy the code

The add, remove, and toggle methods in the Demo source code repeat too much code, and there is no time to optimize. Just the calculation part of addVideoView (line 141 in the source code) is attached here, and with a little modification, add, Remove, and toggle apply. (Also refer to the calcPosition method in CDNLiveVM for an optimized version) When layoutTopicMode = true, it is the size screen mode.

Since this is a custom algorithm, it can only be used for this type of layout, so no comments. Just to be clear, the ultimate goal of this method is to calculate the current position of each View, save it to the data model defined above, and animate it. (The last line of “Post AnimThread” is the code to animate it. Here, I update each frame by Posting a thread.)

Different implementations can be written according to different requirements, and the data model can be defined.

3. Slow down the algorithm to make the animation look more natural.

private inner class AnimThread( private val viewInfoList: Array<ViewLayoutInfo>, private var duration: Float = 0.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 < 1.0f) {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()// This method executes the task that is waiting. As you can see from the source code, remove, add, and other functions need to be executed in sequence, and moving on to the next animation before the previous one has completed may cause unexpected errors. }}}}Copy the code

In addition to providing the deceleration algorithm, this code also updates the intermediate values of the corresponding View data model, namely the left, top, right, bottom of the model definition species.

The median value is obtained by multiplying the progress value provided by the deceleration algorithm by the distance between the target coordinate and the starting coordinate.

The key code of the gradual deceleration algorithm is:

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

The implementation of this algorithm is flawed because it directly changes the progress time, which will result in a high probability that the execution completion time is inconsistent with the set expected time (for example, the execution completion time is set at 200ms, but may actually exceed 200ms). At the end of this article, I will provide an optimized deceleration algorithm.

The variable waitingTime indicates how long to wait to animate the next frame. Use 1000ms per second, if the target is a 60 refresh rate animation, set it to 1000/60 = 16.66667 (approximate).

After calculating and storing the intermediate values for each View, call requestLayout() to notify the system’s onMeasure and onLayout methods to rearrange the 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.0 f - layoutInfo.progress
            else
                layoutInfo.progress
                                                                         
            child.alpha = progress
        }
    }
}
Copy the code

4. Define margin related variables for simple customization

/ * * *@paramMultipleWidgetPadding: Read in equal mode *@paramMaxWidgetPadding: Read the size of the screen layout *@paramDefMultipleVideosTopPadding: distance at the top of the variable pitch * /
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()
    }
}
Copy the code

The responsibility definition for these three variables in the name is different from the definition when writing the logic, so there is a bit of confusion, please refer to the comment.

Since this is just a custom variable, it is not important and can be changed at will according to the business logic.

5. Copy the onMeasure method, where TextureView is notified to update the size.

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.33334 f).toInt()
    smallViewWidth = (widthSize * 0.3125 f).toInt()
    smallViewHeight = (smallViewWidth.toFloat() * 1.33334 f).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)
    )
}
Copy the code

conclusion

  1. Define the data model, and generally it is sufficient to record the starting left and right coordinates, the target left and right coordinates, and the progress percentage.
  2. According to the needs of clear animation algorithm, here add the optimized deceleration algorithm:
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.
Copy the code
  1. Update the layout according to the value calculated by the algorithm.

This kind of ViewGroup implementation is simple and convenient, involving only a few basic system apis. If you do not want to write onMeasure method can inherit FrameLayout has written onMeasure implementation of the ViewGroup.