一、可行性分析
ViewPager是一款相对成熟的Pager切换View,能够实现各种优秀的页面效果,也有不少问题,比如频繁会requestLayout,另外的话如果是加载到ListView或者RecyclerView非固定头部,会偶现白屏或者drawble状态无法更新,还有就是fragment数量无法更新,需要重写FragmentPagerAdapter才行。
使用RecyclerView相对ViewPager来说,会避免很多问题,比如如果是轮播组件View可以复用而且会避免白屏问题,当然今天我们使用RecyclerView代替ViewPager虽然也没有实现复用,但并不影响和ViewPager同样的体验。
二、代码实现
具体原理是我们在RecyclerView.Adapter的如下两个方法中实现fragment的detach和attach,这样可以保证Fragment的生命周期得到准确执行。
onViewAttachedToWindow
onViewDetachedFromWindow
FragmentPagerAdapter源码如下(核心代码),另外需要指明的一点是我们使用PagerSnapHelper来辅助页面滑动:
public abstract class FragmentPagerAdapter extends RecyclerView.Adapter<FragmentViewHolder> {
private static final String TAG = "FragmentPagerAdapter";
private final FragmentManager mFragmentManager;
private Fragment mCurrentPrimaryItem = null;
private PagerSnapHelper snapHelper;
private RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState != RecyclerView.SCROLL_STATE_IDLE) return;
if (snapHelper == null) return;
View snapView = snapHelper.findSnapView(recyclerView.getLayoutManager());
if (snapView == null) return;
FragmentViewHolder holder = (FragmentViewHolder) recyclerView.getChildViewHolder(snapView);
setPrimaryItem(holder.getHelper().getFragment());
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
};
public FragmentPagerAdapter(FragmentManager fm) {
this.mFragmentManager = fm;
}
@Override
public FragmentViewHolder onCreateViewHolder(ViewGroup parent, int position) {
RecyclerView recyclerView = (RecyclerView) parent;
if (snapHelper == null) {
snapHelper = new PagerSnapHelper();
recyclerView.addOnScrollListener(onScrollListener);
snapHelper.attachToRecyclerView(recyclerView);
}
FragmentHelper host = new FragmentHelper(recyclerView, getItemViewType(position));
return new FragmentViewHolder(host);
}
@Override
public void onBindViewHolder(FragmentViewHolder holder, int position) {
holder.getHelper().updateFragment();
}
public abstract Fragment getFragment(int viewType);
@Override
public abstract int getItemViewType(int position);
public Fragment instantiateItem(FragmentHelper host, int position, int fragmentType) {
FragmentTransaction transaction = host.beginTransaction(mFragmentManager);
final long itemId = getItemId(position);
String name = makeFragmentName(host.getContainerId(), itemId, fragmentType);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (BuildConfig.DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
transaction.attach(fragment);
} else {
fragment = getFragment(fragmentType);
if (BuildConfig.DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
transaction.add(host.getContainerId(), fragment,
makeFragmentName(host.getContainerId(), itemId, fragmentType));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
@Override
public abstract long getItemId(int position);
@SuppressWarnings("ReferenceEquality")
public void setPrimaryItem(Fragment fragment) {
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
private static String makeFragmentName(int viewId, long id, int fragmentType) {
return "android:recyclerview:fragment:" + viewId + ":" + id + ":" + fragmentType;
}
@Override
public void onViewAttachedToWindow(FragmentViewHolder holder) {
super.onViewAttachedToWindow(holder);
FragmentHelper host = holder.getHelper();
Fragment fragment = instantiateItem(holder.getHelper(), holder.getAdapterPosition(), getItemViewType(holder.getAdapterPosition()));
host.setFragment(fragment);
host.finishUpdate();
if (BuildConfig.DEBUG) {
Log.d("Fragment", holder.getHelper().getFragment().getTag() + " attach");
}
}
@Override
public void onViewDetachedFromWindow(FragmentViewHolder holder) {
super.onViewDetachedFromWindow(holder);
destroyItem(holder.getHelper(), holder.getAdapterPosition());
holder.getHelper().finishUpdate();
if (BuildConfig.DEBUG) {
Log.d("Fragment", holder.getHelper().getFragment().getTag() + " detach");
}
}
public void destroyItem(FragmentHelper host, int position) {
FragmentTransaction transaction = host.beginTransaction(mFragmentManager);
if (BuildConfig.DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + host.getFragment()
+ " v=" + ((Fragment) host.getFragment()).getView());
transaction.detach((Fragment) host.getFragment());
}
}
ViewHolder源码,本类的主要作用是给FragmentManager打桩,其次还有个作用是连接FragmentHelper(负责Fragment的事务)
public class FragmentViewHolder extends RecyclerView.ViewHolder {
private FragmentHelper mHelper;
public FragmentViewHolder(FragmentHelper host) {
super(host.getFragmentView());
this.mHelper = host;
}
public FragmentHelper getHelper() {
return mHelper;
}
}
FragmentHelper源码
public class FragmentHelper {
private final int id;
private final Context context;
private Fragment fragment;
private ViewGroup containerView;
private FragmentTransaction fragmentTransaction;
public FragmentHelper(RecyclerView recyclerView, int fragmentType) {
this.id = recyclerView.getId() + fragmentType + 1;
// 本id依赖于fragment,因此为防止fragmentManager将RecyclerView视为容器,直接将View加载到RecyclerView中,这种View缺少VewHolder,会出现空指针问题,这里加1
Activity activity = getRealActivity(recyclerView.getContext());
this.id = getUniqueFakeId(activity,this.id);
this.context = recyclerView.getContext();
this.containerView = buildDefualtContainer(this.context,this.id);
}
public FragmentHelper(RecyclerView recyclerView,int layoutId, int fragmentType) {
this.context = recyclerView.getContext();
this.containerView = (ViewGroup) LayoutInflater.from( this.context).inflate(layoutId,recyclerView,false);
Activity activity = getRealActivity(recyclerView.getContext());
this.id = getUniqueFakeId(activity,this.id);
this.containerView.setId(id);
// 本id依赖于fragment,因此为防止fragmentManager多次复用同一个view,这里加1
}
private int getUniqueFakeId(Activity activity, int id) {
if(activity==null){
return id;
}
int newId = id;
do{
View v = activity.findViewById(id);
if(v!=null){
newId += 1;
continue;
}
newId = id;
break;
}while (true);
return newId;
}
public void setFragment(Fragment fragment) {
this.fragment = fragment;
}
public View getFragmentView() {
return containerView;
}
private static ViewGroup buildDefualtContainer(Context context,int id) {
FrameLayout frameLayout = new FrameLayout(context);
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
frameLayout.setLayoutParams(lp);
frameLayout.setId(id);
return frameLayout;
}
public int getContainerId() {
return id;
}
public void updateFragment() {
}
public Fragment getFragment() {
return fragment;
}
public void finishUpdate() {
if (fragmentTransaction != null) {
fragmentTransaction.commitNowAllowingStateLoss();
fragmentTransaction = null;
}
}
public FragmentTransaction beginTransaction(FragmentManager fragmentManager) {
if (this.fragmentTransaction == null) {
this.fragmentTransaction = fragmentManager.beginTransaction();
}
return this.fragmentTransaction;
}
}
以上提供了一个非常完美的FragmentPagerAdapter,来支持RecyclerView加载Fragment
2020-08-18更新
之前发现一个问题,在Fragment使用RecyclerView列表时会出现如下问题
1、交互不准确,比如垂直滑动会变成Pager滑动效果
2、页面fling效果出现闪动
3、事件冲突,导致滑动不了
因此为了解决上述问题,进行了一下规避
public class RecyclerPager extends RecyclerView {
private final DisplayMetrics mDisplayMetrics;
private int pageTouchSlop = 0;
float startX = 0;
float startY = 0;
boolean canHorizontalSlide = false;
public RecyclerPager(Context context) {
this(context, null);
}
public RecyclerPager(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerPager(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
pageTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
mDisplayMetrics = getResources().getDisplayMetrics();
}
private int captureMoveAction = 0;
private int captureMoveCounter = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = e.getX();
startY = e.getY();
canHorizontalSlide = false;
captureMoveCounter = 0;
Log.w("onTouchEvent_Pager", "down startY=" + startY + ",startX=" + startX);
break;
case MotionEvent.ACTION_MOVE:
float currentX = e.getX();
float currentY = e.getY();
float dx = currentX - startX;
float dy = currentY - startY;
if (!canHorizontalSlide && Math.abs(dy) > Math.abs(dx)) {
startX = currentX;
startY = currentY;
if (tryCaptureMoveAction(e)) {
canHorizontalSlide = false;
return true;
}
break;
}
if (Math.abs(dx) > pageTouchSlop && canScrollHorizontally((int) -dx)) {
canHorizontalSlide = true;
}
//这里取相反数,滑动方向与滚动方向是相反的
Log.d("onTouchEvent_Pager", "move dx=" + dx +",dy="+dy+ ",currentX=" + currentX+",currentY="+currentY + ",canHorizontalSlide=" + canHorizontalSlide);
if (canHorizontalSlide) {
startX = currentX;
startY = currentY;
if (captureMoveAction == MotionEvent.ACTION_MOVE) {
return super.dispatchTouchEvent(e);
}
if (tryCaptureMoveAction(e)) {
canHorizontalSlide = false;
return true;
}
}
break;
}
return super.dispatchTouchEvent(e);
}
/**
* 尝试捕获事件,防止事件后被父/子View主动捕获后无法改变捕获状态,简单的说就是没有cancel掉事件
*
* @param e 当前事件
* @return 返回ture表示发送了cancel->down事件
*/
private boolean tryCaptureMoveAction(MotionEvent e) {
if (captureMoveAction == MotionEvent.ACTION_MOVE) {
return false;
}
captureMoveCounter++;
if (captureMoveCounter != 2) {
return false;
}
MotionEvent eventDownMask = MotionEvent.obtain(e);
eventDownMask.setAction(MotionEvent.ACTION_DOWN);
Log.d("onTouchEvent_Pager", "事件转换");
super.dispatchTouchEvent(eventDownMask);
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
super.onInterceptTouchEvent(e); //该逻辑需要保留,因为recyclerView有自身事件处理
captureMoveAction = e.getAction();
switch (e.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
canHorizontalSlide = false;//不要拦截该类事件
break;
}
if (canHorizontalSlide) {
return true;
}
return false;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
consumed[1] = dy;
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public int getMinFlingVelocity() {
return (int) (super.getMinFlingVelocity() * mDisplayMetrics.density);
}
@Override
public int getMaxFlingVelocity() {
return (int) (super.getMaxFlingVelocity()* mDisplayMetrics.density);
}
@Override
public boolean fling(int velocityX, int velocityY) {
velocityX = (int) (velocityX / mDisplayMetrics.scaledDensity);
return super.fling(velocityX, velocityY);
}
}
三、使用
创建一个fragment
@SuppressLint("ValidFragment")
public static class TestFragment extends Fragment{
private final int color;
private String name;
private int[] colors = {
0xffDC143C,
0xff66CDAA,
0xffDEB887,
Color.RED,
Color.BLACK,
Color.CYAN,
Color.GRAY
};
public TestFragment(int viewType) {
this.name = "id#"+viewType;
this.color = colors[viewType%colors.length];
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View convertView = inflater.inflate(R.layout.test_fragment, container, false);
TextView textView = convertView.findViewById(R.id.text);
textView.setText("fagment: "+name);
convertView.setBackgroundColor(color);
if(BuildConfig.DEBUG){
Log.d("Fragment","onCreateView "+name);
}
return convertView;
}
@Override
public void onResume() {
super.onResume();
if(BuildConfig.DEBUG){
Log.d("Fragment","onResume");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.d("Fragment","setUserVisibleHint"+name);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(BuildConfig.DEBUG){
Log.d("Fragment","onDestroyView" +name);
}
}
}
接着我们实现FragmentPagerAdapter
public static class MyFragmentPagerAdapter extends FragmentPagerAdapter{
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getFragment(int viewType) {
return new TestFragment(viewType);
}
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return 3;
}
}
下面设置Adapter
RecyclerView recyclerPagerView = findViewById(R.id.loopviews);
recyclerPagerView.setLayoutManager(new
LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
recyclerPagerView.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));