zl程序教程

您现在的位置是:首页 >  APP

当前栏目

第003天:APP的UI设计

2023-04-18 14:26:38 时间

        我一直都认为程序员在软件的审美方面普遍都比较差,至少我个人就是如此。如果说要追究 其根本原因,我觉得这是由程序员的工作性质所导致的。每当我们看到一个软件时,不会像普通 用户那样仅仅是关注一下它的界面和功能,而是会不自觉地思考这些功能是如何实现的。很多在 普通用户看来理所应当的功能,背后可能却需要非常复杂的逻辑来完成,以至于当别人唾骂一句 “这软件做得真丑”的时候,我们还可能赞叹一句“这功能做得好牛啊”!

        不过缺乏审美观毕竟不是一件值得炫耀的事情,在软件开发过程中,界面设计和功能开发同 样重要。界面美观的应用程序不仅可以大大增加用户粘性,还能帮我们吸引到更多的新用户。而 Android也给我们提供了大量的UI开发工具,只要合理地使用它们,就可以编写出各种各样漂亮 的界面。

        在这里,我无法教会你如何提升自己的审美观,但我可以教会你怎样使用Android提供的 UI开发工具来编写程序界面。你在上一章中反反复复地使用那几个按钮,想必都快要吐了吧, 本章我们就来学习更多的UI开发方面的知识。

3.1如何编写程序界面

        Android中有多种编写程序界面的方式可供选择。Android StudioEclipse中都提供了相应的可视化编辑器,允许使用拖放控件的方式来编写布局,并能在视图上直接修改控件的属性。不过我并不推荐你使用这种方式来编写界面,因为可视化编辑工具并不利于你去真正了解界面背后的实现原理。通过这种方式制作出的界面通常不具有很好的屏幕适配性,而且当需要编写较为复杂的界面时,可视化编辑工具将很难胜任。因此本书中所有的界面都将通过最基本的方式去实现, 即编写XML代码。等你完全掌握了使用XML来编写界面的方法之后,不管是进行高复杂度的界面实现,还是分析和修改当前现有界面,对你来说都将是手到擒来。

        讲了这么多理论的东西,也是时候学习一下到底如何编写程序界面了,下面我们就从Android 中几种常见的控件开始吧。

 

3.2常用控件的使用方法

        Android提供了大量的UI控件,合理地使用这些控件就可以非常轻松地编写出相当不错的界面,下面我们就挑选几种常用的控件,详细介绍一下它们的使用方法。

        首先新建一个UIWidgetTest项目,简单起见,我们还是允许Android Studio自动创建活动, 活动名和布局名都使用默认值。

  1. TextView

        TextView可以说是Android中最简单的一个控件了,你在前面其实已经和它打过一些交道了。 它主要用于在界面上显示一段文本信息,比如你在第1章看到的“Helloworld!”下面我们就来看一看关于TextView的更多用法。

修改activity main.xml中的代码,如下所示:​​​​​​​

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"         
    android:orientation="vertical" 
    android: layout_width="match_parent,' 
    android: layout_height=,'mat chpa rent ">

    <TextView
        android:id="@+id/text_view”
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
        android:text=uThis is TextView" />

</LinearLayout>

        外面的LinearLayout先忽略不看,在TextView中我们使用android: id给当前控件定义了一 个唯一标识符,这个属性在上一章中已经讲解过了。然后使用android:layout widthandroid: layout height指定了控件的宽度和高度。Android中所有的控件都具有这两个属性, 可选值有 3 种:match parentA fill parent wrap contento 其中 match parent fill parent的意义相同,现在官方更加推荐使用match parento match parent表示让当前 控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表示 让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。所以上 面的代码就表示让TextView的宽度和父布局一样宽,也就是手机屏幕的宽度,让TextView的高 度足够包含住里面的内容就行。当然除了使用上述值,你也可以对控件的宽和高指定一个固定的 大小,但是这样做有时会在不同手机屏幕的适配方面岀现问题。

接下来我们通过android:text指定TextView中显示的文本内容,现在运行程序,效果如 图3.1所示。

         虽然指定的文本内容正常显示了,不过我们好像没看出来TextView的宽度是和屏幕一样宽 的。其实这是由于TextView中的文字默认是居左上角对齐的,虽然TextView的宽度充满了整个 屏幕,可是由于文字内容不够长,所以从效果上完全看不出来。现在我们修改TextView的文字 对齐方式,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android: layout_height='1 mat chpa rent ">

    <TextView
        android: id="(a+id/text_view"
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android :gravity="center>,
        android:text=HThis is TextView" />

</LinearLayout>

        我们使用android:gravity来指定文字的对齐方式,可选值有topbottomleftrightcenter等,可以用来同时指定多个值,这里我们指定的center,效果等同于center_ vertical | center horizontal,表示文字在垂直和水平方向都居中对齐。现在重新运行程序, 效果如图3.2所示。

这也说明了 TextView的宽度确实是和屏幕宽度一样的。

另外我们还可以对TextView中文字的大小和颜色进行修改,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android: 'layout_wiclth="mat chpa rent"
    android:layout_height="match_parent">

    <TextView
        android: id=,*@+id/text_view"
        android: layout_width="niatch_parent"
        android:layout_height="wrap_content"
        android :gravity="center,'
        android:textSize="24sp"
        android:textColors'^OOffOO"
        android:text="This is TextView" />

</LinearLayout>

通过and roid: textsize属性可以指定文字的大小,通过and roid: textColo r属性可以指定文字的颜色,在Android中字体大小使用sp作为单位。重新运行程序,效果如图3.3所示。

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

  1. Button

Button是程序用于和用户进行交互的一个重要控件,相信你对这个控件已经非常熟悉了,因为我们在上一章用了太多次Buttono它可配置的属性和TextView是差不多的,我们可以在 activity main.xml 中这样加入 Button:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android: orientation=,,vertical" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent">
    ....

    <Button
    android:id="@+id/button"
    android :layout__width=,,match_parent" 
    android:Layout_height="wrap_content" 
    android:text="Button" />

</LinearLayout>

加入Button之后的界面如图3.4所示。

        细心的你可能会留意到,我们在布局文件里面设置的文字是“Button”但最终的显示结果 却是“BUTTON”这是由于系统会对Button中的所有英文字母自动进行大写转换,如果这不是 你想要的效果,可以使用如下配置来禁用这一默认特性:

<Button
    android:id="@+id/button"
    android: layout_width=,,match_parent"
    android: layout_height="wrap_content '* 
    android:text="Button"
    android:textAllCaps="false" />

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

public class MainActivity extends AppCompatActivity {

    @Override 
    protected void onCreate(Bundle savedlnstanceState) {         
        super.onCreate(savedlnstanceState); 
        setContentView(R.layout.activitymain);
        Button button = (Button) findViewById(R.id.button); 
        button.setOnClickListener(new View.OnClickListener() { 
            ©Override 
                public void onClick(View v) {
                //在此处添加逻辑
                }
        });
    }
}

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedlnstanceState) {
        super.onCreate(savedlnstanceState); 
        setContentView(R.layout.activitymain); 
        Button button = (Button) findViewByld(R.id.button);             
        button.setOnClickListener(this);
}

    @Override
    public void onClick(View v) {
        switch (v.getld()) {
            case R.id.button:
                //在此处添加逻辑
                break;
            default: 
                break;
            }
        }
}

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

  1. EditText

        EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内 容,并可以在程序中对这些内容进行处理。EditText的应用场景非常普遍,在进行发短信、发微 博、聊QQ等操作时,你不得不使用EditTexto那我们来看一看如何在界面上加入EditText吧, 修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    ....

    <EditText
        android:id=”@+id/edit_text" 
        android :layout_width=,,match_parent" 
        android: TLayout_height=,,wrap_content" /> 

</LinearLayout>

        其实看到这里,我估计你已经总结出Android控件的使用规律了,用法基本上都很相似:给控件定义一个id,再指定控件的宽度和高度,然后再适当加入一些控件特有的属性就差不多了。

所以使用XML来编写界面其实一点都不难,完全可以不用借助任何可视化工具来实现。现在重新运行一下程序,EditText就已经在界面上显示出来了,并且我们是可以在里面输入内容的, 如图3.5所示。

​​​​​​​

        细心的你平时应该会留意到,一些做得比较人性化的软件会在输入框里显示一些提示性的文 字,然后一旦用户输入了任何内容,这些提示性的文字就会消失。这种提示功能在Android里是 非常容易实现的,我们甚至不需要做任何的逻辑控制,因为系统已经帮我们都处理好了。修改 activity main.xml, 如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android: layout_width=,'mat chpa rent"
    android:layout_height="match_parent">
    ...

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" android:hint="Type something here"
        />

</LinearLayout>

这里使用android:hint属性指定了一段提示性的文本,然后重新运行程序,效果如图3.6 所示。

 

        可以看到,EditText中显示了一段提示性文本,然后当我们输入任何内容时,这段文本就会自动消失。

        不过,随着输入的内容不断增多,EditText会被不断地拉长。这时由于EditText的高度指定的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非常难看。我们可以使用android:maxLines属性来解决这个问题,修改activity_main.xml,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    ...
    <EditText
        android:id=”@+id/edit_text"
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:hint="Type something here" 
        android :maxLines="2"
        />

</LinearLayout>

这里通过android :maxLines指定了 EditText的最大行数为两行,这样当输入的内容超过两 行时,文本就会向上滚动,而EditText则不会再继续拉伸,如图3.7所示。

