Pull down to refresh and pull up to load controls in Kotlin

introduction


Since the appearance of RecyclerView, ListView has gradually withdrawn from the center of the stage, and all RecyclerView that ListView can do will do better. Today I will talk about
the inferiority of RecyclerView to ListView.

Those who have used both will know that RecyclerView does not have header and footer, because RecyclerView itself does not need to specify header and footer. It has the feature of ViewType, which is perfect for overtaking ListView in curves.

If you need to pull down to refresh, the official also has SwipeRefreshLayout in the MD combination kit.

But if it is some cool pull-to-refresh animation, the animation is dynamically displayed according to the user's pull-down degree, and ListView is much more convenient at this point.

ListView can be converted into a changing ratio according to the distance that the user pulls down, dynamically updating the height of the header or footer and adding animation effects to produce a refreshing elastic effect and interesting animation.

The pull-down refresh and pull-up loading controls that we are going to talk about today are to add this effect to RecyclerView.

renderings

text


The implementation scheme does not use the mainstream online method to achieve the effect by modifying the Adapter and increasing the ViewType. The reasons are

1. Since kotlin needs to strictly declare the variable type (the problem of ViewHolder), the design mode of using proxy (this design mode allows users to not need to care about the two ViewTypes of header and footer, and it is completely completed by the proxy. Greatly guarantees the coding originality of the Adapter) Designing the Adapter is difficult or restrictive.

2. For personal reasons, this is a learning project, and I also want to review the event distribution mechanism and animation-related content by designing this control.

Design ideas

LinearLayout包裹HeaderView、RecyclerView、FooterView

Handle the dispatchTouchEvent of LinearLayout, intercept the event if the related operation is triggered, otherwise release.

Knowledge point

1. Determine the position of RecyclerView

Method 1: Obtain the position of the first visible ItemView in the whole through the layoutManager of RecyclerView, and if it is the first one, then interpret the distance from the top of the parent view.

    private fun isScrollToTop(): Boolean {
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val position = layoutManager.findFirstVisibleItemPosition()
        if (position != 0) {
            return false
        }
        val firstVisiableChildView = layoutManager.findViewByPosition(position)
        return firstVisiableChildView.top == 0
    }

Method 2: Use the height of the RecyclerView, the scrolling distance of the RecyclerView, and the height of the currently displayed RecyclerView, three data to judge.
View itself also has a judgment method canScrollVertically, the principle is the same. (This content was learned from the Internet, but I didn't post it because I couldn't find the author of the original text I read, because when I went to the author later, I found that several people had written similar content, and I couldn't tell the difference first. who has arrived)

fun isScrollToBottom(): Boolean {
        return recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset() >= recyclerView.computeVerticalScrollRange()
    }

2. The event distribution mechanism of Activity, ViewGroup, and View, as well as several methods of event distribution, will be mentioned and will not be expanded in detail.
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

Code

With the above knowledge, the following code is easy to understand

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (isScrollToTop()) {

            Logger.i("RefreshRecyclerView", "ScrollToTop")
            when (ev.action) {

                MotionEvent.ACTION_DOWN -> {
                    startY = ev.y
                }

                MotionEvent.ACTION_MOVE -> {
                    if (currentState != STATE_REFRESHING) {
                        if (ev.y - startY > 0) {
                            changeState(STATE_PULLING)
                            headerView.setVisibleHeight(ev.y - startY)
                            return true
                        }
                        changeState(STATE_NORMAL)
                    }
                }

                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                    if (currentState == STATE_PULLING) {
                        var toState = STATE_NORMAL
                        if (headerView.isEnoughtToRefresh()) {
                            toState = STATE_REFRESHING
                        }
                        changeState(toState)
                    }
                }

            }
        }

        if (isScrollToBottom()) {
            //此时底部没有动画,日后扩展
            Logger.i("RefreshRecyclerView", "ScrollToBottom")
            changeState(STATE_LOADING)

        }

        return super.dispatchTouchEvent(ev)
    }

To make the header specification I used the following method

/**
 * Created by mr.lin on 2018/1/16.
 * RefreshRecyclerView统一header的父类
 */
abstract class HeaderView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : FrameLayout(context, attrs, defStyleAttr) {

    abstract fun setVisibleHeight(height: Float)

    abstract fun isEnoughtToRefresh(): Boolean

    abstract fun startRefresh()

    abstract fun endRefresh()

