An overview of the

In the project, we want to realize the functions of sliding-clear screen and sliding-list in the simulated-Douyin broadcast room. In this project, we record the realization process and the record of stepping on pits, hoping to avoid some detours, as a summary of our own

Let’s take a look at the Demo first

Reading articles need to be familiar with some of the content of the event distribution in advance, I believe that we have already understood, there are many excellent articles on the Internet, here recommend two of their own read the deep impression of the article

www.jianshu.com/p/e99b5e8bd…

www.jianshu.com/p/38015afcd…

On this aspect of knowledge, in Android is the most important, is to master the knowledge sooner or later, so I hope we can master in advance, it is best to follow the source code analysis, understand grasp a more profound point

practice

Therefore, the online explanation based on this part of the content has been very detailed, and I will not move bricks here. I will mainly share some ideas and experience generated in the application process of this part of the knowledge in my own project, and solve some bugs

The above are the problems to be solved in the implementation process of the function, which will be expanded in detail below

1. Layout structure

Layout structure is always one of the first issues to be considered in interface design. From the beginning of receiving a requirement, it is necessary to consider how to embed layout layers more elegantly according to the existing layout structure in the project. If you accidentally go down the wrong implementation path, I’m sorry, but even if the feature is implemented, there are a million reasons to go down the refactoring path at the end of the day.

For example, unreasonable implementation, resulting in complex layout structure, nested redundant layers, such as complex and poor code business logic processing, such as resource waste, excessive memory consumption and so on. Although the function is good, there is no difference in the use, but, as a programmer with pursuit, we still have to avoid the occurrence of this situation, right

Unfortunately, this article belongs to the record of the above pit, the following detailed analysis

1.1 Preliminary Implementation

After coming up, the idea is very direct to realize the function of clear screen and sliding screen is the function of every room, and every room is an Item of RecyclerView. Therefore, it is obvious to wrap a layer on the layout of Item and realize the functions of clear screen and slide list, so that each room can go up and down and switch rooms. After switching, the slide screen function is in each room without affecting each other, so it’s easy to understand

In our project, the function of realizing the slide switch in the live broadcast room is RecyclerView + custom LinearLayoutManager. There are many online demos of this part, so we will not expand it

To implement this, a custom layout inherits a RelativeLayout, parses a custom layout file that contains the layout of the live room room and its own slider layout, and replaces the previous room Item layout position with your own layout

  • Because our custom Container layout inherits from a RelativeLayout layout, and the three inner views all fill the parent layout, we have a three-layer overlay effect, similar to the Tiktok studio effect
  • Here we try to wrap the overlay/clear screen control into a ViewGroup that contains sub-views that are subdivided, such as header profile information, avatars, etc.; Middle barrage, SVGA gift display area; The bottom chat comment area is easy to manage
  • The right slider also inherits from a RelativeLayout to parse your custom layout for easy expansion

Call the Container to add the clear screen control and the right slider layout View

The API provides the following

Vararg views: View? // Add view fun addSlideView(view: RightSlideLayout)Copy the code

This allows us to swipe through the video playing screen to determine gestures within the Container, handle clear controls, or slide out of the right slider

The slider on the right side then dynamically load Fragment, display the list layout, basically complete the function effect

1.2 refactoring

I thought I was happy and could go online, but I still found deficiencies in the process of experiencing and comparing Tiktok below:

The first one is that the RightSlider (called RightSlider) is included in the room, so that the room can be changed up and down (called Container). The RightSlider layout will also be created when the Container is created. However, at least several new holders will be created, resulting in a waste of resources. Second, the creation of RightSlider will result in the creation of a Fragment inside it, so it will request to load the list data again, causing another waste of resources. Moreover, after the creation of RightSlider, the right list will top again, and the previous distance will be lost. As a result, users click to switch rooms from the list on the right and slide out of RightSlider to switch rooms again, only to find that they have to slide down from the beginning again, which is definitely not in line with the user experience. After observing the Douyin list, I found that every time I slid to a fixed position and clicked Item to switch rooms, I slid out the slider again and found that the list was still in the same position as before, as if it was the same slider effect as before. Then I suddenly realized that the slider was bound to Activity. That is, place the RightSlider on the same layer as the Activity layout

In fact, in the process of proposing RightSlider to the outer layer, it still took a lot of detours, because after all, the logic has been implemented before, if the layout structure is changed, it will have to rewrite the sliding conflict, event distribution of this part of the code, and the workload is unpredictable. Therefore, I want to achieve the effect of mimicking tiktok without moving the layout structure

Dynamic replacement Fragment

The first thing that comes to mind is that the list inside the RightSlider seems to be the same every time, so make sure that the Fragment inside the RightSlider is the same. The sliders inside the RightSlider are different, but the Fragment list inside the RightSlider is the same, so that the effect of the same slider is created.

However, there are still problems in the implementation process. Due to the pre-loading function of RecyclerView, in our project, two holders will be created during the process of sliding from the first room to the next room, so there is a problem with Fragment replacement, and the Fragment cannot be added after changing the room. The plan was finally abandoned after an afternoon of agonizing

Fixed List height

Then think, since the Fragment can not be replaced, then RecyclerView must not be the same, if you click to record the current position of RecyclerView sliding, next time out, the code is fixed to the current position can also forge the effect of the same slider, this part also went to find some information. I did a little demo. The main method used is

/** * getScollYDistance(): Int {/ / gain recyclerview layoutManager val layoutManager. = recyclerview layoutManager as LinearLayoutManager / / The location of the first visible View for current val position = layoutManager. FindFirstVisibleItemPosition () / / according to the position for current View val FirstVisiableChildView = layoutManager. FindViewByPosition (position) / / gets the current View highly val itemHeight = FirstVisiableChildView. Height / / sliding distancereturn position * itemHeight - firstVisiableChildView.top
    }
Copy the code

The idea of slide distance calculation is: according to the position of the current visible View * the height of each ItemView + the part of the current View that has been slid out

After calculating the height, call the RecyclerView API each time it is loaded

Recyclerview. scrollBy(0,scroll) // The height of scroll just calculatedCopy the code

There are a few other ways to slide:

Public void smoothScrollBy(int dx, Public void smoothScrollToPosition(int position) // Move to Adapter Position, Public void scrollToPosition(int x, int y) public void scrollTo(int x, int y)Copy the code

In principle, it can be achieved, but in the end, the comprehensive comparison or give up this way, because I always feel that this method is opportunistic rather than the right way, or honestly put RightSlider outside

2. The animation

Animation is also a very important aspect of this feature, as the smoothness of animation directly affects the user experience, so this aspect is also detailed for a long time. First of all, this feature is divided into three animation effects:

2.1 Entry and Exit

Including clear screen control entry, appearance:

mClearAnimator = ValueAnimator.ofFloat(0f, 1.0 f). SetDuration (300) mClearAnimator. AddUpdateListener (ValueAnimator. AnimatorUpdateListener {ValueAnimator - > val value = valueAnimator.animatedValue as Float translateClearChild((startX + value * (endX - startX)).toInt()) }) mClearAnimator.addListener(object :AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { isCleared = ! isCleared } })Copy the code

ValueAnimator is used here, where translateClearChild is responsible for moving the View as follows:

/** * private fun translateClearChild(translate: Int) {for (i in mClearViews.indices) {
            mClearViews[i].translationX = translate.toFloat()
        }
    }
Copy the code

Entrance and appearance of slider:

mSlideInAnimator = ValueAnimator.ofFloat(0f, 1.0 f). SetDuration (500). / / set deceleration blocker mSlideInAnimator interpolator = DecelerateInterpolator (3 f) mSlideInAnimator.addUpdateListener(ValueAnimator.AnimatorUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Float translateSlideView((startX + value * (endX - startX)).toInt()) }) mSlideInAnimator.addListener(object :AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { mSlideView!! .visibility = View.VISIBLE mBgColorView.isClickable =true
            }

            override fun onAnimationEnd(animation: Animator) {
                if(! isSlideShow && translateX == 0) { isSlideShow = ! isSlideShow }else if(isSlideShow && abs(translateX) == width - mSlideView!! .paddingLeft) { isSlideShow = ! isSlideShow }if(! isSlideShow) { parent.requestDisallowInterceptTouchEvent(false) mSlideView!! .visibility = View.GONE removeView(mBgColorView) addView(mBgColorView, childCount - 4) } isSliderGoning =false}})Copy the code

Here startX and endX represent the starting and ending positions of the animation when entering and exiting respectively. Since the clear screen control has no middle position state, it is directly replaced between two values from 0 to screen width; In the middle of the slider, because it moves with the gesture, record the middle translateX, marked as startX

2.2 Following gestures

Follow gesture implementation is mainly to intercept the movement gesture, according to the difference between the gesture position coordinate and Move Move position coordinate, call the method of moving SliderView

Val x = event.rawx.toint () val offsetX = x-mdownx when (event.action) {motionEvent.action_move -> {if((isSlideShow) && offsetX > 0 && mSlideInAnimator.isRunning && ! IsSliderGoning) {// In the slide case, swipe right for a bit of release, swipe right again to clear the rebound animation, following the gesture mslideinanimator.cancel () translateSlideView(offsetX)}if((isSlideShow) && offsetX > 0 && ! MSlideInAnimator. Set the) {/ / slip case, slide to the right, with hand gestures translateSlideView (offsetX)}return true}}Copy the code

2.3 Color Gradient

The color gradient of the left blank area accompanying the gesture slide can be associated with the RightSlider’s distance value during the move, setting the start color transparent and the end color gray mask. Then dynamically calculate the value of the current color within the range according to the distance, and the main code logic is as follows:

/** * private fun translateSlideView(translate: Int) {val percent = (mSlideView!! .width.toFloat() - translate) / mSlideView!! .width // Val color = (MASK_DARK_COLOR * percent).toint () SHL 24 // Dynamically set the background color gradient mBgColorView.setBackgroundColor(color) translateX = translate mSlideView!! .translationX = translate.toFloat() }Copy the code

3 Event Distribution

This part can be said to be the core of the implementation of this function, and it also took a considerable amount of time, from the beginning of the Container containing the RightSlider layout to handle the classical event distribution order, to the last reconstruction of the layout, the RightSlider is not included in the outer layer of the relationship, but parallel or overlay relationship. In the middle, the sequence of events is further understood

3.1 Transfer Sequence

Before refactoring, each Container contains a RightSlider. The two containers are used as a whole, and the sliding logic can be handled in the onInterceptTouchEvent method in the Container layer. Whether to intercept events can, and then RightSlider wanted to ban the parent layer inside the Container to intercept events, you can use the parent. RequestDisallowInterceptTouchEvent layer intercept (true) prohibit the father; Is a classical event distribution model, event distribution order in a U-shaped structure, easier to handle

After refactoring, the layout structure looks like the following figure

Each Container shares a RightSlider, so that the distribution sequence of events is not in a ViewGroup U-shaped model. This is also a bold attempt. Handle Container and RightSlider in a U-shaped structure.

Fortunately, the event was dispatched from the Activity to the RightSlider and then to the Container

Here is the layout from the Demo:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    tools:context=".MainActivity">

    <com.fxf.slide.SlideContainerLayout
        android:id="@+id/layout_slider_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:id="@+id/ll12"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="100dp"
            android:background="#00f"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv111"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="111111111"
                android:textColor="#fff" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="222222222"
                android:textColor="#fff" />
        </LinearLayout>
    </com.fxf.slide.SlideContainerLayout>
  
    <com.fxf.slide.RightSlideLayout
        android:id="@+id/layout_right_slider"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="60dp"
        android:visibility="gone">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/shape_slider_background">
            <View
                android:id="@+id/live_slide_bar"
                android:layout_width="4.5 dp"
                android:layout_height="90dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="5dp"
                android:layout_marginRight="5dp"
                android:background="@drawable/shape_slider_dark_bar" />

            <FrameLayout
                android:id="@+id/list_fragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_toRightOf="@+id/live_slide_bar" />
        </RelativeLayout>

    </com.fxf.slide.RightSlideLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Part of it is simplified to help you understand the layout hierarchy

Then paste the RightSlider core distribution code:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {// Get the coordinates, use rawX relative to the screen absolute position, otherwise with the parent layout during gesture movement, resulting in the obtained coordinates left and right shake, Val x = event.rawx.toint () val y = event.rawy.toint () // X direction displacement val offsetX = x-mdownxif(! MSlideContainerLayout. IsSlideShow) {/ / Container slippery rocks don't slide out not distribute eventsreturn false} when (event.action) {motionEvent.action_down -> {// record the press point coordinates mDownX = x mDownY = y mSlideContainerLayout.setDownXY(mDownX,mDownY) } MotionEvent.ACTION_MOVE ->if(abs(x-mdownx) < abs(y-mdowny) &&paddingLeft < x)if (isSlideHorizontal) {
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
            } else if(offsetX < 0 && mSlideContainerLayout. IsAlignLeftSide ()) {/ / slide to the left, slippery rocks on the far left already, don't distributereturn super.dispatchTouchEvent(event)
            } else if(abs(x-mdownx) > ABS (y-mdowny)){// isSlideHorizontal =true
                    returnMSlideContainerLayout. DispatchTouchEvent (event) / / event is passed to the Container handling} MotionEvent. ACTION_UP, Motionevent.action_cancel ->{// Handle while liftingif (offsetX < 0 && mSlideContainerLayout.isAlignLeftSide()){
                    return super.dispatchTouchEvent(event)
                }
                if (abs(x - mDownX) > abs(y - mDownY) || isSlideHorizontal){
                    isSlideHorizontal = false
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
                isSlideHorizontal = false}}return super.dispatchTouchEvent(event)
    }
Copy the code

3.2 Sliding Conflict

  • Since the room slides up and down, it is safe to judge that if the slider does not grow, return to distribution without letting The RightSlider and Container handle the event
if(! mSlideContainerLayout.isSlideShow){return false
   }
Copy the code
  • Then after the slider slides out, because there is a list inside, so to consume the sliding up and down event, can be handled as follows:
 MotionEvent.ACTION_MOVE -> if (abs(x - mDownX) < abs(y - mDownY) && paddingLeft < x) {
                if (isSlideHorizontal) {
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
            }
Copy the code

PaddingLeft < x because there is a blank paddingLeft area to the left of the slider, so the event is processed when the x coordinate is to the right of this area

  • During the execution of the Container animation, events are being consumed. In this case, prevent the parent layer from intercepting events

    if(mClearAnimator. Set the | | mSlideInAnimator. Set the | | isSlideShow) {/ / into the case, Ban on the slide switch studio parent. RequestDisallowInterceptTouchEvent (true)}Copy the code
  • When the Container processes events, it conflicts with the entry profile picture list in the broadcast room. To resolve this problem, determine that the mDownY is greater than the height of the entry profile picture list. Normal people slide into the room at the lower part of the screen, so it is acceptable not to process events at the upper part of the screen

MotionEvent.ACTION_MOVE -> {
                if(! mClearAnimator.isRunning && mDownY >200 && abs(x - mDownX) > abs(y - mDownY)) {
                    // The height is greater than 200dp when clearing the screen is not executed (resolves the slide conflict when entering the room) &&intercepts the event when sliding horizontally
                    if (abs(x - mDownX) > 10) {
                        return true}}}Copy the code

3.3 Sliding Optimization

There is a lot of detail in this section, including sliding left and right again in the middle of the animation, then left and then right, then sliding up and down in the middle of the animation, and so on. You can see the annotation in the onTouchEvent method in SlideContainerLayout

override fun onTouchEvent(event: MotionEvent): Boolean { mVelocityTracker!! .addMovement(event) val x = event.rawX.toInt() val offsetX = x - mDownXif (mLastOffsetList.size > 2){
            mLastOffsetList.removeFirst()
        }
        mLastOffsetList.add(offsetX)
        var slideRight = (offsetX - mLastOffsetList.first) > 0
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                if((isSlideShow) && offsetX > 0 && mSlideInAnimator.isRunning && ! IsSliderGoning) {// In the slide case, swipe right for a bit of release, swipe right again to clear the rebound animation, following the gesture mslideinanimator.cancel () translateSlideView(offsetX)}if((isSlideShow) && offsetX > 0 && ! MSlideInAnimator. Set the) {/ / slip case, slide to the right, with hand gestures translateSlideView (offsetX)}return true} MotionEvent.ACTION_UP -> { mVelocityTracker!! .computeCurrentVelocity(10)if(isSlideShow && offsetX > 0 && abs(offsetX) > width / 3 && ! isSliderGoning && mVelocityTracker!! .xvelocity >= 0) {startX = offsetX endX = width -mslideView!! .paddingLeft isSliderGoning =true
                    mSlideInAnimator.start()
                    return true
                }
                if(abs(mVelocityTracker!! .xVelocity) > 1) {if(isCleared &&offsetx < 0) {// In the clear case, when the left slider speed exceeds 10 pixels === "slides into the clear control layerShowWithAnim()}else if(! isCleared && offsetX > 0 && ! isSlideShow && ! MSlideInAnimator. Set the) {/ / outstanding screen & speed to the right > 10 && didn't slide into the slider && slider animation isn't execute = = = "CLS layerGoneWithAnim ()}else if(isSlideShow && offsetX > 0 && slideRight) {// In the case of slide && right speed > 10 === "slide out the slider mslideInanimator.cancel () isSliderGoning =truestartX = translateX endX = width - mSlideView!! .paddingLeft mSlideInAnimator.start() }else if(isSlideShow && offsetX < 0 && translateX ! StartX = 0 mslideInanimator.start ()}else if(! isSlideShow && offsetX < 0 && ! MSlideInAnimator. Set the) {/ / not into the case & right & left sliding speed > 10 & no slides into case = = = "slide the slider sliderShowWithAnim ()}else {
                        if(isSlideShow && translateX ! StartX = translateX mslideinanimator.start ()}}else {
                    if(isSlideShow && translateX ! StartX = translateX mslideinanimator.start ()}}return super.onTouchEvent(event)
            }
            MotionEvent.ACTION_CANCEL -> {
                ifStartX = translateX mslideInanimator.start ()}} (isSlideShow) {// Cancel the event when sliding in case of rebound startX = translateX mslideinanimator.start ()}}return super.onTouchEvent(event)
    }
Copy the code

conclusion

Finally, through this practice, the deep feeling is that before the function realization, we must do full research, study the details of the requirements, and think of several implementation strategies in advance, and compare which one is more reasonable. Don’t just write it, only to have to refactor it when it doesn’t meet your needs

Thank you. The logic in Contanier here mainly refers to the processing of this article on gitHub. However, there is less logic to deal with sliding conflicts in the article

Send the GitHub project address

The project address

If you have any help, please give the project a Star, thank you ~