盒子
盒子
Posts List
  1. 前言
  2. 自定义控件
  3. ListView的使用
  4. 总结

Android 学习之路--android基础(五)

作者Talent•C
转载请注明出处

前言

学习完Android中内置的 UI控件简单的布局,是不是还会有那么一点小小的遗憾,如何定制自己喜欢的控件?今天我们就来学习一下Android中自定义控件及Android中最常见并且最难用的ListView

自定义控件

在实际开发中,系统内置的UI控件往往不能满足我们的功能需求,这时就需要我们自定义控件了。今天我们来定义一个与iOS中的默认的导航栏一样的导航栏。

我们在自定义控件之前先来看一下Android中控件和布局的继承结构,如下图(图片来自百度图片)

从图中我们可以看到,我们所用的所有控件都是直接或者间接的继承自 View 的,所用的所有布局都是直接或者间接的继承自 ViewGroup 的, ViewAndroid中最基本的UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此我们使用的各种控件都是在 View 的基础上添加各自特有的功能的。而 ViewGroup 则是一种特殊的 View,它可以包含很多子 View 和 子 ViewGroup, 是一个用于放置控件和布局的容器。
了解完Android中控件和布局的继承结构,我们开始定制一个类似于 iOS中的导航栏控件

第一步,引入布局:
用过iPhone的人都知道,在其app中大部分界面顶部都会有一个标题栏,标题两边各有一个按钮,左侧按钮用于返回操作,有则按钮可以是其他功能。
新建一个布局文件名字为 navigation.xml,关于怎么新建布局文件我们在之前的文章就介绍过了,这里不再啰嗦。
修改内容,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg">
//左侧按钮
<Button
android:id="@+id/button_nav_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="返回"
android:textColor="#fff"
/>
//标题文字
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp"
/>
//右侧按钮
<Button
android:id="@+id/button_nav_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="右侧按钮"
android:textColor="#fff"
/>
</LinearLayout>

我们在工程的默认活动中引入这个布局,我这里默认活动名字为 HomeActivity,我们在其布局文件中修改如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/Home_Root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
//引入我们刚才创建布局文件
<include layout="@layout/navigation"></include>
</LinearLayout>

运行程序,我们可看到,在屏幕上会显示一组控件,左侧是按钮,中间是一个标题,左右还有一个按钮,但是Android中内置的标题栏还会显示在屏幕上,我们使用如下代码将其隐藏。

1
2
3
4
5
6
7
8
9
10
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home_layout);
//获取系统导航栏
ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.hide();
}
}

再次运行,程序的界面就变成我们想要的样式了,不管你程序中有多少界面,只要在每个界面使用一条 include 语句即可,使用引入布局的技巧确实解决了重复编写代码的问题,但是如果有些控件要求能够响应事件,我们还是需要单独在每个界面在写一遍事件注册的代码,如我们刚才定制的导航栏中的左侧按钮都是返回上一个界面的功能(销毁当前活动)。这种情况无疑会增加很多重复的代码,这时最好的解决方式就是使用自定义控件的方式解决。

第二步,创建控件:
新建 NavigationLayout 继承自 LinearLayout, 使它成为自定义的导航栏。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
/**
* Created by chuliangliang on 2017/5/24.
*/
public class NavigationLayout extends LinearLayout {
public NavigationLayout(Context ctx, AttributeSet attrs)
{
super(ctx,attrs);
LayoutInflater.from(ctx).inflate(R.layout.navigation,this);
//获取左右按钮
Button leftButton = (Button)findViewById(R.id.button_nav_left);
Button rightButton = (Button)findViewById(R.id.button_nav_right);
//左侧按钮注册点击事件
leftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity)getContext()).finish();
}
});
//右侧按钮注册点击事件
rightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "点击右侧按钮了 ", Toast.LENGTH_SHORT).show();
}
});
}
}

我们在上述代码中复写了 LinearLayout 的构造方法,然后在构造方法里对导航栏布局进行动态加载,动态加载借助于 LayoutInflater 实现,通过 LinearLayoutfrom() 方法可以构建出一个 LayoutInflater 对象,然后调用 inflate() 方法就实现了动态加载一个布局文件, inflate() 方法接收两个参数,第一个参数是要加载的布局文件的id, 这里我们传入 R.layout.navigation;第二个参数是给加载好的布局再添加一个父布局,这里我们要指定为 NavigationLayout 所以我们传入 this

现在控件创建好了,我们修改一下 HomeActivity 的布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/Home_Root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
//引入自定义控件
<com.customui.chuliangliang.customui.NavigationLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/nav_bar">
</com.customui.chuliangliang.customui.NavigationLayout>
</LinearLayout>

添加自定义控件与普通控件是一样的,只不过在添加的时候我们需要指明完整的类名,包名在这里是不可以省略的。
运行程序,点击右侧按钮会出现一个文字提示,点击右侧按钮退出程序。

以上就是完整的自定义控件的过程。

ListView的使用

