设计目的:
基于Android的位置服务和天气应用程序的设计与实现,实现位置和天气两大模块的综合性应用。
拟为Android系统量身打造一款气象软件,使用户可以简单地从获取网络上的气候信息,并且动态根据用户情况进行舒适度提示,软件的主要工作包括解析json、界面设计等。
设计内容:
设计一个天气预报程序,从互联网上的一些发布天气信息的网站获取天气数据,首先存储到本机的SQLite数据库中,然后显示到本机界面中,要求界面布局美观;可以对存储在数据库中的指定时间段的天气数据进行统计分析,生成统计图表。
天气实况播报系统的基本原理是通过天气API提供的天气查询接口,以Http请求方式从所提供的接口中提取天气播报的数据信息(json或XML格式)为客户端服务,客户端主要的处理工作是界面的设计及各个界面之间切换的逻辑处理
语言、开发环境、数据库、技术和服务器:
语言:java语言
开发工具:Android Studio、Android SDK及Android智能机
数据库:SQLite
技术:HTTP技术、基于位置的服务LBS的技术
服务器:借助天气API
系统功能模块设计:
本系统分为天气详情模块、天气趋势模块、天气舒适度模块及城市管理模块。
用例图:正在上传…重新上传取消
按照本软件的功能需求,拟为软件划分的大功能模块如图:
正在上传…重新上传取消
对于系统的模块主要划分为外部服务器气象数据获取,客户端辅助功能,客户端主要作为展示所用,具体分布如图:
正在上传…重新上传取消
1.天气详情模块:
依照用户的要求显示当前城市的天气信息。包括今日天气详细信息及未来六天的天气信息。
文件:MainActivity.java
运行结果:
正在上传…重新上传取消
正在上传…重新上传取消
既然是请求api解析数据,自然离不开HttpUrlConnection,首先封装一个工具包,表示根据指定地址网络请求得到数据,得到的是string字符串,再是json数据
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class NetworkUtil {
// 一天测试次数有限,慎用
public static final String URL_WEATHER = "https://tianqiapi.com/api?version=v1&appid=(你的appid)&appsecret=(你的appsecret)";
public static String getWeather() {
String result = "";
HttpURLConnection connection = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
// 连接网络
try {
URL urL = new URL(URL_WEATHER);
connection = (HttpURLConnection) urL.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
// 从连接中读取数据(二进制)
InputStream inputStream = connection.getInputStream();
inputStreamReader = new InputStreamReader(inputStream);
// 二进制流送入缓冲区
bufferedReader = new BufferedReader(inputStreamReader);
// 容器
StringBuilder stringBuilder = new StringBuilder();
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
result = stringBuilder.toString();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (connection != null) {
connection.disconnect();
}
if (inputStreamReader != null) {
try {
inputStreamReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
两个封装的实体用来存数据
import java.util.List;
/**
* TextView tv_city,tv_time,tv_weather,tv_week,tv_tem,tv_tem_low_high,tv_win,tv_air;7个
* ImageView iv_weather;//天气图标
*/
public class WeatherBean {
private String cityid;
private String city;//城市名称
private String update_time;//更新时间
private List<DayWeatherBean> data;//获取今日天气,get[0]
//toString(),get,set自行设置
}
接着根据api中data中的属性名选择性封装DayWeatherBean
import java.util.Arrays;
/**
* TextView tv_city,tv_time,tv_weather,tv_week,tv_tem,tv_tem_low_high,tv_win,tv_air;7个
* ImageView iv_weather;//天气图标
*/
public class DayWeatherBean {
private String wea;//天气
private String wea_img;//天气图标
private String week;//周几
private String tem;//温度
//tv_tem_low_high=tem2+tem1拼接一起
private String tem2;//低温
private String tem1;//高温
//tv_win=win+win_speed
private String[] win;//风力
private String win_speed;//风力等级
//tv_air=air+air_level+air_tips拼接一起
private String air;//
private String air_level;//
private String air_tips;//
使用handler来异步处理
开辟一个子线程,拿到网页数据传给handler
private void getWeather() {
// 开启子线程,请求网络
new Thread(new Runnable() {
@Override
public void run() {
// 请求网络
String weatherJson = NetworkUtil.getWeather();
// 使用handler将数据传递给主线程
Message message = Message.obtain();
message.what = 0;
message.obj = weatherJson;
mHandler.sendMessage(message);
}
}).start();
}
2.天气趋势模块:
能够根据用户的要求预测当前城市未来的天气走向(目前只有温度趋势图)。
文件:MainActivity.java、HoursWeatherBean.java、HourWeatherAdapter.java
运行结果:
正在上传…重新上传取消
在这里用RecyclerView展示一天中每个小时的天气,既然用到了RecyclerView,那么就离不开adapter,RecyclerView负责准备一个框框,adapter负责把什么数据传到框内,需要添加的RecyclerView
<LinearLayout
android:layout_width="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_height="100dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rlv_hour_weather"
android:background="@drawable/blackground"
android:alpha="0.75"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
id自定义名字,主要是在MainActivity里新增private RecyclerView rlvHourWeather;并findByView注册拿到之后就可以添加适配器了。
要得到每小时的数据,就要找一个对象,里面封装的是每小时的天气,就是说data数据0下标当天天气里还有一个对象,包了每个小时共24小时的详细数据。
正在上传…重新上传取消
所以,从此hours数据的结构得知有需要封装一个数据类,还是包在当日天气DayWeatherBean下的一个List。
此处这个数据类就叫做HoursWearBean,那么先修改当日天气DayWeatherBean,添加一个。
private List<HoursWeatherBean> hoursWeatherBeanList;属性,由于Gson会根据Json数据的属性名进行封装,所以就需要实现一个序列化接口implements Serializable,并添加注解,这样变量名就可以随便命名了。
@SerializedName("hours")
private List<> hoursWeatherBeanList;
接着就是每小时详细数据类的封装
HoursWeatherBean:
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class HoursWeatherBean implements Serializable {
@SerializedName("hours")
private String hours;
@SerializedName("wea_img")
private String weaImg;
@SerializedName("tem")
private String tem;
public String getHours() {
return hours;
}
public void setHours(String hours) {
this.hours = hours;
}
public String getWeaImg() {
return weaImg;
}
public void setWeaImg(String weaImg) {
this.weaImg = weaImg;
}
public String getTem() {
return tem;
}
public void setTem(String tem) {
this.tem = tem;
}
@Override
public String toString() {
return "HoursWeatherBean{" +
"hours='" + hours + '\'' +
", weaImg='" + weaImg + '\'' +
", tem='" + tem + '\'' +
'}';
}
}
适配器HourWeatherAdapter
(1)继承自RecyclerView.Adapter,构造HourWeatherAdapter
private Context mContext;
private List<HoursWeatherBean> mHoursWeatherBeans;//写Activity时传进来的List,需要展示的数据集合
public HourWeatherAdapter(Context context, List<HoursWeatherBean> hoursWeatherBeans) {
mContext = context;
this.mHoursWeatherBeans = hoursWeatherBeans;
}
(2)新建class类HourViewHolder
class HourViewHolder extends RecyclerView.ViewHolder {
TextView tvHours, tvTem;
ImageView ivWeather;
public HourViewHolder(@NonNull View itemView) {
super(itemView);
//绑定小页面的组件
tvHours = itemView.findViewById(R.id.tv_hours);
tvTem = itemView.findViewById(R.id.tv_tem);
ivWeather = itemView.findViewById(R.id.iv_weather);
}
}
(3)然后重写三个方法
@NonNull
@Override
public HourViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//绑定View,每小时数据的小页面
View view = LayoutInflater.from(mContext).inflate(R.layout.hour_item_layout, parent, false);
HourViewHolder hourViewHolder = new HourViewHolder(view);
return hourViewHolder;
}
@Override
public void onBindViewHolder(@NonNull HourViewHolder holder, int position) {
HoursWeatherBean hoursweatherBean = mHoursWeatherBeans.get(position);
//根据位置position传值
holder.tvTem.setText(hoursweatherBean.getTem()+"℃");
holder.tvHours.setText(hoursweatherBean.getHours().substring(0,2)+":00");
holder.ivWeather.setImageResource(WeatherImgUtil.getImgResOfWeather(hoursweatherBean.getWeaImg()));
}
@Override
public int getItemCount() {
return (mHoursWeatherBeans == null) ? 0 : mHoursWeatherBeans.size();
}
接着就是拿到Json数据,封装称为数据类,我代码中直接Gson封装成WeatherBean,里面封装有List,而DayWeatherBean中又有List,一层一层包着,当数据异步封装好后,在MainActivity添加属性
private HourWeatherAdapter mHourAdapter;//适配器
private RecyclerView rlvHourWeather;//RecyclerView
/**
* 每小时温度
* dayWeather.getHoursWeatherBeanList()==>24小时数据List
*/
mHourAdapter = new HourWeatherAdapter(this, dayWeather.getHoursWeatherBeanList());
rlvHourWeather.setAdapter(mHourAdapter);
//数据水平布局
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
rlvHourWeather.setLayoutManager(layoutManager);
3.天气舒适度模块:
可以根据用户的需求显示每日指数。每日指数信息包括穿衣、洗车、穿衣、紫外线、运动等生活指数,为用户的日常起居住行提供了参考建议。
文件:MainActivity.java、TipAdapter.java
运行结果:正在上传…重新上传取消
本部分所采用的数据也是天气API接口,功能实现的原理与申请天气数据的原理一样,都是异步请求URL地址。解析数据并设置到界面。当请求成功并返回数据后,就需要对返回的JSON数据进行解析,通过JSON数据的规则设置特定的解析方法,获取里面的天气信息。
4.城市管理模块:
能够让用户选择想要查询的城市,还支持以文本输人框方式来筛选查询城市,点触屏幕选中目标城市便可切换至该城市的天气显示界面。
文件:MainActivity.java、CityManager.java、SelectCity.java
运行结果:
正在上传…重新上传取消
正在上传…重新上传取消
城市管理的界面设计city_manager.xml
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/city_manager"/>
用到RecyclerView展示数据库保存的城市List
added_city.xml页面表示要展示的一个个城市数据:
适配器AddCityAdapter
界面布局设计完成接下来就是适配器AddCityAdapter
public class AddCityAdapter extends RecyclerView.Adapter<AddCityAdapter.AddViewHolder>
适配器就是决定在此城市管理页面的RecyclerView中展示哪个页面,跟之前一样,都有概括,基本的一些操作就是,添加一个构造方法
private Context mContext;
private List<CityBean> mCityBeans;
public AddCityAdapter(Context context, List<CityBean> cityBeans) {
mContext = context;
this.mCityBeans = cityBeans;
}
创建一个类class AddViewHolder extends RecyclerView.ViewHolder来绑定控件,此时在城市管理页面要求点击某个城市,需要传值到MainActivity,并获取指定点击城市的天气信息
所以适配器中添加一个点击事件,首先在适配器中添加接口
public interface OnItemClickListener {
/**
* 当RecyclerView某个被点击的时候回调
* @param view 点击item的视图
* @param position 点击得到的数据,参数自定义
*/
public void onItemClick(View view, int position);
}
private OnItemClickListener onItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
然后在适配器中的类AddViewHolder中绑定点击事件
class AddViewHolder extends RecyclerView.ViewHolder {
TextView cityCity,cityTem;//城市名,温度
public AddViewHolder(@NonNull View itemView) {
super(itemView);
//绑定控件
cityCity=itemView.findViewById(R.id.city_city);
cityTem = itemView.findViewById(R.id.city_tem);
//绑定点击事件
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener!=null) {
onItemClickListener.onItemClick(view,getLayoutPosition());
}
}
});
}
}
然后就是一如既往的重写三个方法
@NonNull
@Override
public AddViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//绑定控件,哪个页面展示在RecyclerView中,R.layout.added_city
View view = LayoutInflater.from(mContext).inflate(R.layout.added_city, parent, false);
AddViewHolder cityViewHolder = new AddViewHolder(view);
return cityViewHolder;
}
@Override
public void onBindViewHolder(@NonNull AddViewHolder holder, int position) {
CityBean cityBean = mCityBeans.get(position);
//控件传值
holder.cityCity.setText(cityBean.getName());
holder.cityTem.setText(cityBean.getTem());
}
@Override
public int getItemCount() {
return (mCityBeans == null) ? 0 : mCityBeans.size();
}
操作数据库
然后进行数据库工具的构建,要操作数据库,一般的方法得需要实体类吧,接着就是城市实体类的封装,当获取当前定位天气信息时,把此数据类保存到数据库
所以先构建城市实体类CityBean
public class CityBean {
private String name;//城市名称
private String[] area;//县/区
private String tem;//温度
private String updateTime;//更新时间
//构造方法自定义,主要看数据库操作和new时传值
public CityBean() {
}
public CityBean(String name) {
this.name = name;
}
public CityBean(String name, String tem, String updateTime) {
this.name = name;
this.tem = tem;
this.updateTime = updateTime;
}
//get和set、toString注释
}
接着就是数据库的CityDatabaseConfig,有数据库名,表名的信息
public class CityDatabaseConfig {
//数据库的名字
public static final String DATABASE_NAME = "cityDb";
//表名
public static final String TABLE_NAME = "city_weather";
}
然后是CityDbHelper继承自SQLiteOpenHelper
public class CityDbHelper extends SQLiteOpenHelper {
public CityDbHelper(Context context) {
//创建数据库CityDatabaseConfig.DATABASE_NAME
super(context, CityDatabaseConfig.DATABASE_NAME, null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
//创建表,字段(cityName text,tem text,updateTime text)
String sql = "create table if not exists "+CityDatabaseConfig.TABLE_NAME+"(cityName text,tem text,updateTime text)";
db.execSQL(sql);
Log.i(CityDatabaseConfig.DATABASE_NAME,"created success!!");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(CityDatabaseConfig.DATABASE_NAME,"updated success!!");
}
}
接着是数据库一些操作的工具
public class DBUtils {
private Context context;
private CityDbHelper helper;
/**
* 单例模式
* @param context
*/
public DBUtils(Context context) {
this.context = context;
helper = new CityDbHelper(context) {
};
}
/**
* 向表中插入
*
* @param
* @return
*/
public boolean insertData(String cityName,String tem,String updateTime) {
SQLiteDatabase db = helper.getReadableDatabase();
ContentValues values = new ContentValues();
values.put("cityName", cityName);
values.put("tem", tem);
values.put("updateTime", updateTime);
long insert = db.insert(CityDatabaseConfig.TABLE_NAME, null, values);
if (insert > 0) {
return true;
}
return false;
}
//根据城市名称更新数据
public boolean updateByName(String cityName,String tem,String updateTime){
SQLiteDatabase db = helper.getReadableDatabase();
ContentValues values = new ContentValues();
values.put("cityName", cityName);
values.put("tem", tem);
values.put("updateTime", updateTime);
long update = db.update(CityDatabaseConfig.TABLE_NAME,values,"cityName=?", new String[]{cityName});
if (update>0){
return true;
}return false;
}
/**
* 查询全部的信息
*
* @return
*/
public List<CityBean> getAllCity() {
List<CityBean> list = new ArrayList<>();
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = db.rawQuery("select * from " + CityDatabaseConfig.TABLE_NAME, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String cityName = cursor.getString(0);
String tem = cursor.getString(1);
String updateTime = cursor.getString(2);
CityBean cityBean = new CityBean(cityName,tem,updateTime);
list.add(cityBean);
}
}
return list;
}
/**
* 根据城市名查询一条信息
*/
public CityBean getCityByName(String cityName) {
CityBean cityBean = new CityBean();
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = db.rawQuery("select * from " + CityDatabaseConfig.TABLE_NAME + " where cityName=?", new String[]{cityName});
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToFirst()) {
String cityNameResult = cursor.getString(0);
cityBean.setName(cityName);
break;
}
}
return cityBean;
}
/**
* 根据城市名删除一条数据
* @param cityName
* @return
*/
public boolean delCityByName(String cityName){
SQLiteDatabase db = helper.getReadableDatabase();
long delRow = db.delete(CityDatabaseConfig.TABLE_NAME, "cityName=?", new String[]{cityName});
if (delRow > 0) {
return true;
}
return false;
}
}
跳转页面,展示数据
一些基本的方法写好后就是MainActivity.java文件,用Intent跳转到城市管理界面,注意先引入数据库操作工具
DBUtils dbUtils = new DBUtils(MainActivity.this);
设置右上角图标的点击事件,此小圆点图标绑定为ivMore
ivMore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (null == dbUtils.getCityByName(tvCity.getText().toString()).getName()) {
//数据库不存在此城市就插入
dbUtils.insertData(tvCity.getText().toString(),tvTem.getText().toString(),tvTime.getText().toString());
}
else {
//存在就更新
dbUtils.updateByName(tvCity.getText().toString(),tvTem.getText().toString(),tvTime.getText().toString());
// dbUtils.insertData("北京","30°C","2022-06-19 21:23:35");测试用
// dbUtils.delCityByName("商丘");测试用
List<CityBean> list = dbUtils.getAllCity();
Log.d("getAllCity", "allList<CityBean>>>>》》》" + list.toString());
ToastUtil.showShortToast(MainActivity.this, "当前位置:" + tvCity.getText());
Intent intent = new Intent(MainActivity.this, CityManagerActivity.class);
Log.d("getNowCity", "nowCity<CityBean>>>>》》》" + tvCity.getText());
// 要求跳转页面传值回来,requestCode:200;
startActivityForResult(intent, 200);
}
}
});
跳转到城市管理页面,添加activity.java文件,命名CityManagerActivity
public class CityManagerActivity extends AppCompatActivity {
private AddCityAdapter addCityAdapter;//适配器
private RecyclerView rvAddedCity;
private List<CityBean> cityDbBeanList;
DBUtils dbUtils= new DBUtils(CityManagerActivity.this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.city_manager);
initView();
}
public void initView() {
rvAddedCity = findViewById(R.id.city_manager);
cityDbBeanList = dbUtils.getAllCity();
Log.d("cityList",">>>>>数据库中的数据》》》》》》"+cityDbBeanList);
addCityAdapter = new AddCityAdapter(CityManagerActivity.this,cityDbBeanList);
rvAddedCity.setAdapter(addCityAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
rvAddedCity.setLayoutManager(layoutManager);
//设置点击事件
addCityAdapter.setOnItemClickListener(new AddCityAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
// ToastUtil.showShotToast(CityManagerActivity.this, cityDbBeanList.get(position).getName());
Intent intent = new Intent(CityManagerActivity.this,MainActivity.class);
intent.putExtra("selectedCity",cityDbBeanList.get(position).getName());
//返回MainActivity页面并传值setResult(200,intent);设置resultCode:200;
setResult(200,intent);
finish();
// ToastUtil.showShotToast(CityManagerActivity.this,"finish");
}
});
}
}
既然是要求页面返回传值,MainActivity就要重写方法onActivityResult接受传来的值
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 200 && resultCode == 200) {
//getString拿到传来的值
String dt = data.getExtras().getString("selectedCity");
tvCity.setText(dt);
nowCity=dt;
//重新从api根据传来的城市名称获取天气
getWeather(nowCity);
ToastUtil.showLongToast(this,dt+"天气更新😘~");
}
}
数据库设计:
城市信息表city_weather
列名 | 数据类型 | 允许空 | 说明 |
cityName | String | N | 城市名 |
tem | String | Y | 城市温度 |
updateTime | String | Y | 更新时间 |
系统界面设计:
1.本天气播报系统是运行在手机客户端的软件,继承手机软件的统一界面风格,力求简洁、方便、良好的用户体验。窗口布局需要简洁整齐而不追求华丽。
2.界面上的控件外观一致使用符合手机软件的风格,主界面采用了通过切换底部菜单进行界面的切换,简洁方便实用。除此之外,一些需要有特效的界面(如气温折线图等)还必须通过自定义View(即重写Android提供的视图控件View、ViewGroup等)进行实现。
3.由于此天气播报系统是运行在Android手机设备上的,因此界面上的所有模块都应支持屏幕触摸和键盘操作(其实只包含返回键和、Home键)的两种操作方式,因此关键业务主要应支持屏幕触摸操作。
附录: