Android 换种方式实现ViewPager

一、可行性分析

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 来辅助页面滑动:

java 复制代码
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 的事务)

scala 复制代码
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 源码

kotlin 复制代码
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

三、新问题

在 Fragment 使用 RecyclerView 列表时会出现如下问题

1、交互不准确,比如垂直滑动会变成 Pager 滑动效果

2、页面 fling 效果出现闪动

3、事件冲突,导致滑动不了

因此为了解决上述问题,进行了一下规避

java 复制代码
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

java 复制代码
    @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

java 复制代码
 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

ini 复制代码
 RecyclerView recyclerPagerView = findViewById(R.id.loopviews);
 recyclerPagerView.setLayoutManager(new 
 LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
 recyclerPagerView.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));

五、总结

整个过程轻松而愉快,当然本篇主要学习的是RcyclerView事件冲突的解决,突发奇想然后就写了个轮子,看样子是没什么大问题。

相关推荐
用户42274481246211 分钟前
IndexDB 前端数据库
前端
然我2 分钟前
前端正则面试通关指南:一篇吃透所有核心考点,轻松突围面试
前端·面试·正则表达式
shenshizhong4 分钟前
看懂鸿蒙系统源码 比较重要的知识点
android·harmonyos
LFly_ice30 分钟前
学习React-11-useDeferredValue
前端·学习·react.js
fangzelin542 分钟前
算法-滑动窗口
数据结构·算法
亮子AI1 小时前
【npm】npm 包更新工具 npm-check-updates (ncu)
前端·npm·node.js
信看1 小时前
实用 html 小工具
前端·css·html
Yvonne爱编码1 小时前
构建高效协作的桥梁:前后端衔接实践与接口文档规范详解
前端·git·ajax·webpack·node.js
王源骏1 小时前
Laya使用VideoNode动态加载视频,可以自定义播放视频此处以及位置
前端
zcz16071278211 小时前
LVS + Keepalived 高可用负载均衡集群
java·开发语言·算法