🌷🍁 博主 libin9iOak带您 Go to New World.✨🍁
🦄 个人主页——libin9iOak的博客🎐
🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺
🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐
🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批评指正!🍁🐥
《移动互联网技术》课程简介
《移动互联网技术》课程是软件工程、电子信息等专业的专业课,主要介绍移动互联网系统及应用开发技术。课程内容主要包括移动互联网概述、无线网络技术、无线定位技术、Android应用开发和移动应用项目实践等五个部分。移动互联网概述主要介绍移动互联网的概况和发展,以及移动计算的特点。无线网络技术部分主要介绍移动通信网络(包括2G/3G/4G/5G技术)、无线传感器网络、Ad hoc网络、各种移动通信协议,以及移动IP技术。无线定位技术部分主要介绍无线定位的基本原理、定位方法、定位业务、数据采集等相关技术。Android应用开发部分主要介绍移动应用的开发环境、应用开发框架和各种功能组件以及常用的开发工具。移动应用项目实践部分主要介绍移动应用开发过程、移动应用客户端开发、以及应用开发实例。
课程的教学培养目标如下:
1.培养学生综合运用多门课程知识以解决工程领域问题的能力,能够理解各种移动通信方法,完成移动定位算法的设计。
2.培养学生移动应用编程能力,能够编写Andorid应用的主要功能模块,并掌握移动应用的开发流程。
3. 培养工程实践能力和创新能力。
通过本课程的学习应达到以下目的:
1.掌握移动互联网的基本概念和原理;
2.掌握移动应用系统的设计原则;
3.掌握Android应用软件的基本编程方法;
4.能正确使用常用的移动应用开发工具和测试工具。
第五章 界面开发
本章小结:
1**、本单元学习目的**
通过学习Android开发中最基本的开发模块Activity(活动),掌握Android系统中Activity的堆栈管理方式以及Activity的生命周期;重点掌握视图结构、布局模型、事件处理模型和信息传递方式Intent;对于界面的组成元素,重点掌握常用的几种控件:列表控件(ListView)、滑动页面控件(ViewPager)、碎片(Fragment)、WebView控件的使用。掌握图形、图像以及动画的编程方式。
2**、本单元学习要求**
(1) 掌握Activity的基本概念,Activity的堆栈管理和生命周期;
(2) 掌握视图结构、常用布局模式、信息传递方式Intent和Intent Filter过滤规则;
(3) 掌握各种常用控件的使用方法;
(4) 掌握Fragment在界面上的共享与重用方法;
(5) 了解事件处理机制;
(6) 掌握图形、图像和动画的处理方式。
3**、本单元学习方法**
结合Android Studio开发环境,分析示例代码,编写程序实现界面功能,理解界面设计的基本原则和组织结构,并总结Android界面编程的基本方法。
4**、本单元重点难点分析**
重点
(1) Activity
在一个Android应用中有多个界面,比如:有答题界面、有查看答题界面、答题情况统计界面等等。一个界面就是一个活动,而所有这些活动都是由Android系统统一进行管理。由于手机屏幕的限制,通常屏幕上一次仅显示一个活动界面。而且,由于手机的各种资源有限(内存、电源等),Android系统在内存紧张的时候,往往会销毁当前没有使用的活动(不显示或不能响应的界面)。在系统中,活动将不断经历从创建到销毁的周期运行。了解活动如何生存,以及活动整个生命周期的状态变迁,能更清楚地知道如何去实现活动。
从系统的角度来看,当Android应用(APP)启动运行时,就会创建一个任务(Task)。任务用来存放当前运行的活动,所有的活动都归属于这个任务。任务采用栈结构来保存活动,这个栈通常又称为返回栈。一旦某个活动被创建,就会被压入到任务栈中;而只有在栈顶的活动才可见并且可以和用户进行交互操作,也就是说位于栈顶的活动在前台运行。当任务栈中的所有活动都被清除出栈(弹出栈)时,任务栈会被销毁,程序退出。
从用户的角度来看,用户在使用Android应用的时候,经常打开多个界面(活动),完成一系列的操作,比如:用户要在浏览器中查看新闻就需要点击新闻列表,打开新闻浏览界面;如果想把刚才看到的新闻分享给微信中的朋友,点击分享就会打开微信的界面。所有这些操作的序列就称之为“任务”。任务将一组相互关联的活动组织在一起,形成一个操作集合,每一个活动就代表一个用户操作。
任务通过栈结构来控制所有界面的跳转和返回。在堆栈中,只有栈顶的活动可以操作,也就是说一个任务中只有一个活动处于运行状态,其他的活动都转入到后台暂停运行,Android系统会保存这些活动的状态,以便它们在转入前台时可以恢复运行。采用这种界面管理方式,确保了系统每次都只有一个界面在前台运行,减少了整个系统的内存开销。
在默认情况下,当一个活动启动另一个活动时,两个活动都放置在同一个任务中,即压入同一个返回栈。当用户按下后退键,后压入的活动将从返回栈中弹出,前面压入的活动又显示在屏幕上。当一个应用启动其他应用中的活动时,比如:用户拍照以后把相片共享给QQ中的朋友,这时将打开QQ应用界面,这两个应用(拍照和QQ)的界面(活动)对用户来说好像属于同一个应用程序;而在系统内部,任务与任务之间是相互独立的。
通过分析栈的行为和活动的周期变化,将活动的运行分为四种生存状态:运行状态、暂停状态、停止状态和销毁状态。
(1) 运行状态
活动处于运行状态时,将位于栈顶,表示用户当前正在与活动进行交互操作,即:正在使用活动界面。在系统资源紧张的情况下,通常不会销毁处于运行状态的活动。
(2) 暂停状态
活动处于暂停状态时,活动界面部分可见,用户不能够对它进行操作,比如点击删除图片按钮,在图片显示界面(活动)上会弹出一个对话框,对话框让用户确认是否删除图片,对话框没有占据整个屏幕,因此显示图片的界面(活动)仍然部分可见;但是这时用户无法控制图片显示界面,显示界面(活动)就处于暂停状态。处于暂停状态的活动仍然是存活着的,系统通常不会回收这种活动。
(3) 停止状态
当活动被压到返回栈的下面,在屏幕上完全不可见,这个时候活动就处于停止状态。系统会保存活动的状态和成员变量。但是,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。
(4) 销毁状态。
如果活动被弹出返回栈,活动就被销毁了,系统会回收它所占用的内存和资源,这时活动就处于销毁状态。
首先,系统创建活动。通常应用在活动的onCreate()函数中完成一些初始化操作,比如加载布局,获取控件对象等等。然后,活动开始运行,调用onStart()函数;接下来,onResume()函数使得这个活动获得焦点,活动准备和用户进行交互,活动也就进入到运行状态。
如果用户启动其他活动,比如对话框式的活动,原来的活动就转为暂停状态。转移到暂停状态时,可以在onPause()函数中释放一些不用的资源,保存一些关键的数据。如果活动只是暂停,它可以重新获得焦点(onResume),从而恢复到运行状态。
如果启动新的活动,原来的活动完全不可见,这时会调用onStop()函数。通常在onStop()函数中释放不用的资源,关闭一些耗时的操作,比如向数据库里面写入数据。如果活动停止后,又重新启动它,活动又再次进入运行状态。这时活动由后台切换到前台,会调用onRestart()函数,这时可以在onRestart()函数中做一些必要的恢复操作。
最后,在活动被销毁之前,系统会调用onDestroy()函数,用于释放活动所占用的资源。活动销毁后它的整个生命周期也就结束了。
活动在onCreate()函数和onDestroy()函数之间所经历的状态变迁,就是完整的生命期。把活动在生命周期中调用的生命进行配对,可以更容易的理解如何使用这些方法。首先,onCreate()函数和onDestroy()函数是一对。通常活动在onCreate()函数中完成各种初始化操作,对应的在onDestroy()函数中释放内存和资源。onStart()函数和onStop()函数对应,它们分别使得活动可见和不可见。一般在 onStart()函数中对资源进行加载,而在onStop() 函数中对资源进行释放,从而保证处于停止状态的活动不会占用过多内存。活动在 onResume()函数和 onPause()函数之间所经历的是前台生存期。在前台生存期内,活动总是处于运行状态,这时活动可以和用户进行交互。
(1) 事件处理机制
通过活动构建了Android应用的显示界面,接下来需要实现用户与界面之间的交互操作。在界面上的各种交互操作通常定义为各种事件,比如:按下按钮,在屏幕上通过各种滑动来滚动显示或切换界面等等。所有这些操作都通过系统提供的事件处理机制来实现。事件、事件触发以及事件的处理,构成了界面交互的事件模型。
通常将事件源和事件监听器分离开。事件源上发生的特定事件的具体信息,存放在Event对象中,并通过它传递给事件监听器。事件监听器则负责监听事件,对不同的事件做相应的处理,比如监听登录按钮点击事件,实现登录处理逻辑。
在代码实现上,有三种不同的事件处理方式:
(1)采用监听器的方式,界面控件需要绑定一个特定的监听模块;
(2)采用回调方式,需要重写Android已经定义好的回调函数;
(3)采用轮询的事件处理方式,主要通过Handler来实现消息处理。
采用监听器进行事件处理,包括五种实现方法:第一种,设置界面控件属性,并在活动代码中实现相应的方法;第二种,使用Java的匿名类来实现;第三种,用内部类来实现;第四种,所有在活动上发生的事件,不管是在哪个控件上发生的,都由活动来处理;第五种,使用外部类来统一完成事件处理。
- 界面控件属性
在活动QuizActivity的布局文件中设置按钮(UI组件)的android:onClick属性,然后在QuizActivity.java文件中实现对应的处理方法。在layout文件夹下面找到活动QuizActivity对应的布局文件activity_quiz.xml。打开后在标签下面设置onClick属性(按钮触发的压下事件)的处理函数为:“startAnswerActivity”。设置好以后切换到QuizActivity.java文件,添加startAnswerActivit()函数。按钮按下事件的处理函数需要遵循Android的规范:startAnswerActivit()函数必须是公有函数且没有返回值,函数的输入参数类型必须是视图类View。
- Java匿名类
在QuizActivity的onCreate函数中,通过调用按钮checkAnswerBtn的setOnClickListener()函数来设置onClick监听器。onClick监听器通过java匿名类来实现。
- 内部类
采用内部类方式,按钮onClick的监听类位于QuizActivity类的内部。监听器是单独创建的一个类,它需要实现View.OnClickListener接口,所有事件的处理代码都在这个类中实现。
- Activity自身类
在Activity类中对事件进行监听,可以把活动上的所有事件都汇总到一起来处理。Activity类需要实现View.OnClickListener接口,并且各个控件将自己的事件监听器设置(调用setOnClickListener函数)为活动本身。在活动中还需要实现对应的事件处理函数,比如:onClick(),在这个函数中,需要根据控件的id号来判断事件的发生源,然后针对不同的控件进行处理。
- 外部类
创建一个在Activity类外部、专门处理各种事件的监听器类,比如登录界面的所有触发事件就由LoginListener类来处理。外部类要关联到活动和各个控件,因此需要在LoginListener的构造函数中传入当前活动对象以及响应触发事件的各个控件,比如输入用户名和密码的文本框对象。
(2) Intent
Intent,通常翻译为“意图”,简单来说,就是“你想要干什么”。可以把Intent看做是一个动作的完整描述,比如打开一个活动界面就是一个动作。动作包含了操作的发起对象、接收对象、以及在动作执行过程中传递的数据。在QuizActivity上要查看答案,就要把题目传递给答案界面。在前面的例子中已经给出了答题界面,现在点击“查看答案”按钮,打开一个新的界面AnswerActivity,如下图所示。
public class AnswerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_answer);
}
}
给QuizActivity的“查看答案”按钮设置监听器,当点击按钮,通过Intent启动答案活动界面。Intent把测试题活动(QuizActivity)和答案活动(AnswerActivity)联系在了一起。
public class QuizActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
checkAnswerBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(QuizActivity.this, AnswerActivity.class);
startActivity(i); // 启动答案界面
}
});
另外,要使用AnswerActivity还需要在配置文件(AndroidManifest.xml)中进行注册。Activity标签中的name属性要对应活动的类名,即:AnswerActivity。
<activity android:name=".AnswerActivity"
android:launchMode=“standard”/>
注意:Intent构造函数的两个参数:
Intent(Context packageContext, Class<?> cls)
第一个参数 Context 要求提供一个启动活动的上下文。Context是一个类,而Activity是Context类的子类。所有的Activity对象都可以向上转型为Context对象;第二个参数 Class 用来指定想要启动的目标Activity。
在上面的例子中,Intent的构造函数中传入的是活动的全类名(AnswerActivity.class)。用全类名启动组件,这个类要在AndroidManifest.xml 文件中进行配置,并且启动的组件(AnswerActivity)必须是在自己的APP中。采用这种方式的 Intent,称为“显式 Intent”。
除了显示Intent外,Android官方建议使用隐式 Intent。隐式Intent不使用类名,而是通过定义动作“action”来启动。通常在一个APP中,可以启动各种action,因此需要将action定义在Intent的过滤器(intent-filter)中,以便系统找到应用指定的action。
下面来看一个隐式Intent的例子。在全局配置文件中,设置一个过滤器,定义一个动作“MY_ACTION”,它的类别为缺省类别。这个动作将启动MyActivity活动。接下来,在活动中定义Intent,这时只需要把action的字符串名传给它就可以了。
注意:在这个Intent中,并没有指定具体启动哪一个活动,只是指定了一个动作的名称。隐式Intent是通过Android系统来启动活动。Android系统要处理所有应用的隐式Intent,它需要应用程序给出动作、类别等过滤信息,依据这些信息来找到合适的活动。
Intent除了启动同一个应用中的活动外,还可以打开移动设备上其他应用的活动,实现不同应用功能的共享。
(3) 界面布局
在Andriod系统中,所有界面的布局设置都存放在一个专门的xml文件(布局文件)中。布局就像一个可以放置很多控件的容器,控件就摆放在这个容器中。只是不同的布局,提供了不同的摆放方式。在生活中,用到的容器可以一个套一个,布局也可以多层嵌套。在布局文件中摆放好界面控件,接着在活动的创建函数中调用setContentView函数来设置界面的布局,通过这种方式就把界面的表示层(View)与交互(Controller)的逻辑层联系在了一起。
Android系统提供了多种布局方式。随着Android新版本的发布还会提供更多、更灵活的布局方式。下面介绍五种最基本的布局,分别是线性布局(LinearLayout)、相对布局(RelativeLayout)、帧布局(FrameLayout)、表格布局(TableLayout)和网格布局(GridLayout)。
(1)线性布局(LinearLayout)
线性布局按照水平方向或垂直方向依次摆放控件的方式来设置布局。在布局标签()中可以设置布局本身的各个属性,比如“android:orientation”表示布局的方向,以水平摆放为例,android:orientation=“horizontal”,这样控件就会从左到右进行排列。垂直方向设置为"vertical"。LinearLayout默认为水平方向。
<LinearLayout
xmlns:android=http://schemas.android.com/apk/res/android
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
设置布局的宽度(layout_width)和高度(layout_height)为"match_parent",它表示让布局和整个窗口(layout的parent)的大小一致,这样就可以让控件摆放在整个界面空间上。如果 LinearLayout 的排列方向是 horizontal,在水平方向上又要摆放多个控件时,那么就不能将控件的宽度指定为match_parent,因为这样会让一个控件把整个水平方向占满,其他的控件就没有可放置的位置。同样,如果 LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
android:layout_weight属性值指定为 1,表示EditText 和 Button 将在水平方向平分宽度。系统会先把 LinearLayout 下所有控件指定的 layout_weight 值相加,得到一个总值,然后每个控件所占大小的比例就用该控件的 layout_weight 值除以刚才算出的总值。比如:如果想让 EditText 占据屏幕宽度的 3/5,Button 占据屏幕宽度的 2/5,只需要将 EditText 的layout_weight 改成 3,Button 的 layout_weight 改成 2 就可以了。
为了把控件摆放整齐,Android提供了对齐属性,主要有两种对齐方式,一种是在布局上各个控件的对齐(android:layout_gravity);另一种是在控件上文字的对齐(android:gravity)。
在控件对齐时要注意控件的排列方向,比如:当LinearLayout 的排列方向是 horizontal 时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样,当 LinearLayout 的排列方向是 vertical 时,只有水平方向上的对齐方式才会生效。
(2)相对布局(RelativeLayout)
如果采用相对布局来设置控件的对齐方式,需要有一个参考点,布局的时候都是相对于它来完成控件的摆放。相对布局的参考点就是布局本身。
android:layout_above属性让一个控件位于另一个控件的上方,android:layout_toLeftOf表示让一个控件位于另一个控件的左边。android:layout_below属性让一个控件位于另一个控件的下方,android:layout_toRightOf表示让一个控件位于另一个控件的右边。
(3)帧布局(FlameLayout)
帧布局比较特别。在它上面的控件都会重叠在左上角。它是一种层叠式布局,可以通过布局属性(layout_gravity)来指定它们的对齐方式。所有的控件默认摆放在布局的左上角。FrameLayout内的控件一层覆盖一层。
(4)表格布局(TableLayout)
表格布局把整个布局空间按表格进行划分。表格的行用标签来定义。TableRow表示表格内的行,行内每一个元素算作一列。每加入一个TableRow就表示在表格中添加了一行,在TableRow中每加入一个控件,就表示在该行中加入了一列。注意TableRow中的控件是不能指定宽度的。
因为在TableRow中无法指定控件的宽度,如果想调整控件的宽度,可以设置android:stretchColumns属性来解决这个问题。android:stretchColumns允许将TableLayout中的某一列进行拉伸,以达到自动适应屏幕宽度的作用。这里将 android:stretchColumns 的值指定为 1,表示如果表格不能完全占满屏幕宽度就将第二列进行拉伸。指定成 1 就是拉伸第二列,指定成 0 就是拉伸第一列。
(5)网格布局(GridLayout)
网格布局使用线条将布局空间划分为行、列和单元格。每个格子都可以放置控件。GridLayout将容器划分为rows × columns 个网格,每个网格放置一个控件,并且还可以设置一个控件横跨多列,纵跨多行。在网格上如果跨越单元格,就会在行和列上形成交错的排列样式,比如计算器中“0”、“=”和“+”的排列。
设置网格布局,先要确定有多少个单元格,在这里设置了20个格子,5行、4列。把“0”这个按钮扩展两列,并让它填满两个格子。
**(5)**常用界面控件
1) 列表控件
现有的移动应用,大多采用列表控件(ListView)来展示多个条目。ListView的主要功能就是用来展示各类列表。首先实现一种最简单的列表方式,即列表中只显示文本,如下图所示,把测试题目的类型展示在界面上。在布局文件中,需要设置ListView控件的id、宽度和高度等属性。
首先,需要构造要展示的列表数据,可以用一个字符串数组来存储题目类型。如果要展示的数据很多,那就需要先将数据准备好,这些数据可以从网上下载,也可以从数据库中读取,具体视应用程序的场景来决定。
有了数据以后,需要用适配器对象把数据和界面视图联系起来。在适配器的构造函数中传入数据。题目类型数据都是字符串,因此将适配器ArrayAdapter的泛型指定为 String类型;然后在 ArrayAdapter 的构造函数中依次传入上下文(Context)、ListView 子项布局的 id,以及要适配的数据。注意第二个参数使用了android.R.layout.simple_list_item_1 作为 ListView 子项(即列表的每一行)布局的 id,这是一个 Android 内置的布局文件,里面只有一个 TextView,可用于显示一段文本。
在QuizTypesActivity的onCreate函数中,将布局文件activity_quiz_type_list.xml中ListView的id“R.id.list_view”传给findViewById函数。通过它构造ListView控件对象。注意:findViewById函数使用R文件来引用控件的id。最后,利用setAdapter函数将适配器和listview对象连接起来,完成整个列表控件的构造。
在适配器中,可以设置不同的列表项布局,以展示不同的列表效果。在Android系统中已经定义了常用的列表显示效果。android.R.layout.simple_list_item_1和android.R.layout. simple_list_item_2,这两个列表项布局主要用来显示文本。以下三种显示效果分别对应多选框和单选按钮。
- simple_list_item_1 : 单独一行的文本框
- simple_list_item_2 : 由两个文本框组成
- simple_list_item_checked : 每一行都有一个已选中的列表项
- simple_list_item_multiple_choice : 都带有一个复选框
- simple_list_item_single_choice : 都带有一个单选按钮
Android提供RecyclerView来代替ListView,RecyclerView的灵活性比ListView更好,并且能够在有限的窗口中展示大量数据集。下面创建一个新的活动:知识点活动(KnowledgePointsActivity),它把课程的各个知识点用RecyclerView控件展示出来。
RecyclerView定义在Android系统的支持(support) 库中,要使用它,需要在build.gradle文件中加入以下这段代码(即设置对应的依赖库):
dependencies {
… …
**compile 'com.android.support:recyclerview-v7:24.2.1'**
compile 'com.android.support:appcompat-v7:24.2.1'
}
在KnowledgePointsActivity的布局文件activity_knowledge_points.xml中放置RecyclerView控件,设置它的相关属性。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/kps_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:dividerHeight="5dp"
/>
</LinearLayout>
和ListView一样,先构造数据,再用findViewById函数得到RecyclerView的实例;接下来需要给RecyclerView设置布局管理器。设置不同的布局管理器会产生不同的显示效果。最后,设置RecyclerView的适配器,把数据传给定制的知识点适配器。
public class KnowledgePointsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_knowledge_points);
KnowledgePoints knowledgePoints = new KnowledgePoints();
// get方法获取知识点数组
List<KnowledgePoints.KnowledgePoint> knowledgePointList =
knowledgePoints.getKnowledgePointList();
RecyclerView recyclerView = (RecyclerView) findViewById(
R.id.kps_recycler_view);
// 使用线性布局
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
KnowledgePointsAdapter adapter =new KnowledgePointsAdapter(
knowledgePointList);
recyclerView.setAdapter(adapter);
}
}
自定义的适配器KnowledgePointsAdapter继承自RecyclerView.Adapter。RecyclerView中已经定义了ViewHolder类,需要定义一个新的静态类ViewHolder来继承它。在自定义的ViewHolder类中,获取这两个控件(ImageView和TextView控件)的实例。
public class KnowledgePointsAdapter extends
RecyclerView.Adapter<KnowledgePointsAdapter.ViewHolder> {
static class ViewHolder extends RecyclerView.ViewHolder {
// 加入知识点的图片控件和文本控件
ImageView knowledgePointIcon;
TextView knowledgePointTitle;
public ViewHolder(View view) {
super(view);
knowledgePointIcon = (ImageView)
view.findViewById(R.id.knowledge_point_icon);
knowledgePointTitle = (TextView)
view.findViewById(R.id.knowledge_point_title);
}
}
在KnowledgePointsAdapter中加入需要关联的数据,这里是知识点数组。另外,还要重写适配器的getItemCount函数,返回知识点数组的长度。
private List<KnowledgePoints.KnowledgePoint> knowledgePointList;
public KnowledgePointsAdapter(
List<KnowledgePoints.KnowledgePoint> knowledgePointList) {
this.knowledgePointList = knowledgePointList;
}
@Override
public int getItemCount() {
return knowledgePointList.size();
}
重写KnowledgePointsAdapter 类的onCreateViewHolder函数,加载布局(这是每一个列表项的布局)。最后创建ViewHolder对象。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 设置单个列表项(Item)的布局
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.knowledge_point_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
在KnowledgePointsAdapter 类的onBindViewHolder函数中,可以根据position获取当前选中的列表项。如果用鼠标点击列表项,通过position就能够知道当前点击的是哪一个知识点。接下来,获取知识点的图片Id号和标题文本,把它们显示在控件上。
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 通过位置参数得到对应的知识点对象。
KnowledgePoints.KnowledgePoint kpoint = knowledgePointList.get(position);
holder.knowledgePointIcon.setImageResource(kpoint.getImageID());
holder.knowledgePointTitle.setText(kpoint.getCaption());
}
列表项的布局由图片控件和文本控件(ImageView和TextView)按照线性布局的方式在水平方向上摆放。布局文件的定义也可以根据自己的需要进行调整。
现在再增加点击事件的处理。为了响应点击事件,需要在ViewHolder中加入视图对象kpointView。通过kpointView监听点击事件,它能够区分当前是点击在列表项的哪一个控件上:是图片控件还是文本控件。
static class ViewHolder extends RecyclerView.ViewHolder {
View kpointView; // 新增成员变量
ImageView knowledgePointIcon;
TextView knowledgePointTitle;
public ViewHolder(View view) {
super(view);
kpointView = view;
}
}
在列表kpointView上设置监听器,在onClick函数中得到当前点击的位置,然后实现点击事件处理。在程序中,点击事件用Toast来做演示。在点击后,通过获取当前列表项的信息,用Toast显示当前选中了哪一个知识点。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ViewHolder holder = new ViewHolder(view);
// 设置监听器
holder.kpointView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
KnowledgePoints.KnowledgePoint kpoint = knowledgePointList.get(position);
Toast.makeText(v.getContext(), "知识点:" +
kpoint.getCaption(), Toast.LENGTH_SHORT).show();
}});
return holder;
}
2) 视图翻页ViewPager
在手机、平板等移动设备上经常通过滑动的方式来切换应用界面。Android的视图翻页工具ViewPager提供了多页面的切换功能。ViewPager控件在android-support-v4.jar包中。android-support-v4.jar 是Google提供的兼容低版本Android设备的软件包。现在使用Android Studio开发时,系统默认导入v7包,v7包含了v4。ViewPager常用于应用启动界面的演示,上下方导航条,图片滑动翻页,软件使用向导以及实现广告轮播等功能。
ViewPager类直接继承自ViewGroup类,它相当于一个页面容器,容器中装入多个View作为页面,也可以装入Fragment作为页面。ViewPager需要PagerAdapter适配器来连接显示数据。如果在ViewPager中使用Fragment,则需要使用专门的FragmentPagerAdapter和FragmentStatePagerAdapter类来处理页面。Google官方建议使用Fragment来填充ViewPager,以方便生成和管理每个Page的生命周期。
在使用时,首先在Layout文件中加入一个 ViewPager 控件;然后在Activity(或Fragment等)中获取 ViewPager 引用;接下来通过设置ViewPager的适配器填充显示页面;最后,为ViewPager设置监听器和滑动特效。
创建PagersActivity的布局文件activity_pagers.xml,在其中设置ViewPager控件。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context="pers.cnzdy.tutorial.Pagers.PagersActivity" >
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</RelativeLayout>
在示例中,创建三个View作为滑动页面,它们的布局文件分别是:quiz_pager1.xml、quiz_pager2.xml和quiz_pager2.xml。这些布局文件的结构一致,都包含一个TextView控件。
quiz_pager1.xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<TextView
android:id="@+id/quiz_view_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="简述Activity和Service的生命周期?"
android:textSize="40sp" />
</LinearLayout>
接下来,在PagersActivity的onCreate函数中初始化ViewPager,并添加滑动切换的多个视图。
public class PagersActivity extends AppCompatActivity {
private View viewQuiz1, viewQuiz2, viewQuiz3;
private ViewPager viewPager;
private List<View> viewList; // 保存多个切换页面
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pagers);
viewPager = (ViewPager) findViewById(R.id.view_pager);
LayoutInflater inflater=getLayoutInflater();
viewQuiz1 = inflater.inflate(R.layout.quiz_pager1, null);
viewQuiz2 = inflater.inflate(R.layout.quiz_pager1,null);
viewQuiz3 = inflater.inflate(R.layout.quiz_pager1, null);
// 将要分页显示的View装入数组
viewList = new ArrayList<View>();
viewList.add(viewQuiz1);
viewList.add(viewQuiz2);
viewList.add(viewQuiz3);
}
和ListView控件一样,通过设置PagerAdapter来完成ViewPager中页面和数据的绑定。
public class PagersActivity extends AppCompatActivity {
… …
PagerAdapter pagerAdapter = new PagerAdapter() {
String[] titles = new String[]{ "第一道题", "第二道题", "第三道题" };
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public int getCount() {
return viewList.size();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(viewList.get(position));
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
container.addView(viewList.get(position));
return viewList.get(position);
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
};
viewPager.setAdapter(pagerAdapter);
}
PagerAdapter是一个通用的ViewPager适配器,同时也是一个基类适配器。Fragment的适配器是PagerAdapter的子类,包括:FragmentPagerAdapter和FragmentStatePagerAdapter。FragmentPagerAdapter类在内存中保存每一个生成的 Fragment,通常用于静态页面,适用于页面比较少的情况;而当页面比较多,数据动态变动较大,需要占用较多内存的时候,通常使用FragmentStatePagerAdapter。“State”表示适配器只保留当前页面,当页面切换出屏幕时,就会被回收,并释放资源;当页面需要显示时,将生成新的页面。采用这种方式,ViewPager就能够拥有很多的页面,并且不会占用大量的内存。
3) 界面模块****Fragment
Android系统不仅能用于手机,很多其他的移动设备也采用它作为操作系统,比如很多平板电脑也使用Android系统。如果开发应用程序只考虑在手机上操作,完全忽视其他移动设备,应用程序的适用面就太狭窄了。
在开发移动应用程序时,要兼顾手机和平板来设计移动应用界面。首先分析一下手机和平板的应用界面,整个界面包含两个部分:一个部分展示知识点的标题,另一个部分展示选定知识点的详细内容。为了适应手机和平板不同大小的屏幕,把这两部分分别做成单独的组件模块,在Android系统中称为“碎片”(Fragment)。每一个碎片就像一个积木块,它们可以单独使用,互不相干,也可以组装在一起显示在一个界面上。每一个碎片和活动类似,都有自己的生命周期,也可以把碎片看成简化版的活动。但是碎片不能像活动一样独自存在,它需要嵌入到活动中。如果活动销毁了,它上面的碎片也就不存在了。另外,可以在不同的活动中重用同一个碎片,以提高开发效率。手机和平板兼容的开发方式就采用碎片来实现。
在活动上有两种添加碎片的方式:一种是静态方式,另一种是动态方式。使用静态的方式,把碎片当做普通的控件,就像Button、ListView等控件一样,需要在活动的布局文件中设置它的属性。注意,碎片的名字属性(android:name)要设置为创建碎片的类名,而且是加入包前缀的全称。
<LinearLayout
… …
<fragment
android:id="@+id/**my_fragment**"
<!-- Fragment加入包前缀的全称 -->
android:name="pers.cnzdy.tutorial.MyFragment"
android:layout_width="0dp"
android:layout_height="match_parent“
/>
现在来构造MyFragment碎片。创建MyFragment的布局文件my_fragment_layout.xml,并且在上面加入一个文本控件:
<LinearLayout
<TextView
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_gravity=“center_horizontal”
android:textSize=“20sp”
android:text=“这是我的碎片"
/>
接下来编写定制的碎片类,重写onCreateView方法,在onCreateView方法中,加载布局,完成碎片的构造。
public class MyFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
// 加载碎片的布局,id为my_fragment
View view = inflater.inflate(R.layout.my_fragment, container, false);
return view;
}
}
采用动态方式来加载布局,需要用代码在活动中添加碎片,而不是在活动的布局文件中设置。首先,构造dynamic_fragment_layout.xml布局文件,实例化一个定制的碎片(DynamicFragment类),获取碎片管理器(FragmentManager)对象;通过碎片管理器将原来的碎片替换掉。
// 创建动态加载的碎片。
DynamicFragment fragment = new DynamicFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 替换原来的碎片。
transaction.replace(R.id.my_fragment, fragment);
transaction.commit();
动态创建碎片的过程一共有五个步骤:首先,创建碎片实例,然后获取碎片管理器对象,通过碎片管理器开启事务,事务的工作就是在活动中加入碎片。活动对于碎片来说,就像一个容器,既可以加入,也可以把已有的碎片替换掉。最后,提交事务,完成碎片的动态构造。
碎片与活动之间的通信可以通过获取对象的方式来实现。当活动与碎片进行交互,活动使用getFragmentManager函数获得碎片对象;碎片则使用getActivity函数来得到自身所在的活动。碎片与碎片之间通信,需要把这两个步骤结合在一起,先在一个碎片中得到它归属的活动,然后再通过这个活动去获得另外一个碎片对象,这样就可以实现两个碎片之间的通信。
// 活动调用getFragmentManager函数,根据碎片的id获取碎片实例
Fragment mFragment = (Fragment) getFragmentManager()
.findFragmentById(R.id.right_fragment);
// 碎片调用getActivity() 获取活动
MainActivity activity = (MainActivity) getActivity();
碎片的生命周期与活动类似,如下图所示。当碎片和活动建立关联的时候,需要调用onAttach()函数。碎片在创建视图(也就是加载布局)的时候,要调用onCreateView()函数。与碎片相关联的活动已经创建完毕以后,调用onActivityCreated()函数。
类似于活动的生命周期,onDestroyView()函数对应onCreateView()函数,调用onDestroyView函数将移除与碎片关联的视图。碎片和活动解除关联,调用onDetach()函数。在碎片的生命周期中,同样有运行状态、暂停状态、停止状态和销毁状态。
在整个生命周期中,碎片依附于活动而存在。碎片在运行状态是可见的,并且它所归属的活动也正在运行。当活动进入暂停状态(由于另一个未占满屏幕的活动被添加到了栈顶),它上面的碎片也会进入暂停状态。当活动停止,它上面的碎片就进入停止状态。进入停止状态的碎片,用户看不见,有可能会被系统回收。由于碎片依附于活动,当活动被销毁,它上面的碎片也会被销毁。
4) 滑动标签
滑动标签也称为导航栏功能,Android提供了多种实现导航栏功能的方式。第一种,是谷歌在Material Design库中为开发者提供了BottomNavigationBar来实现底部导航栏。第二种,可以用RadioGroup控件和Fragment控件组合来实现。可以在RadioGroup中加入多个RadioButton,用来实现切换功能;切换的内容通过Fragment来显示。第三种,采用TextView+LinearLayout+Fragment的组合方式来实现导航栏。底部放置LinearLayout,在它的里面水平放置了4个TextView,其余用FrameLayout填充满剩余空间,点击底部的tab,用4个Fragement替换FrameLayout。第四种,通过TabLayout、ViewPager和Fragment 控件来实现导航栏。下面主要介绍最后这种实现方式。在使用首先TabLayout之前,需要在app/build.gradle 文件中,加入依赖包。
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0’
implementation 'com.android.support:design:28.0.0’
}
接下来,在布局文件中,首先加入ViewPager控件,然后再添加TabLayout布局,分别设置它的id、宽度、高度,以及显示风格。
接下来,为TabLayout添加Tab,并且给标签设置文字和图片。首先,创建一个底部导航栏的布局文件:tab_custom.xml;在这个布局文件中,要显示每个标签的图标和文字说明,因此,在线性布局中加入ImageView和TextView两个控件。为了在切换标签时,能动态改变标签的字体颜色,给textColor属性加入了文字选择器tab_txt_selector。而图标的切换变化,是通过代码来实现。
在资源路径drawable下面添加一个文字选择器xml文件,在selector标签中,加入两个item标签,当选中状态为“真”时,设置为文本选中颜色(设置为粉色),当选中状态为“假”时,设置为正常状态颜色(设置为更浅的粉色)。
创建一个SlidingActivity活动,定义4个标签的标题,以及它们的对应图标,每个图标不是一个单一的图片,而是一个图标选择器。以日记图标选择器为例,
public class SlidingActivity extends AppCompatActivity {
private TabLayout tabLayout;
private final int[] tabTitles = new int[]{
R.string.*diary*, R.string.*quiz*,
R.string.*music*, R.string.*me*};
private final int[] tabImgs = new int[]{
R.drawable.*tab_diary_selector*,
R.drawable.*tab_quiz_selector*,
R.drawable.*tab_music_selector*,
R.drawable.*tab_me_selector*};
*// Fragment**数组
\* private final Fragment[] fragments = new Fragment[] {
new DiaryFragment(),
new QuizFragment(),
new MusicFragment(),
new MeFragment()};
*//* *标签的数目
\* private final int count = tabTitles.length;
private TabViewPagerAdapter adapter;
private ViewPager viewPager;
}
SlidingActivity的onCreate函数中调用initTabViews函数,通过它来初始化标签页面。在初始化时,首先设置tablayout对象 ,给TabLayout布局添加标签;然后,给ViewPager控件设置适配器,并且添加页面切换监听器,当页面改变时,能通知tablayout对象。最后,tabLayout对象也加入标签选中监听器,当选中标签时,通知viewPager对象,切换当前显示的页面。
5) WebView****控件
WebView是一个特殊的视图,同时它也是一个ViewGroup可以包含其他子视图。作为Web渲染引擎的包装器,在Android 4.4以下,WebView的内核是WebKit;在Android 4.4及其以上,则采用chromium内核。WebKit 是一个渲染引擎库,用于在视图和窗口中呈现网页。
在Activity中,定义webView对象,在onCreate函数中调用它的setWebViewClient函数,在重载函数中,调用WebView的loadUrl函数载入要访问的网址。在这里载入百度的首页。WebViewClient类用来辅助WebView处理各种事件、通知。
public class WebViewActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
… …
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(
WebView view, String url) {
view.loadUrl(url);
return true;
}
});
webView.loadUrl("http://www.baidu.com");
}
在调用JavaScript函数之前,需要对webView进行设置,调用setJavaScriptEnabled函数,确保可以使用JavaScript代码。
@Override
protected void onCreate(Bundle savedInstanceState) {
… …
webView = findViewById(R.id.*web_view*);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
点击按钮调用javascript函数,首先,如果Android的sdk版本大于4.4,则调用evaluateJavascript函数调用html中的JavaScript函数,并传递两个参数,当JavaScript函数返回结果时,WebView将回调onReceiveValue函数,通过toast来显示执行结果。如果Android的sdk版本小于4.4,则直接调用webview的loadUrl函数来调用JavaScript函数。
public void callJavaScriptMethodWithAndroid (View view){
if (Build.VERSION.*SDK_INT* >= Build.VERSION_CODES.*KITKAT*) {
webView.evaluateJavascript("javascript:sum(56, 17)",
new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Toast.*makeText*(WebViewActivity.this,
"返回值=" + value,
Toast.*LENGTH_LONG*).show();
}
});
}
else
webView.loadUrl("javascript:sum(25, 18)");
}
难点
(1) Activity****启动模式
Android提供了四种不同的启动模式来管理堆栈中的活动,实现了活动的重用和共享。这四种模式分别是:standard模式、singleTop模式、singleTask模式和singleInstance模式。
(1) standard启动模式
standard启动模式是系统的默认模式。每次调用startActivity()函数,都会创建一个新的活动放在栈顶。如果启动同一个活动,活动将被重复创建,并置于栈顶;如果要退出程序,需要连续点击Back键才能退出。这种模式的缺点是浪费内存。
(2) singleTop启动模式
采用singleTop启动模式,能够部分消除创建重复活动的问题。在该模式下,启动活动A时,首先判断栈顶是不是已经有了当前要启动的活动;如果没有,系统就新建一个活动A的实例,并放到栈顶上面;如果栈顶有要启动的活动,就继续使用原来创建的实例,也就是说采用singleTop模式,在栈顶不会有两个相同的活动。如果某个活动已经在栈顶,那么再次跳转会直接使用原来那个活动而不会重新创建一个同样的活动,这样就减少了内存的浪费。
虽然使用singleTop启动模式,在重复启动栈顶活动时,可以减少内存的浪费。但是,如果活动不在栈顶,又重复启动活动,返回栈仍然会存在重复的实例,那么有没有办法让系统中只有一个活动实例?Android通过singleTask启动模式来解决这个问题。
(3) singleTask启动模式
采用singleTask模式的执行方式,启动活动A时,首先判断堆栈中是否已经存在活动A的实例,如果没有活动A就会创建它。如果有活动A存在,那么就把活动A上面的所有其他活动都弹出堆栈,这样活动A就处于栈顶位置了。这时在堆栈中只有一个活动A的实例存在。从上述的执行过程可以看出,如果某个活动采用singleTask模式,那么在任务(Task)栈中将只有一个该活动的实例。
(4) singleInstance启动模式
采用前面三种启动模式无法实现跨应用的活动共享,因为每个应用程序都有自己的返回栈,它们启动活动A的时候,将会在不同的返回栈中创建多个A的实例。为了在不同任务之间复用活动,Andriod提供了singleInstance启动模式来解决这一问题。如果活动A使用singleInstance启动模式,系统就会在首次启动活动A时,创建一个新的栈来存放它,并且保证不会将其他活动实例放入这个堆栈;如果活动A已经存在,无论它位于哪个应用程序,哪个Task中,系统都会把活动A所在的Task转到前台,从而让活动A显示在屏幕上。总之,采用singleInstance启动模式,无论从哪个任务启动目标活动,都只会创建一个活动实例。
(2) 事件的回调模型
Android系统在控件的内部已经定义了事件处理的回调函数。每个View中都有处理事件的回调函数。通过重写View中的这些回调函数就可以响应特定的事件,比如重写onKeyDown、onKeyUp、onTouchEvent等回调函数。在下面的例子中,为了处理触摸事件, AnswerButton按钮继承Android系统提供的按钮类(AppCompatButton),重写了onTouchEvent回调函数。
在基于回调的事件处理模型中,事件源和事件监听器是统一的,因此看不到事件监听器。当用户在控件上触发某个事件时(监听事件),控件(事件源)自身的特定函数将会负责处理该事件。采用回调方式就是把事件监听器放置在事件源上,比如在上面的例子中,按钮类就实现了对触摸事件的处理。用户触发事件所产生的信息由事件(event对象)表示,它包含事件编码和事件本身携带的信息。开发者可以在控件的回调函数中获取event对象,根据事件信息完成对事件的处理。
(3) IntentFilter****的过滤方式
Android系统收到应用发出的Intent,它将Intent进行过滤。过滤的方式就是与所有应用程序定义的过滤器(Intent-filter)进行比较,如果Intent和过滤器匹配,就启动组件,并且把Intent告诉这个组件。如果Intent与多个组件都匹配成功,Android系统就会在对话框中显示所有匹配的组件,让用户去选择启动哪个组件。
Intent的各种属性用于描述过滤器匹配的各种条件,包括:action、category、data、type、extras和flags。
- action(动作)
action用来表现意图的动作,它是最关键的匹配条件,因此在Intent中必须定义action。Intent设置的action属性就是一个字符串标记,用来告知Android系统自己的行为;通过Android系统应用组件就能找到执行者完成相应的动作。
上面是一个最常见的活动定义,这里定义的动作Android.Intent.action.MAIN决定了应用程序最先启动的活动,也就是MainActivity。注意:action的字符串区分大小写。下面一行标签表示应用程序将显示在程序列表里,也就是在Android桌面上会显示一个图标(launcher)。如果两个组件的过滤器(Intent-filter)都添加了这个属性,那么应用将会显示两个图标。
一个过滤器可以包含多个动作,这时节点指定了一个 action 列表用于标识Activity所能接受的“动作”。通常Intent中的动作必须和过滤规则中的动作完全一致才能匹配成功;当过滤器有多个动作时,Intent中的动作只要和其中一个相同就可以匹配成功。
- category(类别)
category用来表示动作的类别,类别对动作进行归类。类别标签包含的类别信息能更精确地指定响应的 Intent 组件。只有当动作和类别同时匹配时,活动才能响应 Intent。所以类别越多,动作就越具体,意图也就越明确。在下面的例子中,MY_ACTION动作定义了两个类别,一个是自定义类别,另一个是缺省类别。
<Intent-filter>
<action android:name="pers.cnzdy.tutorial.MY_ACTION"></action>
<category android:name="pers.cnzdy.tutorial.MY_CATEGORY"></category>
<category android:name="android.Intent.category.DEFAULT"></category>
</Intent-filter>
通过隐式Intent方式激活Activity的时候,如果没有类别,须加上默认类别。Intent中可以不设置category,如果这时使用startActivity或者startActivityForResult,系统会自动添加默认的category。
除了在配置文件中设置类别,还可以用代码给Intent增加类别。首先,创建Intent对象,设置相应的动作,然后再设置类别。
// 用代码增加类别
Intent Intent = new Intent();
Intent.setAction("pers.cnzdy.tutorial.MY_ACTION");
Intent.addCategory("pers.cnzdy.tutorial.MY_CATEGORY");
如果在配置文件中使用了DEFAULT这个默认的类别,在调用startActivity()函数的时候会自动将这个类别添加到Intent中。
- data(数据)
data表示动作要操作的数据。数据作为动作操控的对象,当action + data属性组合在一起,它们描述了意图:“做什么”。在动作上加入数据,表示要完成动作的内容。例如:如果想要发送邮件,首先通过URI来解析邮件地址字符串,因为Intent需要使用URI对象,所以数据采用URI来表示;接下来创建Intent,它的动作是ACTION_SENDTO。最后,启动邮件活动。
// 数据用uri对象表示,uri代表邮件地址
Uri uri = Uri.parse(“mailto:xxx@126.com”);
Intent Intent = new Intent(Intent.ACTION_SENDTO, uri);
// 多个组件匹配成功则显示优先级最高的,如果优先级相同则显示组件列表。
startActivity(Intent.createChooser(Intent, “选择邮件客户端”));
createChooser函数的作用是:显示一个活动选择窗口,让用户可以选择打开哪个邮件应用程序。
数据元素也可以通过一些属性来设置。在匹配规则中,data的scheme,host,port,path等属性可以写在同一个< />中,也可以分开单独写,其功效是一样的。
- scheme协议部分,比如:http 协议。
- host主机名,比如:www.baidu.com 。
- port数据的端口号,比如:80端口。一般紧跟在主机名后面。
- path主机名和端口后面的部分,表示完整的路径。
- type可以处理的数据类型,比如:图片、视频等媒体类型,它允许以使用通配符的方式来指定。
- type(数据类型)
type是对data类型的描写,并依附于数据。type属性用于明确指定data属性的数据类型,比如:android:mimeType=“video/mpeg”,android:mimeType=“image/*” 等。通常Android系统会根据data属性值来分析数据的类型,所以一般不需要指定type属性。当Intent不指定data属性时,type属性才会起作用。如果Intent对象中既包含Uri又包含type,则在中必须二者都包含才能通过测试。
- extras(扩展信息)
扩展信息是数据以外的其他信息。使用extras可以为组件提供扩展信息,比如如果要执行“发送电子邮件”这个动作,可以将电子邮件的标题、正文等保存在extras中,然后传给电子邮件发送组件。
- flags(标志位)
flags表示期望这个意图的运行模式。通过设置Intent中的flag和AndroidMainifest.xml中的Activity元素的属性,可以控制Task中Activity的关联关系和行为。
Intent有时需要在不同的活动间传递数据。当调用startActivity()函数时,可以传递一些必要的数据给Activity,比如点击测试题界面的“查看答案”按钮,把当前的题目传递给答案活动。采用Intent提供的信息传递机制,首先将要传递的数据放入Intent,每个数据给定一个键值(“input_data”),这个键值对应要传递的信息。
// 键值:“input_data”,传递信息:data
Intent.putExtra(“input_data”, data);
// setData()函数也是一种设置数据的方式。
Intent.setData(Uri.parse("http://www.baidu.com"));
Intent.setData(Uri.parse("tel:10086"));
信息随着Intent传递给要启动的活动,收到Intent的活动通过getIntent()函数获取传递过来的Intent。从Intent中取出数据时,需要根据数据的类型调用相应的get函数,比如使用getStringExtra()函数来获取字符串类型数据,函数的参数是对应数据的键值。
Intent Intent = getIntent();
String data = Intent.getStringExtra(“input_data”);
主界面(MainActivity)通过Intent打开某个子活动界面,当子活动代码执行完再次返回主界面,可以获取子活动中的数据。作为数据的接收方,主界面在启动子活动时需要使用startActivityForResult()函数。子活动作为发送方,需要将数据放入Intent,然后调用setResult()函数把子活动想要返回的数据返回到MainActivity。setResult函数的第一个参数表示结果返回码,第二个参数是Intent对象,它携带了要返回的数据。
Intent Intent = new Intent(MainActivity.this, MyActivity.class);
startActivityForResult(Intent, 1); // 启动新的活动,并且需要获取它的返回结果
Intent Intent = new Intent();
Intent.putExtra(“data_return”, “返回的信息");
setResult(RESULT_OK, Intent);
finish();
(4) 绘图
Android的绘图应继承View组件,并重写它的onDraw(Canvas canvas)方法。Canvas代表了“依附”于指定View的画布。Android的Canvas不仅可以绘制简单的几何图形,还可以直接将一个Bitmap绘制到画布上。
创建了一个自定义的绘图控件:DrawingView类,通过它来绘制图形;并且在活动的布局文件中添加这个自定义的DrawingView控件。DrawingView继承视图类,需要重载onDraw函数。onDraw函数的输入参数就是画布对象,调用它的绘制函数,可以完成各种图形绘制操作。首先,将画布设为白色。
public class DrawingView extends View {
public DrawingView(Context context, AttributeSet set) {
super(context, set);
}
@Override
*//* *绘图
\* protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
*//* *画布绘制成白色
\* canvas.drawColor(Color.*WHITE*);
Paint paint = new Paint();
*//* *去锯齿
\* paint.setAntiAlias(true);
paint.setColor(Color.*MAGENTA*);
paint.setStyle(Paint.Style.*STROKE*);
paint.setStrokeWidth(10);
canvas.drawRect(left, top, width, height, paint);
canvas.drawCircle(left * 2 + width + radius, top + radius,
radius, paint);
int rLeft = left * 4 + width * 2;
RectF roundRect = new RectF(rLeft, top,
rLeft + width, top + height);
canvas.drawRoundRect(roundRect, 20, 20, paint);
*//* *绘制椭圆
\* int oLeft = left * 6 + width * 3;
RectF ovalRect = new RectF(oLeft, top, oLeft + width + 60,
top + height - 20);
canvas.drawOval(ovalRect, paint);
Paint表示画布上Canvas的画笔,定义不同的Paint对象,可以设置各种绘制风格,这里设置绘制去锯齿,然后定义了画笔的颜色、填充风格和画笔的粗细。调用各种draw函数在画布上绘制矩形、圆形、圆角矩形和椭圆。注意绘制各种图形需要设置的各个参数。比如,矩形需要设置的参数有:左上角的坐标、长度、宽度和画笔;圆形还包括圆的半径;圆角矩形还要设置圆角半径。另外,也可以通过RectF对象来设置绘图对象的位置和大小。
双缓冲技术是指当程序在指定视图上绘制图形时,程序并不直接绘制在该视图控件组件上,而是先绘制到一个内存中的Bitmap图像上,这就是绘图的缓冲;然后,等到内存中的Bitmap绘制完以后,再一次性地将Bitmap绘制(拷贝到)在视图组件上。通过这种方式,创建一个视图,用户可以通过移动手或鼠标在视图上绘制任意的图形。
public class DoubleBufferingActivity extends AppCompatActivity {
BlurMaskFilter blur;
DoubleBufferingView doubleBufferingView;
@Override
*//* *负责创建选项菜单
\* public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflator = new MenuInflater(this);
*//* *装载menu_double_buffering**菜单
\* inflator.inflate(R.menu.*menu_double_buffering*, menu);
return super.onCreateOptionsMenu(menu);
}
创建一个双缓冲活动,定义了一个模糊遮罩滤镜BlurMaskFilter(实现阴影效果)和一个双缓冲视图对象。然后,创建选项菜单,让用户可以选择各种绘制参数。在活动的onCreate函数中,首先,创建DisplayMetrics对象,通过它来获取与屏幕相关的信息。在创建双缓冲视图是,需要获取屏幕的像素宽度和高度,它们的单位是px(像素)。接着,在布局文件中加载双缓冲视图,并且设置模糊遮罩滤镜。
@Override
public void onCreate(Bundle savedInstanceState) {
… …
LinearLayout linearLayout = new LinearLayout(this);
DisplayMetrics displayMetrics = new DisplayMetrics(); *
\* getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
*
\* doubleBufferingView = new DoubleBufferingView(
this, displayMetrics.widthPixels,
displayMetrics.heightPixels);
linearLayout.addView(doubleBufferingView);
setContentView(linearLayout);
blur = new BlurMaskFilter(10, BlurMaskFilter.Blur.*NORMAL*);
}
双缓冲视图类,除了定义画笔以外,定义了一个Bitmap对象作为缓冲区,另外还定义了拖动事件发生点的位置坐标,路径,画布等对象。在构造函数中,完成视图的初始化,创建与View相同大小的缓存区和缓冲绘制画布,并且通过setBitmap函数将画布和缓冲区联系在一起。接着,设置画笔的各种属性,包括:颜色、风格等。
public class DoubleBufferingView extends View
{
private static final String *TAG* = "DoubleBufferingView";
public Paint paint = null;
*//* *定义记录前一个拖动事件发生点的坐标
\* private float preX;
private float preY;
private Path path;
*//* *定义一个内存中的图片,该图片将作为缓冲区
\* Bitmap bufferBitmap = null;
*//* *定义bufferBitmap**上的Canvas**对象
\* Canvas bufferCanvas = null;
public DoubleBufferingView(Context context, int width, int height) {
super(context);
bufferBitmap = Bitmap.*createBitmap*(width, height,
Bitmap.Config.*ARGB_8888*);
bufferCanvas = new Canvas();
path = new Path();
bufferCanvas.setBitmap(bufferBitmap);
*
\* paint = new Paint(Paint.*DITHER_FLAG*);
paint.setColor(Color.*BLACK*); *
\* paint.setStyle(Paint.Style.*STROKE*);
paint.setStrokeWidth(1); *
\* paint.setAntiAlias(true);
paint.setDither(true);
}
自定义View来绘图,但View的绘图机制存在两个问题,第一View缺乏双缓冲机制,在需要时必须编程来实现;第二,当视图更新绘制时,必须在视图上重绘整张图片。因此当性能要求较高时,比如在游戏绘图是,自定义View无法满足需要。对此,Android提供SurfaceView来代替View,SurfaceView相比View使用更方便,性能也更好。
SurfaceView一般会与SurfaceHolder结合使用,调用SurfaceView 的getHolder获取SurfaceView。SurfaceHolder提供了如下方法来获取Canvas对象。
Canvas lockCanvas():锁定整个SurfaceView对象,获取该Surface上的Canvas。
Canvas tockCanvas(Rect dirty):锁定SurfaceView上Rect划分的区域,获取该Surface上的Canvas。
当通过lockCanvas()获取指定了SurfaceView上的Canvas之后,接下来程序就可以调用Canvas进行绘图了。当调用SurfaceHolder的unlockCanvasAndPost方法之后,该方法之前所绘制的图形还处于缓冲之中,下一次lockCanvas()方法锁定的区域可能会“遮挡”它。
public class SurfaceViewActivity extends AppCompatActivity {
SketchpadSurfaceView surfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.*activity_surface_view*);
surfaceView = findViewById(R.id.*surface_view_sketch_pad*);
}
public void onClick(View v){
surfaceView.clean();
}
}
自定义的Surfaceview类中,定义了SurfaceHolder、画布、画笔、路径等对象。SurfaceView继承自View,并提供一个独立的绘图层;在这个视图中内嵌了一个专门用于绘制的Surface(内存中的绘图缓冲区)。SurfaceView和SurfaceHolder通常一起使用。SurfaceHolder是一个接口,通过它可以访问surface,它就像一个Surface的监听器。在构造函数中,对定制视图进行初始化。
public class SketchpadSurfaceView extends SurfaceView
implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mSurfaceHolder;
private Canvas canvas;
private boolean isDrawing;
Paint paint;
Path path;
private float preX, preY; *//**上次的坐标
\* public SketchpadSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() { *
\* mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
*
\* paint = new Paint(Paint.*ANTI_ALIAS_FLAG* | Paint.*DITHER_FLAG*);
paint.setStrokeWidth(10f);
paint.setColor(Color.*parseColor*("#FF4081"));*
\* paint.setStyle(Paint.Style.*STROKE*);
paint.setStrokeJoin(Paint.Join.*ROUND*);
paint.setStrokeCap(Paint.Cap.*ROUND*); *
\* path = new Path();
}
@Override
public void run() {
long start = System.*currentTimeMillis*();
while(isDrawing){
draw();
}
long end = System.*currentTimeMillis*();
if (end-start < 100) {
try {
Thread.*sleep*(100 - (end - start));
} catch (Exception e) { e.printStackTrace(); }
}
}
draw函数首先调用lockCanvas函数锁定画布,然后,完成画布的绘制工作,绘制完成后在屏幕上更新整张画布的内容。注意在异常处理中,需要解除画布的锁定。
private void draw(){
try {
canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.*WHITE*);
canvas.drawPath(path, paint);
} catch (Exception e) {
e.printStackTrace();
}finally{
if (canvas!=null) {
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
(5) 图像
OpenCV是一个开源的跨平台计算机视觉库,在后面的程序中,使用OpenCV库中的函数来处理图像。在图像处理活动中,实现了图像显示,灰度转换、图像边缘检测和人脸检测四个功能。在活动布局文件中,加入ImageView控件,并且预先载入一幅图片。
在ImageProcessingActivity类中,定义了各个图像处理功能用到的变量和对象,比如:人脸的尺寸、分类器对象、imageView对象等。
public class ImageProcessingActivity extends AppCompatActivity {
private float relativeFaceSize = 0.05f;
private int absoluteFaceSize = 0;
private Scalar faceRectColor = new Scalar(0, 255, 0, 255);
private CascadeClassifier classifier;
private double max_size = 1024;
private int PICK_IMAGE_REQUEST = 1;
private ImageView imageView;
private Bitmap bitmap_selected;
@Override
protected void onCreate(Bundle savedInstanceState) {
… …
imageView = findViewById(R.id.*imageView*);
imageView.setScaleType(ImageView.ScaleType.*FIT_CENTER*);
bitmap_selected = ((BitmapDrawable)imageView
.getDrawable()).getBitmap();
Button btnSelectImage = findViewById(R.id.*btn_select_image*);
btnSelectImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) { selectImage(); }
});
在onCreate函数中,获取ImageView对象,获取imageview的bitmap对象。ScaleType.FIT_CENTER把图片按比例扩大/缩小到View的宽度,居中显示。当点击“选择图片”按钮时,打开文件夹,用户可以选取移动设备上的图片。
private void selectImage() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.*ACTION_GET_CONTENT*);
startActivityForResult(
Intent.*createChooser*(intent, "选择图像..."),
PICK_IMAGE_REQUEST);
}
在选取图片文件后,返回的结果显示在ImageView控件上。从Intent中获取图片的uri,通过输入流读取图片数据,然后解码。BitmapFactory.Options类是BitmapFactory对图片进行解码时使用的一个配置参数类,其中BitmapFactory选项中的inJustDecodeBounds = true表示只读取图片,不加载到内存中。由于读取的图片可能很大,长宽与移动设备的屏幕不匹配,因此需要对原图像进行调整。inSampleSize用来做缩放选项,其默认值设为1。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_IMAGE_REQUEST && resultCode == *RESULT_OK
\* && data != null && data.getData() != null) {
Uri uri = data.getData();
try {
InputStream input = getContentResolver().openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.*decodeStream*(input, null, options);
int raw_width = options.outWidth;
int raw_height = options.outHeight;
int max = Math.*max*(raw_width, raw_height);
int newWidth = raw_width;
int newHeight = raw_height;
int inSampleSize = 1;
if (max > max_size) {
newWidth = raw_width / 2;
newHeight = raw_height / 2;
while ((newWidth / inSampleSize) > max_size ||
(newHeight / inSampleSize) > max_size) {
inSampleSize *= 2;
}
}
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.*ARGB_8888*;
bitmap_selected = BitmapFactory.*decodeStream*(
getContentResolver().openInputStream(uri), null, options);
imageView.setImageBitmap(bitmap_selected);
} catch (Exception e) { e.printStackTrace(); }
}
}
动态调整了inSampleSize后,读取的bitmap的长宽分别为原始长宽的1/inSampleSize。inPreferredConfig表示图片解码时使用的颜色模式,也就是图片中每个像素颜色的表示方式。ARGB_8888表示图片中每个像素用四个字节(32位)存储,Alpha,R,G,B四个通道每个通道用8位表示。最后,将解码的图像显示在imageView上。
(6) 动画
Android动画可以分为两类,一类是传统动画,一类是属性动画;传统动画又分为帧动画(Frame Animation)和补间动画(Tweened Animation)。
帧(Frame)动画是最简单的一种动画方式,它的原理与放电影的原理一样。它把动画过程的静态图片放到一起,并排好序,然后由Android应用来控制显示这些图片,利用人眼的“视觉暂留”原理,给用户造成“动画”的错觉。
在帧动画活动中,通过imageview不断切换显示图片来实现动画效果,首先通过imageview获取AnimationDrawable对象,它是一种可绘制的动画对象,用来实现帧动画操作,比如,在播放和停止按钮中通过控制动画的运行。
public class FrameAnimActivity extends AppCompatActivity {
ImageView imageView;
Button play;
Button stop;
@Override
public void onCreate(Bundle savedInstanceState) {
… …
final AnimationDrawable anim = (AnimationDrawable) imageView
.getBackground();
play.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) { anim.start(); }
});
stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) { anim.stop();}
});
在playWithCode函数中,首先创建AnimationDrawable对象,获取所有图片文件的编号;然后,在循环中,通过图片编号获取每一帧图像,接着调用AnimationDrawable的addFrame函数,将每一张图片作为一帧加入该对象,调用setOneShot函数,参数表示动画是否执行一次,true表示仅执行一次,false表示无限次循环执行动画。通过start函数来启动帧动画。函数编写好以后,还需要在活动的onCreate函数中调用该函数。运行程序,就可以看到动画播放效果,并且可以通过播放和停止按钮控制动画运行。
private void playWithCode() {
AnimationDrawable animDrawable = new AnimationDrawable();
int[] ids = { R.drawable.*nezha1*, … , R.drawable.*nezha40* };
*
\* for (int i = 0; i < 40; i++) {
Drawable frame = getResources().getDrawable(ids[i]);
animDrawable.addFrame(frame, 60);
}
animDrawable.setOneShot(false);
imageView.setBackground(animDrawable);
*
\* animDrawable.start();
}
补间动画是指只需指定动画开始和动画结束“关键帧”,而动画过程的“中间帧”则由系统计算、并插补。通过两种方式来实现补间动画。播放采用xml文件方式设置动画,运行动画代码采用代码方式来设置动画。
补间动画的局限是它们只改变视图的视觉效果,而无法改变对象本身的属性,而属性动画可以作用任意对象,并且动画效果可以按需定义,不再局限于前面介绍的4种基本动画效果。
在活动布局中,加入PropertyAnimView控件,然后在活动中定义两个ObjectAnimator对象和一个ValueAnimator对象。其中,ValueAnimator是属性动画的最核心的类,它也是ObjectAnimator 的父类。ValueAnimator只是进行平滑的动画过渡,它需要给对象属性手动赋值;而ObjectAnimator会给对象属性自动赋值。
public class PropertyAnimActivity extends AppCompatActivity {
private ImageView imageView;
private TextView textView;
private ObjectAnimator objectAnimator;
private ValueAnimator valueAnimator;
ObjectAnimator colorAnim;
public void onClick(View view) {
objectAnimator = ObjectAnimator.*ofFloat*(imageView, "rotationX", 0, 360, 0);
objectAnimator.setDuration(2000);
objectAnimator.start();
*
\* colorAnim = (ObjectAnimator) AnimatorInflater.*loadAnimator*(
PropertyAnimActivity.this, R.animator.*color_anim*);
colorAnim.setEvaluator(new ArgbEvaluator());*
\* colorAnim.setTarget(imageView); *
\* colorAnim.start();
valueAnimator = ValueAnimator.*ofFloat*(0, 200f, 0, 400f, 0);
valueAnimator.setDuration(2000);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float curValueFloat = (Float) animation.getAnimatedValue();
int curValue = curValueFloat.intValue();
imageView.layout(curValue, curValue,
curValue + imageView.getWidth(),
curValue + imageView.getHeight());
}
});
valueAnimator.start();
ObjectAnimator通过控制“值”的变化,将其赋给对象的属性,从而实现动画效果。在按钮事件中,通过ofFloat函数获取ObjectAnimator对象,对imageview对象执行旋转操作。ofFloat函数根据设定的规则改变目标对象的某个属性,从而实现与该属性相关的动画效果,函数的第一个参数用于指定动画操作的是哪个控件;第二个参数用于指定这个动画要操作这个控件的哪个属性;第三个参数是可变长参数(就是指这个属性值是从哪变到哪)。另外,还给colorAnim对象设置计算颜色渐变值的ArgbEvaluator对象,来改变imageview的颜色。用ofFloat函数获取valueAnimator对象,设置动画时间和插值器(减速)。添加一个更新监听器,当动画值发生改变时,重新设置imageview对象的位置。
本章习题:
1、本单元考核点
Activity和各种控件的编程方法。
界面布局的使用。
界面交互的事件处理机制。
Intent的信息传递和过滤方式。
各种常用控件的使用方法。
2、本单元课后习题
1、简要说明Intent中5个主要属性的名称及功能。。
答案:Intent包含下列内容:
Component name:接收并处理Intent的组件名称
Action:用来表示一个要执行的动作( action )名称的字符串
Data:动作处理处理数据的URI和MIME类型
Category:一个包含何种组件有资格处理Intent对象的信息的字符串
Extras:用于为意图附加信息,附加的信息由键值对构成。
Flags:Flags可以为意图设置某种标志,可以用作意图发送者签名。设置了Flags的意图启动一个 Activity 后,这个Activity可以根据意图中的Flags值判断意图来自哪里。
2、Android为何使用称为监听器的接口机制处理事件?
答案:Android系统事先无法知道开发者究竟要如何处理事件,也就无法为每个 View 对象定义好处理事件方法,因此,系统只能规定 View 类能处理什么类型的事件而让开发者去定义事件处理过程。事件处理接口对象也称为监听器,通过覆盖接口的回调方法实现对事件的捕捉和处理;View 类的事件监听器是一个接口,该接口中的回调方法会在 View 的事件监听器被触发时由 Android 系统调用。
3、简述Intent解析的匹配规则。
答案:Intent解析的匹配规则是:
(1)Android系统把所有应用程序包中的Intent过滤器集合在一起,形成一个完整的Intent过滤器列表;
(2)在Intent与Intent过滤器进行匹配时,Android系统会将列表中所有Intent过滤器的“动作”和“类别”与Intent进行匹配;
(3)把Intent数据Uri的每个子部与Intent过滤器的标签中的属性进行匹配;
(4)如果Intent过滤器的匹配结果多于一个,则可以根据在标签中定义的优先级标签来对Intent过滤器进行排序,优先级最高的Intent过滤器将被选择。
参考资源:
1、郭霖著.第一行代码Android.北京:人民邮电出版社,2014.
2、李刚著.疯狂Android讲义(第3版).北京:电子工业出版社,2015.
原创声明
=======
作者: [ libin9iOak ]
本文为原创文章,版权归作者所有。未经许可,禁止转载、复制或引用。
作者保证信息真实可靠,但不对准确性和完整性承担责任。
未经许可,禁止商业用途。
如有疑问或建议,请联系作者。
感谢您的支持与尊重。
点击
下方名片
,加入IT技术核心学习团队。一起探索科技的未来,共同成长。