自动轮播 ViewPager

1. 概述

1.1 特点

  1. 代码入侵性小, 修改一下类名即可, 不需要更换适配器
  2. 通过代码绘制指示器, 没有指示器布局文件和没有指示器资源文件
  3. 支持设置 ViewPager 切换时间
  4. 可通过 adapter.notifyDataSetChanged() 动态更新数据, 解决 setCurrentItem(int item) 卡顿

1.2 关于 ViewPager.setCurrentItem() 卡顿

尝试以下方案无效
  • 设置为不平滑切换, 发现没有任何效果; ViewPager.setCurrentItem(currentItem, false)
  • 调用 setCurrentItem() 前设置 ViewPager.mScrolle 滑动时间为 0
  • 调用 setCurrentItem() 前设置 ViewPager.mFirstLayout = true
  • 重新设置 Adapter
问题分析
  1. 在测试过程中发现在第一次设置 Adapter 的时候调用 ViewPager.setCurrentItem() 不会卡顿, 而在 adapter.notifyDataSetChanged() 时再调用 ViewPager.setCurrentItem() 却会卡顿
  2. 通过 Android Profiler 中的 CPU 模块的 Methed trace 发现是在 ViewPager.populate() 卡顿了
    // ViewPager.setCurrentItem() 源码
    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        ..............
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {        // 第一次进入该分支, 我们后续调用 setCurrentItem() 也是进入该分支就可以了 
            mCurItem = item;
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();        // 如果仅仅只修改 mFirstLayout=true, 该函数最终还是会调用 populate()
        } else {
            populate(item);         // 调用这个函数卡顿了
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }
解决方案
  1. 重新设置 Adapter
  2. 修改 mFirstLayout = true
private void selectFirstItem() {

            stopPlay();

            // 必须重新设置 setAdapter 并将 mFirstLayout 设置为 true, 防止进入 ViewPager.populate(), 避免卡顿
            setAdapter(null);
            setField("android.support.v4.view.ViewPager", AutoPlayViewPager.this, "mFirstLayout", true);
            setAdapter(adapter);

            if (adapter.getCount() > 0) {
                int currentItem = Integer.MAX_VALUE >> 1;
                currentItem = currentItem - currentItem % adapter.getCount();
                setCurrentItem(currentItem, false);
            }
        }

2. 完整代码和使用

2.1 AutoPlayViewPager

public class AutoPlayViewPager extends ViewPager {

    private Paint paint;
    private int position;
    private int size = 15;
    private int selected = Color.RED;
    private int background = 0x66FFFFFF;

    private int displayTime = Integer.MAX_VALUE;
    private final AtomicBoolean isPlaying = new AtomicBoolean(false);
    private final AutoPlayScroller autoPlayScroller;

    private PagerAdapter adapter;
    private final AtomicBoolean dataSetObserverRegistered = new AtomicBoolean(false);


    public AutoPlayViewPager(@NonNull Context context) {
        this(context, null);
    }

    public AutoPlayViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        addOnPageChangeListener(mListener);

