一次地址選擇器的實踐

宇是我發表於2019-03-02

前言

我們知道,地址選擇器是一個通用元件,網上的開源專案也有很多。那麼為什麼還會有這篇文章呢?因為我在調研過程中發現,雖然都是地址選擇器,但是實現的方式卻各不相同。以下是調研地址選擇器的一些總結和思考實踐。希望對大家有幫助,大家有什麼更好的想法,也要告知我哦~

地址選擇器實現方式介紹

  • 本地存放area.db檔案
    大多數App都是此種實現

    • 優點:啟動快,不受網路影響
    • 缺點:
      • 不能實時更新,資料更新依賴釋出新版本。(或則自己實現一套更新機制,檔案從後端下載,實現較為複雜)
      • 各端(伺服器、前端、移動端)需要維護一份相同的地址資訊表,維護成本高。
      • 地址資訊表本地儲存,增大安裝包體積
  • 一次性從服務端拉取所有地址資訊
    在App啟動時候,一次性拉取所有地址資訊。

    • 優點:資料可配置、請求少
    • 缺點:
      • 每次啟動都請求伺服器,大部分是無用請求,浪費了伺服器資源。
      • 請求資料量大
    • 優化方案:更換請求時機,點選選擇地址的時候才去一次性請求所有資料。但是還是不能避免請求資料量過大問題。
  • 實時獲取省、市、區資訊,選中上一級才去獲取對應的下一級資料
    如:京東

    • 優點:資料可配置,請求資料量小
    • 缺點:請求較多,需要處理的異常case較多
    • 思考優化方案:每次喚起地址選擇器時,快取獲取的地址資訊歷史資料。顯示地址資訊的時候,只有本地快取沒有當前資料,才向服務端傳送請求。(其實這種場景很少出現,但是再小的蒼蠅也是肉,能優化就優化了吧……)

因為使用者基本是在有網路的情況下才會使用,所以選擇聯網獲取地址資訊也是合理的。又考慮到網路請求的大小,服務端效能影響,安裝包大小以及地址資訊可配置性等因素,實時獲取地址資訊是一個不錯的方式。下面介紹地址選擇器實現一些關鍵點。

實時獲取資訊的地址選擇器設計

先來看效果,如下圖所示。喚起地址選擇器會有一次省份的網路請求,之後每一級資料都是實時去獲取。在當前地址選擇器喚起狀態,獲取之後就將歷史資料儲存在本地,下一次就不再傳送網路請求了。

地址選擇器效果圖

整體的框架設計類圖如下所示:

框架類圖

資料結構設計

當前的地址資訊按照省、市、區/縣、街道四級劃分,後一級總是和前一級相關聯。

  • Province
public class Province {
    public long id;
    public String name;
}
複製程式碼
  • City
public class City  {
    public long id;
    public long province_id;
    public String name;
}
複製程式碼
  • County
public class County {
    public long id;
    public long city_id;
    public String name;
}
複製程式碼
  • Street
public class Street {
    public long id;
    public long county_id;
    public String name;
}
複製程式碼

回撥介面設計

主要提供了4個回撥方法:

public interface OnAddressSelectedListener {
    // 獲取地址完成回撥
    void onAddressSelected(Province province, City city, County county, Street street);
    // 選取省份完成回撥
    void onProvinceSelected(Province province);
    // 選取城市完成回撥
    void onCitySelected(City city);
    // 選取區/縣完成回撥
    void onCountySelected(County county);
}
複製程式碼

這裡著重說一下onAddressSelected回撥方法,其是在地址選擇完成的時候呼叫。那麼是如何判斷地址選擇已經完成呢。這個在AddressSelector中有如下一個機制。
每次一個級別選擇完成後,會獲取下一個級別的資料(網路請求或者快取獲取)進行顯示。顯示的時候有這麼一個邏輯,當前級別有資料,則正常顯示;若沒有,則說明地址選擇已經完成,此時呼叫onAddressSelected方法。

快取設計

我們這裡預設服務端每個地址的id都是唯一的。一次點選選擇地址的快取操作,可以看下如下的流程圖。

快取機制流程圖

  1. 建立三個快取map,分別快取省-市市-區區-街道