我们还可以结合使用EditTextButton来完成一些功能,比如通过点击按钮来获取EditText 中输入的内容。修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnCtickListener {
    private EditText editText;
    @Override
    protected void onCreate(Bundle savedlnstanceState) {             
        super.onCreate(savedlnstanceState); 
        setContentView(R.layout.activitymain); 
        Button button = (Button) findViewByld(R.id.button); 
        editText = (EditText) findViewById(R.id.edit^text);         
        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getld()) {
            case R.id.button:
                String inputText = editText.getText().toStringO; 
                Toast.makeText(MainActivity.this, inputText;
                Toast.LENGTH_SHORT). show();
                break;
            default:
                break;
        }
    }
}

        首先通过findViewById()方法得到EditText的实例,然后在按钮的点击事件里调用EditText getText()方法获取到输入的内容,再调用toStringO方法转换成字符串,最后还是老方法, 使用Toast将输入的内容显示出来。

        重新运行程序,在EditText中输入一段内容,然后点击按钮,效果如图3.8所示。

  1. ImageView

        ImageView是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多 彩。学习这个控件需要提前准备好一些图片,图片通常都是放在以“drawable”开头的目录下的。 目前我们的项目中有一个空的drawable目录,不过由于这个目录没有指定具体的分辨率,所以一 般不使用它来放置图片。这里我们在res目录下新建一个drawable-xhdpi目录,然后将事先准备 好的两张图片img l.pngimg_2.png复制到该目录当中。

接下来修改activity main.xml,如下所示:

vLinea「Layout xmlns: android="http://schemas.android. com/apk/res/android"
    android: orientation="vertical,'
    android:layout_width="match_parent" 
    android:layout_height="match_parent">
    ...
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content" 
        android :layout_height=,,wrap_content" 
        android:s rc="@drawable/img_l "
        />
</LinearLayout>

可以看到,这里使用android:src属性给ImageView指定了一张图片。由于图片的宽和高 都是未知的,所以将ImageView的宽和高都设定为wap_content,这样就保证了不管图片的尺 寸是多少,图片都可以完整地展示出来。重新运行程序,效果如图3.9所示。

我们还可以在程序中通过代码动态地更改ImageView中的图片,然后修改MainActivity的代 码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText editText;
    private ImageView imageview;
    @Override
    protected void onCreate(Bundle savedlnstanceState) {         
        super.onCreate(savedlnstanceState); 
        setContentView(R.layout.activitymain); 
        Button button = (Button) findViewById(R.id.button); 
        editText = (EditText) findViewByld(R.id.edittext); 
        imageview = (ImageView) findViewById(R.id.image_view);             
        button.setOnClickListener(this);
)
    @Override
    public void onClick(View v) { 
        switch (v.getld()) { 
            case R.id.button:
                imageview. setlmageResource(R.drawable.img__2);
                break;
            default: 
                break;
                }
        }
}

在按钮的点击事件里,通过调用Image ViewsetImageResource()方法将显示的图片改成 img_2,现在重新运行程序,然后点击一下按钮,就可以看到ImageView中显示的图片改变了, 如图3.10所示。

  1. ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。它的用法也 非常简单,修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ProgressBar

        android:id="@+id/prog ress_ba r" 
        android: layout_width="match_pai"ent" 
        android:layout_height="wrap_content" /> " 
</LinearLayout>

重新运行程序,会看到屏幕中有一个圆形进度条正在旋转,如图3.11所示。

        这时你可能会问,旋转的进度条表明我们的程序正在加载数据,那数据总会有加载完的时候 吧?如何才能让进度条在数据加载完成时消失呢?这里我们就需要用到一个新的知识点: Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android: visibility 进行指定,可选值有3种:visible, invisiblegonevisible表示控件是可见的,这个 值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件不可见, 但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。gone则表示控件不仅 不可见,而且不再占用任何屏幕空间。我们还可以通过代码来设置控件的可见性,使用的是 setvisibility()方法,可以传入 View. VISIBLE. View. INVISIBLE View.GONE 3 种值。

接下来我们就来尝试实现,点击一下按钮让进度条消失,再点击一下按钮让进度条出现的这 种效果。修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText editText;

    private Imageview imageview;

    private ProgressBar progressBar;

    @Override

    protected void onCreate(Bundle savedlnstanceState) {             
        super.onCreate(savedlnstanceState); 
        setContentView(R.layout.activitymain);
        Button button = (Button) findViewByld(R.id.button); 
        editText = (EditText) findViewByld(R.id.edittext); 
        imageView = (ImageView) findViewByld(R.id.imageview); 
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);     
        button.setOnClickListener(this);
}
    @Override
    public void onClick(View v) { 
        switch (v.getld()) { 
            case R.id.button:
                if (progressBar.getVisibility() == View.GONE) {                 
                    progressBar.setvisibility(View.VISIBLE);
                } else { 
                    progressBar.setvisibility(View.GONE);
                }
                break;
            default: 
                break;
}
}
}

在按钮的点击事件中,我们通过getVisibility ()方法来判断ProgressBar是否可见,如果 可见就将ProgressBar隐藏掉,如果不可见就将ProgressBar显示出来。重新运行程序,然后不断 地点击按钮,你就会看到进度条会在显示与隐藏之间来回切换。

另外,我们还可以给ProgressBar指定不同的样式,刚刚是圆形进度条,通过style属性可 以将它指定成水平进度条,修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android :orientation="vertical,' 
    android:layout_width="match_parent" 
    android:layout_height="match_parent">

    <ProgressBar

        android: id=,,(a+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" style="?
        android:atti7progressBarStyleHo「izontaU' 
        android :max=>l 166"
        />
</LinearLayout>

指定成水平进度条后,我们还可以通过android:max属性给进度条设置一个最大值,然后 在代码中动态地更改进度条的进度。修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

   public void onClick(View v) (

        switch (v.getld()) {
            case R.id.button:
                int progress = progressBar.getProgress(); 
                progress = progress + 16;
                progressBar.setProgress(progress); 
                break;
            default:
                break;
        }
    }
}

每点击一次按钮,我们就获取进度条的当前进度,然后在现有的进度上加10作为更新后的 进度。重新运行程序,点击数次按钮后,效果如图3.12所示。

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

  1. AlertDialog

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public void onClick(View v) {
        switch (v.getld()) {
            case R.id.button:
                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. OnClickListenerO {     
                    @Override
                    public void onCLickCDialoglnterface dialog, int which) { }
});            
                dialog.show();
                    break; 
                default: 
                    break;
        }
    }
}

