基本了解:
Android系统架构:
- linux内核层:
- 系统运行层:
C/C++库,这一层还有Android运行时库,它主要提供了一些核心库,能够允许开发者使用Java语言来编写Android应用。另外,Android运行时库中还包含了Dalvik虚拟机(5.0系统之后改为ART运行环境),它使得每一个Android应用都能运行在独立的进程当中,并且拥有一个自己的Dalvik虚拟机实例。相较于Java虚拟机,Dalvik是专门为移动设备定制的,它针对手机内存、CPU性能有限等情况做了优化处理。
- 应用框架层
- 应用层
应用开发特色:
- 四大组件:
- 活动:活动是所有Android应用程序的门面,凡是在应用中你看得到的东西,都在活动中。
- 服务:一直在后台默默地运行,即使用户退出了应用,服务仍然是可以继续运行的。
- 广播接收器:允许你的应用接收来自各处的广播消息。
- 内容提供器:为应用程序之间共享数据提供了可能。
- 丰富的系统控件:
- SQLite数据库
- 强大的多媒体服务
- 地理位置定位
创建第一个Android项目:
选择创建的模板:
项目配置:
模拟器:
界面友好,有手就行。那个锤子是用来编译项目用的。绿色的三角是运行按钮,点击之后就会自动将模拟器开启然后运行这个项目。
项目结构图:
app模块内的项目结构:
AndroidManifest.xml
没有在这个文件中进行注册的活动不能使用
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.activitytest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Activitytest"><!--application的属性值 -->
<activity
android:name=".HelloWorldActivity"
android:label="This is FirstActivity"><!--activity的属性值-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!--这里表示了这个项目得主活动,点击图标后执行的第一个活动-->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
标签中的android:name表示这个活动对应的是那个class(一个继承自activity的一个类)
- 当一个活动被创建时一定会执行一个onCreate的方法。
- Android程序的设计讲究逻辑和视图分离,因此是不推荐在活动中直接编写界面的,更加通用的一种做法是,在布局文件中编写界面,然后在活动中引入进来。
res文件夹下的结构:
res/values/strings.xml中的代码如下:
<resources>
<string name="app_name">Activitytest</string>
</resources>
在其他地方使用这些资源有两种方式:
- 在代码中: R.string.app_name,其中string处填写的内容如果是在引用图片资源对应的就是drawable
- 在xml中:@string/app_name
build.gradle文件
Gradle是一个非常先进的项目构建工具
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
// google()
// jcenter()这里表示要把代码放到那个仓库
maven{ url 'https://maven.aliyun.com/repository/google' }
maven{ url 'https://maven.aliyun.com/repository/jcenter' }
maven{url 'http://maven.aliyun.com/nexus/content/groups/public'}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
//classpath中表示了一个插件,用来配合Android项目使用的gradle插件
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
// google()
// jcenter()
maven{ url 'https://maven.aliyun.com/repository/google' }
maven{ url 'https://maven.aliyun.com/repository/jcenter' }
maven{url 'http://maven.aliyun.com/nexus/content/groups/public'}
}
}
在远程依赖库中,Gradle在构建项目时会首先检查一下本地是否已经有这个库的缓存,如果没有的话则会去自动联网下载,然后再添加到项目的构建路径当中。
除了远程依赖,本地依赖,还有一种库依赖
以上内容为《第一行代码第二版》的陈述,现在可能会有很多地方不一样。
日志工具:
- Log.v()。用于打印那些最为琐碎的、意义最小的日志信息。对应级别verbose,是Android日志里面级别最低的一种。
- Log.d()。用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别debug,比verbose高一级。
- Log.i()。用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮你分析用户行为数据。对应级别info,比debug高一级
- Log.w()。用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好去修复一下这些出现警告的地方。对应级别warn,比info高一级。
- Log.e()。用于打印程序中的错误信息,比如程序进入到了catch语句当中。当有错误信息打印出来的时候,一般都代表你的程序出现严重问题了,必须尽快修复。对应级别error,比warn高一级。
上面这些都是可以用在代码中得语句,在IDE中有一个logcat也能方便查看log信息。还可以在旁边的输入栏中输入一定的字符串进行进一步筛除
活动
新建一个活动(empty activity)
- 任何活动都需要重写onCreate方法。
添加资源
新建方法与新建活动类似,在res对应的文件夹下选择对应的文件夹,右键选择新建。
操作简单,有手就行。
layout文件代码解释:
<?xml version="1.0" encoding="utf-8"?>
<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_1"
android:layout_width="match_parent"
android:layout_height="113dp"
android:text="Button 1" />
</LinearLayout>
android:id=“@+id/button_1” 给按钮定义唯一标识符,@+id/button_1中的+表明此处是定义
android:layout_width=“match_parent” 按钮的宽
android:layout_height 按钮的高
android:text 按钮上的字
match_parent 表示当前元素和父元素一样宽。
wrap_content 表示将当前内容恰好包含的高度就可。
如何将资源引入代码中
注意:在对应的xml文件里不要使用另外一个xml里面定义的资源,就是一个activity用自己xml中的,而不是其他地方的xml控件之类的。
setContentView函数用来加载一个布局文件
项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的first_layout.xml布局的id现在应该是已经添加到R文件中了。在代码中去引用布局文件的方法你也已经学过了,只需要调用R.layout.first_layout就可以得到first_layout. xml布局的id,然后将这个值传入setContentView()方法即可。
仅仅如此还不能将布局资源和活动联系起来,因为还没有在Manifest.xml中进行注册。——不过一般来说IDE都帮忙进行活动注册了,但是有时候还需要自己进行一些修改。
manifest中有关activity的代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.activitytest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Activitytest">
<activity android:name=".SecondActivity"></activity>
<activity
android:name=".FirstActivity"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
- 首先activity标签是放在application标签中的
- 其次使用android:name 来表示注册的是哪一个活动,.FirstActivity其实表示的是一个包路径
- android:label 设置活动的标题栏显示的内容,如果这个活动还是主活动的话那么这个label还会成为程序在图标下面的名称比如qq这种。
如果程序没有设置任何一个活动为主活动,那么这个活动虽然可以正常安装,但是无法在启动器中看到这个程序或者启动这个程序。这样的程序一般是用来提供第三方服务的,就是那种不需要被别人看到的活动。
使用findViewById()函数来通过资源id查找,该函数返回一个view对象,一般使用的时候都需要再向下转型
onCreateOptionsMenu()函数创建一个选择菜单,如果函数返回值为true,则会将其显示在活动上。
使用定义的menu资源
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu); // 返回一个menuinflater对象之后在使用inflate方法
// 两个参数,一个是菜单资源,另一个是要将创建的菜单添加到哪个菜单对象中
return true;
}
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.add_item:
Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
break;
case R.id.remove_item:
Toast.makeText(this, "you clicked Remove", Toast.LENGTH_SHORT).show();
break;
default:
}
return true;
}
在活动中使用Toast(一种简单的消息提示框)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1 = (Button)findViewById(R.id.button_1);
button1.setOnClickListener(new View.OnClickListener(){//给按钮添加监听事件,onclick方法将会在按钮被点击时执行
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this, "You clicked the Button 1",
Toast.LENGTH_SHORT).show();
}
});
}
makeText方法返回一个Toast对象,makeText需要传入3个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动本身就是一个Context对象,因此这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG
销毁一个活动
finish()函数,就是相当于点击了手机上的back键的效果。
intent的使用
如何从主活动跳转到其他活动
显式Intent:显式是指很明确的指明启动哪个活动。
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
// SecondActivity.class是Java反射中的一个内容(好像是),第一个参数是上下文。
startActivity(intent); //用来启动一个活动
}
});
隐式Intent:给intent指定action
- 给活动指定为一个字符串,即自己定义的活动,action
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
通过在标签下配置的内容,可以指定当前活动能够响应的action和category
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"></action>
<!--表明这个可以活动可以响应对应的这个活动-->
<category android:name="android.intent.category.DEFAULT"></category>
</intent-filter>
</activity>
标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category。只有和中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。
每个Intent中只能指定一个action,但却能指定多个category。
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("android.intent.category.MYY");
startActivity(intent);
}
对应的xml,如果注册的activity中的category不能包含intent中的category,那么会因为匹配不到activity而报错
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<!-- <category android:name="android.intent.category.MYY"></category>-->
</intent-filter>
</activity>
注册处为活动设置的category不一定要恰好匹配intent中所要求的category,但是一定包含其对应的所有
更多隐式Intent的用法:使用内置的action
比如:Intent.ACTION_VIEW:这是一个Android系统内置的动作(也就是说会有系统中的activity来响应这个intent),其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
//setData函数将一个uri对象作为参数传入intent中,作为intent要处理的对象。
startActivity(intent);
}
中的标签(又多一个过滤条件)
当intent中的数据和下面data标签中的值完全一致时才会匹配到对应的活动。
data标签中可以包含的内容:
android:scheme。用于指定数据的协议部分,如上例中的http部分。
android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
android:port。用于指定数据的端口部分,一般紧随在主机名之后。
android:path。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
如果想要自己写的activity也能够响应网页的intent,则对应在xml中最要进行设置以配得上intent的要求。
<activity android:name=".ThirdActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<!--将这个action标签设置为和系统内那个action同名,这样在寻找intent的时候就也会找到这个activity -->
<category android:name="android.intent.category.DEFAULT"></category>
<data android:scheme="http"></data>
</intent-filter>
</activity>
现在介绍另外一个系统内的action:Intent.ACTION_DIAL,以下代码可以弹出一个拨号界面:
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
//这里可以看出uri不仅仅是网址
//此时如果想要自己写的activity也能够响应,则在data的andriod:scheme属性中应该写入tel
startActivity(intent);
}
如何向下一个活动传递数据:
还是使用intent来进行传递数据
(就是自己先创建一个“包裹”,往里面以键值对的形式putExtra)
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
String message = "hello SencondActivity";
intent.putExtra("mes", message);//将需要传递的信息使用键值对的方式保存到intent中
startActivity(intent);
}
收到“包裹”的activity首先getIntent,然后getStringExtra就可以了。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();//getIntent是Activity的一个静态方法
String mesarecv = intent.getStringExtra("mes");//接收的是字符串就是用getStringExtra
Log.d("SecondActivity: ", mesarecv);
}
将数据返回给上一个活动:
==startActivityForResult()==方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
//其中的1表示的是requestcode请求码用来标识身份
//startActivityForResult(intent2, 2)这样就可以分别对应不同的任务处理
}
@Override//每个活动所调用的返回值都会送到这个函数当中来,通过requestcode进行区分
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode){//按照请求码进行switch
case 1:
if(resultCode == RESULT_OK){
String retData = data.getStringExtra("ret_data");
Log.d("First: ", retData);
}
break;
default:
}
}
public void onClick(View v) {//点击按钮的方式结束活动,在这里设置返回值,同样是发送一个intent“包裹”
Intent data = new Intent();
data.putExtra("ret_data","hello first");
setResult(RESULT_OK, data);
//设置返回数据,第二个参数是intent,第一个是resultcode,RESULT_OK, RESULT_CANCEL来表示处理是否成功
finish();
}
//这样书写代码只有在点击这个按钮时才会返回结果,如果是点击back销毁活动,那么就应该在重写方法onBackPressed中写入该函数逻辑
个人对intent的一个定位:
intent(意图)对应的是一个活动创建的内容,它代表将要创建什么样的活动。
活动之间的信使(设置活动内容的一种方式),在需要启动另外一个活动的时候,startActivity, startActivityForResult就把相关信息告诉信使,让它去找对应的activity然后就启动了另外一个活动
在需要进行信息传递的时候,会将信息使用putExtra()函数将信息包含在intent当中,等到传递过去的时候,对方activity可以在intent中进行提取。
在需要进行返回信息给上一个activity的时候,会雇佣一个intent将信息使用setResult()函数存放在intent那,之后intent会到达前一个activity进而完成信息传递。
不同部分扮演的角色:
intent对于一个activity来说相当于包裹(一个由某activity生成的包裹,想要发送给指定的activity(自定义的activity或者系统中本来就有的activity比如拨号)),activity类则对应了这个活动的方法和动作,activity在xml中定义的标签则相当于activity的一个标签牌(其中的intent-filter表明这个activity能够响应哪些intent)
活动的生命周期
返回栈(Task栈)
Android中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键会销毁最上面的活动,下面的一个活动就会重新显示出来
活动的状态
每个活动在其生命周期最多可能会有4种运行状态
- 运行状态:当一个活动位于返回栈的栈顶时,这个活动就属于运行状态。
- 暂停状态:当一个活动不在处于栈顶位置,但依然肉眼可见,这时活动就进入暂停状态。不是栈顶活动,但还是有可能被看见的,比如说弹出一个对话框,对话框并没有占满整个屏幕。处于暂停状态的活动依然是完全存活的。
- 停止状态:当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收
- 销毁状态:当一个活动从返回栈中移除后就变成了销毁状态,系统往往倾向于回收处于这个状态的活动,从而保证手机的内存充足。
活动的生存期:
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节
- onCreate():会在活动第一次被创建时调用,在这个方法中应该完成一些初始化操作
- onStart():这个方法在活动由不可见变为可见时调用
- onResume():这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态
- onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
- onStop()。这个方法在活动完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。
- onDestroy()。这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。
- onRestart()。这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将活动分为3种生存期
- 完整生存期:活动在onCreate方法和onDestroy方法之间所经历的,就是完整生存期
- 可见生存期:活动在onStart方法和onStop方法之间所经历的就是可见生存期。
- 前台生存期:活动在onResume方法和onPause方法之间所经历的就是前台生存期
活动被回收了怎么办?
当一个活动处于停止状态时是有可能被系统回收的。如果一个活动在停止状态被回收了,而之后又通过back键使得该活动重新出现,那么这个时候并不会执行onRestart()方法,而是执行onCreate方法。这种情况下活动需要重新创建。
这样存在一个问题:
活动在由停止状态重新运行的时候,之前保存的一些变量也会跟着被重新建立。
这里有一个方法可以解决这个问题:onSaveInstanceState()
这个方法会携带一个Bundle类型的参数,而Bundle提供一系列的方法用于保存数据,比如:putString()保存字符串,每个保存方法需要传入两个参数,一个是键,一个是值。
// 在activity类中添加如下代码即可
@Override
protected void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
String tempData = "hello";
outState.putString("data_key", tempData);
}
我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
String tempData = saveInstanceState.getString("data_key");
活动的启动模式
启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给标签指定android:launchMode属性来选择启动模式
standard:
在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
singleTop:
这种启动方式,当发现想要启动的活动就位于栈顶,就直接使用它而不再创建新的实例。但是如果活动不在栈顶,调用时还是会创建新的实例。
singleTask:
这种启动方式会检查整个返回栈中有没有这个活动的实例。
如果栈中存在这个实例,则将这个活动上面的活动统统出栈(从下往上依次出栈)
如果没有就创建一个新的实例。
singleInstance:
这种启动方式会将启动的活动放到另外一个返回栈中,比如A singleinstance启动B,B启动C,那么C直接位于A之上,B单独位于一个返回栈中。从活动C开始按back键,C出栈显现A,再back,A出栈,栈空开始显现B。
tips:
学会自己创建一些类来辅助开发,比如书中的ActivityCollect和BaseActivity
也可以通过编写一些方法来达到优化代码的效果,比如活动的创建,就可以自己创建一个方法Second.actionStart()然后在这里面把创建语句进行结合。
UI开发
常见控件(记单词,缩写),和资源是两个不同的概念
Android控件的可见属性:
3种:
- visible可见
- invisible不可见但是占据空间
- gone不可见不占据空间
Textview:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/text_view"
android:text="hello"
android:gravity="center"
android:textSize="24sp"
android:textColor="#00ff00"
/>
Button:
<Button
android:id="@+id/button_1"
android:layout_width="match_parent"
android:layout_height="113dp"
android:text="Button 1"
android:textAllCaps="false"/>
<!--默认会把字母变成大写,上面的属性可以屏蔽-->
EditText:
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="I am fatw"
android:maxLines="2"
/>
EditText通过实例化为对象之后,提供一个getText()(返回值为Editable类型)的方法获取其中输入的内容
ImageView:
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/abc_vector_test"/>
控件定义好了之后,还可以在代码中动态的修改控件的属性。比如
ImageView imageView = (ImageView)findViewById(R.id.image_view);
imageView.setImageResource(R.drawable.img_1);
ProgressBar:
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:max="100"/>
在代码中可以通过findViewById之后的progress,然后对其值的变换,progress+=10来改变进度条的进度,当满足条件时就将其控件可见属性设置为gone
AlertDialog:
这个控件不是通过xml来定义的
public void onClick(View v) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(FirstActivity.this);
alertDialog.setTitle("This is Dialog");
alertDialog.setMessage("Something important");
alertDialog.setCancelable(false);//可否用back键关闭
alertDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
alertDialog.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
alertDialog.show();
}
ProgressDialog:
这个和上面的类似,值得注意的是当加载部分完成,而且又设置不能通过back键来关闭该对话框时,记得自己调用dismiss函数来关闭这个控件,否则会一直出现在屏幕上。
4种基本布局
- 线性布局
- 相对布局
- 帧布局
- 百分比布局
布局和控件的关系:布局中还可以放布局
线性布局LinerLayout:
记个单词:
orientation:方向,控制布局中的控件以水平线性还是垂直线性进行排列。
- android:layout_gravity
控件在布局中的位置,是控件的属性控制该控件在其所在布局中的位置,布局也可以放在布局中,所以也有这个属性
- android:gravity
文字在控件中的位置
- layout_weight(线性布局独有)
通过比例的方式来进行大小的指定
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="horizontal">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"/>
<!--0dp是一种规范写法,当设置了layout_weight之后就不会在使用layout_width,但是不能把该属性删掉 -->
<Button
android:id="@+id/button_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="send" />
<!--按比例一个是1/11,一个是10/11-->
</LinearLayout>
相对布局RelativeLayout:
明显和线性布局相比,相对布局的位置关系更加丰富(更加复杂)
记个单词:
align:排列
相对于父布局
- alignParentTop=“True” 设置相对于父布局的位置
- alignParentLeft=“True”
上面两个合起来就是该控件位于布局中的左上的位置
android:layout_centerInParent="true"
位于父布局中的中心位置
相对于布局中的控件
相对于控件整体的相对位置,上下左右
- layout_above/below属性值为@id/button1这一类的
- layout_toLeftOf
<Button
android:id="@+id/button_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_weight="10"
android:text="send4" />
<Button
android:id="@+id/button_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="10"
android:layout_below="@id/button_4"
android:layout_toRightOf="@id/button_4"
android:text="send5" />
相对于控件的左右边缘:
右边缘和右边缘对其
layout_alignRight
帧布局
应用场景较少,具体后续fragment处见
百分比布局
通过百分比来控制控件的大小
百分比布局属于新增布局,Android团队将百分比布局定义在了support库当中,只需要在项目的build.gradle中添加百分比布局库的依赖,就能保证百分比布局在Android所有系统版本上的兼容性了。
打开app/build.gradle文件,在dependencies闭包中添加如下内容:
implementation 'androidx.percentlayout:percentlayout:1.0.0'
百分比布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.percentlayout.widget.PercentFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_height="0dp"
android:layout_width="0dp"
android:id="@+id/btn1"
android:layout_gravity="left|top"
android:text="btn1"
android:textAllCaps="false"
app:layout_heightPercent="50%"
app:layout_widthPercent="50%" />
<Button
android:layout_height="0dp"
android:layout_width="0dp"
android:id="@+id/btn2"
android:layout_gravity="right|top"
android:text="btn2"
android:textAllCaps="false"
app:layout_heightPercent="50%"
app:layout_widthPercent="50%" />
<Button
android:layout_height="0dp"
android:layout_width="0dp"
android:id="@+id/btn3"
android:layout_gravity="left|bottom"
android:text="btn3"
android:textAllCaps="false"
app:layout_heightPercent="50%"
app:layout_widthPercent="50%" />
<Button
android:layout_height="0dp"
android:layout_width="0dp"
android:id="@+id/btn4"
android:text="btn4"
android:layout_gravity="right|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"/>
</androidx.percentlayout.widget.PercentFrameLayout>
自定义控件
控件的继承结构
引入布局:
背景:有些时候会固定的使用某种控件的布局,但是有没有对应的控件可以直接使用,为了减少代码量,可以采用引入布局的设置
在使用自己设计的布局的时候就使用include语句将布局引入。
示例:
title.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">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="asda" />
</LinearLayout>
first_layout.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:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="fdgdfg" />
<include layout="@layout/title" />
</LinearLayout>
上述过程仅仅将布局引入,但是如果要对引入的布局中的控件编写响应事件的话,还得在不同的活动中在编写一次
可以通过编写自定义控件(就是写一个对应的类)来解决问题。
创建自定义控件
示例:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
// inflate方法可以动态加载一个布局文件,第二个参数表示
//后续代码将组件中的事件都处理好
Button titleBack = (Button)findViewById(R.id.title_back);
titleBack.setOnClickListener(new OnClickListener(){
public void onClick(View v){
((Activity)getContext()).finish();
}
})
}
}
//创建一个自定义控件就是创建一个类,而相应的资源文件同样需要使用xml进行定义,在如上述代码所示对资源文件进行引用
在xml代码中
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ListView控件
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
public class MainActivity extends AppCompatActivity {
private String[] data = {"apple", "banana", "orange", "pear", "watermelon", "cherry", "mango",
"pineapplej", "asdasd", "werwer", "trtrtr", "trdgfre", "fgdbvbm", "sdjflks", "dfdlkjfg",
"sdfksjd"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 数据并不能直接传入listview中,需要通过适配器
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, data);
// android.R.layout.simple_list_item_1是ListView的子项布局,也就是每一栏是什么布局android.表示系统内置的
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(arrayAdapter); //将构建好的数据传入ListView当中。
}
}
定制ListView界面,也就是定制ListView的子布局
// FruitAdapter.java
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId,
List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@Override // getView方法是当子项被滚进页面的时候就会调用的方法。
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前项的Fruit实例
View view;
ViewHolder viewHolder;
if (convertView == null) { // conertView可以将已经加载的子项进行缓存,提高运行效率
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder); // 将ViewHolder存储在View中
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag(); // 重新获取ViewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder {
ImageView fruitImage;
TextView fruitName;
}
}
package com.example.listviewtest;
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
<?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">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
目前以了解与理解为主,对于需要靠记忆的控件部分的知识,理解就行。
对于新增的控件为了让所有Android版本都可以使用,Android团队将其定义在了support库中,需要在build.gradle中导入同步之后才能继续使用。
Nine-patch图片是一种特殊处理过的图片,能够指定哪些区域可以被拉伸,哪些区域不可以。
碎片(fragment)
碎片(Fragment)是一种可以嵌入在活动当中的UI片段,与活动十分相似。
碎片的使用方式
简单用法
新建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:orientation="horizontal"
android:background="@color/purple_500">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hello fragment" />
</LinearLayout>
新建一个LeftFragment类,并让他继承Android内置的Fragment类或者support库中的Fragment类,默认情况下这个support库会被引入appcompat-v7库时一并引入
//RightFragment
public class RightFragment extends Fragment {
@Nullable
@Override
// 重写onCreateView方法
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = getLayoutInflater().inflate(R.layout.rightfragment, container, false);
return view;
}
}
接着在activity_main.xml中对两个碎片进行布局。使用fragment标签
<?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:orientation="horizontal">
<fragment
android:id = "@+id/left_fragment"
android:layout_weight="1"
android:layout_width = "0dp"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_height="match_parent" />
<fragment
android:id="@+id/right_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:name="com.example.fragmenttest.RightFragment"
android:layout_height="match_parent" />
</LinearLayout>
mainactivity中正常加载mainactivity的布局即可
动态加载碎片
主要步骤为一下5步:
- 创建待添加的碎片实例
- 获取FragmentManager
- 开启一个事务
- 向容器中添加或替换碎片,一般使用replace方法实现,需要传入容器的id和待添加的碎片实例
- 通过commit()提交事务
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.right_layout, fragment);
fragmentTransaction.commit();
}
在碎片中模拟返回栈
主要函数:FragmentTransaction中的addToBackStack()方法
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.right_layout, fragment);
fragmentTransaction.addToBackStack(null);// 在提交事务之前先将事务加入返回栈,这样旧的fragment就会压入返回栈中
fragmentTransaction.commit();
}
这样之后的效果是,不断按返回键首先跳转到之前的rightFragment布局之后,右边布局消失,在之后才退出程序。(???怎么理解变成空白)
碎片与活动之间的通信
在活动中调用碎片中的方法:使用FragmentManager提供的findFragmentById就可以获得碎片的实例。
在碎片中调用活动里的方法:每个碎片都可以通过调用getActivity(返回一个context对象)方法来得到和当前碎片相关联的活动实例
碎片的生命周期
碎片的状态
- 运行状态:
碎片可见并且它所关联的活动正处于运行状态时
- 暂停状态:
一个未占满屏幕的活动被添加到了栈顶,活动进入暂停状态,与之相关联的可见碎片就会进入暂停状态。
- 停止状态:
当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态
- 销毁状态:
碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状态。
通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,如果事务提交之前并没有调用addToBackStack()方法,这时的碎片也会进入到销毁状态就是会执行onDetach。如果调用了addToBackStack方法则会进入停止状态仅执行到onDestoryView。
回调方法
碎片中比较独特的回调方法:
- onAttach()。当碎片和活动建立关联的时候调用。
- onCreateView()。为碎片创建视图(加载布局)时调用。
- onActivityCreated()。确保与碎片相关联的活动一定已经创建完毕的时候调用。
- onDestroyView()。当与碎片关联的视图被移除的时候调用。
- onDetach()。当碎片和活动解除关联的时候调用。
下图为将Fragment加入返回栈中的情况
动态加载布局的技巧
限定符
红色框内就是一种限定符
系统根据当前设备的大小,自己认为应该加载large中的还是layout中的布局。
但是large并不明确,还有一种限定符layout-sw600dp这个表示(SmallWidth)当程序运行在大于等于600dp的设备上时会加载这个文件夹中的xml文件。
广播
简介
Android中每个程序都可以对自己感兴趣的广播进行注册,这样程序就只会接收到自己感兴趣的广播内容。应用程序可以自由的发送和接受广播。
广播是一种可以跨进程的通信。
广播主要有两种类型:
- 标准广播
在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息。这种方式效率比较高,但是无法截断。
- 有序广播
在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。
接收系统广播
动态注册监听网络变化
注册广播的方式一般有两种:
- 在代码中注册
- 在AndroidManifest.xml中注册
广播接收器---->一个继承自Broadcast-Recevier的类
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangeReciver networkChangeReciver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
//将想要监听的广播信息通过action加入
networkChangeReciver = new NetworkChangeReciver();
registerReceiver(networkChangeReciver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReciver);
// 动态注册的广播接收器需要在最后取消注册
}
class NetworkChangeReciver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
}
}
}
静态注册实现开机启动
使用Android Studio提供的快捷方式来创建一个广播接收器,右击com.example.broadcasttest包→New→Other→Broadcast Receiver
静态注册在AndroidManifest.xml中
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--需要提供权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<!--静态注册broadcastReciver与注册activity差不多 -->
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!--在action中的android:name决定要接收什么样的广播信息 -->
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等
发送自定义广播
发送标准广播
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
intent.setComponent(new ComponentName(getPackageName(), "com.example.broadcasttest.MyBroadcastReceiver"));
// Android8.0后的新特性,静态注册的话需要添加component,MyBroadcastReceiver是接收广播的一个类,通过静态注册
sendBroadcast(intent);//发送标准广播
}
});
}
发送有序广播
可以通过设置优先级来实现广播的有序播送,而且在优先级较高的广播接收器接收到广播之后,可以通过abortBroadcast()来截断广播的继续传播。
使用本地广播
简单的说就是发出的广播只能在应用程序内部进行传递,并且广播接收器只能够接收来本应用程序发出的广播。
本地广播无法通过静态注册。进行静态注册的广播能够在程序未启动的状态下接收广播信息
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);
}
});
// 通过LocalBroadcastManager来进行本地的广播发送和管理,和动态注册的步骤差不多
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
}
}
}
数据存储
Android系统主要提供了3中数据化持久的方式
- 文件存储
- SharedPreferences
- 数据库存储
文件存储
将数据存储到文件中
context类中提供了一个openFileOutput方法,方法接收两个参数:
- 参数的文件名
- 文件的操作模式
- Context.MODE_PRIVATE:会覆盖原文件的内容
- Context.MODE_APPEND:在原文件的内容的基础上附加
// 与Java中的数据存储到文件差不多
public void save(String inputText){
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在app中得到的文件在虚拟设备文件系统,/data/data/com.example.filepersistencetest/files路径下,Android studio中查看方式如下
从文件中读取数据
openFileInput方法,返回一个FileInputStream对象(字节流)
public String Load() {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
// in是FileInputStream是一个字节流,通过InputStreamReader实现字节流与字符流之间的桥梁的构建。
String line = "";
while ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}
SharedPreferences
该方式是使用键值对的方式来存储数据,还支持多种不同的数据类型的存储
SharedPreferences文件都是存放在/data/data//shared_prefs/目录下
数据存储
获取SharedPreferences对象的三种方法
- Context类中的getSharedPreferences()。两个参数,一个是文件名,一个是操作模式(目前只有MODE_PRIVATE这一种指定操作模式,表示只有当前的应用程序才可以对这个文件进行读写)
- Activity类中的getPreferences()。这个方法自动将活动名作为文件名
- PreferencesManager类中的getDefaultSharedPreferences()
向SharedPreferences文件中存储数据:
- 调用SharedPreferences对象的edit方法来获取一个SharedPreferences.Editor对象
- 向Editor对象中添加数据,putString():表示添加一个字符串。格式(putxxx)
- 调用apply方法将添加的数据提交,从而完成数据存储操作
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.apply();
数据读取
SharedPreferences对象中提供了一系列的get方法,这些get方法与写入时的putxxx方法对应,方法有两个参数:存储时的键值,找不到对应键的时候返回什么默认值。
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
String name = pref.getString("name", "");
int age = pref.getInt("age", 0);
boolean married = pref.getBoolean("married", false);
数据库存储
SQLite:轻量级关系型数据库
创建数据库
SQLiteOpenHelper帮助类(抽象类中)
两个抽象方法:
- onCreate()------创建数据库
- onUpgrade()--------更新数据库
两个实例方法:
- getReadableDatabase()
- getWritableDatabase()
这两个方法可以创建或打开一个现有的数据库。
不同的是,在磁盘空间已满的时候,readable那个方法将只以可读的方式打开数据库
writable方式则会出错
升级数据库
如果dbHelper = new MyDatabaseHelper(this, “BookStore.db”, null, 2);的第三的参数version变大时,就会调用upgrade方法
Android提供的数据库操作的方法
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
});
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
// 开始组装第一条数据
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values); // 插入第一条数据
values.clear();
// 开始组装第二条数据
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values); // 插入第二条数据
}
});
//update
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
//后两个参数用来进行条件约束,?是一个占位符,第四个参数(字符数组)指定相应的内容
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });
}
});
// delete
Button deleteButton = (Button) findViewById(R.id.delete_data);
deleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });
}
});
//查询
Button queryButton = (Button) findViewById(R.id.query_data);
queryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
} while (cursor.moveToNext());
}
cursor.close();
}
});
}
使用sql操作数据库
db.exeSQL("insert into BOOK(name, author, pages, price) values(?,?,?,?)", new String[] {"The da vinci code", "Dan Brown", "454", "16.96"});
db.exeSQL("update Book set price = ? where name = ?",new String[]{"10.99","The davinci code"});
db.exeSQL("delete from Book where pages > ?", new String[]{"500"});
db.rawQuery("select * from Book", null);//null这个地方本来应该是string[]
使用LitePal操作数据库
LitePal是一款开源的数据库框架
配置开源库litepal
首先在app/build.gradle中dependencies闭包中添加引用
之后在src/main目录下创建一个assets目录(这个目录的大致功能与res差不多,但是在访问的时候需要通过AssetsManager来进行访问,并且他不会被编译到R类中)
在assets目录中定义litepal.xml
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" ></dbname>
<version value="2" ></version>
<!--list 指定映射关系,映射关系可以使我们使用面向对象的方式来操作数据库 -->
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
<mapping class="com.example.litepaltest.Category"></mapping>
</list>
</litepal>
在litepal中的创建和升级数据库
// 创建一个类,让他与数据库中的表相对应,在litepalxml中有与之对应的映射关系
public class Book extends DataSupport {
private int id;
private String author;
private double price;
private int pages;
private String name;
private String press;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPress() {
return press;
}
public void setPress(String press) {
this.press = press;
}
}
使用litepal创建数据库
public void onClick(View v){
LitePal.getDatabase();//就完成了数据库创建,创建的内容一litepal中Mapping对应的类
}
使用litepal升级数据库的时候,不需要将之前有的表删除,可以直接在修改了版本号,mapping和对应的Java类之后调用litepal.getdatabase()
使用litepal进行增删改查
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Connector.getDatabase();// LitePal.getDatabase()就是调用的这个函数
}
});
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Book book = new Book();
book.setName("The Da Vinci Code");
book.setAuthor("Dan Brown");
book.setPages(454);
book.setPrice(16.96);
book.setPress("Unknow");
book.save(); //将数据添加到数据库中
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Book book = new Book();
book.setPrice(14.95);
book.setPress("Anchor");
//updataAll()中的参数可以添加约束条件,如果没有则代表对所有的列进行更新
//book.setToDefault("pages");
//book.updataAll();这两行代码则将pages列的数据恢复成默认值
book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan Brown");
}
});
Button deleteButton = (Button) findViewById(R.id.delete_data);
deleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DataSupport.deleteAll(Book.class, "price < ?", "15");
}
});
Button queryButton = (Button) findViewById(R.id.query_data);
queryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
List<Book> books = DataSupport.findAll(Book.class);
// 还有类似findFirst(Book.class);findLast(Book.class);
for (Book book: books) {
Log.d("MainActivity", "book name is " + book.getName());
Log.d("MainActivity", "book author is " + book.getAuthor());
Log.d("MainActivity", "book pages is " + book.getPages());
Log.d("MainActivity", "book price is " + book.getPrice());
Log.d("MainActivity", "book press is " + book.getPress());
}
}
});
}
查询时litepal还提供了连续查询的方式,
List<Book> books = DataSupport.select("name", "author", "pages").where("pages > ?", "400").order("pages").limit(10).offset(10).find(Book.class);
也支持原生sql的查询方式
Cursor c = DataSupport.findBySQL("select * from Book where pages > ?","400");
内容提供器
实现不同程序之间的是数据共享
操作和SQLiteDatabase差不多就是定位资源的时候使用的是内容uri(毕竟不在同一个应用程序当中)
运行时权限
有些app在安装时就会要求获取权限,如果不给就不能安装。而现在Android提供一种运行时权限获取,对于那些比较敏感的权限,在需要时才会向用户发起请求。
Android将程序分为两类,一类是普通权限,一类是危险权限。(对于普通权限的申请系统会自动帮忙进行处理)。对于危险权限的申请需要进行运行时权限处理,普通权限就只要在AndroidManifest.xml中添加申明一下即可(就是在broadcast章节中使用的xml标签)
危险权限:
表中的每个危险权限属于一个权限组,用户一旦同意授权组中某一权限,该组中其他权限也会被授权。
在程序运行时申请权限
对于危险权限的申请如果只是在AndroidManifest.xml中进行了声明,而没有进行权限处理的话,程序会抛出异常。(捕获到的异常信息)
程序进行权限处理,主要做的工作就是查询是否已经授权,在未授权的情况下请求授权,并对不同的授权结果进行对应的处理。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makecall = (Button) findViewById(R.id.make_call);
makecall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);// 这个函数调用完之后会弹出一个对话框,之后无论是否授权都会调用onRequestPermissionResult方法,最后一个参数为请求码
}else{
call();
}
}
});
}
private void call(){
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
}else{
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
访问其他程序中的数据
通过其他程序提供的内容提供器访问其数据
ContentResolver类,这个类提供了一系列的CRUD操作
这个类接受一个uri参数,由authority和path组成
比如同一程序中有两个表,它对应的uri为:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
content表示对应的协议
使用方法除了是参数是内容uri之外,其他的和SQLiteDatabase的操作差不多
创建自己的内容提供器
创建自己的内容提供器可以新建一个类去继承ContentProvider,该类中有6个抽象方法
- onCreate:初始化内容提供器的时候调用,返回true则代表初始化成功
- query:返回的为Cursor
- insert:
- update:
- delete:
- getType:返回MIME类型,
一个标准的内容URI写法如下:
content://com.example.app.provider/table1
# 但是后面还可以跟id来具体到某一条数据
content://com.example.app.provider/table1/1
# 则代表id为1的那一条数据
还可以使用通配符来构成URI。Uri.parse()可以将字符串形式,解析成Uri对象
content://com.example.app.provider/*
*匹配任意长度的字符,该uri表示可以匹配任何表
content://com.example.app.provider/table1/#
#匹配任意长度的数字,该uri表示可以匹配任何单项
// 四大组件有一个共同点就是要在AndroidManifest.xml中进行注册
// 在<provider>标签中进行注册
// 内容提供器是根据外来程序传进来的URI进行数据库操作,所以在创建内容提供器的时候要创建一个DatabaseHelper用于之后连接数据库进行操作
public class MyProvider extends ContentProvider{
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
}
public boolean onCreate(){
dbHelper = new MydataHelper(getContext(), "BookStore.db", null, 2);
return true;
}
public Cursor query(Uri uri, String[] project. String selection, String[] selectionArgs, String sortOrder){
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
// 通过提供可以访问的uri来对可访问内容进行控制
switch(uriMatcher.match(uri)){ // 根据提供的uri进行匹配执行对应的操作
case TABLE1_DIR:
// query table1表中的所有数据
break;
case TABLE1_ITEM:
// 通过解析Uri参数获取uri中的id值
String bookId = Uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id=?",new String[] {bookId},null,null,sortOrder);
// query table1表中的单条数据
break;
default:
break;
}
}
}
Android手机多媒体
通知(Notification)
就是下拉栏那一条条QQ消息提醒,新闻推送之类的
通知既可以在活动中创建(没啥必要毕竟通知主要是为了程序在后台运行时进行通知),也可以在广播接收器和服务中创建
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
// 新建一个manager来管理通知
// 新建一个notification,并对其内容进行设置,最后使用build进行创建
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.build();
manager.notify(1, notification);// 显示通知
以上只能显示出通知,但是点击之后没有反应。需要借助PendingIntent类来实现打开一个intent
Intent intent = new Intent(this, NotificationActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
// getBroadcast(), getService()
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
// 这个接收一个pendingintent作为参数表示点击之后打开一个活动(广播,服务)
.setAutoCancel(true)
// 这个表示在点击通知栏之后会自动取消通知栏上的那个图标,也可以通过manager.cancel将显示通知时设置的id作为参数传入进行取消
.build();
manager.notify(1, notification);
进行通知设置的时候还可以setSound,setLights,setVibrate(震动)
还可以在通知栏中显示长文字和大图片,通过setStyle方法,比如设置大图片:
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.big_image)))
还可以设置通知的优先级,一共有5个常量值可选:
- PRIORITY_DEFAULT表示默认的重要程度,和不设置效果是一样的;
- PRIORITY_MIN表示最低的重要程度,系统可能只会在特定的场景才显示这条通知,比如用户下拉状态栏的时候;
- PRIORITY_LOW表示较低的重要程度,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在更重要的通知之后;
- PRIORITY_HIGH表示较高的重要程度,系统可能会将这类通知放大,或改变其显示的顺序,将其排在比较靠前的位置;
- PRIORITY_MAX表示最高的重要程度,这类通知消息必须要让用户立刻看到,甚至需要用户做出响应操作(QQ消息就是这种)
.setPriority(NotificationCompat.PRIORITY_MAX)
调用摄像头和相册
调用摄像头
就是传递一个intent开启一个活动
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);// imageUri表示拍摄的图片File所转成的Uri
startActivityForResult(intent, TAKE_PHOTO);
打开相册
传递一个intent
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO);
播放多媒体文件
播放音频文件
在一首歌播放完的时候应该调用的是reset方法。stop方法被调用后MediaPlayer将无法在播放音频
播放视频文件
与播放音频类似有一个对应的类:VideoView
使用网络技术
WebView的用法
WebView控件(Button也是一种控件)类似于一个小小的浏览器
<?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" >
<!--在布局文件中进行定义 -->
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取Webview实例
WebView webView = (WebView) findViewById(R.id.web_view);
// getSettings()方法可以去设置一些浏览器的属性
webView.getSettings().setJavaScriptEnabled(true);
// 表示需要从一个网页跳转到另一个网页时,使目标网页仍在当前WebView中显示,而不是打开系统浏览器
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("http://www.baidu.com");
}
使用HTTP协议访问网络
WebView对网络请求进行了较好的封装。现在使用HttpURLConnection来手动进行网络访问
使用HttpURLConnection
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
TextView responseText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendRequest = (Button) findViewById(R.id.send_request);
responseText = (TextView) findViewById(R.id.response_text);
sendRequest.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if(view.getId()==R.id.send_request){
sendRequestWithHttpURIConnection();
}
}
private void sendRequestWithHttpURIConnection(){
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL("https://www.baidu.com");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while((line = reader.readLine())!=null){
response.append(line);
}
showResponse(response.toString());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null){
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response){
// Android是不允许在子线程中进行UI操作的,需要通过这个方法将线程切换到主线程,然后再更新UI元素
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.setText(response);
}
});
}
}
使用OkHttp开源库
public void run() {
try {
OkHttpClient client = new OkHttpClient();
// 创建request来发起http请求
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
// 发送请求并接收响应数据
Response response = client.newCall(request).execute();
String responseData = response.body().toString();
showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
}
}
// 如果是发起post请求,先要构造好POST数据内容,之后构造好请求内容,之后再发送出去
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
解析XML格式数据
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>
使用pull方式
private void parseXMLWithPull(String xmlData) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
int eventType = xmlPullParser.getEventType();
String id = "";
String name = "";
String version = "";
while (eventType != XmlPullParser.END_DOCUMENT) {
String nodeName = xmlPullParser.getName();
switch (eventType) {
// 开始解析某个结点, xml以树状的形式存储,每个app是一个节点
case XmlPullParser.START_TAG: {
if ("id".equals(nodeName)) {
id = xmlPullParser.nextText();
} else if ("name".equals(nodeName)) {
name = xmlPullParser.nextText();
} else if ("version".equals(nodeName)) {
version = xmlPullParser.nextText();
}
break;
}
// 完成解析某个结点
case XmlPullParser.END_TAG: {
if ("app".equals(nodeName)) {
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
break;
}
default:
break;
}
eventType = xmlPullParser.next();
}
} catch (Exception e) {
e.printStackTrace();
}
}
使用SAX方式
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
@Override// 开始解析xml文件时调用的方法
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
@Override// 开始解析某个节点时调用的方法
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
// 记录当前结点名
nodeName = localName;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// 根据当前的结点名判断将内容添加到哪一个StringBuilder对象中
if ("id".equals(nodeName)) {
id.append(ch, start, length);
} else if ("name".equals(nodeName)) {
name.append(ch, start, length);
} else if ("version".equals(nodeName)) {
version.append(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("app".equals(localName)) {
Log.d("ContentHandler", "id is " + id.toString().trim());
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
// 最后要将StringBuilder清空掉
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
解析JSON格式数据
[{"id":"5", "version":"5.5","name":"Clash of Clans"},
{"id":"6", "version":"7.0","name":"Boom Beach"},
{"id":"7","version":"3.5","name":"Clash Royale"}]
使用JSONObject
private void parseJSONWithJSONObject(String jsonData) {
try {
JSONArray jsonArray = new JSONArray(jsonData);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String id = jsonObject.getString("id");
String name = jsonObject.getString("name");
String version = jsonObject.getString("version");
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
} catch (Exception e) {
e.printStackTrace();
}
}
使用GSON
可以将一段JSON格式的字符串自动映射成一个对象。如果需要解析的是一段JSON数组会稍微麻烦一点,需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中。
使用Gson解析上面的json数据
首先新建一个类
package com.example.networktest;
public class App {
private String id;
private String name;
private String version;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}
private void parseJSONWithGSON(String jsonData) {
Gson gson = new Gson();
List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>() {}.getType());
for (App app : appList) {
Log.d("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
Android异步消息处理机制
- Message(消息的载体)
Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
- Handler
主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。
- MessageQueue
主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
- Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中也只会有一个Looper对象。
服务
服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
基本用法
启动和停止服务
startService与stopService分别对应启动和停止服务。与启动活动类似,首先要构建一个Intent对象,再将其作为参数传入对应的方法当中。
- onCreate()方法会在服务创建的时候调用
- onStartCommand()方法会在每次服务启动的时候调用
- onDestroy()方法会在服务销毁的时候调用
活动与服务之间进行通信
onBind()-------bind(绑定)
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
// 在服务与活动成功绑定时进行调用
public void onServiceConnected(ComponentName componentName, IBinder service) {
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
// 在解绑时进行调用
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button startService = (Button) findViewById(R.id.start_service);
Button stopService = (Button) findViewById(R.id.stop_service);
startService.setOnClickListener(this);
stopService.setOnClickListener(this);
Button bindService = (Button) findViewById(R.id.bind_service);
Button unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
// 通过调用bindService进行绑定,最后一个参数表示在连接成功时自动进行服务创建,但是没有启动服务。
// bindIntent表示绑定那个服务,connetion可以在其中设置连接时调用的方法。
bindService(bindIntent, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(connection);
break;
default:
break;
}
}
}
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload(){
Log.d("MyService", "startDownload executed");
}
public int getProgress(){
Log.d("MyService", "getProgress executed");
return 0;
}
}
public MyService() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService", "onStartCommand executed");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService", "onDestroy executed");
}
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
}
以上面的程序为例:
任何一个服务在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后它们都可以获取到相同的DownloadBinder实例
生命周期
虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopSelf()方法,服务就会停止下来了
Android系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行
前台服务
服务的系统优先级还是比较低的,当系统出现内存不足的情况时,就有可能会回收掉正在后台运行的服务。如果你希望服务可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台服务。
使用IntentService
应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。
服务一旦启动之后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来。
为了解决程序员可能忘记上述两种情况,Android提供了IntentService
switch (view.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
// 通过调用bindService进行绑定,最后一个参数表示在连接成功时自动进行服务创建,但是没有启动服务。
// bindIntent表示绑定那个服务,connetion可以在其中设置连接时调用的方法。
bindService(bindIntent, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(connection);
break;
default:
break;
}
}
}
```java
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload(){
Log.d("MyService", "startDownload executed");
}
public int getProgress(){
Log.d("MyService", "getProgress executed");
return 0;
}
}
public MyService() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService", "onStartCommand executed");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService", "onDestroy executed");
}
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
}
以上面的程序为例:
任何一个服务在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后它们都可以获取到相同的DownloadBinder实例
生命周期
虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopSelf()方法,服务就会停止下来了
Android系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行
前台服务
服务的系统优先级还是比较低的,当系统出现内存不足的情况时,就有可能会回收掉正在后台运行的服务。如果你希望服务可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台服务。
使用IntentService
应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。
服务一旦启动之后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来。
为了解决程序员可能忘记上述两种情况,Android提供了IntentService