/** 快取資料:省-市 */
private ArrayMap<Long, List<City>> province2city = new ArrayMap<>();
/** 快取資料:市-區 */
private ArrayMap<Long, List<County>> city2county = new ArrayMap<>();
/** 快取資料:區-街道 */
private ArrayMap<Long, List<Street>> county2street = new ArrayMap<>();
複製程式碼
  1. 選擇省、市、區某一級時,先檢視是否有快取資料,若有則使用快取資料;若沒有,則向服務端傳送網路請求。
// 有快取則直接使用快取,否則去重新請求
if(province2city.containsKey(province.id)){
    setCities(province2city.get(province.id));
} else {
    progressBar.setVisibility(View.VISIBLE); 
    listener.onProvinceSelected(province);
}
複製程式碼

3、每次獲取的歷史資料,儲存在相應的快取map中。

province2city.put(provinceId, cities);
複製程式碼

Tab切換動畫實現

不知道大家有沒有發現,當我們選擇地址時,頂部tab下有一個小橫條,它會跟著我們選擇進行平滑切換的。橫條的動畫主要包含兩個部分:

  • 平滑移動到所選tab的文字下面
  • 根據選擇tab內容的長度,在橫條移動過程中,動態變化橫條長度。當移動結束時,剛好變化為與tab內容長度一致。

具體思路是通過ObjectAnimator來設定橫條移動的動畫效果,ValueAnimator實現橫條長度的動態變化,最後通過AnimatorSet來設定兩個動畫一起執行。控制元件中,封裝了一個方法實現此動畫效果。

private AnimatorSet buildIndicatorAnimatorTowards(TextView tab) {
    ObjectAnimator xAnimator = ObjectAnimator.ofFloat(indicator, "X", indicator.getX(), tab.getX() + tab.getPaddingLeft() * 2);

    final ViewGroup.LayoutParams params = indicator.getLayoutParams();
    ValueAnimator widthAnimator = ValueAnimator.ofInt(params.width, tab.getMeasuredWidth() - tab.getPaddingLeft() * 2);
    widthAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            params.width = (int) animation.getAnimatedValue();
            indicator.setLayoutParams(params);
        }
    });

    AnimatorSet set = new AnimatorSet();
    set.setInterpolator(new FastOutSlowInInterpolator());
    set.playTogether(xAnimator, widthAnimator);

    return set;
}
複製程式碼

控制元件使用

  1. gradle匯入控制元件
compile 'com.zr.addressselector:library:1.0.1'
複製程式碼
  1. Activity實現OnAddressSelectedListener介面
public class MainActivity extends AppCompatActivity implements OnAddressSelectedListener
複製程式碼
  1. 展現地址選擇器
dialog = new BottomSelectorDialog(MainActivity.this);
dialog.setOnAddressSelectedListener(MainActivity.this);
dialog.show();
複製程式碼
  1. 根據網路返回,設定資料
dialog.getSelector().setProvinces(Collections.singletonList(province));
複製程式碼
  1. 控制元件提供的幾個設定方法
/**
     * 設定回撥介面
     * @param listener
     */
    public void setOnAddressSelectedListener(OnAddressSelectedListener listener) {
        this.listener = listener;
    }

    /**
     * 設定省列表
     * @param provinces 省份列表
     */
    public void setProvinces(List<Province> provinces){
        handler.sendMessage(Message.obtain(handler, WHAT_PROVINCES_PROVIDED, provinces));
    }

    /**
     * 設定市列表
     * @param cities 城市列表
     */
    public void setCities(List<City> cities){
        handler.sendMessage(Message.obtain(handler, WHAT_CITIES_PROVIDED, cities));
    }

    /**
     * 設定區列表
     * @param countries 區/縣列表
     */
    public void setCountries(List<County> countries){
        handler.sendMessage(Message.obtain(handler, WHAT_COUNTIES_PROVIDED, countries));
    }

    /**
     * 設定街道列表
     * @param streets 街道列表
     */
    public void setStreets(List<Street> streets){
        handler.sendMessage(Message.obtain(handler, WHAT_STREETS_PROVIDED, streets));
    }
複製程式碼

寫在最後

雖然提供了gradle compile匯入的方式,但是整個控制元件的原始碼其實是很簡單的。如果有定製需求,可以到這裡下載原始碼。

專案原始碼下載:ZRAddressSelector

特別感謝

JDAddressSelector,本元件設計也是受此開源專案啟發,非常感謝作者開源~

相關文章