原文链接:Android学习羁绊–>UI设计
软件开发过程中,界面设计和功能开发同样重要。Android中有多种编写程序界面的方式可供选择。接下来学习一下Android的UI开发。
文章目录
常见UI控件
TextView
TextView是Android中最简单的一个控件,它主要用于在界面上显示一段文本信息。<TextView>标签有以下这些属性:
- andriod:id :给当前控件定义一个唯一标识符。
- android:layout_width :指定控件的宽度。
android:layout_height :指定控件的高度。
Android中所有控件都有这两个属性,有三个可选值:
- match_parent:表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。
- fill_parent:与match_parent意义相同,官方推荐使用match_parent。
- wrap_content:表示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。
- android:text :指定TextView中显示的文本内容。
- android:gravity :指定文字的对齐方式,可选值有top 、bottom 、left、right 、center 等,可以用“|”来同时指定多个值。
- android:textSize :指定文字大小。Android中字体大小使用sp作为单位。
- android:textColor :指定文字的颜色。
- android:layout_gravity :用于指定控件在布局中的对齐方式,可选值和android:gravity差不多
- android:background :为布局或控件指定一个背景,可以使用颜色或图片来进行填充
- android:layout_margin :指定控件在上下左右方向上偏移的距离
以上属性都是所有控件都拥有的。
Button
Button(按钮)用于与用户进行交互,<Button>标签中除了共有属性,还有下面这个属性:
- android:textAllCaps:指定系统是否将Button中的英文字母自动进行大写转换,默认为true。
在界面上定义了一个Button控件还不够,还需要将Button的点击事件注册一个监听器,如下代码所示:
Button button = (Button) findViewById(R.id.bottom);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击按钮的逻辑
}
});
- 通过findViewById()方法获取到在布局文件中定义的元素。findViewById()方法返回的是一个View对象,需要向下转型将它转成Button对象。
- 通过调用setOnClickListener()方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()方法。监听器有两种构建方式,一种就是上述代码通过View.OnClickListener接口的匿名内部类来构建,还有一种方式,就是实现View.OnClickListener接口,代码如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}
@Override
public void onClick(View v) {
// 在此处添加逻辑
}
}
EditText
EditText允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。除了通用属性,<EditText>标签还有这些属性:
- android:hint:指定一段提示性的文本。当输入任何内容时,这段文本就会自动消失。
- android:maxLines:限制EditText最大行数
- android:maxHeight:限制EditText最大高度
- android:maxLength:限制EditText最大长度
- android:maxWidth:限制EditText最大宽度
可通过如下代码来获得EditText的实例,获取EditText中的内容,然后进行相关操作:
EditText editText = (EditText) findViewById(R.id.edit_text);
String text = editText.getText().toString();
ImageView
ImageView是用于在界面上展示图片的一个控件,图片通常都是放在以“drawable”开头的目录下的。<ImageView>标签中除了通用属性,还有android:src属性,这个属性给ImageView指定了一张图片。
可通过以下代码获取ImageView实例,进行相关的操作:
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageResource(R.drawable.img);
ImageView的setImageResource()方法将显示的图片改成指定的图片,可用于动态变换图片。
ProgressBar
ProgressBar用于在界面上显示一个进度条,表示程序正在加载一些数据。除了通用属性,<ProgressBar>标签中还有这些属性:
- style:指定进度条的样式
- android:max:给进度条设置一个最大值
通过代码可以动态更改进度条的进度,如下所示:
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar);
int progress = progressBar.getProgress();
progress = progress + 10;
progressBar.setProgress(progress);
Android控件的可见属性
在所有的Android控件中除了上面的通用属性,还有可见属性:android:visibility,这个属性有三个值:
- visible:表示控件是可见的,这个值是默认值,不指定android:visibility 时,控件都是可见的。
- invisible:表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
- gone:则表示控件不仅不可见,而且不再占用任何屏幕空间。
除了在xml文件中可以设置控件的可见性,还可以通过代码来设置控件可见性:
//以ProgressBar为例,获取控件实例然后设置控件可见性
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar);
//获取控件可见性
progressBar.getVisibility();
progressBar.setVisibility(View.VISIBLE);
//progressBar.setVisibility(View.INVISIBLE);
//progressBar.setVisibility(View.GONE);
getVisibility();方法用于获取控件的可见性,对应的值为:View.VISIBLE,View.INVISIBLE,View.GONE。
AlertDialog
AlertDialog在当前的界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此AlertDialog一般都是用于提示一些非常重要的内容或者警告信息。相关代码如下所示:
AlertDialog.Builder dialog = new AlertDialog.Builder (MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something");
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();
创建AlterDialog实例通过AlertDialog.Builder()来创建,传入当前Activity。AlterDialog常见方法:
- setTitle():设置对话框标题。
- setMessage():设置对话框的内容。
- setCancelable():可否用Back键关闭对话框。
- setPositiveButton():为对话框设置确定按钮的点击事件。这个方法有两个参数,一个是确认按钮的名称,一个是监听事件。
- setNegativeButton():设置取消按钮的点击事件。这个方法有两个参数,一个是取消按钮的名称,一个是监听事件。
- show():显示对话框。
ProgressDialog
ProgressDialog是AlterDialog的子类,功能与AlterDialog相似,不同点在于ProgressDialog会在对话框中显示一个进度条,一般用于表示当前操作比较耗时,让用户耐心地等待。这个类在API level 26 中,ProgressDialog被声明不赞成使用,应使用的替代方法是ProgressBar。
ProgressDialog用法与AlterDialog类似,如下所示:
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
PS:如果在setCancelable()中传入了false,表示ProgressDialog是不能通过Back键取消掉。这时一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。
基本布局
布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。布局和控件关系如下图所示
线性布局
LinearLayout称作线性布局,是一种非常常用的布局,这个布局会将它所包含的控件在线性方向上依次排列。<ProgressBar>标签通过android:orientation属性指定排列方向,可选值有两个:
- vertical:控件沿垂直方向排列。
- horizontal:控件沿水平方向排列,默认的排列方式。
Tips:
- 如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,因为这样的话,单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
- 当布局中的控件指定了android:layout_gravity这个属性,LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。
LinearLayout中控件还有一个重要属性android:layout_weight,这个属性使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。如下所示
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"/>
<!--PS:dp是Android中用于指定控件大小、间距等属性的单位-->
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send"/>
</LinearLayout>
使用了android:layout_weight 属性,此时控件的宽度就不应该再由android:layout_width 来决定,这里指定成0dp是一种比较规范的写法。
android:layout_weight如何控制控件大小:系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值。
相对布局
RelativeLayout又称作相对布局,也是一种非常常用的布局。RelativeLayout可以通过相对定位的方式让控件出现在布局的任何位置。
在RelativeLayout布局下,控件属性也有非常多:
- android:layout_alignParentLeft:是否使控件位于父布局左侧(true or false)
- android:layout_alignParentTop:是否使控件位于父布局顶部(true or false)
- android:layout_alignParentRight:是否使控件位于父布局右侧(true or false)
- android:layout_alignParentBottom:是否使控件位于父布局底部(true or false)
- android:layout_centerInParent:是否使控件位于父布局中间(true or false)
- android:layout_above:让一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用(当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况)
- android:layout_below:让一个控件位于另一个控件的下方,需要为这个属性指定相对控件id的引用
- android:layout_toLeftOf:表示让一个控件位于另一个控件的左侧,需要为这个属性指定相对控件id的引用
- android:layout_toRightOf:表示让一个控件位于另一个控件的右侧,需要为这个属性指定相对控件id的引用
- android:layout_alignLeft:让一个控件的左边缘和另一个控件的左边缘对齐
- android:layout_alignRight:让一个控件的右边缘和另一个控件的右边缘对齐
- android:layout_alignTop:让一个控件的上边缘和另一个控件的上边缘对齐
- android:layout_alignBottom:让一个控件的下边缘和另一个控件的下边缘对齐
- 。。。。。。
RelativeLayout属性较多,可通过属性名称来判断该属性对控件的定位
帧布局
FrameLayout又称作帧布局,它的应用场景少很多。这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角。
可以使用layout_gravity属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。
自定义控件
所有控件都是直接或间接继承自View的,所有布局都是直接或间接继承自ViewGroup的。
View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,Android的各种控件其实就是在View的基础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
引入布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<include layout="@layout/title" />
</LinearLayout>
在一个布局文件中引入另外一个布局文件,使用<include>标签,在layout属性填入要引入的布局id。
若引入的布局是标题栏,还需要将系统自带的标题栏隐藏,隐藏代码如下:
ActionBar actionbar = getSupportActionBar();
if (actionbar != null) {
actionbar.hide();
}
- getSupportActionBar()方法来获得ActionBar的实例
- hide()方法将标题栏隐藏起来
创建自定义控件
自定义控件能解决系统控件不能满足实际需求,重复代码的问题。
以自定义标题栏为例,自定义标题栏控件,代码如下:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
//添加其他相关逻辑
}
}
新建一个类继承View或者View的子类,然后重写构造方法,在构造方法中添加响应的逻辑(还可以重写View中的其他方法)。使用LayoutInflater的from()方法可以构建出一个LayoutInflater 对象,然后调用inflate()方法就可以动态加载一个布局文件。
inflate() 方法接收两个参数:
- 第一个参数是要加载的布局文件的id
- 第二个参数是给加载好的布局再添加一个父布局
创建好自定义布局之后,需要在需要在其他布局中引用自定义布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cn.chenjianlink.android.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,需要指明控件的完整类名,包名不可以省略。
ListView
程序中有大量的数据需要展示的时候,可以借助ListView来实现。
基本使用
在Activity布局文件中引入ListView控件
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/List_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
ListView是用于展示大量数据的,这些数据一般是数组或者是集合,无法直接导入到ListView中,需要借助适配器来导入数据。一般使用的是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。
ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects)
ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List<T> objects)
构造好适配器对象后,获取ListView实例,调用ListView的**setAdapter()**方法,将构建好的适配器对象传递进去。这样ListView就能显示数据了
定制ListView
官方提供的ListView界面不一定能满足需求,可以自己定制一个。
定制ListView官方提供的适配器可能就不好用了,需要自定义一个适配器。适配器可通过继承ArrayAdapter或是其父类来实现。
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
//添加相关逻辑
}
- 一般要重写getView方法。这个方法在每个子项被滚动到屏幕内的时候会被调用。
- 使用LayoutInflater来指定每个子项的布局文件
- getContext()获取当前Activity
- 一旦View有了父布局之后,它就不能再添加到ListView中了,所以LayoutInflater的inflate()方法的第三个参数设置为false。
- convertView这个参数用于将之前加载好的布局进行缓存,可以之后进行重用(可以判断convertView是否为空,若为空,则使用LayoutInflater去加载布局,若不为空,则直接重用)
优化ListView
- getView(int position, @Nullable View convertView, @NonNull ViewGroup parent),判断convertView是否为空,来进行优化
- 新建一个ViewHolder类来对控件实例进行缓存,调用View的getTag()方法将ViewHolder存进View中,使用getTag()方法获取ViewHolder对象
点击事件
setOnItemClickListener()方法为ListView注册了一个监听器,当用户点击了ListView中的任何一个子项时,就会回调onItemClick()方法
ListView listView = (ListView) findViewById(R.id.List_view);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//添加相关逻辑
}
});
RecyclerView
RecyclerView是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。目前Android官方更加推荐使用RecyclerView。
添加RecyclerView
RecyclerView属于新增控件,需要在build.gradle文件中添加相应的依赖库,由于AndroidSDK在高版本出现变动,所以添加的依赖有些不同,AndroidSDK版本在28及以上以及Gradle版本为4.6及以上时,dependencies闭包添加如下内容
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
}
在AndroidSdk版本在28以下以及Gradle版本为4.6以下时,dependencies闭包添加如下内容
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:recyclerview-v7:24.2.1'
testCompile 'junit:junit:4.12'
}
基本用法
在布局文件中引入RecyclerView,AndroidSDK版本在28及以上以及Gradle版本为4.6及以上时,使用以下代码引入RecyclerView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在AndroidSdk版本在28以下以及Gradle版本为4.6以下时使用以下标签引入RecyclerView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
引入RecyclerView后,需要给RecyclerView配置一个适配器,新建一个适配器,让这个适配器继承RecyclerView.Adapter类,相关代码如下:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
static class ViewHolder extends RecyclerView.ViewHolder {
View MyView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
//构建ViewHolder的相关逻辑
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.My_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
//相关逻辑
//......
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
//相关逻辑
}
@Override
public int getItemCount() {
//相关逻辑
}
}
- 定义一个内部类ViewHolder,ViewHolder要继承自RecyclerView.ViewHolder。然后ViewHolder的构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局。
- onCreateViewHolder()、onBindViewHolder()和getItemCount()这三个方法必须重写
- onCreateViewHolder()方法用于创建ViewHolder实例
- onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行
- getItemCount()方法返回RecyclerView一共有多少子项
适配器准备好之后,需要在Activity中进行必要设置:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
//下面这句代码是设置布局滚动方向
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
MyAdapter adapter = new MyAdapter();
recyclerView.setAdapter(adapter);
//其他逻辑
}
}
- 获取RecyclerView实例,然后创建布局管理器以及适配器,并将这两者绑定到RecyclerView实例中
- LinearLayoutManager用于指定RecyclerView为线性布局,可实现和ListView一样的效果。可通过LinearLayoutManager的setOrientation()方法来设置布局的排列方向,默认是纵向排列,默认值为LinearLayoutManager.VERTICAL。要设置成横向排列,则传入LinearLayoutManager.HORIZONTAL。
- ListView的布局排列是由自身去管理的,而RecyclerView则将布局排列交给了LayoutManager,LayoutManager中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局。除了LinearLayoutManager,RecyclerView还提供了GridLayoutManager(网格布局)和StaggeredGridLayoutManager(瀑布流布局)这两种内置的布局排列方式。
使用瀑布流布局代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
StaggeredGridLayoutManager的构造函数接收两个参数,第一个参数用于指定布局的列数;第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列(传入StaggeredGridLayoutManager.HORIZONTAL表示让布局横向排列),最后再把创建好的实例设置到RecyclerView当中就可以了
点击事件
RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要自行给子项具体的View去注册点击事件,相比于ListView来说,实现起来要复杂一些。这么做的好处是,当点击的是子项里具体的某一个按钮,实现起来比较简单,而ListView实现这个就很困难了。
开发Tips
使用Nine-Patch图片指定图片可被拉伸的部分,防止图片因为拉伸而失真。