打造自己的 APP「冰與火百科」(二):介面、索引頁

SouthernBox發表於2017-07-17

開始「冰與火百科」開發之旅!

網路資料

先說一下我的介面是怎麼來的。

存放資料

首先確定自己需要一些什麼資料,在滿足自己要求的情況下越簡單越好。對每個詳情頁面,我需要一張圖片和一個 html 顯示描述就夠了。以奶德為例,在伺服器的對應目錄下,就會有 Eddard_Stark.png 和 Eddard_Stark.html 這兩個檔案。

這一步其實是整個專案最麻煩的地方。圖片還好,但收集整理描述的內容真的要非常有耐心,至今才造了十幾條資料。

建立資料集合

我需要兩個實體類。一個是分類,也就是到時 TabLayout 中的 Tab,另一個就是內容。對應的要生成兩個 json 檔案。

建立一個 java 專案,新增 Gson 依賴,建立兩個要轉換 json 的集合:

private static List<TabDTO> tabList = new ArrayList<>();
private static List<ContentDTO> contentList = new ArrayList<>();複製程式碼

再次以奶德為例,往裡面新增資料:

tabList.add(new TabDTO(10100,"person","Stark","史塔克"));複製程式碼
String starkUrl = "person/Stark/";
contentList.add(new ContentDTO(
        "Eddard_Stark",
        "person",
        "Stark",
        starkUrl + "Eddard_Stark.png",
        "艾德·史塔克",
        "臨冬城公爵、北境守護",
        starkUrl + "Eddard_Stark.html"));複製程式碼

生成 json 檔案

完了輸出為 json 檔案就好了,以 content 為例:

Gson gson = new Gson();
String jsonString = gson.toJson(contentList);
File contentJsonFile = new File("../IceAndFireServer/content.json");
try {
    OutputStreamWriter osw = new OutputStreamWriter(
            new FileOutputStream(contentJsonFile), "UTF-8");
    BufferedWriter bw = new BufferedWriter(osw);
    bw.write(jsonString, 0, jsonString.length());
    bw.flush();
    bw.close();
} catch (IOException e) {
    e.printStackTrace();
}複製程式碼

然後將資料上傳到網路就好了,json 檔案所在的網路地址就是你的介面了。剛開始我上傳到了 GitHub,但發現經常會發生靈異事件,導致資料無法訪問或者速度超慢,後來又上傳到了九牛雲。

這部分內容大家看一下就好了,畢竟不是常規的做法。有興趣的可以到這裡,資料和程式碼都在裡面了。

APP主題色

下面終於來到我們的 Android 專案了。

建立 Android 專案後,第一反應是主題色得改一改。

在官方 Material Design 的色板裡面,我選用了這一套:

對應的,color 檔案的主題色值修改如下:

<color name="colorPrimary">#607d8b</color>
<color name="colorPrimaryDark">#546e7a</color>
<color name="colorAccent">#40c4ff</color>複製程式碼

索引頁

我也學著別的 APP,做一個索引頁 IndexActivity。就簡單展示一句「挖了蘑菇立死」,噢不對,展示一句「Valar Morghulis」就好了,像這樣:

加入一點簡單的動畫,然後還能做一些耗時的啟動操作。

DataBinding

我會比較在意程式碼的簡潔性,在實現同樣功能的情況下程式碼越少越好,而且排版一定要看上去舒服,縮排要少,甚至不允許程式碼裡面有警告。

DataBinding 是一個可以增加程式碼簡潔性的東西。這裡以索引頁為例,簡單介紹一下它最簡單的一個應用,代替 findViewByid。

配置

在對應 Module 的 build.grade 裡配置:

android {
    ....
    dataBinding {
        enabled = true
    }
}複製程式碼

佈局

在需要繫結的佈局檔案裡,最外層增加一個 layout 標籤,比如這裡的 activity_index.xml :

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/tv_index"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/valar_morghulis"
        android:textColor="@color/color3A3A3A"
        android:textSize="36sp" />

</layout>複製程式碼

使用

建立一個成員變數:

private ActivityIndexBinding binding;複製程式碼

注意,這裡的變數型別是和佈局檔案相關的,比如 ActivityIndexBinding 對應 activity_index。

然後將原來 setContentView 的地方修改為:

binding = DataBindingUtil.setContentView(this, R.layout.activity_index);複製程式碼

當我要使用佈局裡的 TextView 的時候,直接用 binding.tvIndex 就可以了。tvIndex 這個名字是和佈局裡的 id:tv_title 相對應的。

DataBinding 的一些更高階的用法這裡就不贅述了,網上的教程很多,大家可以多搜尋瞭解一下。

動畫

為了讓索引頁的字更生動,我打算加一個漸變放大的動畫效果。

xml

我這裡用的是 View Animation(檢視動畫),動畫過程是通過 xml 檔案定義的。在 res/anim 資料夾下新建一個 xml 檔案,程式碼如下:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:shareInterpolator="true" >

    <scale
        android:duration="1300"
        android:fillAfter="true"
        android:fromXScale="0.95"
        android:fromYScale="0.95"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="1"
        android:toYScale="1" />

    <alpha
        android:duration="1300"
        android:fillAfter="true"
        android:fromAlpha="0"
        android:toAlpha="1" />

</set>複製程式碼

這個的意思很好理解,就是用 1.3 秒的時間,控制元件大小從 95% 漸變到 100%,透明度從 0 漸變到10%。

使用

執行動畫的程式碼也很簡單,大家直接看吧:

Animation animation = AnimationUtils
        .loadAnimation(this, R.anim.anim_valar_morghulis);
