Android 天氣APP(三十)分鐘級降水

語言: CN / TW / HK

我正在參加「掘金·啟航計劃」

執行效果圖

在這裡插入圖片描述


前言

  說實話也蠻久沒有更新這個天氣APP了,原因主要是沒有什麼好的更新的因素和新的功能。當這兩者都具備時才有了這一篇文章。首先是和風天氣更新的新的分鐘級降水API,這個是可以提供給開發者免費呼叫的。話不多說了,進入正文。


正文

  平時工作之餘有空我就會去看看部落格和GitHub上有沒有問題,也會去看看和風天氣API的資料訪問量,因為我知道有很多開發者也會直接執行我的程式碼或者是安裝APK去使用。

  這個是會根據API Key來記錄請求次數的。然後我就看到了和風偷偷地更新了一個分鐘級降水API,應該就是近段時間更新的,這個說實話做的不夠地道,你既然更新了新的API那麼應該告訴平臺的開發者,讓他們去使用,順便給你們找出問題。兩全其美,好了,我的廢話已經很多了。下面進入正文。

一、新增分鐘級降水API

這個分鐘級降水的API測試地址如下:

java http://devapi.qweather.com/v7/minutely/5m?location=113.92942,22.53122&key=d4a619bfe3244190bfa84bb468c14316 隨便用一個瀏覽器開啟你就會看到這樣的返回資料,如下圖所示。 在這裡插入圖片描述   這個API和其他的API略有不同,需要使用經緯度作為請求引數,並且經度和緯度的值用英文逗號分隔開,經度在前,緯度在後,如果你不按照這個方式來的話,就會出現報錯400、403之類的。key就沒有什麼好說的了,就用自己的就可以了。

下面先通過這個返回值生成一個數據實體Bean再說。

在bean包下新建一個MinutePrecResponse類,裡面的程式碼如下

```java package com.llw.goodweather.bean;

import java.util.List;

/* * 分鐘級降水 V7 * @author llw / public class MinutePrecResponse {

/**
 * code : 200
 * updateTime : 2020-12-02T10:00+08:00
 * fxLink : http://hfx.link/1
 * summary : 未來兩小時無降水
 * minutely : [{"fxTime":"2020-12-02T10:00+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:05+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:10+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:15+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:20+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:25+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:30+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:35+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:40+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:45+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:50+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T10:55+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:00+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:05+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:10+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:15+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:20+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:25+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:30+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:35+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:40+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:45+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:50+08:00","precip":"0.0","type":"rain"},{"fxTime":"2020-12-02T11:55+08:00","precip":"0.0","type":"rain"}]
 * refer : {"sources":["Weather China"],"license":["no commercial use"]}
 */

private String code;
private String updateTime;
private String fxLink;
private String summary;
private ReferBean refer;
private List<MinutelyBean> minutely;

public String getCode() {
    return code;
}

public void setCode(String code) {
    this.code = code;
}

public String getUpdateTime() {
    return updateTime;
}

public void setUpdateTime(String updateTime) {
    this.updateTime = updateTime;
}

public String getFxLink() {
    return fxLink;
}

public void setFxLink(String fxLink) {
    this.fxLink = fxLink;
}

public String getSummary() {
    return summary;
}

public void setSummary(String summary) {
    this.summary = summary;
}

public ReferBean getRefer() {
    return refer;
}

public void setRefer(ReferBean refer) {
    this.refer = refer;
}

public List<MinutelyBean> getMinutely() {
    return minutely;
}

public void setMinutely(List<MinutelyBean> minutely) {
    this.minutely = minutely;
}

public static class ReferBean {
    private List<String> sources;
    private List<String> license;

    public List<String> getSources() {
        return sources;
    }

    public void setSources(List<String> sources) {
        this.sources = sources;
    }

    public List<String> getLicense() {
        return license;
    }

    public void setLicense(List<String> license) {
        this.license = license;
    }
}

public static class MinutelyBean {
    /**
     * fxTime : 2020-12-02T10:00+08:00
     * precip : 0.0
     * type : rain
     */

    private String fxTime;
    private String precip;
    private String type;

    public String getFxTime() {
        return fxTime;
    }

    public void setFxTime(String fxTime) {
        this.fxTime = fxTime;
    }

    public String getPrecip() {
        return precip;
    }

    public void setPrecip(String precip) {
        this.precip = precip;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

}

``` 然後進入到ApiService中,準備新增API介面,這時我發現了一個問題,那就是和風的請求地址都變了 在這裡插入圖片描述 在這裡插入圖片描述 我記得這裡之前是heweather。為什麼現在改成了qweather。嚇得我趕緊去執行一手,看我原來的地址還能不能訪問,好在虛驚一場,原來的地址還能夠訪問,那麼和風為什麼要改請求地址呢。難道原來的地址會轉到這個新的地址嗎?我的猜測目前是這樣的。

