引言
自从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()
}
}
结束语
不知道有没有更好的实现方案,总感觉性能还能够再优化,奈何实力不足,还请大家多多赐教