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.
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