自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)

一.概述

今天主要来分享个自定义viewpagerindicator,效果主要是仿 猫眼电影 顶部的栏目切换,也就是我们常说的indicator,难度简单,为了让滑动时效果更炫酷,我在滑动到左边第一个item或者最右边的item时,添加了滑动到边缘位置后,回弹然后复位的效果(其实也是很简单,只要计算好距离就好啦)
大致的效果图就是这样。大家可以凑合看看(可以看到当滑动到边缘位置的时候有回弹的效果,是不是挺带感的O(∩_∩)O)
这里写图片描述 这里写图片描述

二.使用方法

  1. layout布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red">
<mr_immortalz.com.viewpagerindicator.ViewPagerIndicator
android:id="@+id/indicator"
android:layout_width="200dp"
android:layout_height="36dp"></mr_immortalz.com.viewpagerindicator.ViewPagerIndicator>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v4.view.ViewPager>
</LinearLayout>

2.MainActivity使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private ViewPagerIndicator indicator;
private FragmentPagerAdapter mAdapter;
private List<Fragment> mList;
private List<String> mDatas;
private int itemCount = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.vp);
indicator = (ViewPagerIndicator) findViewById(R.id.indicator);
mList = new ArrayList<Fragment>();
for (int i = 0; i < itemCount; i++) {
Fragment fragment = new MeFragment();
mList.add(fragment);
}
mDatas = new ArrayList<>();
for (int i = 0; i < itemCount; i++) {
mDatas.add("i=" + i);
}
mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return mList.get(position);
}
@Override
public int getCount() {
return mList.size();
}
};
viewPager.setAdapter(mAdapter);
//将viewpager与indicator绑定
indicator.setDatas(mDatas);
indicator.setViewPager(viewPager);
}
}

3.自定义ViewpagerIndicator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
public class ViewPagerIndicator extends LinearLayout {
private ViewPager mViewPager;
private int width;
private int height;
private int visibleItemCount = 3;
private int itemCount = 3;
//绘制框框
private Paint paint;
private float mWidth = 0;
private float mHeight = 0;
private float mLeft = 0;
private float mTop = 0;
private float radiusX = 10;
private float radiusY = 10;
private int mPadding = 8;
private List<String> mDatas;
private boolean isSetData = false;
private Context context;
private int currentPosition;
private boolean isAutoSelect = false;//判断是否进行切换
private float rebounceOffset;
public ViewPagerIndicator(Context context) {
super(context);
this.context = context;
init();
}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init() {
this.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg));
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(getResources().getColor(R.color.white));
paint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
mWidth = width / visibleItemCount;
mHeight = height;
LogUtil.m("width " + width + " height " + height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//添加TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
protected void onDraw(Canvas canvas) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect需要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
//ogUtil.m();
super.dispatchDraw(canvas);
}
public void setViewPager(ViewPager viewpager, int position) {
this.mViewPager = viewpager;
this.currentPosition = position;
if (mViewPager != null) {
viewpager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//当移动的是最左边item
if (isAutoSelect && currentPosition == 0) {
//滑动手松开时,让最左边(即第一个)item滑动到左边缘位置
if (positionOffset > rebounceOffset / 2) {
mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth;
} else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) {
//让最左边(即第一个)item 向右回弹一部分距离
mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12;
} else {
//让最左边(即最后一个)item 向左回弹到边缘位置
mLeft = (position + positionOffset) * mWidth * 6 / 12;
}
invalidate();
} else if (isAutoSelect && currentPosition == itemCount - 1) {
//当移动的是最右边(即最后一个)item
//滑动手松开时,让最右边(即最后一个)item滑动到右边缘位置
if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) {
//
mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth;
//当item数大于visibleItem可见数,本控件(本质LinearLayout)才滚动
if (visibleItemCount < itemCount) {
scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0);
}
if ((mLeft + mWidth) > (getChildCount() * mWidth)) {
//当(mLeft + mWidth)大于最边缘的宽度时,设置
mLeft = (itemCount - 1) * mWidth;
}
} else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) {
//让最右边(即最后一个)item 向左回弹一部分距离
//当item数大于visibleItem可见数,且本控件未滚动到指定位置,则设置控件滚动到指定位置
if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) {
scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0);
}
mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12;
} else {
//让最右边(即最后一个)item 向右回弹到边缘位置
//因为onPageScrolled 最后positionOffset会变成0,所以这里需要判断一下
//当positionOffset = 0 时,设置mLeft位置
if (positionOffset != 0) {
mLeft = (position + 1) * mWidth - (1.0f - positionOffset) * mWidth * 7 / 12;
if (mLeft > (itemCount - 1) * mWidth) {
mLeft = (itemCount - 1) * mWidth;
}
} else {
mLeft = (itemCount - 1) * mWidth;
}
}
invalidate();
} else {
//当移动的是中间item
scrollTo(position, positionOffset);
rebounceOffset = positionOffset;
}
setTitleColor();
}
@Override
public void onPageSelected(int position) {
LogUtil.m("position " + position);
currentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
LogUtil.m("state " + state);
if (state == 2) {
//当state = 2时,表示手松开,viewpager开启动画自动滑动
isAutoSelect = true;
}
if (state == 0) {
//当state = 0时,表示viewpager滑动停止
isAutoSelect = false;
}
}
});
}
}
public void setViewPager(ViewPager viewpager) {
setViewPager(viewpager, 0);
}
/**
* 正常滑动
* @param position
* @param positionOffset
*/
private void scrollTo(int position, float positionOffset) {
if (visibleItemCount < itemCount) {
if (positionOffset > 0 && position > (visibleItemCount - 2)) {
this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0);
}
}
mLeft = (position + positionOffset) * mWidth;
invalidate();
}
/**
* 设置字体颜色
*/
private void setTitleColor() {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
if (i == currentPosition) {
((TextView) getChildAt(currentPosition)).setTextColor(getResources().getColor(R.color.font_red));
} else {
((TextView) getChildAt(i)).setTextColor(getResources().getColor(R.color.font_white));
}
}
}
}
/**
* 设置内容数据
*
* @param mDatas
*/
public void setDatas(List<String> mDatas) {
this.isSetData = true;
this.mDatas = mDatas;
this.itemCount = mDatas.size();
if (itemCount < visibleItemCount) {
visibleItemCount = itemCount;
}
}
}

三.代码分析

很明显,核心代码在ViewPagerIndicator中,因为代码中已经对每个函数方法给出了注释,下面说下大体思路。

1.首先init(),onMeasure中对paint,width,height等必不可少的数据进行获取。
2.因为整个indicator是继承自linearlayout,对于里面的文字展示,用textview来显示,因为不知道用户使用的时候到底有多少个item,所以在setDatas()方法中对textview数目进行绑定。然后在onSizeChanged中动态生成需要的textview数目(isSetData用来控制是否绑定了数据,绑定了的话,需要将之前所有生成的全部清空)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//添加TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}
}

只所以在onsizechanged中动态添加,是因为该方法会在ondraw前,onMeasure方法后回调,这样就保证我们能获取到需要的width,height。
这里写图片描述
3.Ok,现在获取到需要绘制的数目后接下来就是绘制白色背景框框啦。

1
2
3
4
5
6
7
8
9
10
11
12
protected void onDraw(Canvas canvas) {
LogUtil.m();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect需要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}
}

很好理解,不解释`(∩_∩)′
4.接下来,最最关键的就是setViewPager()这个方法。
为了方便理解,大家可以看看
onPageScrolled,
onPageSelected,
onPageScrollStateChanged,
这三个方法滑动时,具体回调顺序。