自動輪播 ViewPager
阿新 • • 發佈:2018-12-15
1. 概述
1.1 特點
- 程式碼入侵性小, 修改一下類名即可, 不需要更換介面卡
- 通過程式碼繪製指示器, 沒有指示器佈局檔案和沒有指示器資原始檔
- 支援設定 ViewPager 切換時間
- 可通過 adapter.notifyDataSetChanged() 動態更新資料, 特殊處理了 setCurrentItem(int item) 卡頓
1.2 關於 ViewPager.setCurrentItem() 卡頓
嘗試以下方案無效
- 設定為不平滑切換, 發現沒有任何效果;
ViewPager.setCurrentItem(currentItem, false)
- 呼叫 setCurrentItem() 前設定 ViewPager.mScrolle 滑動時間為 0
- 呼叫 setCurrentItem() 前設定
ViewPager.mFirstLayout = true
- 重新設定 Adapter
問題分析
- 在測試過程中發現在第一次設定 Adapter 的時候呼叫
ViewPager.setCurrentItem()
不會卡頓, 而在adapter.notifyDataSetChanged()
時再呼叫ViewPager.setCurrentItem()
卻會卡頓 - 通過 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);
}
}
解決方案
- 重新設定 Adapter
- 修改
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