1.实现带首字母Title的ListView
重点在MyAdapter的注释
MainActivity.java
public class MainActivity extends Activity {
private MyAdapter mAdapter;
private ListView mListView;
private List<String> mList;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
mList=new ArrayList<String>();
String a="";
for(int i=0;i<26;i++){
a=((char)('A'+i))+"";
mList.add(a);
mList.add(a);
mList.add(a);
}
mListView=(ListView)findViewById(R.id.lv);
mAdapter=new MyAdapter();
mAdapter.update(mList,MainActivity.this);
mListView.setAdapter(mAdapter);
}
}
MyAdapter.java
public class MyAdapter extends BaseAdapter{
private List<String> mList;
private Context mContext;
public void update(List<String> list,Context context){
mContext=context;
Collections.sort(list);//如果是乱序输入的字母,需要排序一下,不然getView的时候会显示很多相同的Title
mList=list;
this.notifyDataSetChanged();
}
class ViewHolder{
TextView title;
TextView content;
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder=new ViewHolder();
//听说用convertView是提升ListView性能的好习惯
if(convertView!=null){
holder=(ViewHolder)convertView.getTag();
}else{
convertView=LayoutInflater.from(mContext).inflate(R.layout.item, null);
holder.title=(TextView) convertView.findViewById(R.id.item_title);
holder.content=(TextView)convertView.findViewById(R.id.item_content);
convertView.setTag(holder);
}
holder.content.setText(mList.get(position));
//显示title的逻辑
/*看到下面的item.xml文件就可以知道,其实每个listview item都是包含了title和content两个TextView的,只不过title都被隐藏了,我们只要把在首字母相同的分块中的第一个item的title显示出来并setText为该分块的首字母即可*/
//判断一个item是否是一个分块的第一个item的办法:该item的首字母与它的前一个item首字母不相同
String cur=mList.get(position);
String pre=position-1>=0?mList.get(position-1):"";//mList的第一个元素做特殊处理,防止数组越界
if(!(pre.equals(cur))){
holder.title.setVisibility(View.VISIBLE);
holder.title.setText(cur);
}else{
holder.title.setVisibility(View.GONE);
}
return convertView;
}
}
item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/item_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="#ffffff"
android:textSize="18sp"
android:visibility="gone"
android:text="A"
android:paddingLeft="10dip"
android:background="#40E0D0" />
<TextView
android:id="@+id/item_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="#000000"
android:textSize="18sp"
android:text="content"
android:paddingLeft="10dip"
android:background="#ffffff" />
/>
</LinearLayout>
main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ListView
android:id="@+id/lv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="@null"
/>
</RelativeLayout>
2.实现可以推动的title
实际上,实现可以被推动的Title的原理是新增一个独立的TextView显示在顶端,配合listView的滚动
上图顶部的B-Title,它是独立于listview中所有item 的一个textView,浮在顶部,我们需要的效果是:当下一个title触碰到该TextView的时候,触发推动的效果。
title.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="#ffffff"
android:textSize="18sp"
android:visibility="gone"
android:text="A"
android:paddingLeft="10dip"
android:background="#40E0D0" />
这时候,我们需要判断,这个独立Title(TextView)的状态:什么时候隐藏(INVISIBLE_STATE=0),什么时候显示(SHOW_STATE=1)以及什么时候触发推动的效果(PUSHING_STATE=2)。
这里会用到SectionIndexer的接口,我们改写上面的MyAdapter,让它实现SectionIndexer:
public class MyAdapter extends BaseAdapter implements SectionIndexer,OnScrollListener{
private List<String> mList;
private Context context;
private String sections[];
public void update(List<String> list,Context context){
this.context=context;
sections=new String[26];//实验传入的是三个一组的26个字母,所以写成了硬代码26,大家灵活判断一下长度
Collections.sort(list);
mList=list;
int pos=0;
for(int i=0;i<mList.size();i++){
String cur=mList.get(i);
String pre=(i-1)>=0?mList.get(i-1):"";
if(!(pre.equals(cur))){
sections[pos]=cur;
pos++;
}
}
this.notifyDataSetChanged();
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder=new ViewHolder();
if(convertView!=null){
holder=(ViewHolder)convertView.getTag();
}else{
convertView=LayoutInflater.from(context).inflate(R.layout.list_item, null);
holder.title=(TextView) convertView.findViewById(R.id.item_header);
holder.content=(TextView)convertView.findViewById(R.id.item_content);
convertView.setTag(holder);
}
holder.content.setText(mList.get(position));
String cur=mList.get(position);
String pre=position-1>=0?mList.get(position-1):"";
if(!(pre.equals(cur))){
holder.title.setVisibility(View.VISIBLE);
holder.title.setText(cur);
}else{
holder.title.setVisibility(View.GONE);
}
return convertView;
}
class ViewHolder{
TextView title;
TextView content;
}
public int getTitleState(int position) {
if (position < 0 || getCount() == 0) {
return 0;
}
int index = getSectionForPosition(position);
if(index==-1||index>sections.length){
return 0;
}
int section = getSectionForPosition(position);
int nextSectionPosition = getPositionForSection(section + 1);
if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {
return 2;
}
return 1;
}
@Override
public int getPositionForSection(int section) {
String sec=sections[section];
int pos=mList.indexOf(sec);
return pos;
}
@Override
public int getSectionForPosition(int position) {
String a=mList.get(position);
for(int i=0;i<sections.length;i++){
if(sections[i]==a){
return i;
}
}
return -1;
}
@Override
public Object[] getSections() {
return sections;
}
public void setTitleText(View mHeader, int firstVisiblePosition) {
String title=mList.get(firstVisiblePosition);
TextView sectionHeader = (TextView) mHeader;
sectionHeader.setText(title);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if(view instanceof PushTitleListView){
System.out.println("onScroll");
((MyListView)view).titleLayout(firstVisibleItem);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
我也是学习Android不久,很多东西不是很懂,所以大家不要喷我,简单的说说我对Section的理解,如图中的每个字母的区,前面三个A(title不算,一共三个A)构成的分块(区)是一个Section,定为Sections的第0个Section,然后后面三个B也是一个Section,是Sections的第1个Section,后面以此类推。
接下来根据上面约定的基础介绍一下实现SectionIndexer接口会实现下面几个方法:
@Override
public int getPositionForSection(int section) {
String sec=sections[section];
int pos=mList.indexOf(sec);
return pos;
}
getPositionForSection(int section):根据Section在Sections的位置(第几个Section)返回该Section第一个item在全部listview中的位置
例如:根据文章第一张图片,传入0(第0个Section-“A”)它的第一个item在listview的位置是0,传入1(第1个Section-“B”),它的第一个item在listview的位置是3。
@Override
public int getSectionForPosition(int position) {
String a=mList.get(position);
for(int i=0;i<sections.length;i++){
if(sections[i]==a){
return i;
}
}
return -1;
}
getSectionForPosition(int position):根据listview某个item的位置返回该item所在Section在Sections的位置
例如:根据文章第一张图片,传入0,1,2(listview的第0,1,2个item都是A)都会返回0,(A-section在Sections的位置是0),传入 4(listview的第四个item),会返回1(listview的第四个item是“B”,属于Sections中的第1个section),传入10(“D”)返回3。
@Override
public Object[] getSections() {
return sections;
}
上面这个方法不做介绍,实际代码中没有用到,有兴趣的同学可以研究一下官方文档。
有了Section的概念,我们现在可以方便而准确定位每一个Title的位置了(当然还有其他方法,这里是顺带介绍一下SectionIndexer)。接下来介绍一下如何判断独立Title的三个状态:什么时候隐藏(INVISIBLE_STATE=0),什么时候显示(SHOW_STATE=1)以及什么时候触发推动的效果(PUSHING_STATE=2)。
重点是什么时候触发推动效果(PUSHING_STATE=2):
如图,当第一个可视的Item(“B”)(其position由getFirstVisiblePosition()或者onScroll中的参数firstVisibleItem可以得到)的下一个item(“C”)刚好是下一个Section的第一个item,就可以返回PUSHING_STATE=2的状态,提示独立title不断地layout。下面就是获取title状态的方法:
//参数position 传入的是当前可视的第一个item在整个listview中的位置
public int getTitleState(int position) {
if (position < 0 || getCount() == 0) {
return 0;
}
int index = getSectionForPosition(position);
if(index==-1||index>sections.length){
return 0;
}
//当前可视的第一个item所在的section
int section = getSectionForPosition(position);
//下一个section的首位置
int nextSectionPosition = getPositionForSection(section + 1);
//如果下一个section的首位置等于当前可视的第一个item的位置+1,可以返回推动状态(2)了
if (nextSectionPosition != -1 && nextSectionPosition == position + 1) {
return 2;
}
return 1;
}
ps:还是建议大家用Magic Number来描述这三个状态,我是偷懒了,直接返回0,1,2。
有了判断三个状态的方法,我们就可以在listview中调用此方法,根据结果进行独立title的layout
MyListView.java
public class MyListView extends android.widget.ListView {
private View mTitle;
private boolean visible;
private int width;
private int height;
private MyAdapter mAdapter;
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if(visible){
drawChild(canvas, mTitle, getDrawingTime());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if(mTitle!=null){
measureChild(mTitle, widthMeasureSpec, heightMeasureSpec);
width=mTitle.getMeasuredWidth();
height=mTitle.getMeasuredHeight();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(mTitle!=null){
mTitle.layout(0, 0, width, height);
titleLayout(getFirstVisiblePosition());
}
}
public void setTitle(View view){
mTitle=view;
if(mTitle!=null){
setFadingEdgeLength(0);
}
requestLayout();
}
public void titleLayout(int firstVisiblePosition) {
if(mTitle==null){
return;
}
if(mAdapter==null||!(mAdapter instanceof MyAdapter)){
return;
}
int state=0;
state = mAdapter.getTitleState(firstVisiblePosition);
switch(state){
case 0:
visible=false;
break;
case 1:
if(mTitle.getTop()!=0){
mTitle.layout(0, 0, width, height);
}
mAdapter.setTitleText(mTitle,firstVisiblePosition);
visible=true;
break;
case 2:
View firstView=getChildAt(0);
if(firstView!=null){
int bottom=firstView.getBottom();
int headerHeight=mTitle.getHeight();
int top;
if(bottom<headerHeight){
top=(bottom-headerHeight);
}else{
top=0;
}
mAdapter.setTitleText(mTitle, firstVisiblePosition);
if(mTitle.getTop()!=top){
mTitle.layout(0, top, width, height+top);
}
visible=true;
}
break;
}
}
@Override
public void setAdapter(ListAdapter adapter) {
if(adapter instanceof MyAdapter){
mAdapter=(MyAdapter) adapter;
super.setAdapter(adapter);
}
}
}
MainActivity.java
public class MainActivity extends Activity {
private MyAdapter mAdapter;
private MyListView mListView;
private List<String> mList;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
mList=new ArrayList<String>();
String a="";
for(int i=0;i<26;i++){
a=((char)('A'+i))+"";
mList.add(a);
mList.add(a);
mList.add(a);
}
mListView=(MyListView)findViewById(R.id.lv);
mAdapter=new MyAdapter();
mAdapter.update(mList,MainActivity.this);
mListView.setTitle(LayoutInflater.from(MainActivity.this).inflate(R.layout.title, mListView, false));
mListView.setAdapter(mAdapter);
mListView.setOnScrollListener(mAdapter);
}
}
main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<!-- 注意改一下包名,改成你的包名-->
<com.lk.test.MyListView
android:id="@+id/lv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="@null"
/>
</RelativeLayout>
MyListView中的titleLayout方法:
public void titleLayout(int firstVisiblePosition) {
if(mTitle==null){
return;
}
if(mAdapter==null||!(mAdapter instanceof MyAdapter)){
return;
}
int state=0;
state = mAdapter.getTitleState(firstVisiblePosition);
switch(state){
case 0:
visible=false;
break;
case 1:
if(mTitle.getTop()!=0){
mTitle.layout(0, 0, width, height);
}
mAdapter.setTitleText(mTitle,firstVisiblePosition);
visible=true;
break;
case 2:
View firstView=getChildAt(0);
if(firstView!=null){
int bottom=firstView.getBottom();
int itemHeight=mTitle.getHeight();
int top;
if(bottom<itemHeight){
top=(bottom-itemHeight);
}else{
top=0;
}
mAdapter.setTitleText(mTitle, firstVisiblePosition);
if(mTitle.getTop()!=top){
mTitle.layout(0, top, width, height+top);
}
visible=true;
}
break;
}
}
case 2也就是PUSHING_STATE的时候,调用getChildAt(0)得到当前可视的第一个view,不断的获取它的bottom在Y坐标轴的值bottom(bottom=firstView.getBottom()),如果该值开始小于一个item的高(itemHeight=mTitle.getHeight(),此时注意:独立title的高必须与item中的title高一样,都是wrap_content,否则在推动的时候会有一点误差)的时候,说明第一个item的顶部开始超出y的正轴,准备移动到负轴了。定义一个变量int top=bottom-itemHeight,top的值是负数,也恰好是第一个可视item顶部边界(firstView.getTop())所在Y轴的值,这时候我们令独立Title的位置画的跟第一个可视item的位置一样,调用:mTitle.layout(0,top,width,height+top),其中width和height是在onMeasure的时候确定的,是title的宽度和高度;
case 1的时候只需在mTitle.getTop()!=0的时候调用mTitle.layout(0,0,width,height),将其显示在listview的顶部。
最后在Adapter中重写onScroll监听器,在onScroll的时候不断调用titleLayout方法。
写代码的时候发现两个现象:
(如果在onMeasure,onLayout,dispatchDraw中打log,可以看到执行顺序大致为onMeasure-onMeasure-onLayout-dispatchDraw,其中onMeasure会被调用两次,第一次得到的宽高什么的都是0)
在listview.setOnScrollListener的时候,调用的是:
AbsListview的方法
public void setOnScrollListener(OnScrollListener l) {
mOnScrollListener = l;
invokeOnItemScrollListener();
}
/**
* Notify our scroll listener (if there is one) of a change in scroll state
*/
void invokeOnItemScrollListener() {
if (mFastScroller != null) {
mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
}
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
}
onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
}
所以onScroll会在这里被触发一次。
其他类中的部分方法不做解释,比较容易看懂。
就先写到这里吧,该回家吃饭了,公司空调都关了。。。。
第一次写博客,代码区不咋会用,乱的不能看了,抱歉