美文网首页Android
【Android】ViewPager2简单了解

【Android】ViewPager2简单了解

作者: 欢子3824 | 来源:发表于2020-01-18 20:54 被阅读0次

    前言

    忙碌的2019过去了,2020继续努力!

    实现原理

    首先来看下其初始化

     private void initialize(Context context, AttributeSet attrs) {
          ...省略...
            mRecyclerView = new RecyclerViewImpl(context);
            mRecyclerView.setId(ViewCompat.generateViewId());
            mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
    
            mLayoutManager = new LinearLayoutManagerImpl(context);
            mRecyclerView.setLayoutManager(mLayoutManager);
            mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
            setOrientation(context, attrs);
    
            mRecyclerView.setLayoutParams(
                    new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
    
            // Create ScrollEventAdapter before attaching PagerSnapHelper to RecyclerView, because the
            // attach process calls PagerSnapHelperImpl.findSnapView, which uses the mScrollEventAdapter
            mScrollEventAdapter = new ScrollEventAdapter(this);
            // Create FakeDrag before attaching PagerSnapHelper, same reason as above
            mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
            mPagerSnapHelper = new PagerSnapHelperImpl();
            mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
            ...省略...
        }
    

    看到RecyclerViewLinearLayoutManagerPagerSnapHelper是不是有点熟悉?详情可点击【Android 进阶】仿抖音系列之翻页上下滑切换视频(四)
    其使用方式前文中已有详情描述,这里不再赘述。

    基本使用

    添加依赖

    implementation "androidx.viewpager2:viewpager2:1.0.0"

    布局中引入

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vpBase"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    

    Adapter

    由于ViewPager2 基于RecyclerView实现,所以Adapter应继承于RecyclerView.Adapter

    public class BaseAdapter extends RecyclerView.Adapter<BaseAdapter.MyViewHolder> {
    
    
        @NonNull
        @Override
        public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fm_demo, parent, false);
            return new MyViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
            int color = Color.parseColor(randomHexStr(6));
            holder.tvDemo.setBackgroundColor(color);
        }
    
        @Override
        public int getItemCount() {
            return 10;
        }
    
    
        class MyViewHolder extends RecyclerView.ViewHolder {
            private TextView tvDemo;
    
            public MyViewHolder(@NonNull View itemView) {
                super(itemView);
    
                tvDemo = itemView.findViewById(R.id.tvDemo);
            }
        }
       }
    

    使用

            BaseAdapter baseAdapter = new BaseAdapter();
            vpBase.setAdapter(baseAdapter);
    

    滑动方向

    通过 setOrientation方法设置,可设置水平ORIENTATION_HORIZONTAL和垂直ORIENTATION_VERTICAL

       public void setOrientation(@Orientation int orientation) {
            mLayoutManager.setOrientation(orientation);
            mAccessibilityProvider.onSetOrientation();
        }
    

    可见,其原理是设置LayoutManager的方向

    滑动监听

    通过设置registerOnPageChangeCallback方法

        public void registerOnPageChangeCallback(@NonNull OnPageChangeCallback callback) {
            mExternalPageChangeCallbacks.addOnPageChangeCallback(callback);
        }
    

    mExternalPageChangeCallbacks 在初始化时,被添加到mPageChangeEventDispatcher

    mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);

    mPageChangeEventDispatcher 又被设置给mScrollEventAdapter

    mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);

    mScrollEventAdapter则继承于RecyclerView.OnScrollListener

    ScrollEventAdapter extends RecyclerView.OnScrollListener

    简而言之,基于RecyclerView.OnScrollListener的滑动事件

    禁止滑动

    通过setUserInputEnabled方法

    预加载

    通过setOffscreenPageLimit方法

        /**
         * <p>Set the number of pages that should be retained to either side of the currently visible
         * page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to
         * {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} to use RecyclerView's caching strategy. The given value
         * must either be larger than 0, or {@code #OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
         *
         * <p>Pages within {@code limit} pages away from the current page are created and added to the
         * view hierarchy, even though they are not visible on the screen. Pages outside this limit will
         * be removed from the view hierarchy, but the {@code ViewHolder}s will be recycled as usual by
         * {@link RecyclerView}.</p>
         *
         * <p>This is offered as an optimization. If you know in advance the number of pages you will
         * need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting
         * can have benefits in perceived smoothness of paging animations and interaction. If you have a
         * small number of pages (3-4) that you can keep active all at once, less time will be spent in
         * layout for newly created view subtrees as the user pages back and forth.</p>
         *
         * <p>You should keep this limit low, especially if your pages have complex layouts. By default
         * it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
         *
         * @param limit How many pages will be kept offscreen on either side. Valid values are all
         *        values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}
         * @throws IllegalArgumentException If the given limit is invalid
         * @see #getOffscreenPageLimit()
         */
        public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
            if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
                throw new IllegalArgumentException(
                        "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
            }
            mOffscreenPageLimit = limit;
            // Trigger layout so prefetch happens through getExtraLayoutSize()
            mRecyclerView.requestLayout();
        }
    

    其中OFFSCREEN_PAGE_LIMIT_DEFAULT默认为-1,如果不设置,默认使用RecyclerView的缓存机制,详细点击查看【Android进阶】RecyclerView之缓存(二)
    如果设置为大于的数,则会预加载limit

    PageTransformer

    ViewPager2延续了ViewPager中的PageTransformer,内置的有MarginPageTransformerCompositePageTransformer

    使用方式如下:

            BaseAdapter baseAdapter = new BaseAdapter();
            vpBase.setPageTransformer(new MarginPageTransformer(DensityUtils.dp2px(this, 20)));
            vpBase.setAdapter(baseAdapter);
    

    当然,我们也可以自己实现,只需要实现ViewPager2.PageTransformer

        class ScalePageTransformer implements ViewPager2.PageTransformer {
            private static final float DEFAULT_MIN_SCALE = 0.85f;
            private static final float DEFAULT_CENTER = 0.5f;
            private float mMinScale = DEFAULT_MIN_SCALE;
    
            @Override
            public void transformPage(@NonNull View view, float position) {
    
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    view.setElevation(-Math.abs(position));
                }
    
                int pageWidth = view.getWidth();
                int pageHeight = view.getHeight();
    
                view.setPivotY(pageHeight / 2);
                view.setPivotX(pageWidth / 2);
                if (position < -1) {
                    view.setScaleX(mMinScale);
                    view.setScaleY(mMinScale);
                    view.setPivotX(pageWidth);
                } else if (position <= 1) {
                    if (position < 0) {
                        float scaleFactor = (1 + position) * (1 - mMinScale) + mMinScale;
                        view.setScaleX(scaleFactor);
                        view.setScaleY(scaleFactor);
                        view.setPivotX(pageWidth * (DEFAULT_CENTER + DEFAULT_CENTER * -position));
                    } else {
                        float scaleFactor = (1 - position) * (1 - mMinScale) + mMinScale;
                        view.setScaleX(scaleFactor);
                        view.setScaleY(scaleFactor);
                        view.setPivotX(pageWidth * ((1 - position) * DEFAULT_CENTER));
                    }
                } else {
                    view.setPivotX(0);
                    view.setScaleX(mMinScale);
                    view.setScaleY(mMinScale);
                }
            }
        }
    

    使用方式如下

            BaseAdapter baseAdapter = new BaseAdapter();
            CompositePageTransformer pageTransformer = new CompositePageTransformer();
            pageTransformer.addTransformer(new MarginPageTransformer(DensityUtils.dp2px(this, 20)));
            pageTransformer.addTransformer(new ScalePageTransformer());
            vpBase.setPageTransformer(pageTransformer);
            vpBase.setAdapter(baseAdapter);
    

    Fragment

    ViewPager2中使用Fragment只需要Adapter继承于FragmentStateAdapter

    public class BaseFragmentStateAdapter extends FragmentStateAdapter {
    
        private List<DemoFragment> fragments;
    
        public BaseFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity, List<DemoFragment> fragments) {
            super(fragmentActivity);
            this.fragments = fragments;
        }
    
        public BaseFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
            super(fragmentActivity);
        }
    
        @NonNull
        @Override
        public Fragment createFragment(int position) {
            return fragments.get(position);
        }
    
        @Override
        public int getItemCount() {
            return fragments == null ? 0 : fragments.size();
        }
    }
    

    然后这样使用

            fragmentList = new ArrayList<>();
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
            fragmentList.add(new DemoFragment());
    
            BaseFragmentStateAdapter stateAdapter = new BaseFragmentStateAdapter(this, fragmentList);
            vpFragment.setAdapter(stateAdapter);
    

    这样就完了吗?恭喜你,收获内存泄漏一枚


    Screenshot_2020-01-11-11-51-25-206_com.ch.viewpager2demo.png

    ArrayList持有DemoFragment的强引用,导致DemoFragment无法销毁。
    所以正确的写法是

    public class TabAdapter extends FragmentStateAdapter {
        private List<String> titles;
    
        public TabAdapter(@NonNull FragmentActivity fragmentActivity, List<String> titles) {
            super(fragmentActivity);
            this.titles = titles;
        }
    
        @NonNull
        @Override
        public Fragment createFragment(int position) {
            return new DemoFragment(titles.get(position));
        }
    
        @Override
        public int getItemCount() {
            return titles.size();
        }
    }
    

    TabLayout

    ViewPager2配合TabLayout的联动可以使用TabLayoutMediator,记得销毁时,解除绑定tabLayoutMediator.detach();

            tabLayoutMediator = new TabLayoutMediator(tabLayout, vpTablayout, new TabLayoutMediator.TabConfigurationStrategy() {
                @Override
                public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                    tab.setText(titles.get(position));
                }
            });
            tabLayoutMediator.attach();
    

    踩坑

    TabLayout使用自定义布局和ViewPager2联动时会出现问题,TabLayout的自定义效果会无效,关键代码如下

      void populateTabsFromPagerAdapter() {
        tabLayout.removeAllTabs();
    
        if (adapter != null) {
          int adapterCount = adapter.getItemCount();
          for (int i = 0; i < adapterCount; i++) {
            TabLayout.Tab tab = tabLayout.newTab();
            tabConfigurationStrategy.onConfigureTab(tab, i);
            tabLayout.addTab(tab, false);
          }
          // Make sure we reflect the currently set ViewPager item
          if (adapterCount > 0) {
            int lastItem = tabLayout.getTabCount() - 1;
            int currItem = Math.min(viewPager.getCurrentItem(), lastItem);
            if (currItem != tabLayout.getSelectedTabPosition()) {
              tabLayout.selectTab(tabLayout.getTabAt(currItem));
            }
          }
        }
      }
    

    可见,onConfigureTab返回的都是tabLayout.newTab(),所以,正确的做法是采用最原始的联动

     tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
                @Override
                public void onTabSelected(TabLayout.Tab tab) {
                    vpTablayout.setCurrentItem(tab.getPosition());
                }
    
                @Override
                public void onTabUnselected(TabLayout.Tab tab) {
    
                }
    
                @Override
                public void onTabReselected(TabLayout.Tab tab) {
    
                }
            });
    
            vpTablayout.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageSelected(int position) {
                    tabLayout.selectTab(tabLayout.getTabAt(position));
                }
            });
    

    所涉及到的源码:ViewPagerDemo

    相关文章

      网友评论

        本文标题:【Android】ViewPager2简单了解

        本文链接:https://www.haomeiwen.com/subject/udgkzctx.html