一、先看效果

二、分析
這個效果用動畫來寫,并不是很好實現(xiàn)??梢钥紤]用自定義view,自定義view只需要不斷的重繪view,看起來和動畫沒有區(qū)別。
自定義view的話,我們來分析view的屬性:
- 圓點的半徑 (知道半徑,高度也就知道了)
- 圓之間的padding
- 圓點個數(shù)
- 圓形view
- 滾動view (下面稱呼為BarView)
大概就這么多,其他的不需要了。再來看3,4兩個view的屬性:
3.1 圓心
3.2 半徑
3.3 是否選中 (涉及到填充顏色)
BarView我們可以看成首尾兩個圓,中間一個矩形的組合體:
4.1 左邊圓的圓心和半徑
4.2 右邊圓的圓心和半徑 (半徑和左邊的圓一致)
4.3 矩形的寬度高度 (這個并不需要,已知左右圓,寬高都是可以求出來的)
全部需要的條件就這么多,接下來是代碼分析了。
三、代碼實現(xiàn)
首先建立兩個對象,一個圓點對象,一個滑塊對象:
/**
* 圓點對象
*/
public class PointView {
private int x;
private int y;
private int radius;
private boolean isChecked;
}
/**
* 滑塊對象
*/
public class BarView {
private int leftX;
private int leftY;
private int rightX;
private int rightY;
private int radius;
}
可以說,這兩個對象建立起來,這個view已經(jīng)完成一半了。(這里不一定一開始想的很清楚,后面可以逐漸的完善這兩個類)
首先當然繼承自view, 初始化工作:
public class IndicatorView extends View {
private Context mContext;
private Paint pointPaint;
private int childCount;
private Paint selectPointPaint;
private int selectPosition;
private int scrollPosition; //這個是滑動的時候 要到達的position
private float ratio;
private int pointSpace = 80; // 球之間的空隙
private int radius = 40; // 球半徑
private List<PointView> pointViews;
private BarView barView;
public IndicatorView(Context context) {
this(context, null);
}
public IndicatorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mContext = context;
pointPaint = new Paint();
pointPaint.setAntiAlias(true);
pointPaint.setColor(Color.GRAY);
selectPointPaint = new Paint();
selectPointPaint.setAntiAlias(true);
selectPointPaint.setColor(Color.parseColor("#eb1c42"));
}
}
這個時候需要繪制了,但是從哪里繪制呢?我們這個view是配合viewpager使用的,所以需要暴漏一個方法,設(shè)置一個viewpager進來:
/**
* 關(guān)聯(lián)viewpager
* @param viewPager
*/
public void setViewPager(ViewPager viewPager) {
childCount = viewPager.getAdapter().getCount();
initPoints();
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
scrollPosition = position;
Log.d("------->", "position:" + position + ", positionOffset:" + positionOffset + ", positionOffsetPixels:" + positionOffsetPixels);
ratio = positionOffset;
if(ratio >= 1 || ratio <= 0){
return;
}
compute();
invalidate();
}
@Override
public void onPageSelected(int position) {
selectPosition = position;
ratio = 0;
compute();
invalidate();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
ratio = 0;
compute();
invalidate();
}
當viewpager設(shè)置進來的時候,我們第一步拿到頁面的個數(shù),去initPoints()初始化小圓點:
/**
* 初始化點
*/
private void initPoints() {
pointViews = new ArrayList<>();
for (int i = 0; i < childCount; i++) {
PointView pointView = new PointView();
pointViews.add(pointView);
}
//順便初始化bar
barView = new BarView();
}
然后監(jiān)聽viewpager的滾動,在滾動里面去計算BarView的位置和設(shè)置圓點的選中狀態(tài)。
在監(jiān)聽方法里面,需要注意 scrollPosition 和 selectPosition 的區(qū)別,這個 scrollPosition 是頁面將要到達的頁面的 index,而 selectPosition 是記錄當前選中圓點的index。兩者的區(qū)別比較大,用處也不一樣。這個 scrollPosition 我們在后面還需要用到。
監(jiān)聽方法里面有個 compute() 計算方法:
/**
* 繪制之前準備, 這個計算一定要在canvas外面計算
*/
private void compute() {
//計算球位置
for (int i = 0; i < pointViews.size(); i++) {
PointView pointView = pointViews.get(i);
pointView.setRadius(radius);
pointView.setX(radius * (2 * i + 1) + pointSpace* i);
pointView.setY(radius);
pointView.setChecked(selectPosition == i);
}
//計算bar位置
PointView selectPointView = pointViews.get(selectPosition);
int selectX = selectPointView.getX();
int selectY = selectPointView.getY();
if(selectPosition <= scrollPosition){
//往右是增加右邊圓的圓心
barView.setLeftX(selectX);
barView.setLeftY(selectY);
barView.setRightX((int) (selectX + (2 * radius + pointSpace) * ratio));
barView.setRightY(selectY);
barView.setRadius(radius);
}else{
//往左是減少左邊圓的圓心
barView.setRightX(selectX);
barView.setRightY(selectY);
barView.setLeftX((int) (selectX - (2 * radius + pointSpace) * (1 - ratio)));
barView.setLeftY(selectY);
barView.setRadius(radius);
}
}
圓點(也就是小球)的位置很好計算,半徑圓心和選中狀態(tài),幾乎計算一遍就行。主要是BarView的位置,它是隨著viewpager滾動而改變的,viewpager滾動有向左和向右兩個
方向,這個時候 scrollPosition 就起到關(guān)鍵性的作用了,往右,這個 index 是大于當前 selectPosition 的,往左,這個 index 是小于或者等于(經(jīng)過打印是等于,嚴謹點,小于也判斷了) selectPosition 的,
那就有兩種計算方法,往右,左邊圓心等于選中圓的圓心,右邊圓心等于選中圓心加上偏移量,偏移量可以計算得出,代碼如上,往左,右邊圓心是等于選中圓的圓心,左邊圓心是選中圓心減去偏移量。
計算完調(diào)用 invalidate(),自動重繪頁面,調(diào)用 view 的 onDraw() 方法。接下來就是重點的onDraw()方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//畫圓點
for (PointView pointView : pointViews) {
canvas.drawCircle(pointView.getX(), pointView.getY(), pointView.getRadius(), pointView.isChecked()?selectPointPaint:pointPaint);
}
//畫bar的頭尾圓
canvas.drawCircle(barView.getLeftX(), barView.getLeftY(), barView.getRadius(), selectPointPaint);
canvas.drawCircle(barView.getRightX(), barView.getRightY(), barView.getRadius(), selectPointPaint);
//畫bar的中間rect
canvas.drawRect(new RectF(barView.getLeftX(), 0, barView.getRightX(), radius * 2), selectPointPaint);
}
是不是很簡單?為什么重點部分這么少代碼,因為計算已經(jīng)在外面計算過了,這里只需要把數(shù)據(jù)拿來繪制一下就好了。
到這是不是結(jié)束了,不,自定義view除了 onDraw() 這個關(guān)鍵方法還有 onMeaure() 沒有用到,如果不計算view的高度,默認是充滿屏幕的,就無法動態(tài)放置 view,所以最后:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//設(shè)置view寬高 不設(shè)置就是默認全屏view,沒辦法改變位置
if(pointViews != null && pointViews.size() > 0) {
int width = pointViews.get(pointViews.size() - 1).getX() + radius;
int height = radius * 2;
setMeasuredDimension(width, height);
}else{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
到這就結(jié)束了。當然還可以添加屬性,直接在xml文件就可以設(shè)置圓點 padding 和圓點半徑,這些就交給你去拓展了。附上 github地址