Kotlin之下拉刷新与上拉加载控件

引言


自从RecyclerView出现后ListView便渐渐退出了舞台的中央,所有ListView能够做到的RecyclerView都会做的更加优秀。今天就来讲讲
RecyclerView比ListView逊色的地方。

用过两者的人会知道RecyclerView没有header和footer,这是因为RecyclerView本身并不需要特别指明header和footer,他有ViewType这一特性完美的弯道超车ListView。

如果需要下拉刷新,官方也有MD组合套件中的SwipeRefreshLayout。

但如果是一些酷炫的下拉刷新动画,根据用户下拉的程度动态的展示动画,单在这一点上ListView就方便很多。

ListView可以根据用户下拉的距离换算成变化比例,动态的更新header或footer的高度再加以动画效果可以产生令人舒爽的弹性效果和趣味的动画。

今天要来讲的下拉刷新与上拉加载控件就是为了给RecyclerView增加此种效果。

效果图

正文


实现方案并没有采用网上主流的通过修改Adapter,增加ViewType来达到效果,原因有

1、由于kotlin需要严格声明变量类型(ViewHolder的问题),导致通过使用代理的设计模式(这种设计模式可以让使用者不需要关心header和footer的这两种ViewType,完全交由代理去完成。极大的保证了Adapter的编码原生性)设计Adapter困难重重或是说束手束脚。

2、个人原因,本就是学习项目,借此也想通过设计此控件复习下事件的分发机制和动画相关的内容。

设计思路

LinearLayout包裹HeaderView、RecyclerView、FooterView

处理LinearLayout的dispatchTouchEvent,如果触发了相关操作则截获事件,否则放行。

知识点

1、判断RecyclerView的位置

方法一:通过RecyclerView的layoutManager获取可见的第一个ItemView在整体中的位置,如果是第一个再判读其距离父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
    }

方法二:利用RecyclerView的高度、RecyclerView滚动的距离、当前显示RecyclerView的高度,三个数据进行判断。
View本身也有判断方法canScrollVertically,原理相同。(这方面内容是从网上学习到的,但由于找不到我所看的原文作者了所以没贴,因为后来去找作者时发现了好几个人写了类似的内容,实在分不清最先看到的是谁了)

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

2、Activity、ViewGroup、View的事件分发机制,以及事件分发的几个方法,提一下而已不做详细展开
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

Code

有了以上的知识,下面的代码就好理解了

    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)
    }

为了使header规范我用了以下方法

/**
 * 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()

}

所有的headerView需要实现此抽象类,这样更换header也会比较方便。

动画

只讲简单的和我遇到的问题

最初我使用了Animation,由于多动画之间有间隔时间,于是采用了view的postDelayed()方法,导致动画乱七八糟,无法cancel。这才想到了Set,但是用AnimationSet还是AnimatorSet。

Animation和Animator
Animator是4.0之后才有的,与Animation不同的是Animator是通过改变属性从而产生动画,而Animation则是多次绘制。性能上Animator会占优,详细内容可以查看这里。

AnimationSet和AnimatorSet
AnimationSet真的就只是一个集合,内部成员可以设置是否公用AnimationSet的属性。
AnimatorSet则不同,它可以控制内部Animator的播放顺序和其他操作。

我可能讲的不是很清楚,大家可以查阅下相关资料。我这里做个Animation和Animator的总结:

Animation翻译成中文为动画,Animator翻译成中文为动画者

只可意会,不可言传。

/**
 * 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()
    }

}

结束语

不知道有没有更好的实现方案,总感觉性能还能够再优化,奈何实力不足,还请大家多多赐教

猜你喜欢

转载自blog.csdn.net/s1991721/article/details/79101785
今日推荐