Android 自制ViewPager的指示器PagerIndicator
2017-01-14 15:05
411 查看
1.实际效果
由于之前用到了一个比较老的类似的指示器框架,发现它功能并不完善(发现显示层是用图片来做的,感觉好呆板的控件),而且UI很丑(使用蓝色png图片填充的,为了搭配我的app色调,特意用ps把原图片素材全改为白色了[]~( ̄▽ ̄)~*),就如下面我之前做的App上展示的那样。后来发现Bilibili客户端的效果不错,稍微思考一下发现原理不难,就尝试着自己写一个,终于成品出来了!从左到右依次是纯粹菜谱,Bilibili客户端,本案例demo
2.原理机制
完成这个自定义ViewGroup时遇到了不少bug,不过主要是逻辑方面的bug,认真检查后一一排除问题了。制作类似的控件要解决的主要是滑动ViewPager时指示器的显示问题,这个需要很细致清晰的逻辑,然后是对自定义View各个内部方法调用顺序和机制的熟练程度。
整个PagerIndicator运行原理:根据传入的ViewPager,调用其监听方法,根据其状态动态绘制标题下方的小横条(矩形)。当用户滑动页面时,小横条会按比例移动相应的距离;当滑动结束时,小横条固定在指定的位置
下面贴出PagerIndicator源码:
package chen.capton.custom; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.List; import static android.content.ContentValues.TAG; /** * Created by CAPTON on 2017/1/13. */ public class PagerIndicator extends HorizontalScrollView implements View.OnClickListener{ private Context context; private int textColor; //标题字体的颜色 private int textCheckedColor; //选中时标题字体的颜色 private int textSize; //标题字体大小 private int lineColor; //横条颜色 private int lineHeight; //横条厚度(高度) private float lineProportion; //横条占比(宽度)0.0~1.0 private ViewPager viewPager; //保存传进来的ViewPager private List<TextView> textViewList=new ArrayList<>();//显示各个标题的TextView集合 private List<String> titleList;//标题字符串ArrayList private LinearLayout wrapper; //由于控件继承自HorizontalView,其直系子View必须是唯一的LinearLayout。 public ViewPager getViewPager() {return viewPager;} //返回值为控件本体对象,方便用户链式调用,下同 public PagerIndicator setViewPager(ViewPager viewPager) { this.viewPager = viewPager; return this; } public List<String> getTitleList() {return titleList;} public PagerIndicator setTitleList(List<String> titleList) { this.titleList = titleList; return this; } public int getTextColor() {return textColor;} public PagerIndicator setTextColor(int textColor) { this.textColor = getResources().getColor(textColor); invalidate(); return this; } public int getTextSize() {return textSize;} public PagerIndicator setTextSize(int textSize) { this.textSize = DisplayUtil.sp2px(context,textSize);; invalidate(); return this; } public int getTextCheckedColor() {return textCheckedColor;} public PagerIndicator setTextCheckedColor(int textCheckedColor) { this.textCheckedColor = getResources().getColor(textCheckedColor); invalidate(); return this; } public int getLineColor() {return lineColor;} public PagerIndicator setLineColor(int lineColor) { this.lineColor = getResources().getColor(lineColor); paint.setColor(this.lineColor); invalidate(); return this; } public int getLineHeight() {return lineHeight;} public PagerIndicator setLineHeight(int lineHeight) { this.lineHeight = DisplayUtil.dip2px(context,lineHeight); invalidate(); return this; } public float getLineProportion() {return lineProportion;} public PagerIndicator setLineProportion(float lineProportion) { this.lineProportion = lineProportion; invalidate(); return this; } public PagerIndicator(Context context) { this(context,null); } public PagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);} public PagerIndicator(Context context, AttributeSet attrs) { this(context, attrs,0); this.context=context; TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.PagerIndicator); textColor=ta.getColor(R.styleable.PagerIndicator_textColor,getResources().getColor(R.color.pager_indicator_black)); //默认标题字体为黑色 textCheckedColor=ta.getColor(R.styleable.PagerIndicator_textCheckedColor,getResources().getColor(android.R.color.white)); //默认选中标题为白色 lineColor=ta.getColor(R.styleable.PagerIndicator_lineColor,getResources().getColor(android.R.color.white)); //默认横线为白色 textSize= (int) ta.getDimension(R.styleable.PagerIndicator_textSize,44); //默认14sp(44px) lineHeight= (int) ta.getDimension(R.styleable.PagerIndicator_lineHeight,18); //默认6dp(18px) lineProportion=ta.getFloat(R.styleable.PagerIndicator_lineProportion,0.33f); //默认选项宽度的1/3 /*防止用户输入参数超出范围造成显示错误*/ if(lineProportion>1){ lineProportion=1; } if(lineProportion<0){ lineProportion=0; } ta.recycle(); wrapper=new LinearLayout(context); paint=new Paint(); paint.setColor(lineColor); } boolean once; int width,heigth;//控件宽度,控件高度 int childrenWidth=0; //标题视图的宽度总和 List<Integer> childWidthList=new ArrayList<>(); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSize=MeasureSpec.getSize(widthMeasureSpec); int heightSize=MeasureSpec.getSize(heightMeasureSpec); int heightMode=MeasureSpec.getMode(heightMeasureSpec); //当用户设置本空间高度为“wrap_content”时,默认高度设置为40dp,其他情况则是实际高度 heightSize=heightMode==MeasureSpec.AT_MOST?DisplayUtil.dip2px(context,40):heightSize; width=widthSize; heigth=heightSize; while (!once) { LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(widthSize, heightSize); lp2.gravity=LinearLayout.HORIZONTAL; wrapper.setLayoutParams(lp2); //根据传进来的标题ArrayList动态添加子View(标题项) for (int i = 0; i <titleList.size(); i++) { int childWidth=titleList.get(i).length()*60; childWidthList.add(childWidth); TextView textView=new TextView(context); textView.setText(titleList.get(i)); textView.setTextSize(DisplayUtil.px2sp(context,textSize)); textView.setTextColor(textColor); textView.setGravity(Gravity.CENTER); LayoutParams lp3=new LayoutParams(childWidth, heightSize-DisplayUtil.px2dip(context,lineHeight)); textView.setLayoutParams(lp3); //设置Tag,用于标识此TextView对象,当点击标题时,根据此Tag的i值,ViewPager将跳转至页面i。 textView.setTag(i); textView.setOnClickListener(this); childrenWidth+=childWidth; textViewList.add(textView); wrapper.addView(textView); } textViewList.get(0).setTextColor(textCheckedColor); addView(wrapper); once=true; } setMeasuredDimension(widthSize,heightSize);//测量控件本体 } int tempPosition=0; float goneWidth=0;//保存当前标题之前的所有标题宽度,通过计算,确定当前小横条的位置 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { wrapper.layout(0,0,childrenWidth,heigth);//布局直系唯一子View,参数 r 必须等于其子View的宽度之和,如果等于父控件宽度,则当子View宽度大于父控件时,多出部分的子View没法显示,也没有拖动效果。 viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* *核心算法部分,当手指滑动屏幕时页面跟着滑动,指示器的小横条跟着移动;停止滑动时,小横条显示在正确的位置上。(ps:这部分算法因人而异,我相信大家可以写出更简洁高效的代码,就不多做注释了) */ if(position==tempPosition) { left = childWidthList.get(position)*(1-lineProportion)/2 +positionOffset*childWidthList.get(position)+goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+ positionOffset*childWidthList.get(position)+goneWidth; top = heigth - DisplayUtil.px2dip(context,lineHeight); bottom = heigth; }else { int sum=0; for (int i = 0; i < position; i++) { sum+=childWidthList.get(i); } goneWidth=sum; if(position>tempPosition) { left = childWidthList.get(position)*(1-lineProportion)/2+ goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+ goneWidth; }else{ left = childWidthList.get(position)*(1-lineProportion)/2+positionOffset*childWidthList.get(position)+goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+positionOffset*childWidthList.get(position)+goneWidth; } /* *这是当页面很多,标题宽度很大(超过屏幕了)时,将选定的标题居中显示的算法 */ float scrollCenter=goneWidth+childWidthList.get(position)/2; if (scrollCenter>=width/2){ smoothScrollTo((int) (scrollCenter-width/2),0); }else { smoothScrollTo(0,0); } tempPosition=position; } invalidate(); } @Override public void onPageSelected(int position) { for(TextView textview:textViewList){ textview.setTextColor(textColor); } textViewList.get(position).setTextColor(textCheckedColor); invalidate(); } @Override public void onPageScrollStateChanged(int state) {} }); } Paint paint; //绘制小横条的画笔 float left,top,right,bottom;//绘制小横条(矩形)的四个边界 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(left,top,right,bottom,paint); } @Override public void onClick(View v) { if(v instanceof TextView){ //根据之前动态初始化TextView时设置的Tag(int类型),跳转页面 viewPager.setCurrentItem((Integer) v.getTag()); } } }
在ViewGroup中,onMeasure,onLayout,onDraw三个方法依次调用,其中onDraw方法在用户触摸或者拖动控件时会调用多次,当调用invalidate方法时,onDraw也会被调用。所以有了我们重绘控件的契机,即调用传进来的ViewPager对象的setOnPageChangeListener(new ViewPager.OnPageChangeListener() {})方法,通过在添加的OnPageChangeListener监听器中根据ViewPager的滑动状态来同步重绘我们的PagerIndicator控件的小横条,例如:
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* *核心算法部分,当手指滑动屏幕时页面跟着滑动,指示器的小横条跟着移动;停止滑动时,小横条显示在正确的位置上。(ps:这部分算法因人而异,我相信大家可以写出更简洁高效的代码,就不多做注释了) */ if(position==tempPosition) { left = childWidthList.get(position)*(1-lineProportion)/2 +positionOffset*childWidthList.get(position)+goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+ positionOffset*childWidthList.get(position)+goneWidth; top = heigth - DisplayUtil.px2dip(context,lineHeight); bottom = heigth; }else { int sum=0; for (int i = 0; i < position; i++) { sum+=childWidthList.get(i); } goneWidth=sum; if(position>tempPosition) { left = childWidthList.get(position)*(1-lineProportion)/2+ goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+ goneWidth; }else{ left = childWidthList.get(position)*(1-lineProportion)/2+positionOffset*childWidthList.get(position)+goneWidth; right = childWidthList.get(position)*(1+lineProportion)/2+positionOffset*childWidthList.get(position)+goneWidth; } /* *这是当页面很多,标题宽度很大(超过屏幕了)时,将选定的标题居中显示的算法 */ float scrollCenter=goneWidth+childWidthList.get(position)/2; if (scrollCenter>=width/2){ smoothScrollTo((int) (scrollCenter-width/2),0); }else { smoothScrollTo(0,0); } tempPosition=position; } invalidate(); } @Override public void onPageSelected(int position) { for(TextView textview:textViewList){ textview.setTextColor(textColor); } textViewList.get(position).setTextColor(textCheckedColor); invalidate(); } @Override public void onPageScrollStateChanged(int state) {} }); } Paint paint; //绘制小横条的画笔 float left,top,right,bottom;//绘制小横条(矩形)的四个边界 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(left,top,right,bottom,paint); } @Override public void onClick(View v) { if(v instanceof TextView){ //根据之前动态初始化TextView时设置的Tag(int类型),跳转页面 viewPager.setCurrentItem((Integer) v.getTag()); } } }
3.具体用法
详情请见 5.相关文件4.作者的话
对于自定义View与自定义ViewGroup我还不是特别熟悉,很多没见过的用法和函数都没深入去涉猎,例如专注界面绘制的Canvas,Paint,Drawable等等。希望看到我文章的大触们多多指教,对于那些看不懂这片代码的同学,建议先去掌握好自定义View(ViewGroup)的基础再来吐槽吧。5.相关文件
GitHub链接:https://github.com/Ccapton/pagerIndicator相关文章推荐
- android 中 viewpager 滑动的指示器
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android Viewpager界面指示器案例
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android 自定义的颜色滑动转换ViewPager指示器 ColorTransformIndicator
- android 中 viewpager 滑动的指示器
- [Android]最省内存的ViewPager添加小圆点指示器
- Android ViewPager等自制图片轮播器
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- android: 带很多tab的指示器的ViewPager
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器 .
- Android ViewPager 点击或滑动时指示器文字渐变、光标跟随
- 开源项目推荐(1):Android-ViewPagerIndicator 分页指示器,实现左右滑分页视图
- 打造Android 最实用的ViewPager 指示器控件
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
- Android 之一个很好的Viewpager滑动指示器