首先通过AlertDialog.Builder创建一个AlertDialog的实例,然后可以为这个对话框设置标题、 内容、可否取消等属性,接下来调用setPositiveButton()方法为对话框设置确定按钮的点击 事件,调用setNegativeButton ()方法设置取消按钮的点击事件,最后调用show()方法将对话 框显示出来。重新运行程序,点击按钮后,效果如图3.13所示。

 

  1. ProgressDialog

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener (

(aOverride

public void onClick(View v) {

switch (v.getld()) { case R.id.button:

ProgressDialog progressDialog = new ProgressDialog (MainActivity.this);

progressDialog.setTitle("This is ProgressDialog"); progressDialog.setMessage("Loading. ..**); progressDialog.setCancelable(true);

progressDialog.show();

break; default: break;

}

}

}

可以看到,这里也是先构建出一个ProgressDialog对象,然后同样可以设置标题、内容、 可否取消等属性,最后也是通过调用show()方法将ProgressDialog显示出来。重新运行程序,点 击按钮后,效果如图3.14所示。

 

 

注意,如果在setCancelable()中传入了 false,表示ProgressDialog是不能通过Back键 取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialogdismissO方法来关闭对话框,否则ProgressDialog将会一直存在。

好了,关于Android常用控件的使用,我要讲的就只有这么多。一节内容就想覆盖Android 控件所有的相关知识不太现实,同样一口气就想学会所有Android控件的使用方法也不太现实。 本节所讲的内容对于你来说只是起到了一个引导的作用,你还需要在以后的学习和工作中不断 地摸索,通过查阅文档以及网上搜索的方式学习更多控件的更多用法。当然,当本书后面涉及一 些我们前面没学过的控件和相关用法时,我仍然会在相应的章节做详细的讲解。

3.3详解4种基本布局

一个丰富的界面总是要由很多个控件组成的,那我们如何才能让各个控件都有条不紊地摆放 在界面上,而不是乱糟糟的呢?这就需要借助布局来实现了。布局是一种可用于放置很多控件的 容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部 除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面 实现,图3.15很好地展示了它们之间的关系。

 

3.15布局和控件的关系

下面我们来详细讲解下Android4种最基本的布局。先做好准备工作,新建一个 UILayoutTest项目,并让AndroidStudi。自动帮我们创建好活动,活动名和布局名都使用默认值。

3.3.1线性布局

LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个 布局会将它所包含的控件在线性方向上依次排列。相信你之前也已经注意到了,我们在上一节中 学习控件用法时,所有的控件就都是放在LinearLayout布局里的,因此上一节中的控件也确实是 在垂直方向上线性排列的。

既然是线性排列,肯定就不仅只有一个方向,那为什么上一节中的控件都是在垂直方向排列 的呢?这是由于我们通过android:orientation属性指定了排列方向是vertical,如果指定的是 horizontal,控件就会在水平方向上排列了。下面我们通过实战来体会一下,修改activity main.xml 中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android: orientation=,,vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android :id=,,@+id/buttonl,'
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content" android :layout_height=,,wrap_content" android:text="Button 2" />
<Button
android :id=,,(a+id/button3,1
android:layout_width="wrap_content" android:layout_height="wrap_content" and roid:text="Button 3" />
</LinearLayout>

我们在LinearLayout中添加了 3Button,每个Button的长和宽都是wrap content,并指 定了排列方向是verticaL现在运行一下程序,效果如图3.16所示。

 

然后我们修改一下LinearLayout的排列方向,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal"
android:layout_width="match_pa rent"
and roid: layout_height="match_pa rent **>
</LinearLayout>

android:orientation属性的值改成了 horizontal,这就意味着要让LinearLayout中的控 件在水平方向上依次排列。当然如果不指定android:orientation属性的值,默认的排列方向 就是horizontal重新运行一下程序,效果如图3.17所示。

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

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android :layout_height=,,match_parent">
<Button
android: id=,,(a+id/buttonl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
and roid:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
and roid:text="Button 2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button 3" />
</LinearLayout>

由于目前LinearLayout的排列方向是horizontal,因此我们只能指定垂直方向上的排列方向, 将第一个Button的对齐方式指定为top,第二个Button的对齐方式指定为center vertical,第三个 Button的对齐方式指定为bottom重新运行程序,效果如图3.18所示。

接下来我们学习下LinearLayout中的另一个重要属性 android:layout_weight0这个

属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要 的作用。比如我们正在编写一个消息发送界面,需要一个文本编辑框和一个发送按钮,修改 activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android :layout_width=,,match_pa rent" android:layout_height="match_parent">
<EditText
android:id="@+id/input_message" android:layout_width="Odp" android :layout_height=,,wrap__content,, android:layout_weight="l" android:hint="Type something"
/>
<Button
android:id="@+id/send"
android:layout_width="Odp"
android:layout_height="wrap_content"
android:layout_weight="1" android :text=,,Send"
/>
</LinearLayout>

你会发现,这里竟然将EditTextButton的宽度都指定成了 Odp,这样文本编辑框和按钮还 能显示出来吗?不用担心,由于我们使用了 android: layout weight属性,此时控件的宽度就 不应该再由android :layout_width来决定,这里指定成Odp是一种比较规范的写法。另外, dpAndroid中用于指定控件大小、间距等属性的单位,后面我们还会经常用到它。

然后在EditTextButton里都将android: layout weight属性的值指定为1,这表示 EditTextButton将在水平方向平分宽度。

为什么将android :layout_weight属性的值同时指定为1就会平分屏幕宽度呢?其实原理 也很简单,系统会先把LinearLayout下所有控件指定的layout weight值相加,得到一个总值, 然后每个控件所占大小的比例就是用该控件的layout weight值除以刚才算岀的总值。因此如 果想让EditText占据屏幕宽度的3/5, Button占据屏幕宽度的2/5,只需要将EditTextlayout, weight 改成 3, Button layout weight 改成 2 就可以了。

重新运行程序,你会看到如图3.19所示的效果。

 

 

我们还可以通过指定部分控件的layout weight值来实现更好的效果。修改activity main. xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="matchparent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android: 'layout_weight=,,l"
android:hint="Type something"
/>
<Button
android: id="(a+icl/send"
android:layout_width="wrap_content"
and roid: ■Layout_height=,,wrap_content"
android:text="Send"
/>
</LinearLayout>

这里我们仅指定了 EditTextandroid:layout weight属性,并将Button的宽度改回 wrap_content0这表示Button的宽度仍然按照wrap content来计算,而EditText则会占满屏 幕所有的剩余空间。使用这种方式编写的界面,不仅在各种屏幕的适配方面会非常好,而且看起 来也更加舒服。重新运行程序,效果如图3.20所示。

 

3.3.2相对布局

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

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<Button
android:id="@+id/buttonl"
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="(a+id/button2"
android:layout_width="wrap_content"
and roid: layoutheight^wrapcontent"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2" />
<Button
android:id="@+id/button3"
android: layout_width=l,wrap_content"
android:layout_height="wrap_content"
android:layoutcenterlnParent="true" android:text="Button 3" />
<Button
android: id="(a+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" and roid:text="Button 4" />
<Button
android: id="(a+id/button5"
and roid: layout_width=,,wrap_content"
android:layout_height="wrap_content" android :layout_aHgnParentBottom="t rue" android:layout_alignParentRight="true" and roid:text="Button 5" />
</RelativeLayout>

我想以上代码已经不需要我再做过多解释了,因为实在是太好理解了。我们让Button 1和父 布局的左上角对齐,Button 2和父布局的右上角对齐,Button3居中显示,Button4和父布局的左 下角对齐,Button 5和父布局的右下角对齐。虽然android:layout_alignParentLeft. android:layoutalignParentTop.android:layoutalignParentRight^android:layout_ alignParentBottomandroid :layout_centerlnParent 这几个属性我们之前都没接触过, 可是它们的名字已经完全说明了它们的作用。重新运行程序,效果如图3.21所示。

 

上面例子中的每个控件都是相对于父布局进行定位的,那控件可不可以相对于控件进行定位

呢?当然是可以的,修改activity main.xml中的代码,如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<Button
android: id="(a+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
and roid:text="Button 3" />
<Button
android: id="@+id/button]."
android:layout_width="wrap_content"
android: layout_height=,,wrap_content"
android:layout_above=”@id/button3”
android:Layout_toLeft0f="@id/button3"
android:text="Button 1" />
<Button
android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android :Layout_above="@id/button3,' android :layout_toRight0f="(aid/button3" android:text="Button 2" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android :layout_toLeft0f=,,@id/button3H
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRight0f="@id/button3"
android:text="Button 5" />
</RelativeLayout>

这次的代码稍微复杂一点,不过仍然是有规律可循的。android: layout above属性可以让 一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用,这里我们填入了 @id/batton3,表示让该控件位于Button 3的上方。其他的,属性也都是相似的,android: layout below表示让一个控件位于另一个控件的下方,android: layout toLeftOf表示让一 个控件位于另一个控件的左侧,and roid: layout toRightOf表示让一个控件位于另一个控件 的右侧。注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面, 不然会出现找不到id的情况。重新运行程序,效果如图3.22所示。

 

RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout alignLeft 表示让一个控件的左边缘和另一个控件的左边缘对齐,android: layout alignRight表示让一 个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout alignTopandroid:layout alignBottom,道理都是一样的,我就不再多说,这几个属性就留给你自己去 尝试吧。

好了,正如我前面所说,RelativeLayout中的属性虽然多,但都是有规律可循的,所以学起 来一点都不觉得吃力吧?

3.3.3帧布局

FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景也少 了很多。这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角。让我们通过 例子来看一看吧,修改activity main.xml中的代码,如下所示:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="matchparent"
android:layout_height="match_parent">
<TextView
android: id="(a+id/text_view"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="This is Textview"
/>
<ImageView
and roid: id="@+id/iinage_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:src="@mipmap/ic_launcher"
/> -
</FrameLayout>

FrameLayout中只是放置了一个TextView和一个Image View o需要注意的是当前项目我们 没有准备任何图片,所以这里ImageView直接使用了@mipmap来访问ic launcher这张图,虽说 这种用法的场景可能非常少,但我还是要告诉你,这是完全可行的。重新运行程序,效果如图 3.23所示。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<TextView
and roid: id=,,@+id/text_view"
android:layout_width="wrap_content" android: ■Layout_height="wrap_content" and roid: layout__g ravity="left" and roid:text="fhis is Textview"
/>
<ImageView
android:id="@+id/button"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android: src="(amipmap/ic_launcher" /> ~
</FrameLayout>

可以看到,文字和图片都是位于布局的左上角。由于ImageView是在TextView之后添加的, 因此图片压在了文字的上面。

当然除了这种默认效果之外,我们还可以使用layout_gcavity属性来指定控件在布局中的 对齐方式,这和LinearLayout中的用法是相似的。修改activity_main.xml中的代码,如下所示:

​​​​​​​

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

<TextView

and roid: id=,,@+id/text_view"
android:layout_width="wrap_content" android: ■Layout_height="wrap_content" and roid: layout__g ravity="left" and roid:text="fhis is Textview"

/>

<ImageView

android:id="@+id/button"

android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android: src="(amipmap/ic_launcher" /> ~

</FrameLayout>

我们指定TextViewFrameLayout中居左对齐,指定ImageViewFrameLayout中居右对齐, 然后重新运行程序,效果如图3.24所示。

UlLayoutTest

this

 

 

3.24 指定layout gravity的效果

总体来讲,FrameLayout由于定位方式的欠缺,导致它的应用场景也比较少,不过在下一章 中介绍碎片的时候我们还是可以用到它的。

3.3.4百分比布局

前面介绍的3种布局都是从Android 1.0版本中就开始支持了,一直沿用到现在,可以说是 满足了绝大多数场景的界面设计需求。不过细心的你会发现,只有LinearLayout支持使用 layout_weight属性来实现按比例指定控件大小的功能,其他两种布局都不支持。比如说,如 果想用RelativeLayout来实现让两个按钮平分布局宽度的效果,则是比较困难的。

为此,Android引入了一种全新的布局方式来解决此问题一一百分比布局。在这种布局中, 我们可以不再使用wrap content, match parent等方式来指定控件的大小,而是允许直接指 定控件在布局中所占的百分比,这样的话就可以轻松实现平分布局甚至是任意比例分割布局的 效果了。

由于LinearLayout本身已经支持按比例指定控件的大小了,因此百分比布局只为Frame- Layout RelativeLayout 进行了功能扩展,提供了 PercentFrameLayout PercentRelativeLayout 这两个全新的布局,下面我们就来具体学习一下。

不同于前3种布局,百分比布局属于新增布局,那么怎么才能做到让新增布局在所有Android 版本上都能使用呢?为此,Android团队将百分比布局定义在了 support库当中,我们只需要在项 目的build.gradle中添加百分比布局库的依赖,就能保证百分比布局在Android所有系统版本上的 兼容性了。

打开app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {

compile fileTree(dir: 'libs', include: [jar'])

compile 'com.android.support:appcompat-v7:24.2.1' compile 'com.androidsupport:percent:24.2.1* testCompile *junit:junit:4.12'

}

需要注意的是,每当修改了任何gradle文件时,Android Studio都会弹出一个如图3.25所 示的提示。

| Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work Sync |

3.25 gradle文件修改后的提示

这个提示告诉我们,gradle文件自上次同步之后又发生了变化,需要再次同步才能使项目正 常工作。这里只需要点击Sync Now就可以了,然后gradle会开始进行同步,把我们新添加的百 分比布局库引入到项目当中。

接下来修改activity main.xml中的代码,如下所示:

<android.support.percent.PercentFrameLayout

xmlns:android="http://schemas.android.com/apk/res/android"

xmlns :app="http://schenias .android. com/apk/res-auto"

android:layout_width="match_parent" android:layout_height="match_parent">

<Button

android:id="@+id/buttonl"

android:text="Button 1"

android:layoutgravity="left]top"

app:layout_widthPercent="50%"

app:layout_heightPercent="50%"

 

/>

<Button

android:id="@+id/button2"

android:text="Button 2"

and roid:layoutg ravity="right|top"

app: layout_widthPercent=,,50%"

app:layout_heightPercent="50%"

/>

<Button

android: id="(a+id/button3"

android:text=HButton 3"

android:layoutg ravity="left|bottom"

app:layout_wid£hPecent="50%”

app:layout_heightPercent="50%"

/> -

<Button

android:id="@+id/button4"

android:text="Button 4"

android: layout_gravity=,' right | bottom"

app:layout_widthPercent="50%"

app:layout_heightPercent="50%"

/>

</android.support.percent.PercentFrameLayout>

最外层我们使用了 PercentFrameLayout,由于百分比布局并不是内置在系统SDK当中的,所 以需要把完整的包路径写出来。然后还必须定义一个app的命名空间,这样才能使用百分比布局 的自定义属性。

PercentFrameLayout中我们定义了 4个按钮,使用app: layout widthPercent属性将各 按钮的宽度指定为布局的50%,使用app:layout heightPercent属性将各按钮的高度指定为 布局的50%。这里之所以能使用app前缀的属性就是因为刚才定义了 app的命名空间,当然我们 一直能使用android前缀的属性也是同样的道理。

不过PercentFrameLayout还是会继承FrameLayout的特性,即所有的控件默认都是摆放在布 局的左上角。那么为了让这4个按钮不会重叠,这里还是借助了 layout gravity来分别将这4 个按钮放置在布局的左上、右上、左下、右下4个位置。

现在我们已经可以重新运行程序了,不过如果你使用的是老版本的Android Studio,可能会 在activity main.xml中看到一些如图3.26所示的错误提示。

1ayout_heightr attribute should be defined more.„ (Ctrl 4-Fl) j

'layoutjwwdth1 attribute should be defined more*** (Ctrl+Fl) j

3.26 activity main.xml 中错误提示

这是因为老版本的Android Studio中内置了布局的检查机制,认为每一个控件都应该通过 android:layout widthandroid height属性指定宽高才是合法的。而其实我们是通过 app:layout widthPercent app:layout_heightPercent 属性来指定宽高的,所以 Android Studio没检测到。不过这个错误提示并不影响程序运行,我们直接忽视就可以了。当然最新的 Android Studio 2.2版本中已经修复了这个问题,因此你可能并不会看到上述的错误提示。

现在重新运行程序,效果如图3.27所示。

3.27 PercentFrameLayout 运行效果

可以看到,每一个按钮的宽和高都占据了布局的50%,这样我们就轻松实现了 4个按钮平分 屏幕的效果。

PercentFrameLayout的用法就介绍到这里,另外一个PercentRelativeLayout的用法也是非 常相似的,它继承了 RelativeLayout中的所有属性,并且可以使用app: layout widthPercent app:layout_heightPercent来按百分比指定控件的宽高,相信聪明的你一定可以举一反 三了。

这样我们就把最常用的几种布局都讲解完了,其实Android中还有AbsoluteLayoutA TableLayout等布局,不过由于使用得实在是太少了,就不在本书中进行讲解了。

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

在前面两节我们已经学习了 Android中的一些常用控件以及基本布局的用法,不过当时我们 并没有关注这些控件和布局的继承结构,现在是时候来看一下了,如图3.28所示。

3.28常用控件和布局的继承结构

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

这个时候我们就可以思考一下,当系统自带的控件并不能满足我们的需求时,可不可以利用 上面的继承结构来创建自定义控件呢?答案是肯定的,下面我们就来学习一下创建自定义控件的 两种简单方法。先将准备工作做好,创建一个UlCustomViews项目。

3.4.1引入布局

如果你用过iPhone应该会知道,几乎每一个iPhone应用的界面顶部都会有一个标题栏,标 题栏上会有一到两个按钮可用于返回或其他操作iPhone没有实体返回键)。现在很多Android 程序也都喜欢模仿iPhone的风格,在界面的顶部放置一个标题栏。虽然Android系统已经给每个 活动提供了标题栏功能,但这里我们决定先不使用它,而是创建一个自定义的标题栏。

经过前面两节的学习,相信创建一个标题栏布局对你来说已经不是什么困难的事情了,只需 要加入两个Button和一个TextView,然后在布局中摆放好就可以了。可是这样做却存在着一个 问题,一般我们的程序中可能有很多个活动都需要这样的标题栏,如果在每个活动的布局中都编 写一遍同样的标题栏代码,明显就会导致代码的大量重复。这个时候我们就可以使用引入布局的 方式来解决这个问题,新建一个布局title.xml,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android: layout_height=,,wrap_content" android:backg round="@d rawable/titlebg">

<Button

android:id="@+id/tifle_back"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:layout_margin="5dp"

android:background="@drawable/backbg" 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: ■tayout_weight="l"

android :gravity=,*center'* android:text="Title Text" android:textColor="#fff" android:textSize="24sp" />

<Button

android: id="(a+id/title_edit"

android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="5dp" android:backg round="@d rawable/editbg" android:text="Edit" android:textColor="#fff" />

</LinearLayout>

可以看到,我们在LinearLayout中分别加入了两个Button和一个Text View,左边的Button 可用于返回,右边的Button可用于编辑,中间的TextView则可以显示一段标题文本。上面代码 中的大多数属性都是你已经见过的,下面我来说明一下几个之前没有讲过的属性。android: background用于为布局或控件指定一个背景,可以使用颜色或图片来进行填充,这里我提前准 备好了 3张图片 itle_bg.png. back bg.pngedit_bg.png,分另U用于作为标题栏、返回按钮和

编辑按钮的背景。另外,在两个Button中我们者K使用了 and roid: layout margin这个属性,它 可以指定控件在上下左右方向上偏移的距离,当然也可以使用android :layout_marginLeftandroid:layout marginTop等属性来单独指定控件在某个方向上偏移的距离。

现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了,修改 activity_main.xml中的代码,如下所示:

<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语句将标题栏布局引入进来就可以了。

最后别忘了在MainActivity中将系统自带的标题栏隐藏掉,代码如下所示:

public class MainActivity extends AppCompatActivity {

^Override protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

ActionBar actionbar = getSupportActionBar();

if (actionbar != null) { actionbar.hide();

}

}

}

这里我们调用了 getSupportActionBar()方法来获得ActionBar的实例,然后再调用 ActionBarhide()方法将标题栏隐藏起来。关于ActionBar的更多用法我们将会在第12章中讲 解,现在你只需要知道可以通过这种写法来隐藏标题栏就足够了。现在运行一下程序,效果如图 3.29所示。

Title Text

3.29引入标题栏布局的效果

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

3.4.2创建自定义控件

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

新建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中带有两个参数的构造函数,在布局中引入TitleLayout控件就 会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,这就要借助 Layoutlnflater来实现了。通过 Layoutlnflater f rom()方法可以构建出一个 Layoutlnflater 对 象,然后调用inflateO方法就可以动态加载一个布局文件,inflate方法接收两个参数,第 一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局 再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入thiso

现在自定义控件已经创建好了,然后我们需要在布局文件中添加这个自定义控件,修改 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" />

</LinearLayout>

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

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

下面我们尝试为标题栏中的按钮注册点击事件,修改TitleLayout中的代码,如下所示:

public class TitleLayout extends LinearLayout {

public TitleLayout(Context context, AttributeSet attrs) { super(context, attrs);

Layoutlnflater.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();

}

});

}

}

首先还是通过findViewByldO方法得到按钮的实例,然后分别调用setOnClickListener() 方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹岀 一段文本。重新运行程序,点击一下编辑按钮,效果如图3.30所示。

Title Text

3.30点击编辑按钮的效果

这样的话,每当我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已 经自动实现好了,这就省去了很多编写重复代码的工作。

3.5最常用和最难用的控件——ListView

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

不过比起前面介绍的几种控件,ListView的用法也相对复杂了很多,因此我们就单独使用一 节内容来对ListView进行非常详细的讲解。

3.5.1 ListView的简单用法

首先新建一个ListViewTest项目,并让Android Studio自动帮我们创建好活动。然后修改 activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android: ■layout_height="match_parent">

<ListView

android: id=,,(a+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" />

</LinearLayout>

在布局中加入ListView控件还算非常简单,先为ListView指定一个id,然后将宽度和高度 都设置为match parent,这样ListView也就占满了整个布局的空间。

接下来修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private String[] data = { "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango", "Apple", "Banana", ,,0rangeH, "Watermelon", ,,Pear,,f "Grape", "Pineapple", "Strawberry", “Cherry”,,Mangou };

^Override

protected void onCreate(Bundle savedlnstanceState) ( super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

ArrayAdapter<String> adapter = new ArrayAdapter<String>( MainActivity.this, android.R.layout. simpte_l.ist_item_l, data);

ListView listview = (ListView) findViewById(R.id.list_view); ListView.setAdapter(adapter);

}

}

既然ListView是用于展示大量数据的,那我们就应该先将数据提供好。这些数据可以是从网 上下载的,也可以是从数据库中读取的,应该视具体的应用程序场景而定。这里我们就简单使用 了一个data数组来测试,里面包含了很多水果的名称。

不过,数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android 中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdaptero它可以通过泛型来指定 要适配的数据类型,然后在构造函数中把要适配的数据传入。ArrayAdapter有多个构造函数的重

 

载,你应该根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将 ArrayAdapter的泛型指定为String,然后在ArrayAdaptei■的构造函数中依次传入当前上下文、 ListView子项布局的id,以及要适配的数据。注意,我们使用了 android.R. layout. simple_ list item l作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个 TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。

最后,还需要调用ListViewsetAdapter()方法,将构建好的适配器对象传递进去,这样 ListView和数据之间的关联就建立完成了。

现在运行一下程序,效果如图3.31所示。可以通过滚动的方式来查看屏幕外的数据。

ListViewTest

Apple

Banana

Orange

term終心

Banana

3.31 ListView运行效果

3.5.2定制ListView的界面

只能显示一段文本的ListView实在是太单调了,我们现在就来对ListView的界面进行定制, 让它可以显示更加丰富的内容。

首先需要准备好一组图片,分别对应上面提供的每一种水果,待会我们要让这些水果名称的 旁边都有一个图样。

接着定义一个实体类,作为ListView适配器的适配类型。新建类Fruit,代码如下所示:

public class Fruit {

private String name;

private int imageld;

public Fruit(String name, int imageld) {

this.name = name;

this.imageld = imageld;

}

public String getName() {

return name;

}

public int getlmageld() {

return imageld;

}

}

Fruit类中只有两个字段,name表示水果的名字,imageld表示水果对应图片的资源id然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建fruitJtem.xml, 代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layoutwidth="matchparent" android:layout_height="wrap_content">

<ImageView

android:id="@+id/fruitimage"

android:layout_width="wrap_content"

android:layout_height=,,wrap_content,, />

<TextView

android:id="(a+id/fruit name"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layoutgravity="cente r_ve rtical" android:layout_marginLeft="10dp" />

</LinearLayout>

在这个布局中,我们定义了一个ImageView用于显示水果的图片,又定义了一个TextView 用于显示水果的名称,并让TextView在垂直方向上居中显示。

接下来需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为 Fruit类。新建类FruitAdapter,代码如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {

private int resourceld;

public FruitAdapter(Context context, int textViewResourceld,

List<Fruit> objects) {

super(context, textViewResourceld, objects);

resourceld = textViewResou rceld;

^Override

public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getltem(position); // 获取当前项的 Fruit 实例

View view = Layoutlnflater.from(getContext()).inflate(resourceld, parent, false);

ImageView fruitimage = (ImageView) view.findViewById(R.id.fruitimage); Textview fruitName = (Textview) view.findViewByld(R.id.fruit name); fruitimage.setImageResource(fruit,getlmageld());

fruitName.setText(fruit.getName()); return view;

}

}

FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据 都传递进来。另外又重写了 getViewO方法,这个方法在每个子项被滚动到屏幕内的时候会被调 用。在getView()方法中,首先通过getltem()方法得到当前项的Fruit实例,然后使用 Layoutlnflater来为这个子项加载我们传入的布局。

这里Layoutlnflaterinflate方法接收3个参数,前两个参数我们已经知道是什么 意思了,第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不为 这个View添加父布局,因为一旦View有了父布局之后,它就不能再添加到ListView中了。如 果你现在还不能理解这段话的含义也没关系,只需要知道这是ListView中的标准写法就可以了, 当你以后对View理解得更加深刻的时候,再来读这段话就没有问题了。

我们继续往下看,接下来调用ViewfindViewById()方法分别获取到ImageViewTextView的实例,并分别调用它们的setImageResource()setText ()方法来设置显示的图 片和文字,最后将布局返回,这样我们自定义的适配器就完成了。

下面修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<>();

@0verride protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

initFruits(); //初始化水果数据 FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);

ListView listview = (ListView) findViewByld(R.id.iistview); listview.setAdapter(adapter);

}

private void initFruitsO { for (int i = 0; i < 2; i++) ( Fruit apple = new Fruit ("Apple", R. d rawable. apple__pic); fruitList.add(apple);

 

Fruit banana = new Fruit("Banana", R.drawable.banana_pic); fruitList.add(banana);

Fruit orange = new Fruit("Orange", R.drawable.orange_pic); fruitList.add(orange);

Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic); fruitList.add(watermelon);

Fruit pear = new FruitC'Pear", R.drawable.pear_pic);

f ruit List.add(pea r);

Fruit grape = new Fruit("Grape", R.drawable.grape_pic); fruitList.add(grape);

Fruit pineapple = new Fruit ("Pineapple", R.drawable. pineapple__pic); fruitList.add(pineapple);

Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic); fruitList.add(strawberry);

Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic); fruitList.add(cherry);

