掌握高级技巧
Context用到的地方很多:弹出Toast,启动活动,发送广播,操作数据库,使用通知,等等等等。
活动本身就是一个Context对象。当应用程序的架构逐渐复杂,逻辑代码脱离Activity类,需要获取Context对象。比如,在第9章编写的HttpUtil类(封装了通用的网络操作),代码如下:
public class HttpUtil {
public static void sendHttpRequest(final String address, final HttpCallbackListener listener) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
if (listener != null) {
listener.onFinish(response.toString());
}
} catch (Exception e) {
e.printStackTrace();
if (listener != null) {
listener.onError(e);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}
为了解决在HttpUtil类中获取Context对象,我们在sendHttpRequest()方法中添加Context参数,修改HttpUtil类中的代码如下:
public class HttpUtil {
public static void sendHttpRequest(Context context, final String address, final HttpCallbackListener listener) {
if (!isNetworkAvailable()) {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
...
}).start();
}
//用于判断当前网络状态是否可用
private static boolean isNetworkAvailable() {
...
}
}
在项目的任何地方都有能够轻松获取到Context,Android提供了一个Application类,每当启动应用程序,系统会自动初始化这个类。因而定制自己的Application类,便于管理内部的全局状态信息,如Context。
创建一个MyApplication类继承自Application,代码如下:
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
接下来我们在AndroidManifest.xml中application指定(注意,完整的包名加类名),代码如下:
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
package="com.example.hjw.networktest">
...
"com.example.hjw.networktest.MyApplication"
...
>
...
这样就实现了全局获取Context机制,使用MyApplication.getContext()就Ok,如下:
public static void sendHttpRequest( final String address, final HttpCallbackListener listener) {
if (!isNetworkAvailable()) {
Toast.makeText(MyApplication.getContext(), "network is unavailable", Toast.LENGTH_SHORT).show();
return;
}
...
}
在第6章中,为了让LitePal正常工作(内部自动获取Context),必须在AndroidManifest.xml中配置如下:
"org.litepal.LitePalApplication"
...>
...
任何一个项目中只能配置一个Application,为了解决MyApplication和LitePalApplication发生冲突,我们可以在MyApplication中调用LitePal初始化方法(效果和直接在AndroidManifest.xml配置的效果一样),如下:
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
LitePalApplication.initialize(context);
}
public static Context getContext() {
return context;
}
}
Intent用法:启动活动,发送广播,启动服务等。还可以在进步上述操作的时候在Intent中添加一些附加数据,以达到传值的效果,比如在FirstActivity中添加如下代码:
Intent intent=new Intent(FirstActivity.this,SencondActivity.class);
intent.putExtra("string_data","Hello");
intent.putExtra("int_data",100);
startActivity(intent);
在SencondActivity中取值,如下:
getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data");
putExtra()所支持的数据类型是有限的,传递自定义对象就无从下手。
使用Intent来传递的对象的两种方式Serializable 和 Parcelable。
Serializable序列化,表示将传入一个对象转换成可储存或可输入的状态。序列化后的对象可在网路进行传输,也可存储到本地。
实现序列化的方法,让一个类实现Serializable 接口(这样所有的Person对象都可是序列化的了)。
比如Person类,包含name和age两个字段,将它序列化如下:
public class Person implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
接下来在FristActivity中的代码如下:
Person person = new Person();
person.setName("Jack");
person.serAge(23);
Intent intent=new Intent(FirstActivity.this,SencondActivity.class);
intent.putExtra("string_data","Hello");
intent.putExtra("person_data",person);
startActivity(intent);
在SecondActivity中获取对象(通过向下转型),如下:
Person person = (Person)getIntent.getSerializableExtra("person_data");
Parcelable也可以实现同样的效果,实现原理将一个完整对象进行解析,而分解后的每一个对象都是Intent所支持的数据类型,也可以实现传递对象的功能。
Parcelable的实现方式,修改Person中的代码如下:
public class Person implements Parcelable {
private String name;
private int age;
...
@Override
public int describeContents() {
//直接返回0
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
//将Person中的字段一一写出
dest.writeString(name);
dest.writeInt(age);
}
protected Person(Parcel in) {
name = in.readString();
age = in.readInt();
}
public static final Creator CREATOR = new Creator() {
@Override
public Person createFromParcel(Parcel in) {
return new Person(in); //读取所写入的字段
}
@Override
public Person[] newArray(int size) {
return new Person[size]; //传入数组大小
}
};
}
在FristActivity中使用相同的代码传递Person对象,在SecondActivity中的获取都对象如下:
Person person = (Person)getIntent.getParcelableExtra("person_data");
通常情况下更加推荐使用Parcelable(效率高)的方式来实现Intent传递对象。
自由的控制日志的打印,当程序处于开发阶段就让日志打印出来,当程序上线了之后屏蔽日志(从而提升程序运行的效率)。
定义一个自己的日志工具类,新建LogUtil类,代码如下:
public class LogUtil {
public static final int VERBOSE=1;
public static final int DEBUG=2;
public static final int INFO=3;
public static final int WARN=4;
public static final int ERROR=5;
public static final int NOTHING=6;
public static int level=VERBOSE;
public static void v(String tag,String msg){
if (level<=VERBOSE){
Log.v(tag,msg);
}
}
public static void d(String tag,String msg){
if (level<=DEBUG){
Log.d(tag,msg);
}
}
public static void i(String tag,String msg){
if (level<=INFO){
Log.i(tag,msg);
}
}
public static void w(String tag,String msg){
if (level<=WARN){
Log.w(tag,msg);
}
}
public static void e(String tag,String msg){
if (level<=ERROR){
Log.e(tag,msg);
}
}
}
只有当level(上面6个随便指定)值小于或对应日志级别,才会打印日志,当level等于NOTHING时屏蔽所有的日志。
比如打印一行DEBUG日志,如下:
LogUtil.d("TAG","debug log");
使用Android Studio调试第5章强制下线功能来学习调试方法。比如这个登录出现了问题,我们就可以通过调试找不问题的所在。
调试工作的第一步就是添加断点(点击代码行的左边添加或取消),然后点击Android Studio顶部工具栏中的Debug按钮,如图(最右边的按钮),使用调试模式来启动程序。
运行程序,首先你会看到一个提示框,如下:
这个框很快消失,输入账号密码后,点击Login,这是Android Studio自动打开Debug窗口,如下:
接下来每按一次F8,代码就会向下执行一行,并且通过Variables视图查看到内存中的数据,如下:
我们输入的账号密码分别是admin和admin,而程序里要求的是admin和123456,登录就会问题。调试完成之后,点击Debug窗口中的Stop按钮(最下边的按钮),如图:
Android提供了一种随时进入到调试模式的方式。正常启动程序,先输入好账号密码。然后点击Android Studio顶部的工具栏的 Attach debugger to Android process按钮(最左边的按钮),如下:
此时会弹出一个进程选择提示框,如图:
选中当前应用程序的进程,点击Ok。
接下来点击Login按钮,Android Studio同样也会打开Debug窗口。(更加灵活,更加常用)。
Android中的定时任务一般有两种方式:
注意:唤醒CPU和唤醒屏幕完全不是一个概念。
Alarm机制的用法,借助AlarmManager类实现(与NotificationManager有点类似),用过调用Context的getSystemService()方法来获取AlarmManager的实例。
传入Context.ALARM_SERVICE。实例如下:
AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
接下来调用AlarmManager中的set()方法,来设置定时任务。这个方法传入3个参数,第一个参数是一个整型参数,用于指定AlarmManager的工作类型(4种可选值,ELAPSED_REALTIME表示定时任务的触发时间从系统开机开始算起,但不会唤醒CPU;ELAPSED_REALTIME_WAKEUP同样表示定时任务的触发时间从系统开机开始算起,但会唤醒CPU;RTC表示定时任务的触发时间从1970年1月1日0点至今所经历的毫秒数,但不会唤醒CPU;RTC_WAKEUP同意表示定时任务的触发时间从1970年1月1日0点至今所经历的毫秒数,但会唤醒CPU)。使用SystemClock.elapsedRealtime()方法可获取到系统开机至今所经历的毫秒数,使用System.currentTimeMillis()方法可获取从1970年1月1日0点至今所经历的毫秒数;
第二个参数定时任务触发的时间(毫秒);第三个参数是一个PendingIntent(调用getService()或getBroadcast()来获取一个能够执行服务或广播的PendingIntent),当定时任务出发的时候,服务的onStartCommand()或广播接收器的onReceive()可以得到执行。使用如下:
设置任务在10秒之后执行:
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);
也可写成:
long triggerAtTime = System.currentTimeMillis() + 10 * 1000;
manager.set(AlarmManager.RTC_WAKEUP,triggerAtTime,pendingIntent);
实现一个长时间在后台定时运行的服务:新建一个普通的服务。LongRunningService,在onStartCommand()方法中实现触发定时任务的逻辑,如下所示:
public class LongRunningService extends Service {
@Override
public IBinder onBind(Intent intent) {
throw null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
//执行具体的逻辑操作
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int auHour = 60 * 60 * 1000;
long triggerAtTime = SystemClock.elapsedRealtime() + auHour;
Intent i = new Intent(this, LongRunningService.class);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
return super.onStartCommand(intent, flags, startId);
}
}
一旦启动了LongRuningService,就会在onStartCommand()设置了一个定时任务,保证了LongRunService的onStartCommand()每隔一个小时就会启动一次(永久循环)。
调用启动定时服务,如下:
Intent intent = new Intent(context, LongRunningService.class);
startService(intent);
注意:Android4.4系统开始,由于系统在耗电性方面的优化(系统自动检测目前有多少个Alarm任务存在,然后将出发几个相近的几个任务一起执行,大幅度减少CPU唤醒的次数,从而有效延长电池的使用时间),Alarm任务的触发时间将会变得不准确。
使用AlarmManager的setExact()代替set(),就可保证任务能够准确执行。
为了解决后台服务滥用,手机电量消耗过快的问题。在Android系统6.0中加入了全新的Doze模式,从而极大幅度地延续电池的使用寿命。
Doze模式,当用户的设备是Android6.0系统或以上时,如果该设备未插入电源,处于静止状态(Android7.0中删除了这一条件),且关闭屏幕一段时间之后,就会进入Doze模式。在Doze模式下,系统会对CPU,网络,Alarm等活动进行限制,从而延长电池的使用寿命。
当然,系统并不会一直处于Doze模式,而是会间接性地退出Doze模式一小段时间,在这段时间中,应用程序就可以完成他们的同步操作,Alarm任务,等等。
如图:描述了Doze模式的工作流程。
可以看到,随着设备进入Doze模式的时间越长,间歇性地退出Doze模式的时间间隔也会越长。因为长时间不适用设备的话,是没必要频繁的退出来执行同步等操作的使得电池寿命的延长。
Doze模式下限制的功能如下:
特殊情况,在Doze模式下Alarm任务必须正常执行,调用AlarmManager中的sendAllowWhileIdle()或setExactAndAllowWhileIdle()。区别同于set(),setExact()。
Android7.0系统中引入了一个非常特色的功能——多窗口模式。
我们不需要编写任何额外的代码来让应用程序支持多窗口模式。系统化的了解多窗口模式才能编写出在多窗口模式下兼容性更好的应用程序。
手机导航栏(3个按钮)如下:
左边Back按钮,中间Home按钮,右边Overview按钮(打开近期访问程序的列表,方便多个应用程序之间的切换)。
进入多窗口模式两种方式:
如图:
模拟器旋转至水平,效果图如下:
退出多窗口模式,长按Overview按钮或将屏幕的分割线向屏幕任意方向拖到底。
使用math_parent属性,RecycleView,ListView,ScrollView等控件,来让应用的界面能够更好地适配各种不同尺寸的屏幕。
多窗口模式不会改变原有的生命周期,只是会将用户交互的那个活动设置为运行状态,而多窗口模式下的另一个可见的活动设置为暂停状态。
首先打开我们的MaterialTest项目,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MaterialTest";
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
...
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart: ");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause: ");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop: ");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG, "onRestart: ");
}
...
}
在Android Studio导航栏上的File—>Open Recent—>任意项目,修改MainActivity中的代码同上。
分别重新运行这两个项目到模拟器上,启动MaterialTest程序观察Logcat日志(注意要将logcat的过滤器选择为No Filters)。如下:
接下来我们长按Overview按钮,进入多窗口模式,此时logcat如下:
接下来在Overview界面选择CameraAlbumTest这个项目,此时如下:
接下来我们来操作MaterialTest,观察logcat如下:
这就是多窗口模式下的生命周期,
比如播放视频,我们在onStop()处理视频播放暂停的逻辑,onStart()方法中处理恢复播放视频的逻辑。
进入多窗口模式时活动会被重新创建(默认),为了不管进入多窗口模式,还是横竖屏切换,活动动不会被重新创建,加入下面一行代码:
<activity
android:name=".MainActivity"
android:label="Fruits"
android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
activity>
这样,将屏幕发生变化的事件通知到Activity中的onConfigurationChanged()中(处理相应的逻辑)。
关闭这个功能,在AndroidManifest.xml中的< application/>或< activity/>标签添加如下属性:
android:resizeableActivity=["true" | "false"]
true表示支持多窗口模式(默认),false表示不支持多窗口模式(Toast出不支持分屏)。
解决targetSdkVersion低于24,活动是不允许横竖屏切换的,禁用多窗口模式。
不允许横竖屏切换,在AndroidManifest.xml中的< activity/>标签添加如下属性:
android:screenOrientation=["portrait" | "landscape"]
portrait表示只支持竖屏,landscape表示只支持横屏。(常用值)
这样我们就解决了targetSdkVersion低于24,禁用多窗口模式问题了。
Java8中的新特性:Lambda表达式(兼容到Android系统2.3),stream API(只支持Android系统7.0及以上),接口默认实现(只支持Android系统7.0及以上),等等。
Lambda表达式,是一种匿名方法,它既没有方法,特没有访问修饰符和返回值类型(编写代码更加简洁,更加易读)。
在Android项目中使用Java8新特性,首先需要在app/build.gradle中添加配置如下:
android {
...
defaultConfig {
...
jackOptions.enabled = true
}
compileOptions{
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Lambda表达式来编写代码,比如开启一个子线程写法代码如下:
new Thread(() ->{
//处理逻辑
}).start();
之所以这样写,Runable写法如下:
Runnable runnable=() ->{
//添加具体的实现
};
Lambda表达式来定义接口。
新建接口MyListener接口,如下:
public interface MyListener {
String doSomething(String a,int b);
}
Lambda表达式实现(自动推判参数)如下:
MyListener Listener=(a,b) ->{
return a+b;
};
举例:定义一个方法接收MyListener参数,如下:
public void hello(MyListener listener){
String a="Hello Lamdba";
int b=1024;
String result=listener.doSomething(a,b);
Log.d(TAG, "hello: "+result);
}
调用hello()方法如下:
hello((a,b) ->{
return a+b;
});
打印结果是“Hello Lamdba1024”。
设置点击事件使用Lambda表达式,代码如下:
button=(Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
});
本章学习了如何全局获取Context,定制日志工具,调试程序,多窗口编程模式,Lambda表达式等高级技巧。
本书中学习了四大组件,UI,碎片,数据存储,多媒体,网络,定位服务,Material Design等。