Android 代替ViewPager使用RecyclerView加載Fragment

一、可行性分析

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()));

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章