Fruit mango = new Fruit("Mango", R.drawable.mango_pic); fruitList.add(mango);

可以看到,这里添加了一个initFruitsO方法,用于初始化所有的水果数据。在Fruit类 的构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中。另 外我们使用了一个f「循环将所有的水果数据添加了两遍,这是因为如果只添加一遍的话,数据 量还不足以充满整个屏幕。接着在onCreatef)方法中创建了 FruitAdapter对象,并将Fruit- Adapter 作为适配器传递给ListView,这样定制ListView界面的任务就完成了。

现在重新运行程序,效果如图3.32所示。

3.32定制界面的ListView运行效果

虽然目前我们定制的界面还很简单,但是相信聪明的你已经领悟到了诀窍,只要修改 fruit item.xml中的内容,就可以定制出各种复杂的界面了。

3.5.3提升ListView的运行效率

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

仔细观察会发现,getView()方法中还有一个convertView参数,这个参数用于将之前加 载好的布局进行缓存,以便之后可以进行重用。修改FruitAdapter中的代码,如下所示: public class FruitAdapter extends ArrayAdapter<Fruit> {

(QOverride

public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getltem(position);

View view;

if (convertView == null) {

view = Layoutlnflater.from(getContext()).inflate(resourceld, parent, false);

} else {

view = convertView;

}

ImageView fruitimage = (ImageView) view.findViewById(R.id.fruitimage); Textview fruitName = (Textview) view.findViewById(R.id.fruit name); f ruitimage.setlmageResou rce(f ruit.getlmageld()) f ruitName.setText(fruit.getName());

return view;

}

}

