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图片和天气信息我是在启动界面展示时获取的。

天气信息和之上边获取方式一致,不再贴了,可以参考。

链接:使用聚合数据的接口进行的RxAndroid学习

需要他俩都获取到数据后在进行跳转。当然了,我会先判断有没有网络,没有就等2秒后跳转到主界面,

有网络就获取后再跳转。不过要记得自定义超时时间,毕竟网速慢的话不能在启动界面停留10秒啊(默认是几秒来着,反正很长啦)。

写到这里我想起来我没有处理进入主界面后如果有网络了怎么破 ,啊咧。

天气信息还好,只要切换城市就能再次发出请求获取数据,壁纸就不行了。

可以定义一个服务来监听网络状态,然后配合Rxbus再次请求数据。

先看看使用combineLatest操作符的使用吧:

CombineLatest

当两个Observables中的任何一个发射了数据时,使用一个函数结合每个Observable发射的最近数据项,并且基于这个函数的结果发射数据。

combineLatest

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框架并实现增删改查案例

但我觉得这都不是重点。因为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());
                    }
                }
            }
        });

完美实现:

1466235696112011.png

到这里就算结束了,在无人指引的情况下,多看书,打基础,然后在代码中获得收获。

代码已经传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();
        }
    });
}

效果:

blob.png blob.png