一、前言
我们都知道不能用子线程来更新UI,否则可能引起主线程阻塞造成ApplicationNotResponseException。一般我们可以通过Handler机制,在子线程中不断给Handler对象发送消息来达到动态更新UI的目的。另外,也可以通过Service+BroadcastReceiver的方式来实现动态更新UI甚至定时更新UI ,功能更为广泛和强大。小弟初来乍道,讲得不好还望海涵 ^_^
二、原理
好了,切入正题。
很多时候我们希望实时动态地更新UI,如数字时钟,每一秒都有变化;还有每隔一段时间更新的要求,如天气预报,适合以小时间隔来更新。以下是我正在做的一个小项目的效果图,之后就按图中的效果来讲解。
左边是一个ImageView控件来显示天气图片,中间上下两个TextView分别显示天气情况和温度,都属于天气信息,每小时联网更新一次;右边上下两个TextView分别显示日期和时间,每秒更新一次。
Service+BroadcastReceiver的基本思路:在需要被更新的UI所在的Activity中开启Service,并在该Activity中注册广播过滤器和广播接收器,以接收Service发送的广播;在Service中定时发送广播;广播接收器接收到Service一定时间发送的广播来执行更新UI动作。
三、代码及注释
看代码前,我先贴出项目的类的结构,因为我在看别人博客这样做的时候,会更明晰一些:)
每个类的功能应该能见文知意,若不明白后面会详解。先讲实时动态更新时间,相对简单。
(一)实时动态更新时间
在需要被更新的UI所在的Activity(我这里是MainActivity)的onCreate()方法里开启服务:
/**
* 启动更新时间服务
*/
private void startTimeService() {
Intent timeService = new Intent(this, UpdateTimeService.class);
startService(timeService);
}
另外,请记得在Activity的onDestroy()方法中停止服务:
@Override
protected void onDestroy() {
stopService(new Intent(MainActivity.this, UpdateTimeService.class));//停止更新时间服务
super.onDestroy();
}
package app.weather_date;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class UpdateTimeService extends Service {
@Override
public void onCreate() {
super.onCreate();
//定时器每隔1秒发送一次广播
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Intent timeIntent = new Intent();
timeIntent.setAction("TIME_CHANGED_ACTION");//自定义Action
sendBroadcast(timeIntent); //发送广播
}
}, 0, 1000);{ //每隔1秒
};
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
public class MyTimeBroadcast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
updateTime();//更新时间的操作
}
}
注意: 更新时间广播接收器 broadcastReceiver写成Activity的内部类,这个onReceiver可以直接调用activity 的方法来更新界面 。但是内部类只能采用代码注册的方法registerReceiver(),不能在AndroidManifest.xml文件中进行静态的声明,因为内部类要依赖于外部类而存在的。
接着在MainActivity的onStart()方法中实例化刚才的MyTimeBroadcast并创建广播过滤器,然后注册广播接收器:
@Override
protected void onStart() {
// 更新时间/天气广播接收器
mTimeBroadcast = new MyTimeBroadcast();
// 过滤出系统发送的时间改变的广播
IntentFilter filter1 = new IntentFilter();
filter1.addAction("TIME_CHANGED_ACTION");
// 注册广播
registerReceiver(mTimeBroadcast, filter1);
updateTime();// 第一次启动先更新时间
super.onStart();
}
/**
* 更新时间
*/
private void updateTime() {
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd");
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
String dateStr = dateFormat.format(date);
String timeStr = timeFormat.format(date);
this.date.setText(dateStr);//显示出日期
this.time.setText(timeStr);//显示出时间
}
(二)定时更新天气信息
定时更新UI由于时间较长,如果不想让服务在这期间一直在后台运行,而是到了时间设定的时间自动启动,有什么办法呢?看到这里是不是很像一个闹钟的功能呢?哈哈所以我想到了利用闹钟机制来启动服务,只有到设定的时间才启动,而不是一直在后台,并且我让服务发送完更新天气的广播后主动关闭自己,等待下一个”闹钟“,从而节约了手机资源。但这里的代码间关系比较复杂,一定要前后结合起来看。
同样,在MainActivity的onCreate()方法中启动Service,我在其中还做了一些逻辑的优化,看代码和注释:
SharedPreferences sharedPreferences;
SharedPreferences.Editor editor;
long updatetime = 0;//最近一次的更新时间
int lastWeaImg;//最近一次更新的天气图片
String lastWeaText;//最近一次更新的天气情况
String lastTemp;//最近一次更新的温度
/**
* 启动更新天气服务 每隔一小时更新一次,时间跨度较长,可以通过闹钟机制一小时后启动服务,优化性能
*/
private void startWeatherService() {
sharedPreferences = getSharedPreferences("time_data",
Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
// 设置阻隔时间10分钟,避免频繁进开启服务界面造成频繁访问网络
if (System.currentTimeMillis() -
sharedPreferences.getLong("time", 0) > 600 * 1000) {
Intent weatherService = new Intent(this, UpdateWeatherService.class);
startService(weatherService);// 启动更新天气服务
// 通过闹钟机制一小时后启动服务
Intent intent = new Intent(this, TimerBroadcastReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1,
intent, 0);
AlarmManager almManager = (AlarmManager) getSystemService(ALARM_SERVICE);
almManager.set(AlarmManager.RTC, // RTC 在指定的时刻,发送广播,但不唤醒设备
System.currentTimeMillis() + (1000 * 3600), pendingIntent);// 一小时
updatetime = System.currentTimeMillis();
editor.putLong("time", updatetime);//保存最近更新时间
editor.commit();
} else {//否则加载最新一次更新的数据
lastWeaImg = sharedPreferences.getInt("lastWeaImg", 0);
lastWeaText = sharedPreferences.getString("lastWeaText", null);
lastTemp = sharedPreferences.getString("lastTemp", null);
this.weatherImg.setImageResource(weatherImgs[lastWeaImg]);
this.weatherText.setText(lastWeaText);
this.temperature.setText(lastTemp);
}
}
这里初次进入MainActivity是需要更新天气的,所以开了一次服务,UpdateWeatherService.java:
package app.weather_date;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.IBinder;
public class UpdateWeatherService extends Service {
WeatherInfo weatherInfo;// 天气信息
boolean isRunning = true;
/**
* 子线程判断联网获取数据成功后,发送广播
*/
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
while (isRunning) {
if (weatherInfo.img != null) {
updatePerHour();
isRunning = false;
}
}
}
};
@Override
public void onCreate() {
super.onCreate();
if (isNetWorkAvailable()) {
weatherInfo = new WeatherInfo();
new Thread(mRunnable).start();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
// 每隔一小时定时发送更新天气广播
private void updatePerHour() {
Intent weatherIntent = new Intent();
weatherIntent.setAction("WEATHER_UPDATE_ACTION");// 自定义Action
Bundle bundle = new Bundle();
bundle.putInt("weatherImg", Integer.parseInt(weatherInfo.img));
bundle.putString("weather", weatherInfo.weather);
bundle.putString("temperature", weatherInfo.temperature);
weatherIntent.putExtras(bundle);
sendBroadcast(weatherIntent);
stopSelf();// 发送广播后,停止自己,优化资源
}
/**
* 检查网络状态
*/
private boolean isNetWorkAvailable() {
ConnectivityManager connManager = (ConnectivityManager) getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo[] netInfos = connManager.getAllNetworkInfo();
if (netInfos != null) {
for (int i = 0; i < netInfos.length; i++) {
if (netInfos[i].getState() == NetworkInfo.State.CONNECTED) {
return true;
}
}
}
return false;
}
}
package app.weather_date;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import org.json.JSONException;
import org.json.JSONObject;
public class WeatherInfo {
public String city;
public String weather;
public String temperature;
public String img;
public WeatherInfo() {
new Thread(new Runnable() {
@Override
public void run() {
//android API >= 10 之后联网操作必须放在子线程,
//否则会抛NetworkOnMainThreadException异常
getWeather();
}
}).start();
}
/**
* 联网获得指定城市天气ID的json,并解析出相应的天气信息
* 101270101为成都
*/
private void getWeather() {
try {
URL url = new URL("http://m.weather.com.cn/atad/101270101.html");
InputStream ips = url.openStream();//输入流
ByteArrayOutputStream bos = new ByteArrayOutputStream();//输出流
//边读边写
int len = -1;
byte[] buff = new byte[1024];
while ((len = ips.read(buff)) != -1){
bos.write(buff, 0, len);
}
String info = bos.toString("utf-8");
JSONObject jsonObj = new JSONObject(info);
JSONObject json = jsonObj.getJSONObject("weatherinfo");
city = json.getString("city");
weather = json.getString("weather1");
temperature = json.getString("temp1");
img = json.getString("img1");
bos.flush();
//关闭流操作
ips.close();
bos.close();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}
package app.weather_date;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class TimerBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//开启UpdateWeatherService服务
intent.setClass(context, UpdateWeatherService.class);
context.startService(intent);
//通过闹钟机制一小时后启动本广播接收器,实现以后每隔1小时启动本接收器,开启服务
Intent intent2 = new Intent(context, TimerBroadcastReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1,
intent2, 0);
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, //RTC 在指定的时刻,发送广播,但不唤醒设备
System.currentTimeMillis() + (1000 * 3600), pendingIntent);
}
}
@Override
protected void onStart() {
// 更新时间/天气广播接收器
mTimeBroadcast = new MyTimeBroadcast();
mWeatherBroadcast = new MyWeatherBroadcast();
// 过滤出系统发送的时间改变的广播
IntentFilter filter1 = new IntentFilter();
filter1.addAction("TIME_CHANGED_ACTION");
IntentFilter filter2 = new IntentFilter();
filter2.addAction("WEATHER_UPDATE_ACTION");
// 注册广播
registerReceiver(mTimeBroadcast, filter1);
registerReceiver(mWeatherBroadcast, filter2);
updateTime();// 第一次启动先更新时间
super.onStart();
}
@Override
protected void onDestroy() {
stopService(new Intent(MainActivity.this, UpdateTimeService.class));
stopService(new Intent(MainActivity.this, UpdateWeatherService.class));
unregisterReceiver(mTimeBroadcast);
unregisterReceiver(mWeatherBroadcast);
super.onDestroy();
}
最后的最后,联网等操作需要权限,都在manifest.xml里了:
至此,实时动态更新时间和定时更新天气预报功能实现完毕。作为初学者讲解之处难免有诸多纰漏,请看官勿喷。
由于该功能是我小项目里一个小功能,所以源码不方便提供。但以上我已经尽量地详细讲解了,如果你是初学者的话,相信根据我的思路并自己查阅资料,应该能运行起来。若还有疑惑的可评论留言,看我能不能帮到你。