開啟ServiceGenerator,而我要改動的也就只有這兩處而已。 在這裡插入圖片描述 改成qweather即可。改完之後我運行了一下和之前也沒有什麼區別,看來這次的更新是很有必要的。不然到時候之前地址訪問不了,肯定很多問題會出現的,估計要被叼。

下面在ApiService中新增新的介面。

java /** * 分鐘級降水 最近兩小時內 * * @param location 經緯度拼接字串,使用英文逗號分隔,經度在前緯度在後 * @return */ @GET("/v7/minutely/5m?key=" + API_KEY) Call<MinutePrecResponse> getMinutePrec(@Query("location") String location);

二、修改佈局

  從上面的API得知想要獲取資料,就必須拿到經緯度。而獲取經緯度有兩種方式:① 通過百度定位獲取。② 通過和風天氣的城市搜尋獲取。

這裡我們使用第二種方式來獲取經緯度,那麼就是在搜尋城市的返回值中拿到經緯度之後去請求分鐘級降水的的介面,獲取資料之後顯示出來。因此我這裡先改變一下activity_main.xml。

增加的佈局程式碼如下:

```css

                    <TextView
                        android:id="@+id/tv_precipitation"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:drawableLeft="@mipmap/icon_weather_prec"
                        android:drawablePadding="4dp"
                        android:text="降水預告"
                        android:textColor="@color/white"
                        android:textSize="@dimen/sp_12" />
                    <!--檢視更多降水資訊-->
                    <TextView
                        android:gravity="center_vertical"
                        android:id="@+id/tv_prec_more"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="檢視詳情"
                        android:drawableRight="@mipmap/icon_more_blue_small"
                        android:textColor="@color/blue_more"
                        android:textSize="@dimen/sp_12" />
                </LinearLayout>

                <!--降水詳情列表-->
                <androidx.recyclerview.widget.RecyclerView
                    android:visibility="gone"
                    android:paddingTop="@dimen/dp_8"
                    android:id="@+id/rv_prec_detail"
                    android:paddingLeft="@dimen/dp_12"
                    android:paddingRight="@dimen/dp_12"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"/>

                <!--分隔線 增加UI效果-->
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginLeft="20dp"
                    android:layout_marginTop="8dp"
                    android:layout_marginRight="20dp"
                    android:alpha="0.1"
                    android:background="@color/white" />

``` 增加位置如下圖所示 在這裡插入圖片描述 那麼既然有列表自然也要有item了。所以在layout新建一個item_prec_detail_list.xml佈局,裡面的程式碼如下:

```css

<!--時間-->
<TextView
    android:id="@+id/tv_time"
    android:layout_marginTop="@dimen/dp_8"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="上午10:00"
    android:textColor="#FFF"
    android:textSize="@dimen/sp_12" />

<TextView
    android:id="@+id/tv_precip_info"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_marginTop="@dimen/dp_8"
    android:layout_marginBottom="@dimen/dp_8"
    android:gravity="center"
    android:text="0.1  雨"
    android:textColor="#FFF"
    android:textSize="@dimen/sp_12" />

```

顯示的地方有了下面就是要去MainActivity中實現具體的業務邏輯了。不過在此之前還需要增加一個方法。內容很簡單,因為我之後要使用GridLayoutManager,同時也要讓RecyclerView橫向滾動,因此我設定高度為佔滿父佈局高度。

三、增加介面卡

有佈局了,然後就是寫列表的介面卡了,不然資料怎麼填充進去呢?在adapter包下新建一個MinutePrecAdapter,裡面的程式碼如下:

