吸顶终极方案github地址: Adsorbent
1. Single RecyclerView:简单模式
【利用RecyclerView.OnScrollListener监听滑动位置,吸顶View被 ViewHolder和Activity复用】
2. Double RecyclerView:RecyclerView嵌套RecyclerView
【事件分发,吸顶View是个单独ViewHolder,无须做其他处理】
3. Viewpager RecyclerView:RecyclerView嵌套ViewPager(其中包含的页面内容是RecyclerView)
【事件分发,吸顶View是个单独ViewHolder,无须做其他处理】
1.onScrolled机制
abstract class SingleAdsorbentListener : RecyclerView.OnScrollListener(){
/** 获取被吸顶ViewGroup*/
abstract fun getUiViewGroup():ViewGroup
/** 获取吸顶View*/
abstract fun getPinView(): View
/** 获取吸顶View在RecyclerView中的位置*/
abstract fun getPinViewPosition():Int
/** 吸顶的时候 停止滚动并定位在吸顶位置*/
fun stopWhenAdsorbent():Boolean = true
fun getPinViewLayoutParam():ViewGroup.LayoutParams =
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val manager = recyclerView.layoutManager
if(manager is LinearLayoutManager) {
val first = manager.findFirstVisibleItemPosition()
val last = manager.findLastVisibleItemPosition()
//val down = recyclerView.canScrollVertically(1)
//val up = recyclerView.canScrollVertically(-1)
//Log.e("xx", "first=$first, vertically down=$down, up=$up, dy=$dy")
val position = getPinViewPosition()
if (first >= position && dy > 0) {
addPin2Ui(recyclerView,position)
} else if (position in (first + 1)..last) {
addPin2ViewHolder(recyclerView,position-first)
}
}
}
/** 吸顶*/
private fun addPin2Ui(recyclerView: RecyclerView,position :Int){
val uiView = getUiViewGroup()
val pinView = getPinView()
if(uiView.indexOfChild(pinView) < 0){
/** 吸顶的时候 停靠在吸顶viewholder位置,符合吸顶习惯*/
if(stopWhenAdsorbent()) {
recyclerView.stopScroll()
recyclerView.scrollToPosition(position)
}
val p = pinView.parent
if(p is ViewGroup){
p.removeView(pinView)
}
uiView.addView(pinView, getPinViewLayoutParam())
}
}
/** 恢复或添加 到ViewHolder*/
private fun addPin2ViewHolder(recyclerView: RecyclerView,childPosition:Int){
val uiView = getUiViewGroup()
val pinView = getPinView()
val holderView = recyclerView.getChildAt(childPosition)
if(uiView.indexOfChild(pinView) > 0){
uiView.removeView(pinView)
}
if(holderView is ViewGroup && holderView.indexOfChild(pinView) < 0) {
holderView.addView(pinView, getPinViewLayoutParam())
}
}
}
2.事件分发机制,支持fling
class ParentRecyclerView :RecyclerView, OnInterceptListener {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
private var isChildTop = true
private var startdy = 0f
private var startdx = 0f
private var velocity : VelocityTracker? = null
private var velocityY = 0
private var isMoveY = false
private var isSelfTouch = true
/** true 开启滑动冲突处理*/
var enableConflict = true
/** 开启快速滚动parent带动child联动效果(默认false)*/
var enableParentChain = false
/** 开启快速滚动child带动parent联动效果*/
var enableChildChain = true
init {
addOnScrollListener(object :OnScrollListener(){
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
/** 滚动停止且到了底部,快速滑动事件下发给child view*/
if(enableParentChain && SCROLL_STATE_IDLE == newState && !canScrollVertically(1) && velocityY != 0){
val manager = layoutManager
if(manager is LinearLayoutManager){
val first = manager.findFirstVisibleItemPosition()
val total = manager.itemCount - 1
manager.getChildAt(total - first)?.let {
searchViewGroup(recyclerView,it)
}
}
}
}
/** 采用递归算法遍历*/
fun searchViewGroup(parent :ViewGroup,child : View):Boolean{
if(flingChild(parent,child)){
return true
}
if(child is ViewGroup) {
for (i in 0 until child.childCount) {
val subChild = child.getChildAt(i)
if(subChild is ViewGroup && searchViewGroup(parent,subChild)){
return true
}
}
}
return false
}
/** 确保child的屏幕坐标在parent内,主要适配ViewPager*/
fun flingChild(parent :ViewGroup,child : View):Boolean{
val locChild = IntArray(2)
val locParent = IntArray(2)
parent.getLocationOnScreen(locParent)
child.getLocationOnScreen(locChild)
val childX = locChild[0]
val parentX = locParent[0]
if (childX >= parentX && childX< (parentX+parent.measuredWidth) && child is RecyclerView) {
child.fling(0, velocityY/2)
return true
}
return false
}
})
}
override fun onTopChild(isTop: Boolean) {
isChildTop = isTop
}
override fun onScrollChain() {
/** child带动parent联动效果,快速滑动事件下发给self view*/
if(enableChildChain && isChildTop ) {
fling(0,velocityY/2)
}
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if(enableConflict){
dispatchConflictTouchEvent(ev)
}
return super.dispatchTouchEvent(ev)
}
private fun dispatchConflictTouchEvent(ev: MotionEvent){
//Log.e("xx","dispatchConfict action=${ev.action},dy=${ev.y}")
velocity?:{
velocity = VelocityTracker.obtain()
}.invoke()
velocity?.addMovement(ev)
when(ev.action){
MotionEvent.ACTION_DOWN ->{
startdx = ev.x
startdy = ev.y
velocityY = 0
isMoveY = false
}
MotionEvent.ACTION_MOVE ->{
conflictTouchEvent(ev)
}
MotionEvent.ACTION_UP ->{
isSelfTouch = true
velocity?.let {
it.computeCurrentVelocity(1000, maxFlingVelocity.toFloat())
velocityY = -it.yVelocity.toInt()
velocity?.recycle()
}
velocity = null
}
}
}
private fun conflictTouchEvent(ev: MotionEvent){
/** 纵向滑动处理,横向滑动过滤*/
if(isMoveY || Math.abs(ev.y-startdy) > Math.abs(ev.x-startdx)){
isMoveY = true
/** true 在底部*/
val isBottom = !canScrollVertically(1)
/** true向上滑动*/
val directUp = (ev.y - startdy) < 0
//Log.e("xx","isBootom=$isBottom,directUp=$directUp,ischildTop=$isChildTop,y=${ev.y}")
if(isBottom){
if(isChildTop && !directUp){
dispatchSelfTouch(ev)
}else{
dispatchChildTouch(ev)
}
}else{
dispatchSelfTouch(ev)
}
}
}
/** 给selft view分发*/
private fun dispatchSelfTouch(ev: MotionEvent){
if(!isSelfTouch){
isSelfTouch = true
dispatchTouchEvent(obtainCancelEvent(ev.x, ev.y))
dispatchTouchEvent(obtainDownEvent(ev.x, ev.y))
}
}
/** 给child view分发*/
private fun dispatchChildTouch(ev: MotionEvent){
if (isSelfTouch){
isSelfTouch = false
dispatchTouchEvent(obtainCancelEvent(ev.x,ev.y))
dispatchTouchEvent(obtainDownEvent(ev.x,ev.y))
requestDisallowInterceptTouchEvent(true)
}
}
private fun obtainDownEvent(dx :Float, dy :Float) :MotionEvent{
return obtainActionEvent(MotionEvent.ACTION_DOWN,dx,dy)
}
private fun obtainCancelEvent(dx :Float, dy :Float) :MotionEvent{
return obtainActionEvent(MotionEvent.ACTION_CANCEL,dx,dy)
}
private fun obtainActionEvent(action :Int,dx :Float, dy :Float) :MotionEvent{
val now = SystemClock.uptimeMillis()
return MotionEvent.obtain(now,now,action,dx,dy,0)
}
}