Android案例学开发,天气记事本项目学习总结。
之前一直没怎么做过涉及数据库的应用,只会书上讲的的基础方法进行增删改查,举得挺费事的。
最近学了greenDAO,就试着结合以前学的写个记事本的小应用练手,顺便巩固一下之前所学。
项目很简单,CollapsingToolbarLayout 配合 CoordinatorLayout 使用。
效果图:
**
**
**
**
我觉得这种控件就得自己保存一个样例,不然时间一长不去用,随之就会忘掉一些属性的用法。
内容用Tablayout+ ViewPager来展示数据。
TabLayout有两种设置标签的方式:
第一种
TabLayout tabLayout = ...;
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
第二种
<android.support.design.widget.TabLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<android.support.design.widget.TabItem
android:text="@string/tab_text"/>
<android.support.design.widget.TabItem
android:icon="@drawable/ic_android"/>
</android.support.design.widget.TabLayout>
接下来就是代码java部分,首先是从网络获取bing今日的图片。我之前有文章写怎么获取:
【Android学习】获取Bing 15天前到明天的壁纸,并设置为背景
使用Retrofit 获取图片地址;
定义接口:
public interface BingApi {
@GET("bing/day/{what_day}/mkt/{country}")
Observable getBingPicPath(@Path("what_day") String what_day,
@Path("country") String country);
}
创建Retrofit:
public static WeatherApi weatherApi;
//获取bing壁纸地址
public static BingApi getBingApi() {
if (bingApi == null) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://test.dou.ms/")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
bingApi = retrofit.create(BingApi.class);
}
return bingApi;
}
截取字符串(当然你可以直接在map中转换一下):
// 截取字符串中 图片的地址
public static String GetBingImageUrl(String str) {
String\[\] strArray = str.split("地址:");
return strArray\[1\];
}
Bing图片和天气信息我是在启动界面展示时获取的。
天气信息和之上边获取方式一致,不再贴了,可以参考。
需要他俩都获取到数据后在进行跳转。当然了,我会先判断有没有网络,没有就等2秒后跳转到主界面,
有网络就获取后再跳转。不过要记得自定义超时时间,毕竟网速慢的话不能在启动界面停留10秒啊(默认是几秒来着,反正很长啦)。
写到这里我想起来我没有处理进入主界面后如果有网络了怎么破 ,啊咧。
天气信息还好,只要切换城市就能再次发出请求获取数据,壁纸就不行了。
可以定义一个服务来监听网络状态,然后配合Rxbus再次请求数据。
先看看使用combineLatest操作符的使用吧:
CombineLatest
当两个Observables中的任何一个发射了数据时,使用一个函数结合每个Observable发射的最近数据项,并且基于这个函数的结果发射数据。
CombineLatest
操作符行为类似于zip
,但是只有当原始的Observable中的每一个都发射了一条数据时zip
才发射数据。CombineLatest
则在原始的Observable中任意一个发射了数据时发射一条数据。当原始Observables的任何一个发射了一条数据时,CombineLatest
使用一个函数结合它们最近发射的数据,然后发射这个函数的返回值。
所以用这个来观测获取图片和天气后进行跳转。
(异常也直接跳转)
代码:(我偷懒把天气信息保存到一个静态变量中去了,应该在Intent中传值)
Observable.combineLatest(NetWork.getBingApi().getBingPicPath("0", "ZH-CN"), NetWork.getWeatherApi()
.getWeatherInfo(city, API_KEY), new Func2, Weather, Boolean>() {
@Override
public Boolean call(ResponseBody responseBody, Weather weather) {
try {
AppUtils.back_url = GetBingImageUrl(responseBody.string());
}
catch (IOException e)
{
e.printStackTrace();
}
// 判断并传值
if (weather.getError_code() == 0) //查询成功 可以保存
{
AppUtils.today_weather = weather;
}
return true;
}
}).compose(this.bindToLifecycle())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
goHome();
}
@Override
public void onNext(Boolean aBoolean) {
goHome();
}
});
好的,数据获取完就开始展示。
效果如图:
**
**
有一点要说的,就是天气图片。聚合数据提供了两套图片,都挺好看的,我取了其中一套放到了去年申请的虚拟主机上(免费两年,快到期了,之前都没怎么用过)。
地址在源码里,欢迎给Star。
好的,到这里要说数据库的事情了。
greenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案。
我也是在网上看别人的教程。在这就推荐一个:
黄帅(音译)的文章
但我觉得这都不是重点。因为greenDAO封装好了Api,我们不需要写sql语句。轻松简单,所以我遇到的问题
是viewpager 显示Fragment的时候,这是个新手级问题。
首先 我在创建Fragment时 传入了一个参数:
ViewPagerAdapter vpAdapter = new ViewPagerAdapter(getSupportFragmentManager());
vpAdapter.addFragment(new DailyFragment().newInstance("学习"), "学习");
vpAdapter.addFragment(new DailyFragment().newInstance("工作"), "工作");
vpAdapter.addFragment(new DailyFragment().newInstance("运动"), "运动");
vpAdapter.addFragment(new DailyFragment().newInstance("日常"), "日常");
main_vp_container.setAdapter(vpAdapter);
然后fragment 获取到这个参数,通过这个参数去 查询数据库:
public DailyFragment newInstance(String type) {
Bundle args = new Bundle();
args.putString(TYPE, type);
DailyFragment dailyFragment = new DailyFragment();
dailyFragment.setArguments(args);
return dailyFragment;
}
这都是我想象的流程,实际上并没有这样正常进行。
为什么呢,这就得从Fragment的生命周期和 viewpager缓存说起了。
**
**
Viewpager不设置默认缓存页面数量的话,默认是两个。
我们现在有 1 、 2、 3 、 4 ,4个界面。
通过跟踪声明周期函数可以知道。
先是 (其他的就不追踪了,配合上面的图都能理解)onCreate 1 、onResume 1、 onCreate 2、 onResume 2
这时候显示的是 第一个界面。 滑动到第2个后 开始执行onCreate 3、 onResume 3
为什么呢,因为2已经创建了,只是你没看到 。有人问滑动到能看到的时候为啥没onResume ,
这就相当于一个很大的图片,你在手机上只看到一部分,滑动之后看到其余部分差不多一个意思。
然后再向右滑动 就是onCreate 4、 onResume 4.
这时候滑动到第四个界面 发现什么也没执行,原因就像上面说的,在显示3的时候已经加载完毕了。
然后接下来无论怎么滑动都是执行onResume,且加载的是相邻的。
也就是说我们无法在获取create 时传入的参数。 那么我们该向数据库查询什么呢?显然结果会是错误的。
在一开始我的解决方法是创建4个一模一样的fragment,当然名字不一样。分别执行查询"学习","运动",什么的。
这样当然没问题,但咱这也太low了,一模一样的fragment还写4个。。。
所以我就想在tab改变的时候可以获取到此时的TabTitle。然后传给 fragment,让它执行查询方法,再次获取数据。
没错,这样也能解决。但有一个小bug。当时是使用RxBus,发车之后在fragment中监听获取事件,但由于之前说,此时有两个fragment存在,它们都在监听。
所以如果不加以限制,他们会触发重新查询的方法,所以你会在滑动的时候发现相邻的界面数据和你当前的一样。
最简单的办法就是设置缓存页数:
这就是这种办法的最简单解决办法。
main_vp_container.setOffscreenPageLimit(4); //设置4页缓存
好尴尬0 0,没关系,加深了对fragment生命周期和tablayout+viewpager的用法。
来看信息展示界面:
**
**
然后是添加界面:
**
**
使用是DialogFragment :
鸿洋大神的教程,很详细:
Android 官方推荐 : DialogFragment 创建对话框
不过我也贴一下简单的代码:
布局就不贴了。
直接上java代码,很简单。
public class AddDialogFragment extends DialogFragment {
private EditText et_title;
private EditText et_info;
private MaterialSpinner bp;
//创建接口在Acitvity中调用
public interface AddDutyInputListener {
void onAddDutyInputComplete(String title, String type, String info);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String\[\] ITEMS = {"学习", "工作", "运动", "日常"};
ArrayAdapter adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_spinner_item, ITEMS);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.dialog_add, null);
et_title = (EditText) view.findViewById(R.id.et_title);
et_info = (EditText) view.findViewById(R.id.et_info);
bp = (MaterialSpinner) view.findViewById(R.id.spinner);
bp.setAdapter(adapter);
builder.setView(view)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
AddDutyInputListener listener = (AddDutyInputListener) getActivity();
listener.onAddDutyInputComplete(et_title.getText().toString(), bp.getSelectedItem().toString(), et_info.getText().toString());
}
}).setNegativeButton("取消", null);
return builder.create();
}
}
然后在activity中调用显示:
AddDialogFragment adddialog = new AddDialogFragment();
adddialog.show(getFragmentManager(), "addDialog");
这样就会显示出来。
点击确定后通过实现的接口进行数据的返回。
@Override
public void onAddDutyInputComplete(String title, String type, String info) {
if (title.trim().isEmpty()) {
Toast.makeText(MainActivity.this, "标题不能为空!", Toast.LENGTH_SHORT).show();
} else {
Duty newduty = new Duty(null, title, info, type, false, new Date());
DbServices.getInstance(this).saveNote(newduty);
if (_rxBus.hasObservers()) { //是否有观察者,有,则发送一个事件
_rxBus.send(new Event.AddEvent(newduty,type));
}
}
}
在这里我使用了rxBus 来通知fragment 添加了一个数据, 让他们看看是不是属于自己那一组的,
属于的话就自己往adapter里增添一条数据。
代码如下:(注意生命周期)
_rxBus.toObserverable()
.compose(this.bindToLifecycle())
.subscribe(new Action1
() {
@Override
public void call(Object event) {
if (event instanceof Event.AddEvent) {
//如果 传来的 新增事件 和当前 查询结果类型一致 则直接往里面填充
if (((Event.AddEvent) event).getMduty().getType() == mytype) {
qadapter.add(0, ((Event.AddEvent) event).getMduty());
}
}
}
});
完美实现:
到这里就算结束了,在无人指引的情况下,多看书,打基础,然后在代码中获得收获。
代码已经传github:https://github.com/VongVia1209/WeatherAndNote
更新:增加了设置壁纸功能
首先需要给权限:
<uses-permission android:name = "android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS"/>
然后就是使用picasso获取bitmap 然后设置就好。
代码如下:(picasso创建bitmap属于io操作)
void setBackground() {
final WallpaperManager instance = WallpaperManager.getInstance(this);
int desiredMinimumWidth = this.getWindowManager().getDefaultDisplay().getWidth();
int desiredMinimumHeight = this.getWindowManager().getDefaultDisplay().getHeight();
instance.suggestDesiredDimensions(desiredMinimumWidth, desiredMinimumHeight);
Observable<Void> setBack = Observable.create(new Observable.OnSubscribe<Void>() {
@Override
public void call(Subscriber<? super Void> subscriber) {
try {
Bitmap bmp = Picasso.with(MainActivity.this).load(AppUtils.back_url).get();
instance.setBitmap(bmp);
} catch (Exception e) {
e.printStackTrace();
}
subscriber.onNext(null);
subscriber.onCompleted();
}
}).compose(this.<Void>bindToLifecycle())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
setBack.subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {
Toast.makeText(MainActivity.this, "设置成功", Toast.LENGTH_SHORT).show();
}
});
}
效果: