好久没有更新过博客了,趁今天有空分享一个导航栏的自定义控件。有关此控件的demo相信在网上已经烂大街了,一搜一大把。
我现在只着重分享一些我认为比较难理解的知识点。整个控件的难点大概有三个
1、游标的绘制。
2、ViewPager监听器的理解。
3、游标的移动。
本文将注重这三个方面重点分析。
先上Demo的最终效果
样例Module,有四个java文件和两个xml文件
总结一下此控件的主要知识点
1、ViewGroup绘制流程。
2、ViewPager的用法。
3、OnPageChangeListener接口的用法。
4、scrollTo方法的使用。
需要完整代码,请看底部链接,谢谢!(^_^)。下面我直接讲核心代码。
(1)SlideTab继承了HorizontalScrollView控件之后,咋们需要重写onDraw方法。接下来需要看个图了解SlideTab控件的内部组成
整个SlideTab控件就是由这三个类型的控件组成。假设SlideTab控件已经初始化完成了。第一次由系统开始调用onDraw方法。
/**
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取当前Tab的左右两边的横坐标值
View currentTabView = horizontalContainer.getChildAt(currentPosition);
float currentTabLeftX = currentTabView.getLeft();
float currentTabRightX = currentTabView.getRight();
int childCount = horizontalContainer.getChildCount();
//ViewPager在滑动的过程中会重复调用onDraw方法。下面if语句的内容是用来计算游标的起点坐标和终点坐标
if (currentPositionOffset > 0f && currentPosition < childCount - 1) {
//获取下一个Tab左右两边的横坐标值
View nextTab = horizontalContainer.getChildAt(currentPosition + 1);
float nextTabLeftX = nextTab.getLeft();
float nextTabRightX = nextTab.getRight();
//计算起点
currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset)
* currentTabLeftX);
//计算终点
currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset)
* currentTabRightX);
}
//绘制下划线
// drawUnderline(canvas, horizontalContainer);
//绘制指示器
drawIndicator(canvas, currentTabLeftX, currentTabRightX);
}
currentPosition变量的初始值为0。
第8行代码View currentTabView = horizontalContainer.getChildAt(currentPosition);获取到LinearLayout容器里面第一个View(实际是TextView)视图。
第9,10行分别得到View视图的左上角坐标和右上角坐标。
第11行获取LinearLayout容器中TextView控件的总数。
第13行的判断语句。currentPositionOffset 这是一个记录着当前页面滑动过程中的偏移量(如果不理解先放下,后面再讲)初始值为0,很明显,currentPositionOffset 不大于 0f,if语句不成立,略过(if语句里面的内容稍后再分析)。继续往下走。
来到底28行执行drawIndicator方法,开始绘制游标。
(2)现在来解决此控件的第一个难点,游标的绘制。
这个游标实际上是一条线。要画一条先就必须得确定两点的坐标值(初中知识,两点才能确定一条直线嘛)。
/**
* 绘制游标
*
* @param canvas SlideTab控件的画板
* @param currentLeftX 标题控件的左坐标
* @param currentRightX 标题控件的右坐标
*/
private void drawIndicator(Canvas canvas, float currentLeftX, float currentRightX) {
float indicatorMiddle = (indicatorPaint.getStrokeWidth() / 2);
float indicatorY = getHeight() - indicatorMiddle;
canvas.drawLine(currentLeftX, indicatorY, currentRightX, indicatorY, indicatorPaint);
}
为了更好的理解这几段代码,还是画个图。下图黑色框是LinearLayout容器,绿色框是游标。
绿色框高度的值就是indicatorPaint.getStrokeWidth()。接下的工作就是计算下图左右两边的橙色点。
红色横线表示currentLeftX变量。
蓝色横线表示currentRightX变量。
红色竖线表示indicatorMiddle变量。
粉色竖线表示indicatorY变量。
由第9,10行的计算方法得到
左边橙色坐标点(currentLeftX,indicatorY)。
右边橙色坐标点(currentRightX,indicatorY)。
在第11行调用canvas.drawLine方法(这是一个绘制直线的方法)绘制游标。这个方法的最后的参数indicatorPaint是一个画笔(用来描述这个直线的状态,例如颜色,宽度,直线末端是否圆角等等。)
以上的内容就是游标绘制的流程。
(3)绘制好游标之后如何让导航栏控件跟随着ViewPager的滑动而滑动呢?现在我们需要写一个setViewPager方法,将(Fragment+ViewPager)与SlideTab控件关联起来。这个方法是提供给用户(使用你控件的程序猿)调用。他们只需要传来一个ViewPager的实例和一个标题名称数组即可完成此控件的调用。
/**
* @param viewPager 用户传进来的ViewPager
* @param titleString 标题名称
*/
public void setViewPager(ViewPager viewPager, String[] titleString) {
this.viewPager = viewPager;
addTab(titleString);
//设置ViewPager监听事件
viewPager.addOnPageChangeListener(new SlideTabPageViewListener());
}
/**
* 添加Tab
*
* @param titleString 标题数组
*/
private void addTab(String[] titleString) {
//清空所有控件
horizontalContainer.removeAllViews();
for (int i = 0; i < viewPager.getAdapter().getCount(); i++) {
//创建垂直容器,用来包裹住下面TextView
tabVerticalContainer = new LinearLayout(context);
tabVerticalContainer.setOrientation(LinearLayout.VERTICAL);
tabVerticalContainer.setHorizontalGravity(Gravity.CENTER_HORIZONTAL);
//设置点击事件
tabVerticalContainer.setOnClickListener(new ViewPagerClickListener(i));
tabVerticalContainer.setVerticalGravity(Gravity.CENTER_VERTICAL);
//将垂直LinearLayout容器放入水平LinearLayout容器中
horizontalContainer.addView(tabVerticalContainer, isExtendTab ? expandedTabLayoutParams
: defaultTabLayoutParams);
if (titleString != null) {
//创建标题
TextView textViews = new TextView(context);
textViews.setText(titleString[i]);
textViews.setTextSize(14);
textViews.setTextColor(Color.parseColor("#000000"));
textViews.setSingleLine(true);
tabVerticalContainer.addView(textViews, textViewLayoutParams);
}
}
}
第8行是一个自定义方法。根据ViewPager的页面总数,设置标题导航栏。逻辑比较简单只是单纯的堆代码,咋们略过吧。
我们重点关注第10行代码。ViewPager注册了一个监听事件的实例。此实例有三个回调方法用来监听用户对屏幕的滑动操作。具体详情请往下看,(SlideTab控件的游标滑动与这个监听事件有很大关系。)
(4)SlideTabPageViewListener是SlideTab控件的内部类,实现了ViewPager.OnPageChangeListener接口,这个接口必须实现3个方法。
现在来解决第2个难点。就是ViewPager的监听事件。
public void onPageScrolled(int position, float positionOffset,int positionOffsetPixels)
当你滑动页面的时候会调用此方法,在滑动停止之前,此方法回一直被调用。
position表示当前页面的下标。例如你有三个选项卡,现在从第一页滑动到第二页的过程中,这个position的下标是0(下标从0开始),滑动到第二页position的时候下标就变成1了。
positionOffset表示当前页面偏移的百分比。这个参数我们待会就会用到。
positionOffsetPixels表示当前页面偏移的像素,一般情况不用。
public void onPageScrollStateChanged(int state)
当ViewPager页面的状态被改变的时候会调用此方法。怎么理解这句话呢?
1、假如你触摸屏幕从第一页滑动到第二页这个过程中,会回调此方法(如果你手指一直处于滑动状态此方法就会一直被调用),传过来的state的值是1表示正在滑动。
2、滑动结束之后,会再次回调此方法,传过来的state的值是2表示滑动结束了。
3、结束滑动之后如果没有其他的滑动操作,会再次回调此方法,传过来的state的值是0,表示ViewPager处理闲置状态。
public void onPageSelected(int position)
此方法是从当前页面滑动到另一个页面才会调用,并且这个position是新页面的下标。注意如果你从当前页往下一页滑动的过程中(不松手)又滑回原页面,此方法不会调用。
以上关于ViewPager的OnPageChangeListener接口的方法详情,有了这些基础之后就比较好解释SlideTabPageViewListener内部类。
(5)SlideTabPageViewListener内部类
/**
* ViewPager滚动监听事件
*/
public class SlideTabPageViewListener implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
currentPosition = position;
currentPositionOffset = positionOffset;
scrollToCurrentPosition(position, (int) (positionOffset * (horizontalContainer)
.getChildAt(position).getWidth()));
//重新绘制onDraw方法
invalidate();
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onPageSelected(int position) {
}
}
最后,咋们来解决此控件的最后一个难题,如何控制游标的移动。这是我感觉最难讲清楚的一部分。
假设咋们正在触摸屏幕从第一页滑动到第二页,在这个过程中。以上的onPageScrolled方法会一直被调用。
第8,9行的代码是更新当前最新的页面下标(currentPosition )和页面的偏移值(currentPositionOffset )。这两个变量咋们已经在上面的onDraw方法中见过。
第10行调用scrollToCurrentPosition方法(这个方法很重要)。将当前的页面下标(值为0)和当前View(TextView)的偏移值传递过去。具体代码如下。
/*****
*
*
* @param position
* @param offset
*/
public void scrollToCurrentPosition(int position, int offset) {
int currentOffsetX = horizontalContainer.getChildAt(position).getLeft() + offset;
int startScrollX = currentOffsetX;
if (position > 0 || offset > 0) {
//remainOffset表示剩余偏移量
startScrollX = currentOffsetX - remainOffset;
}
//如果位移发生变化,则滑动
if (startScrollX != lastScrollX) {
//更新最后一次滑动的距离
lastScrollX = startScrollX;
//horizontalContainer控件开始滑动
scrollTo(startScrollX, 0);
}
}
第11行的语句成立。计算得到startScrollX这是horizontalContainer 容器实际的滑动偏移值。
第17行lastScrollX默认初始值为0,因此if语句也成立。
最终在21行开始滑动horizontalContainer容器。(需要注意的是如果startScrollX的值大于0则往左滑动,小于0往右滑动。)
紧接着调用invalidate()方法,其内部代码又会回调咋们刚才所说的onDraw方法。在onDraw方法中,执行了前面的代码后来到了刚才没有讲解的if语句,由于此时处于滑动状态。currentPositionOffset和currentPosition的值肯定是成立的。
(6)我再贴一下onDraw方法中if语句的代码。
if (currentPositionOffset > 0f && currentPosition < childCount - 1) {
//获取下一个Tab左右两边的横坐标值
View nextTab = horizontalContainer.getChildAt(currentPosition + 1);
float nextTabLeftX = nextTab.getLeft();
float nextTabRightX = nextTab.getRight();
//计算起点
currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset)
* currentTabLeftX);
//计算终点
currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset)
* currentTabRightX);
}
在第3行,由于滑动没有结束,此时currentPosition 的值还是0,又因为currentPosition + 1,所以nextTab得到的是第二个TextView的值。
第4,5行获取nextTab控件(实际就是TextView)左右两边的坐标。
第7,10行计算滑动过程中游标的起点和终点,也就是下图中左右两边的红点的坐标。
计算完毕之后。最后就是再次调用drawIndicator方法重新绘制游标。
至此有关SlideTab控件与ViewPager滑动的流程就走完了。谢谢(^_^)Y
鉴于篇幅的关系,我就不再演示控件的使用了。各位可以下载下面的demo看看源码。demo中的SlideTabDemonstration类是入口。
此demo有BUG在所难免。仅限于学习。希望能帮到各位。
Demo源码请戳这里(Android Studio编译器)