Rock with Android


ListView

大部分应用的内容都是是纵向的列表,因为手机上下划屏成本很低,而且用户对下面的内容长度是有心理预期的,所以大部分应用都是使用这个设计的。对应这个需求,安卓里面提供了ListView这个控件,是做安卓开发打交道最多的,类似iOS的TableView。

ListView是一个多行的列表,每行是一个item,item本身对应一个Layout。ListView本身是AdapterView的派生类,需要绑定Adapter来负责管理生成对应每行的item。item的View是复用的,即使Adapter里面的item有成百上千,但是真正生成的View一般只有屏幕显示的那么多个。这和iOS的TableCell的复用是一致的。

        //下面是一个ListView在layout里的样子
        <ListView
            android:id="@id/list"
            android:layout_height="match_parent"
            android:layout_width="match_parent"
            android:fadingEdge="none"
            android:fastScrollEnabled="false"
            android:cacheColorHint="#00000000"
        />
        //下面是具体的一个item的layout,item_notify.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="wrap_content"
            android:orientation="horizontal"
            android:padding="10dp"
            android:gravity="center_vertical"
            >
            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_weight="1.0"
                >
                <TextView
                    android:id="@id/title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:textColor="#000000"
                    android:paddingBottom="2dp"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentTop="true"
                    />
                <TextView
                    android:id="@id/time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="14sp"
                    android:textColor="#999999"
                    android:layout_below="@id/title"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentBottom="true"
                    />
            </RelativeLayout>
            <ImageView
                android:id="@id/icon"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:scaleType="centerInside"
                />
        </LinearLayout>
        

下面是典型的调用过程:

        ListView list = (ListView)findViewById(R.id.list);
        ListAdapter adapter = new ListAdapter();
        list.setAdapter(adapter);
        

Adapter

    //假设数据是存在一个叫做data 的数组里的
    class ListAdapter extends BaseAdapter{
        //返回item的数量
        public int getCount() {
            return data.length;
        }

        //返回对应位置item的数据对象
        public Object getItem(int position) {
            return data[position];
        }

        //返回对应位置item的id,一般不直接用这个函数,而是getItem得到对象后,用对象的id
        public long getItemId(int position) {
            return 0;
        }

        //使用ViewHolder机制,避免findViewById的操作,优化性能
        public View getView(int position, View convertView, ViewGroup parent){
            //注意这里使用了View的tag属性来把一个ViewHolder对象绑定在一个convertView上面
            //convertView就是对应item的真正View
            ViewHolder holder = null;
            //如果没有可以复用的convertView,也就是真正要创建item的layout
            if (convertView == null) {
                holder = new ViewHolder();
                convertView = getLayoutInflater().inflate(R.layout.item_notify, null);
                holder.icon = (ImageView)convertView.findViewById(R.id.icon);
                holder.title = (TextView)convertView.findViewById(R.id.title);
                holder.time = (TextView)convertView.findViewById(R.id.time);
                //把holder对象绑定到convertView上
                convertView.setTag(holder);
            //如果convertView已经被创建过,也就是说该item被复用
            } else {
                //从tag里取出来holder对象
                holder = (ViewHolder) convertView.getTag();
            }
            refreshView(position, holder);
            return convertView;
        }

        private void refreshView(int position, ViewHolder h) {
            //根据position对应的item数据对象,具体的更新UI
        }
    };

    static class ViewHolder {
        ImageView icon;
        TextView title;
        TextView time;
    };
        

当adapter的数据有任何变化时,需要及时调用adapter.notifyDataSetChanged()来通知ListView来更新界面,否则会在滑动或UI重绘时,因为数据已经变了,导致fc。

优化

  1. 单个item的版式尽量规整, 如果有大图,一屏内为1-1.5个item比较合适
  2. 如果图片较大,尽量固定规格,尤其高度。
  3. 如果单个item的Layout比较复杂,可以考虑使用ViewSub的缓式创建,优化ListView的第一次加载速度
  4. 尽量减少item的Layout层级,如果结构简单,每个item只有5个左右的View的化,直接使用隐藏/显示组件,性能也足够,不用使用ViewSub。
  5. 当item和item内的控件都可以点击时,需要留出些空白区域,使单条容易点击。
            <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                //这里的利用了Layout的descendantFocusability属性
                //afterDescendants表示单条只有在里面的控件没有得到焦点时,最外面的item整个才获得焦点。
                android:descendantFocusability="afterDescendants"
                >
                ...
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
            </LinearLayout>
                
  6. 单条操作如果不是单击或长按时,需要给提示(比如滑动删除)。
  7. 如果图片太大,滑动卡顿,可以考虑只在滑动停止时加载图片,滑动时不加载,使用默认底色或图片。
        //给ListView设置AbsListView.OnScrollListener
        listView.setOnScrollListener(new AbsListView.OnScrollListener(){
            public void onScrollStateChanged(AbsListView view, int scrollState){
                if (lastScrollState == SCROLL_STATE_IDLE){
                    //加载图片
                }
            }
    
        });
                
  8. 如果需要做“主帖+回复列表”这种样式,可以使用addHeaderView。
  9. 推荐使用类似gmail的新版ActionBar-PullToRefresh作下拉刷新效果。
  10. 做翻页时,不推荐“上拉加载更多”,而是使用到底自动加载,或者到底显示加载更多按钮,点击后加载更多。具体实现可以到github搜索android endless。

目录上一章图片处理