自定義ViewPager指示器
app的引導頁和banner功能,通常採用ViewPager實現,並且往往都會有指示器,顯示當前所選頁。本文用自定義view的方式實現一個通用的圓形指示器,繼承自View。
一、效果預覽
二、效果分析
上圖中顯示兩個小圓點,表示 ViewPager有兩頁(不考慮無限輪播)
紅色表示選中頁,灰色表示未選中頁
實際上紅色小圓點下方也有灰色小圓點,只是被覆蓋了
所以圖上有兩個灰色小圓點,它們統稱為背景小圓點
紅色小圓點稱為移動小圓點
三、自定義View的屬性
<declare-styleable name="CirclePagerIndicator">
<!--是否水平居中-->
<attr name="indicator_centerHorizontal" format="boolean" />
<!--滑動小圓點是否跟隨-->
<attr name="indicator_follow" format="boolean" />
<!--背景小圓點顏色-->
<attr name="indicator_color" format="color" />
<!--背景小圓點描邊顏色-->
<attr name="indicator_stroke_color" format="color" />
<!--背景小圓點描邊寬度-->
<attr name="indicator_stroke_width" format="dimension" />
<!--移動小圓點顏色-->
<attr name="indicator_move_color" format="color" />
<!--背景小圓點半徑-->
<attr name="indicator_radius" format="dimension" />
<!--移動小圓點半徑-->
<attr name="indicator_move_radius" format="dimension" />
<!--小圓點間距-->
<attr name="indicator_space" format="dimension" />
</declare-styleable>
四、自定義View套路程式碼
public class CirclePagerIndicator extends View implements PagerIndicator {
public CirclePagerIndicator(Context context) {
this(context, null);
}
public CirclePagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
//獲取上面自定義的屬性,並初始化畫筆。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePagerIndicator);
mCenterHorizontal = a.getBoolean(R.styleable.CirclePagerIndicator_indicator_centerHorizontal, true);
//背景小圓點畫筆
mBgCirclePaint.setStyle(Paint.Style.FILL);
mBgCirclePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_color, 0x0000ff));
//背景小圓點描邊畫筆
mBgStrokePaint.setStyle(Paint.Style.STROKE);
mBgStrokePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_stroke_color, 0x000000));
mBgStrokePaint.setStrokeWidth(a.getDimension(R.styleable.CirclePagerIndicator_indicator_stroke_width, 0));
//移動小圓點畫筆
mMoveCirclePaint.setStyle(Paint.Style.FILL);
mMoveCirclePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_move_color, 0x0000ff));
//背景小圓點半徑
mBgCircleRadius = a.getDimension(R.styleable.CirclePagerIndicator_indicator_radius, 10);
//移動小圓點半徑
mMoveCircleRadius = a.getDimension(R.styleable.CirclePagerIndicator_indicator_move_radius, 10);
//小圓點間距
mIndicatorSpace = a.getDimension(R.styleable.CirclePagerIndicator_indicator_space, 20);
//移動小圓點是否隨viewpager移動跟隨
mIsFollow = a.getBoolean(R.styleable.CirclePagerIndicator_indicator_follow, true);
if (mMoveCircleRadius < mBgCircleRadius) mMoveCircleRadius = mBgCircleRadius;
a.recycle();
}
}
五、測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec) {
int width;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
width = specSize;
} else {
final int count = mViewPager.getAdapter().getCount();
width = (int) (getPaddingLeft() + getPaddingRight()
+ (count * 2 * mBgCircleRadius) + (mMoveCircleRadius - mBgCircleRadius) * 2 + (count - 1) * mIndicatorSpace);
if (specMode == MeasureSpec.AT_MOST) {
width = Math.min(width, specSize);
}
}
return width;
}
private int measureHeight(int measureSpec) {
int height;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
height = specSize;
} else {
height = (int) (2 * mBgCircleRadius + getPaddingTop() + getPaddingBottom() + 1);
if (specMode == MeasureSpec.AT_MOST) {
height = Math.min(height, specSize);
}
}
return height;
}
六、獲取自定義View尺寸
只有在onMeasure之後才能獲得正確的控制元件寬高。所以在onSizeChanged中獲取,同時當控制元件尺寸變化後該方法會再次執行,確保了尺寸獲取的正確性。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw || h != oldh) {
//控制元件總寬度
mWidth = getWidth();
//控制元件左邊距
mPaddingLeft = getPaddingLeft();
//控制元件右邊距
mPaddingRight = getPaddingRight();
//控制元件頂邊距
mPaddingTop = getPaddingTop();
}
}
七、繫結ViewPager
小圓點需要隨著ViewPager的切換而移動,而我們都知道ViewPager中有個監聽頁面切換的api:addOnPageChangeListener(OnPageChangeListener listener);
。所以直接將ViewPager繫結給指示器,定義繫結方法:
@Override
public void bindViewPager(ViewPager viewPager, int initialPosition, int realSize) {
bindViewPager(viewPager, initialPosition);
this.mRealSize = realSize;
}
public void bindViewPager(ViewPager viewPager, int initialPosition) {
bindViewPager(viewPager);
setCurrentItem(initialPosition);
}
public void bindViewPager(ViewPager viewPager) {
if (mViewPager == viewPager) {
return;
}
if (viewPager.getAdapter() == null) {
throw new IllegalStateException("ViewPager does not set adapter");
}
mViewPager = viewPager;
mViewPager.addOnPageChangeListener(this);
mViewPager.getAdapter().registerDataSetObserver(mObserver);
invalidate();
}
八、處理Viewpager的頁面切換監聽
//*********************************OnPageChangeListener*************************************************
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mRealSize == 0) {
mCurrentPosition = position;
} else {
mCurrentPosition = position % mRealSize;
}
mPositionOffset = positionOffset;
//如果指示器跟隨ViewPager緩慢滑動,那麼滾動時一直繪製介面
if (mIsFollow) {
invalidate(); //實時繪製小圓點.
}
}
@Override
public void onPageSelected(int position) {
if (mRealSize == 0) {
mFollowPage = mCurrentPosition = position;
} else {
mFollowPage = mCurrentPosition = position % mRealSize; //輪播圖無限, 記錄的當前頁.
}
invalidate();
}
//******************************************************************************************
九、重頭戲,小圓點繪製
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mViewPager == null) {
return;
}
//輪播圖實際頁面數(小圓點個數)
int count = mRealSize == 0 ? mViewPager.getAdapter().getCount() : mRealSize;
if (count == 0) {
return;
}
if (mCurrentPosition >= count) {
setCurrentItem(count - 1);
return;
}
//直徑+間隔距離
final float circleAndSpace = 2 * mBgCircleRadius + mIndicatorSpace;
final float circleCenterY = mPaddingTop + mBgCircleRadius;
//第一個小圓點的圓心x座標
float circleCenterXFirst = mPaddingLeft + mBgCircleRadius;
if (mCenterHorizontal) {
//(總長度 - 繪製圓點所佔空間) / 2,居中
circleCenterXFirst += ((mWidth - mPaddingLeft - mPaddingRight) - (count * circleAndSpace - mIndicatorSpace)) / 2.0f;
}
float cX;
float cY;
float strokeRadius = mBgCircleRadius;
//如果繪製描邊
if (mBgStrokePaint.getStrokeWidth() > 0) {
strokeRadius -= mBgStrokePaint.getStrokeWidth() * 1f / 2;
}
//繪製所有圓點
for (int i = 0; i < count; i++) {
cX = circleCenterXFirst + (i * circleAndSpace);//計算下個圓繪製起點偏移量
cY = circleCenterY;
//繪製背景小圓點
if (mBgCirclePaint.getAlpha() > 0) {
canvas.drawCircle(cX, cY, mBgCircleRadius, mBgCirclePaint);
}
//繪製背景小圓點描邊
if (strokeRadius != mBgCircleRadius) {
canvas.drawCircle(cX, cY, strokeRadius, mBgStrokePaint);
}
}
//繪製移動的小圓點。
if (mIsFollow && mCurrentPosition == mRealSize - 1) {
if (mPositionOffset < 0.5) { //當前為最後一頁, 偏移小於一半時
canvas.drawCircle(circleCenterXFirst + mCurrentPosition * circleAndSpace, circleCenterY, mMoveCircleRadius, mMoveCirclePaint);
return;
} else { //當前為最後一頁, 偏移大於一半時
canvas.drawCircle(circleCenterXFirst, circleCenterY, mMoveCircleRadius, mMoveCirclePaint);
return;
}
}
float cx = (mIsFollow ? mCurrentPosition + mPositionOffset : mFollowPage) * circleAndSpace;
cX = circleCenterXFirst + cx;
cY = circleCenterY;
canvas.drawCircle(cX, cY, mMoveCircleRadius, mMoveCirclePaint);
}
十、對外提供一些方法
1、設定選中小圓點
2、重新整理頁面
@Override
public void setCurrentItem(int item) {
if (mViewPager == null) {
throw new IllegalStateException("indicator has not bind ViewPager");
}
if (mRealSize == 0) {
mCurrentPosition = item;
} else {
mCurrentPosition = item % mRealSize;
}
invalidate(); //呼叫onDraw.
}
@Override
public void notifyDataSetChanged() {
invalidate();
requestLayout();//當view的寬高不變,不會呼叫invalidate();
}
十一、完善,觀察者設計模式
ViewPager的Adapter資料發生改變,呼叫Adapter的notifyDataSetChanged方法,ViewPager的資料改變,當然,與之關聯的指示器控制元件也要重新整理。怎麼辦?
通過檢視原始碼,發現ViewPager的adapter中持有一個Observable物件,adapter在這裡就相當於被觀察者。
而ViewPager相當於觀察者,它裡面定義了Observer內部類,並將這個內部類物件註冊給Adapter中的Observable。
很明顯,非常典型的觀察者模式,都是套路。(我認為這樣做的好處就是解耦)
被觀察者的變化,從而引起觀察者的變化。不難看出,我們這裡的自定義view相當於觀察者,所以仿照ViewPager原始碼,定義一個Observer內部類如下:
//============================ 觀察者設計模式 ======================================
private class IndicatorObserver extends DataSetObserver {
@Override
public void onChanged() {
notifyDataSetChanged();
}
}
那麼該Observer內部類什麼時候註冊給adapter呢?往回看第七步的繫結Viewpager中的這句程式碼:
mViewPager.getAdapter().registerDataSetObserver(mObserver);
mObserver是自定義view的成員變數:
private final DataSetObserver mObserver = new IndicatorObserver
這樣,自定義ViewPager指示器就完成了。
相關文章
- Android 自定義控制元件 ViewPager頭部指示器控制元件 ViewPagerBelowIndicatorAndroid控制元件ViewpagerIndicator
- Android 自定義控制元件玩轉字型變色 打造炫酷ViewPager指示器Android控制元件Viewpager
- 自定義控制元件ViewPager控制元件Viewpager
- ViewPager之標籤的自定義Viewpager
- 小程式輪播圖自定義指示器
- 國人自定義React Native開源元件ViewPagerReact Native元件Viewpager
- Android ViewPager 指示器控制元件的最佳實現AndroidViewpager控制元件
- 敲酷炫的 ViewPager 切換效果和彈性指示器。Viewpager
- 直播軟體原始碼,橫向滾動 自定義底部指示器樣式原始碼
- 在 Linux 上為你的任務建立一個自定義的系統托盤指示器Linux
- 自定義View:自定義屬性(自定義按鈕實現)View
- 支援橫向、豎向無限滾動和自定義指示器的廣告條BannerView和淘寶頭條效果View
- 08.Django自定義模板,自定義標籤和自定義過濾器Django過濾器
- Android進階之自定義ViewGroup—帶你一步步輕鬆實現ViewPagerAndroidViewpager
- 自定義ImageView完成圓形頭像自定義View
- 自定義VIEWView
- 自定義圓環
- 自定義SnackBar
- 自定義useState
- 自定義_ajax
- 自定義Annotation
- 自定義OrderedMap
- 自定義 Drawable
- 自定義UICollectionViewLayoutUIView
- 自定義UITabBarUItabBar
- 自定義scrollbar
- 自定義Drawable
- 自定義ToastAST
- 自定義吐司
- 自定義表格
- 自定義 GitGit
- tailwind自定義AI
- 自定義 tabBartabBar
- android自定義view(自定義數字鍵盤)AndroidView
- vue自定義全域性元件(或自定義外掛)Vue元件
- android自定義View&自定義ViewGroup(下)AndroidView
- android自定義View&自定義ViewGroup(上)AndroidView
- Android自定義控制元件——自定義屬性Android控制元件