可以看到,现在我们在getViewO方法中进行了判断,如果convertView null,则使用 Layoutlnflater去加载布局,如果不为null则直接对convertView进行重用。这样就大大提 高了 ListView的运行效率,在快速滚动的时候也可以表现出更好的性能。

不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但 是每次在getView()方法中还是会调用ViewfindViewById()方法来获取一次控件的实例。 我们可以借助一个ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如下 所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {

 

public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getltem(position);

View view;

ViewHolder viewHolder;

if (convertView == null) {

view = Layoutlnflater.from(getContext()).inflate(resourceld, parent, false);

viewHolder = new ViewHolder(); viewHolder.fruitlmage = (Imageview) view.findViewByld (R.id.f ruit_image);

viewHolder.fruitName = (Textview) view.findViewByld (R.id.fruit_name); view. setTag(viewHolder); // ViewHolder 存储在 View

} else ( view = convertView;

viewHolder = (ViewHolder) view.getTag(); // 重新获取 ViewHolder } viewHolder.fruitlmage.setlmageResource(fruit.getlmageld()); viewHolder.f ruitName.setText(f ruit.getName());

return view;

}

class ViewHolder {

Imageview fruitlmage;

TextView fruitName;

}

}

我们新增了一个内部类ViewHolder,用于对控件的实例进行缓存。当convertView jAj null 的时候,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder里,然后调用View setTag()方法,将ViewHolder对象存储在View中。当convertView不为null的时候, 则调用ViewgetTagO方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了 ViewHolder里,就没有必要每次都通过findViewByld()方法来获取控件实例了。

