效果图:
顶部的标题颜色,跟随手指滑动时进行歌词的染色效果。
思路分析:
首先想到的是利用两层TextView,一层是默认的颜色,一层是要染色的颜色,然后染色层宽度随着手指滑动的距离进行改变,这样由于染色层在默认层的上方,所以会覆盖默认层,就能达到歌词染色的效果了。
简单实现:
那么先试试我们的想法是否可行,写一个FramLayout来作为容器。
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--这是默认的文字-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一段文字"
android:textColor="#000000"
android:textSize="30sp"/>
<!--这是染色的文字-->
<TextView
android:layout_width="20dp"
android:layout_height="wrap_content"
android:text="这是一段文字"
android:textColor="@color/colorPrimary"
android:textSize="30sp"/>
</FrameLayout>
可以看到,布局很简单,只是将第二个,也就是染色层的TextView的宽度变了,那么效果如何呢?
嗯,效果实现了部分,至少证明思路的没错的,的确能实现文字染色的效果。但问题是这个染色层自动换行了,那简单,加个"singleLine",不就好了?
看上去好像可以了,那我们再把宽度变长点看看?
奇怪了,为什么明明覆盖上去了,但是染色层的字没出现呢?其实这是TextView的一种对超出文本的默认处理,我们只需要告诉TextView,对于超出文本,不用去做任何处理就好了,也就是"ellipsize = none"就好了,同时将"maxLines = 1"替换为"singleLine = true"就好了。让我们看看效果图。
到此,我们想要的效果就实现了,下面就是将思路应用到滑动栏中了。滑动栏我们使用谷歌提供的TabLayout来实现,TabLayout的滑动动画非常舒服,而且提供了自定义Tab的功能。既然如此,那么我们就可以使用自定义Tab,然后去监听滑动状态,然后控制我们的染色层的宽度就好了。
注意:使用TabLayout需要依赖design库 implementation 'com.android.support:design:28.0.0'。
常见滑动效果实现:
我们首先实现常见的根据滑动切换Fragment的效果。主要使用TabLayout+ViewPager实现。
主布局文件activity_main.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"
tools:context=".MainActivity"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="40dp" />
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
很简单,只有一个TabLayout和一个ViewPager。
Java代码MainActivity.java:
TabLayout tabLayout = findViewById(R.id.tabLayout);
ViewPager viewPager = findViewById(R.id.viewPager);
//模拟数据源
List<String> titleList = new ArrayList<>();
List<TestFragment> fragmentList = new ArrayList<>();
for (int i = 0; i < 3; i++){
tabLayout.addTab(tabLayout.newTab());//给TabLayout生成Tab项
titleList.add("tab_" + i);//标题,会设置给TabLayout中的Tab项
fragmentList.add(new TestFragment());//要切换的Fragment
}
//给ViewPager设置适配器
viewPager.setAdapter(new TestFragmentAdapter(getSupportFragmentManager(),titleList,fragmentList));
//将TabLayout和ViewPager关联起来
tabLayout.setupWithViewPager(viewPager);
代码中的TestFragment是一个非常非常非常简单的Fragment,其中只是显示一个Hello,World的TextView而已。
TestFragmentAdapter.java:
public class TestFragmentAdapter extends FragmentStatePagerAdapter {
List<String> titleList;
List<TestFragment> fragmentList;
public TestFragmentAdapter(FragmentManager fm,List<String> titleList,List<TestFragment> fragmentList) {
super(fm);
this.titleList = titleList;
this.fragmentList = fragmentList;
}
@Override
public Fragment getItem(int i) {
return fragmentList.get(i);
}
@Override
public int getCount() {
return fragmentList.size();
}
/**
* 给TabLayout设置标题使用的,如果不写这个,会出现TabLayout为空白的现象
* @param position 索引值
* @return title
*/
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
}
适配器没啥好说的,跟平常使用的时候一样。写到这里,我们已经实现了最常见的切换效果了。
下面,我们就要利用TabLayout的setCustomView来实现今日头条的切换效果了。
仿今日头条滑动效果实现:
首先写我们的自定义Tab的布局文件,一个FramLayout中包含两个TextView,第一个TextView为默认的,第二个TextView为染色的。
<?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="wrap_content">
<TextView
android:id="@+id/tab_normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="20sp"
/>
<TextView
android:id="@+id/tab_act"
android:layout_width="10dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="none"
android:textColor="@color/colorPrimary"
android:text="test"
android:textSize="20sp"/>
</FrameLayout>
然后在java代码中,我们要使用我们的自定义布局,而不使用TabLayout自带的布局。
TabLayout tabLayout = findViewById(R.id.tabLayout);
ViewPager viewPager = findViewById(R.id.viewPager);
//模拟数据源
List<String> titleList = new ArrayList<>();
List<TestFragment> fragmentList = new ArrayList<>();
for (int i = 0; i < 3; i++){
tabLayout.addTab(tabLayout.newTab().setCustomView(R.layout.tab));//给TabLayout生成Tab项,并使用自定义布局文件
titleList.add("tab_" + i);//标题,会设置给TabLayout中的Tab项
fragmentList.add(new TestFragment());//要切换的Fragment
}
//因为我们没调用TabLayout的setupWithViewPager()方法,所以我们需要自己设置监听器,让自定义布局生效
viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager));
//给ViewPager设置适配器
viewPager.setAdapter(new TestFragmentAdapter(getSupportFragmentManager(),titleList,fragmentList));
//将TabLayout和ViewPager关联起来
// tabLayout.setupWithViewPager(viewPager);
这个时候再看看我们的效果:
可以看到TabLayout已经使用了我们的tab.xml这个布局文件了,下面就是本文最关键的部分了,根据滑动距离去更改染色层的宽度,达到今日头条的效果。
在此之前,我们需要想想,我们如何能知道手指滑动的距离?查看上文我们设置的两个监听器,发现其中"TabLayoutOnPageChangeListener"这个监听器中,有一个叫"onPageScrolled"的方法,不错,这个就是我们所需要的东西了。那么找到了需要的,我们就在这个监听器的基础上,进行我们的染色操作就好了,我们新建一个类去继承这个监听器。
MyOnPageChangeListener.java:
public class MyOnPageChangeListener extends TabLayout.TabLayoutOnPageChangeListener {
private Context context;
private List<Integer> tabWidthList;//Tab宽度集合
private List<TextView> textViewToRightList;//染色层TextView集合
private int currentScrollState;//当前滑动状态
private int lastScrollStare;//上一次的滑动状态
private int lastPositionOffsetPix;//上一次手指滑动的位置
private int times;//已间隔次数
private final int RECORD_TIMES = 8;//两次记录滑动位置的间隔
public MyOnPageChangeListener(TabLayout tabLayout, Context context) {
super(tabLayout);
this.context = context;
tabWidthList = new ArrayList<>();
textViewToRightList = new ArrayList<>();
//循环获取每一个Tab的宽度,因为每一个Tab宽度可能是不一样的
for (int i = 0; i < tabLayout.getTabCount(); i++) {
final View view = tabLayout.getTabAt(i).getCustomView();
//由于存在view还未添加进容器中,所以可能出现getWidth为0的情况
//为了保证getWidth不为0,使用post(runnable)的方式获取。
view.post(new Runnable() {
@Override
public void run() {
tabWidthList.add(view.getWidth());//每个Tab的宽度
TextView textViewToRight = view.findViewById(R.id.tab_act);
//将所有的染色层宽度设置为0
ViewGroup.LayoutParams layoutParams = textViewToRight.getLayoutParams();
layoutParams.width = 0;
textViewToRight.setLayoutParams(layoutParams);
textViewToRightList.add(textViewToRight);
}
});
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
/**
* 滑动状态,0:静止,1:手指滑动,2:自动滑动
*/
//我们只在手指滑动和手指滑动过后的自动滑动做染色效果
//由于没有点击监听,所以经过测试发现,点击的时候,滑动状态是才从0→2→0这样一个过程
if (currentScrollState != 2 || lastScrollStare != 0) {
int currentPosition, nextPosition;
//计算,得出是向哪个方向移动
int orientation = positionOffsetPixels - lastPositionOffsetPix;
if (orientation > 0) {
//向右
currentPosition = position;
nextPosition = currentPosition + 1;
//操作下一个目标向右染色
textViewToRightList.get(nextPosition).setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = textViewToRightList.get(nextPosition).getLayoutParams();
//设置宽度从0开始递增
layoutParams.width = 0;
textViewToRightList.get(nextPosition).setLayoutParams(layoutParams);
float step = Float.valueOf(tabWidthList.get(nextPosition)) / context.getResources().getDisplayMetrics().widthPixels;
layoutParams.width = (int) (step * positionOffsetPixels);
textViewToRightList.get(nextPosition).setLayoutParams(layoutParams);
}
//记录滑动位置,两次记录之间间隔要稍微拉开一点,否则可能出现两次滑动位置在同一个地方
if (times >= RECORD_TIMES){
lastPositionOffsetPix = positionOffsetPixels;
times = 0;
}
times++;
}
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
//更新滑动状态
lastScrollStare = currentScrollState;
currentScrollState = state;
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
}
具体的逻辑都已经在代码中注释好了,进过上面的一番操作,我们已经实现了向右滑动时,下一个Tab中的文字逐渐染色的效果。
不错,后面就可以按照同样的思路实现完整的效果了。
向右滑动时,当前Tab染色层从左向右收缩,下一个Tab染色层从左向右展开。
向左滑动时,当前Tab染色层从右向左收缩,下一个Tab染色层从右向左展开。
这里就出现一个问题了,怎么让染色层做到从左向右收缩呢?我先说说我的思路,我是利用了TextView的"gravity"和"layout_gravity"来实现的。其中,"gravity"指的是TextView中的内容的位置如何;"layout_gravity"指的是TextView在父容器中的位置如何。如果我们想实现TextView从左到右收缩的效果,那么我们就需要让TextView中的"gravity"和"layout_gravity"都设置为"end"即可。
为了不在代码中做过多的操作,所以我在自定义Tab的布局文件中又写了一个TextView,这个新的TextView也是染色层,主要负责从左到右收缩和从右到左展开的染色效果。至此,我们的自定义Tab中就有了三个TextView,他们按照从下到上的顺序分别是:默认的TextView,向右染色的TextView,向左染色的TextView。下面只需要按照我们之前的逻辑来进行操作即可,由于逻辑基本相同,所以我就不再贴新的代码了。
本文所有源码已上传到GitHub,FollowTouTiao