1 控件
Android 提供大量的 UI 控件,合理地使用这些控件,就可以编写出不错的界面。
在每个布局文件中都会使用 xmlns:android
这个属性来指定一个命名空间,这样就可以一直使用 android:id
、android:layout_width
等类似 android:attribute
的写法,那么在布局文件再指定一个 xmlns:app
指定一个新的命名空间,也就可以使用 app:attribute
的写法了。
所有控件都具有 android:layout_width
和 android:layout_height
两个属性,它们分别指定了控件的宽度和高度,可选值有三个:
match_parent
和fill_parent
:让当前控件的大小与父布局大小一样wrap_content
:表示当前控件的大小能够刚好包含主里面的内容
1.1 TextView
- 通过
android:text
指定TextView
中的显示文本内容 - 使用
android:gravity
指定文本的对齐方式,可选值有:top
bottom
left
right
center_vertical
center_horizontal
center
:等同于center_vertical|center_horizontal
android:textSize
:文字大小,单位使用sp
android:textColor
:文字颜色
1.2 Button
系统会对 Button
中的所有英文字母自动进行大写转换,可以使用以下属性禁用:
android:textAllcaps="false"
1.3 EditText
EditText
允许用户在控件中输入和编辑内容,其实就是 html 的 <input>
标签,而达到 placeholder
属性功能的就是使用 android:hint
属性,比如:
android:hint="输入用户密码"
如果 EditText
的高度设置 wrap_content
,所以而随着输入的内容不断变长,EditText
也会不断地拉长,此时可以使用 android:maxLines
属性来解决这个问题,比如:
android:maxLines=2
就表示该 EditText
的高度最高为 2 行。
在代码中获取了对应的 View
对象(比如使用了 findViewById
方法),需要转换为 EditText
对象,它通过 getText()
方法可以获取对应的输入的数值。
1.4 ImageView
ImageView
用于在界面上展示图片,图片资源通常都是放置在 drawable
开头的目录下,它使用 android:src
的属性指定图片,如:
android:src="@drawable/img_1"
在代码中获取到对应的 ImageView
对象,可以使用 setImageResource
方法设置图片,如:
imageView.setImageResource(R.drawable.img_2);
1.5 ProgressBar
ProgressBar
用于在界面显示一个进度条,控件可以通过设置 android:visibility
来指定该控件是否可见,可选值有:
visible
:默认值,控件可见invisible
:控件不可见,但依旧占据位置gone
:控件不可见,且不占据位置
以上数值代码可以通过 getVisibility()
使用 setVisibility()
方法设置,分别为:
View.VISIBLE
View.INVISIBLE
View.GONE
默认的进度条时一个圆形进度,可以通过设置 style
属性将它指定为水平进度条,如:
style="?android:attr/progressBarStyleHorizontal"
指定水平进度条后,可通过 android:max
属性设置进度条一个最大值,而在代码中可以通过 setProgress
方法传入小于最大值的数值,表示完成进度。
1.6 AlertDialog
AlertDialog
用于显示对话框,这个对话框置顶在所有界面元素之上,能够屏蔽掉其他控件的交互能力。
在代码中使用 AlertDialog.Builder
创建 AlertDialog
实例,然后可以为这个对象设置标题、内容、可否使用手机的 Back
键关闭对话框、绑定确定按钮事件、绑定取消按钮事件,设置完之后使用 show
方法将对话框显示出来。
如下:
// 创建 dialog 对象
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
// 设置标题
dialog.setTitle("this is title");
// 设置消息
dialog.setMessage("this is Message");
// 设置是否可使用手机 Back 取消对话框
dialog.setCancelable(false);
// 绑定确定按钮事件
// 第一参数为:显示字符串
// 第二为:事件逻辑
dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "点击确定按钮", Toast.LENGTH_SHORT).show();
}
});
// 绑定取消按钮事件
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "点击取消按钮", Toast.LENGTH_LONG).show();
}
});
// 最后要调用show
dialog.show();
1.7 ProgressDialog
ProgressDialog
和 AlertDialog
类似,不同在于 ProgressDialog
会在对话框中显示一个进度条,表示当前操作比较耗时,用法如下:
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setMessage("加载中。。。");
// 设置了 Cancelable 为 false 之后,需要调用 dismiss 方法才能将对话框消失
progressDialog.setCancelable(false);
progressDialog.show();
2 布局
为了让控件能有条不紊地摆放在界面中,就需要使用到布局了。Android 有四种基本布局
2.1 线性布局 LinearLayout
线性布局就像名字所描述的一样,这个布局将它包含的控件在 线性方向 上依次排列。
2.1.1 android:orientation
通过设置 android:orientation
属性指定排列方向:
vertical
:垂直方向horizontal
:水平方向
2.1.2 android:layout_gravity
通过设置控件的 android:layout-gravity
属性可以指定控件在布局形式,它的用法与 android:gravity
类似,不过要注意布局的排列方向,比如当排列方向是 horizontal
的时候,只有垂直方向上的对齐方式才生效,此时设置水平方向的 android:layout-gravity
不会生效。
2.1.3 android:layout_weight
android:layout_weight
属性允许使用比例的方式指定控件的大小,系统会先把 LinearLayout
下所有控件指定的 layout_weight
值相加,得到一个总值,然后每个控件所占大小的比例就是该控件的 layout_weight
数值除以总值。
注:由于每个控件都应该设置
android:layout_width
属性,所以在设置android:layout_weight
的同时比较规范的写法就是设置android:width
属性的值为0
2.2 相对布局 ReletiveLayout
相对布局通过相对定位的方式让控件出现在布局的任何位置。
2.2.1 相对于父布局定位
android:layout_alignParentTop="true"
:位于父布局的顶部android:layout_alignParentRight="true"
:位于父布局的右部android:layout_alignParentBottom="true"
:位于父布局的底部android:layout_alignParentLeft="true"
:位于父布局的左部android:layout_centerInParent="true"
:位于父布局的中心
2.2.2 相对于控件定位
android:layout_above="@id/button3"
:位于button3
控件的上方android:layout_toRightOf="@id/button3"
:位于button3
控件的右方android:layout_below="@id/button3"
:位于button3
控件的下方android:layout_toLeftOf="@id/button3"
:位于button3
控件的下方android:layout_alignTop="@id/button3"
:与button3
控件的上边缘对齐android:layout_alignRight="@id/button3"
:与button3
控件的右边缘对齐android:layout_alignBottom="@id/button3"
:与button3
控件的下边缘对齐android:layout_alignLeft="@id/button3"
:与button3
控件的左边缘对齐
2.3 帧布局 FrameLayout
在帧布局的所有控件都会默认摆放在布局的左上角,同时还可以使用 android:layout_gravity
属性指定控件在布局中的对齐方式
2.4 百分比布局
在线性布局中可以使用 android:layout_weight
属性达到按比例指定控件大小的功能,那么帧布局和相对定位布局就需要 PercentFrameLayout
和 PecentRelativeLayout
3 自定义控件
上图是控件和布局的继承结构,可以看到所有的控件都直接或间接地继承自 View
,而布局直接或间接地继承自 ViewGroup
,而 ViewGroup
又继承自 View
,ViewGroup
可以包含很多子 View
和子 ViewGroup
,这是一个用于放置控件和布局的控件。
View
是 Android 中最基本的一种 UI 组件,它可以在屏幕上绘制一个矩形区域,并能响应这个区域的各种事件,因此,我们使用的各种控件其实就是在 View
的基础上添加各自特有的功能。
3.1 引入布局
有些布局的重复度很高,就可以将它提取出来,作为公共布局引入需要的布局文件。
引入布局的做法很简单,只需编写一个布局文件,在需要引用的布局文件调用 <include>
标签即可,如:
<include layout="@layout/title" />
表示导入 title.xml
布局文件的布局,此时在代码中,也可以使用 findViewById
方法获取到 title.xml
布局文件中对应 id
的控件。
3.2 创建自定义控件
引入布局可以消除很多重复的代码,但它对应控件事件都需要在使用引用布局的活动中重新声明,如果事件都是重复,就可以创建自定义控件,比如创建一个标题栏,它有个返回键,用于返回上一个活动。
public class TitleLayout extends LinearLayout {
public TitleLayout(final Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 通过 LayoutInflater 的 from 方法获取 LayoutInflater 对象
// LayoutInflater 对象调用 inflate 方法可以加载一个布局文件
// 第一个参数是控件35·布局文件Id
// 第二个参数是给加载好的的布局再添一个父布局,这里用 this 指定 TitleLayout 作为父布局
LayoutInflater.from(context).inflate(R.layout.title, this);
Button button = (Button) findViewById(R.id.backBtn);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 通过 getContext 可以获取当前的上下文,可以强制转化为 Activity
((Activity) getContext()).finish();
}
});
}
}
上面代码用于创建一个公共标题栏控件,LayoutInflater
的 from()
方法可以构建一个 LayoutInflater
对象,然后调用 inflate()
方法可以动态地加载一个布局文件,它接受两个参数,分别是:
- 需要加载的布局 ID
- 给加载好的布局再添上一个父布局
使用自定义控件和使用普通控件的方式基本一样,在添加自定义控件的时候需要 指明控件的完整类名,包名在这里是不可以忽略的。如:
<top.seiei.aboutactivity.component.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"></top.seiei.aboutactivity.component.TitleLayout>
此时在使用了自定义控件的控件活动类中无法使用 findViewById
的形式获取对应的自定义控件 View
。
4 滚动控件
4.1 ListView
不同于网页设置滚动只需设置样式 overflow:scroll
,Android 设置滚动有点麻烦。
其中 ListView
控件可以达到竖直方向下的滚动效果,而一般的数组类型的数据无法直接传递给 ListView
,它需要接受 适配器 类型的数据。
适配器(Adapter
)在 Android 中是数据和视图(View
)之间的一个桥梁,通过适配器以便于数据在 View
视图上显示,它的 getView
方法可以指定如何布局、显示数据。
ListView
的基本用法是:
- 在对应的布局文件添加
ListView
标签 - 声明数据源
- 初始化对应适配器
- 对应的
ListView
对象绑定适配器
4.1.1 布局中 ListView
控件的创建
布局文件中创建 ListView
控件很简单,如下
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
4.1.2 适配器的建立
Android 定义了很多适配器的实现类,这里采用最常用的 ArrayAdapter
,它可以通过泛型来指定要适配的数据类型(即将导入数组的数据类型),然后通过构造函数把要适配的数据传入。
ArrayAdapter
的构造函数也有多个重载,这里也采用最常用的构造函数,它依次传入的参数是:
- 当前上下文
- 用于设置展示
ListView
子项的布局文件id
- 需要适配的数据(即数据源数组)
下面代码,就是一个继承 ArrayAdapter
的自定义适配器,同时重写了 getView
方法用于定制 ListView
的界面:
public class TestAboutAdapter extends ArrayAdapter<Map<String, String>> {
// 记录展示子项布局文件id
private int resourceId;
public TestAboutAdapter(@NonNull Context context, int textViewResourceId, @NonNull List<Map<String, String>> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
/**
* getView 方法在每个子项被滚动到屏幕内时,被调用
* @param position 用于获取数据源对应的子项数据
* @param convertView 用于将之前加载好的布局进行缓存
* @param parent 父布局
* @return
*/
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)
{
// 获取数组当前项的数据
Map<String, String> item = getItem(position);
// 获取 ListView 子项布局的 view
// View 一旦有了父布局,就不能添加到 ListView 中,所以第三个参数要设置为 false
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
TextView textView1 = (TextView) view.findViewById(R.id.title);
TextView textView2 = (TextView) view.findViewById(R.id.value);
textView1.setText("标题");
textView2.setText(item.get("内容"));
return view;
}
}
4.1.3 绑定适配器
获取对应的 ListView
对象后,通过调用 setAdapter
方法传入对应的适配器即可完成绑定。
public class AboutListViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about_list_view);
// 准备数据
List<Map<String, String>> data = new ArrayList<>();
for (int i=0; i<100; i++) {
Map<String, String> obj = new HashMap<>();
obj.put("内容", i + "");
data.add(obj);
}
// 创建适配器
ArrayAdapter<Map<String, String>> adapter = new TestAboutAdapter(AboutListViewActivity.this, R.layout.listview_item, data);
// 获取 ListView,导入 adapter 数据
ListView listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);
}
}
上面代码中配置适配器时,传入的 R.layout.listview_item
即是用于显示子项的布局文件 id
。
4.1.4 ListView
的点击事件
ListView
中有一个 setOnItemClickListenter
方法为 ListView
注册一个监听器,当用户点击 ListView
中的任何子项时,就会回调 onItemClick
方法,如:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Map<String, String> obj = data.get(position);
Toast.makeText(AboutListViewActivity.this, obj.get("value"), Toast.LENGTH_LONG).show();
}
});
4.1.5 ListView
的优化
上述自定义适配器的代码中,getView
方法每次调用都会加载一次子项布局,当 ListView
快速滚动的时候,这就会成为性能的瓶颈。
此时可以使用 getView
方法中的第二个参数 converView
进行布局的缓存,代码修改后如下:
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)
{
// 获取数组当前项的数据
Map<String, String> item = getItem(position);
View view;
// 缓存布局
if (convertView != null) {
view = convertView;
} else {
// 获取 ListView 子项布局的 view
// View 一旦有了父布局,就不能添加到 ListView 中,所以第三个参数要设置为 false
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
}
TextView textView1 = (TextView) view.findViewById(R.id.title);
TextView textView2 = (TextView) view.findViewById(R.id.value);
textView1.setText("标题");
textView2.setText(item.get("内容"));
return view;
}
但此时代码也还有可以改进的地方,那就是尽管缓存了布局,但每次 getView
的调用还是会调用 findViewById
方法获取控件实例,此时就需要使用到 View
的 setTag
和 getTag
方法了。
创建一个内部类用于存储 TextView
控件,并将该内部类通过 setTag
的形式传入到 convertView
中,然后在使用的时候通过 getTag
的形式获取出来即可,所以上面的代码就演变成:
public class TestAboutAdapter extends ArrayAdapter<Map<String, String>> {
// 记录展示子项布局文件id
private int resourceId;
public TestAboutAdapter(@NonNull Context context, int textViewResourceId, @NonNull List<Map<String, String>> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
/**
* getView 方法在每个子项被滚动到屏幕内时,被调用
* @param position
* @param convertView
* @param parent
* @return
*/
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
// 获取数组当前项的数据
Map<String, String> item = getItem(position);
View view;
Msg msg;
// 缓存布局
if (convertView != null) {
view = convertView;
msg = (Msg) view.getTag();
} else {
// 获取 ListView 子项布局的 view
// View 一旦有了父布局,就不能添加到 ListView 中,所以第三个参数要设置为 false
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
msg = new Msg();
viewHolder.setTextView1((TextView) msg.findViewById(R.id.title));
viewHolder.setTextView2((TextView) msg.findViewById(R.id.value));
view.setTag(msg);
}
TextView textView1 = msg.getTextView1();
TextView textView2 = msg.getTextView2();
textView1.setText("标题");
textView2.setText(item.get("内容"));
return view;
}
// 用于存储信息的内部类
class Msg {
private TextView textView1;
private TextView textView2;
public TextView getTextView1() {
return textView1;
}
public void setTextView1(TextView textView1) {
this.textView1 = textView1;
}
public TextView getTextView2() {
return textView2;
}
public void setTextView2(TextView textView2) {
this.textView2 = textView2;
}
}
}
4.2 RecyclerView
ListView
控件只能实现数据纵向滚动的效果,并不能做到横向滚动,同时不优化的话,性能也非常差。
为此 Android 推出了 RecyclerView
控件。
RecyclerView
的基本用法:
- 在对应的布局文件添加
RecyclerView
标签 - 声明数据源
- 设定
RecyclerView
的布局方式 - 初始化对应适配器
- 对应的
RecyclerView
对象绑定适配器
RecyclerView
控件属于新增的控件,因此想要使用,就需要在 build.gradle
中添加相关的依赖库:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
4.2.1 布局中 RecyclerView
控件的创建
由于 RecyclerView
并不是内置在系统 SDK 中,所以需要把完整的包路径写出来,如:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyler"
android:layout_width="match_parent"
android:layout_height="match_parent"></androidx.recyclerview.widget.RecyclerView>
4.2.2 适配器的建立
RecyclerView
的适配器需要继承自 RecyclerView.Adapter
,要注意一下两点:
- 它需要指定泛型,该泛型需要继承自
Recycler.ViewHolder
类(view
支架),其主要作用是存储自定义的子项布局文件里控件的实例,以便向其填充数据。这个ViewHolder
类一般在类的内部声明即可。 - 继承自
RecyclerView.Adapter
类需要重写三个方法,分别是:onCreateViewHolder
:实例化ViewHolder
类的地方onBindViewHolder
:每个子项被滚动到屏幕内的时候执行,在这里,可以获取ViewHolder
实例,并将数据设置到布局文件中getItemCount
:获取源数据的长度
- 创建用于设置源数据的构造函数
一般的做法是。先设置 ViewHolder
内部类,然后创建构造方法,接着实现三个方法,例子代码如下:
public class TestAboutRecyclerViewAdapter extends RecyclerView.Adapter<TestAboutRecyclerViewAdapter.ViewHolder> {
private List<Map<String, String>> objList;
// 接受数据源的构造函数
public TestAboutRecyclerViewAdapter(List<Map<String, String>> args) {
objList = args;
}
// 重写 onCreateViewHolder 方法
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listview_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
// 重写 onBindViewHolder
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Map<String, String> obj = objList.get(position);
holder.textView1.setText("标题");
holder.textView2.setText(obj.get("内容"));
}
// 获取数据源的长度
@Override
public int getItemCount() {
return objList.size();
}
// viewHolder 内部类
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView1;
TextView textView2;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textView1 = itemView.findViewById(R.id.title);
textView2 = itemView.findViewById(R.id.value);
}
}
}
4.2.3 绑定适配器
获取对应的 RecyclerView
对象后,通过调用 setAdapter
方法传入对应的适配器即可完成绑定。其中 LayoutManager
用于指定 RecyclerView
的布局方式,LinearLayoutManager
表示线性布局,同样的还有 实现网格布局的 GridLayoutManager
和实现瀑布流布局的 StaggerderGridLayoutManager
public class AboutRecyclerViewActivity extends AppCompatActivity {
private List<Map<String, String>> data = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about_recycler_view);
// 获取 RecyclerView 控件
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
// LayoutManager 用于指定 RecyclerView 的布局方式,LinearLayoutManager 表示线性布局
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// 以下设置为水平方向滚动,注意此时子项的布局文件也做响应的修改
// linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
// 准备数据
data = new ArrayList<>();
for (int i=0; i<100; i++) {
Map<String, String> obj = new HashMap<>();
obj.put("内容", i + "");
data.add(obj);
}
// 创建适配器
TestAboutRecyclerViewAdapter testAboutRecyclerViewAdapter = new TestAboutRecyclerViewAdapter(data);
recyclerView.setAdapter(testAboutRecyclerViewAdapter);
}
}
4.2.4 RecyclerView
的点击事件
RecyclerView
没有提供类似于 setOnItemClickListener
这样的注册监听器方法,而是需要我们给 子项 具体的 View
去注册点击事件,这里的绑定事件声明应写在 onCreateViewHolder
重写方法中。
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listview_item, parent, false);
// 绑定事件
viewHolder.textView1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// ViewHolder 透过 getAdapterPosition 获取对应的数据源数组 索引
int position = viewHolder.getAdapterPosition();
Map<String, String> obj = objList.get(position);
Toast.makeText(v.getContext(), obj.get("内容"), Toast.LENGTH_LONG).show();
}
});
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
4.2.5 刷新列表数据
有时候需要刷新滚动布局里的列表数据,这里的刷新逻辑应该写在 适配器 中,通过修改适配器内用于存储列表数据的变量,再调用 notifyDataSetChanged
方法达到刷新列表数据的效果,代码如下,在所属的活动中调用适配器的 refresh
方法即可:
class NewsContentAdapter extends RecyclerView.Adapter<NewsContentAdapter.ViewHolder> {
private String content;
NewsContentAdapter(String arg) {
content = arg;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_content_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TextView newsContent = holder.newsContent;
newsContent.setText(content);
}
@Override
public int getItemCount() {
return 1;
}
/**
* 刷新数据
* @param args
*/
public void refresh(String args) {
content = args;
notifyDataSetChanged(); // 用于提醒数据刷新
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView newsContent;
public ViewHolder(@NonNull View itemView) {
super(itemView);
newsContent = itemView.findViewById(R.id.news_content);
}
}
}