1. 程式人生 > >自動輪播 ViewPager

自動輪播 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 Abstra