通过这两步优化之后,我们ListView的运行效率就已经非常不错了。

3.5.4 ListView的点击事件

话说回来,ListView的滚动毕竟只是满足了我们视觉上的效果,可是如果ListView中的子项 不能点击的话,这个控件就没有什么实际的用途了。因此,本小节我们就来学习一下ListView如 何才能响应用户的点击事件。

修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<>();

@Override

protected void onCreate(Bundle savedlnstanceState) {

super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); initFruits();

FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout. fruititem, fruitList);

Listview listview = (Listview) findViewById(R.id.list_view); listview.setAdapter(adapter);

HstView.setOnItemClickListener(new Adapterview.OnltemClickListenerO { ©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将 水果的名字显示出来O

重新运行程序,并点击一下橘子,效果如图3.33所示。

3.33点击ListView的效果

3.6更强大的滚动控件 RecyclerView

ListView由于其强大的功能,在过去的Android开发当中可以说是贡献卓越,直到今天仍然 还有不计其数的程序在继续使用着ListViewo不过ListView并不是完全没有缺点的,比如说如果 我们不使用一些技巧来提升它的运行效率,那么ListView的性能就会非常差。还有,ListView的 扩展性也不够好,它只能实现数据纵向滚动的效果,如果我们想实现横向滚动的ListView是 做不到的。

为此,Android提供了一个更强大的滚动控件——RecyclerViewo它可以说是一个增强版的 ListView,不仅可以轻松实现和ListView同样的效果,还优化了 ListView中存在的各种不足之处。 目前Android官方更加推荐使用RecyclerView,未来也会有更多的程序逐渐从ListView转向 RecyclerView,那么本节我们就来详细讲解一下RecyclerView的用法。

首先新建一个RecyclerViewTest项目,并让Android Studio自动帮我们创建好活动。

3.6.1 RecyclerView 的基本用法

和百分比布局类似,RecyclerView也属于新增的控件,为了让RecyclerView在所有Android 版本上都能使用,Android团队采取了同样的方式,将RecyclerView定义在了 support库当中。因 此,想要使用RecyclerView这个控件,首先需要在项目的build.gradle中添加相应的依赖库才行。

打开app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {

compile fileTree(dir: 1 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'

}

添加完之后记得要点击一下Sync Now来进行同步。然后修改activity main.xml中的代码, 如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="matchparent" android: 'layout_height="match_pa rent ">

<android.support.v7.widget.RecyclerView

android:id="@+id/recycler_view"

android:layout_width="match_parent" android:layout_height="match_parent" />

</LinearLayout>

在布局中加入RecyclerView控件也是非常简单的,先为RecyclerView指定一个id,然后将 宽度和高度都设置为match_paent,这样RecyclerView也就占满了整个布局的空间。需要注意 的是,由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写岀来。

 

这里我们想要使用RecyclerView来实现和ListView相同的效果,因此就需要准备一份同样 的水果图片。简单起见,我们就直接从ListViewTest项目中把图片复制过来就可以了,另外顺便 将Fruit类和fruit_item.xml也复制过来,省得将同样的代码再写一遍。

接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自 RecyclerView.Adapter,并将泛型指定为 FruitAdapter.ViewHolder0 其中,ViewHolder 是我们在FruitAdapter中定义的一个内部类,代码如下所示:

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> { private List<Fruit> mFruitList;

static class ViewHolder extends RecyclerView.ViewHolder { ImageView fruitimage; Textview fruitName;

public ViewHolde「(View view) { super(view); fruitimage = (ImageView) view.findViewByld(R.id.fruit image); fruitName = (Textview) view.findViewByld(R.id.fruit name);

}

}

public FruitAdapter(List<Fruit> fruitList) { mFruitList = fruitList;

}

(QOverride public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext())

,inflate(R.layout.fruititem, parent, false); ViewHolder holder = new ViewHolder(view); return holder;

}

^Override public void onBindViewHolder(ViewHolder holder, int position) { Fruit fruit = mFruitList.get(position); holder.fruitimage.setImageResource(fruit.getlmageld()); holder.fruitName.setText(fruit.getName());

}

(aOverride

public int getltemCount() { return mFruitList.size();

}

}

虽然这段代码看上去好像有点长,但其实它比ListView的适配器要更容易理解。这里我们首 先定义了一个内部类 ViewHolder, ViewHolder 要继承自 RecyclerView.ViewHoldero 然后 ViewHolder的构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外 层布局,那么我们就可以通过findViewByldO方法来获取到布局中的ImageViewTextView 的实例了。

接着往下看,FruitAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来, 并赋值给一个全局变量mFruitList,我们后续的操作都将在这个数据源的基础上进行。

继续往下看,由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写 onCreateViewHolder() onBindViewHolder()getltemCount()3 个方法。onCeate ViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将f ruit item布局加载 进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到构造函数当中,最后将 ViewHolder的实例返回。onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值 的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit 实例,然后再将数据设置到ViewHolderImageViewTextView当中即可。getltemCount () 方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。

适配器准备好了之后,我们就可以开始使用RecyclerView 了,修改MainActivity中的代码, 如下所示:

public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<>();

(QOverride protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

initFruitsO ; //初始化水果数据

RecyclerView recyclerView = (RecyclerView) findViewById(R.id. recycler_view); LinearLayoutManager layoutManager = new LinearLayoutManager(this); recycle rView.set LayoutManage r(layoutManage r); FruitAdapter adapter = new FruitAdapter(fruitList);

recyclerView.setAdapter(adapter);

}

private void initFruitsO { for (int i = 0; i < 2; i++) { Fruit apple = new Fruit("Apple", R.drawable.apple_pic); f ruit List.add(apple);

Fruit banana = new Fruit("Banana", R.drawable.banana_pic); fruitList.add(banana); "

Fruit orange = new Fruit("Orange", R.drawable.orange_pic); f ruit List.add(o range);

Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon__pic); fruitList.add(watermelon);

Fruit pear = new FruitC'Pear", R. drawable. pear_pic);

f ruit List.add(pea r); Fruit grape = new Fruit("Grape", R.drawable.grape_pic); f ruit List.add(grape);

Fruit pineapple = new Fruit ("Pineapple", R.drawable. pineappte__pic); fruitList.add(pineapple);

Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic); f ruit List.add(st rawbe r ry);

Fruit cherry = new FruitCXherry", R.drawable.cherry_pic);

fruitList.add(cherry);

Fruit mango = new Fruit("Mango", R.drawable.mango_pic); fruitList.add(mango);

}

}

}

可以看到,这里使用了一个同样的initFruitsO方法,用于初始化所有的水果数据。接着 在onCreate()方法中我们先获取到RecyclerView的实例,然后创建了一个LinearLayout- Manager对象,并将它设置到RecyclerView当中。LayoutManager用于指定RecyclerView的布局 方式,这里使用的LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。 接下来我们创建了 FruitAdapter的实例,并将水果数据传入到FruitAdapter的构造函数中, 最后调用RecyclerViewsetAdapter()方法来完成适配器设置,这样RecyclerView和数据之间 的关联就建立完成了。

现在可以运行一下程序了,效果如图3.34所示。

可以看到,我们使用RecyclerView实现了和ListView几乎一模一样的效果,虽说在代码量 方面并没有明显地减少,但是逻辑变得更加清晰了。当然这只是RecyclerView的基本用法而已, 接下来我们就看一看RecyclerView还能实现哪些ListView实现不了的效果。

3.6.2实现横向滚动和瀑布流布局

我们已经知道,ListView的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚 动的话,ListView就做不到了。那么RecyclerView就能做得到吗?当然可以,不仅能做得到,还 非常简单,那么接下来我们就尝试实现一下横向滚动的效果。

首先要对fruit_item布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于 纵向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_item里的元素改成垂直排列 才比较合理。修改fruit item.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android :layout_width=ll100dp>*

android:layout_height="wrap_content" >

<ImageView

android:id="@+id/fruit_image"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:l.ayout_gravity=,,center_horizonta'l" />

<TextView

android: id="(a+id/f ruitname"

android :layout_width=,,wrap_content"

android:layout_height="wrap_content"

android :layout_gravity=,,center__horizontal" android:layout__marginTop="10dp" />

</LinearLayout>

可以看到,我们将LinearLayout改成垂直方向排列,并把宽度设为lOOdp这里将宽度指定 为固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView的子项 就会有长有短,非常不美观;而如果用match_parent的话,就会导致宽度过长,一个子项占满 整个屏幕。

然后我们将ImageViewTextView都设置成了在布局中水平居中,并且使用layout. marginTop属性让文字和图片之间保持一些距离。

接下来修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<>();

^Override

protected void onCreate(Bundle savedlnstanceState) {

super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); initFruits();

RecyclerView recyclerView = (RecyclerView) findViewByld (R. id. recycler view); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL): recyclerView.setLayoutManager(layoutManager);

FruitAdapter adapter = new FruitAdapter(fruitList); recyclerView.setAdapter(adapter);

}

}

MainActivity 中只加入了一行代码,调用 LinearLayoutManager setOrientation ()方法来 设置布局的排列方向,默认是纵向排列的,我们传入LinearLayoutManager .HORIZONTAL表示 让布局横行排列,这样RecyclerView就可以横向滚动了。

重新运行一下程序,效果如图3.35所示。

3.35 横向 RecyclerView 效果

你可以用手指在水平方向上滑动来查看屏幕外的数据。

