阅读郭林《第一行代码》的笔记——第3章 软件也要拼脸蛋,UI开发的点点滴滴

来源:互联网 发布:淘宝买电器售后怎么办 编辑:程序博客网 时间:2024/04/28 14:44

一、常见控件的使用方法

使用android:layout_width指定了控件的宽度,
使用android:layout_height指定了控件的高度
Android中所有的控件都具有这两个属性,可选值有三种match_parent、fill_parent和wrap_content,其中match_parent和fill_parent的意义相同,现在官方更加推荐使用match_parent。match_parent表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。

Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有三种,visible、invisible和gone。visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。gone则表示控件不仅不可见,而且不再占用任何屏幕空间。我们还可以通过代码来设置控件的可见性,使用的是setVisibility()方法,可以传入View.VISIBLE、View.INVISIBLE和View.GONE三种值。

android:background用于为布局或控件指定一个背景,可以使用颜色或图片来进行填充。

1、TextView

主要用于在界面上显示一段文本信息。
android:gravity=”center”//指定文字的对齐方式

使用android:gravity来指定文字的对齐方式,可选值有top、bottom、left、right、center等,可以用“|”来同时指定多个值,这里我们指定的”center”,效果等同于”center_vertical|center_horizontal”,表示文字在垂直和水平方向都居中对齐。

android:textSize=”24sp”//指定文字的大小

android:textColor=”#00ff00”//指定文字的颜色

当然TextView中还有很多其他的属性,这里我就不再一一介绍了,需要用到的时候去查阅文档就可以了。

2、Button

Button是程序用于和用户进行交互的一个重要控件。

然后我们可以在MainActivity中为Button的点击事件注册一个监听器,如下所示:

