03_用户界面
03_用户界面
0. 如何排查解决常见异常
常见的异常:
-
NullPointerException原因:调用对象的方法/属性,但对象为
null -
ClassCastException原因:类型转换异常
-
ActivityNotFoundException原因:没有注册Activity,或注册不正确
基本常见异常的一般分析步骤:
-
在logcat中从下向上找,尽量找到
causeBy(),会显示哪种异常导致的(AS的problems栏也可) -
找到出异常的类及行号,点击进入对应的行
1. 理论概述
1.1 理解UI
UI的定义
-
user interface,意为:用户界面 -
UI由
View和ViewGroup组成 -
View类是所有视图(包括ViewGroup)的根基类
-
View在屏幕上占据一片矩形区域,并会在上面进行内容绘制
-
ViewGroup包含一些View或ViewGroup,用于控制子View的布局
View的hierarchy结构

ViewGroup下重要的子类




UI的组成
-
界面的整体布局(Layout)

-
组成可视界面的各个UI组件(Component)
![]()
1.2 UI事件
理解UI事件
-
当用户通过手指触摸UI时,系统会自动创建对象的
Event对象 -
Android中提供了多种方式拦截处理不同类型的事件
-
视图本身就可以处理发生在该视图上的事件
-
事件是什么?
点击、长按、触碰...
-
谁是事件源?
发生事件的源头,视图
-
谁是事件监听器?
new Listener

使用UI事件
-
Android提供了很多不同类型的
事件监听器接口-
View.OnClickListener:onClick() -
View.OnLongClickListener:onLongClick() -
View.OnTouchListener:onTouch() -
View.OnCreateContextMenuListener:onCeateContextMenu()
-
View.OnFocusChangeListener:onFocusChange()
-
View.OnKeyListener:onKey()
-
-
给视图添加事件监听的方式
view.
setOn...Listener(listener)-
new
-
this
-
成员变量
-
布局绑定
-
2. UI开发
2.1 常用UI组件
TextView EditText Button ImageView CheckBox RadioGroup RadioButton Toast
OptionMenu ContextMenu
ProgressBar SeekBar
Dialog
测试用例

简单组件
测试常用简单的Component

TextView:文本框
<TextView
android:text="这是TextView的内容"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/simple_tv"
android:background="#e6e6e6"
android:textColor="#90a"
android:textSize="20sp" />
| 属性 | 含义 |
|---|---|
| text | 文本内容 |
| textColor | 字体颜色 |
| textStyle | 字体风格 normal / bold / italic |
| textSize | 字体大小,单位sp |
| background | 背景颜色 |
| gravity | 对齐方向 |
private TextView simple_tv;
iv_simple = findViewById(R.id.iv_simple);
simple_tv.getText(); // 获取文本
simple_tv.setText("被改变了~~"); // 设置文本
EditText:输入框
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:id="@+id/et_phone"
android:hint="请输入手机号" />
| 属性 | 含义 |
|---|---|
| text | 文本 |
| hint | 提示默认文本 |
| textColorHint | 提示文本颜色 |
| selectAllOnFocus | 获得焦点后全选文本 true / false(默认) |
| inputType | 输入类型 |
| minLines / maxLines | 设置最小行,最大行 number |
| singleLine | 单行输入 true (默认多行显示,自动换行) |
| textScaleX / textScaleY | 设置文件水平间隔、垂直间隔 |
| capitalize | 英文字母大写类型 none(默认) / sentences(第一个字母大写) / characters(全部大写) |
String number = et_phone.getText().toString();
et_phone.setText("110");
Button: 按钮
<Button
android:text="提交"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false" // 取消全部大写(默认开启)
android:id="@+id/btn_submit" />
btn_submit.setOnClickListener(v -> {
String number = et_phone.getText().toString();
Toast.makeText(SimpleActivity.this, number, Toast.LENGTH_SHORT).show();
});
ImageView:图片视图
<ImageView
android:id="@+id/iv_simple"
android:layout_width="70dp"
android:layout_height="70dp"
android:backgroud="@drawable/ic_launcher" // 背景图片
android:src="@android:drawable/ic_media_play" /> // 前景图片,android:drawable系统图片
iv_simple.setOnClickListener(v -> {
// 设置背景图片
iv_simple.setBackgroundResource(android.R.drawable.alert_light_frame);
// 设置前景图片
iv_simple.setImageResource(android.R.drawable.ic_media_pause);
});
CheckBox:多选框
<CheckBox
android:text="篮球"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/checkBox"
android:checked="true" />
boolean isChecked() // 判断是否被勾选
void setChecked(boolean checked) // 设置勾选
void setOnCheckedChangeListener(OnCheckedChangeListener listener) // 状态改变监听
cb_pingpong.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(isChecked) {
//Log.e("TAG",this.toString()); // E/TAG: com.example.helloandroid.SimpleActivity@19a89e77
// 编译为匿名内部方法,this 指针指向lambda 外部类
Toast.makeText(this, "选中了乒乓球", Toast.LENGTH_SHORT).show();
}
});
RadioGroup:单选框组 和 RadioButton:单选
<RadioGroup
android:layout_width="409dp"
android:layout_height="45dp"
android:orientation="horizontal"
android:id="@+id/rg_gender"
android:layout_marginLeft="5dp">
<RadioButton
android:text="男"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/rb_male" />
<RadioButton
android:text="女"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:id="@+id/rb_female" />
<RadioButton
android:text="无性别"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/rb_no_gender" />
</RadioGroup>
rg_gender.setOnCheckedChangeListener((group, checkedId) -> { // checkedId 选中的 radioButton 的 id
RadioButton rb = findViewById(checkedId);
String val = rb.getText().toString();
Toast.makeText(this, val, Toast.LENGTH_SHORT).show();
});
菜单组件
测试菜单Component

