前面已经提到过一种效果,不过那是以前版本,而且也是项目中用到的效果。现在今日头条版本已经6.3.9了,他的tab切换动画有所更改,那么我们今天来看看这个高大上的今日头条就这个功能用到那些技术点。。
看效果图,过过眼
看内容就知道上面一张是头条的内容样式,下面一张是自己实现的,效果都差不多吧,看看这个技术点。
1,首先我们必须计算出滑块的矩形rect,而且是根据文本的多少变化的,这里需要用到viewpager里面的方法public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}根据这些参数,我们就知道当前的显示的item和滑动的时候的偏移量,来控制rect的大小。
2,滑动的时候文本显示状态是切割的,随着移动字是一点点的显示出来,这里涉及到一个知识点canvas.clipRect(indicatorRect)切割画布,在画布里面切割的区域内,按照坐标绘制文本,就可以实现效果。简单来说就是是就是两层文本,一层是原生的textview设置的,一层是canvas里面绘制的,这就和我以前提到的逐字歌词一个道理,有的需求滑块可能在文本的下方,我相信有了这个案例,实现起来就太easy了。
/**
* 滑块的区域Rect
*/
private void initIndicatorRect() {
currentTab = (ViewGroup) tabsContent.getChildAt(currentPosition);
TextView newsText = (TextView) currentTab.findViewById(R.id.news_text);
float left = currentTab.getLeft() + newsText.getLeft();
float right = newsText.getWidth() + left;
if (currentPositionOffset > 0 && currentPosition < tabCount - 1) {
ViewGroup nextTab = (ViewGroup) tabsContent.getChildAt(currentPosition + 1);
TextView nextNewsText = (TextView) nextTab.findViewById(R.id.news_text);
float nextLeft = nextTab.getLeft() + nextNewsText.getLeft();
left = left + (nextLeft - left) * currentPositionOffset;
right = left + newsText.getWidth() * (1.0f - currentPositionOffset) + currentPositionOffset * nextNewsText.getWidth();
}
indicatorRect.set(
((int) left) + getPaddingLeft(),
getPaddingTop() + currentTab.getTop() + newsText.getTop(),
(int) (right + getPaddingLeft()),
newsText.getHeight() + currentTab.getTop() + getPaddingTop() + newsText.getTop());
}
滑块rect的计算,由于我们的需求效果就必须利用俩个相邻的item来计算,仔细看上面代码,left最开始获取的是当前高亮显示的textview相对最外侧HorizontalScrollView的距离,而left = left + (nextLeft - left) * currentPositionOffset公式可以看出,随着滑动left的值是变化的,不管是向左滑动还是向右滑动,left都会相应改变。right = left + newsText.getWidth() * (1.0f - currentPositionOffset) + currentPositionOffset * nextNewsText.getWidth();right计算,right是在right的值加上width,看公式一个宽度变小,一个宽度变大,这就和效果一样,在tab文本不一样长的时候,左右滑动,滑块的宽度会变化,最终变成目标宽度。
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if(tabCount == 0){
return;
}
initIndicatorRect();
/**
* 滑块绘制
*/
if (indicatorBG != null) {
indicatorBG.setBounds(indicatorRect);
indicatorBG.draw(canvas);
}
for (int i = 0; i < tabCount; i++) {
ViewGroup tab = (ViewGroup) tabsContent.getChildAt(i);
TextView newsText = (TextView) tab.findViewById(R.id.news_text);
if (newsText != null) {
initIndicatorRect();
canvas.clipRect(indicatorRect);
canvas.drawText(newsText.getText().toString(),
tab.getLeft() + newsText.getLeft() + newsText.getPaddingLeft(),
getPaddingTop() + tab.getTop() + newsText.getTop() + newsText.getPaddingTop() + newsText.getMeasuredHeight() / 2 + offset,
textPait);
}
}
}
根据rect 绘制高亮,这里就可以看出,绘制的是所有tab的文本,但是我们只能看到一部分高亮,关键就是canvas.clipRect(indicatorRect);这个区域rect。如果我们吧这句干掉,会是什么效果呢,看下图
这个图还是具有一点说服力的,至少可以说明绘制两层这个问题,这符合我的猜测。到现在为止基本的效果已经出来了,还有一个问题,当tab数量比较多,HorizontalScrollView一屏肯定装不下,部分会看不到。当你滑动到屏幕边缘的时候,tab也会滑动,这个时候隐藏的部分也会跟着出来,始终让高亮的文本在屏幕可见区域。于是下面代码开始工作了
private void scrollToItem() {
if (tabCount == 0) {
return;
}
initIndicatorRect();
int newScrollX = lastScrollX;
if (indicatorRect.left < getScrollX() + scrollOffset) {
newScrollX = indicatorRect.left - scrollOffset;
} else if (indicatorRect.right > getScrollX() + getWidth() - scrollOffset) {
newScrollX = indicatorRect.right - getWidth() + scrollOffset;
}
if (newScrollX != lastScrollX) {
lastScrollX = newScrollX;
scrollTo(newScrollX, 0);
}
}
当滑动到最左边和最右边的时候,让HorizontalScrollView.scrollTo移动到指定位置,好关键代码已经贴完了,下面吧所有的代码贴出来,copy 就能跑。。
xml主布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.apple.Custom.TabIndicator
android:id="@+id/indicator"
android:layout_width="match_parent"
android:layout_height="80dp" />
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
滑块背景xml
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#F00" />
<corners android:radius="4.0dip" />
</shape>
item xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp" >
<TextView
android:id="@+id/news_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="4dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="4dp"
android:textColor="#666"
android:textSize="14sp" />
</FrameLayout>
最关键的自定义TabIndicator
public class TabIndicator extends HorizontalScrollView implements OnPageChangeListener {
final String TAG = this.getClass().getSimpleName();
private LayoutInflater mLayoutInflater;
private ViewPager pager;
/**
* item容器
*/
private LinearLayout tabsContent;
/**
* item数量
*/
private int tabCount;
/**
* 当前显示item
*/
private int currentPosition = 0;
/**
* 偏移量
*/
private float currentPositionOffset = 0f;
/**
* 滑块区域
*/
private Rect indicatorRect;
private Paint textPait;
/**
* 滑块停留屏幕偏移量
*/
private int scrollOffset = 20;
private int offset = 1;
private int lastScrollX = 0;
/**
* 滑块Drawable
*/
private Drawable indicatorBG;
private OnPageChangeListener listener;
private int itemLayout;
public TabIndicator(Context context) {
this(context, null);
}
public TabIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TabIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setFillViewport(true);
mLayoutInflater = LayoutInflater.from(context);
indicatorRect = new Rect();
textPait = new Paint(Paint.ANTI_ALIAS_FLAG);
textPait.setColor(Color.WHITE);
tabsContent = new LinearLayout(context);
tabsContent.setOrientation(LinearLayout.HORIZONTAL);
addView(tabsContent, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
DisplayMetrics dm = getResources().getDisplayMetrics();
scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm);
offset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, offset, dm);
itemLayout = R.layout.news_tab;
indicatorBG = getResources().getDrawable(R.drawable.bg_indicator, context.getTheme());
}
public void setViewPager(ViewPager pager) {
this.pager = pager;
if (pager.getAdapter() == null) {
throw new IllegalStateException("ViewPager does not have adapter");
}
pager.addOnPageChangeListener(this);
notifyDataSetChanged();
}
public void notifyDataSetChanged() {
tabsContent.removeAllViews();
tabCount = pager.getAdapter().getCount();
for (int i = 0; i < tabCount; i++) {
addTab(i, pager.getAdapter().getPageTitle(i).toString());
}
}
private void addTab(final int position, String title) {
ViewGroup tab = (ViewGroup) mLayoutInflater.inflate(itemLayout, this, false);
TextView newsText = (TextView) tab.getChildAt(0);
newsText.setText(title);
newsText.setGravity(Gravity.CENTER);
newsText.setTextColor(Color.GRAY);
newsText.setSingleLine();
textPait.setTextSize(newsText.getTextSize());
tab.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
pager.setCurrentItem(position);
}
});
tabsContent.addView(tab);
}
/**
* 滑块的区域Rect
*/
private void initIndicatorRect() {
ViewGroup currentTab = (ViewGroup) tabsContent.getChildAt(currentPosition);
TextView newsText = (TextView) currentTab.findViewById(R.id.news_text);
float left = currentTab.getLeft() + newsText.getLeft();
float right = newsText.getWidth() + left;
if (currentPositionOffset > 0 && currentPosition < tabCount - 1) {
ViewGroup nextTab = (ViewGroup) tabsContent.getChildAt(currentPosition + 1);
TextView nextNewsText = (TextView) nextTab.findViewById(R.id.news_text);
float nextLeft = nextTab.getLeft() + nextNewsText.getLeft();
left = left + (nextLeft - left) * currentPositionOffset;
right = left + newsText.getWidth() * (1.0f - currentPositionOffset) + currentPositionOffset * nextNewsText.getWidth();
}
indicatorRect.set(
((int) left) + getPaddingLeft(),
getPaddingTop() + currentTab.getTop() + newsText.getTop(),
(int) (right + getPaddingLeft()),
newsText.getHeight() + currentTab.getTop() + getPaddingTop() + newsText.getTop());
}
private void scrollToItem() {
if (tabCount == 0) {
return;
}
initIndicatorRect();
int newScrollX = lastScrollX;
if (indicatorRect.left < getScrollX() + scrollOffset) {
newScrollX = indicatorRect.left - scrollOffset;
} else if (indicatorRect.right > getScrollX() + getWidth() - scrollOffset) {
newScrollX = indicatorRect.right - getWidth() + scrollOffset;
}
if (newScrollX != lastScrollX) {
lastScrollX = newScrollX;
scrollTo(newScrollX, 0);
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if(tabCount == 0){
return;
}
initIndicatorRect();
/**
* 滑块绘制
*/
if (indicatorBG != null) {
indicatorBG.setBounds(indicatorRect);
indicatorBG.draw(canvas);
}
for (int i = 0; i < tabCount; i++) {
ViewGroup tab = (ViewGroup) tabsContent.getChildAt(i);
TextView newsText = (TextView) tab.findViewById(R.id.news_text);
if (newsText != null) {
initIndicatorRect();
canvas.clipRect(indicatorRect);
canvas.drawText(newsText.getText().toString(),
tab.getLeft() + newsText.getLeft() + newsText.getPaddingLeft(),
getPaddingTop() + tab.getTop() + newsText.getTop() + newsText.getPaddingTop() + newsText.getMeasuredHeight() / 2 + offset,
textPait);
}
}
}
/**
* @param position 当前item
* @param positionOffset 0和1区间变化
* @param positionOffsetPixels 像素偏移量
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (listener != null) {
listener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
currentPosition = position;
currentPositionOffset = positionOffset;
scrollToItem();
invalidate();
}
@Override
public void onPageScrollStateChanged(int state) {
if (listener != null) {
listener.onPageScrollStateChanged(state);
}
}
@Override
public void onPageSelected(int position) {
if (listener != null) {
listener.onPageSelected(position);
}
}
/**
* 设置viewpage滑动监听
*
* @param listener
*/
public void addPageListener(OnPageChangeListener listener) {
this.listener = listener;
}
public void setItemLayout(int lay) {
itemLayout = lay;
}
activity是最简单的代码了
String[] titles = {"我爱北京", "北京", "十九大", "北京是我家", "国贸", "中关村", "百度", "鸟窝", "天安门"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_pager_indicator);
TabIndicator indicator = (TabIndicator) findViewById(R.id.indicator);
ViewPager mViewPager = (ViewPager) findViewById(R.id.viewpager);
mViewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return titles.length;
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
TextView textView = new TextView(getApplication());
textView.setText(titles[position] + " " + position);
textView.setTextColor(Color.RED);
textView.setBackgroundColor(Color.GRAY);
container.addView(textView);
textView.setGravity(Gravity.CENTER);
return textView;
}
});
indicator.setViewPager(mViewPager);
}
拓展一点
如果在上面我们不用scrollToItem里面的内容,干掉这个方法,在onPageSelected里面添加如下代码,给你五秒时间,猜猜看是什么样的效果
@Override
public void onPageSelected(int position) {
if (listener != null) {
listener.onPageSelected(position);
}
ViewGroup checkView = (ViewGroup) tabsContent.getChildAt(position);
if (checkView == null) {
return;
}
int offset = scrollOffset / 2 - checkView.getWidth() / 2;
smoothScrollTo(checkView.getLeft() - offset, 0);
}
上面提到过,逐字歌词,这种方法可以实现逐字歌词,本篇效果还可以用LinearGradient,有激情的基友,可以试试。
以上就是所有代码了,本着学习的态度,错误肯定是有的,就不要喷了。。。。好了,该睡觉了,晚安。。。