为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就能实现了呢? 这主要得益于RecyclerView出色的设计oListView的布局排列是由自身去管理的,而RecyclerView 则将这个工作交给了 LayoutManager, LayoutManagei•中制定了一套可扩展的布局排歹lj接口,子类 只要按照接口的规范来实现,就能定制岀各种不同排列方式的布局了。

除了 LinearLayoutManager 之外,RecyclerView 还给我们提供了 GridLayoutManager StaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格 布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。这里我们来实现一下效果更加炫 酷的瀑布流布局,网格布局就作为课后习题,交给你自己来研究了。

首先还是来修改一下fruit item.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"

android: ■Layout_width=,,match_parent"

android:layout_height=" wrapcontent"

android:layout__margin=,,5dp,, >

<ImageView

android: id="(a+icl/f ruitimage"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layoutgravity="center_horizontal" />

<TextView

android:id="@+id/fruit name"

android:layout_width="wrap_content" android:layout_height="wrap_content" and roid: layout__g ravity="left" android:layout_marginTop=,,10dp" />

</LinearLayout>

这里做了几处小的调整,首先将LinearLayout的宽度由lOOdp改成了 match parent,因为 瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。另外我们使用了 layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。还有 就是将TextView的对齐属性改成了居左对齐,因为待会我们会将文字的长度变长,如果还是居 中显示就会感觉怪怪的。

接着修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<>();

^Override

protected void onCreate(Bundle savedlnstanceState) {

super.onCreate(savedlnstanceState);

setContentView(R.layout.activitymain); initFruits();

RecyclerView recyclerView = (RecyclerView) findViewByld (R. id. recycler view); StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3f StaggeredGridLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager);

FruitAdapter adapter = new FruitAdapter(fruitList); recyclerView.setAdapter(adapter);

}

