current position:Home>Android technology sharing | customize ViewGroup to realize seamless switching between large and small screens in live broadcasting room

Android technology sharing | customize ViewGroup to realize seamless switching between large and small screens in live broadcasting room

2022-01-26 23:50:20 anyRTC

Source code : please Click here

 Insert picture description here

demand

Two display modes :

  1. Anchor full screen , Other tourists hover on the right . Hereinafter referred to as large and small screen mode .

 Insert picture description here

  1. Everyone split the screen . Hereinafter referred to as bisection mode .

 Insert picture description here

analysis

  • most 4 Man and wheat , Making this clear is convenient to customize the coordinate algorithm .
  • Self defined ViewGroup It is better to provide margin setting interfaces for equal division mode and large and small screen mode respectively , Easy to modify .
  • SDK Manage yourself TextureView Drawing and measurement of , therefore ViewGroup Need to copy onMeasure Method to notify TextureView Measure and draw .
  • A calculation 0.0f ~ 1.0f A function of gradual deceleration , Support the animation process .
  • A data model for recording coordinates . And one based on existing Child View The quantity is calculated in two layout modes , Every View Function of placement .

Realization

1. Define the coordinate data model

private data class ViewLayoutInfo( var originalLeft: Int = 0,// original The beginning is the starting value before the animation starts  var originalTop: Int = 0, var originalRight: Int = 0, var originalBottom: Int = 0, var left: Float = 0.0f,//  Prefixed are temporary values during animation  var top: Float = 0.0f, var right: Float = 0.0f, var bottom: Float = 0.0f, var toLeft: Int = 0,// to The beginning is the animation target value  var toTop: Int = 0, var toRight: Int = 0, var toBottom: Int = 0, var progress: Float = 0.0f,//  speed of progress  0.0f ~ 1.0f, Used to control the  Alpha  Animation  var isAlpha: Boolean = false,//  Transparent animation , The newly added performs this animation  var isConverted: Boolean = false,//  control  progress  Inverted markers  var waitingDestroy: Boolean = false,//  Destroy when finished  View  The tag  var pos: Int = 0//  Record your index , In order to destroy  ) {
    init {
        left = originalLeft.toFloat()
        top = originalTop.toFloat()
        right = originalRight.toFloat()
        bottom = originalBottom.toFloat()
    }
}
 Copy code 

above , The execution animation and destruction are recorded View Data required .( In the source code 352 That's ok )

2. Calculate... Under different display modes View A function of coordinates

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 code 

Demo In the source add、remove、toggle Method duplicate code too much , The future needs to be optimized . Only... Is attached here addVideoView The calculation part of ( In the source code 141 That's ok ), It only needs a little modification to apply add、remove and toggle.( Also can reference CDNLiveVM Medium calcPosition Method , For the optimized version )layoutTopicMode = true when , For large and small screen mode .

Because it's a custom algorithm , Only one layout can be applied , So don't write notes . Just be clear , The ultimate goal of this method is to calculate each View Where it should appear at present , Save to the data model defined above and start the animation ( The last line post AnimThread Code for opening animation , I'm here through post A thread updates each frame ).

Different implementations can be written according to different requirements , Finally, it can meet the defined data model .

3. Gradual deceleration algorithm , Make the animation look more natural .

private inner class AnimThread(
    private val viewInfoList: Array<ViewLayoutInfo>,
    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 < 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([email protected], 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 performs the task that is waiting , You can see from the source code ,remove、add And other functions need to be executed in turn , Performing the next animation before the previous animation is completed may lead to unpredictable errors .
            }
        }
    }
}
 Copy code 

In addition to providing deceleration algorithm , Also updated the corresponding View The intermediate value of the data model , That is, the definition of the model left, top, right, bottom .

The progress value provided by the deceleration algorithm , Multiply by the distance between the target coordinate and the starting coordinate , Get the middle value .

The key code of the gradual deceleration algorithm is :

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
}
 Copy code 

The implementation of this algorithm is flawed , Because it directly modifies the progress time , The probability will lead to the completion time and the set expected time ( Such as setting 200ms completion of enforcement , May actually exceed 200ms) Not in conformity with . At the end of the paper, I will provide an optimized deceleration algorithm .

Variable waitingTime Indicates how long to wait to execute the next animation . In seconds 1000ms Calculation is enough , If the goal is 60 Refresh rate animation , Set to 1000 / 60 = 16.66667 that will do ( Approximate value ).

Calculate and store each View After the middle value of , call requestLayout() Of the notification system onMeasure and onLayout Method , Rearrange 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
        }
    }
}
 Copy code 

4. Define variables related to margins , For simple customization and modification

/** * @param multipleWidgetPadding :  Bisection mode read  * @param maxWidgetPadding :  Large and small screen layout reading  * @param defMultipleVideosTopPadding :  Variable distance from the top  */
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 code 

The responsibilities for these three variables are defined when naming , It is different from the definition when writing logic , So it's a little vague , Refer to note .

Because this is only a customized variable , Not important , It can be modified according to the business logic .

5. make carbon copies onMeasure Method , This is mainly to inform TextureView Update 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.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)
    )
}
 Copy code 

summary

  1. Define the data model , In general, record the starting upper, lower, left and right coordinates 、 Up, down, left and right coordinates of the target 、 And progress percentage is enough .
  2. Define the animation algorithm according to the needs , Here we 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 code 
  1. Update according to the value calculated by the algorithm layout Just layout .

Such kind ViewGroup The implementation is simple and convenient , Only a few basic systems are involved API. If you don't want to write onMeasure Methods are inheritable FrameLayout When it has been written onMeasure Realized ViewGroup .

copyright notice
author[anyRTC],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/01/202201262350186819.html

Random recommended