public class MainActivity extends Activity {    private Button button;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button = (Button) findViewById(R.id.button);        button.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                // 在此处添加逻辑            }        });    }}

这样每当点击按钮时,就会执行监听器中的onClick()方法,我们只需要在这个方法中加入待处理的逻辑就行了。如果你不喜欢使用匿名类的方式来注册监听器,也可以使用实现接口的方式来进行注册,代码如下所示:

public class MainActivity extends Activity implements OnClickListener {    private Button button;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button = (Button) findViewById(R.id.button);        button.setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch (v.getId()) {        case R.id.button:            // 在此处添加逻辑            break;        default:            break;        }    }}

这两种写法都可以实现对按钮点击事件的监听,至于使用哪一种就全凭你喜好了。

3、EditText

EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。EditText的应用场景应该算是非常普遍了,发短信、发微博、聊QQ等等。

可以看到,EditText中显示了一段提示性文本,然后当我们输入任何内容时,这段文本就会自动消失。不过随着输入的内容不断增多,EditText会被不断地拉长。这时由于EditText的高度指定的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非常难看。我们可以使用android:maxLines属性来解决这个问题。

android:maxLines=”2”//指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,而EditText则不会再继续拉伸

4、ImageView

ImageView是用于在界面上展示图片的一个控件,通过它可以让我们的程序界面变得更加丰富多彩。

android:src=”@drawable/ic_launcher”//给ImageView指定了一张图片

5、ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。

style=”?android:attr/progressBarStyleHorizontal”//执行ProgressBar的样式为水平进度条

android:max=”100”//设置水平进度条的最大值

ProgressBar还有几种其他的样式,你可以自己去尝试一下。

6、AlertDialog

AlertDialog可以在当前的界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此一般AlertDialog都是用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。

AlertDialog.Builder dialog = new AlertDialog.Builder (MainActivity.this);//通过AlertDialog.Builder创建出一个AlertDialog的实例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();//显示对话框

7、ProgressDialog

ProgressDialog和AlertDialog有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其他控件的交互能力。不同的是,ProgressDialog会在对话框中显示一个进度条,一般是用于表示当前操作比较耗时,让用户耐心地等待。

ProgressDialog progressDialog = new ProgressDialog (MainActivity.this);//新建一个ProgressDialog对象progressDialog.setTitle("This is ProgressDialog");//设置对话框的标题progressDialog.setMessage("Loading...");//设置对话框的内容progressDialog.setCancelable(true);//设置对话框是否可以取消   progressDialog.show();//显示对话框

二、详解四种基本布局

布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现,示意图3.15很好地展示了它们之间的关系。
布局与控件的关系

1、LinearLayout

LinearLayout又称作线性布局,是一种非常常用的布局。正如它名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列。

android:orientation属性:指定布局的排列方向,有两个值,分别是horizontal(水平)和vertical(垂直)

这里需要注意,如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,因为这样的话单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。

首先来看android:layout_gravity属性,它和我们上一节中学到的android:gravity属性看起来有些相似,这两个属性有什么区别呢?其实从名字上就可以看出,android:gravity是用于指定文字在控件中的对齐方式,而android:layout_gravity是用于指定控件在布局中的对齐方式。android:layout_gravity的可选值和android:gravity差不多,但是需要注意,当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。

接下来我们学习下LinearLayout中的另一个重要属性,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:text="Send"        />

你会发现,这里竟然将EditText和Button的宽度都指定成了0,这样文本编辑框和按钮还能显示出来吗?不用担心,由于我们使用了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定,这里指定成0是一种比较规范的写法。

2、RelativeLayout

RelativeLayout又称作相对布局,也是一种非常常用的布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意一些,它可以通过相对定位的方式让控件出现在布局的任何位置。也正因为如此,RelativeLayout中的属性非常多,不过这些属性都是有规律可循的,其实并不难理解和记忆。

android:layout_alignParentLeft——让一个控件 和父组件的左边对齐android:layout_alignParentTop——让一个控件和父组件的顶部对齐android:layout_alignParentRight——让一个控件 和父组件的右边对齐android:layout_alignParentBottom——让一个控件 和父组件的底部对齐android:layout_centerInParent——让一个控件 居中显示这几个属性都是相对于父布局进行定位的。
android:layout_above——让一个控件位于另一个控件的上方android:layout_below——让一个控件位于另一个控件的下方android:layout_toLeftOf——让一个控件位于另一个控件的左侧android:layout_toRightOf——让一个控件位于另一个控件的右侧这几个属性都是控件相对于空间进行定位的。
android:layout_alignLeft——让一个控件的左边缘和另一个控件的左边缘对齐android:layout_alignRight——让一个控件的右边缘和另一个控件的右边缘对齐android:layout_alignTop——让一个控件的上边缘和另一个控件的上边缘对齐android:layout_alignBottom——让一个控件的下边缘和另一个控件的下边缘对齐这几个属性都是相对于控件进行定位的。

3、FrameLayout

rameLayout相比于前面两种布局就简单太多了,因此它的应用场景也少了很多。这种布局没有任何的定位方式,所有的控件都会摆放在布局的左上角。

4、TableLayout

TableLayout允许我们使用表格的方式来排列控件,这种布局也不是很常用,你只需要了解一下它的基本用法就可以了。既然是表格,那就一定会有行和列,在设计表格时我们尽量应该让每一行都拥有相同的列数,这样的表格也是最简单的。不过有时候事情并非总会顺从我们的心意,当表格的某行一定要有不相等的列数时,就需要通过合并单元格的方式来应对。

在TableLayout中每加入一个TableRow就表示在表格中添加了一行,然后在TableRow中每加入一个控件,就表示在该行中加入了一列,TableRow中的控件是不能指定宽度的。不过使用android:stretchColumns属性就可以很好地解决这个问题,它允许将TableLayout中的某一列进行拉伸,以达到自动适应屏幕宽度的作用。

android:stretchColumns=”1”//这里将android:stretchColumns的值指定为1,表示如果表格不能完全占满屏幕宽度,就将第二列进行拉伸。没错!指定成1就是拉伸第二列,指定成0就是拉伸第一列,不要以为这里我写错了哦。

Android中其实还有一个AbsoluteLayout,不过这个布局官方已经不推荐使用了,因此我们直接将它忽略就好。

三、系统控件不够用?创建自定义控件

控件和布局的继承结构
控件和布局的继承结构

可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中一种最基本的UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多的子View和子ViewGroup,是一个用于放置控件和布局的容器。

引入布局
例子: 使用这种方式,不管有多少布局需要添加标题栏,只需一行include语句就可以了。

创建自定义控件
引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个活动中为这些控件单独编写一次事件注册的代码。比如说标题栏中的返回按钮,其实不管是在哪一个活动中,这个按钮的功能都是相同的,即销毁掉当前活动。而如果在每一个活动中都需要重新注册一遍返回按钮的点击事件,无疑又是增加了很多重复代码,这种情况最好是使用自定义控件的方式来解决。

例子:
新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件,代码如下所示:

public class TitleLayout extends LinearLayout {    public TitleLayout(Context context, AttributeSet attrs) {//重写了LinearLayout中的带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数        super(context, attrs);        LayoutInflater.from(context).inflate(R.layout.title, this);    }}

在构造函数中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。

现在自定义控件已经创建好了,然后我们需要在布局文件中添加这个自定义控件,修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <com.example.uicustomviews.TitleLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        ></com.example.uicustomviews.TitleLayout></LinearLayout>

添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候我们需要指明控件的完整类名,包名在这里是不可以省略的。

重新运行程序,你会发现此时效果和使用引入布局方式的效果是一样的。

然后我们来尝试为标题栏中的按钮注册点击事件,修改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()方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹出一段文本。这样的话,每当我们在一个布局中引入TitleLayout,返回按钮和编辑按钮的点击事件就已经自动实现好了,也是省去了很多编写重复代码的工作。

四、最常用和最难用的控件——ListView

ListView绝对可以称得上是Android中最常用的控件之一,几乎所有的应用程序都会用到它。由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕。相信你其实每天都在使用这个控件,比如查看手机联系人列表,翻阅微博的最新消息等等。

1、ListView的简单用法

public class MainActivity extends Activity {    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是用于展示大量数据的,那我们就应该先将数据提供好。这些数据可以是从网上下载的,也可以是从数据库中读取的,应该视具体的应用程序场景来决定。
不过,数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入即可。ArrayAdapter有多个构造函数的重载,你应该根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。注意我们使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。
最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。
可以通过滚动的方式来查看屏幕外的数据。

2、定制ListView的界面

——例子见原书3.5.2——

3、提升ListView的运行效率

之所以说ListView这个控件很难用,就是因为它有很多的细节可以优化,其中运行效率就是很重要的一点。目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法中每次都将布局重新加载了一遍,当ListView快速滚动的时候这就会成为性能的瓶颈。
仔细观察,getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。

View view;        if (convertView == null) {            view = LayoutInflater.from(getContext()).inflate(resourceId, null);        } else {            view = convertView;        }

可以看到,现在我们在getView()方法中进行了判断,如果convertView为空,则使用LayoutInflater去加载布局,如果不为空则直接对convertView进行重用。这样就大大提高了ListView的运行效率,在快速滚动的时候也可以表现出更好的性能。
不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。我们可以借助一个ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如下所示:

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); // 将ViewHolder存储在View中        } else {            view = convertView;            viewHolder = (ViewHolder) view.getTag(); // 重新获取ViewHolder        }        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不为空的时候则调用View的getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder里,就没有必要每次都通过 findViewById()方法来获取控件实例了。
通过这两步的优化之后,我们ListView的运行效率就已经非常不错了。

4、ListView的点击事件

public class MainActivity extends Activity {    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);        listView.setOnItemClickListener(new 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();            }        });    }     ……}

可以看到,我们使用了setOnItemClickListener()方法来为ListView注册了一个监听器,当用户点击了ListView中的任何一个子项时就会回调onItemClick()方法,在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的名字显示出来。

五、单位和尺寸

在布局文件中指定宽高的固定大小有以下常用单位可供选择:px、pt、dp和sp。

px和pt的窘境
px是像素的意思,即屏幕中可以显示的最小元素单元,我们应用里任何可见的东西都是由一个个像素点组成的。单独一个像素点非常的微小,肉眼是无法看见的,可是当许许多多的像素点聚集到一起时,就可以拼接成五彩缤纷的图案。

pt是磅数的意思,1磅等于1/72英寸,一般pt都会作为字体的单位来使用。

过去在PC上使用px和pt的时候可以说是非常得心应手,能把程序打扮得漂漂亮亮。可是现在到了手机上,这两个单位就显得有些力不从心了,因为手机的分辨率各不相同,一个200px宽的按钮在低分辨率的手机上可能将近占据满屏,而到了高分辨率的手机上可能只占据屏幕的一半。

dp和sp来帮忙
谷歌当然也意识到了这个令人头疼了问题,于是为Android引入了一套新的单位dp和sp。

dp是密度无关像素的意思,也被称作dip,和px相比,它在不同密度的屏幕中的显示比例将保持一致。

sp是可伸缩像素的意思,它采用了和dp同样的设计理念,解决了文字大小的适配问题。

什么叫密度?Android中的密度就是屏幕每英寸所包含的像素数,通常以dpi为单位。比如一个手机屏幕的宽是2英寸长是3英寸,如果它的分辨率是320*480像素,那这个屏幕的密度就是160dpi,如果它的分辨率是640*960,那这个屏幕的密度就是320dpi,因此密度值越高的屏幕显示的效果就越精细。我们可以通过代码来得知当前屏幕的密度值是多少,修改MainActivity中的代码,如下所示:

public class MainActivity extends Activity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        float xdpi = getResources().getDisplayMetrics().xdpi;        float ydpi = getResources().getDisplayMetrics().ydpi;//动态获取到了当前屏幕的密度值        Log.d("MainActivity", "xdpi is " + xdpi);        Log.d("MainActivity", "ydpi is " + ydpi);    }}

根据Android的规定,在160dpi的屏幕上,1dp等于1px,而在320dpi的屏幕上,1dp就等于2px。因此,使用dp来指定控件的宽和高,就可以保证控件在不同密度的屏幕中的显示比例保持一致。

sp的原理和dp是一样的,它主要是用于指定文字的大小,这里就不再进行介绍了。
总结一下,在编写Android程序的时候,尽量将控件或布局的大小指定成match_parent或wrap_content,如果必须要指定一个固定值,则使用dp来作为单位,指定文字大小的时候使用sp作为单位。

六、制作Nine-Patch图片

.9图片是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸而哪些区域不可以。

在Android sdk目录下有一个tools文件夹,在这个文件夹中找到draw9patch.bat文件,我们就是使用它来制作Nine-Patch图片的。

我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分就表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分则表示内容会被放置的区域。

左侧和顶部是控制拉伸区域,右侧和底部是可能控制显示的内容区域。

0 0
原创粉丝点击