一、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(this, R.layout.list_item, mDatas)); }}
需要注意的是,上面的list_item布局中,TextView必须作为根节点,否则报错:
如果需要使用系统自定义的样式,由于ArrayAdapter是单行显示,所以只能用simple_list_item_1
mListView.setAdapter(new ArrayAdapter(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
如果需要用到系统的样式,有如下系统样式:
- 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中的数据时调用。@Overridepublic 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,代表的是可复用的对象,当它为空则创建一个对象,否则直接复用。
@Overridepublic 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中存储的成员变量进行复用即可。
@Overridepublic 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; @Overridepublic 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()中获取不同布局的种类数
@Overridepublic int getViewTypeCount() { return 3;}
c)此时我们需要给定义三个不同的布局,并创建三个不同的ViewHolder来针对不同的布局进行缓存复用
其他三个布局都是如此,只是指定的背景颜色不一样,同时定义三个ViewHolder
class ViewHolder1{ TextView textView;}class ViewHolder2{ TextView textView;}class ViewHolder3{ TextView textView;}
d)此时我们在getView中来判断常量,进行填充不同的布局以及设置资源等操作
@Overridepublic 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属性引入一个动画文件。
b) 在anin文件下创建该动画文件list_item_animation
android:delay的单位是s,每个Item出现的时间间隔
android:animation:表示每个Item对应的动画
android:animationOrder:动画执行顺序,normal从上到下;reverse从下到上;random随机
c) 动画文件又引入item_animation文件,该文件描述动画效果。
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 ListmDatas; 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。
c) 最后是item的布局,这里需要处理焦点问题
二、GridView
ListView是列表,GridView就是显示网格,他和ListView一样是AbsListView的子类,使用非常类似。
1.0、GridView网格线
前面我们知道ListView设置分割线是非常容易的,设置ListView的分割线颜色和宽度,只需要在布局中定义
android:divider和android:divideHeight属性即可。但是GridView并没有这样的方法。
a)其实实现这种效果并不难,原理是让每个item都设置成带有分割线的背景。
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被点击选中时的效果。
d) 给 GridView的item布局使用上面定义的selector
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 staticT get(View view, int id) { SparseArray sparseArray = (SparseArray ) view.getTag(); if (sparseArray == null) { sparseArray = new SparseArray (); view.setTag(sparseArray); } View childView = sparseArray.get(id); if (childView == null) { childView = view.findViewById(id); sparseArray.put(id, childView); } return (T) childView; }}
开源控件下载地址:
链接: 密码:hnyv