private void initFruits() {

for (int i = 0; i < 2; i++) {

Fruit apple = new Fruit(

getRandomLengthNameC'Apple11), R.drawable.apple pic);

fruitList.add(apple);

Fruit banana = new Fruit( getRandomLengthName("Banana"), R.drawable.banana pic); fruitList.add(banana);

Fruit orange = new Fruit(

 

getRandomLengthName("Orange"), R.drawable.orangepic);

fruitList.add(orange);

Fruit watermelon = new Fruit( getRandomLengthName("Watermelon"), R.drawable.watermelon pic); fruitList.add(watermelon);

Fruit pear = new Fruit( getRandomLengthName("Pear"), R.drawable.pear pic); fruitList.add(pear);

Fruit grape = new Fruit( getRandomLengthName("Grape"), R.drawable.grape pic); fruitList.add(grape);

Fruit pineapple = new Fruit( getRandomLengthName("Pineapple"), R.drawable.pineapplepic); fruitList.add(pineapple);

Fruit strawberry = new Fruit( getRandomLengthName("Strawberry"), R.drawable.strawberry pic); fruitList.add(st rawberry);

Fruit cherry = new Fruit(

getRandomLengthName("Cherry11), R.drawable.cherry pic);

fruitList.add(cherry);

Fruit mango = new Fruit( getRandomLengthName("Mango"), R.drawable.mangopic); fruitList.add(mango);

}

}

private String getRandomLengthName(St ring name) {

Random random = new Random();

int length = random.nextlnt(2G) + 1;

StringBuilder builder = new StringBuilderO;

for (int i = 0; i < length; i++) {

builde r.append(name);

} return builder.toString();

}

}

首先,在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例。 StaggeredGridLayoutManager的构造函数接收两个参数,第一个参数用于指定布局的列数, 传入3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager. VERTICAL表示会让布局纵向排列,最后再把创建好的实例设置到RecyclerView 当中就可以了,就是这么简单!

没错,仅仅修改了一行代码,我们就已经成功实现瀑布流布局的效果了。不过由于瀑布流布 局需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。这里我们把眼 光聚焦在getRandomLengthName()这个方法上,这个方法使用了 Random对象来创造一个120之间的随机数,然后将参数中传入的字符串重复随机遍。在initFruitsO方法中,每个水果 的名字都改成调用getRandomLengthName ()这个方法来生成,这样就能保证各水果名字的长短

 

差距都比较大,子项的高度也就各不相同了。

现在重新运行一下程序,效果如图3.36所示。

RecyclerViewTest

3.36瀑布流布局效果

当然由于水果名字的长度每次都是随机生成的,你运行时的效果肯定和图中还是不一样的。

3.6.3 RecyclerView 的点击事件

ListView 一样,RecyclerView也必须要能影响点击事件才可以,不然的话就没什么实际用 途了。不过不同于ListView的是,RecyclerView并没有提供类似于setOnItemClickListener() 这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件,相比于ListView 来说,实现起来要复杂一些。

那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点 击事件上却没有处理得非常好呢?其实不是这样的,ListView在点击事件上的处理并不人性化, setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的 某一个按钮呢?虽然ListView也是能做到的,但是实现起来就相对比较麻烦了。为此, RecyclerView干脆直接摒弃了子项点击事件的监听器,所有的点击事件都由具体的View去注册, 就再没有这个困扰了。

下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改FruitAdapter中的代 码,如下所示:

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {

private List<Fruit> mFruitList;

 

static class ViewHolder extends RecyclerView.ViewHolder {

View fruitview;

ImageView fruitimage;

Textview fruitName;

public ViewHolder(View view) {

super(view);

fruitView = view;

fruitimage = (ImageView) view.findViewByld(R.id.fruitimage); fruitName = (Textview) view.findViewById(R.id.fruit name);

} ~

}

public FruitAdapter(List<Fruit> fruitList) {

mFruitList = fruitList;

}

@0verride

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

View view = Layoutlnflater.from(parent.getContext()).inflate(R.layout. fruit_item, parent, false);

final ViewHolder holder = new ViewHolder(view);

holder.fruitview.setOnClickListener(new View.OnClickListener() { ^Override public void onClick(View v) {

int position = holder.getAdapterPosition();

Fruit fruit = mFruitList.get(position);

Toast.makeText(v.getContext(), "you clicked view " + fruit.getName(), Toast.LENGTH_SHORT).show();

}

});

holder.fruitimage.setOnClickListener(new View.OnClickListener() { ©Override public void onClick(View v) {

int position = holder.getAdapterPosition();

Fruit fruit = mFruitList.get(position);

Toast .makeText(v.getContext(), "you clicked image " + f ruit ,getName(), Toast,LENGTH_SHORT). show();

} "

});

return holder;

}

}

我们先是修改了 ViewHolder,ViewHolder中添加了 fruitView变量来保存子项最外层 布局的实例,然后在onCreateViewHolder()方法中注册点击事件就可以了。这里分别为最外层 布局和ImageView都注册了点击事件,RecyclerView的强大之处也在这里,它可以轻松实现子项 中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的position,然后通过 position拿到相应的Fruit实例,再使用Toast分别弹出两种不同的内容以示区别。

 

现在重新运行代码,并点击香蕉的图片部分,效果如图3.37所示。可以看到,这时触发了 ImageView的点击事件。

然后再点击菠萝的文字部分,由于TextView并没有注册点击事件,因此点击文字这个事件 会被子项的最外层布局捕获到,效果如图3.38所示。

 

 

3.7编写界面的最佳实践

既然已经学习了那么多ui开发的知识,也是时候实战一下了。这次我们要综合运用前面所 学的大量内容来编写出一个较为复杂且相当美观的聊天界面,你准备好了吗?要先创建一个 UIBestPractice项目才算准备好了哦。

3.7.1 制作 Nine-Patch 图片

在实战正式开始之前,我们还需要先学习一下如何制作Nine-Patch图片。你可能之前还没有 听说过这个名词,它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域 不可以。

那么Nine-Patch图片到底有什么实际作用呢?我们还是通过一个例子来看一下吧。比如说项 目中有一张气泡样式的图片messagejeft.png,如图3.39所示。

 

 

3.39气泡样式图片

 

我们将这张图片设置为LinearLayout的背景图片,修改activity_main.xm 1中的代码,如下 所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android: layout_width=,,match_parent" android:layout_height="wrap_content" android: background="(ad rawable/messagelef t"

>

</LinearLayout>

LinearLayout的宽度指定为match parent,然后将它的背景图设置为message left, 现在运行程序,效果如图3.40所示。

UlBeslPrac lice

3.40气泡被均匀拉伸的效果

可以看到,由于message_left的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉 伸了!这种效果非常差,用户肯定是不能容忍的,这时我们就可以使用Nine-Patch图片来进行 改善。

Android sdk目录下有一个tools文件夹,在这个文件夹中找到draw9patch.bat文件,我们 就是使用它来制作Nine-Patch图片的。不过,要打开这个文件,必须先将JDKbin目录配置到 环境变量当中才行,比如你使用的是Android Studio内置的jdk,那么要配置的路径就是Android Studio安装目录>/jre/bino如果你还不知道该如何配置环境变量,可以先去参考6.4.1小节的内容。

双击打开draw9patch.bat文件,在导航栏点击File—>Open 9-patchmessage_left.png加载进 来,如图3.41所示。

3.41 使用 draw9patch 编辑 message_left 图片

我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片 需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容会被放置的区域。使 用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift键拖动可以进行擦除。绘制完成后效果 如图3.42所示。

3.42绘制完成后的message left图片

最后点击导航栏File-^Save 9-patch把绘制好的图片进行保存,此时的文件名就是message_ left.9.pngo使用这张图片替换掉之前的messagejeft.png图片,重新运行程序,效果如图3.43 所示。

UIBcstPractice

3.43气泡只拉伸绘制区域的效果

这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也有了很大的改进。 有了这个知识储备之后,我们就可以进入实战环节了。

3.7.2编写精美的聊天界面

既然是要编写一个聊天界面,那就肯定要有收到的消息和发出的消息。上一节中我们制作的 message_left.9.png可以作为收到消息的背景图,那么毫无疑问你还需要再制作一张message. right.9.png作为发出消息的背景图。

图片都提供好了之后就可以开始编码了。由于待会我们会用到RecyclerView,因此首先需要 在app/build.gradle当中添加依赖库,如下所示:

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 1junit:junit:4.12'

}

接下来开始编写主界面,修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android=nhttp://schemas.android.com/apk/res/android" android:orientation="vertical"

android:layout_width="match_parent" android:layout_height="match_parent" android:background="#d8eQe8" >

ondroid. support. v7 .widget. RecyclerView

android:id="@+id/msg_recycler_view"

android:layout_width="matchparent"

android:layout_height="0dp"

android:layout_weight="l" />

<LinearLayout

android:layout_width="match_parent"

android:layout_height="wrap_content" >

<EditText

android: id=,,(a+id/input_text"

android: 'layout_wiclth="0dp"

android:layout_height="wrap_content"

android:layout_weight="l"

android:hint="Type something here"

android:maxLines="2" />

<Button

android:id="@+id/send"

android: layoutwidthMwrapcontent"

android:layout_height="wrap_content" and roid:text=HSend" />

</LinearLayout>

</LinearLayout>

我们在主界面中放置了一个RecyclerView用于显示聊天的消息内容,又放置了一个EditText 用于输入消息,还放置了一个Button用于发送消息。这里用到的所有属性都是我们之前学过的, 相信你理解起来应该不费力。

然后定义消息的实体类,新建Msg,代码如下所示:

public class Msg {

public static final int TYPE_RECEIVED = 0;

public static final int TYPESENT = 1;

private String content;

private int type;

public Msg(String content, int type) {

this.content = content;

this.type = type;

}

public String getContent() {

return content;

}

public int getType() {

}

)

Msg类中只有两个字段,content表示消息的内容,type表示消息的类型。其中消息类型 有两个值可选,TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发岀的 消息。

接着来编写RecyclerView子项的布局,新建msg_item.xml,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height=,,wrap_content" android:padding="10dp" >

<LinearLayout

android:id="@+id/leftlayout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layoutg ravity="left" android: background="(aclrawable/message left" >

<TextView

and roid: id="@+id/'left_msg"

android:layout_width="wrap_content"

android: layout_height=,,wrap_content" android:layout_gravity="center" android:layoutmargin="10dp" android:textColor="#fff" />

</LinearLayout>

<LinearLayout

android:id="@+id/right_layout" android :layout_width=,,wrap_content" android: layout_height=,,wrap_content" android:layoutgravity="right" android:background="@drawable/message right" >

<TextView

android:id="@+id/rightmsg"

android:layout_width="wrap_content" android:layout_height="wrap_content" android:layoutgravity="center" android:layout_margin="10dp" />

</LinearLayout>

</LinearLayout>

这里我们让收到的消息居左对齐,发出的消息居右对齐,并且分别使用message_left.9.pngmessage_right.9.png作为背景图。你可能会有些疑虑,怎么能让收到的消息和发出的消息都放在 同一个布局里呢?不用担心,还记得我们前面学过的可见属性吗?只要稍后在代码中根据消息的 类型来决定隐藏和显示哪种消息就可以了。

接下来需要创建RecyclerView的适配器类,新建类MsgAdapter,代码如下所示:

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {

private List<Msg> mMsgList;

static class ViewHolder extends RecyclerView.ViewHolder {

LinearLayout leftLayout;

LinearLayout rightLayout;

Textview leftMsg;

Textview rightMsg;

public ViewHolder(View view) {

super(view);

leftLayout = (LinearLayout) view.findViewByld(R.id.leftlayout); rightLayout = (LinearLayout) view.findViewByld(R.id.rightlayout); leftMsg = (Textview) view.findViewByld(R.id.leftmsg); rightMsg = (Textview) view.findViewByld(R.id.rightmsg);

}

}

public MsgAdapter(List<Msg> msgList) { mMsgList = msgList;

}

(QOverride

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) ( View view = Layoutlnflater.from(parent.getContext()).inflate

(R.layout.msgitem, parent, false); return new ViewHolder(view);

}

^Override

public void onBindViewHolder(ViewHolder holder, int position) { Msg msg = mMsgList.get(position);

if (msg.getTypeO == Msg.TYPE_RECEIVED) (

//如果是收到的消息,则显示左法的消息布局,将右边的消息布局隐藏

holder.leftLayout.setVisibility(View.VISIBLE); holder.rightLayout.setvisibility(View.GONE); holder.leftMsg.setText(msg.getContent());

} else if(msg.getTypeO == Msg.TYPE_SENT) {

//如果是发出的消息,则显示右边的消息布局,将左边的消息布局隐藏

holder.rightLayout.setvisibility(View.VISIBLE); holder.leftLayout.setvisibility(View.GONE);

holder.rightMsg.setText(msg.getContent());

}

}

(QOverride

public int getltemCount() { return mMsgList.size();

}

}

以上代码你应该非常熟悉了,和我们学习RecyclerView那一节的代码基本是一样的,只不过 在onBindViewHolderO方法中增加了对消息类型的判断。如果这条消息是收到的,则显示左边 的消息布局,如果这条消息是发出的,则显示右边的消息布局。

最后修改MainActivity中的代码,来为RecyclerView初始化一些数据,并给发送按钮加入事 件响应,代码如下所示:

public class MainActivity extends AppCompatActivity {

private List<Msg> msgList = new ArrayList<>();

private EditText inputText;

private Button send;

private RecyclerView msgRecyclerView;

private MsgAdapter adapter;

(QOverride protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

initMsgs(); //初始化消息数据

imputText = (EditText) findViewByld(R.id.inputtext); send = (Button) findViewById(R.id.send);

msgRecyclerView = (RecyclerView) findViewByld(R.id.msgrecyclerview); LinearLayoutManager layoutManager = new LinearLayoutManager(this); msgRecyclerView.setLayoutManager(layoutManager);

adapter = new MsgAdapter(msgList); msgRecyclerView.setAdapter(adapter); send. setOnClickListener(new View.OnC"lickListener() {

@0verride public void onClick(View v) { String content = inputText.getText().toString(); if (!"".equals(content)) {

Msg msg = new Msg(content, Msg.TYPESENT);

msgList.add(msg);

adapter. notifyltemlnserted(msgList. size() - 1); // 当有新消息时, 刷新Listview市的显示

msgRecyclerView.scrollToPosition(msgList.size() - 1); 〃将 Listview定位到最后一行 inputText. setText (""); //清空输入框中的内容

} } });

}

private void initMsgs() {

Msg msgl = new Msg("Hello guy.", Msg.TYPE_RECEIVED);

msgList.add(msgl);

Msg msg2 = new Msg ("Hello. Who is that?11, Msg .TYPE_SENT); msgList,add(msg2);

Msg msg3 = new Msg ("This is Tom. Nice talking to you. ", Msg .TYPERECEIVED); msgList.add(msg3);

}

}

initMsgs()方法中我们先初始化了几条数据用于在RecyclerView中显示。然后在发送按 钮的点击事件里获取了 EditText中的内容,如果内容不为null则创建出一个新的Msg对象,并 把它添加到msgList列表中去。之后又调用了适配器的notifyltemlnsertedO方法,用于通知 列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示。接着调用 RecyclerViewscrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得 到最后发出的一条消息。最后调用EditTextsetText ()方法将输入的内容清空。

这样所有的工作就都完成了,终于可以检验一下我们的成果了,运行程序之后你将会看到非 常美观的聊天界面,并且可以输入和发送消息,如图3.44所示。

UlBestPractict?

3.44精美的聊天界面

相信这个例子的实战过程不仅加深了你对本章中所学UI知识的理解,还让你有了如何灵活 运用这些知识来设计出优秀界面的思路。这一章也是学了不少东西,让我们来总结一下吧。

 

3.8小结与点评

虽然本章的内容很多,但我觉得学习起来应该还是挺愉快的吧。不同于上一章中我们来来回 回使用那几个按钮,本章可以说是使用了各种各样的控件,制作出了丰富多彩的界面。尤其是在 实战环节,编写出了那么精美的聊天界面,你的满足感应该比上一章还要强吧?

本章从Android中的一些常见控件开始入手,依次介绍了基本布局的用法、自定义控件的方 法、ListView的详细用法以及RecyclerView的使用,基本已经将重要的UI知识点全部覆盖了。 想想在开始的时候我说不推荐使用可视化的编辑工具,而是应该全部使用XML的方式来编写界 面,现在你是不是已经感觉使用XML非常简单了呢?以后不管面对多么复杂的界面,我希望你 都能够自信满满,因为真正理解了界面编写的原理之后,是没有什么能够难得倒你的。

不过到目前为止,我们还只是学习了 Android手机方面的开发技巧,下一章将会涉及一些 Android平板方面的知识点,能够同时兼容手机和平板也是自Android4.0系统开始就支持的特性。 适当地放松和休息一段时间后,我们再来继续前行吧!