前言
因为自己写的东西里面我想加入一个ViewPager,自然就要加入一个Indicator,本来是想找一个第三方控件,但是找了几个之后发现都不好,因为需要太多的东西,要添加好多的文件,非常的复杂,不适合我,所以就想自己写,但是关于自定义控件这块内容十分的薄弱,勉强能看懂都算是不错的了,但是还好我在最后看到了【张鸿洋】,他写了一个类似我要求的Indicator,但是不全符合。因为我需要的是横线的指示器,而他的是三角形,所以为了 能使用,我只能认真看源码了,完全搞懂之后再写自己的,算是二次开发吧。终于花费几个小时看懂了源码也重新写好了自己的Indicator,总共就一个类加上一个attr.xml文件比较简单。为了不忘记当时是怎么理解的,所以写这篇文章。
实现
【张鸿洋】效果图
【我】效果图
下面我就以解析源码的和添加自己代码的方式来说明。
先把源码贴上来:
public class ViewPagerIndicator extends LinearLayout {
//标题正常时候的颜色
private static final int COLOR_TEXT_NORMAL = Color.parseColor("#8B8E91");
//标题被选中时候的颜色
private static final int COLOR_TEXT_SELECT = Color.parseColor("#2E94FD");
//指示器的颜色
private static final int COLOR_INDICATOR = Color.parseColor("#2E94FD");
//默认可见的标题的数量
private static final int DEFAULT_VISIBLE_TAB_COUNT = 4;
private int mTextNormalColor = COLOR_TEXT_NORMAL;
private int mTextSelectColor = COLOR_TEXT_SELECT;
private int mIndicatorColor = COLOR_INDICATOR;
private int mVisibleTabCount = DEFAULT_VISIBLE_TAB_COUNT;
//指示器的上顶点,左顶点,宽度,高度
private int mTop,mLeft,mWidth,mHeight;
//标题的内容
private List<String> mTitles;
//与之绑定的ViewPager
private ViewPager mViewPager;
//绘制指示器的画笔
private Paint mPaint;
public ViewPagerIndicator(Context context) {
this(context, null);
}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
//获取自定义属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator);
mTextNormalColor = a.getColor(R.styleable.ViewPagerIndicator_title_normal_color,COLOR_TEXT_NORMAL);
mTextSelectColor = a.getColor(R.styleable.ViewPagerIndicator_title_select_color,COLOR_TEXT_SELECT);
mIndicatorColor = a.getColor(R.styleable.ViewPagerIndicator_indicator_color,COLOR_INDICATOR);
mHeight = (int)a.getDimension(R.styleable.ViewPagerIndicator_indicator_height,2);
//初始化画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mIndicatorColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
//绘制指示器
@Override
protected void dispatchDraw(Canvas canvas){
Rect rect = new Rect(mLeft,mTop-mHeight,mLeft+mWidth,mTop);
canvas.drawRect(rect, mPaint);
super.dispatchDraw(canvas);
}
//初始化指示器
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//获取指示器的宽度
mWidth = (int)(w/mVisibleTabCount);
//获取指示器的顶点
mTop = getMeasuredHeight();
}
//设置可见的标题的数量
public void setVisibleTabCount(int count){
mVisibleTabCount = count;
}
//设置标题的内容
public void setTitles(List<String> titles){
//如果标题不为空
if(titles!=null&&titles.size()>0){
mTitles = titles;
for (String title:mTitles){
addView(createTitleView(title));
}
setTitleClickEvent();
}
}
//设置标题的点击事件
private void setTitleClickEvent() {
int count = getChildCount();
for(int i=0;i<count;i++){
final int j = i;
View v = getChildAt(i);
v.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mViewPager.setCurrentItem(j);
}
});
}
}
//生成标题的TextView
private View createTitleView(String title) {
TextView tv = new TextView(getContext());
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
lp.width = getScreenWidth()/mVisibleTabCount ;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(mTextNormalColor);
tv.setText(title);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
tv.setLayoutParams(lp);
return tv;
}
//得到屏幕的宽度
public int getScreenWidth(){
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
//得到屏幕的高度
public int getScreenHeight(){
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
//对外的ViewPager的回调接口
public interface PagerChangeListener{
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels);
public void onPageSelected(int position);
public void onPageScrollStateChanged(int state);
}
//设置对外的ViewPager的回调接口
private PagerChangeListener onPagerChangeListener;
public void setViewPager(ViewPager viewPager,int pos){
mViewPager = viewPager;
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//LinearLayout滚动
scroll(position, positionOffset);
}
@Override
public void onPageSelected(int position){
//重置标题颜色
retTitleViewColor();
//设置标题被选中弄的颜色
setTitleSelectColor(position);
//回调
if (onPagerChangeListener != null) {
onPagerChangeListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
// 回调
if (onPagerChangeListener != null){
onPagerChangeListener.onPageScrollStateChanged(state);
}
}
});
mViewPager.setCurrentItem(pos);
// 设置标题被选中的颜色
setTitleSelectColor(pos);
}
//Linearlayout滚动,指示器滚动
private void scroll(int position, float positionOffset) {
//当标题移动到最后一个的时候Linearlayout开始滚动
if(positionOffset>0&&position>=(mVisibleTabCount-2)&&getChildCount()>mVisibleTabCount){
if(mVisibleTabCount!=1){
this.scrollTo((position - (mVisibleTabCount - 2)) * mWidth
+ (int) (mWidth * positionOffset), 0);
}else{
this.scrollTo(
position * mWidth + (int) (mWidth* positionOffset), 0);
}
}
mLeft = (int) ((position + positionOffset) * mWidth);
invalidate();
}
//设置被选中的标题的颜色
private void setTitleSelectColor(int position) {
View view = getChildAt(position);
if (view instanceof TextView){
((TextView) view).setTextColor(COLOR_TEXT_SELECT);
}
}
//重置标题的颜色
private void retTitleViewColor() {
for (int i = 0; i < getChildCount(); i++){
View view = getChildAt(i);
if (view instanceof TextView){
((TextView) view).setTextColor(COLOR_TEXT_NORMAL);
}
}
}
}
我们先新建一个类ViewPagerIndicator,它继承至LinearLayout,在布局文件里面我们只要把android:orientation属性设置成horizontal即可。
然后我们要新建一个attrs.xml文件,在里面声明自定义的属性。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ViewPagerIndicator">
<!-- 标题正常时候的颜色-->
<attr name="title_normal_color" format="color|reference" />
<!--标题被选中时候的颜色-->
<attr name="title_select_color" format="color|reference" />
<!--指示器的颜色-->
<attr name="indicator_color" format="color|reference" />
<!--指示器的高度-->
<attr name="indicator_height" format="dimension|reference" />
</declare-styleable>
</resources>
我声明了四个属性:
- title_normal_color 标题正常时候的颜色
- title_select_color 标题被选中时候的颜色
- indicator_color 指示器的颜色
- indicator_height 指示器的高度
我们要在布局文件里使用这四个属性即可,当然我也提供了默认值在类 里面,也可不使用这写属性,效果就是如效果图所示,可以根据自己的需要设置相应的属性。
声明完属性之后,我们就要在ViewPagerIndicator的构造方法里面得到这些属性的值。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator);
mTextNormalColor = a.getColor(R.styleable.ViewPagerIndicator_title_normal_color,COLOR_TEXT_NORMAL);
mTextSelectColor = a.getColor(R.styleable.ViewPagerIndicator_title_select_color,COLOR_TEXT_SELECT);
mIndicatorColor = a.getColor(R.styleable.ViewPagerIndicator_indicator_color,COLOR_INDICATOR);
mHeight = (int)a.getDimension(R.styleable.ViewPagerIndicator_indicator_height,2);
这样我们就得到了属性值,如果你没有使用属性,那么属性值就采用我提供的默认值。
在效果图中,我们可以看到,界面里可见的标题数量是4个,总共的标题数量是5个,这样显示是因为我提供的一个方法:
//设置可见的标题的数量
public void setVisibleTabCount(int count){
mVisibleTabCount = count;
}
该方法用来设置显示的标题数量为几个,同时:
//设置标题的内容
public void setTitles(List<String> titles){
//如果标题不为空
if(titles!=null&&titles.size()>0){
mTitles = titles;
for (String title:mTitles){
addView(createTitleView(title));
}
setTitleClickEvent();
}
}
这个方法用来设置标题的内容,传入的是一个List,根据List的内容,往LinearLayout里面添加相应的TextView作为标题显示,并给每一个TextView添加点击事件,点击哪一个标题,ViewPager就设置为那一页。
因为我们最主要的就是绘制那个矩形指示器,所以:
//绘制指示器
@Override
protected void dispatchDraw(Canvas canvas){
Rect rect = new Rect(mLeft,mTop-mHeight,mLeft+mWidth,mTop);
canvas.drawRect(rect, mPaint);
super.dispatchDraw(canvas);
}
我们覆盖dispatchDraw方法来绘制指示器,因为指示器是矩形,所以我们要绘制一个矩形。
初始时如下图所示:
指示器的Width为屏幕宽度除以可见的标题的数量,高度需要自定义属性设置,top为整个LinearLayout的高度,left在开始为0,后来随着滑动,left在不断的更改,其他的都不变。
然后在滑动的时候不断的调用dispatchDraw就可以实现指示器的绘制,然后就是我们的效果。
因为我们要监听ViewPager的滑动,所以我们需要传入一个ViewPager对象。
//对外的ViewPager的回调接口
public interface PagerChangeListener{
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels);
public void onPageSelected(int position);
public void onPageScrollStateChanged(int state);
}
//设置对外的ViewPager的回调接口
private PagerChangeListener onPagerChangeListener;
public void setViewPager(ViewPager viewPager,int pos){
mViewPager = viewPager;
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//LinearLayout滚动
scroll(position, positionOffset);
}
@Override
public void onPageSelected(int position){
//重置标题颜色
retTitleViewColor();
//设置标题被选中弄的颜色
setTitleSelectColor(position);
//回调
if (onPagerChangeListener != null) {
onPagerChangeListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
// 回调
if (onPagerChangeListener != null){
onPagerChangeListener.onPageScrollStateChanged(state);
}
}
});
我们传入ViewPager的对象之后,要设置ViewPager的setOnPageChangeListener方法来监听滑动,onPageScrolled这个方法就是在滑动的过程中不断的进行调用,其中的参数positionOffset代表滑动过程中的偏移量的百分比,如果ViewPager已经滑动完毕到下一个页面,那么positionOffset的值为1,没有滑动的时候为0,我们需要得到这个值来进行我们指示器的滑动,做到和ViewPager一起滑动的效果。而方法onPageSelected表示已经滑动完毕,这是我们需要做的工作就是显示正确的标题,我们先把所有的标题的颜色重置为正常时候的颜色,然后将选中的页面的标题设置为选中的颜色。
因为我们要实现指示器随ViewPager一起滑动,所以我们要生成一个方法,这里我创建了scroll方法,得到positionOffset参数值,从效果图中我们可以看到,不仅指示器要滑动,当滑动到可见标题的最后一个的时候,LinearLayout也要随之滑动,所以这里我们就要做处理判断:
//Linearlayout滚动,指示器滚动
private void scroll(int position, float positionOffset) {
//当标题移动到最后一个的时候Linearlayout开始滚动
if(positionOffset>0&&position>=(mVisibleTabCount-1)&&getChildCount()>mVisibleTabCount){
if(mVisibleTabCount!=1){
this.scrollTo((position - (mVisibleTabCount - 1)) * mWidth
+ (int) (mWidth * positionOffset), 0);
}else{
this.scrollTo(
position * mWidth + (int) (mWidth* positionOffset), 0);
}
}
mLeft = (int) ((position + positionOffset) * mWidth);
invalidate();
}
然后我们重新设置mLeft = (int) ((position + positionOffset) * mWidth);之后调用invalidate方法来刷新页面,就实现了指示器的滑动。
好了,一个简单的改造Indicator就完成了。
小结
本来我是打算直接用【张鸿洋】的这个Indicator,但是,突然醒悟到,只是用别人的东西,那么东西始终都是别人的,如果自己可以搞懂,那么东西就变成自己的了所以如果能自己做出来就千万不要用别人的东西,当然还有一个说法叫”不重复造轮子“,没错,当进行大开发的时候使用第三方的东西确实能省不少时间,但是如果你还是在学习阶段的话,就少用第三方的东西,把基础打扎实再说,只会用第三方东西的话对以后来说未必是好事。
【源码下载】
【补充】
因为我是在模拟机上运行的,运行结果没有问题所以我以为没有Bug,等我自己用到实际手机的时候出现了错误,现在我把错误改改过来。错误之处在于函数dispatchDraw,就是绘制指示器的函数,我们要绘制的指示器是一个矩形,我把矩形的那四个坐标值搞混了,导致绘制不出来矩形在真机上。
可以看到,我们传入的四个值,其中top要不大于bottom,这个需要注意一下,我已经把代码改正了过来。
【修改版源码下载】