滑动一个rv的时候另一个也一起动的效果
以前看过一个帖子,不记得了,一直想学着写,太久了。找了半天没找到那帖子,所以也就没好的效果图了,就贴下自己实现的图,有点丑,我记得原图好像是个银行的。
最开始长这样,恩,为了省事我就用个颜色块代替具体的布局拉。
滑动的中间是这样
最后一个长这样
这个是用2个recyclerview写的,下边那个item宽度就是屏幕宽,上边那个宽度比屏幕小点,所以中间加了间隔,用itemdecoration写的
下图是代码中变量代表的意思,简单画一下,好理解,实际中你只要按需求修改edgeShow的大小即可。
先上代码
2个item也就是弄个线性布局包裹一个textview。
item_linkage_top.xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_num"
android:gravity="center"
android:textColor="#fff"
android:layout_gravity="center"
android:textSize="25sp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
item_linkage_bottom.xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_bottom"
android:gravity="center"
android:textColor="#fff"
android:layout_gravity="center"
android:textSize="25sp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
完事activity里就2个rv
<android.support.v7.widget.RecyclerView
android:id="@+id/rv1"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv2"
android:layout_marginTop="10dp"
android:layout_weight="1.5"
android:layout_width="match_parent"
android:layout_height="0dp"/>
核心kotlin代码
主要原理就是监听rv的滚动距离,根据效果图,其实可以知道两者滚动的距离是不一样的,然后我们可以根据比例,用其中一个算出另外一个应该滚动的距离。
下边的代码适合只有3页数据的情况,因为用到了computeHorizontalScrollOffset方法,而这个方法对于decoration的offset不一样的情况,算出来的偏移量是有误差的,3页的话,第一个和最后一个量变的偏移量一样大,所以刚好没太大影响 .
文末有修改后的代码,就是把computeHorizontalScrollOffset方法删了,弄了2个变量存储offset
var testDatas = arrayListOf<Int>()
var smallWidth = 1//上边那个的item宽
var screenWidth=1;//屏幕宽,也就是rv2的item宽
var edgeShow=20 //凸出来的那部分距离
var smallScroll=0//rv1每移动一个item滚动的距离
var whoScroll=0;//正在触摸哪个rv,1和2分别表示rv1和rv2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_linkage_recyclerviews)
testDatas.add(Color.RED)
testDatas.add(Color.BLUE)
testDatas.add(Color.GREEN)
testDatas.add(Color.YELLOW)
testDatas.add(Color.LTGRAY)
screenWidth=windowManager.defaultDisplay.width
smallWidth = screenWidth - edgeShow*4
smallScroll=screenWidth-edgeShow*3
method1()
}
private fun method1() {
rv1.apply {
layoutManager = LinearLayoutManager(this@ActivityLinkageRecyeclerViews, LinearLayoutManager.HORIZONTAL, false)
addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
var position = parent.getChildAdapterPosition(view)
var count = parent.adapter.itemCount
outRect.left = edgeShow/2
outRect.right = edgeShow/2
if (position == 0) {
outRect.left = edgeShow*2
} else if (position == count - 1) {
outRect.right = edgeShow*2
}
}
})
adapter = object : BaseRvAdapter<Int>(testDatas) {
override fun getLayoutID(viewType: Int): Int {
return R.layout.item_linkage_top
}
override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
holder.setText(R.id.tv_num, "$position")
var params = holder.itemView.layoutParams
params.width = smallWidth
holder.itemView.setBackgroundColor(testDatas[position])
}
}
}
rv2.apply {
layoutManager =LinearLayoutManager(this@ActivityLinkageRecyeclerViews, LinearLayoutManager.HORIZONTAL, false)
adapter = object : BaseRvAdapter<Int>(testDatas) {
override fun getLayoutID(viewType: Int): Int {
return R.layout.item_linkage_bottom
}
override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
holder.setText(R.id.tv_bottom, "$position")
holder.itemView.setBackgroundColor(testDatas[ position])
}
}
}
//2个helper 让rv的item自动居中显示
PagerSnapHelper().apply { attachToRecyclerView(rv1)}
PagerSnapHelper().apply { attachToRecyclerView(rv2)}
rv1.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if(whoScroll==1){//等于1表示当前手指触摸的是rv1,用这里的滚动来操作rv2
val rv2preScroll= screenWidth*rv1.computeHorizontalScrollOffset()/smallScroll//根据rv1滚动的距离来计算rv2应该滚动的距离,2者的比列就是
rv2.scrollBy(rv2preScroll-rv2.computeHorizontalScrollOffset(),0)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState==RecyclerView.SCROLL_STATE_IDLE){
whoScroll=0
}else if(newState==RecyclerView.SCROLL_STATE_DRAGGING||newState==RecyclerView.SCROLL_STATE_SETTLING){
whoScroll=1
}
}
})
rv2.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if(whoScroll==2){//等于2表示当前触摸的是rv2,用这里的滚动来处理rv1的滚动
val rv1preScroll=smallScroll *rv2.computeHorizontalScrollOffset()/screenWidth
rv1.scrollBy(rv1preScroll-rv1.computeHorizontalScrollOffset(),0)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState==RecyclerView.SCROLL_STATE_IDLE){
whoScroll=0
}else if(newState==RecyclerView.SCROLL_STATE_DRAGGING||newState==RecyclerView.SCROLL_STATE_SETTLING){
whoScroll=2
}
}
})
}
//这个方法是必要的,因为如果你两根手指分别按在2个rv上就不好了。。这里显示只能一个手指触摸。
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if((ev.action and MotionEvent.ACTION_MASK)==MotionEvent.ACTION_POINTER_DOWN){
return true
}
return super.dispatchTouchEvent(ev)
}
写的时候发现的问题
我以为SCROLL_STATE_DRAGGING 手指触摸滑动,完事松开手指,还想着接下来就是SCROLL_STATE_SETTLING的状态了,结果不是。
SCROLL_STATE_DRAGGING状态,松开手指,会先SCROLL_STATE_IDLE,完事接着SCROLL_STATE_SETTLING,最后又是SCROLL_STATE_IDLE的状态。
2个方法学习下
需要说明下,这2个方法在item的decoration的offset,margin都一样的情况下才是准确的,否则是不准确的,具体原因后边有分析
rv1.computeHorizontalScrollOffset()
这个就是当前rv的偏移量。最开始是0,比如移动一个item的距离100,那么偏移量就是100,继续移动2个item,那偏移量就是200.
rv2.computeHorizontalScrollRange()
这个就是rv能滚动的总距离,比如一个item宽度是100,有5个item,那么这个就是500.【不算间距的情况】
最近发现的问题
上边写demo测试都用的3个item测试了,然后没事弄了5个,发现从第二个item开始往第三个滑动的时候就出问题了。
被动滑动的那个rv会瞬间漂移一段距离,打印了下日志如下
//rv1的监听
if(whoScroll==1){//等于1表示当前手指触摸的是rv1,用这里的滚动来操作rv2
val rv2preScroll= screenWidth*rv1.computeHorizontalScrollOffset()/smallScroll//根据rv1滚动的距离来计算rv2应该滚动的距离,2者的比列就是
rv2.scrollBy(rv2preScroll-rv2.computeHorizontalScrollOffset(),0)
}
println("rv1 ====================${dx}")
//rv2的监听
if(whoScroll==2){//等于2表示当前触摸的是rv2,用这里的滚动来处理rv1的滚动
val rv1preScroll=smallScroll *rv2.computeHorizontalScrollOffset()/screenWidth
println("rv2 scroll====================${dx}====${rv1preScroll}/${rv1.computeHorizontalScrollOffset()}" +
"====first=${(rv1.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()}")
rv1.scrollBy(rv1preScroll-rv1.computeHorizontalScrollOffset(),0)
}
I/System.out: rv2 scroll====================1====993/992====first=0
I/System.out: rv1 ====================1
I/System.out: rv2 scroll====================1====994/993====first=0
I/System.out: rv1 ====================1
I/System.out: rv2 scroll====================1====995/964====first=1
I/System.out: rv1 ====================31
I/System.out: rv2 scroll====================1====996/995====first=1
I/System.out: rv1 ====================1
看日志发现,当rv1的第一个可见的view的position改变的一瞬间,那个获取到的computeHorizontalScrollOffset发生了极大的变化,然后瞬间又变回来了。
题外话:查资料的时候发现还有个rv1.computeHorizontalScrollExtent()方法,感觉返回的就是控件的宽度
看到一篇帖子https://blog.csdn.net/u010291880/article/details/50724663
滑动测试,能感觉到就是第一个item消失的瞬间,第二个item立马冲上去了,就是中间的itemdecoration瞬间没了。
computeHorizontalScrollOffset
看来得仔细看下这个方法了
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled, boolean reverseLayout) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|| endChild == null) {
return 0;
}
final int minPosition = Math.min(lm.getPosition(startChild),
lm.getPosition(endChild));
final int maxPosition = Math.max(lm.getPosition(startChild),
lm.getPosition(endChild));
final int itemsBefore = reverseLayout
? Math.max(0, state.getItemCount() - maxPosition - 1)
: Math.max(0, minPosition);
if (!smoothScrollbarEnabled) {
return itemsBefore;
}
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild));
final int itemRange = Math.abs(lm.getPosition(startChild)
- lm.getPosition(endChild)) + 1;
final float avgSizePerRow = (float) laidOutArea / itemRange;
//上边这些和computeHorizontalScrollRange里的差不多,可以先到下边看下解释再回来,
//avgSizePerRow 可以看到这里也是算了一下没个item的平均值,然后乘以itemBefore数
//而我们这里的offset是不一样的,所以这里计算出来的就有问题了
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
- orientation.getDecoratedStart(startChild)));
}
看下最开始的代码,我们的rv1里的offset ,position=0的时候和最后一个位置的offset是不一样的
position为0的时候offset的left是edgeshow*2
computeScrollOffset刚开始startchild=0,endchild=1,laidOutArea 是比较大的,因为第一个的offset的left很大,完事当startChild=1,endchild=2的时候,laidOutArea 就是正常的,也就是变小了,所以上边的日志里
在findFirstVisibleItemPosition由0变到1的时候,也就是上边的情况,laidOutArea 变小了,导致计算出来的
computeScrollOffset的值就瞬间变小了。
computeHorizontalScrollRange
也简单看了下代码,会发现,如果你item的offset设置的不一样,这玩意计算出来感觉是有问题的。
//
private int computeScrollRange(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
ensureLayoutState();
return ScrollbarHelper.computeScrollRange(state, mOrientationHelper,
findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
this, mSmoothScrollbarEnabled);
}
// smooth scrollbar enabled. try to estimate better.
final int laidOutArea = orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild);
final int laidOutRange = Math.abs(lm.getPosition(startChild)
- lm.getPosition(endChild))
+ 1;
// estimate a size for full list.
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
如果可见的有2个item,比如startChild=0,endChild=1,下边先算出endChild的右边位置,减去startChild的左边位置,
这算出来的是2个item的距离,然后除以laidOutRange =2的
如果可见的item只有1个,那么laidOutArea 就是它的右边界减去左边界,完事除以laidOutRange =1
最后算的总range,就是乘以itemcount,这里就应该可以看出来,如果我们每个item的offset不一样,那么
getDecoratedStart(View view) Returns the start of the view including its decoration and margin.
getDecoratedEnd(View view) Returns the end of the view including its decoration and margin.
看下这2个方法,可以看到,计算位置的时候还包括了decoration以及margin的。所以,如果item的decoration的offset不一样,或者margin不一样,其实最后算出来的range并不准确,也只是个大概值。
修改后的代码
自己手动存下offset吧,最开始就这样写的,后来看到computeHorizontalScrollOffset方法,还以为发现新大陆了,觉得比自己手动保存offset高大上,而且那时候测试也是3个item,所以没发现问题。
现在又改回去了。。
import android.graphics.Color
import android.graphics.Rect
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.PagerSnapHelper
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent
import android.view.View
import com.charliesong.demo0327.R
import com.charliesong.demo0327.base.BaseActivity
import com.charliesong.demo0327.base.BaseRvAdapter
import com.charliesong.demo0327.base.BaseRvHolder
import kotlinx.android.synthetic.main.activity_linkage_recyclerviews.*
/**
* Created by charlie.song on 2018/5/21.
*/
class ActivityLinkageRecyeclerViews : BaseActivity() {
var testDatas = arrayListOf<Int>()
var smallWidth = 1//上边那个的item宽
var screenWidth=1;//屏幕宽,也就是rv2的item宽
var edgeShow=30 //凸出来的那部分距离
var smallScroll=0//rv1每移动一个item滚动的距离
var whoScroll=0;//正在触摸哪个rv,1和2分别表示rv1和rv2
var offset1=0;//rv1的offset
var offset2=0;//rv2的offset
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_linkage_recyclerviews)
testDatas.add(Color.RED)
testDatas.add(Color.BLUE)
testDatas.add(Color.GREEN)
testDatas.add(Color.YELLOW)
testDatas.add(Color.LTGRAY)
screenWidth=windowManager.defaultDisplay.width
smallWidth = screenWidth - edgeShow*4
smallScroll=screenWidth-edgeShow*3
method1()
}
private fun method1() {
rv1.apply {
layoutManager = LinearLayoutManager(this@ActivityLinkageRecyeclerViews, LinearLayoutManager.HORIZONTAL, false)
addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
var position = parent.getChildAdapterPosition(view)
var count = parent.adapter.itemCount
outRect.left = edgeShow/2
outRect.right = edgeShow/2
if (position == 0) {
outRect.left = edgeShow*2
} else if (position == count - 1) {
outRect.right = edgeShow*2
}
}
})
adapter = object : BaseRvAdapter<Int>(testDatas) {
override fun getLayoutID(viewType: Int): Int {
return R.layout.item_linkage_top
}
override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
holder.setText(R.id.tv_num, "$position")
var params = holder.itemView.layoutParams
params.width = smallWidth
holder.itemView.setBackgroundColor(testDatas[position])
}
}
}
rv2.apply {
layoutManager =LinearLayoutManager(this@ActivityLinkageRecyeclerViews, LinearLayoutManager.HORIZONTAL, false)
adapter = object : BaseRvAdapter<Int>(testDatas) {
override fun getLayoutID(viewType: Int): Int {
return R.layout.item_linkage_bottom
}
override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
holder.setText(R.id.tv_bottom, "$position")
holder.itemView.setBackgroundColor(testDatas[ position])
}
}
}
//2个helper 让rv的item自动居中显示
PagerSnapHelper().apply { attachToRecyclerView(rv1)}
PagerSnapHelper().apply { attachToRecyclerView(rv2)}
rv1.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
offset1+=dx;
if(whoScroll==1){//等于1表示当前手指触摸的是rv1,用这里的滚动来操作rv2
val rv2preScroll= screenWidth*offset1/(smallScroll)//根据rv1滚动的距离来计算rv2应该滚动的距离,2者的比列就是
rv2.scrollBy(rv2preScroll-offset2,0)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState==RecyclerView.SCROLL_STATE_IDLE){
whoScroll=0
}else if(newState==RecyclerView.SCROLL_STATE_DRAGGING||newState==RecyclerView.SCROLL_STATE_SETTLING){
whoScroll=1
}
}
})
rv2.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
offset2+=dx;
if(whoScroll==2){//等于2表示当前触摸的是rv2,用这里的滚动来处理rv1的滚动
val rv1preScroll=(smallScroll) *offset2/screenWidth
rv1.scrollBy(rv1preScroll-offset1,0)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState==RecyclerView.SCROLL_STATE_IDLE){
whoScroll=0
}else if(newState==RecyclerView.SCROLL_STATE_DRAGGING||newState==RecyclerView.SCROLL_STATE_SETTLING){
whoScroll=2
}
}
})
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if((ev.action and MotionEvent.ACTION_MASK)==MotionEvent.ACTION_POINTER_DOWN){
return true
}
return super.dispatchTouchEvent(ev)
}
}