《Android第一行代码》coolweather项目个人总结

14.4 总结

1.点击ADM不能查看data文件夹,android device monitor不能查看/data目录
2.不要忘记china后面是有/。

String address="http://guolin.tech/api/china/"+provincecode+"/"+citycode;

3.ProgressDialog在24以上的是用不了的。
4.setselection(int position)的作用就是把第position位置的数据显示在listview的最上面一项。来自ListView的setSelection用法。下文代码就是把第一项放在最上面一项。假如你设置1,就是把第二位的数据显示在listview的最上面一项,那么第一项呢,向上滑动就可以看到。

 listView.setSelection(0);

5.OkHttp 建立一个新方法,使程序可以多次调用,并带有回调参数callback,同时在client.newCall(request)没有调用excuter()方法,而是调用enqueue()方法,该方法的内部已经开好子线程了,在子线程中去执行HTTP请求,并将最终的请求结果回调OKhttp.Callback当中,在调用它的时候要重写onResponse(),得到服务器返回的具体内容,重写onFailure()方法,在这里对异常情况进行处理。不可以再这里进行UI操作。

public class HttpUtil {
    public static void sendOkHttpRequest(String address,okhttp3.Callback callback){
        OkHttpClient client=new OkHttpClient();
        Request request=new Request.Builder().url(address).build();
              client.newCall(request).enqueue(callback);
    }

那么我们在调用sendOkHttpRequest()方法的时候就可以这样写

HttpUtil。sendOkHttpRequest("http://www.baidu.com",new okhhtp3.Callback())
{
	@Override
	public void onResponse(Call call,Response response)throws IOException{
	//得到服务器返回的具体内容
	String responseData =response.body().string();
	}
	@Override
	public void onFailure(Call call,IOException e){
	//在这里对异常情况进行处理
	}
}

在coolweather中,重写了onFailure()方法,onResponse()方法。

  • onFailure()方法

    如果加载失败,要返回UI线程,去关闭加载框,并提示加载失败字样。

  • onResponse()方法
    首先去判断要去服务器查询什么type的数据,包含三个种类,去调用相应解析json的方法。之后数据库中已经包含数据了,所以调用对应type的读取数据库数据的方法。

  HttpUtil.sendOkHttpRequest(address, new Callback() {
      @Override
            public void onFailure(Call call, IOException e) {
                //失败了,返回UI线程处理逻辑
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        //关闭加载框
                       // closeProgressBar();
                       closeProgressDialog();
                        Toast.makeText(getContext(),"加载失败",Toast.LENGTH_SHORT).show();
                    }
                });
            }
            //得到服务器返回的具体内容
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String responseText=response.body().string();
                boolean result=false;
                if("province".equals(type))
                    result = Utility.handleProvinceResponse(responseText);
                else if("city".equals(type)){
                    result=Utility.handleCityResponse(responseText,selectedProvince.getId());
                }
                else if("county".equals(type)){
                    result=Utility.handleCountyResponse(responseText,selectedCity.getId());
                }
                //解析完数据牵涉到UI操作,因此必须要在主线程中调用。
                //数据库中有了,可以显示了。
                if(result){
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            ///关闭进度框
                          //  closeProgressBar();
                           closeProgressDialog();
                            if("province".equals(type))
                                queryProvinces();
                            else if("city".equals(type)){
                               queryCities();
                            }
                            else if("county".equals(type)){
                               queryCounties();
                            }

                        }
                    });
                }
            }
        });
  1. 和风天气默认让你去使用sdk类型的key,所以要注意我们使用的key要是Web API类型,刚开始我注册后,一直不好使,最后才发现申请的key是sdk类型。和风天气注册地址

14.5总结

1. gson

1.1 gson基本用法

回顾gson的使用,并找一些json格式的数据进行解析。

String response="[{img=0.0,name=非会员,qualify_amount=0.0}," +
                "{img=1.0,name=一级会员,qualify_amount=100.0}," +
                "{img=2.0, name=二级会员,qualify_amount=300.0}]";

