RecyclerView内嵌ViewPager无限滑动Banner的爬坑之旅

前言收到线上用户反馈,RecyclerView 实现的 Feed 流列表中的 Banner Item 在滑动过程中偶现没有进行内容切换,而是进行了外层频道切换。嵌套的UI布局如下图所示: 问题原因定位猜测原因是:最外层OuterViewPager拦截了Touch事件,没有将Touch事件传递给内层的BannerViewPager,从而导致外层频道切换。 想证实猜测的准确性,定位为什么OuterViewPager拦截了事件,只能通过阅读ViewPager的事件拦截源码进行分析,这是最快也是最靠谱的证实方案。 ViewPager事件拦截原理从onInterceptTouchEvent源码分析一下ViewPager对Touch事件的拦截机制,相关源码已经添加中文注解: @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // cancel和up事件代表触摸事件结束,需要重置触摸变量 resetTouch(); return false; } if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) { // 如果ViewPager已经响应拖拽事件,则直接拦截后续事件 return true; } if (mIsUnableToDrag) { // 如果ViewPager不能响应拖拽事件,则不拦截后续事件 return false; } } switch (action) { case MotionEvent.ACTION_MOVE: { // 多指触摸处理,值得学习阅读 final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { break; } final int pointerIndex = ev.findPointerIndex(activePointerId); final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float xDiff = Math.abs(dx); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); // 这里是关键,判断OuterViewPager是否需要将touch事件传递给内层BannerViewPager if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // 如果内层Child可以滑动,则OuterViewPager不拦截事件,将事件向下传递 mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } // OuterViewPager开始接管Touch事件处理. // X轴横向偏移量大于最小滑动距离,并且滑动角度小于45度 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { // 设置拦截拖拽标记位 mIsBeingDragged = true; // 通知父View不要拦截事件 requestParentDisallowInterceptTouchEvent(true); // 设置滑动状态为开始拖拽 setScrollState(SCROLL_STATE_DRAGGING); // 设置滑动开始的坐标 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { // 竖向滑动不拦截后续TOUCH事件 mIsUnableToDrag = true; } if (mIsBeingDragged) { // 执行滑动 if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } case MotionEvent.ACTION_DOWN: { // 多指处理的逻辑,值得学习,标准写法 mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); mIsUnableToDrag = false; mIsScrollStarted = true; mScroller.computeScrollOffset(); if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { // down事件到来,需要终止上次的滑动 mScroller.abortAnimation(); mPopulatePending = false; populate(); // 因为上次滑动没有终止,因此需要拦截后续TOUCH事件,开始新的滑动 mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } else { completeScroll(false); mIsBeingDragged = false; } break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } // 速度追踪 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); return mIsBeingDragged;}通过onInterceptTouchEvent源码分析,可以看出: ...

July 7, 2019 · 4 min · jiezi

开源库Banner如何实现无限轮播

记得最初使用ViewPager实现无限轮播大致是这样的. class BannerPagerAdapter extends PagerAdapter { List<View> list; @Override public int getCount() { return Integer.MAX_VALUE; } @Override public Object instantiateItem(ViewGroup container, final int position) { int realPosition = position%list.size(); return list.get(realPosition); } }在使用了开源库Banner后,感觉这个库处理无限轮播,写的非常好.这个库可以左右无限滑动,也可以自动左右无限轮播.首先先了解下,其大致原理.在数据的前后两端各添加一条数据.前端添加的是最后一天数据,尾端添加的是第一条数据.如图:当从C滑动到D时,在ViewPager.OnPageChangeListener#onPageScrollStateChanged()中做监听并快速切换到A;当从A滑动到E时,在ViewPager.OnPageChangeListener#onPageScrollStateChanged()中做监听并快速切换到C;这里切换使用viewPager.setCurrentItem(int,boolean);第二个参数为false,表示不使用动画,直接快速切换,造成一种无限轮播的假象.然后在ViewPager.OnPageChangeListener中做监听处理并快速切换.//当前页面索引int currentIndex = 1;//数据源的实际大小int pageCount; private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { currentIndex = position; } @Override public void onPageScrollStateChanged(int state) { //在这里做快速切换,当你滑动到首位,要快速切换到尾端 if (currentIndex==0){ viewPager.setCurrentItem(pageCount,false); } //当你滑动到尾端,要快速切换到首位 if (currentIndex==pageCount+1){ viewPager.setCurrentItem(1,false); } } }; ...

April 11, 2019 · 1 min · jiezi

TabLayout与ViewPager结合时不显示Tab标题