ListView 绝对称得上是Android中最常用的控件之一,几乎所有的程序都会使用到它。由于手机屏幕空间有限,能够一次性在屏幕上展示的内容并不多,当我们程序中有大量的数据需要展示时,就可以借助 ListView 来实现。ListView 允许用户通过上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕内的数据会滚出屏幕,例如QQ的聊天界面。

1.ListView 的简单用法:
我们新建一个活动名为 ListViewActivity,我们在其布局文件上添加一个 ListView
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/ListAc_Root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
//引入自定义导航栏控件
<com.customui.chuliangliang.customui.NavigationLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.customui.chuliangliang.customui.NavigationLayout>
//添加 ListView 控件
<ListView
android:id="@+id/list_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"></ListView>
</LinearLayout>

可以看出来 ListView 与普通控件的添加完全一样。
接下来我们修改一下 ListViewActivity 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.customui.chuliangliang.customui;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.lang.reflect.Array;
public class ListViewActivity extends AppCompatActivity {
//字符串数组 作为 listview 的数据源
private String[] dataArray = {"1234","dsda","eazduriof","哈哈哈", "第一个字符", "把安静的啦",
"u9wrjwejrleiw", "啊多瘦返回空i","dghjklj","34423","uwoqu","话电话","百度","123456789","腾讯","阿里是是是","你后方可","多少积分看电视"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview_layout);
ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.hide();
}
//为list view 填充数据
ArrayAdapter <String> adapter = new ArrayAdapter<String>(ListViewActivity.this,android.R.layout.simple_list_item_1,dataArray);
ListView listView = (ListView) findViewById(R.id.list_1);
listView.setAdapter(adapter);
}
}

数组中的数据是无法直接传递给 ListView 的,我们需要借助适配器来完成, Android中提供了很多适配器的实现类,我们今天以 ArrayAdapter 为例(本人也比较喜欢使用这个适配器),它可以用过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。因为我们数组中都是字符串,所以我们的泛型指定为 String,然后在 ArrayAdapter 的构造函数中依次传入上下文、ListView的子项布局 的id,以及需要展示的数据。我们这里使用了系统内置的 android.R.layout.simple_list_item_1 作为 ListView的子项布局

ListView的子项布局 等价于 iOS中的 UITableViewCell

运行程序,就会在屏幕上呈现一个可以滑动的区域,内部有很多行文字。这是 ListView 最最基础的用法。我们看到每条只显示一行文字太过于单调了,我们现在就对其界面进行定制。

2.定制ListView界面
我们使每条子项布局都展示一个图片+一段文字,首先我们需要一个数据模型,用来保存每个子项布局的数据;之后我们自定义一个用来展示数据的子项布局。我们通过上面的例子知道,数组中的数据是不可以直接被 ListView 使用的,所以我们还需要一个适配器。这些都有了

第一步,定义数据模型
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.customui.chuliangliang.customui;
/**
* Created by chuliangliang on 2017/5/24.
*/
public class DataModel {
private String name;
private int pid;
//构造函数
public DataModel(String name, int imgId)
{
this.name = name;
this.pid = imgId;
}
//获取名字
public String getName()
{
return this.name;
}
//获取图片标识id
public int getPid ()
{
return this.pid;
}
}

这个是数据模型,一共有两个属性, 一个是图片的在名字(R.drawable 文件夹下为图片自动生成的id),一个是图片对应的描述文字。

第二步,自定义子项布局,新建一个布局文件名字为 data_list_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/data_list_item_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/data_list_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
/>
</LinearLayout>

这个布局与 OC中的自定义UITableViewCell 作用一样,用来展示数据;左边展示一张图片,图片右侧显示一句文字信息。

第三步,创建我们自己的适配器,取名 DataModelAdapter
代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.customui.chuliangliang.customui;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.w3c.dom.Text;
import java.net.URL;
import java.util.List;
/**
* Created by chuliangliang on 2017/5/24.
*/
public class DataModelAdapter extends ArrayAdapter {
private int resourceID;
public DataModelAdapter(Context ctx, int textViewResourceId, List<DataModel> objects)
{
super(ctx,textViewResourceId,objects);
resourceID = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
//获取DataModel 数据模型对象
DataModel dataModel = (DataModel)getItem(position);
//获取显示的view
View view = LayoutInflater.from(getContext()).inflate(resourceID,parent,false);
//获取View (类似于中的cell) 上的 图片视图
ImageView imgView = (ImageView)view.findViewById(R.id.data_list_item_img);
//获取View (类似于中的cell) 上的 文字控件
TextView textView = (TextView)view.findViewById(R.id.data_list_item_name);
//为控件赋值
imgView.setImageResource(dataModel.getPid());
textView.setText(dataModel.getName());
return view;
}
}