由于是数组中有多个对象,所以可以用List的方法进行解析,解析方法如下:

 Gson gson=new Gson();
        //数组的话可以这么做
     List<User>users=gson.fromJson(response,new TypeToken<List<User>>(){}.getType());
     for (User user:users){
         System.out.println(user.img+" "+user.name+" "+user.qualify_amount);

1.2 属性重命名

这里参考自Gson用法详解。当我们的接口返回的数据与我们期望的数据不同时,我们可以使用属性重命名。或者下述的json数据中有一个tz,我们怕忘记它的意思想给他改成time,就需要使用@SerializedName 来建立与java字段的联系。

 /**
     * admin_area : 北京
     * tz : +8.00
     * location : 北京
     * lon : 116.4052887
     * parent_city : 北京
     * cnty : 中国
     * lat : 39.90498734
     * cid : CN101010100
     */
    private String admin_area;
    @SerializedName("tz")
    private String time;

SerializeName远比你想象的更强大,假如,json数据如下:userName,user_name,Name,都是指向一个值,此时我们可以使用alternate属性。很奇怪的是,我翻看了我的SerializedName类发现竟然没有alternate这个方法,黑人问号脸,为什么问号它围绕着我?

{\"userName\":\"leavesC\",\"age\":24,\"sex\":true}
{\"user_name\":\"leavesC\",\"age\":24,\"sex\":true}
{\"Name\":\"leavesC\",\"age\":24,\"sex\":true}


@SerializedName(value = "userName", alternate = {"user_name", "Name"})
    private String name;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.google.gson.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface SerializedName {
    String value();
}

1.3 JsonDeserializer

参考自Gson 解析复杂数据。首先有一个Json对象,表示一本书的基本信息。

{
  'title':    'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
  'isbn-10':  '032133678X',
  'isbn-13':  '978-0321336781',
  'authors':  ['Joshua Bloch', 'Neal Gafter']
}

java的变量名不容许有‘-’符号的,我们可以使用@SerializedName注解来处理,不过对于一些复杂情况是难以处理的,所以我们使用JsonDeserializer。我们需要自定义一个JsonDeserializer,然后注册到GsonBuilder,GsonBuilder在解析时就会自动使用我们自定义的JsonDeserializer。
首先我们需要创建一个BookJsonDeserializer。它负责把Book对应的Json对象,解析为Java对象。

public class BookDeserializer implements JsonDeserializer<Book> {

      @Override
      public Book deserialize(final JsonElement jsonElement, final Type typeOfT, final JsonDeserializationContext context)
          throws JsonParseException {

        //todo 解析字段

        final Book book = new Book();
        book.setTitle(title);
        book.setIsbn10(isbn10);
        book.setIsbn13(isbn13);
        book.setAuthors(authors);
        return book;
      }
    }

简单介绍下JsonDeserializer,它需要有一个Type,那肯定是我们的Book。我们要重写deserialize()函数,并在其中解析json数据,最后返回一个Book对象。这里比较重要的是,Gson解析Json对象在内部表示为JsonElement,虽然element是元素的意思,但JsonElement可以表示为下列的任何一种:

1. JsonPrimitive :Java 基本类型的包装类,以及 String
2. JsonObject:类比 Js 中 Object的表示,或者 Java 中的 Map<String, JsonElement>,一个键值对结构。
3. JsonArray:JsonElement 组成的数组,注意:这里是 JsonElement,说明这个数组是混合类型的。
4. JsonNull:值为 null

完整代码如下,根据上述所讲,就可以解释,为什么要用getAsString和getAsJsonArray了。使用getAsString时是这个JsonElement表示为JsonPrimitive。使用getAsJsonArray时,虽然读取的是[‘Joshua Bloch’, ‘Neal Gafter’]这个数组,但表示为JsonElement所以,要给他转化一下。

class BookDeserializer implements JsonDeserializer<Book>{
    @Override
    public Book deserialize(final JsonElement jsonElement, final Type typeOfT,
                            final JsonDeserializationContext context)throws JsonParseException{
        final JsonObject jsonObject=jsonElement.getAsJsonObject();
        JsonElement titleElement=jsonObject.get("title");
        final  String title=titleElement.getAsString();
        JsonElement isbn10Element=jsonObject.get("isbn-10");
        final String isbn10=isbn10Element.getAsString();
        JsonElement isbn13Element=jsonObject.get("isbn-13");
        final String isbn13=isbn13Element.getAsString();
        //虽然是个数组,但Gson解析json对象并在内部表示为JsonElement,
        // 这里的其实是表示为JsonElement的JsonArray,所以要在get之后用getAsJsonArray();
        JsonArray authors=jsonObject.get("authors").getAsJsonArray();
        String[]Authors=new String[authors.size()];
        //Array中存放的是element
        int i=0;
        for(JsonElement jsonElement1:authors){
            Authors[i++]=jsonElement1.getAsString();
        }

        //todo 解析字段
        final  Book book=new Book();
        book.setTitle(title);
        book.setIsbn10(isbn10);
        book.setIsbn13(isbn13);
        book.setAuthors(Authors);
        return book;
    }

这里要注意一下,Book类我们要重写一下toString(),否则的话无法得到我们想要的结果,直接System.out.println(book)就会输出book对应的地址。我们的检验代码如下:

public static void main(String[]args){
        String response="{\"title\":\"Java Puzzlers: Traps, Pitfalls, and Corner Cases\"," +
                "\"isbn-10\":\"032133678X\"," +
                "\"isbn-13\":\"978-0321336781\"," +
                "\"authors\":[\"Joshua Bloch\"," +
                "\"Neal Gafter\"]}";
        //使用GsonBuilder来注册BookDeserializer,并创建Gson对象,这样的话,
        // 需要解析Book的时候,就会使用BookDeserializer来解析
        GsonBuilder gsonBuilder=new GsonBuilder();
        gsonBuilder.registerTypeAdapter(Book.class,new BookDeserializer());
        Gson gson=gsonBuilder.create();
        
        Book book = gson.fromJson(response, Book.class);
        System.out.println(book);

    }

最后是解析的过程:
1.将输入的字符串解析为JsonElement
2.找到对应的JsonDeserializer来解析,就找到我们的BookDeserializer
3.传入参数进deserialize()函数,讲JsonElement转换成Book对象
4.将Book对象返回fromJson()

1.4 对象嵌套

假如数据的结构如下,大类是Book类,其中还包括Author类

{
  'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
  'isbn': '032133678X',
  'authors':[
    {
      'id': 1,
      'name': 'Joshua Bloch'
    },
    {
      'id': 2,
      'name': 'Neal Gafter'
    }
  ]
}
public class Author{
    long id;
    String name;
}

我们有三种方法去解析这个Author类,
1.我们更新BookDeserializer,同时在其中解析authors字段。
2.可以使用默认的Gson实现,因为Author类的数据域和author的Json是一一对应的。
3.新写一个AuthorDeserializer来处理author的解析问题。

第二种方式:JsonDeserializer 的 deserialize() 方法提供了一个 JsonDeserializationContext 对象,这个对象基于 Gson 的默认机制, 我们可以选择性的将某些对象的反序列化工作委托给这个JsonDeserializationContext。JsonDeserializationContext 会解析 JsonElement 并返回对应的对象实例,并在BookDserializer中set一下值即可。

Author author = jsonDeserializationContext.deserialize(jsonElement, Author.class);
Book.setauthor(author);

第三种方式:新写一个AuthorDeserializer,这个类很简单,就不放代码了,要记得在主函数中,给GsonBuilder注册。

 // Configure GSON
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());
    gsonBuilder.registerTypeAdapter(Author.class, new AuthorDeserializer());
    final Gson gson = gsonBuilder.create();

1.5 使用JsonDeserializer解析和风天气数据

首先要说明JSONObject和JsonObject是不同的,前者是json类下的,后者是gson类下的,我们尝试全部使用gson来解析和风天气数据。下面是json数据类型。其实解析起来很简单,我们定义三个类,分别是basic,update,now,三个类。这里主要就是想练习一下JsonDeserializer。

{
HeWeather6: [
{
basic: {
cid: "CN101010100",
location: "北京",
parent_city: "北京",
admin_area: "北京",
cnty: "中国",
lat: "39.90498734",
lon: "116.4052887",
tz: "+8.00"
},
update: {
loc: "2019-03-21 15:55",
utc: "2019-03-21 07:55"
},
status: "ok",
now: {
cloud: "0",
cond_code: "100",
cond_txt: "晴",
fl: "8",
hum: "11",
pcpn: "0.0",
pres: "1021",
tmp: "12",
vis: "16",
wind_deg: "3",
wind_dir: "北风",
wind_sc: "2",
wind_spd: "11"
}
}
]
}

我们定义的WeatherDeserializer定义如下,主要就是练习对象与数组的转换,以及context的默认方法。注意的就是可以使用gsonformat插件来生成类,非常的便捷。

class WeatherDeserializer implements JsonDeserializer<Weather>{
    @Override
    public Weather deserialize(final JsonElement jsonElement, final Type typeOfT,
                            final JsonDeserializationContext context)throws JsonParseException {
             final JsonObject jsonObject = jsonElement.getAsJsonObject();
             final JsonArray jsonArray=jsonObject.get("HeWeather6").getAsJsonArray();
             final JsonObject jsonObject1=jsonArray.get(0).getAsJsonObject();
            Basic basic=context.deserialize(jsonObject1.get("basic"), Basic.class);
            Update update=context.deserialize(jsonObject1.get("update"),Update.class);
            String status=context.deserialize(jsonObject1.get("status"),String.class);
            Now now=context.deserialize(jsonObject1.get("now"),Now.class);
            Weather weather=new Weather();
            weather.basic=basic;
            weather.update=update;
            weather.status=status;
            weather.now=now;
            return weather;
    }
    }

1.6 使用JsonDeserializer解析其他类型数据

最初想深入学习Gson是由于看了这篇文章,你真的会用Gson么。这篇文章有一个评论,有人问这个json形式数据该如何解析。我打算解析下这个数据练下手。

{0={img=0.0,name=非会员,qualify_amount=0.0}," +
                "1={img=1.0,name=一级会员,qualify_amount=100.0}," +
                "2={img=2.0, name=二级会员,qualify_amount=300.0}}

我们的类定义如下,数据存储形式为map的键值对类型。

class User{
    Map<Integer,Data> map;
    class Data {
        public double img;
        public String name;
        public double qualifty_amount;

        @Override
        public String toString() {
            return "Data{" +
                    "img=" + img +
                    ", name='" + name + '\'' +
                    ", qualifty_amount=" + qualifty_amount +
                    '}';
        }
    }

定义一个MapDeserializer来方便我们解析json数据。首先我们的数据是一个object,所以要将其变为JsonObject,最初的想法是去get(),但这里数据量较小,只有0,1,2,但真实数据一定很大,我看了一下JsonObject的函数,惊喜的发现有entrySet()方法,我们利用这个方法就可以实现所有数据的遍历,也节省了代码量。

class MapDeserializer implements JsonDeserializer<User> {
    @Override
    public User deserialize(final JsonElement jsonElement, final Type typeOfT,
                            final JsonDeserializationContext context)throws JsonParseException {
     JsonObject jsonObject=jsonElement.getAsJsonObject();
      Set<Map.Entry<String,JsonElement>> set=jsonObject.entrySet();
        Map<Integer, User.Data>map=new HashMap<>();
         //User.Data data0=context.deserialize(jsonObject.get("0"),User.Data.class);
        //map.put(0,data0);
      for(Map.Entry<String,JsonElement>s:set){
          User.Data data=context.deserialize(s.getValue().getAsJsonObject(), User.Data.class);
          map.put(Integer.valueOf(s.getKey()),data);
      }
     User user=new User();
     user.map=map;
     return user;
    }
}

2. sharedPreferences

2.1 基本操作

读取数据:
服务与活动

SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(this);
        String times=prefs.getString("times",null);

碎片

SharedPreferences prefs= PreferenceManager.getDefaultSharedPreferences(getContext());
                        String test=prefs.getString("times",null);

写入数据:
在碎片中写入:

 SharedPreferences.Editor editor=PreferenceManager.
                                //不能使用getDefaultSharedPreferences(WeatherActivity.this)
                                getDefaultSharedPreferences(getContext())
                                .edit();

                        editor.putString("weather_id",weatherid);
                        editor.apply();

在活动或服务中写入:

SharedPreferences.Editor editor=PreferenceManager.
                                    getDefaultSharedPreferences(AutoUpdateService.this)
                                    .edit();
                            editor.putString("weather",responseText);
                            editor.apply();
SharedPreferences.Editor editor = PreferenceManager.
                                    getDefaultSharedPreferences(WeatherActivity.this).edit();
                            editor.putString("weather", responseText);
                            editor.apply();

3.Glide库

14.6总结

1 下拉刷新

2 滑动窗口

14.7总结

1 android异步处理信息

  • 服务是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互且要求长期运行的任务。
  • 服务不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。
  • 需要在服务内部手动创建子线程。

1.1 handler

使用handler可以解决在子线程中进行UI操作,首先创建一个Handler对象,重写handleMessage()方法,在这里对具体的Message进行处理,这里可以判断Message的what字段,然后进行操作。同时,在子线程中的run()中创建一个Message对象,并将该对象的what字段指定为指定值,并使用Handler的sendMessage去发送创建的Message对象。这里handleMessage方法在主线程,所以可以使用UI操作。

 public static final int UPDATE_TEXT =1;
  private Handler handler=new Handler(){
        public  void  handleMessage(Message msg){
            switch (msg.what){
                case UPDATE_TEXT:
                    text.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };
new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message=new Message();
                        message.what=UPDATE_TEXT;
                        handler.sendMessage(message);
                    }
                }).start();

Android中的异步消息处理主要包括:Message、Handler、MessageQueue和Looper。

  1. Message
    Message是在线程之间传递的消息,他可以在内部携带少量的信息,用于在不同线程之间交换数据。包括what字段,arg1字段,arg2字段来携带一些整型数据。使用obj字段携带一个Object字段。
  2. Handler
    Handler主要用于发送和处理消息。发送消息是使用Handler的sendMessage()方法(一般在子线程中使用,新建一个Message对象,并设置字段的信息),最终会传递到Handler的handleMessage()方法(一般会新建一个Handler对象,然后重写这个方法)。
  3. MessageQueue
    它是一个消息队列,用于存放所有通过Handler发送的消息。
  4. Looper
    调用Looper的loop()方法后,就会进入到一个无限循环,每当发现MessageQueue中存在一条消息,就将其取出,并传递到Handler的handleMessage()方法中。

整体流程:
首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息就会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发会Handler的handleMessage()方法中。

1.2 AsyncTask

AsyncTask是一个抽象类,我们在继承时,需要指定3个泛型参数。

  • params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress:后台任务执行时,如果需要在界面上显示当前的进度,使用这里指定的泛型作为进度单位。
  • Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

AsyncTask一般需要重写四个函数:

  1. onPreExecute()
    在后台任务开始之前调用,用于一些界面上的初始化操作。
  2. doInBackground(Params…)
    所有代码都会在子线程中执行。在这里处理所有耗时任务,任务一旦完成就可以通过return语句将任务的执行语句返回。但是不可以进行UI操作。可以调用publishProgress(Progress)方法来反馈进度。
  3. onProgressUpdate(Progress…)
    当在后台任务中调用了publishProgress(Progress…)方法后,onProgressUpdate(Progress…)方法就会很快调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作。
  4. onPostExecute(Result)
    当后台任务执行完毕并通过return语句返回时,这个方法就会被调用,可以利用返回的数据来进行一些UI操作,比如提醒任务执行的结果。

2.服务

1 服务一般要重写onCreate(),onStartCommand(),onDestroy()这三个方法。同时需要注意服务需要注册。
2 服务要启动都需要使用Intent。

Intent startIntent=new Intent(this,MyService.class);
startService(startIntent);

3 活动与服务通信,需要使用onBind()方法。

我们在服务中新建一个DownloadBinder类,并在其中写一些方法以便我们使用,然后新建一个该类的对象,在onBind()方法返回这个对象。

//这是在服务类
private DownloadBinder mBinder=new DownloadBinder();
   class DownloadBinder extends Binder{
       public void startDownload(){
           Log.d("MyService","startDownload executed");
       }
       public int getProgress(){
           Log.d("MyService","getProgress executed");
           return 0;
       }
   }
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mBinder;
    }

我们在活动中,调用Binder。我们在活动中创建一个ServiceConnection的匿名类,并在里面重写onServiceConnected()方法和onServiceDisconnected()方法,分别在活动与服务成功绑定以及接触绑定时调用。在onServiceConnected我们通过向下转型得到了DownloadBinder的是咧,之后就可以调用DownloadBinder中任何public()的方法。

//这是在活动中
private MyService.DownloadBinder downloadBinder;
    private ServiceConnection connection=new ServiceConnection() {;
        //活动与服务成功绑定时调用
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder=(MyService.DownloadBinder)service;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }
        //活动与服务成功解绑时调用
        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

4 使用startForeground()方法可以使通知一直显示,前台服务。
5. 使用IntentService开启新线程。

3.《第一行代码中的下载示例》

1. 回调接口

首先是定义一个回调接口,用于对下载过程中的各种状态进行监听和回调。

public interface DownLoadListener {
    //通知当前的下载进度
    void onProgress(int progress);
    //通知下载成功事件
    void onSuccess();
    //通知下载失败事件
    void onFailed();
    //通知下载暂时时间
    void onPaused();
    //通知下载取消事件
    void onCanceled();
}

2. DownloadTask

2.1 泛型参数

新写一个DownloadTask完成下载功能.。首先是传入的三个泛型参数,第一个泛型参数(Param)为String,这个是要传入的Url数据,会传入doInbackground()方法。第二个泛型参数(Progress)是进度显示单位,会通过publishProgress()更新,并回调给onProgressUpdate()方法。第三个(Result)是返回的结果的类型,这个参数用于doInbackground()方法返回,以及onPostExcute()方法执行一些操作。

2.2 数据域

首先有四个整形常量。代表了当前下载任务的不同状态。之后有一个我们定义的接口的实例对象,我们要在onPostExcute()方法中根据不同的result来执行该对象的不同方法。最后有两个boolean型变量,因为我们在doInbackground()中要时刻判断是否点击取消或暂停。

2.3 doInbackground()方法

在这个方法要完成文件的下载,以及更新下载的进度。
文件的下载中需要实现断点下载。我们将其存在sd卡的download目录,我们首先要判断文件是否存在,如果存在就要记录已下载文件长度,我们通过okhttp来获取下载文件的长度,判断已下载文件长度是否等于下载文件长度,相等就可以直接返回Type_SUCCESS,否则以已下载长度开始下载。增加断点的方法如下。

 OkHttpClient client=new OkHttpClient();
            Request request=new Request.Builder()
                    //增加断点
                    .addHeader("RANGE","bytes="+downloadedLength+"-")
                    .url(downloadUrl)
                    .build();
            Response response=client.newCall(request).execute();

接下来通过java的文件流方式,不断从网络上读取数据,并写入本地,同时计算出progress,并使用publishProgress()方法来传递给onProgressUpdate()。在这个过程我们要时刻判断那两个boolean变量是否为true,为true就返回相应的result。正常完成循环后返回TYPE_SUCCESS,如果出现任何错误最终会返回TYPE_FAILED。

2.4 onProgressUpdate()方法

这个方法在publishProgress()后就会回调,虽然一直用progress代替,但传入的是values,从values【0】中取出progress,与一个对象lastProgress进行比较,如果有更改就去调用listener的onProgress()函数。我们在listener的onProgress()中使用新的progress来创建新的通知,就可以达到实时更新下载进度的功能。

 @Override
    protected void onProgressUpdate(Integer...values){
        int progress=values[0];
        if (progress>lastProgress)
            listener.onProgress(progress);
        lastProgress=progress;
    }
2.5 onPostExecute()方法

这个函数是用来通知下载结果的,由于在doInbackground()不可以进行UI操作,所以需要使用这个方法来通知下载结果,这个函数在doInbackground()返回结果后会调用。

 @Override
    protected void onPostExecute(Integer status){
        switch (status){
            case TYPE_CANCELED:
                listener.onCanceled();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            default:
                break;
        }
    }
2.6 通用函数

首先是两个设置函数,会在Activity中按下相应button时调用,将boolean设置为true,就会在doInBackground中暂停下载或取消下载。最后的这个通用函数是使用OkHttp来获取文件的总长度。

 public void pauseDownload(){
        isPaused=true;
    }
    public void cancelDownload(){
        isCanceled=true;
    }
    private long getContentLength(String downloadUrl)throws IOException{
        OkHttpClient client=new OkHttpClient();
        Request request=new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response=client.newCall(request).execute();
        if (response!=null&&response.isSuccessful()){
            long contentLength=response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }

完整代码如下:

public class DownloadTask extends AsyncTask<String,Integer,Integer> {
    public static final  int TYPE_SUCCESS = 0;
    public static final int TYPE_FAILED = 1;
    public static final int TYPE_PAUSED = 2;
    public static final int TYPE_CANCELED = 3;
    private DownLoadListener listener;
    private boolean isCanceled=false;
    private boolean isPaused=false;
    private int lastProgress;
    public DownloadTask(DownLoadListener listener){
        this.listener = listener;
    }
    //在后台做的东西
    @Override
    protected Integer doInBackground(String...params){
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file=null;
        try{
            long downloadedLength=0;//记录已下载的文件。
            String downloadUrl = params[0];
            String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            //下载到sd卡的Download目录
            String directory= Environment.getExternalStoragePublicDirectory
                    (Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory+fileName);
            //已经下载了一部分了
            if (file.exists()){
                downloadedLength=file.length();
            }
            long contentLength=getContentLength(downloadUrl);
            if (contentLength==0)
                return TYPE_FAILED;
            else if(contentLength==downloadedLength)
                //已下载等于文件总字节,说明下载完毕
                return TYPE_SUCCESS;
            OkHttpClient client=new OkHttpClient();
            Request request=new Request.Builder()
                    //增加断点
                    .addHeader("RANGE","bytes="+downloadedLength+"-")
                    .url(downloadUrl)
                    .build();
            Response response=client.newCall(request).execute();
            if (response!=null){
                is=response.body().byteStream();
                savedFile=new RandomAccessFile(file,"rw");
                savedFile.seek(downloadedLength);
                byte[]b=new byte[1024];
                int total=0;
                int len;
                while((len = is.read(b))!=-1)
                {
                    //随时判断是否被取消或终止,
                    if (isCanceled){
                        return TYPE_CANCELED;
                    }
                    else  if (isPaused)
                        return TYPE_PAUSED;
                    else
                    {
                        total+=len;
                        savedFile.write(b,0,len);
                        int  progress=(int)((total+downloadedLength)*100/contentLength);
                        //通知下载进度变化,之后很快回调用onProgressUpdate()方法
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        }catch (Exception e)
        {e.printStackTrace();}
        finally {

                try {
                    if (is!=null)
                        is.close();
                    if (savedFile!=null)
                        savedFile.close();
                    if (isCanceled&&file!=null)
                        file.delete();
                }catch (Exception e)
                {
                    e.printStackTrace();
                }
        }
        return TYPE_FAILED;
    }
    //更新下载数据。在这里去调用listener的onProgress()方法,
    //listener的onProgress()方法中时刻创建新通知,去更新数据。
    @Override
    protected void onProgressUpdate(Integer...values){
        int progress=values[0];
        if (progress>lastProgress)
            listener.onProgress(progress);
        lastProgress=progress;
    }
    //根据参数去回调
    @Override
    protected void onPostExecute(Integer status){
        switch (status){
            case TYPE_CANCELED:
                listener.onCanceled();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            default:
                break;
        }
    }
    public void pauseDownload(){
        isPaused=true;
    }
    public void cancelDownload(){
        isCanceled=true;
    }
    private long getContentLength(String downloadUrl)throws IOException{
        OkHttpClient client=new OkHttpClient();
        Request request=new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response=client.newCall(request).execute();
        if (response!=null&&response.isSuccessful()){
            long contentLength=response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }
}

3. DownloadService

在该类中,我们主要重写了之前定义的回调接口的匿名类;新写了一个DownloadBinder与Activity绑定,最后写了使用通知的函数。

3.1 通知函数

正常的通知是如下去写的。首先要有一个NotificationManger,之后为了防止不同版本的问题,会使用NotificationCompat.Builder(this)来创建,之后包括了不同的set函数。
值得一提的是下面的setstyle是为了完成内容很多的时候不被屏幕挡住,内容完全显示出来的功能。这里使用PendingIntent的作用是当我们点击顶端通知栏可以跳转到其他acitivity。传入四个参数分别是Context,第二个和第四个一般传入0,第三个参数是一个Intent对象,可以通过这个对象构建出PendingIntent的“意图”。也就是跳转到哪个Activity。Intent倾向于立即执行某个操作,而PendingIntent更加倾向于在某个合适的时机去执行某个操作

Intent intent=new Intent(this,Main2Activity.class);
                PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
NotificationManager manager=(NotificationManager)getSystemService
                        (NOTIFICATION_SERVICE);
                //防止不同版本
                Notification notification=new NotificationCompat.Builder(this)
                        .setContentTitle("this is content title")
                        .setContentText("This is content text")
                        //会覆盖text
                        .setStyle(new NotificationCompat.BigTextStyle().bigText
                                ("abcdefgauwohewqkehwqkehwqkewkqejqwkejqwkejwqkekwqehwqke" +
                                        "hwqkehqwkehwqweqwewqeqwewqewq"))
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setContentIntent(pi)
                        //不能是id。要求是传入  bitmap。
                        .setLargeIcon(BitmapFactory.decodeResource(getResources()
                        ,R.mipmap.ic_launcher))
                        .setAutoCancel(true)
                        .build();
                manager.notify(1,notification);

我们在这个类中将其分开成两个函数,方便我们去多次调用。第一个函数没什么说的,就是去获取一个NotificationManger,第二个函数有一个跳转。跳转回主活动。这里有一个setProgress函数,第一个参数传入通知的最大进度,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条。

private NotificationManager getNotificationManager(){
        return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
    }
    private Notification getNotification(String title,int progress){
        Intent intent=new Intent(this,MainActivity.class);
        PendingIntent pi= PendingIntent.getActivity(this,0,intent,0);
        NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),
                R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);
        if (progress>0){
            //当progress大于或等于0时才显示下载进度。
            builder.setContentText(progress+"%");
            //第一个参数传入通知的最大进度,第二个为当前进度,第三个为是否使用
            //模糊进度条。
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
3.2 DownloadListener匿名类

主要重写了以下几个函数。

  1. onProgress
    重新建立一个通知,也就是在doInbackground中进度更改后,使用publishProgress()去告诉progress更改,此时onProgressUpdate()就会调用,确定progress比lastProgress大后,就会去调用listener的onProgress方法,就会重新建立一个通知,达到时刻去更新通知中的进度数值。
  2. onSuccess
    回调这函数,代表下载完成,所以要关闭我们的downloadTask,停止前台服务通知,创建一个下载成功的通知,然后使用Toast弹窗来告诉用户下载完成。
  3. onFailed
    回调这个函数,代表下载失败,和onSuccess相似。
  4. onPaused
    与onSuccess相似,只不过不用停止前台服务通知,因为它是暂停。
  5. onCanceled
    与onSuccess相似。
private DownLoadListener listener=new DownLoadListener() {
        @Override
        public void onProgress(int progress) {
            //建立一个通知
            getNotificationManager().notify(1,getNotification("Downloading...",
                progress));
        }

        @Override
        public void onSuccess() {
            downloadTask=null;
            //下载成功时将前台服务通知关闭,并创建一个下载成功的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Download Success",
                    -1));
            Toast.makeText(DownloadService.this,"Download Success",
                    Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onFailed() {
            downloadTask=null;
            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Download Failed",
                    -1));
            Toast.makeText(DownloadService.this,"Download Failed",
                    Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPaused() {
            downloadTask=null;
            Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT)
                    .show();
        }

        @Override
        public void onCanceled() {
            downloadTask=null;
            stopForeground(true);
            Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT)
                    .show();
        }
    };
3.3 DownloadBinder

我们通过自定义DownloadBinder类使活动可以与服务通信。这里包含三个方法,分别用于开始下载,暂停下载和取消下载的。

  1. startDownload
    在这个方法中,我们创建了一个DownloadTask的实例,然后调用execute方法开启下载,execute方法传入的是下载url地址,也是三个泛型参数的第一个参数Params。同时开启前台服务,和Toast提示用户。
  2. pauseDownload
    这个方法比较简单就是调用了DownloadTask对象的pauseDownload方法。回忆这个方法的流程,就是将boolean型变量isPause变成true,这样就在InBackGround中返回一个Type_Pause,就会在onPostExecute中去回调DownloadListener的onPause()方法。
  3. cancelDownload
    和上一个方法类似,但我们不光要将boolean型变量设置为true,还需要去删除掉已经下载了的文件。
private DownloadBinder mBinder=new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    //服务与活动通信
    class DownloadBinder extends Binder{
        public void  startDownload(String url){
            if (downloadTask==null)
            {
                downloadUrl=url;
                downloadTask=new DownloadTask(listener);
                //execute传入AsyncTask<Params, Progress, Result>的第一个参数。
                downloadTask.execute(downloadUrl);
                startForeground(1,getNotification("Downloading...",0));
                Toast.makeText(DownloadService.this,"Downloading...",
                        Toast.LENGTH_SHORT).show();
            }
        }
        public void pauseDownload(){
            if (downloadTask!=null)
            {
                downloadTask.pauseDownload();
            }
        }
        public void cancelDownload(){
            if(downloadTask!=null)
                downloadTask.cancelDownload();
            else{
                if (downloadUrl!=null){
                    //取消下载时需将文件删除
                    String fileName=downloadUrl.substring(
                            downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory
                            (Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file=new File(directory+fileName);
                    if (file.exists())
                        file.delete();
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "Canceled",
                            Toast.LENGTH_SHORT).show();;
                }
            }
        }
    }

4. MainAcitivity

在主活动中,我们要实现服务的绑定,app的打开时授权,button的点击函数。layout文件很简单就是包含三个button:分别是startdownload,canceldownload,pausedownload。

4.1 服务的绑定

我们首先创建了ServiceConnection的匿名类,并在onServiceConnected方法中获取到DownloadBinder的实例,这样在与服务连接上的时候就可以调用服务中提供的方法了(startDownload,cancelDownload和pauseDownload)。调用startService和bindService方法来启动和绑定服务,启动服务就可以保证DownloadService一直在后台运行,绑定服务可以让MainActivity和DownloadService进行通信。

private DownloadService.DownloadBinder downloadBinder;
    private ServiceConnection connection=new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder=(DownloadService.DownloadBinder)service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Intent intent=new Intent(this,DownloadService.class);
        startService(intent);
        //使活动与服务可以通信
        bindService(intent,connection,BIND_AUTO_CREATE);
    
    }
4.2 打开时授权

我们在onCreate函数中,使用checkSelfPermiison来判断是否被许可使用权限。未被允许的话,就会调用requestPermissions方法,我们想要被用户允许的权限要放在一个String数组里,最后的1就是随便传入的一个id值。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
    ...
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
        permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(MainActivity.this,new
            String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
        }
    }

当弹窗出现提示用户是否允许后,不管结果如何,都会回调这个onRequestPemissionsResult()方法,首先判断的是requestCode,这个就是刚刚的那个id1,我们的结果存储在grantResults数组中。我们要在不允许的情况下,弹出一个Toast提示用户。

//用户选择是否接受权限后回调的函数
    @Override
    public void onRequestPermissionsResult(int requestCode,String[]permissions,
    int[]grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] != PackageManager.
                        PERMISSION_GRANTED) {
                    Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT)
                            .show();
                    finish();
                }
                break;
            default:
        }
    }
4.3 button的点击函数

这里要说明的我们不能为每个button去写他的clickLinstener,这样很麻烦,我们可以使用实现View.OnClickListener接口。

public class MainActivity extends AppCompatActivity implements View.OnClickListener

在onCreate中我们直接setOnclickLinstener(this)即可。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button startDownload=(Button)findViewById(R.id.start_download);
        Button pauseDownload=(Button)findViewById(R.id.pause_download);
        Button cancelDownload=(Button)findViewById(R.id.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);
        }

在onClick中我们使用v.getId()去判断究竟是哪个button即可。根据不同button我们去使用服务(DownloadBinder实例)提供的不同方法。

@Override
    public void onClick(View v){
        if (downloadBinder==null)
            return;
        switch (v.getId()){
            case R.id.start_download:
                String url="https://raw.githubusercontent.com/guolindev/eclipse/master" +
                        "/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();;
                break;
        }
    }

5.结果

最后运行效果如下:最初是一个运行时权限,之后可以看到,点击开始后可以开始下载,点击暂停后再次点击开始会从之前暂停处继续下载,而点击取消下载后再次点击开始下载会从0开始,说明文件已被删除。
在这里插入图片描述

发布了55 篇原创文章 · 获赞 28 · 访问量 9274

猜你喜欢

转载自blog.csdn.net/weixin_41796401/article/details/88542179