Android学习笔记--Service+BroadcastReceiver实现动态更新UI和定时更新UI (如时间和天气预报)

一、前言

我们都知道不能用子线程来更新UI,否则可能引起主线程阻塞造成ApplicationNotResponseException。一般我们可以通过Handler机制,在子线程中不断给Handler对象发送消息来达到动态更新UI的目的。另外,也可以通过Service+BroadcastReceiver的方式来实现动态更新UI甚至定时更新UI ,功能更为广泛和强大。小弟初来乍道,讲得不好还望海涵 ^_^

二、原理

好了,切入正题。

很多时候我们希望实时动态地更新UI,如数字时钟,每一秒都有变化;还有每隔一段时间更新的要求,如天气预报,适合以小时间隔来更新。以下是我正在做的一个小项目的效果图,之后就按图中的效果来讲解。

Android学习笔记--Service+BroadcastReceiver实现动态更新UI和定时更新UI (如时间和天气预报)_第1张图片

左边是一个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();
	}

服务 UpdateTimeService.java:

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;
	}
}

回到MainActivity中,在该Activity里定义一个继承自BroadcastReceiver的内部类:

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();
	}

更新时间的方法updateTime()方法很简单,就是获取时间并时间格式化,然后为TextView设置文字属性:

/**
	 * 更新时间
	 */
	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;
	}
}

顺便把如何获取天气信息的WeatherInfo类贴出来,关于获取天气信息,可以自行搜索其他博客,有讲这个的专题,我这里就只抛砖引玉一下:

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();
		}
	}
}

初次启动服务后,我紧接着设置了一个“闹钟”,一小时后让另一个独立的广播接收器(TimerBroadcastReceiver.java)去启动更新天气的服务,从而不必依赖当前的Activity了。会发现我在这个广播接收器里又设置了一个“闹钟”,一小时后启动自己,然后开启服务,又设置一个周期为一小时的“闹钟”……如此循环,就达到了,周期定时的目的了:

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);
	}
}

最后,在MainActivity的onStart()中实例化MyWeatherBroadcast并创建天气更新过滤器,注册天气更新广播接收器;另外在onDestroy()中取消注册并停止服务。最终代码为:

@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里了:




    
    
	
    
    
    
        
            
                

                
            
        
        
        
        
        
    



四、结语

至此,实时动态更新时间和定时更新天气预报功能实现完毕。作为初学者讲解之处难免有诸多纰漏,请看官勿喷。

由于该功能是我小项目里一个小功能,所以源码不方便提供。但以上我已经尽量地详细讲解了,如果你是初学者的话,相信根据我的思路并自己查阅资料,应该能运行起来。若还有疑惑的可评论留言,看我能不能帮到你。

你可能感兴趣的:(动态更新UI,学习笔记)