现在我们创建一个活动展示我们自定义的 ListView,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.customui.chuliangliang.customui;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class CustomActivity extends AppCompatActivity {
//列表数据源 <数组>
private List<DataModel> dataObjects = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom);
ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.hide();
}
initDataObjects();
DataModelAdapter adapter = new DataModelAdapter(CustomActivity.this,R.layout.data_list_item,dataObjects);
ListView listView = (ListView)findViewById(R.id.custom_list_1);
listView.setAdapter(adapter);
}
//构造数据模型
private void initDataObjects()
{
//初始化数组
for (int i = 0; i < 2 ; i++)
{
//第一条数据
DataModel dataModel_pg = new DataModel("苹果",R.drawable.pg);
dataObjects.add(dataModel_pg);
//第二条数据
DataModel dataModel_cm = new DataModel("草莓",R.drawable.cm);
dataObjects.add(dataModel_cm);
//第三条数据
DataModel dataModel_cz = new DataModel("橙子",R.drawable.cz);
dataObjects.add(dataModel_cz);
//第四条数据
DataModel dataModel_pt = new DataModel("葡萄",R.drawable.pt);
dataObjects.add(dataModel_pt);
//第五条数据
DataModel dataModel_xj = new DataModel("香蕉",R.drawable.xj);
dataObjects.add(dataModel_xj);
//第六条数据
DataModel dataModel_yt = new DataModel("樱桃",R.drawable.yt);
dataObjects.add(dataModel_yt);
}
}
}

运行程序程序,我们就会看到条 子项布局(OC中的cell) 都会有一个图片和一个文字描述,这就是我们自定义的 ListView,通过这个例子相信大家可以定制出属于自己的 ListView 了。

3.ListView的优化
之前说 ListView 很难用,就是因为它有很多细节可以优化,其中运行效率就是很重要的一点。目前我们 ListView 的运行效率是很低的,因为在 DataModelAdaptergetView() 方法中每次都将布局重新加载了一遍,当 ListView 快速滚动时,这里就会消耗很大的资源,导致性能下降。仔细观察会发现, getView() 方法中有一个 convertView 参数,这个参数就是将之前加载好的布局进行缓存,以便之后可以进行复用。我们修改一下 DataModelAdapter 适配器中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.customui.chuliangliang.customui;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.w3c.dom.Text;
import java.net.URL;
import java.util.List;
/**
* Created by chuliangliang on 2017/5/24.
*/
public class DataModelAdapter extends ArrayAdapter {
private int resourceID;
public DataModelAdapter(Context ctx, int textViewResourceId, List<DataModel> objects)
{
super(ctx,textViewResourceId,objects);
resourceID = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
/*
* 1. 复用布局 view (也就是oc中的cell) convertView 如果存在即为已经初始化过
* 2. 避免频繁获取控件 如 从 view中获取 ImageView 和 TextView
* */
//获取DataModel 数据模型对象
DataModel dataModel = (DataModel)getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null)
{
//此种布局从未创建或者不存在可复用的布局
//创建布局
view = LayoutInflater.from(getContext()).inflate(resourceID,parent,false);
//初始化内部类
viewHolder = new ViewHolder();
//获取View (类似于中的cell) 上的 图片控件 保存近内部类中
viewHolder.imgView = (ImageView)view.findViewById(R.id.data_list_item_img);
//获取View (类似于中的cell) 上的 文字控件 保存近内部类中
viewHolder.textView = (TextView)view.findViewById(R.id.data_list_item_name);
//将内部类 保存进 布局中 (oc中的cell)
view.setTag(viewHolder);
}else {
//存在可复用的布局(oc中的cell)
view = convertView;
viewHolder = (ViewHolder)convertView.getTag();
}
//为布局填充数据源 (oc中的cell 填充数据源)
viewHolder.textView.setText(dataModel.getName());
viewHolder.imgView.setImageResource(dataModel.getPid());
return view;
}
private class ViewHolder {
ImageView imgView;
TextView textView;
}
}

上述代码中可以看到,我们在 getView() 方法中对布局进行了重用,并且在 DataModelAdapter 适配器中增加一个内部类 ViewHolder, ViewHolder 用于将布局中所有的控件实例进行保存,避免频繁调用 findViewById() 方法获取控件实例,这样就大大提高了 ListView 的运行效率。

4.ListView的点击事件
ListView 不光可以呈现好看的界面,它也接受用户的点击事件,那么应该如何实现呢?其实很简单,我们直接看代码吧~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CustomActivity extends AppCompatActivity {
//列表数据源 <数组>
private List<DataModel> dataObjects = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom);
ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.hide();
}
initDataObjects();
DataModelAdapter adapter = new DataModelAdapter(CustomActivity.this,R.layout.data_list_item,dataObjects);
ListView listView = (ListView)findViewById(R.id.custom_list_1);
listView.setAdapter(adapter);
//添加点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DataModel dataModel = dataObjects.get(position);
Toast.makeText(CustomActivity.this, dataModel.getName(), Toast.LENGTH_SHORT).show();
}
});
}
}

是不是很简单呢, 我们在点击事件中弹出一个Toast提示,提示点击的是哪条数据。

总结

这几天事情太多, 学习也处于断断续续的状态,调整状态继续前进…
本文中使用的Demo 下载

支持一下
扫一扫,支持Talent•C