```java package com.llw.goodweather.adapter;

import androidx.annotation.Nullable;

import com.chad.library.adapter.base.BaseQuickAdapter; import com.chad.library.adapter.base.BaseViewHolder; import com.llw.goodweather.R; import com.llw.goodweather.bean.MinutePrecResponse; import com.llw.goodweather.utils.DateUtils; import com.llw.goodweather.utils.WeatherUtil;

import java.util.List;

/* * 分鐘級降水列表介面卡 * @author llw / public class MinutePrecAdapter extends BaseQuickAdapter {

public MinutePrecAdapter(int layoutResId, @Nullable List<MinutePrecResponse.MinutelyBean> data) {
    super(layoutResId, data);
}

@Override
protected void convert(BaseViewHolder helper, MinutePrecResponse.MinutelyBean item) {
    String time = DateUtils.updateTime(item.getFxTime());
    //時間
    helper.setText(R.id.tv_time, WeatherUtil.showTimeInfo(time) + time);
    String type = null;
    if("rain".equals(item.getType())){
        type = "雨";
    }else if("snow".equals(item.getType())){
        type = "雪";
    }
    helper.setText(R.id.tv_precip_info,item.getPrecip()+"   "+type);
}

}

```

四、增加網路請求與回撥

現在佈局和準備工作都做好了,然後進入WeatherContract,寫一個請求介面的方法和回撥,在裡面增加如下程式碼

```java /* * 分鐘級降水 * * @param location 經緯度拼接字串,使用英文逗號分隔,經度在前緯度在後 / public void getMinutePrec(String location) { ApiService service = ServiceGenerator.createService(ApiService.class, 3); service.getMinutePrec(location).enqueue(new NetCallBack() { @Override public void onSuccess(Call call, Response response) { if (getView() != null) { getView().getMinutePrecResult(response); } }

            @Override
            public void onFailed() {
                if (getView() != null) {
                    getView().getWeatherDataFailed();
                }
            }
        });
    }

```

返回的方法

java //分鐘級降水 void getMinutePrecResult(Response<MinutePrecResponse> response); 增加的位置如下圖所示 在這裡插入圖片描述 在這裡插入圖片描述

五、控制元件初始化、資料請求和返回

然後回到MainActivty,你會發現有報錯,先不用管它,先初始化一些資料和類。

java @BindView(R.id.tv_precipitation) TextView tvPrecipitation;//降水預告 @BindView(R.id.tv_prec_more) TextView tvPrecMore;//降水詳情 @BindView(R.id.rv_prec_detail) RecyclerView rvPrecDetail;//分鐘級降水列表 通過butterknife繫結控制元件,然後

```java private List minutelyList = new ArrayList<>();//分鐘級降水資料列表 private MinutePrecAdapter mAdapterMinutePrec;//分鐘級降水介面卡

private boolean state = false;//分鐘級降水資料 收縮狀態  false 收縮  true 展開

``` 然後在initList方法中,對剛才定義的變數進行例項化。

java //分鐘級降水 mAdapterMinutePrec = new MinutePrecAdapter(R.layout.item_prec_detail_list,minutelyList); GridLayoutManager managerMinutePrec = new GridLayoutManager(context,2); managerMinutePrec.setOrientation(RecyclerView.HORIZONTAL); rvPrecDetail.setLayoutManager(managerMinutePrec); rvPrecDetail.setAdapter(mAdapterMinutePrec); 新增位置如下圖所示 在這裡插入圖片描述 最後就是在介面返回中進行資料賦值了。

重寫getMinutePrecResult方法。