当我们将TabLayout与ViewPager结合时,会出现Tab标题不显示的问题. mTabLayout.setupWithViewPager(viewPager);从源码里找答案.先看下方法调用流程大致流程如图,这里只给出populateFromPagerAdapter()方法源码.void populateFromPagerAdapter() { removeAllTabs(); if (mPagerAdapter != null) { //返回ViewPager设置的PagerAdapter.getCount() final int adapterCount = mPagerAdapter.getCount(); for (int i = 0; i < adapterCount; i++) { //重新添加,并通过PagerAdapter.getPageTitle()为tab设置标题 addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); } // Make sure we reflect the currently set ViewPager item if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { selectTab(getTabAt(curItem)); } } } }这个方法的第一步 removeAllTabs();./** * Remove all tabs from the action bar and deselect the current tab. */ public void removeAllTabs() { // Remove all the views for (int i = mTabStrip.getChildCount() - 1; i >= 0; i–) { removeTabViewAt(i); } for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) { final Tab tab = i.next(); i.remove(); tab.reset(); sTabPool.release(tab); } mSelectedTab = null; }移除了TabLayout添加的tab.下一步final int adapterCount = mPagerAdapter.getCount();则是返回你设置给ViewPager的PagerAdapter的getCount()返回值.然后在for循环中 addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), 添加新的tab,并将PagerAdapter.getPageTitle()返回值设置为tab的标题.因此,当ViewPager与TabLayout结合使用时,不必给TabLayout添加tab,也就是不用调用TabLayout.addTab(). ...

March 29, 2019 · 1 min · jiezi

Android-快速实现ViewPager+Tablayout的联动效果

Android-快速实现ViewPager+Tablayout的联动效果在项目开发中很多场景都会碰到tab栏切换的效果,实现的思路也有很多种,tabhost+fragment,radionbtton+viewpager等方式都可以实现,这里就说下tablayout+viewpager的实现方式;tablayout是android5.0推出来的一个MaterialDesign风格的控件,是专门用来实现tab栏效果的;功能强大,使用方便灵活;引入依赖库:implementation ‘com.android.support:support-v4:26.1.0’ implementation ‘com.android.support:design:26.1.0’布局文件:<?xml version=“1.0” encoding=“utf-8”?><LinearLayoutxmlns:android=“http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/bg"android:orientation="vertical"><android.support.design.widget.TabLayout android:id=”@+id/tablayout" android:layout_width=“match_parent” android:layout_height=“50dp” android:layout_gravity=“top|center” app:tabMode=“fixed” app:tabGravity=“fill” app:tabTextColor="@color/black999" app:tabSelectedTextColor="@color/orange" app:tabIndicatorColor="@color/orange" app:tabIndicatorHeight=“4dp”/><android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width=“match_parent” android:layout_height=“match_parent” android:background="@android:color/white"></android.support.v4.view.ViewPager></LinearLayout>tablayout提供了很多的属性可以设置:app:tabIndicatorColor 指示器颜色app:tabTextColor tab栏字体颜色app:tabSelectedTextColor tab栏字体被选颜色app:tabIndicatorHeight 指示器高度app:tabBackground tab背景颜色app:tabMaxWidth tab栏最大宽度app:tabTextAppearance tab栏字体样式app:tabMinWidth tab栏最小宽度这些属性可以下xml中设置,也可以使用代码进行设置;需要注意这两个属性:app:tabMode="";有scrollable和fixed两个属性值scrollable:可滑动;fixed:不能滑动,平分tabLayout宽度;app:tabGravity="";有center和fill两个属性值fill:tabs平均填充整个宽度;center:tab居中显示;页面实现方式:public static final String[] tabTitles = new String[]{“全部”, “代付款”, “代发货”, “配送中”};private TabLayout mTabLayout;private ViewPager mViewPager;private List<MyOrderFragment> mFragments = new ArrayList<MyOrderFragment>();mTabLayout = (TabLayout) findViewById(R.id.tablayout); mViewPager = (ViewPager) findViewById(R.id.viewpager); tv_title.setText(“所有订单”); for (int i = 0; i < tabTitles.length; i++) { mFragments.add(MyOrderFragment.createFragment(tabTitles[i],activity)); } //为ViewPager设置FragmentPagerAdapter mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); } /** * 为TabLayout中每一个tab设置标题 */ @Override public CharSequence getPageTitle(int position) { return tabTitles[position]; } }); //TabLaout和ViewPager进行关联 mTabLayout.setupWithViewPager(mViewPager);其中MyOrderFragment就是我们装载的页面 ,这样就可以快速的实现tablayout和viewpager相互关联。做个简单的记录~ ...

March 13, 2019 · 1 min · jiezi