一、可行性分析
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()));