粘性列表特效,一般在类似通讯录这种强调分类的需求中广泛使用。它的好处在于可以直观的知道某一个ITEM属于哪种类型。在github上也有Star量很大的StickyListHeaders可以进行参考学习,但是我觉得,如果可以自己动手写,那么在特效的理解以及后期的维护上将会更加得心应手
本次代码已经上传到github上,欢迎star,follow

实现思路
其实一开始还是比较困惑这个被顶上去的流程的,但是你听我分析之后,肯定就会豁然开朗
-
首先看看Item,不用说它肯定是由蓝色的粘性部分和真实数据两部分组成。
Item布局效果
-
以RecyclerView为例,看看列表部分的布局。这里也有一个蓝色部分,这个是真正的索引视图,每一个索引的值由当前列表中每一个firstVisibleItem的值决定
RecyclerView组合
基于以上2点,你肯定可以实现一个普通的按类型分类的列表了,下面说的就是滑动部分了
- 如果要监听RecyclerView或者ListView的滑动,肯定要通过ScrollListener去完成。可以滚之后,下面就要先想办法去解决粘性部分的值如何去修改。这也很简单,只需要将firstVisibleItem所对应的Item的值直接赋值给粘性部分即可。这里直接通过getChildAt(0)即可获取完成
- 最后剩下的就是动画部分了。这里有个简单的逻辑判断:当粘性部分与某一个Item进行无缝相接之后,我们就得判断粘性部分与这个Item是不是属于同一个分类类型。如果是一个分类,则粘性部分保持当前位置;如果是一个新类别的Item,那么粘性部分就需要根据这个item的getTop值去修改自己Y轴方向的位置,直到与顶部交接而复原。RecylerView通过findChildViewUnder即可通过坐标点获取相应的视图,ListView则是通过pointToPosition去获取。这里需要判断的Y坐标点就是粘性部分高度值+1
动手写代码
- 布局
先看看头部文件,这个没什么好说的,就是粘性部分
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/header_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:padding="10dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
再看看Item的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:orientation="vertical">
<include layout="@layout/view_header" />
<RelativeLayout
android:id="@+id/rl_content_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:padding="10dp">
<TextView
android:id="@+id/adapter_item_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:padding="5dp"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="text1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/adapter_item_tv"
android:padding="5dp"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="text2"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="text3" />
</RelativeLayout>
</LinearLayout>
看看RecyclerView的布局
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_sticky"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/view_header" />
</FrameLayout>
- 代码
先看看Adapter部分。首先我们先要确定一下布局。position为0的布局肯定需要将粘性部分一直显示出来,其余条件下则不一样了,只有新分类的布局才会显示粘性部分,相同分类不需要显示。同时我们将状态保存以便获取
public static final int FIRST_STICKY_VIEW = 1;
public static final int HAS_STICKY_VIEW= 2;
public static final int NONE_STICKY_VIEW= 3;
@Override
public void onBindViewHolder(MainViewHolder holder, int position) {
holder.header_view.setText(strings.get(position));
holder.itemView.setContentDescription(strings.get(position));
if (position==0) {
holder.header_view.setVisibility(View.VISIBLE);
holder.itemView.setTag(FIRST_STICKY_VIEW);
}
else {
if (!strings.get(position).equals(strings.get(position-1))) {
holder.header_view.setVisibility(View.VISIBLE);
holder.itemView.setTag(HAS_STICKY_VIEW);
}
else {
holder.header_view.setVisibility(View.GONE);
holder.itemView.setTag(NONE_STICKY_VIEW);
}
}
}
随后就是关键的地方,随着RecyclerView滚动而改变粘性部分的文字,粘性部分动画范围为(0--粘性部分高度)。最后等滚动完成之后,就重新恢复到之前的位置。
rv_sticky.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
View stickyInfoView=recyclerView.getChildAt(0);
if (stickyInfoView!=null && stickyInfoView.getContentDescription()!=null) {
header_view.setText(String.valueOf(stickyInfoView.getContentDescription()));
}
View transInfoView=recyclerView.findChildViewUnder(header_view.getMeasuredWidth()/2, header_view.getMeasuredHeight()+1);
if (transInfoView!=null && transInfoView.getTag()!=null) {
int tag= (int) transInfoView.getTag();
int deltaY=transInfoView.getTop()-header_view.getMeasuredHeight();
if (tag==MainRVAdapter.HAS_STICKY_VIEW) {
if (transInfoView.getTop()>0) {
header_view.setTranslationY(deltaY);
}
else {
header_view.setTranslationY(0);
}
}
else {
header_view.setTranslationY(0);
}
}
}});
整套代码还是很简单的,最后附上ListView的处理方法
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View stickyInfoView=view.getChildAt(0);
if (stickyInfoView!=null && stickyInfoView.getContentDescription()!=null) {
header_view.setText(String.valueOf(stickyInfoView.getContentDescription()));
}
int index_=view.pointToPosition(header_view.getMeasuredWidth()/2, header_view.getMeasuredHeight()+1);
View transInfoView=view.getChildAt(index_-firstVisibleItem);
if (transInfoView!=null && transInfoView.findViewById(R.id.*header_view*).getTag()!=null) {
int tag= (int) transInfoView.findViewById(R.id.header_view).getTag();
int deltaY=transInfoView.getTop()-header_view.getMeasuredHeight();
if (tag==MainListAdapter.HAS_STICKY_VIEW) {
if (transInfoView.getTop()>0) {
header_view.setTranslationY(deltaY);
}
else {
header_view.setTranslationY(0);
}
}
else {
header_view.setTranslationY(0);
}
}
}
网友评论
Github上吗?