最早是用 VIewPage+TabLayout 搭建的 ,
官方为何要用ViewPage2 替换 ViewPage? viewpager和viewpager2原理和使用
去年年初 google官网上更新了viewpager2, 支持垂直滚动, 重写之前的viewpager.
我们以前项目中使用的是 Viewpage
最近替换为ViewPage2
为何呢?
首先你会发现问题所在,解决,解决不了?
换控件,升级控件,解决问题,代码优美简洁。
ViewPager2是Google 在 androidx 组件包里增加的一个组件,目前已经到了1.0.0-beta02版本。
谷歌为什么要出这个组件呢?官方是这么说的
ViewPager2 replaces ViewPager, addressing most of its predecessor’s pain-points,
including right-to-left layout support, vertical orientation, modifiable Fragment collections, etc.
翻译:ViewPager2取代了ViewPager,解决了它的前任的大部分难点,包括从右到左的布局支持、垂直方向、可修改的片段集合等。
大家看到这句话,是不是感觉 浩然开朗,我们用VIewPage的时候是不是就遇到过这些问题,然后写了好多代码去解决,这下好了,google已经帮我们解决了,
直接用ViewPage2即可。
关于原理: 大家都可以在网上看到或者进入源码深究
下面我就大概述说:viewpager2 内部实现原理是使用recycleview加LinearLayoutManager实现竖直滚动, 其实可以理解为对recyclerview的二次封装,所有功能都是围绕着RecyclerView和LinearLayoutManager展开,不过我们在这里不展开介绍了,我们主要关注两个点。
-
ViewPager2是怎么使用RecyclerView实现ViewPager的效果的?
-
RecyclerView来怎么配合Fragment的生命周期的
然后大家可以细想一下:
ViewPager2目前只支持ItemView的布局参数是MATCH_PARENT,就是填充父布局的效果;由于ViewPager2是基于RecyclerView,理论上每个ItemView一定会是MATCH_PARENT,控制一屏只加载一个Item,但是一旦MATCH_PARENT计算失效,那么ViewPager2基本上就是RecyclerView的效果,瞬间多个Fragment是可以解释通的;
ViewPager2是怎么使用RecyclerView实现ViewPager的效果的
1、我们先从ViewPager2的初始化入手,代码如下
大家可以去看看源码:
我这边吧源码贴出来:源代码如下:
ViewPage2.java中的方法:initialize()
在initialize()方法里面,初始化了RecyclerView组件。
主要做了这几件事:
-
RecycleView的基本初始化
-
设置了PagerSnapHelper,目的是实现切面切换的效果
-
给RecyclerView设置了滑动监听事件,涉及到的组件是ScrollEventAdapter,后面的基本功能都需要这个组件的支持
ViewPager的效果,就是靠PagerSnapHelper和ScrollEventAdapter实现的。
2、然后我们在研究一下PagerSnapHelper是干什么的?
从源码中可以点击进去:
PagerSnapHelper继承于SnapHelper。SnapHelper顾名思义是Snap+Helper的组合,Snap有移到某位置的含义,Helper为辅助者,
综合场景解释是 将RecyclerView移动到某位置的辅助类。
SnapHelper的内部有两个监听接口:OnFlingListener和OnScrollListener,分别用来监听RecyclerView的fling事件和scroll事件。
SnapHelper有三个重要的方法:
-
findTargetSnapPosition(计算Fling事件能滑动到位置)
-
findSnapView(找到需要显示在RecyclerView的最前面的View)
-
calculateDistanceToFinalSnap(计算需要滑动的距离)。
PagerSnapHelper重写了这三个方法,用以拦截fling事件,做到每次滑动一个item
那么这些方法什么时间调用的?
-
手指在快速滑动,在手指离开屏幕前,三个方法均不调用。
-
手指在快速滑后,手指离开了屏幕,会发生Fling事件,findTargetSnapPosition方法会被调用。
-
当Fling事件结束时,RecyclerView会回调SnapHelper内部OnScrollListener接口的onScrollStateChanged方法。
此时RecyclerView的滑动状态为RecyclerView.SCROLL_STATE_IDLE,会调用findSnapView方法来找到需要
显示在RecyclerView的最前面的View。找到目标View之后,就会调用calculateDistanceToFinalSnap方法来计算需要滑动的距离。然后调用smoothScrollBy滑动到对应位置。
我们先从PagerSnapHelper调用的attachToRecyclerView方法说起。
SnapHelper.java
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
//判断是否是同一个RecyclerView,如果是直接返回,防止重复attach
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
//如果是一个新的RecyclerView,先销毁所有的回调接口,这里指的是
//RecyclerView的scroll监听和fling监听
if (mRecyclerView != null) {
destroyCallbacks();
}
//保存这个RecyclerView
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
//重新设置监听
setupCallbacks();
//初始化一个Scoller,用于滚动RecylerView
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
//移动到指定的已存在的View 后面讲解
snapToTargetExistingView();
}
}
//设置回调关系
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException(“An instance of OnFlingListener already set.”);
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
//注销回调关系
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
SnapHelper在attachToRecyclerView方法中先注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程?
我们先来看下onScroll事件的处理。
SnapHelper.java
// Handles the snap on scroll case.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//当newState == 静止状态且滚动距离不等于0,触发snapToTargetExistingView();
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
//移动到指定的已存在的View
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//查找SnapView
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//计算SnapView的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
snapToTargetExistingView()这个方法会在第一次attach的时候和RecyclerView滑动状态改变的时候调用,用于将RecyclerView移动到指定的位置,移动的距离根据当前距离RecyclerView中心点最近的那个itemView获取,获取到这个距离后,调用smoothScrollBy方法移动RecyclerView。在这个方法中findSnapView和calculateDistanceToFinalSnap是两个抽象方法。需要子类重写,实现不同的效果。
2 具体改动:New features:
-
支持竖向滚动
-
完整支持notifyDataSetChanged
-
能够关闭用户输入 (setUserInputEnabled, isUserInputEnabled)
API changes(API变动):
-
FragmentStateAdapter替换了原来的 FragmentStatePagerAdapter
-
RecyclerView.Adapter替换了原来的 PagerAdapter
-
-
registerOnPageChangeCallback替换了原来的 addPageChangeListener
FragmentStateAdapter和FragmentStatePagerAdapter作用相同, 可以用viewpager来管理fragment, 区别在于viewpager2的FragmentStateAdapter与recycleview的生命周期绑定
另外viewpager2的Adapter是继承自recyclerview的adapter, 支持除了notifyDataSetChanged()以外的notifyItemChanged(int position)等方式, 使用上更加的便捷
这样就可以随意的动态的添加item可删除item 直接调用notifyDataSetChanged()即可(这个对资讯类项目app或者电商类的都很有用处,突然发生了一件大事,需要马上上一个栏目,如比特币,影音,杂志,美国等等),这时候就需要后台直接添加栏目而我们App不需要专门去为了市场而加急发布新版。
-
-
使用
-
viewpager2放在AndroidX的库中, 引用方式:
-
去年十一月发布的稳定版本
-
-
我在demo中使用的是 最新版本 1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01’
注意: 这里是用的androidx库, 如果你的项目中还在使用support库的话, 需要将support库迁移至androidx才可以正常使用, 否则会报各种类库找不到的问题.
这个视图是横向滑动和竖向滑动切换页面,如何实现可直接去demo中查看源码
-
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
tools:context=".MainActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="0dp"
android:layout_height="0dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
在xml中设置orientation, 或者在代码中设置setOrientation(),可以控制横纵向
也可以在代码中设置:
ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter();
viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
activity中
onCreate 方法中,设置 ViewPager2:
package com.example.viewpager2demo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;
/**
-
@author wdx
-
@date 2019/6/01
*/
public class VerticalScrollingActivity extends AppCompatActivity {
public static void start(Context context) {
Intent starter = new Intent(context, VerticalScrollingActivity.class);
context.startActivity(starter);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_horizontal_scrolling);
ViewPager2 viewPager2 = findViewById(R.id.viewpager2);
ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter();
viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager2.setAdapter(viewPagerAdapter);
}
}
adapter中的代码:
package com.example.viewpager2demo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
-
@author wdx
-
@date 2012/6/01
*/
public class ViewPagerAdapter extends RecyclerView.Adapter<ViewPagerAdapter.ViewPagerViewHolder> {
private List colors = new ArrayList<>();
{
colors.add(android.R.color.holo_red_dark);
colors.add(android.R.color.holo_purple);
colors.add(android.R.color.holo_blue_dark);
colors.add(android.R.color.holo_green_light);
}
@NonNull
@Override
public ViewPagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewPagerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_page, parent,false));
}
@Override
public void onBindViewHolder(@NonNull ViewPagerViewHolder holder, int position) {
holder.mTvTitle.setText("item— " + position);
holder.mContainer.setBackgroundResource(colors.get(position));
}
@Override
public int getItemCount() {
return colors.size();
}
class ViewPagerViewHolder extends RecyclerView.ViewHolder {
TextView mTvTitle;
RelativeLayout mContainer;
public ViewPagerViewHolder(@NonNull View itemView) {
super(itemView);
mContainer = itemView.findViewById(R.id.container);
mTvTitle = itemView.findViewById(R.id.tvTitle);
}
}
}
App主体UI框架就可以用这个来搭建了:
下面可以看看效果图:(ViewPage2+TabLayout+Fragment)
代码实现如下:
Xml布局文件: <?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
android:layout_above="@+id/tablayout"
android:id="@+id/viewpager2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tablayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.tabs.TabLayout
android:layout_alignParentBottom="true"
android:id="@+id/tablayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/cardview_dark_background"
app:tabIndicator="@null"
app:tabPadding="0dp"
app:tabBackground="@color/transparent"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
主activity中的代码:(完整代码在demo中可以获取) 下面对主activity进行讲解一下: 1,viewppage的编写 (1),viewpage2页面的fragment初始化 private void fragmentInit() { Fragment1 fragemnt1 = new Fragment1(); Fragment2 fragemnt2 = new Fragment2(); Fragment3 fragemnt3 = new Fragment3(); fragmentList.clear(); fragmentIdList.clear(); fragmentList.add(fragemnt1); fragmentIdList.add(Long.valueOf(PAGE_HOME)); fragmentList.add(fragemnt2); fragmentIdList.add(Long.valueOf(PAGE_CATE)); fragmentList.add(fragemnt3); fragmentIdList.add(Long.valueOf(PAGE_ONLINE)); }
(2), ViewPager2 页面变化回调选中tab
mViewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (position != mTabLayout.getSelectedTabPosition()) {
mTabLayout.getTabAt(position).select();
}
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
2,tablayout的编写
(1),TabLaoyout动态添加 TabLayout.Tab
private void tabInit() {
mTabLayout.removeAllTabs();
View view1 = LayoutInflater.from(this).inflate(R.layout.tabone_layout, mViewPager2, false);
tab1 = mTabLayout.newTab();
tab1.setCustomView(view1);
mTabLayout.addTab(tab1);
View view2 = LayoutInflater.from(this).inflate(R.layout.tabtwo_layout, mViewPager2, false);
tab2 = mTabLayout.newTab();
tab2.setCustomView(view2);
mTabLayout.addTab(tab2);
View view3 = LayoutInflater.from(this).inflate(R.layout.tabthree_layout, mViewPager2, false);
tab3 = mTabLayout.newTab();
tab3.setCustomView(view3);
mTabLayout.addTab(tab3);
}
(2),TabLayout添加tab选中监听—且安魂mViewPage2 选中页
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (tab.equals(mTabLayout.getTabAt(0))) {
mSelectedTab = 0;
mViewPager2.setCurrentItem(0, false);
} else if (tab.equals(mTabLayout.getTabAt(1))) {
mSelectedTab = 1;
mViewPager2.setCurrentItem(1, false);
}else if (tab.equals(mTabLayout.getTabAt(2))) {
mSelectedTab = 2;
mViewPager2.setCurrentItem(2, false);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
3,两者相互关联起来(viewpage的滑动出发tablayout的选中,tablayout的点击选中触发viewpage的滑动页面切换)
mAdapter = new ViewPagerFragmentStateAdapter(this, fragmentList, fragmentIdList);
mViewPager2.setAdapter(mAdapter);
mViewPager2.setUserInputEnabled(false);//不可左右滑动
mViewPager2.setOffscreenPageLimit(3);
mTabLayout.getTabAt(mSelectedTab).select();//tablayout默认选中
FragmentStateAdapter 的使用:ViewPagerFragmentStateAdapter重写:
class ViewPagerFragmentStateAdapter extends FragmentStateAdapter {
private List list;
private List fragmentIds;
public ViewPagerFragmentStateAdapter(@NonNull AppCompatActivity fragmentManager, List fragmentList, List fragmentIdList) {
super(fragmentManager);
this.list = fragmentList;
this.fragmentIds = fragmentIdList;
}
@Override
public int getItemCount() {
return fragmentList.size();
}
public List getFragmentIds() {
return fragmentIds;
}
@Override
public boolean containsItem(long itemId) {
return fragmentIds.contains(itemId);
}
@Override
public long getItemId(int position) {
return fragmentIdList.get(position);
}
@NonNull
@Override
public Fragment createFragment(int position) {
return list.get(position);
}
}
对应的三个fragment就是简单的页面 里面包含了 Textview而已 就不贴进来了,大家进行自己补充,或者从demo中直接获取。
总结
ViewPager2本身是一个ViewGroup,只是封装一个RecyclerView。
-
-
PagerSnapHelper实现页面切换效果:
-
calculateDistanceToFinalSnap
-
findSnapView
-
findTargetSnapPosition
-
ScrollEventAdapter将RecyclerView的滑动事件转换成为ViewPager2的页面滑动事件。
-
PageTransformerAdapter的作用将普通的页面滑动事件转换为特殊事件。
-
FragmentStateAdapter中使用Adapter加载Fragment,也考虑到了ViewHolder的复用,Fragment加载和remove。
这些东西都要我们自己去源码中一步一步深入的深扒和分析才能看到,是怎么回事,就可以完全知道是什么原理了。
我估计这些东西分开,对咱们来说可能都知道怎么用,都懂怎么回事,但是把他们都串起来,搭配起来使用,就变成一个大的功能,
所以我们一定要读懂原理,让我们自己可以慢慢去学习他们的这种思路,组装
这个就类似于Flutter的widget组件,各种组件写好,放那边,单独的
然后需要的时候再去更具需求,业务逻辑组装,各种组装,就变成了我们的app
所以Flutter 现在都提倡的是 少继承,多组装
这个思路是不是也要应用到我们的应用中呢,对于复杂的业务逻辑。各种封装继承真的能灵活解决我们的业务需求吗?
这个是值得我们深思的问题。
官方gtihub上的demo
https://github.com/googlearchive/android-viewpager2 本文demo的github地址:
https://github.com/1136346879/ViewPage2