文字换行可以直接在文本中添加
\n
关于Menu的三个问题
-
如何触发Menu的显示?
-
如何向Menu中添加MenuItem?
-
选择某个MenuItem时如何响应?
OptionMenu:选项菜单
-
OptionMenu在点击手机的menu键触发(一般为应用右上角三个点)
-
Activity:
onCreateOptionsMenu(Menu menu)-
显示OptionMenu的回调方法,在此方法中向Menu中添加MenuItem
-
-
加载menuItem的两种方式:
-
纯编码方式:menu.add(...)
-
加载menu文件的方式:
-
MenuInflater menuInflater = getMenuInflater(); -
menuInflater.inflate(R.menu.main_option, menu);
-
-
-
Activity:
onOptionsItemSelected(Menu menu)-
当选择某个菜单项的回调方法
-
1. 纯编码方式
// 用来显示menu的方法:向menu中添加item
2. 加载menu文件方式
res文件夹右键新建menu的xml文件

type选择menu


添加item
Java文件

ContextMenu:上下文菜单
-
view:
setOnCreateContextMenuListener(listener)-
为某个视图添加创建ContextMenu的监听(需要长按触发)
-
-
view:
onCreateContextMenu(menu, view, menuInfo)-
显示菜单的回调方法
-
-
Activity:
onContextItemSelected(Menuitem item)-
当选择某个菜单项的回调方法
-
-
如何触发Menu的显示?
长按某个视图
view.setOnCreateContextMenuListener(listener) -
如何向Menu中添加MenuItem?
重写
onCreateContextMenu(menu, view, menuInfo)menu.add() / 加载menu文件
-
选择某个MenuItem时如何响应?
onContextItemSelected(Menuitem item)
代码实现

进度条组件
测试进度条Component

