编写用户界面
Android
中有好几种编写程序界面的方式可供选择。比如使用DroidDraw
,这是一种可视化的界面编辑工具。Eclipse
和Android Studio
中也有相应的可视化编辑器,和DroidDraw
相似。但是可视化编辑工具并不利于你去真正了解界面背后的实现原理,通常这种方式制作出的页面都不具有很好的屏幕适配性。而且当需要编写较为复杂的界面时,可视化编辑工具很难胜任,因此依然推荐直接编写xml
代码的方式。
常见控件的使用
TextView
TextView
是Android
中最简单的一个控件。主要用于在界面上显示一段文本信息。下面是基础的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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=
"com.example.oujitsune.uiwidgettext.MainActivity">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
外面的LinerLayout
先忽略,android:id
是当前控件的唯一标识符。android:layout_width``android:layout_height
指定控件的宽和高。Android
中所有的控件都有这两个属性。可选值有三种:
-
match_parent
表示让当前控件的大小和父布局一致。 -
wrap_content
表示让当前控件大小适应其内容。 -
fill_parent
表示让当前控件占据整个父布局。
除了三个预设值,还可以自己指定大小,当然有可能会不够适配。
现在修改一下文字对齐方式、颜色和大小:
android:gravity="center"
android:textSize="24sp"
android:textColor="#00ff00"
Button
Button
的属性和TextView
类似,不再赘述,补充一下它的加入方法:
<Button
android:id="@+id/button_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="#000000"
android:text="This is a Button" />
可以在活动中为Button
的点击事件注册一个监听器:
Button button = (Button) findViewById(R.id.button_1);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加逻辑
}
});
这样每当点击按钮时,就会执行监听器中的onClick
方法。如果不想用匿名类来注册,也可以用实现接口的方式来注册。
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
Button button = (Button) findViewById(R.id.button_1);
button.setOnClickListener(this);
}
@Override
public void onClick(View v){
switch (v.getId()){
case R.id.button_1:
//添加逻辑
break;
default:
break;
}
}
}
EditText
EditText
是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并且可以在程序中对这些内容进行处理。
添加EditText
:
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type Something Here"/>
hint
属性表示在文本框中出现的提示文字。一旦用户输入,提示就会消失。
当输入内容增多时,EditText
会不断拉长,当输入过多时,界面就会非常难看。我们可以用android:maxLines
来解决这个问题。
添加一个maxLines
属性来限制文本框的最大行数:android:maxLines=“2”
。
输入超过两行,文本就会向上滚动,EditText
就不会再继续拉伸。
还可以结合EditText
和Button
来完成一些功能,比如点击按钮来获取EditText
中输入的内容。下面的示例通过Toast
来显示EditText
中输入的文字。
public class MainActivity extends AppCompatActivity {
private Button button;
private EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button_1);
editText = (EditText) findViewById(R.id.edit_text);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
}
});
}
}
ImageView
ImageView
是用于在界面上显示图片的一个控件,通过它可以让我们的程序界面变得更加丰富。使用这个控件需要在包中预置一些图片
添加ImageView
:
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
还可以在程序中通过代码动态地更改图片。
imageView.setImageResource(R.drawable.新闻写作_海报2);
ProgressBar
ProgressBar
用于在界面上显示一个进度条,表示程序正在加载一些数据。
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
如果只是这样,会有一个进度条一直在程序中旋转,这就涉及到可见属性visibility
。可选值有三种:
-
visible
表示可见,为默认值。 -
invisible
表示不可见,但依然存在,只是透明。 -
gone
表示不可见,并且不再占用屏幕空间。
可以在代码中设置控件的可见性,使用setVisiblity
方法,传入Visw.VISIBLE
、Visw.INVISIBLE
、Visw.GONE
三种值。
另外,我们还可以给ProgressBar
指定不同的样式,刚刚是圆形,通过style
属性可以将其指定成水平进度条:
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"/>
指定成水平进度条之后,我们还可以通过android:max
属性给进度条设置一个最大值,然后再代码中动态地更改进度条的进度。
int progress = progressBar.getProgress();
progress = progress + 10;
progressBar.setProgress(progress);
每点击一次按钮,就获取进度条的当前进度,然后再现有进度条上加10作为更新后的进度。
AlertDialog
AlertDialog
可以在当前界面弹出一个对话框,这个对话框是制定与所有界面元素之上的,能够屏蔽掉其他空间的交互能力。因此一般AlertDialog
都是用于提示一些非常重要的内容或者警告信息。比如防误删等。
添加AlertDialog
:
public void onClick(View v) {
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something important.");
dialog.setCancelable(false);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {}
});
dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog, int which){}
});
dialog.show();
}
ProgressDialog
ProgressDialog
和AlertDialog
有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其他空间的交互能力。不同的是,ProgressDialog
会在对话框中显示一个进度条,一般是用于表示当前操作比较耗时,让用户耐心等待。它的用法和AlertDialog
也比较相似:
public void onClick(View v) {
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setCancelable(true);
progressDialog.show();
}
注意如果再setCancelable
中传入了flase
,表示ProgressDialog
是不能被取消的。这是一定要在代码中做好控制,在加载之后一定要用dismiss
来关闭对话框,否则ProgressDialog
会一直存在。
详解四种基本布局
丰富的界面总是由很多控件组成,布局就是让各个控件都有条不紊地摆放在界面上。布局就是一个可以用于放置很多控件的容器,可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。布局内部也可以嵌套布局,完成一些比较复杂的界面实现。
LinearLayout
LinearLayout
又被称为线性布局,是一种非常常用的布局。正如它名字中所描述的那样,这个布局会将它所包含的控件在线性方向上依次排列。
既然是线性排列,就肯定不仅有一个方向。可以通过android:orientation
属性指定排列方向。vertical
是垂直,horizontal
是水平方向。
需要注意的是,如果排列方向的horizontal
,内部空间就绝对不能将宽度指定为match_parent
。
几个重要属性:
-
android:layout_gravity
属性可以改变控件在布局中的对齐方式。如果排列方向是horizontal
,只有竖直方向的对齐方式才会生效,vertical
同理。 -
android:layout_weight
属性可以允许用比例的方式来指定控件的大小。在手机屏幕的适配性方面可以起到非常重要的作用。
示例:
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="send"/>
把width
设为0是一种比较规范的写法,实际上宽度是由weight
来指定的。两者都设置为1就会平分屏幕的宽度。
RelativeLayout
RelativeLayout
也是一种常用布局,与LinearLayout
不同,RelativeLayout
更加随意一些,它通过相对定位的方式来让控件出现在布局的任何位置。也正因为如此,它其中的属性非常多,不过不难理解和记忆。
<?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">
<Button
android:id="@+id/button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1"/>
<Button
android:id="@+id/button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2"/>
<Button
android:id="@+id/button_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>
<Button
android:id="@+id/button_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="Button 4"/>
<Button
android:id="@+id/button_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Button 5"/>
</RelativeLayout
上面的代码是相对于父布局进行定位的,控件也可以相对于控件进行定位。
<?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">
<Button
android:id="@+id/button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button_3"
android:layout_toLeftOf="@id/button_3"
android:text="Button 1"/>
<Button
android:id="@+id/button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button_3"
android:layout_toRightOf="@id/button_3"
android:text="Button 2"/>
<Button
android:id="@+id/button_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>
<Button
android:id="@+id/button_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button_3"
android:layout_toLeftOf="@id/button_3"
android:text="Button 4"/>
<Button
android:id="@+id/button_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button_3"
android:layout_toRightOf="@id/button_3"
android:text="Button 5"/>
</RelativeLayout>
layout_above
属性让控件位于另一个控件上方,需要为这个属性指定相对控件id
的引用。
FrameLayout
FrameLayout
没有任何定位方式,所有控件都会摆放在布局的右上角。应用场景不多,不过在后面介绍碎片的时候还能够用到。
TableLayout
TableLayout
允许通过表格的方式来排列控件。这种布局也不是很常用。
创建自定义控件
所有控件都是直接或者间接地继承自View
的,所用的所有布局都是直接或间接继承自ViewGroup
的。View
是Android
中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View
的基础之上添加了各自特有的功能。ViewGroup
是一种特殊的View
,他可以包含很多的子View
和ViewGroup
。
引入布局
我们先绘制一个标题栏:
<?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="wrap_content"
android:background="@color/colorPrimaryDark">
<Button
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dip"
android:background="@color/colorPrimaryDark"
android:text="Back"
android:textColor="#fff"/>
<TextView
android:id="@+id/title_text"
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/title_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dip"
android:background="@color/colorPrimaryDark"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>
标题栏中定义了两个Button
和一个TextView
,左边的Button
用于返回,右边的Button
用于编辑。
如何使用这个标题栏呢?
只需要在布局文件中加上一句<include layout="@layout/title"/>
就可以了。
顺便,需要在onCreate
方法中设置隐藏标题栏。
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) actionBar.hide();
创建自定义控件
新建TitleLayout
继承自LinearLayout
,让它成为自定义的标题栏控件。
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
}
}
我们重写了LinearLayout
中带有两个参数的构造函数,在布局中引入:
<com.example.oujitsune.uiwidgettext.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.oujitsune.uiwidgettext.TitleLayout>
TitleLayout
控件就会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,这就要借助LayoutInflater
来实现了。
然后我们来尝试为标题栏中的按钮注册点击事件,修改TitleLayout
中的代码。
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity) getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "You clicked Edit button", Toast.LENGTH_SHORT).show();
}
});
}
}
首先还是通过findViewByid()
方法得到按钮的实例,然后分别调用setOnClickListener
方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹出一段文本。
ListView
ListView
是Android
上最常用的控件之一,几乎所有的应用程序都会用到它。由于手机屏幕空间有限,能够一次性在屏幕上显示的内容不多,当我们的程序有大量数据需要显示的时候就可以借助ListView
来实现。
简单用法
在布局中引入一个ListView
还算很简单。
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
只要宽和高都设定为match_parent
就可以占据整个屏幕了。
public class MainActivity extends AppCompatActivity {
private String[] data = {
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, data
);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
数组中的数据没有办法直接传递给ListView
,我们还需要借助适配器来完成。Android
中提供了很多适配器的实现类,ArrayAdapter
是其中之一。它可以通过泛型来指定钥匙胚的数据类型,然后在构造函数中把要适配的数据传入即可。ArrayAdapter
有多个构造函数的重载,可以根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将ArrayAdapter
的泛型指定为String
,然后在ArrayAdapter
的构造函数中依次传入当前的上下文、ListView
子项布局的id
,以及要适配的数据。注意我们使用了android.R.layout.simple_list_item_1
作为ListView
子项布局的id
。这是一个Android
内置的布局文件,里面只有一个TextView
,可以用于简单地显示一段文本。
最后,还需要调用ListView
的setAdapter
方法,将构建好的适配器对象传递进去,这样ListView
和数据之间的关联就建立完成了。
定制ListView的界面
为了让界面更好看一些,我们来改善一下。
定义一个Fruit
类,加入两个字段,一个name
,一个imageId
。
为ListView
的子项制定一个我们自定义的布局,在layout
目录下新建fruit_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">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="10dip"/>
</LinearLayout>
接下来创建一个自定义适配器:
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects){
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view = LayoutInflater.from(getContext()).inflate(resourceId, null);
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
}
}
FruitAdapter
重写了父类的一组构造函数,用于将上下文、ListView
子项布局的id
和数据都传递竟来。另外又重写了getView
方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在getView
方法中,首先通过getItem
方法得到当前项的Fruit
实例,然后使用LayoutInflater
来为这个子项加载我们传入的布局,接着调用View
的findViewById
方法分别获取到ImagView
和TextView
的实例,并分别调用它们的setImageResource
和setText
方法来设置显示的图片和文字,最后将布局返回。这样我们的自定义适配器就完成了。
最后修改MainActivity
中的代码:
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<Fruit>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
private void initFruits(){
Fruit apple = new Fruit("Apple", R.mipmap.ic_launcher);
fruitList.add(apple);
Fruit banana = new Fruit("Banana", R.mipmap.ic_launcher);
fruitList.add(banana);
Fruit orange = new Fruit("Orange", R.mipmap.ic_launcher);
fruitList.add(orange);
}
}
提升ListView的运行效率
之所以说ListView
这个控件很难用,是因为它有很多的细节可以优化。其中运行效率是很重要的一点。目前我们的ListView
的运行效率的很低的,因为在FruitAdapter
的getView
方法中每次都将布局重新加载了一遍, 当ListView
快速滚动的时候就会成为性能的瓶颈。
getView
方法中还有一个convertView
参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用:
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view;
if (convertView == null)
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
else
view = convertView;
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
现在我们对getView
方法进行了判断,如果convertView
为空才会使用LayoutInflater
去加载布局。如果不为空则直接对convertView
进行重用。这样就大大提高了ListView
的运行效率。
我们还可以继续优化。每次getView
方法会调用View
的findViewById
方法来获取一次控件的实例。我们可以借助一个ViewHolder
来对这部分性能进行优化。
public class FruitAdapter extends ArrayAdapter<Fruit> {
...
@Override
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView fruitImage;
TextView fruitName;
}
}
我们新增了一个内部类ViewHolder
,用于对控件的实例进行缓存。当convertView
为空的时候,创建一个ViewHolder
对象,并且将控件的实例都存放在ViewHolder
里,然后调用View
的setTag
方法,将ViewHolder
对象存储在View
中。在convertView
不为空的时候调用getTag
方法,把ViewHolder
重新取出。这样,所有的控件的实例都缓存在了ViewHolder
里,没必要每次都通过findViewById
方法来获取控件实例了。
点击事件
滚动只是视觉效果,子项还可以点击。
修改MainActivity
:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
}
...
}
我们可以给ListView
注册一个监听器,用position
参数判断出用户点击的是哪一个子项。