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,);
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,); //默认14sp(44px)
lineHeight= (int) ta.getDimension(R.styleable.PagerIndicator_lineHeight,); //默认6dp(18px)
lineProportion=ta.getFloat(R.styleable.PagerIndicator_lineProportion,f); //默认选项宽度的1/3
/*防止用户输入参数超出范围造成显示错误*/
if(lineProportion>){
lineProportion=;
}
if(lineProportion<){
lineProportion=;
}
ta.recycle();
wrapper=new LinearLayout(context);
paint=new Paint();
paint.setColor(lineColor);
}
boolean once;
int width,heigth;//控件宽度,控件高度
int childrenWidth=; //标题视图的宽度总和
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,):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 = ; i <titleList.size(); i++) {
int childWidth=titleList.get(i).length()*;
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().setTextColor(textCheckedColor);
addView(wrapper);
once=true;
}
setMeasuredDimension(widthSize,heightSize);//测量控件本体
}
int tempPosition=;
float goneWidth=;//保存当前标题之前的所有标题宽度,通过计算,确定当前小横条的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
wrapper.layout(,,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)*(-lineProportion)/
+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
positionOffset*childWidthList.get(position)+goneWidth;
top = heigth - DisplayUtil.px2dip(context,lineHeight);
bottom = heigth;
}else {
int sum=;
for (int i = ; i < position; i++) {
sum+=childWidthList.get(i);
}
goneWidth=sum;
if(position>tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/+
goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
goneWidth;
}else{
left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
}
/*
*这是当页面很多,标题宽度很大(超过屏幕了)时,将选定的标题居中显示的算法
*/
float scrollCenter=goneWidth+childWidthList.get(position)/;
if (scrollCenter>=width/){
smoothScrollTo((int) (scrollCenter-width/),);
}else {
smoothScrollTo(,);
}
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)*(-lineProportion)/
+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
positionOffset*childWidthList.get(position)+goneWidth;
top = heigth - DisplayUtil.px2dip(context,lineHeight);
bottom = heigth;
}else {
int sum=;
for (int i = ; i < position; i++) {
sum+=childWidthList.get(i);
}
goneWidth=sum;
if(position>tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/+
goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
goneWidth;
}else{
left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
}
/*
*这是当页面很多,标题宽度很大(超过屏幕了)时,将选定的标题居中显示的算法
*/
float scrollCenter=goneWidth+childWidthList.get(position)/;
if (scrollCenter>=width/){
smoothScrollTo((int) (scrollCenter-width/),);
}else {
smoothScrollTo(,);
}
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