作者Talent•C 转载请注明出处
前言 学习完Android
中内置的 UI控件 和 简单的布局 ,是不是还会有那么一点小小的遗憾,如何定制自己喜欢的控件?今天我们就来学习一下Android
中自定义控件及Android
中最常见并且最难用的ListView
。
自定义控件 在实际开发中,系统内置的UI控件往往不能满足我们的功能需求,这时就需要我们自定义控件了。今天我们来定义一个与iOS中的默认的导航栏一样的导航栏。
我们在自定义控件之前先来看一下Android
中控件和布局的继承结构,如下图(图片来自百度图片) 从图中我们可以看到,我们所用的所有控件都是直接或者间接的继承自 View 的,所用的所有布局都是直接或者间接的继承自 ViewGroup 的, View 是Android
中最基本的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 实现,通过 LinearLayout 的 from() 方法可以构建出一个 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 {
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();
}
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;
}
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)getItem(position);
View view = LayoutInflater.from(getContext()).inflate(resourceID,parent,false );
ImageView imgView = (ImageView)view.findViewById(R.id.data_list_item_img);
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 的运行效率是很低的,因为在 DataModelAdapter 的 getView() 方法中每次都将布局重新加载了一遍,当 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)getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null )
{
view = LayoutInflater.from(getContext()).inflate(resourceID,parent,false );
viewHolder = new ViewHolder();
viewHolder.imgView = (ImageView)view.findViewById(R.id.data_list_item_img);
viewHolder.textView = (TextView)view.findViewById(R.id.data_list_item_name);
view.setTag(viewHolder);
}else {
view = convertView;
viewHolder = (ViewHolder)convertView.getTag();
}
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 下载