    abstract fun cancelRefresh()

}

All headerView needs to implement this abstract class, so it will be more convenient to replace the header.

animation

Just talk about the simple and the problems I encountered

At first, I used Animation. Because of the interval between multiple animations, I used the postDelayed() method of the view, which caused the animation to be messy and unable to cancel. That's when I thought of Set, but using AnimationSet or AnimatorSet.

Animation and Animator
Animator is only available after 4.0. Unlike Animation, Animator generates animation by changing properties, while Animation is drawn multiple times. Animator will take the lead in performance. Details can be found here.

AnimationSet and AnimatorSet
AnimationSet is really just a collection, and internal members can set whether to share the properties of AnimationSet.
AnimatorSet is different, it can control the playback order and other operations of the internal Animator.

I may not be very clear, you can check the relevant information. Here is a summary of Animation and Animator:

Animation translated into Chinese as animation, Animator translated into Chinese as animator

It can only be understood, not spoken.

/**
 * Created by mr.lin on 2018/1/15.
 * 默认HeaderView
 */
class DefaultHeaderView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : HeaderView(context, attrs, defStyleAttr) {

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context) : this(context, null)

    private var headerHeight = CommonUtils.dpTopx(50f)

    private lateinit var rotateAnimator1: ObjectAnimator
    private lateinit var rotateAnimator2: ObjectAnimator
    private lateinit var rotateAnimator3: ObjectAnimator
    private lateinit var rotateAnimator4: ObjectAnimator
    private lateinit var animatorSet: AnimatorSet

    private var valueAnimator: ValueAnimator = ValueAnimator()

    init {
        LayoutInflater.from(context).inflate(R.layout.view_defaultheaderview, this)

        var params = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0)
        params.gravity = Gravity.CENTER
        layoutParams = params
    }

    override fun setVisibleHeight(height: Float) {
        var params = layoutParams
        params.height = height.toInt()
        layoutParams = params
    }

    override fun isEnoughtToRefresh(): Boolean {
        var currentHeight = layoutParams.height
        return currentHeight >= headerHeight / 2
    }

    override fun startRefresh() {
        changeHeight(layoutParams.height, headerHeight, {}, { startRotate() })
    }

    override fun endRefresh() {
        changeHeight(layoutParams.height, 0, { stopRotate() }, {})
    }

    override fun cancelRefresh() {
        changeHeight(layoutParams.height, 0, {}, {})
    }

    private fun changeHeight(currentHeight: Int, target: Int, start: () -> Unit, end: () -> Unit) {
        if (valueAnimator.isRunning) {
            valueAnimator.cancel()
        }
        valueAnimator = ValueAnimator.ofInt(currentHeight, target)
        valueAnimator.duration = 500
        valueAnimator.addUpdateListener {
            var params = layoutParams
            params.height = valueAnimator.animatedValue as Int
            layoutParams = params
        }
        valueAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
                start()
            }

            override fun onAnimationEnd(animation: Animator?) {
                end()
            }
        })
        valueAnimator.start()
    }

    private fun startRotate() {
        initRotate()
        animatorSet.start()
    }

    private fun initRotate() {
        rotateAnimator1 = ObjectAnimator.ofFloat(iv1, "rotation", 0f, 360f).setDuration(1000)
        rotateAnimator1.repeatCount = INFINITE
        rotateAnimator2 = ObjectAnimator.ofFloat(iv2, "rotation", 0f, 360f).setDuration(1000)
        rotateAnimator2.repeatCount = INFINITE
        rotateAnimator3 = ObjectAnimator.ofFloat(iv3, "rotation", 0f, 360f).setDuration(1000)
        rotateAnimator3.repeatCount = INFINITE
        rotateAnimator4 = ObjectAnimator.ofFloat(iv4, "rotation", 0f, 360f).setDuration(1000)
        rotateAnimator4.repeatCount = INFINITE
        animatorSet = AnimatorSet()
        animatorSet.play(rotateAnimator1)
        animatorSet.play(rotateAnimator4).after(200)
        animatorSet.play(rotateAnimator3).after(400)
        animatorSet.play(rotateAnimator2).after(600)
    }

    private fun stopRotate() {
        animatorSet.end()
        animatorSet.cancel()
    }

}

concluding remarks

I don't know if there is a better implementation plan, but I always feel that the performance can be optimized, but the strength is not enough, please give me more advice

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325995273&siteId=291194637