Auto rotate ViewPager

Posted by xhelle on Sun, 15 Dec 2019 21:07:46 +0100

1. overview

1.1 characteristics

  1. The code is less intrusive. Just change the class name without changing the adapter
  2. Draw indicators by code, no indicator layout file and no indicator resource file
  3. Support setting ViewPager switching time
  4. The data can be dynamically updated through adapter.notifyDataSetChanged() to solve setCurrentItem(int item) stuck

1.2 about ViewPager.setCurrentItem() stuck

Invalid attempt to
  • Set to not smooth switch, no effect found; ViewPager.setCurrentItem(currentItem, false)
  • Set the viewpager.mscollle sliding time to 0 before calling setCurrentItem()
  • Set ViewPager.mFirstLayout = true before calling setCurrentItem()
  • Reset Adapter
problem analysis
  1. During the test, it is found that the call to ViewPager.setCurrentItem() will not jam when the Adapter is set for the first time, while the call to ViewPager.setCurrentItem() when the adapter.notifyDataSetChanged(), will jam
  2. Through the measured trace of CPU module in Android Profiler, it is found that it is stuck in ViewPager.populate()
    // ViewPager.setCurrentItem() source code
    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        ..............
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {        // The first time we enter this branch, we will call setCurrentItem() later to enter this branch as well 
            mCurItem = item;
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();        // If only mFirstLayout=true is modified, the function will eventually call populate()
        } else {
            populate(item);         // This function is stuck
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }
Solution
  1. Reset Adapter
  2. Modify mFirstLayout = true
private void selectFirstItem() {

            stopPlay();

            // setAdapter must be reset and mFirstLayout must be set to true to prevent access to ViewPager.populate() to avoid jamming
            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. Complete code and use

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. Package the original adapter to support infinite round robin
        super.setAdapter(new AutoPlayAdapter(adapter));
        this.adapter = adapter;

        // Prevent registration error caused by repeatedly setting Adapter
        if (dataSetObserverRegistered.get()) {
            adapter.unregisterDataSetObserver(mDataSetObserver);
        }
        adapter.registerDataSetObserver(mDataSetObserver);
        dataSetObserverRegistered.set(true);

        // 2. Make sure that the first page and the first dot are selected by default
        if (adapter.getCount() > 0) {
            int currentItem = Integer.MAX_VALUE >> 1;
            currentItem = currentItem - currentItem % adapter.getCount();
            setCurrentItem(currentItem, false);
        }
    }


    /**
     * Start autoplay
     * @param displayTime : Page display time
     */
    public void startPlay(int displayTime) {

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

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

    /**
     * stop playing
     */
    public void stopPlay() {
        removeCallbacks(player);
        isPlaying.set(false);
    }


    // Loop message
    private final Runnable player = new Runnable() {

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

    /**
     * Set switching time
     * @param duration Switching time (ms), default: 600ms
     */
    public void setSwitchDuration(int duration) {
        if (duration >= displayTime) {
            throw new IllegalArgumentException("The augment duration must less than displayTime!");
        }
        autoPlayScroller.setDuration(duration);
    }


    /**
     * Set dot style
     * @param size       Dot size
     * @param background Dot background color
     * @param selected   Dot foreground
     */
    public void setPointStyle(int size, int background, int selected) {
        this.size = size;
        this.selected = selected;
        this.background = background;
        invalidate();
    }


    /**
     * Draw dot indicator
     * @param canvas 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();
    }


    /**
     * Prevent users from manually sliding, and immediately play the next one
     */
    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();
            }
        }
    };



    /**
     * Used to support 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. Set mFirstLayout = true
            setField("android.support.v4.view.ViewPager", AutoPlayViewPager.this, "mFirstLayout", true);
            
            // 2. Reset 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();
    }


    // Reflection settings class fields
    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 test code

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

        // Test dynamic data update
        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();                 // Stop playing, save resources
    }

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

Topics: Android github less