ProgressBar:进度条
<ProgressBar
style="?android:attr/progressBarStyle" // 默认为圆形进度条
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar" />
<ProgressBar
style="?android:attr/progressBarStyleHorizontal" // 水平进度条
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="2" // 当前进度,默认为0
android:max="10" // 最大进度,默认100
android:id="@+id/progressBar2" />
-
ProgressBar
-
void setProgress(int progress):设置当前进度 -
int getProgress():获取进度 -
void setMax(int max):设置最大进度 -
int getMax():得到最大进度
-
-
View
-
void setVisibility(int visibility):设置视图的可见性 -
View.VISIBLE:标识可见 -
View.INVISIBLE:标识不可见,但占屏幕空间 -
View.GONE:标识不可见,也不占屏幕空间
-
SeekBar:可手动滑动的进度条(是progressBar子类)
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/sb" />
-
SeekBar:
-
setOnSeekBarChangeListener(OnSeekBarChangeListener l):设置改变的监听
-
-
OnSeekBarChangeListener:
-
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser):进度改变 -
onStartTrackingTouch(SeekBar seekBar):按下滑杆 -
onStopTrackingTouch(SeekBar seekbar):从滑杆离开
-
测试用例实现
布局文件
代码
public class ProgressActivity extends AppCompatActivity {
private LinearLayout ll_progress_layout;
private SeekBar sb;
private ProgressBar pb_2;
private final SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
对话框组件
测试对话框Component

-
测试AlertDialog
-
测试ProgressDialog
-
测试DatePickerDialog TimePickerDialog
Dialog的hierarchy

显示一般AlertDialog
AlertDialog:
-
show():显示警告框 -
没有公开的构造方法,只能通过其内部类Builder来创建
AlertDialog.Builder:
-
create():创建AlertDialog对象
-
show():创建AlertDialog对象,同时显示出来 -
setTitle(CharSequence title):设置标题 -
setMessage(CharSequence message):设置内容 -
setPositiveButton(String text, OnClickListener l):设置正面按钮 -
setNegativeButton(String text, OnClickListener l):设置负面按钮 -
dismiss():移除dialog
btn_show_simple_alert.setOnClickListener(v -> {
new AlertDialog.Builder(this)
.setTitle("删除数据")
.setMessage("确定删除吗?")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {

显示单选列表AlertDialog
setSingleChoiceItems(...) 设置单选列表
btn_show_radio_alert.setOnClickListener(v -> {
final String[] items = {"红", "蓝", "绿", "灰"};
new AlertDialog.Builder(this)
.setTitle("指定背景颜色")
.setSingleChoiceItems(items, 2, new DialogInterface.OnClickListener() {

显示自定义AlertDialog
-
动态加载布局文件得到对应的View对象
View.inflate(Context context, int resource, ViewGroup root); -
设置View
DialogBuilder :
setView(View view)设置dialog中的视图
新建一个布局文件 res/layout/dialog_view.xml
btn_show_custom_alert.setOnClickListener(v -> {
// 动态加载布局文件,得到对应的View对象
View view = View.inflate(this, R.layout.dialog_view, null);
// View的真实类型? 是布局文件根标签的类型
// 如何得到一个独立View的子View? view.findViewById(id)
EditText et_name = view.findViewById(R.id.et_name);
EditText et_pwd = view.findViewById(R.id.et_pwd);
new AlertDialog.Builder(this)
.setView(view)
.setNegativeButton("取消", null)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {

显示圆形进度ProgressDialog
ProgressDialog:
static show(Context context, CharSequence title, CharSequence message):显示dialog
ProgressDialog(Context context):构造方法
setProgressStyle(int style):设置样式
ProgressDialog.STYLE_HORIZONTAL:水平进度条样式
btn_show_circle_progress.setOnClickListener(v -> { // 回调方法都在主线程执行
// 1、final 关键字,提高了性能,JVM 和 Java 应用都会缓存 final 变量。
// 2、final 变量,可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
final ProgressDialog dialog = ProgressDialog.show(this, "数据加载", "数据加载中...");
// 模拟一个长时间的工作
// 长时间的工作不能在主线程做,得启动分线程完成
new Thread() {

显示水平进度ProgressDialog
btn_show_horizon_progress.setOnClickListener(v -> {
ProgressDialog pd = new ProgressDialog(this);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.show();
new Thread(new Runnable() {

显示DatePickerDialog
btn_show_date_picker.setOnClickListener(v -> {
// 创建日历对象
Calendar calendar = Calendar.getInstance();
// 得到当前的年月日
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
Log.e("TAG", year + "-" + (month + 1) + "-" + dayOfMonth); // 初始日期 month从0开始
new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() {
显示TimePickerDialog
btn_show_time_picker.setOnClickListener(v -> {
// 创建日历对象
Calendar calendar = Calendar.getInstance();
// 得到当前的年月日
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
Log.e("TAG", hourOfDay + ":" + minute);
new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() {
补充
1. 启动分线程
方式1:
new Thread() {
public void run() {
// 此处在分线程执行
}
}.start();
方式2:
new Thread(new Runnable() {
public void run() {
// 分线程执行
}
}).start();
2. 根据id查找View对象
-
查找当前界面的View对象:
findViewById(id) -
查找某个View对象的子View:
view.findViewById(id)
3. 更新视图
-
不能在分线程直接更新UI
-
长时间工作只能在分线程执行
2.2 常用UI布局
概述
-
布局本身是不能显示出任何数据,它可以包含一些子视图,并控制子视图的布局。
-
常用的Layout
-
LinearLayout
-
RelativeLayout
-
FrameLayout
-
ConstraintLayout(2017才出,本教程没有,推荐博客Android新特性介绍,ConstraintLayout完全解析)
-
ListView(后面讲)
-
GridView(后面讲)
-
其他
-
LinearLayout 线性布局
-
线性布局:用来控制其子View以水平或垂直方式展开显示。
-
重要属性:
-
orientation(方向)
-
layout_weight(权重)
-
理解LinearLayout权重
-
layout_weight(权重)的值
-
=0(默认值):指定多大空间就占据多大空间 -
>0:将父视图中的可用空间进行分割,值越大权重就越大,占据的比例就会越大
-
-
layout_weight的使用场景
-
将布局的宽度或高度平均分成几个等份
-
垂直方向上占用中间所有空间 或 水平方向上占据中间所有空间
-
均等分布
如要创建线性布局,让每个子视图使用大小相同的屏幕空间,请将每个视图的
android:layout_height设置为"0dp"(针对垂直布局),或将每个视图的android:layout_width设置为"0dp"(针对水平布局)。然后,请将每个视图的android:layout_weight设置为"1"。
实例

RelativeLayout 相对布局
-
相对布局:用来控制其子View以相对定位的方式进行布局展示
-
相对布局是最灵活、最强大,也是学习难度最大的布局
-
相对布局相关属性比较多:
-
兄弟视图之间:同方向对齐、反方向对齐
-
与父视图之间:同方向对齐、居中
-
实例

FrameLayout 帧布局
-
帧布局中的每一个子View都代表一个画面,默认以屏幕左上角作为(0, 0)坐标,按定义的先后顺序依次逐屏显示,后面出现的会覆盖前面的画面
-
可以通过
android:layout_grivity等属性来指定子视图的位置
gravity:控制当前视图的内容 / 子view
layout_grivity:控制当前视图自己
实例

2.3 常用视图标签属性
属性的划分
-
针对任何
View的属性-
常用的最基本属性
-
内边距属性
padding -
外边距属性
margin
-
-
只针对
RelativeLayout的属性-
反方向对齐属性:
to / above / below -
同方向对齐属性:
align -
相对父视图的属性
alignparent / center
-
-
只针对
LinearLayout的属性-
权重属性
weight -
方向属性
orientation
-
常用基本属性
| 属性名 | 作用 |
|---|---|
| id | 为控件指定相应的ID @+id/idname |
| layout_width | 指定当前视图的宽度 |
| layout_height | 指定当前视图的高度 |
| text | 指定控件当中显示的文字 |
| textSize | 指定控件文字大小 |
| background | 指定该控件所使用的背景(图片 / 颜色) |
| layout_gravity | 控件本身相对于父视图的位置 |
| gravity | 指定控件中的内容的基本位置 |
内边距与外边距
-
内边距属性
-
android:padding -
android:paddingLeft -
android:paddingRight -
android:paddingStart -
android:paddingEnd -
android:paddingTop -
android:paddingBottom
-
-
外边距属性
-
android:layout_margin -
android:layout_marginLeft -
android:layout_marginRight -
android:layout_marginStart -
android:layout_marginEnd -
android:layout_marginTop -
android:layout_marginBottom
-
对于从左到右(LTR)语言,start就是left,end就是right。对于从右到左(RTL)语言(例如,希伯来语,阿拉伯语,中东地区),end是left,start是right。如果您使用end和start属性,那么当您的布局在设置为RTL区域设置的设备上运行时,布局将会镜像,如果使用left和right则不会镜像。一般推荐使用
start/end属性。配置
RTL布局默认是关闭的。
AndroidManifest.xml文件下<application>配置属性android:supportsRtl="true"。然后将相应视图标签设置属性android:layoutDirection="rtl"。例如,修改本章2.2相对布局:
注意:start 和 end 属性在 API Level 17(Android 4.2)才被添加。低于此版本不能使用。
同方向对齐和反方向对齐:相对兄弟视图
-
同方向对齐属性
-
android:layout_alignLeft -
android:layout_alignRight -
android:layout_alignStart -
android:layout_alignEnd -
android:layout_alignTop -
android:layout_alignBottom
-
-
反方向对齐属性
-
android:layout_toLeftOf -
android:layout_toRightOf -
android:layout_toStartOf -
android:layout_toEndOf -
android:layout_above -
android:layout_below
-
相对父视图定位
-
与父视图同方向对齐属性
-
android:layout_alignParentStart -
android:layout_alignParentEnd -
android:layout_alignParentLeft -
android:layout_alignParentRight -
android:layout_alignParentTop -
android:layout_alignParentBottom
-
-
相对父视图居中属性
-
android:layout_centerInParent -
android:layout_centerVertical -
android:layout_centerHorizontal
-
2.4 ListView
-
ListView是一种用来显示多个可滑动项(Item)列表的ViewGroup
-
需要使用Adapter将集合数据和每一个Item所对应的布局动态适配到LIstView中显示
-
显示列表:
listView.setAdapter(adapter)

Adapter

-
ArrayAdapter:显示最简单的列表(文本),集合数据为
List<String> 或 String[] -
SimpleAdapter:显示复杂的列表,集合数据必须是
List<Map<String, Object>>类型 -
BaseAdapter:显示复杂的列表,集合数据可以是任意类型的集合
List<Xxx> -
SimpleCursorAdapter:显示复杂的列表,集合数据是数据库查询结果集
ListView + ArrayAdapter
ArrayAdapter(Contxet context, int resource, T[] objects)
ArrayAdapter(Context context, int resource, List<T> objects)
-
context:上下文对象,一般为Activity对象
-
resource:Item的布局文件标识
-
objects:需要显示的集合数据(Array 或 List)
// MainActivity.java
public class MainActivity extends AppCompatActivity {
private ListView lv_main;
<!-- activity_main.xml -->
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<!-- activity_item.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:textSize="20sp"
android:gravity="center"
android:layout_height="60dp">
</TextView>

ListView + SimpleAdapter
SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
-
context:上下文对象,一般为Activity对象
-
data:需要显示的数据集合
-
resource:Item的布局文件标识
-
from:map对象中的key的数组,用于得到对应的value
-
to:Item布局文件中的子view的id的数组

布局文件
<!--activity_main.xml-->
代码
lv_main = findViewById(R.id.lv_main);
// 准备集合数据
List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();
Map<String, Object> map = new HashMap<String, Object>();
map.put("icon", R.drawable.duck);
map.put("name", "鸭子");
map.put("content", "---");
data.add(map);
map = new HashMap<String, Object>();
map.put("icon", R.drawable.cat);
map.put("name", "猫");
map.put("content", "---");
data.add(map);
...
map = new HashMap<String, Object>();
map.put("icon", R.drawable.zhongli);
map.put("name", "钟离");
map.put("content", "---");
data.add(map);
// map对象中的key的数组,用于得到对应的value
String[] from = {"icon", "name", "content"};
// item 布局文件中子view的id的数组
int[] to = {R.id.iv_icon, R.id.tv_name, R.id.tv_content};
// 准备Adapter对象
SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.activity_item, from, to);
// 设置Adapter显示列表
lv_main.setAdapter(adapter);
ListView + BaseAdapter
class MyBaseAdapter extends BaseAdapter { // BaseAdapter抽象类 需要重写方法
实现效果和 SimpleAdapter 一样,布局文件无需变动,下面是代码:
com/example/helloandroid/ShopInfo.java
/**
* 每行Item数据信息封装类
*/
public class ShopInfo {
private int icon;
private String name;
private String content;
...generate 空参构造函数 三个参数构造函数 getter setter toString
}
mainActivity.java
public class MainActivity extends AppCompatActivity {
private ListView lv_main;
private List<ShopInfo> data;


