今天是模仿这个项目的第三天,看到风格优秀的标签-viewpager联动,我有点头痛,怎样做才能达到像这个项目一样的优秀效果呢?
要点:
- 标签和viewpager是联动的,标签动,viewpager动,viewpager动,标签也要动
难度:☆ - 标签选中时是有中间的过度动画的,同时之前选择的标签颜色渐变为浅色,新选择的标签颜色渐变为深色,同时尽量将标签移动到中间
难度:☆☆ - 标签和viewpager之间的联动性非常好,标签和viewpager几乎同时移动
难度:☆☆☆
根据难度,由易到难,一条一条的来
标签联动:
第一条,好简单,估计很多人都做过,思路如下:
(1)自定义一个HorizontalScrollView
(2)为这个HorizontalScrollView动态添加标签【动态有助于扩展,部分标签可能需要刷新】
这条略难,可以添加一些textview进来,定义选中,未选中两种状态。
(3)为自定义标签和viewpager添加联动类,点击标签时,判断是否已选,已选不处理,否则触发setCurrentItem(),进而触发onPageSelected()
注意,这里一定要对标签页是否选择进行判断,否则会造成死循环,死循环有两种情况,
1.从标签开始
点击标签->setCurrentItem->onPageSelected()->设置标签->点击标签。。。
2.从onPageSelected开始
setCurrentItem->onPageSelected()->设置标签->点击标签->setCurrentItem()
如果判断已选,循环顺序如下:
onPageSelected->设置标签->点击标签->和当前标签不同,选择标签-》setCurrentItem->onPageSelected->点击标签-》和当前标签相同,返回
这样就结束,或者也可以在setCurrentItem中也判断一次,就变成:
onPageSelected->设置标签->点击标签->和当前标签不同,选择标签-》setCurrentItem->和当前标签相同,返回
关键代码:
标签方面的添加标签:
/**
* 添加标签
* @param tabs 标签列表
* @param paddingLeftRight 左右间距
* @param paddingTopBottom 上下间距
* @param textSize 字号
*/
public void addTabs(String[] tabs, int paddingLeftRight, int paddingTopBottom, int textSize) {
container = new LinearLayout(getContext().getApplicationContext());
container.setOrientation(LinearLayout.HORIZONTAL);
addView(container);
for (int i = 0; i < tabs.length; i++) {
String name = tabs[i];
TextView tv = new TextView(getContext().getApplicationContext());
tv.setText(name);
tv.setTextSize(textSize);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
tv.setLayoutParams(params);
tv.setGravity(Gravity.CENTER);
tv.setPadding(paddingLeftRight, paddingTopBottom, paddingLeftRight, paddingTopBottom);
container.addView(tv);
}
initData();
}
初始化标签的选中项,初始化标签代号,初始化标签点击事件:
private void initData() {
container = (LinearLayout) getChildAt(0);
for (int i = 0; i < container.getChildCount(); i++) {
TextView view = (TextView) container.getChildAt(i);
view.setTag(i);
if (i == 0)
view.setTextColor(selectWordColor);
else
view.setTextColor(noSelectWordColor);
view.setOnClickListener(onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
fromTab = true;
newPos = (int) v.getTag();
if(selectedPos == newPos)
return;
select(newPos);//这个是给外面的接口用的
}
});
}
invalidate();
}
移动到中间的逻辑:
final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
smoothScrollTo(scrollPos, 0);
realLeft是当前左边距,可用container.getChildAt(newPos).getLeft()获取
realWidth是当前宽度,可用container.getChildAt(newPos).getWidth()获取
然后绘制,这里贴个大概,主要是提供思路。具体的请看下面源码链接:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//去除滚动条和半弧背景
setHorizontalScrollBarEnabled(false);
setHorizontalFadingEdgeEnabled(false);
setOverScrollMode(OVER_SCROLL_NEVER);
if (isInEditMode()) {
return;
}
if (width == 0) {
width = container.getWidth();
ceilCount = container.getChildCount();
ceils = new float[ceilCount];
if (container.getChildCount() != 0) {
for (int i = 0; i < container.getChildCount(); i++) {
ceils[i] = container.getChildAt(i).getWidth();
}
}
startWidth = ceils[0];
}
if (underlineHeight != 0) {
float posLeft = container.getChildAt(selectedPos).getLeft();
float aidLeft = container.getChildAt(newPos).getLeft();
float realLeft = (aidLeft - posLeft) * followOffset + posLeft;
float postWidth =ceils[selectedPos];
float aidWidth = ceils[newPos];
float realWidth = (aidWidth - postWidth) * followOffset + postWidth;
float left = realLeft + underlineLeftRight;
float top = getHeight() - underlineHeight;
float right = realLeft + realWidth - underlineLeftRight;
float bottom = getHeight();
//下滑线/背景
if (underlineDrawable == null) {
canvas.drawRect(left, top, right, bottom, underlinePaint);
}
}
然后看下联动部分:
tabLayout.setOnSelectTabListener(new TabLayout.SelectTabListener() {
@Override
public void onSelect(int position) {
if (position == nowPos)
return;
nowPos = position;
if (fromVp) {
fromVp = false;
return;
}
fromTab = true;
viewPager.setCurrentItem(position);
}
});
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (fromTab) {
fromTab = false;
return;
}
fromVp = true;
tabLayout.setTab(position);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
然后setTab中,只要模拟点击就行:
public void setTab(int position){
if(ceils == null || ceils.length == 0)
return;
if(container!=null && container.getChildCount()>position){
container.getChildAt(position).performClick();
}
}
因为本来是别的项目移植来的,那个项目的每个标签的宽度不一样,所以需要计算宽度,如果宽度一致,可以直接将realWidth写成:realWidth = container.getChildAt(0).getWidth();就行了,如果读者想自定义个标签,不想看过程,可以直接移动到最下面看源码,本文只提供思路,直接复制本文的代码,不能直接使用,但有助于大家学会怎样做标签,viewpager联动,如果要学习,请务必仔细阅读本文。
过渡动画:
过渡动画包含两种效果,从标签a移动到标签b:
- 指示器(代码选中的背景)从a向b移动
- a颜色变浅,b颜色变深
这里可以用ValueObject:
ValueAnimator animator = ValueAnimator.ofInt(0,100);
animator.setTarget(targetBtn);
animator.setDuration(1000).start();
final int height = targetBtn.getHeight();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animation){
int value = (int) animation.getAnimatedValue();
//value代表进度
followOffset = 1.0 * value / 100;
}
});
关于颜色计算,我这里也是百度的,感谢仁兄的提供:
http://blog.csdn.net/super_spy/article/details/49332719
其实这部分的难度主要是颜色部分,动画倒是没什么难度。
然后,在你做完这方面的工作后,会发现一个坑爹的问题,不同步!标签页和viewpager两者根本就没有达到联动效果,原因如下:
1.按照逻辑,是选择标签后,标签动画,触发viewpager,viewpager变化
2.viewpager滚动时候的逻辑根本没写,这个时候标签根本没动过
好用同步的联动性:
这个时候,我发现,滚动的时候会调用一个监听方法,addOnPageChangeListener中的onPageScrolled()
这里面有三个参数,先解释一下:
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
position:偏移从这个位置开始计算,注意,这个position并不一定代表你选中的position,这是这个方法的坑点,因为这个调整了好久
positionOffset:
position位置移动到右边页面的百分比数字在[0,1),注意,最后会变为0而不是1
positionOffsetPixels:
position位置移动到右边的偏移像素值,永远>=0
于是有了大概的思路:
(1)删除之前写的标签页动画
(2)在viewpager移动的时候,同时移动标签位置
(3)标签本身的点击事件不做处理,只用于触发setCurrentItem()
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (fromTab) {
return;
}
tabLayout.followPos(position, positionOffset);
}
跟随处理:
public void followPos(int position, float positionOffset) {
followPos = position;
if(positionOffset != 0)
nextPos = followPos + 1;
else
nextPos = followPos;
followOffset = positionOffset;
invalidate();
}
private void drawFollowMode(Canvas canvas) {
if(ceils == null || ceils.length == 0)
return;
int nowIndex = 0;
int nextIndex = 0;
nowIndex = followPos;
nextIndex = nextPos;
float posLeft = container.getChildAt(nowIndex).getLeft();
float aidLeft = container.getChildAt(nextIndex).getLeft();
float realLeft = (aidLeft - posLeft) * followOffset + posLeft;
float postWidth =ceils[nowIndex];
float aidWidth = ceils[nextIndex];
float realWidth = (aidWidth - postWidth) * followOffset + postWidth;
float left = realLeft + underlineLeftRight;
float top = getHeight() - underlineHeight;
float right = realLeft + realWidth - underlineLeftRight;
float bottom = getHeight();
//下滑线/背景
if (underlineDrawable == null) {
canvas.drawRect(left, top, right, bottom, underlinePaint);
} else {
underlineDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom);
underlineDrawable.draw(canvas);
}
//渐变颜色计算
int color = ColorUtil.caculateColor(selectWordColor, noSelectWordColor, followOffset);
int color2 = ColorUtil.caculateColor(noSelectWordColor, selectWordColor, followOffset);
TextView tv = (TextView) container.getChildAt(nowIndex);
TextView tv2 = null;
tv2 = (TextView) container.getChildAt(nextIndex);
tv2.setTextColor(color2);
tv.setTextColor(color);
if(followOffset == 0) {
if(selectedPos == followPos)
return;
final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
smoothScrollTo(scrollPos, 0);
selectedPos = followPos;
}
}
大概是这样,简单总结:
viewpager滑动到哪里,标签也跟着滑动到哪里,这样伴随效果比较好。
可是,上述方法存在后续问题,本篇博客本来是2017.9.6号开始写的,后面发现该问题于是2017.9.7解决才继续写。问题出在颜色渐变上。因为viewpager的onPageScrolled那个position总是在变化,所以在隔一个以上标签点击标签而非滑动viewpager的时候,颜色会有残留。所以,需要对选择标签和viewpager直接滚动两种情况进行分别处理:
(1)viewpager滚动:正常,按照之前的方式处理
(2)标签页滚动:a.标签页滚动保存新的位置,根据这个位置进行单独处理
(3)标签页滚动方式处理完后,注意还原成viewpager方式处理也正确的方式,目的是让invalidate时效果正确,另外,滚动是不会发生闪烁。
下面代码请看和fromTab相关部分:
@Override
public void onClick(View v) {
fromTab = true;
newPos = (int) v.getTag();
if(selectedPos == newPos)
return;
select(newPos);
}
public void followPos(int position, float positionOffset) {
if(fromTab){
int moved = 0;
if(selectedPos!=container.getChildCount()-1){
int leftNext = container.getChildAt(selectedPos+1).getLeft();
int leftNow = container.getChildAt(selectedPos).getLeft();
moved = (int) ((leftNext - leftNow)*positionOffset);
}else{
int leftNow = container.getChildAt(selectedPos).getLeft();
int leftLast = container.getChildAt(selectedPos-1).getLeft();
moved = (int) ((leftNow - leftLast)*positionOffset);
}
int newLeft = container.getChildAt(newPos).getLeft();
int oldLeft = container.getChildAt(selectedPos).getLeft();
int posLeft = container.getChildAt(position).getLeft();
int moveDistance = posLeft - oldLeft + moved;
int allDistance = newLeft-oldLeft;
followOffset = moveDistance*1.0f / allDistance;
followPos = position;
}else{
followPos = position;
if(positionOffset != 0)
nextPos = followPos + 1;
else
nextPos = followPos;
followOffset = positionOffset;
}
invalidate();
}
这里的followOffset重新计算了,思路是a和a+1的偏移量=宽度*百分比
移动距离是a左边距-起始左边距+a到a+1偏移量
总距离是结束位置左边距-起始位置左边距
移动偏移为移动距离除以总距离,注意这里最后positionOffset会变成1不是0,所以下面的判断算的是positionOffset==1
private void drawFollowMode(Canvas canvas) {
if(ceils == null || ceils.length == 0)
return;
int nowIndex = 0;
int nextIndex = 0;
if(fromTab){
nowIndex = selectedPos;
nextIndex = newPos;
}else{
nowIndex = followPos;
nextIndex = nextPos;
}
float posLeft = container.getChildAt(nowIndex).getLeft();
float aidLeft = container.getChildAt(nextIndex).getLeft();
float realLeft = (aidLeft - posLeft) * followOffset + posLeft;
float postWidth =ceils[nowIndex];
float aidWidth = ceils[nextIndex];
float realWidth = (aidWidth - postWidth) * followOffset + postWidth;
float left = realLeft + underlineLeftRight;
float top = getHeight() - underlineHeight;
float right = realLeft + realWidth - underlineLeftRight;
float bottom = getHeight();
//下滑线/背景
if (underlineDrawable == null) {
canvas.drawRect(left, top, right, bottom, underlinePaint);
} else {
underlineDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom);
underlineDrawable.draw(canvas);
}
//渐变颜色计算
int color = ColorUtil.caculateColor(selectWordColor, noSelectWordColor, followOffset);
int color2 = ColorUtil.caculateColor(noSelectWordColor, selectWordColor, followOffset);
TextView tv = (TextView) container.getChildAt(nowIndex);
TextView tv2 = null;
tv2 = (TextView) container.getChildAt(nextIndex);
tv2.setTextColor(color2);
tv.setTextColor(color);
if(fromTab && followPos == newPos && followOffset == 1){
//数据还原,模拟viewpager滑动时数据
fromTab = false;
selectedPos = newPos;
nextPos = selectedPos;
followOffset = 0;
final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
smoothScrollTo(scrollPos, 0);
}else if(followOffset == 0 && !fromTab) {
if(selectedPos == followPos)
return;
final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
smoothScrollTo(scrollPos, 0);
selectedPos = followPos;
}
}
好,大概这么多,总结几个关键点:
1.单联动不要动画注意保存位置信息,防止死循环
2.不严格跟随可添加动画,严格跟随不必添加动画
3.根据onPageScrolled来更新标签页位置,可以实现完全的跟随效果
4.onPageScrolled提供的position不完全可靠,只能当做偏移量的参照坐标。如果需要渐变等特殊状态,需要将viewpager滚动和标签滚动分开处理。
原项目的封装性更好一些,如果只需要移植,不想尝试自己做可以从原项目地址中提取;代码阅读能力强的可直接参看原项目地址,模仿项目的滑动写的封装性不大好,但是好理解一些:
模仿项目地址:https://github.com/nfwuzhongdemeng/ImitateNBA
原项目地址:https://github.com/smuyyh/SprintNBA