打气筒(LayoutInflater对象)介绍:
MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;public class MainActivity extends AppCompatActivity {private String TAG = "MainActivity";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 找到控件ListView lv = (ListView) findViewById(R.id.lv);// 设置数据适配器lv.setAdapter(new MyAdapter());}private class MyAdapter extends BaseAdapter {@Overridepublic int getCount() {return 7;}@Overridepublic Object getItem(int i) {return null;}@Overridepublic long getItemId(int i) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {// 1.想办法把自己定义的布局转换成一个view对象就可以了View view;if (convertView == null) {// 创建新的view对象,可以通过打气筒把一个布局资源转换成一个view对象// resource 就是我们定义的布局文件// 第一种获取打气筒服务// view = View.inflate(getApplicationContext(), R.layout.item, null);// 第二种打气筒写法view = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, parent, false);// 第三种打气筒写法/*LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);view = inflater.inflate(R.layout.item, parent, false);*/Log.d(TAG, "新建的item: " + position);} else {view = convertView;Log.d(TAG, "回收的item" + position);}return view;}}
}
如果inflate(R.layout.item, null);如果item比较少,没有占满屏幕空间,那么第二次及以后打开应用程序在日志中会看到创建新item和使用回收item的交替奇怪现象,这种交替情况也会在ListView设置layout_height="wrap_content"时出现,所以ListView的layout_height要设置为match_parent。 如果inflate(R.layout.item, parent, false);布局按照R.layout.item来,可以手动设置宽高看到效果(仅仅宽度,高度不管是wrap_content还是match_parent或者其他具体值展示出来的效果是一样的,由系统内部指定了),推荐使用。但如果是RecyclerView则不一样,指定的高度就按指定的高度来。
第三个参数为false表示暂时不要附属到父ListView,以方便下一步操作,比如设置控件的属性。
最后return view;ListView会将返回值作为子项添加进来。 如果inflate(R.layout.item, parent, true);或者view = inflater.inflate(R.layout.item, parent);效果相同,但是都会报错 java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView
因为如果第三个参数为true,那么返回的就是根节点parent—>listview。
我们现在分析如果第三个参数为true时为什么出错。
首先看到我们之前有设置数据适配器,lv.setAdapter(new MyAdapter());
现在看到ListView源码的setAdapter
public void setAdapter(ListAdapter adapter) {// mAdapter为红色看不了,说明这个变量不在这个类里,那么查找父类AbsListView(抽象类)里面// 有一个ListAdapter mAdapter;if (mAdapter != null && mDataSetObserver != null) {mAdapter.unregisterDataSetObserver(mDataSetObserver);}resetList();mRecycler.clear();if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);} else {mAdapter = adapter;}mOldSelectedPosition = INVALID_POSITION;mOldSelectedRowId = INVALID_ROW_ID;// AbsListView#setAdapter will update choice mode states.super.setAdapter(adapter);......}
首先我们通过打印log可以知道这个parent就是ListView实例,那么再通过源码来找找看,在父类AbsListView(抽象类)声明了ListAdapter mAdapter(ListAdapter接口的引用),这个mAdapter已经用new myAdapter()实例化了,那么到底什么时候调用getView呢?
调用getView会回调实现类的getView,我们来看看mAdapter什么时候调用了getView,现在来看看AbsListView里面的mAdapter.getView(…)会在哪里调用?
找到有一句
final View updatedView = mAdapter.getView(position, transientView, this);
这里的getView声明为View getView(int position, View convertView, ViewGroup parent);
传进去了this,parent实例化为this,这个this指什么?别忘了我们是通过lv对象找上来的,this就是ListView实例啊,解决了上面的一个问题。
至于为什么传true出错,看到public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法
首先声明了View result = root ;//最终返回值为result 中间执行了temp = createViewFromTag(root, name, attrs);
创建了View
然后接着看到
if(root!=null){params = root.generateLayoutParams(attrs);if (!attachToRoot){temp.setLayoutParams(params);}}可以看到,当root不为null,attachToRoot为false时,为temp设置了LayoutParams.继续往下,
if (root != null && attachToRoot){root.addView(temp, params);}当root不为null,attachToRoot为true时,将tmp按照params添加到root中。然后
if (root == null || !attachToRoot) { result = temp; }
如果root为null,或者attachToRoot为false则,将temp赋值给result。 最后返回result。
从上面的分析已经可以看出:
Inflate(resId , null ) 只创建temp ,返回temp
Inflate(resId , parent, false )创建temp,然后执行temp.setLayoutParams(params);返回temp
Inflate(resId , parent, true ) 创建temp,然后执行root.addView(temp, params);最后返回root
由上面已经能够解释:
Inflate(resId , null )不能正确处理宽和高是因为:layout_width,layout_height是相对了父级设置的,必须与父级的LayoutParams一致。而此temp的getLayoutParams为null
Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这个params正是root.generateLayoutParams(attrs);得到的。
Inflate(resId , parent,true )不仅能够正确的处理,而且已经把resId这个view加入到了parent,并且返回的是parent,和以上两者返回值有绝对的区别,还记得文章前面的例子上,MyAdapter里面的getView报的错误:
java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView
这是因为源码中调用了root.addView(temp, params);而此时的root是我们的ListView对象,而ListView里面没有addView方法,我们继续往父类上面找,看到了AdapterView里面有addView方法,ListView为AdapterView的子类: 直接看AdapterView的源码:
@Overridepublic void addView(View child) {thrw new UnsupportedOperationException("addView(View) is not supported in AdapterView");}
可以看到这个错误为啥产生了。
AdapterView下不应该有任何视图!
获取打气筒LayoutInflater对象推荐第二种或者第三种写法,即
LayoutInflater.from(getApplicationContext())或者(LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
ArrayAdapter源码是第二种写法,SimpleAdapter的源码是第三种写法。这两种写法都可以。
inflater从指定的xml结点加载布局只推荐inflate(R.layout.item, parent, false);写法,源码都是这么写的
item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/iv_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/ic_launcher_round" /><TextViewandroid:id="@+id/tv_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="3dp"android:layout_toRightOf="@+id/iv_icon"android:ellipsize="end"android:lines="1"android:text="谢霆锋王菲旧情复燃阿迪法拉利删掉了福建省富拉尔基发父路径爱上附件为"android:textColor="#000000"android:textSize="20sp" /><TextViewandroid:id="@+id/tv_message"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_below="@id/tv_title"android:layout_toRightOf="@id/iv_icon"android:ellipsize="end"android:lines="1"android:text="谢霆锋王菲旧情复燃阿迪法拉利删掉了福建省富拉尔基发父路径爱上附件为"android:textColor="#999999"android:textSize="15sp" />
</RelativeLayout>
activity_main.xml
<?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"><ListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:layout_height="match_parent"></ListView></LinearLayout>
运行结果:
如果需要隐藏列表项的分割线, 可在对应xml 文件中的 ListView XML 元素上设置 两个属性。
我们希望将 android**:divider 设置为 “@null” 并将 android:dividerHeight 设置为 “0dp”。**
<ListView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/list"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:divider="@null"android:dividerHeight="0dp"/>
设置前(左)和设置后(右)的示范图:
ArrayAdapter用法:
简单来讲一下ListView和ArrayAdapter:
我们可以将 ListView 看成由 ArrayAdapter 提供支持,没有该适配器的话 ListView 就是个空的容器。提供支持是指 ArrayAdapter 关系到应该显示在屏幕上的数据集,例如,它可以关系到数组甚至数据列表。所以 ArrayAdapter 知道如何将该数据传输或调整到列表项视图中,并在 ListView 中显示。
我们来详细了解下这一切的原理
当你第一次将适配器与 ListView 相关联时 该 ListView 会询问,你想要显示多少个项?
ArrayAdapter 知道这一信息,所以 ListView 对 ArrayAdapter 调用方法,即用户当前正在查看的列表位置。比如它会传入位置 0
但用户也可能位于列表中的位置 1 处 甚至 100 处,知道这一信息后,ArrayAdapter 会查看数据的内部来源,如果传入数组,那么它会查看数组,若传入列表比如ArrayList,它也可能会查看列表(如果列表是来源数据的话)并获取相关信息。ArrayAdapter具有说明来告诉它自己如何创建列表项视图,并返回给ListView,当屏幕被占满后 ListView 将停止向ArrayAdapter 寻求更多的列表项,列表项视图仅在需要时才创建,**当视图被滚动离开屏幕后,它们就会被添加到Scrap Pile,**比如前两个列表项不再可见,它们将进入 Scrap Pile,然后当我们请求新的列表项时,我们可以通过将这些视图再返回到 ArrayAdapter 重复使用它们,**ListView 将请求列表中特定位置的视图,同时传入之前用过的视图。比如,ListView 请求的是位置 6 处的项,并向 ArrayAdapter 传入可重复使用的以前视图,ArrayAdapter 可以通过在回收过的视图里放入数据,使用回收的视图,**然后再接着比如通过调用 TextView setText() 方法来更改名字,这样我们可以向 ListView 返回全新的列表项以便添加到新的层级并显示到屏幕上。
我们将讨论下适配器可以如何应用到每个类别
ListView.setAdapter() 方法需要 ListAdapter 作为其输入参数,所以 ListAdapter 是个接口,意味着没有实现任何状态,所有方法都是抽象的,此外 Android 团队创建了 BaseAdapter 类,它是个抽象类,为 ListAdapter 的某些方法提供了实现,同时让其他某些方法保留为抽象方法,这时候就需要 ArrayAdapter 了。ArrayAdapter 是个具体类,方法都实现了,没有方法保留为抽象方法。我们可以创建一个 ArrayAdapter 对象实例并用在我们的应用中,因为我们已经验证了 ArrayAdapter 是个 ListAdapter,我们可以将 ArrayAdapter 对象作为listView.setAdapter() 输入参数传入。
在 Android 中适配器模式是个非常常见的模式。适配器知道数据来源是什么,例如数组或列表。并且知道如何将每项呈现为视图,与此同时 ListView 负责在屏幕上显示这些视图,检测用户的触摸手势,并跟踪用户是否位于整个列表中。 ListView lv = (ListView) findViewById(R.id.lv); ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item1, R.id.tv_name, objects); // 适合自定义布局 lv.setAdapter(adapter);
这三行代码总结了ListView 和 ArrayAdapter 之间的这些互动关系。
暂时你可以想象成用户界面与数据模型之间是分开的,分开的并不完全清晰,因为 ArrayAdapter 处理的是视图,但是主要是 ListView 负责处理用户界面的细节内容,而适配器负责数据,因为界面和数据二者是分开的,你完全可以将某部分替换为其他内容。例如,你可以将同一 ArrayAdapter 与 GridView 相关联,ArrayAdapter 逻辑完全保持不变,GridView 请求的是网格项视图而不是列表项行,适配器依然负责提供这里的每个视图。
你看过 Android 中的下拉菜单吗?在 Android 中,这些叫做 Spinner,你猜怎么着?要填充下拉菜单中的每项,我们为其关联了一个适配器,菜单中的每项都来自数据来源中的某项,可以看出,当你构建 Android 应用时,就会遇到适配器模式。
MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ArrayAdapter;
import android.widget.ListView;/*** 不管是什么adapter,作用就是把数据展示到listview*/
public class MainActivity extends AppCompatActivity {String objects[] = {"张三", "李四", "王五", "老六", "凑数", "还有谁"};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 1。找到控件ListView lv = (ListView) findViewById(R.id.lv);// 2.创建一个arrayAdapter//ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, objects);/*第二个参数resource:包含要在实例化视图时使用的布局文件的资源ID。第三个参数textViewResourceId:要填充的布局资源中TextView的id*/ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item1, R.id.tv_name, objects); // 适合自定义布局// 设置数据适配器lv.setAdapter(adapter);}
}
item1.xml
<?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"><TextViewandroid:id="@+id/tv_name"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="30sp"android:text="TextView" />
</LinearLayout>
批注:
ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item1, R.id.tv_name, objects);
这是4个参数的方法public ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects)
第二个参数是布局,第三个是textview的id
如果换成3个参数的方法也可以,ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item1, objects);
那么方法声明如下public ArrayAdapter(Context context, int resource, T[] objects)
第二个参数就是布局R.layout.item1。
但是得注意,这么写一定要将根结点变成TextView结点,并且加上xmlns:android=“http://schemas.android.com/apk/res/android”
如果不加,则会报异常java.lang.IllegalStateException: ArrayAdapter requires the resource ID to be a TextView
解决方案如下:
item1.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_height="wrap_content"android:layout_width="match_parent" />
运行结果:
如果是自定义适配器呢?比如public class WordAdapter extends ArrayAdapter {…}这里必须要创建构造器去匹配父类,因为父类没有默认的空构造器。
但是如果我们在外面调用只需要传入2个参数WordAdapter itemsAdapter = new WordAdapter(this, list);
那么WordAdapter构造器怎么创建呢?它的父类ArrayAdapter并没有2个参数的构造器,最少也是声明为
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,@NonNull List<T> objects) {...}
第二个参数为布局id,我们传入0即可
public class WordAdapter extends ArrayAdapter<Word> {public WordAdapter(Context context, ArrayList<Word> list) {super(context, 0, list);}
}
解释如下:
在这里,我们为上下文和列表初始化ArrayAdapter的内部存储。当ArrayAdapter填充单个TextView时,使用第二个参数。因为这是我们自定义的适配器,所以适配器将不使用第二个参数,因此它可以是任何值。在这里,我们使用0。
如果不传入布局id,我们该怎么才能按照这个布局来显示呢?
我们使用 LayoutInflater将 XML 布局文件变成实际的视图对象,我们手动inflate视图,不需要在构造函数中super向父类传入布局资源 ID,直接LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false);即可
具体的示例请见下面链接 ↓↓↓↓↓↓↓↓
关于具有自定义 ArrayAdapter 的示例应用见这里:
https://github.com/udacity/ud839_CustomAdapter_Example
不管调用ArrayAdapter的哪个重载方法,最终到这个方法:
private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,@IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) {mContext = context;mInflater = LayoutInflater.from(context);mResource = mDropDownResource = resource;mObjects = objects;mObjectsFromResources = objsFromResources;mFieldId = textViewResourceId;}
这里用到的是刚刚介绍的三种拿到打气筒LayoutInflater对象的方式的第二种,源码这么写LayoutInflater.from(context),第三种也可以,我个人比较喜欢这种写法,拿到打气筒LayoutInflater是LayoutInflater.from(context);
而ArrayAdapter继承了BaseAdapter,而BaseAdapter是个抽象类,那么一定有重写getCount方法,getView方法等4个方法
@Overridepublic int getCount() {return mObjects.size();}
这个mObject就是外面传进来的字符串数组asList转换成固定大小的List集合的引用,所以有几个字符串就显示几个item,该方法返回此适配器表示的数据集中有多少项。
再来看getView方法
@Overridepublic @NonNull View getView(int position, @Nullable View convertView,@NonNull ViewGroup parent) {return createViewFromResource(mInflater, position, convertView, parent, mResource);}private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,@Nullable View convertView, @NonNull ViewGroup parent, int resource) {final View view;final TextView text;if (convertView == null) {view = inflater.inflate(resource, parent, false);} else {view = convertView;}try {if (mFieldId == 0) {// If no custom field is assigned, assume the whole resource is a TextViewtext = (TextView) view;} else {// Otherwise, find the TextView field within the layouttext = view.findViewById(mFieldId);if (text == null) {throw new RuntimeException("Failed to find view with ID "+ mContext.getResources().getResourceName(mFieldId)+ " in item layout");}}} catch (ClassCastException e) {Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");throw new IllegalStateException("ArrayAdapter requires the resource ID to be a TextView", e);}final T item = getItem(position);if (item instanceof CharSequence) {text.setText((CharSequence) item);} else {text.setText(item.toString());}return view;}
这里看到了inflater从指定的xml结点加载布局写法,推荐。
if (convertView == null) { view = inflater.inflate(resource, parent, false); } else { view = convertView; }
SimpleAdapter用法:
MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import android.widget.SimpleAdapter;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 1.找到控件ListView lv = (ListView) findViewById(R.id.lv);// 1.1准备listview要显示的数据List<Map<String, String>> data = new ArrayList<Map<String, String>>();Map<String, String> map1 = new HashMap<String, String>();map1.put("name", "张飞");map1.put("phone", "1388888");Map<String, String> map2 = new HashMap<String, String>();map2.put("name", "赵云");map2.put("phone", "110");Map<String, String> map3 = new HashMap<String, String>();map3.put("name", "貂蝉");map3.put("phone", "1386666");Map<String, String> map4 = new HashMap<String, String>();map4.put("name", "关羽");map4.put("phone", "119");// 1.2把Map加到listdata.add(map1);data.add(map2);data.add(map3);data.add(map4);// 2.定义数据适配器,我们定义的布局文件SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.item,new String[]{"name", "phone"}, new int[]{R.id.tv_name, R.id.tv_phone});// 3,设置数据是配资lv.setAdapter(adapter);}
}
item.xml
<?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="horizontal"><TextViewandroid:id="@+id/tv_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="测试1"android:textSize="20sp" /><TextViewandroid:id="@+id/tv_phone"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="测试2"android:textColor="#ff0000"android:textSize="20sp" />
</LinearLayout>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:layout_height="match_parent"></ListView>
</RelativeLayout>
运行结果:
分析一下SimpleAdapter源码:
构造方法就这一种,没有其他重载方法。
public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,@LayoutRes int resource, String[] from, @IdRes int[] to) {mData = data;mResource = mDropDownResource = resource;mFrom = from;mTo = to;mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);}
参数:
context:与此SimpleAdapter关联的视图正在运行的上下文
data:地图清单。列表中的每个条目对应于列表中的一行。映射包含每一行的数据,并应包括“from“中指定的所有条目。
resource:定义此列表项视图的视图布局的资源标识符。布局文件至少应包括“to”中定义的命名视图
from:将添加到与每个项关联的Map中的列名列表。
to:应该在“from”参数中显示列的视图。这些都应该是TextView。此列表中的第一个N个视图给出from参数中第一个N列的值。
意思就是从一个list集合中(装的map集合)获取数据,from要输入键和值,to就是给出到底哪个textview显示键,哪个textview显示值,给出textview的id就可以了。
这里获取inflater是刚刚说的第三种方式(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
SimpleAdapter也是继承的BaseAdapter,实现了抽象方法,其中有getCount()和getView()
public int getCount() {return mData.size();}
这个就是返回集合list大小,list里面有几个map,就有几条信息,就会显示几个item,该方法返回此适配器表示的数据集中有多少项。
public View getView(int position, View convertView, ViewGroup parent) {return createViewFromResource(mInflater, position, convertView, parent, mResource);}private View createViewFromResource(LayoutInflater inflater, int position, View convertView,ViewGroup parent, int resource) {View v;if (convertView == null) {v = inflater.inflate(resource, parent, false);} else {v = convertView;}bindView(position, v);return v;}
仍然是推荐inflater.inflate(resource, parent, false);
关于ListView的点击事件onItemClick4个参数的意义
举个例子,我们用打印log的形式来了解形如下面的形式,我们实现的方法onItemClick的4个参数是什么意思?
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {Word word = words.get(position);Log.d(TAG, "========= id:" + l + "\nAdapterView: " + adapterView + "\nview: " + view);}});
word_list.xml
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/list"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.android.miwok.NumbersActivity"/>
list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/tan_background"android:minHeight="@dimen/list_item_height"android:orientation="horizontal"><ImageViewandroid:id="@+id/image"android:layout_width="wrap_content"android:layout_height="wrap_content" /><LinearLayoutandroid:id="@+id/text_container"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:paddingLeft="16dp"><TextViewandroid:id="@+id/miwok_text_view"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:gravity="bottom"android:textAppearance="?android:textAppearanceMedium"android:textColor="@android:color/white"android:textStyle="bold"tools:text="lutti" /><TextViewandroid:id="@+id/default_text_view"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:gravity="top"android:textAppearance="?android:textAppearanceMedium"android:textColor="@android:color/white"tools:text="one" /></LinearLayout>
</LinearLayout>
在某一个activity中,比如NumbersActivity.java
.....ListView listView = (ListView) findViewById(R.id.list);listView.setAdapter(adapter);listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {// Get the {@link Word} object at the given position the user clicked onWord word = words.get(position);Log.d(TAG, "========= id:" + l + "\nAdapterView: " + adapterView + "\nview: " + view);}});...
log显示如下:
随便点击一个
从0开始从上往下数,第三个下标为2,所以id=2
因为发生单击的AdapterView(抽象类)是ListView(实现类),所以打印出来是android.widget.ListView
因为AdapterView中被单击的视图是LinearLayout(这将是适配器提供的视图),所以日志打印出来是android.widget.LinearLayout
那么我们现在修改一下
将实现类改为GridView,并且将xml文件中的ListView改为GridView
GridView listView = (GridView) findViewById(R.id.list);
<GridView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/list"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.android.miwok.NumbersActivity"/>
打印出来就是android.widget.GridView,如下图:
继续修改,将list_item.xml的外层布局LinearLayout改为RelativeLayout,效果如下:
打印出来就是android.widget.RelativeLayout
在抽象类AdapterView中有一个内部接口是OnItemClickListener,里面只有唯一的抽象方法onItemClick,我们来看看
public abstract void onItemClick (AdapterView<?> parent, View view, int position, long id)
单击此AdapterView中的项时要调用的回调方法。 如果需要访问与所选项关联的数据,实施者可以调用getItemAtPosition(position)。
参数 | |
---|---|
parent | AdapterView:发生单击的AdapterView。 |
view | View:AdapterView中被单击的视图(这将是适配器提供的视图) |
position | int:适配器中视图的位置。 |
id | long:已单击的项的行ID。 |
将数据库的数据显示到ListView
这里给出主要代码,详细代码见Demo源码,地址在文章开头。
主要实现数据库的增删改查和把数据显示到ListView,以及getView优化和listView点击事件。
MainActivity.java
import android.database.Cursor;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;import com.example.listview_database.dao.ContactInfoDao;import java.util.ArrayList;
import java.util.List;public class MainActivity extends AppCompatActivity {private final String TAG = "MainActivity";private EditText et_name;private EditText et_phone;private ContactInfoDao dao;private List<Person> lists;private ListView lv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);lv = (ListView) findViewById(R.id.id_lv);et_name = (EditText) findViewById(R.id.id_name);et_phone = (EditText) findViewById(R.id.id_number);dao = new ContactInfoDao(this, "mydb", "contactinfo", null, 1);lists = new ArrayList<Person>();// lv.setAdapter(new MyAdapter());// 放在这里添加数据再查询会挂掉Exception dispatching input event.}public void add(View view) {String name = et_name.getText().toString().trim();String phone = et_phone.getText().toString().trim();if (TextUtils.isEmpty(name) || TextUtils.isEmpty(phone)) {Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();return;} else {dao.add(name, phone);Toast.makeText(this, "添加成功", Toast.LENGTH_SHORT).show();}}public void delete(View view) {String name = et_name.getText().toString().trim();if (TextUtils.isEmpty(name)) {Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();return;} else {dao.delete(name);Toast.makeText(this, "删除成功", Toast.LENGTH_SHORT).show();}}public void modify(View view) {String name = et_name.getText().toString().trim();String phone = et_phone.getText().toString().trim();if (TextUtils.isEmpty(name) || TextUtils.isEmpty(phone)) {Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();return;} else {dao.update(name, phone);Toast.makeText(this, "修改成功", Toast.LENGTH_SHORT).show();}}public void find(View view) {String name = et_name.getText().toString().trim();if (TextUtils.isEmpty(name)) {Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();return;} else {Cursor cursor = dao.query(name);String phone = null;if (cursor.moveToFirst()) { // 将光标移动到第一行,如果游标为空,此方法将返回false。String str1 = null;do {phone = cursor.getString(cursor.getColumnIndex("phone"));str1 = "name:" + name + "phone:" + phone;// 把javabean对象装到集合lists.add(new Person(name, phone));Log.d(TAG, str1);} while (cursor.moveToNext()); // 将光标移动到下一行,如果游标已经超过结果集中的最后一个条目,此方法将返回false。lv.setAdapter(new MyAdapter());lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {Person person = lists.get(position);Toast.makeText(MainActivity.this, person.getName() + ":" + person.getPhone(),Toast.LENGTH_SHORT).show();}});}cursor.close();if (phone == null) {Toast.makeText(this, "无此联系人信息", Toast.LENGTH_SHORT).show();}}}// 定义listview的数据适配器private class MyAdapter extends BaseAdapter {@Overridepublic int getCount() {return lists.size();}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {View view = null;ViewHolder viewHolder;if (convertView == null) {view = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, parent, false);viewHolder = new ViewHolder();viewHolder.tv_name = (TextView) view.findViewById(R.id.tv_name);viewHolder.tv_phone = (TextView) view.findViewById(R.id.tv_phone);view.setTag(viewHolder);} else {view = convertView;viewHolder = (ViewHolder) view.getTag();}Person person = lists.get(position);viewHolder.tv_name.setText(person.getName());viewHolder.tv_phone.setText(person.getPhone());return view;}class ViewHolder {TextView tv_name, tv_phone;}}
}
这里给出优化版的getView,避免快速滑动的时候每次加载布局,这就会成为性能的瓶颈。
getView方法有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便于之后可以重用。
接着我们新增一个内部类ViewHolder,用于对控件的实例进行缓存。当convertView为null的时候,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder里,然后调用View的setTag方法,将ViewHolder对象存储在View中,当convertView不为null时,调用View的getTag方法,把ViewHolder对象重新取出。这样所有控件的实例都换存在了ViewHolder里,就没必要每次都通过findViewById()方法来获取控件实例了。
这里一定要写view.findViewById,因为findViewById是有上下文的,默认是在Activity的主布局中,我们获取的子布局是view
如果不写view.findViewById而直接写findViewById会有异常java.lang.NullPointerException: Attempt to invoke virtual method ‘void android.widget.TextView.setText(java.lang.CharSequence)’ on a null object reference
ContactInfoDao.java
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;import com.example.listview_database.MyDatabaseHelper;public class ContactInfoDao {private MyDatabaseHelper helper;public ContactInfoDao(Context context, String name, String tableName, SQLiteDatabase.CursorFactory factory, int version) {helper = new MyDatabaseHelper(context, name, tableName, factory, version);}public long add(String name, String phone) {SQLiteDatabase db = helper.getWritableDatabase();ContentValues values = new ContentValues();values.put("name", name);values.put("phone", phone);long rowId = db.insert(helper.getTableName(), null, values);db.close();return rowId;}public int delete(String name) {SQLiteDatabase db = helper.getWritableDatabase();int rowId = db.delete(helper.getTableName(), "name = ?", new String[]{name});db.close();return rowId;}public int update(String name, String phone) {SQLiteDatabase db = helper.getWritableDatabase();ContentValues values = new ContentValues();values.put("name", name);values.put("phone", phone);int rowId = db.update(helper.getTableName(), values, "name = ?", new String[]{name});db.close();return rowId;}public Cursor query(String name) {SQLiteDatabase db = helper.getWritableDatabase();Cursor cursor = db.query(helper.getTableName(), null, "name = ?", new String[]{name}, null, null, null);return cursor;}
}
MyDatabaseHelper.java
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.widget.Toast;public class MyDatabaseHelper extends SQLiteOpenHelper {private Context mContext;private String tableName;private String TAG = "MyDatabaseHelper";public final String CREATE_TABLE;public MyDatabaseHelper(Context context, String name, String tableName, SQLiteDatabase.CursorFactory factory, int version) {super(context, name, factory, version);mContext = context;this.tableName = tableName;CREATE_TABLE = "create table " + tableName + "(" +"id integer primary key autoincrement," +"name char(20), " +"phone varchar(20));";}public String getTableName() {return tableName;}@Overridepublic void onCreate(SQLiteDatabase db) {Log.d(TAG, "onCreate: ");db.execSQL(CREATE_TABLE);Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {Log.d(TAG, "onUpgrade: ");db.execSQL("drop table if exits " + tableName);onCreate(db);}
}
item.xml
<?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="horizontal"><TextViewandroid:id="@+id/tv_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="左边"android:textSize="20sp" /><TextViewandroid:id="@+id/tv_phone"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="右边"android:textColor="#ff0000"android:textSize="20sp" /></LinearLayout>
更多Android进阶指南 可以扫码 解锁 《Android十大板块文档》
1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路
敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