        paint = new Paint();
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);

        autoPlayScroller = new AutoPlayScroller(context);
        setField("android.support.v4.view.ViewPager", this, "mScroller", autoPlayScroller);
    }


    @Override
    public void setAdapter(@Nullable PagerAdapter adapter) {

        if (adapter == null) {
            super.setAdapter(null);
            return;
        }

        // 1. 包装原 adapter, 使其支持无限轮播
        super.setAdapter(new AutoPlayAdapter(adapter));
        this.adapter = adapter;

        // 防止重复设置 Adapter 导致注册报错
        if (dataSetObserverRegistered.get()) {
            adapter.unregisterDataSetObserver(mDataSetObserver);
        }
        adapter.registerDataSetObserver(mDataSetObserver);
        dataSetObserverRegistered.set(true);

        // 2. 确保默认选中的是第一个页面和第一个圆点
        if (adapter.getCount() > 0) {
            int currentItem = Integer.MAX_VALUE >> 1;
            currentItem = currentItem - currentItem % adapter.getCount();
            setCurrentItem(currentItem, false);
        }
    }


    /**
     * 开始自动播放
     * @param displayTime : 页面显示时间
     */
    public void startPlay(int displayTime) {

        this.displayTime =  displayTime;
        if (adapter == null || adapter.getCount() <= 1)
            return;

        stopPlay();
        isPlaying.set(true);
        postDelayed(player, this.displayTime);
    }

    /**
     * 停止播放
     */
    public void stopPlay() {
        removeCallbacks(player);
        isPlaying.set(false);
    }


    // 循环播放消息
    private final Runnable player = new Runnable() {

        @Override
        public void run() {
            if (isPlaying.get()) {
                setCurrentItem(getCurrentItem() + 1, true);
                postDelayed(player, displayTime);
            }
        }
    };

    /**
     * 设置切换时间
     * @param duration 切换时间(ms), 默认 600ms
     */
    public void setSwitchDuration(int duration) {
        if (duration >= displayTime) {
            throw new IllegalArgumentException("The augment duration must less than displayTime!");
        }
        autoPlayScroller.setDuration(duration);
    }


    /**
     * 设置圆点样式
     * @param size       圆点大小
     * @param background 圆点背景色
     * @param selected   圆点前景色
     */
    public void setPointStyle(int size, int background, int selected) {
        this.size = size;
        this.selected = selected;
        this.background = background;
        invalidate();
    }


    /**
     * 绘制圆点指示器
     * @param canvas 画布
     */
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        final int count = adapter == null ? 0 : adapter.getCount();
        if (count < 1) return;

        canvas.save();
        canvas.translate(getScrollX(), getScrollY());

        float x = (getWidth() - size * count * 2) / 2 + size;
        float y = getHeight() - size;

        paint.setColor(background);
        for (int i = 0; i < count; i++) {
            canvas.drawCircle(x + i * size * 2, y, size >> 1, paint);
        }

        paint.setColor(selected);
        canvas.drawCircle(x + position * size * 2, y, size >> 1, paint);

        canvas.restore();
    }


    /**
     * 防止用户手动滑动后, 马上又播放下一张
     */
    private final OnPageChangeListener mListener = new ViewPager.SimpleOnPageChangeListener() {

        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                startPlay(displayTime);
            } else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
                stopPlay();
            }
        }

        @Override
        public void onPageSelected(int pos) {
            if (adapter.getCount() > 0) {
                position = pos % adapter.getCount();
                invalidate();
            }
        }
    };



    /**
     * 用于支持 Adapter.notifyDataSetChanged()
     */
    private final DataSetObserver mDataSetObserver = new DataSetObserver() {

        @Override
        public void onChanged() {
            update();
        }

        @Override
        public void onInvalidated() {
            update();
        }

        private void update() {

            if (getAdapter() == null)
                return;

            getAdapter().notifyDataSetChanged();
            selectFirstItem();

            if (displayTime != Integer.MAX_VALUE) {
                startPlay(displayTime);
            }
        }

        private void selectFirstItem() {

            stopPlay();

            // 1. 设置 mFirstLayout = true
            setField("android.support.v4.view.ViewPager", AutoPlayViewPager.this, "mFirstLayout", true);
            
            // 2. 重新设置 adapter
            setAdapter(null);
            setAdapter(adapter);

            if (adapter.getCount() > 0) {
                int currentItem = Integer.MAX_VALUE >> 1;
                currentItem = currentItem - currentItem % adapter.getCount();
                setCurrentItem(currentItem, false);
            }
        }
    };

    @Override
    protected void onDetachedFromWindow() {
        stopPlay();
        removeOnPageChangeListener(mListener);
        if (adapter != null && dataSetObserverRegistered.get()) {
            adapter.unregisterDataSetObserver(mDataSetObserver);
            dataSetObserverRegistered.set(false);
        }
        super.onDetachedFromWindow();
    }


    // 反射设置类字段
    public static boolean setField(String className, Object object, String filedName, Object filedValue) {
        try {
            Class clazz = Class.forName(className);
            Field field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(object, filedValue);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }



    private static class AutoPlayAdapter extends PagerAdapter {

        private final PagerAdapter adapter;

        AutoPlayAdapter(PagerAdapter adapter) {
            if (adapter == null)
                throw new NullPointerException("adapter is null!");
            this.adapter = adapter;
        }

        @Override
        public int getCount() {
            if (adapter.getCount() <= 1)
                return adapter.getCount();
            return Integer.MAX_VALUE;
        }

        @Override
        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
            return adapter.isViewFromObject(view, object);
        }

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
            if (adapter.getCount() > 0) {
                return adapter.instantiateItem(container, position % adapter.getCount());
            }
            return null;
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
            if (adapter.getCount() != 0) {
                adapter.destroyItem(container, position % adapter.getCount(), object);
            }
        }

        @Nullable
        @Override
        public CharSequence getPageTitle(int position) {
            if (adapter.getCount() != 0) {
                return adapter.getPageTitle(position % adapter.getCount());
            }
            return "";
        }
    }


    private static class AutoPlayScroller extends Scroller {

        private int duration = 600;

        AutoPlayScroller(Context context) {
            super(context, interpolator);
        }

        private static final Interpolator interpolator = (t) -> {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        };

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, Math.max(duration, this.duration));
        }

        void setDuration(int duration) {
            this.duration = duration;
        }
    }
}

2.2 测试代码

public class TestAutoPlayViewPager extends BaseActivity {

    private AutoPlayViewPager viewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_auto_play_viewpager_layout);
        init();
    }

    private void init() {

        List<Map.Entry<Integer, String>> data = new ArrayList<>();
        data.add(new AbstractMap.SimpleEntry<>(R.mipmap.aaa, "Page-1"));
        data.add(new AbstractMap.SimpleEntry<>(R.mipmap.bbb, "Page-2"));
        data.add(new AbstractMap.SimpleEntry<>(R.mipmap.ccc, "Page-3"));

        PagerAdapter adapter = getPagerAdapter(data);
        viewPager = findViewById(R.id.view_page);

        viewPager.setAdapter(adapter);
        viewPager.setSwitchDuration(800);
        viewPager.setPointStyle(UiUtil.dp2px(10), 0x66FFFFFF, Color.RED);

        // 测试动态数据更新
        viewPager.postDelayed(() -> {
            data.add(new AbstractMap.SimpleEntry<>(R.mipmap.dddd, "Page-4"));
            adapter.notifyDataSetChanged();
        }, 4000);
    }

    @Override
    protected void onStart() {
        super.onStart();
        viewPager.startPlay(2000);
    }

    @Override
    protected void onStop() {
        super.onStop();
        viewPager.stopPlay();                 // 停止播放, 节省资源
    }

    private PagerAdapter getPagerAdapter(final List<Map.Entry<Integer, String>> data) {

        return new PagerAdapter() {

            @Override
            public int getCount() {
                return data == null ? 0 : data.size();
            }

            @Override
            public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
                return view == object;
            }

            @NonNull
            @Override
            public Object instantiateItem(@NonNull ViewGroup container, int position) {

                TextView view = new TextView(getBaseContext());
                view.setText(data.get(position).getValue());
                view.setBackgroundResource(data.get(position).getKey());

                container.addView(view);
                return view;
            }

            @Override
            public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
                if (container != null && object != null && object instanceof View) {
                    container.removeView((View) object);
                }
            }
        };
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值