animation.setAnimationListener(new AnimationListener() {

    @Override
    public void onAnimationStart(Animation animation) {

    }

    @Override
    public void onAnimationEnd(Animation animation) {
        SystemClock.sleep(500);
        animationComplete = true;
        goMainPage();
    }

    @Override
    public void onAnimationRepeat(Animation animation) {

    }

});
SystemClock.sleep(200);
binding.tvIndex.startAnimation(animation);複製程式碼

為了讓使用者能看清動畫,我在裡面加入了一些停頓。經過我自己的多次試驗,最終定下的這個停頓時常,我認為長度是在能看清動畫的情況下,又不會長到讓人感到厭煩的,效果如下:

耗時操作

前面說到,在索引頁可以做一些耗時的操作。動畫的執行總共有兩秒的時間,使用者的時間是寶貴的,要是在這兩秒裡面什麼都不做就太浪費了。

最耗時的操作,應該是調介面了。

其實剛開始我是進入到首頁才調介面的,進入不同的頁面獲取不同的資料。但這樣會有一個問題,由於我沒有後臺,只有兩個假介面,所以搜尋功能就無法實現了。

所以現在改為,在索引頁獲取到所有資料並儲存起來,在不同分類頁面下通過篩選展示資料,這樣搜尋也可以實現了。

下面就簡單講一下目前比較流行的兩個框架 Retrofit 2 和 Realm,來完成資料的獲取和儲存。

Retrofit 2

Retrofit 的厲害之處我就不多說了,網上的教程很多的,我只講最最簡單的用法。

配置

在 Module 的 build.grade 裡新增依賴:

compile "com.squareup.retrofit2:retrofit:${RETROFIT_VERSION}"
compile "com.squareup.retrofit2:converter-scalars:${RETROFIT_VERSION}"
compile "com.squareup.retrofit2:converter-gson:${RETROFIT_VERSION}"複製程式碼

目前最新版是 2.3.0,大家可以自行替換。

這裡面 converter-scalars 是新增 String 型別的返回,converter-gson 是新增 Gson 的支援(返回實體類)。

介面定義

新建一個介面檔案(interface),用來統一管理所有要呼叫的介面(url),我暫時只有兩個介面,再留一個通用的 Get 請求備用:

public interface RequestServes {

    @GET("{url}")
    Call<String> get(@Path("url") String url);

    @GET("tab.json")
    Call<List<TabDTO>> getTab();

    @GET("content.json")
    Call<List<ContentDTO>> getContent();

}複製程式碼

註解 @GET 後面的內容就是要請求的介面,這裡不用寫基礎域名(BaseUrl)。

初始化

需要通過 Retrofit.Builder 初始化 Retrofit,呼叫 baseUrl 設定基礎域名:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(ServerAPI.BASE_URL)
        //增加返回值為String的支援
        .addConverterFactory(ScalarsConverterFactory.create())
        //增加返回值為實體類的支援
        .addConverterFactory(GsonConverterFactory.create())
        .build();
requestServes = retrofit.create(RequestServes.class);複製程式碼

需要注意的是,BaseUrl 必須以斜槓「/」結尾,否則會報錯。

考慮到可能多個頁面都需要呼叫介面,可以把這段程式碼放在 BaseActivity 裡。

使用

用起來超簡單:

Call<List<TabDTO>> call = requestServes.getTab();
call.enqueue(new Callback<List<TabDTO>>() {
    @Override
    public void onResponse(@NonNull Call<List<TabDTO>> call,
                           @NonNull retrofit2.Response<List<TabDTO>> response) {
        List<TabDTO> tabList = response.body();
        //...
        goMainPage();
    }

    @Override
    public void onFailure(@NonNull Call<List<TabDTO>> call, @NonNull Throwable t) {
        netError();
    }
});複製程式碼

請求失敗的時候,我會彈吐司提醒,並且讓頁面可點選重試。

Realm

Realm 是 SQLite 的替代者,它更快速、更易用。下面看看 Realm 的簡單使用。

配置

修改 Project 下的 build.gradle :

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "io.realm:realm-gradle-plugin:3.5.0"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}複製程式碼

目前最新版是 3.5.0。然後再到 Module 的 build.gradle,新增:

apply plugin: 'realm-android'複製程式碼

配置完畢

初始化

在使用 Realm 之前,必須先呼叫:

Realm.init(this);複製程式碼

獲取 Realm 例項有以下兩種方法:

Realm mRealm = Realm.getDefaultInstance();複製程式碼

這裡有個小細節。如果實體類的欄位發生了改變,這裡是會報錯的。我的做法比較粗暴,清空資料庫後再重新獲取:

try {
    mRealm = Realm.getDefaultInstance();
} catch (RuntimeException e) {
    Realm.deleteRealm(Realm.getDefaultConfiguration());
    mRealm = Realm.getDefaultInstance();
}複製程式碼

儲存

讓需要儲存下來的實體類繼承 RealmObject,然後就可以使用以下程式碼儲存了:

mRealm.beginTransaction();
mRealm.copyToRealm(list);
mRealm.commitTransaction();複製程式碼

查詢

查詢也很簡單,就一句程式碼的事:

List<Data> list = mRealm.where(Data.class).findAll();複製程式碼

複雜查詢這裡就不多說了。

需要注意的是,如果要對查詢的結果進行修改或刪除等操作,則必須要在 transaction 裡完成,修改的結果會同步到資料庫。比如,我想對上面查詢到的第一個元素進行修改:

Data data = list.get(0)
mRealm.beginTransaction()
mRealm.copyFromRealm(data)
data.num = 666
mRealm.commitTransaction()複製程式碼

小結

就先到這吧,一個索引頁都能扯這麼多。

其實我沒什麼想總結的,我只想提醒一下:GOT Session 7 !!!

專案地址

相關文章