java /** * 分鐘級降水返回 * @param response */ @Override public void getMinutePrecResult(Response<MinutePrecResponse> response) { dismissLoadingDialog();//關閉載入彈窗 checkAppVersion();//檢查版本資訊 if(response.body().getCode().equals(Constant.SUCCESS_CODE)) { tvPrecipitation.setText(response.body().getSummary()); if(response.body().getMinutely()!=null && response.body().getMinutely().size()>0){ minutelyList.clear(); minutelyList.addAll(response.body().getMinutely()); mAdapterMinutePrec.notifyDataSetChanged(); }else { ToastUtils.showShortToast(context, "分鐘級降水資料為空"); } }else { ToastUtils.showShortToast(context, CodeToStringUtils.WeatherCode(response.body().getCode())); } }

現在你的這個MainActivity頁面就不會報錯了。

不過這個時候你執行你會發現你看不到這個列表,那是因為我隱藏了。既然是隱藏的,那麼就需要一個開關了控制它的顯示才行,於是可以在onViewClick方法中增加一個id. 在這裡插入圖片描述 在這裡插入圖片描述 這裡通過點選的方式來控制這個列表的顯示和隱藏了,而很明顯這個顯示和隱藏我還加了動畫效果,否則就會顯得很突兀。這裡注意這個state的全域性變數,它的初始值是false,也就是不顯示,當我第一次點選時,它會進行判斷,會進入else中。這時候通過動畫展開這個佈局,展開之後設定為true,而此時你再點選時就會進入if中,然後就會收縮佈局,之後又把值設定為false。

OK,這個邏輯就講清楚了,下面來看看那這個動畫的方法吧。

六、動畫展開收縮效果

之前在mvplibrary中的utils包下建了一個AnimationUtil動畫工具類。那麼現在新增加兩個方法

```java /* * 展開動畫 * @param view 需要展開的View * @param textView 修改文字 / public static void expand(final View view, final TextView textView) { //檢視測量 傳入容器的寬高測量模式 view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); //獲取檢視的測量高度 final int viewHeight = view.getMeasuredHeight(); //設定佈局引數高度 view.getLayoutParams().height = 0; //檢視顯示 view.setVisibility(View.VISIBLE); textView.setText("收起詳情");

    Animation animation = new Animation() {
        /**
         * 重寫動畫更新函式
         * @param interpolatedTime 補插時間 計算動畫進度
         * @param t
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            if (interpolatedTime == 1) {
                //動畫已完成
                view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
            } else {
                //正在進行中
                view.getLayoutParams().height = (int) (viewHeight * interpolatedTime);
            }
            view.requestLayout();
        }
    };
    animation.setDuration(600);
    //設定插值器,即動畫改變速度
    animation.setInterpolator(new LinearOutSlowInInterpolator());
    view.startAnimation(animation);
}

/**
 * 收縮動畫
 * @param view 需要收縮的View
 * @param textView 修改文字
 */
public static void collapse(final View view,final TextView textView) {
    view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    final int viewHeight = view.getMeasuredHeight();

    Animation animation = new Animation() {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            if (interpolatedTime == 1) {
                view.setVisibility(View.GONE);
                textView.setText("檢視詳情");
            } else {
                view.getLayoutParams().height = viewHeight - (int) (viewHeight * interpolatedTime);
                view.requestLayout();
            }
        }
    };
    animation.setDuration(600);
    animation.setInterpolator(new LinearOutSlowInInterpolator());
    view.startAnimation(animation);
}

```

這裡的展開和收縮用的是補間動畫,通過動畫的執行時間和變化軌跡來操作,interpolatedTime == 1則表示動畫執行完成了,else中表示動畫進行中,進行時需要不斷的變更檢視的高度,然後之後重新繪製,以此達到動畫的效果。

java animation.setInterpolator(new LinearOutSlowInInterpolator()); 這個設定表示動畫的執行模式。它還有其他的的一些模式,不過我進過測試之後,最喜歡這個模式,感興趣的自行去嘗試。

七、執行GIF效果圖

好了,到了現在程式碼就寫完了,那麼來看看執行效果吧。

在這裡插入圖片描述

本來我是想放一個高清一點的GIF的,但是超過了5M就不行,所以只能看這個模糊的了。


文末

  說到這裡也就是這篇部落格的結束了,其實挺感慨的,這個天氣APP從我剛開始寫大概是3月份,現在已經到了12月了,時間過得真快呀。其實程式碼就像一個朋友一樣,你對它花的時間越多,它就對你越熟悉,而當你冷落它之後,你再回來它還在這裡,只不過你需要重新認識熟悉一下了。好在程式碼是講道理,每一個報錯都會有原因,挺感慨的。重新認識一下,你好!我是初學者,請多指教。山高水長,後會有期~

原始碼地址:GoodWeather 歡迎 StarFork