17、ListView & GridView
一、ListView
listView是用来解决大量的相似数据显示问题,同时大量的数据会导致不断在内存中创建对象,可能导致OOM。
1.1、listView的属性
该控件采用MVC的设计模式,而且还有大量适配器,一般情况是:BaseXXX、BasicXXX、SimpleXXX、DefaultXXX。
除此之外,来看看ListView的常用事件:
(1)setOnclickListener()
(2)setOnItemLongClickListener()
(3)setOnItemClickListener()
(4)setOnScrollListener()
(5)setOnItemSelectedListener()
(6)setOnTouchListener()
1.2、MVC模式的简单理解
- Model:通常可以理解为数据
- View:用户的操作接口,说白了就是GUI,应该使用哪种接口组件,组件间的排列位置与顺序都需要设计
- Controller:控制器,作为model与view之间的枢纽,负责控制程序的执行流程以及对象之间的一个互动
而这个Adapter则是中间的这个Controller的部分: Model(数据) ---> Controller(以什么方式显示到)---> View(用户界面)
1.3、Adapter概念解析
让我们来看看常用的Adapter:
- ArrayAdapter:支持泛型操作,最简单的一个Adapter,只能展现一行文字。
- SimpleAdapter:同样具有良好扩展性的一个Adapter,可以自定义多种效果。
- SimpleCursorAdapter:用于显示简单文本类型的listView,一般在数据库那里会用到。
- BaseAdapter:抽象类,实际开发中我们会继承这个类并且重写相关方法,用得最多的一个Adapter。
1、ArrayAdapter:它是BaseAdapter的子类,主要用于存放字符串。
ArrayAdapter<T>(context:这个参数表示上下文,一般都是this,
textViewResourceId:这个参数是指定自定义的布局文件,
objects):接收的参数,添加到ListView视图中的,类型根据泛型而变化。
public class MainActivity extends Activity { private ListView mListView; private static final String[] mDatas = {"功能1","功能2","功能3"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.listview); // 设置适配器,第三个数组是根据泛型而变化的 mListView.setAdapter(new ArrayAdapter<String>(this, R.layout.list_item, mDatas)); } }
需要注意的是,上面的list_item布局中,TextView必须作为根节点,否则报错:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_content" android:layout_width="match_parent" android:layout_height="wrap_content" />
如果需要使用系统自定义的样式,由于ArrayAdapter是单行显示,所以只能用simple_list_item_1
mListView.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1, new String[]{"功能1","功能2","功能3"}));
如果ArrayAdapter想要实现更多的效果则需要自定义ArrayAdapter,不过目前这种方式已经过时。
2、SimpleAdapter:它也是BaseAdapter的子类,用来实现一些图片、文字并排的效果。
SimpleAdapter(context:上下文
data:代表整个ListView的List集合,
resource:自定义的布局文件或系统布局文件,
from:对应的key的数组。
to):对应的value的数组。
public class MainActivity extends Activity { private ListView mListView; private String[] mDatas = {"声音","显示","存储","电池","应用"}; private int[] resources = {R.drawable.akb,R.drawable.akc,R.drawable.akd,R.drawable.ake,R.drawable.akf}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.listview); // 构建数据模型 List<Map<String, Object>> lists = new ArrayList<Map<String,Object>>(); for (int i = 0; i < resources.length; i++) { Map<String, Object> maps = new HashMap<String, Object>(); maps.put("name", mDatas[i]); maps.put("drawable", resources[i]); lists.add(maps); } mListView.setAdapter(new SimpleAdapter(this, lists, R.layout.item, new String[]{"name","drawable"}, new int[]{R.id.tv_content,R.id.iv_img})); } }
如果需要用到系统的样式,有如下系统样式:
- simple_list_item_1:单行文本组成
- simple_list_item_2:两行文本组成
- simple_list_item_checked:每项都是由一个已选中的列表框。
- simple_list_item_single_choice:都带有一个单选按纽。
- simple_list_item_multiple:全部带有一个复选框。
3.CursorAdapter:它同样是BaseAdapter的子类,它为Cursor和ListView提供连接的桥梁。
(1) newView():并不是每次都被调用,它只在实例化和数据增加时调用,而修改条目的内容时不会调用。
(2) bindView():在绘制item之前或重绘时一定会调用。
(3) changeCursor():类似于notifyDataSetChange()方法。
详细请参考附件中的实例
1.4、ListView优化
BaseAdapter: 是经常用到的基础数据适配器,它的主要用途是将一组数据传到像ListView、Spinner、Gallery及GrideView等组件。
(1) getCount():是listView的长度。
(2) getView(): 根据这个长度逐一绘制它的每一行。
(3) getItem()和getItemId()则需要处理和取得Adapter中的数据时调用。
@Override public View getView(int position, View convertView, ViewGroup parent) { View view = View.inflate(MainActivity.this, R.layout.item, null); TextView tvContent = (TextView) findViewById(R.id.tv_content); tvContent.setText(mDatas.get(position)); return view; }
第一种优化:复用对象
由于上方的代码每次需要一个View对象都会重新inflate一个view出来,没有实现对象的复用。
而系统给我们提供convertView,代表的是可复用的对象,当它为空则创建一个对象,否则直接复用。
@Override public View getView(int position, View convertView, ViewGroup parent) { View view; if(convertView == null){ view = View.inflate(MainActivity.this, R.layout.item, null); }else{ view = convertView; } TextView tvContent = (TextView) view.findViewById(R.id.tv_content); tvContent.setText(mDatas.get(position)); return view; }
第二种优化:减少查找次数
当converView为空时,会重新inflate一个View对象,除此之外还会findViewById进行查找工作,我们可以通过一个ViewHolder类来存储对应的
成员变量,然后通过getTag和setTag来操作,这时,当convertView为空时,只需要取出ViewHolder中存储的成员变量进行复用即可。
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if(convertView == null){ convertView = View.inflate(MainActivity.this, R.layout.item, null); holder = new ViewHolder(); holder.mTvContent = (TextView) convertView.findViewById(R.id.tv_content); convertView.setTag(holder); }else{ holder = (ViewHolder) convertView.getTag(); } holder.mTvContent.setText(mDatas.get(position)); return convertView; } class ViewHolder{ TextView mTvContent; }
第三种写法:分批分页加载(待整理)
1.5、ListView多样式
- 重写getViewTypeCount() -- 该方法返回多个不同的布局总数。
- 重写getItemViewType(int) -- 根据position返回响应的Item。
- 根据view item的类型,在getView中创建正确的convertView。
a)创建MyAdapter继承BaseAdapter,首先在适配的getItemViewType()中通过计算得出不同的状态,用常量进行标记
public static final int TYPE_1 = 0; public static final int TYPE_2 = 1; public static final int TYPE_3 = 2; @Override public int getItemViewType(int position) { int p = position % 6; if(p == 0){ return TYPE_1; }else if (p < 3) { return TYPE_2; }else if (p < 6) { return TYPE_3; }else{ return TYPE_1; } }
注意:type必须从0开始,否则会报数组角标越界异常。
b)在getViewTypeCount()中获取不同布局的种类数
@Override public int getViewTypeCount() { return 3; }
c)此时我们需要给定义三个不同的布局,并创建三个不同的ViewHolder来针对不同的布局进行缓存复用
<TextView android:id="@+id/textview1" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center" android:text="我是绿色的" />
其他三个布局都是如此,只是指定的背景颜色不一样,同时定义三个ViewHolder
class ViewHolder1{ TextView textView; } class ViewHolder2{ TextView textView; } class ViewHolder3{ TextView textView; }
d)此时我们在getView中来判断常量,进行填充不同的布局以及设置资源等操作
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder1 holder1 = null; ViewHolder2 holder2 = null; ViewHolder3 holder3 = null; int type = getItemViewType(position); if(convertView == null){ // 按当前所需样式,确定new出的布局 switch (type) { case TYPE_1: convertView = View.inflate(MainActivity.this, R.layout.item1, null); holder1 = new ViewHolder1(); holder1.textView = (TextView) convertView.findViewById(R.id.textview1); convertView.setTag(holder1); break; case TYPE_2: convertView = View.inflate(MainActivity.this, R.layout.item2, null); holder2 = new ViewHolder2(); holder2.textView = (TextView) convertView.findViewById(R.id.textview2); convertView.setTag(holder2); break; case TYPE_3: convertView = View.inflate(MainActivity.this, R.layout.item3, null); holder3 = new ViewHolder3(); holder3.textView = (TextView) convertView.findViewById(R.id.textview3); convertView.setTag(holder3); break; } }else{ switch (type) { case TYPE_1: holder1 = (ViewHolder1) convertView.getTag(); break; case TYPE_2: holder2 = (ViewHolder2) convertView.getTag(); break; case TYPE_3: holder3 = (ViewHolder3) convertView.getTag(); break; } } // 根据不同样式设置资源 switch (type) { case TYPE_1: holder1.textView.setText("我是绿色"+mDatas.get(position)); break; case TYPE_2: holder2.textView.setText("我是蓝色"+mDatas.get(position)); break; case TYPE_3: holder3.textView.setText("我是红色"+mDatas.get(position)); break; } return convertView; }
1.6、ListView的Item动画
a) 在ListView布局使用layoutAnimation属性引入一个动画文件。
<ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" android:layoutAnimation="@anim/list_item_animation"> </ListView>
b) 在anin文件下创建该动画文件list_item_animation
<?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:delay="0.2" android:animation="@anim/item_animation" android:animationOrder="normal"/>
android:delay的单位是s,每个Item出现的时间间隔
android:animation:表示每个Item对应的动画
android:animationOrder:动画执行顺序,normal从上到下;reverse从下到上;random随机
c) 动画文件又引入item_animation文件,该文件描述动画效果。
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="100%" android:fromYDelta="0" android:toXDelta="0" android:toYDelta="0" android:duration="1000"/> <alpha android:fromAlpha="0" android:toAlpha="1" android:duration="1000"/> <rotate android:fromDegrees="0" android:toDegrees="360" android:pivotX="50%" android:pivotY="50%" android:duration="1000"/> </set>
d) 我们也可以在代码中来设置Item的加载动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.item_animation); LayoutAnimationController animationController = new LayoutAnimationController(animation); animationController.setDelay(0.4f);// 设置间隔时间 animationController.setOrder(LayoutAnimationController.ORDER_NORMAL);// 设置列表显示顺序 mListView.setLayoutAnimation(animationController);
1.7、ListView的焦点
android:descendantFocusability="blocksDescendants"
如题,在Item布局的根节点添加上述属性,android:descendantFocusability="blocksDescendants" 即可,另外该属性有三个可供选择的值:
- beforeDescendants:viewgroup会优先其子类控件而获取到焦点
- afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
- blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
例如:ListView的Item条目中的Button事件冲突解决。
- 在ItemView配置的xml文件中的根节点添加属性android:descendantFocusability="blocksDescendants"
- 在要添加事件的控件上添加android:focusable="false"
2.1、ListView中使用CheckBox错位问题
思路:
首先ListView在使用到CheckBox的时候会存在焦点问题,我们可以使用上方的方式处理该问题,其次是Item会被复用的问题,
我们可以通过在Bean中创建一个变量isChecked标记当前Item是否被选中的变量即可,当选中条目时就将isChecked设置为true,则可以解决错位问题。
a) 在MainActivity中创建ListView并创建适配器
public class MainActivity extends AppCompatActivity { private ListView mListView; private List<ItemBean> mDatas; private MyAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.lv_listview); // 模拟数据 mDatas = new ArrayList<>(); for (int i = 0; i< 100; i++) { ItemBean item = new ItemBean(); item.setName("名字" + i); item.setAge("年龄" + i); mDatas.add(item); } mAdapter = new MyAdapter(); mListView.setAdapter(mAdapter); // 条目点击事件 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { MyAdapter.ViewHold hold = (MyAdapter.ViewHold) view.getTag(); ItemBean itemBean = mDatas.get(position); if(hold.cbClick.isChecked()) { hold.cbClick.setChecked(false); itemBean.setChecked(false); }else { hold.cbClick.setChecked(true); itemBean.setChecked(true); } } }); } private class MyAdapter extends BaseAdapter { @Override public int getCount() { return mDatas.size(); } @Override public Object getItem(int position) { return mDatas.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHold hold; if (convertView == null) { convertView = View.inflate(MainActivity.this,R.layout.item_name_list, null); hold = new ViewHold(); hold.cbClick = (CheckBox) convertView.findViewById(R.id.cb_check); hold.tvName = (TextView) convertView.findViewById(R.id.tv_name); hold.tvAge = (TextView) convertView.findViewById(R.id.tv_age); convertView.setTag(hold); }else { hold = (ViewHold) convertView.getTag(); } ItemBean itemBean = mDatas.get(position); hold.cbClick.setChecked(itemBean.isChecked()); hold.tvName.setText(itemBean.getName()); hold.tvAge.setText(itemBean.getAge()); return convertView; } class ViewHold { CheckBox cbClick; TextView tvName; TextView tvAge; } } }
b) 然后是MainActivity的布局,Bean的字段为Name、Age和isCheckd。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="phoneserver.hll.com.myapplication.MainActivity"> <ListView android:id="@+id/lv_listview" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </RelativeLayout>
c) 最后是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="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants" android:padding="10dp"> <CheckBox android:id="@+id/cb_check" android:layout_width="wrap_content" android:layout_height="wrap_content" android:focusable="false"/> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="名字"/> <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:text="年龄"/> </RelativeLayout> </LinearLayout>
二、GridView
ListView是列表,GridView就是显示网格,他和ListView一样是AbsListView的子类,使用非常类似。
1.0、GridView网格线
前面我们知道ListView设置分割线是非常容易的,设置ListView的分割线颜色和宽度,只需要在布局中定义
android:divider和android:divideHeight属性即可。但是GridView并没有这样的方法。
a)其实实现这种效果并不难,原理是让每个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="match_parent" android:orientation="vertical" > <ScrollView android:layout_width="fill_parent" android:layout_height="wrap_content" android:fillViewport="true" android:scrollbars="none" > <com.finddreams.alipay.MyGridView android:id="@+id/gridview" android:layout_width="fill_parent" android:layout_height="wrap_content" android:horizontalSpacing="0.0dip" android:listSelector="@null" android:numColumns="2" android:scrollbars="none" android:stretchMode="columnWidth" android:verticalSpacing="0.0dip" /> </ScrollView> </LinearLayout>
b) 考虑到有时候Item会比较多的情况,一般用ScrollView嵌套起来。但是这样会导致只显示第一行。
产生问题的原因是因为GridView和ListView都是根据子item的宽高来显示大小的,而嵌套ScrollView中上下滑动就导致
系统无法正确的识别item的大小。下面是解决方案:
public class MyGridView extends GridView { public MyGridView(Context context, AttributeSet attrs) { super(context, attrs); } public MyGridView(Context context) { super(context); } public MyGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } }
c) 定义一个selector,在里面设置形状外矩形rectangle,设置这个矩形的stroke描边属性的颜色为分割线的颜色,然后
在不同的state的item中设置不同的gradient渐变属性,从而实现单个item被点击选中时的效果。
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape android:shape="rectangle"> <gradient android:angle="270.0" android:endColor="#ffe8ecef" android:startColor="#ffe8ecef" /> <stroke android:width="1.0px" android:color="@color/line" /> </shape> </item> <item android:state_focused="true"> <shape android:shape="rectangle"> <gradient android:angle="270.0" android:endColor="#ffe8ecef" android:startColor="#ffe8ecef" /> <stroke android:width="1.0px" android:color="@color/line" /> </shape> </item> <item> <shape android:shape="rectangle"> <gradient android:angle="270.0" android:endColor="#ffffffff" android:startColor="#ffffffff" /> <stroke android:width="1.0px" android:color="@color/line" /> </shape> </item> </selector>
d) 给 GridView的item布局使用上面定义的selector
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_margin="0.0dip" android:background="@color/griditems_bg" > <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_centerInParent="true" android:background="@drawable/bg_gv" android:padding="12.0dip" > <ImageView android:id="@+id/iv_item" android:layout_width="58.0dip" android:layout_height="58.0dip" android:layout_centerHorizontal="true" android:contentDescription="@string/app_name" /> <TextView android:id="@+id/tv_item" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/iv_item" android:layout_centerHorizontal="true" android:layout_marginTop="5.0dip" android:maxLines="1" android:textColor="@color/commo_text_color" android:textSize="14.0sp"/> </RelativeLayout> </RelativeLayout>
e) 之后是适配器的编写,比较简单则不详细叙述
public class MyGridAdapter extends BaseAdapter { private Context mContext; public String[] strs = { "转账", "余额宝", "手机充值", "信用卡还款", "淘宝电影", "**", "当面付", "亲密付", "机票", }; public int[] imgs = { R.drawable.app_transfer, R.drawable.app_fund, R.drawable.app_phonecharge, R.drawable.app_creditcard, R.drawable.app_movie, R.drawable.app_lottery, R.drawable.app_facepay, R.drawable.app_close, R.drawable.app_plane }; public MyGridAdapter(Context mContext) { super(); this.mContext = mContext; } @Override public int getCount() { return strs.length; } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = View.inflate(mContext, R.layout.grid_item, null); } TextView tv = BaseViewHolder.get(convertView, R.id.tv_item); ImageView iv = BaseViewHolder.get(convertView, R.id.iv_item); iv.setBackgroundResource(imgs[position]); tv.setText(strs[position]); return convertView; } }
f) 比较重要的是下面的这个万能ViewHolder的写法,详细demo参考附件GridView网格
public class BaseViewHolder { @SuppressWarnings("unchecked") public static <T extends View> T get(View view, int id) { SparseArray<View> sparseArray = (SparseArray<View>) view.getTag(); if (sparseArray == null) { sparseArray = new SparseArray<View>(); view.setTag(sparseArray); } View childView = sparseArray.get(id); if (childView == null) { childView = view.findViewById(id); sparseArray.put(id, childView); } return (T) childView; } }
开源控件下载地址:
链接:http://pan.baidu.com/s/1pLs0n2r 密码:hnyv