首先看一下效果動圖:
數據來源是公司的一個api,網址就不貼了,數據格式大概是這樣:
數據Bean:
-
public class BaseBean implements Serializable { private String Code; private String Message; public String getCode() { return Code; } public void setCode(String code) { Code = code; } public String getMessage() { return Message; } public void setMessage(String message) { Message = message; } public boolean isSuccessful() { return TextUtils.equals("1", getCode()); } }
-
public class Areas extends BaseBean { private Map<String, List<Cities>> Area; private List<Cities> HotCities; private String Version; public Map<String, List<Cities>> getArea() { return Area; } public void setArea(Map<String, List<Cities>> area) { Area = area; } public List<Cities> getHotCities() { return HotCities; } public void setHotCities(List<Cities> hotCities) { HotCities = hotCities; } public String getVersion() { return Version; } public boolean isSucceed() { return "1".equals(Version); } public void setVersion(String version) { Version = version; } }
-
@Entity(tableName = "city_table") public class Cities { /** * Province : 四川省 * ProvinceId : 23 * City : 阿壩藏族羌族自治州 * CityId : 2362 * District : 阿壩藏族羌族自治州 * DistrictId : 2362 * PinYin : aba */ @PrimaryKey(autoGenerate = true) private int Id; private int CityId; private String Province; private int ProvinceId; private String City; private String District; private int DistrictId; private String PinYin; /** * 忽略該屬性 */ @Ignore private List<Cities> mtags; /** * 忽略該屬性 */ @Ignore public int position; /** * @param tag 屬於哪個字母 * @param tagid -9 代表當前定位城市標題、熱門城市標題、 字母組標題 * -8 代表當前定位城市 * -7 熱門城市 */ public static Cities newCities(String tag, int tagid) { Cities cit = new Cities(); cit.setDistrict(tag); cit.setId(tagid); return cit; } public static Cities newCities(String tag, int tagid,int position) { Cities cit = new Cities(); cit.setDistrict(tag); cit.setId(tagid); cit.position = position; return cit; } public int getId() { return Id; } public int getViewType() { int cityid; switch (Id) { case -9: cityid = 0; break; case -8: cityid = 1; break; case -7: cityid = 2; break; case -10: cityid = 4; break; default: cityid = 3; break; } return cityid; } public void setId(int id) { Id = id; } public List<Cities> getMtags() { return mtags; } public void setMtags(List<Cities> mtags) { this.mtags = mtags; } public String getProvince() { return Province; } public void setProvince(String Province) { this.Province = Province; } public int getProvinceId() { return ProvinceId; } public void setProvinceId(int ProvinceId) { this.ProvinceId = ProvinceId; } public String getCity() { return City; } public void setCity(String City) { this.City = City; } public int getCityId() { return CityId; } public void setCityId(int CityId) { this.CityId = CityId; } public String getDistrict() { return District; } public void setDistrict(String District) { this.District = District; } public int getDistrictId() { return DistrictId; } public void setDistrictId(int DistrictId) { this.DistrictId = DistrictId; } public String getPinYin() { return PinYin; } public void setPinYin(String PinYin) { this.PinYin = PinYin; } }
可以看出在city類中配置了一些屬性,主要是爲了緩存它,整個的設計框架採用的是ViewModel+LiveData+Room,關於這個框架之前我已經詳細寫過一篇了,所以,這裏不做詳述;
實現的主要思路是:
1.使用retrofit+rxjava+okhttp3進行網絡請求,
api接口:
public interface CitiesService {
/**
* 選擇城市相關的網絡請求
*/
@FormUrlEncoded
@POST("api地址")
Observable<Areas> getCities(@Field("version") String version);
}
網絡獲取工具
public class NetWork {
private static SparseArray<NetWork> retrofitManagers = new SparseArray<>();
private Retrofit mRetrofit;
private NetWork(int hostType) {
Gson gson = new GsonBuilder()
//配置你的Gson
.setDateFormat("yyyy-MM-dd hh:mm:ss")
.create();
mRetrofit = new Retrofit.Builder()
.client(getFreeClient())
.baseUrl(Constant.getHost(hostType))
.addConverterFactory(ScalarsConverterFactory.create())//直接將數據轉換爲String
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
private OkHttpClient getFreeClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
X509TrustManager[] trustManager = new X509TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws
CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws
CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManager, new SecureRandom());
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
if (sslSocketFactory != null)
builder.sslSocketFactory(sslSocketFactory);
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(logging);
builder.connectTimeout(20, TimeUnit.SECONDS);
builder.writeTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS);
builder.retryOnConnectionFailure(true);
return builder.build();
}
public static NetWork getInstance(int hostType) {
NetWork retrofitManager = retrofitManagers.get(hostType);
if (retrofitManager == null) {
retrofitManager = new NetWork(hostType);
retrofitManagers.put(hostType, retrofitManager);
return retrofitManager;
}
return retrofitManager;
}
/**
* @param service 服務接口
* @return T
*/
public <T> T createService(final Class<T> service) {
return mRetrofit.create(service);
}
}
這裏爲了避免有多個retrofit生成,造成內存不足,使用了SparyArray去將每一個網絡工具實例類存儲 ,可以達到複用的效果;
2.數據庫存儲
BaseDb:數據庫配置:
@Database(entities = {Cities.class}, version = 1)
public abstract class BaseDB extends RoomDatabase {
public abstract CityDao cityDao();
private static BaseDB INSTANCE;
private static final Object sLock = new Object();
public static BaseDB getInstance(Context context) {
synchronized (sLock) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
BaseDB.class, "city").allowMainThreadQueries().build();
}
return INSTANCE;
}
}
}
數據庫操作類:CityDao
@Dao
public interface CityDao {
@Query("SELECT COUNT(1) from city_table")
int getDBDataCount();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertCitys(List<Cities> cities);
@Query("SELECT * from city_table where PinYin like '%' + :searStr +'%' or District like '%'+:searStr+ '%'" )
List<Cities> getAllCitiesData(String searStr);
}
viewModel類:數據的容器
public class BaseViewModel<T> extends ViewModel {
//Disposable 容器,拿到一個Disposable就執行clear,解除訂閱事件
private final CompositeDisposable mDisposable = new CompositeDisposable();
//LiveData封裝類,添加/消除數據源
private MutableLiveData<T> mTMutableLiveData = new MutableLiveData<>();
//LiveData的set,post方法,同步,異步設置數據
protected void setValue(T value) {
mTMutableLiveData.setValue(value);
}
protected void postValue(T value) {
mTMutableLiveData.postValue(value);
}
//添加一個Disposable到集合中
protected void add(Disposable disposable){
if (disposable!=null){
mDisposable.add(disposable);
}
}
//獲取LiveData
public LiveData<T> getData(){
return mTMutableLiveData;
}
//當Activity被銷燬時,取消所有訂閱事件
@Override
protected void onCleared() {
super.onCleared();
if(mDisposable!=null){
mDisposable.clear();
}
}
}
當接收到一個Disposable時,就添加到集合中,當Activity被銷燬時,就clear,取消所有訂閱事件;
城市的viewModel主要有 個方法:
1.從網絡加載數據
public void loadNetData(){
add(NetWork.getInstance(1)
.createService(CitiesService.class)
.getCities("5.3.1")
.filter(getPredicateByNet())
.filter(getPredicate())
.map(changeData())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Cities>>() {
@Override
public void accept(List<Cities> cities) throws Exception {
if (cities!=null&&cities.size()>0){
setValue(cities);
}
}
}));
}
在加載過程中,作類兩層過濾:
(1)判斷網絡地址庫版本號是否一致
/**
* 判斷網絡地址庫版本號是否一致
* @return
*/
private Predicate<? super Areas> getPredicateByNet() {
return new Predicate<Areas>() {
@Override
public boolean test(Areas areas) throws Exception {
if (areas!=null&&areas.isSucceed()){
//Todo: 判斷版本號
return true;
}
return true;
}
};
}
(2)過濾數據爲空的
/**
* 過濾數據爲null的
* @return
*/
private Predicate<? super Areas> getPredicate() {
return new Predicate<Areas>() {
@Override
public boolean test(Areas areas) throws Exception {
return areas!=null;
}
};
}
還做了一次數據的轉換,將通過網絡json封裝好的對象,轉換爲列表頁需要的數據,簡而言之就是,將由各自字母打頭的數據封裝到一個集合中,然後排序,這個過程還比較複雜:
/**
* 轉換數據
* @return
*/
private Function<? super Areas, List<Cities>> changeData() {
return new Function<Areas, List<Cities>>() {
@Override
public List<Cities> apply(Areas areas) throws Exception {
//用於存數據庫的數據
List<Cities> allData=new ArrayList<>();
//文字索引對應的position
HashMap<String,Integer> alphaIndexer=new HashMap<>();
//文字索引集合
List<String> sections=new ArrayList<>();
//所有城市數據
List<Cities> cities=new ArrayList<>();
//定位標題的邏輯
cities.add(Cities.newCities("定位城市",-9,0));
sections.add("定");
alphaIndexer.put("定",0);
//定位城市的邏輯
cities.add((Cities.newCities("定位中",-8,0)));
//熱門城市的邏輯
List<Cities> hotCities = areas.getHotCities();
if (hotCities!=null&&hotCities.size()>0){
cities.add(Cities.newCities("熱門城市",-9,2));
Cities rmcity = Cities.newCities("熱門城市", -7, 2);
sections.add("熱");
alphaIndexer.put("熱",2);
rmcity.setMtags(hotCities);
cities.add(rmcity);
}
//填充所有城市數據
if (areas!=null){
Map<String,List<Cities>> mCitiess=areas.getArea();
//排序方案,便於之後數據的使用
//存儲key值
List<String> keys=new ArrayList<>();
//循環遍歷所有城市的key(A,B,C....)
for (Map.Entry<String,List<Cities>> entry:mCitiess.entrySet()){
String key=entry.getKey();//獲取key值
sections.add(key);//將所有(A,B,C...)存入sections,(定,熱,A,B,...)
keys.add(key);//(A,B,C...)
cities.add(Cities.newCities(key,-9,cities.size()));
List<Cities> mCitys=entry.getValue();
int position=cities.size();
for (int i=0;i<mCitys.size();i++){
Cities cities1 = mCitys.get(i);
if (cities1!=null){
cities1.position=position-1;
}
}
alphaIndexer.put(key,position);
cities.addAll(mCitys);
allData.addAll(mCitys);
}
Collections.sort(keys);
}
//設置索引
mSections.postValue(sections);
//設置索引相關
mAlphaIndexer.postValue(alphaIndexer);
//服務器城市數據升級
// Todo:服務器城市數據升級 如果版本號不一致,或者數據個數不一樣,那麼將數據庫緩存刷新
if (cityDao.getDBDataCount()<DBSAVESUCCESSCOUNT){
cityDao.insertCitys(allData);
}
//返回所有數據到界面
return cities;
}
};
}
2.從數據庫加載數據:
(1)得到數據庫狀態:
/**
* 得到數據庫數據的狀態
*/
private void getSaveDBState() {
//超過50條城市數據代表保存成功
if (cityDao.getDBDataCount()>DBSAVESUCCESSCOUNT){
mSaveDBSuccess.setValue(true);
}else {
mSaveDBSuccess.setValue(false);
}
}
(2)從數據庫加載數據:
public void loadLocalData(){
getSaveDBState();
add(selectJsonDatasByKey()
.filter(getPredicateByNet())
.filter(getPredicate())
.map(changeData())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Cities>>() {
@Override
public void accept(List<Cities> cities) throws Exception {
setValue(cities);
loadNetData();
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
setValue(null);
loadNetData();
}
}));
}
每次從數據庫中取完數據,都再請求一次數據,然後重新存到數據庫中,這樣可以保證數據庫中數據每次都是最新的數據,同時,如果沒有網絡時,也可以顯示出數據;
同時,因爲樣本數據量過大,可以再本地也保存一份文件,這樣就可以保證一定可以取到數據了:
public Observable<Areas> selectJsonDatasByKey(){
return Observable.create(new ObservableOnSubscribe<Areas>() {
@Override
public void subscribe(ObservableEmitter<Areas> e) throws Exception {
if (!e.isDisposed()){
String json=null;
if (TextUtils.isEmpty(json)) {
json = AssetsUtil.getFromAssets("tuhucity.data", MyApplication.getContext().getAssets());
}
Areas data = new Gson().fromJson(json, Areas.class);
if (data != null) {
e.onNext(data);
e.onComplete();
} else {
e.onError(new Throwable("數據不存在"));
}
}
}
});
}
數據已經獲取到了,接下來就是數據的展示了:
總體頁面是這個樣子:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<!-- 標題 -->
<RelativeLayout
android:id="@+id/choicecity_head"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/choicecity_close"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="center">
<ImageView
android:id="@+id/img_back"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/back" />
</LinearLayout>
<TextView
android:id="@+id/choicecity_txt_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:gravity="center"
android:singleLine="true"
android:text="選擇城市"
android:textColor="#333"
android:textSize="@dimen/text_17" />
<LinearLayout
android:id="@+id/choicecity_txt_ts"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_alignParentRight="true"
android:layout_marginRight="15dp"
android:gravity="center">
<ProgressBar
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:indeterminate="false" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜索暫不可用" />
</LinearLayout>
</RelativeLayout>
<!-- 分割線 -->
<View
android:id="@+id/choicecity_view"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_below="@id/choicecity_head"
android:background="@color/app_bg" />
<!-- 搜索 -->
<include
android:id="@+id/view_choicecity_topserach"
layout="@layout/view_choicecity_topserach"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_below="@id/choicecity_view"
android:visibility="visible" />
<!-- 搜索 -->
<include
android:id="@+id/view_choicecity_serach"
layout="@layout/view_choicecity_serach"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_below="@id/choicecity_view"
android:visibility="gone" />
<!-- 內容 -->
<FrameLayout
android:id="@+id/choicecity_result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/choicecity_view"
android:layout_marginTop="44dp"
android:visibility="visible">
<RelativeLayout
android:id="@+id/choicecity_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/choicecity_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:overScrollFooter="@android:color/transparent"
android:overScrollHeader="@android:color/transparent"
android:overScrollMode="never"
android:scrollbars="none" />
<include
android:id="@+id/choicecity_title"
layout="@layout/item_choice_title"
android:visibility="gone"/>
<com.example.pengganggui.choiceCity.Widget.LetterListView
android:id="@+id/choicecity_city_letterlistview"
android:layout_width="30dip"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_gravity="right|center"
android:layout_marginBottom="30dp"
android:background="#00FFFFFF" />
</RelativeLayout>
<TextView
android:id="@+id/choicecity_overlay"
android:layout_width="80.0dp"
android:layout_height="80.0dp"
android:layout_gravity="center"
android:background="@drawable/show_head_toast_bg"
android:gravity="center"
android:textColor="#ffffffff"
android:textSize="30.0dip"
android:visibility="invisible" />
</FrameLayout>
<!-- 結果內容 -->
<android.support.v7.widget.RecyclerView
android:id="@+id/choicecity_srecyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/choicecity_view"
android:layout_marginTop="44dp"
android:background="@color/white"
android:overScrollFooter="@android:color/transparent"
android:overScrollHeader="@android:color/transparent"
android:overScrollMode="never"
android:scrollbars="none"
android:visibility="gone" />
</RelativeLayout>
主體時使用一個RecycleView,看看他的適配器:
public class ChoiceCityOldAdapter extends RecyclerView.Adapter{
/**
* 標題類型
*/
public static final int TYPE_TITLE=0;
/**
* 定位城市類型
*/
public static final int TYPE_LOCATION=1;
/**
* 熱門城市類型
*/
public static final int TYPE_HOT=2;
/**
* 顯示城市列表項
*/
public static final int TYPE_ITEM=3;
/**
* 空佈局類型
*/
public static final int TYPE_EMPTY=4;
private Context context;
private List<Cities> data=new ArrayList<>();
private LayoutInflater inflater;
private IClickCityListener iClickCityListener;
private Cities locationData = new Cities();
public ChoiceCityOldAdapter(Context context) {
this.context=context;
inflater=LayoutInflater.from(context);
if (context instanceof IClickCityListener) {
iClickCityListener = (IClickCityListener) context;
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType==TYPE_TITLE){
return new TitleViewHolder(inflater.inflate(R.layout.item_choice_title,parent,false));
}else if (viewType==TYPE_HOT){
return new HotCityViewHolder(context,iClickCityListener,inflater.inflate(R.layout.item_choice_context2,parent,false));
}else if (viewType==TYPE_ITEM){
return new CityViewHolder(inflater.inflate(R.layout.item_choice_context3,parent,false));
}else if (viewType==TYPE_LOCATION){
return new LocationCityViewHolder(inflater.inflate(R.layout.item_choice_context1,parent,false));
}else if (viewType==TYPE_EMPTY){
return new NoDataViewHolder(inflater.inflate(R.layout.item_choice_nodata,parent,false));
}
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Cities currentdata = data.get(position);
if (holder instanceof TitleViewHolder) {
TitleViewHolder holder1 = (TitleViewHolder) holder;
holder1.bindDataandListener(currentdata.getDistrict());
} else if (holder instanceof LocationCityViewHolder) {
LocationCityViewHolder holder1 = (LocationCityViewHolder) holder;
// 先後順序問題,定位先完成,或者數據解析先完成
holder1.bindDataandListener(iClickCityListener, TextUtils.isEmpty(locationData.getCity())?currentdata:locationData);
} else if (holder instanceof HotCityViewHolder) {
HotCityViewHolder holder1 = (HotCityViewHolder) holder;
holder1.bindDataandListener(currentdata);
} else if (holder instanceof CityViewHolder) {
CityViewHolder holder1 = (CityViewHolder) holder;
holder1.bindDataandListener(iClickCityListener, currentdata);
} else if (holder instanceof NoDataViewHolder) {
NoDataViewHolder holder1 = (NoDataViewHolder) holder;
holder1.bindDataandListener("true".equals(currentdata.getDistrict()));
}
}
@Override
public int getItemViewType(int position) {
if (position>data.size()){
return -1;
}
return data.get(position).getViewType();
}
@Override
public int getItemCount() {
return data.size();
}
public Object getItem(int position) {
if (data.size() <= position) {
return null;
}
return data.get(position);
}
public List<Cities> getData(){
return data;
}
public void setLocation(String city, String pr, String district) {
this.locationData.setCity(city);
this.locationData.setProvince(pr);
this.locationData.setDistrict(district);
notifyDataSetChanged();
}
public void setData(List<Cities> data) {
if (data != null) {
this.data = data;
} else {
this.data.clear();
}
notifyDataSetChanged();
}
public void clear() {
if (data != null) {
data.clear();
notifyDataSetChanged();
}
}
public void addData(List<Cities> datas) {
if (datas != null) {
data.addAll(datas);
notifyDataSetChanged();
}
}
}
Item分爲5種狀態:標題,定位城市,熱門城市,顯示城市的Item,還有一個空佈局:
對應的就是5個ViewHolder
TitleViewHolder,HotCityViewHolder,CityViewHolder,LocationCityViewHolder,NoDataViewHolder
ViewHolder都比較簡單,根據數據一一填充到控件中就可以了;
這裏有一個標題懸浮欄的控制類:主要就是通過監聽RecycleView的滑動事件去實現的:
public class SuspendView {
/**
* h1用於保存title分組的實際高度
*/
private int h1 = 0;
/**
* 用於保存當前RecycleView頂部第一個可見的position
*/
private int mCurrentPosition = 0;
/**
* 用於記錄,當前懸浮顯示的組內容對應的position
*/
private int mCurrentTitlePositioin = 0;
private RecyclerView mRecyclerView;
private TitleViewHolder mTitleViewHolder;
private ChoiceCityOldAdapter mChoiceCityAdapter;
private LinearLayoutManager mLinearLayoutManager;
public SuspendView(RecyclerView recyclerView, TitleViewHolder titleViewHolder, ChoiceCityOldAdapter choiceCityAdapter) {
this.mRecyclerView = recyclerView;
this.mTitleViewHolder = titleViewHolder;
this.mChoiceCityAdapter = choiceCityAdapter;
this.mLinearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
addOnScrollListener();
}
public void addOnScrollListener() {
if (this.mRecyclerView != null) {
this.mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//title分組的實際高度賦值的地方
h1 = mTitleViewHolder.itemView.getHeight();
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(mChoiceCityAdapter.getItemViewType(mCurrentPosition + 1)==-1){
return;
}
//通過第二個可見的position去判斷下一個view是否是標題
if (mChoiceCityAdapter.getItemViewType(mCurrentPosition + 1) == ChoiceCityOldAdapter.TYPE_TITLE) {
//如果是標題就獲取這個view
View view = mLinearLayoutManager.findViewByPosition(mCurrentPosition + 1);
//如果這個view不爲null
if (view != null) {
//判斷這個view距離頂部的top是否小於h1(標題的高度)
if (view.getTop() <= h1) {
//主要代碼:小於等於h1說明,懸浮的view需要往上滑動(h1-view.top)的距離,就可以實現將view1擠上去的效果
mTitleViewHolder.itemView.setY(-(h1 - view.getTop()));
} else {
//距離不夠的情況,判斷view1是否有滑動過,如果滑動過就歸位。
if (mTitleViewHolder.itemView.getY() != 0f) {
mTitleViewHolder.itemView.setY(0f);
}
}
}
}
if (mCurrentPosition != mLinearLayoutManager.findFirstVisibleItemPosition()) {
//判斷當前記錄的可見positon是否一致
mCurrentPosition = mLinearLayoutManager.findFirstVisibleItemPosition();
//不一致就重新賦值
if (mTitleViewHolder.itemView.getY() != 0f) {
//同時判斷view1是否有滑動過,如果滑動過就歸位。
mTitleViewHolder.itemView.setY(0f);
}
if (mCurrentPosition < mChoiceCityAdapter.getData().size() && mCurrentTitlePositioin != (mChoiceCityAdapter.getData()).get(mCurrentPosition).position) {
//用於更新標題的ViewHolder,即滑動之後更新
mCurrentTitlePositioin = mChoiceCityAdapter.getData().get(mCurrentPosition).position;
mTitleViewHolder.bindDataandListenerBySuspendView(mChoiceCityAdapter.getData().get(mCurrentTitlePositioin).getDistrict());
}
}
}
});
}
}
/**
* 綁定數據
*/
public void bindData() {
mTitleViewHolder.itemView.setVisibility(View.VISIBLE);
if (0 < mChoiceCityAdapter.getData().size() && mChoiceCityAdapter.getData().get(0).getViewType() == ChoiceCityOldAdapter.TYPE_TITLE) {
//用於更新標題的ViewHolder,即第一次更新
mTitleViewHolder.bindDataandListenerBySuspendView(mChoiceCityAdapter.getData().get(0).getDistrict());
}
}
}
還有右側滑動欄,他是一個自定義控件,這個實現起來也比較簡單,根據字母數量計算出每個字母的高度,再一個個畫上去就好了:
public class LetterListView extends View {
private Context context;
List<String> chars;//字母集合
int choose = -1; //選擇位置
Paint paint = new Paint();//畫筆
boolean showBkg = false;//是否顯示字母框
OnTouchingLetterChangedListener onTouchingLetterChangedListener;
public LetterListView(Context context) {
this(context, null);
}
public LetterListView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LetterListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
public void setChars(List<String> chars) {
this.chars = chars;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (chars != null && chars.size() > 0) {
if (showBkg) {
canvas.drawColor(Color.parseColor("#00FFFFFF"));
}
int height = getHeight();
int width = getWidth();
int singleHeight = height / chars.size();//每個字母的高度
for (int i = 0; i < chars.size(); i++) {
paint.setColor(Color.parseColor("#4A90E2"));
paint.setTextAlign(Paint.Align.CENTER);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(DensityUtils.sp2px(context, 11));
if (i == choose) {
paint.setColor(Color.parseColor("#3399ff"));
paint.setFakeBoldText(true);//設置粗體
}
float xPos = width / 2 - paint.measureText(chars.get(i)) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(chars.get(i), xPos, yPos, paint);
paint.reset();
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (chars != null && chars.size() > 0) {
final int action = event.getAction();
final float y = event.getY();
final int oldChoose = choose;
final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;
final int c = (int) (y / getHeight() * chars.size());
switch (action) {
case MotionEvent.ACTION_DOWN:
showBkg = true;
if (oldChoose != c && listener != null) {
if (c >= 0 & c < chars.size()) {
listener.onTouchingLetterChanged(chars.get(c));
choose = c;
invalidate();
}
}
break;
case MotionEvent.ACTION_MOVE:
if (oldChoose != c && listener != null) {
if (c >= 0 && c < chars.size()) {
listener.onTouchingLetterChanged(chars.get(c));
choose = c;
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
showBkg = false;
choose = -1;
invalidate();
break;
}
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
public void setOnTouchingLetterChangedListener(OnTouchingLetterChangedListener listener){
this.onTouchingLetterChangedListener=listener;
}
//觸摸到字母的接口
public interface OnTouchingLetterChangedListener {
void onTouchingLetterChanged(String s);
}
}
大致的思路就是這樣了,最後貼一下主活動的代碼:
public class CityChooseActivity1 extends AppCompatActivity {
public static final int ACCESS_LOCATION = 0;
public final static int LocationOK_Msg = 1;
public final static int LocationError_Msg = 2;
public final static String LOCATION_STATE1 = "定位中";
public final static String LOCATION_STATE2 = "失敗";
private Dialog dialog;
@BindView(R.id.img_back)
ImageView img_back;
@BindView(R.id.choicecity_txt_ts)
LinearLayout choicecity_txt_ts;
@BindView(R.id.view_choicecity_topserach)
View view_choicecity_topserach;
@BindView(R.id.choicecity_topsearch_ed)
TextView choicecity_topsearch_ed;
@BindView(R.id.view_choicecity_serach)
View view_choicecity_serach;
@BindView(R.id.choicecity_search_del)
ImageView choicecity_search_del;
@BindView(R.id.choicecity_search_cancel)
TextView choicecity_search_cancel;
@BindView(R.id.choicecity_result)
FrameLayout choicecity_result;
@BindView(R.id.choicecity_layout)
RelativeLayout choicecity_layout;
@BindView(R.id.choicecity_recyclerview)
RecyclerView choicecity_recyclerview;
@BindView(R.id.choicecity_title)
View choicecity_title;
@BindView(R.id.item_choice_text)
TextView item_choice_text;
@BindView(R.id.choicecity_city_letterlistview)
LetterListView choicecity_city_letterlistview;
@BindView(R.id.choicecity_overlay)
TextView choicecity_overlay;
@BindView(R.id.choicecity_srecyclerview)
RecyclerView choicecity_srecyclerview;
@BindView(R.id.choicecity_search_ed)
EditText choicecity_search_ed;
@BindView(R.id.choicecity_close)
LinearLayout choicecity_close;
/**
* 索引相關的集合
*/
private HashMap<String, Integer> mStringIntegerHashMap;
/**
* 數據庫查詢Dao
*/
CityDao cityDao= BaseDB.getInstance(MyApplication.getContext()).cityDao();
/**
* 服務器返回的數據,作適配器
*/
private ChoiceCityOldAdapter mChoiceCityAdapter1;
/**
* 本地搜索的數據,作適配器
*/
private ChoiceCityOldAdapter mChoiceCityAdapter2;
private Handler handler;
private OverlayThread overlayThread;
private Observer<CharSequence> mObserver;
/**
* 標題滑動模塊
*/
private TitleViewHolder mTitleViewHolder;
private SuspendView mSuspendView;
private MyHandler mHandler = null;
/**
* 右邊欄點擊擡起後將中間文字1秒後消失
*/
private class OverlayThread implements Runnable {
@Override
public void run() {
choicecity_overlay.setVisibility(View.GONE);
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設置狀態欄爲白色
StatusBarUtil.setColor(this, getResources().getColor(R.color.white));
setContentView(R.layout.activity_choicecity);
ButterKnife.bind(this);
StatusBarUtil.StatusBarLightMode(this);
initView();
initData();
initViewModel();
}
private void initViewModel() {
CitiesViewModel mCitiesViewModel = ViewModelProviders.of(this).get(CitiesViewModel.class);
mCitiesViewModel.getData().observe(this, new android.arch.lifecycle.Observer<List<Cities>>() {
@Override
public void onChanged(@Nullable List<Cities> cities) {
mChoiceCityAdapter1.setData(cities);
mSuspendView.bindData();
}
});
mCitiesViewModel.mAlphaIndexer.observe(this, new android.arch.lifecycle.Observer<HashMap<String, Integer>>() {
@Override
public void onChanged(@Nullable HashMap<String, Integer> stringIntegerHashMap) {
mStringIntegerHashMap = stringIntegerHashMap;
}
});
mCitiesViewModel.mSections.observe(this, new android.arch.lifecycle.Observer<List<String>>() {
@Override
public void onChanged(@Nullable List<String> strings) {
choicecity_city_letterlistview.setChars(strings);
}
});
mCitiesViewModel.mSaveDBSuccess.observe(this, new android.arch.lifecycle.Observer<Boolean>() {
@Override
public void onChanged(@Nullable Boolean aBoolean) {
choicecity_txt_ts.setVisibility(aBoolean ? View.GONE : View.VISIBLE);
}
});
mCitiesViewModel.loadLocalData();
}
private void initData() {
handler = new Handler();
mHandler = new MyHandler(new WeakReference<Activity>(this));
mChoiceCityAdapter1 = new ChoiceCityOldAdapter(this);
init(mChoiceCityAdapter1,choicecity_recyclerview);
mSuspendView = new SuspendView(choicecity_recyclerview, mTitleViewHolder, mChoiceCityAdapter1);
mChoiceCityAdapter2 = new ChoiceCityOldAdapter(this);
init(mChoiceCityAdapter2,choicecity_srecyclerview);
}
private void initView() {
//監聽搜索框EditText的內容改變事件
//使用RxTextView監聽,可以過濾用戶輸入太快的文字,這樣就可以減小查詢數據庫的次數
Observable<CharSequence> subscription = RxTextView.textChanges(choicecity_search_ed)
.debounce(400, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread());
mObserver = new Observer<CharSequence>() {
@Override
public void onSubscribe(@NonNull Disposable disposable) {
}
@Override
public void onError(@NonNull Throwable throwable) {
}
@Override
public void onComplete() {
}
@Override
public void onNext(@NonNull CharSequence charSequence) {
if (choicecity_search_ed.getText().length() > 0) {
choicecity_search_del.setVisibility(View.VISIBLE);
String SearStr = (charSequence + "").replaceAll("'", "")
.replace(" while ", "")
.replace(" in ", "")
.replace(" like ", "")
.trim();
List<Cities> list = cityDao.getAllCitiesData(SearStr);
if (list != null && list.size() > 0) {
mChoiceCityAdapter2.setData(list);
mChoiceCityAdapter2.notifyDataSetChanged();
} else {
addNoDataViewHolder("true");
}
} else {
choicecity_search_del.setVisibility(View.GONE);
addNoDataViewHolder("false");
}
}
};
subscription.subscribe(mObserver);
mTitleViewHolder = new TitleViewHolder(findViewById(R.id.choicecity_title));
mTitleViewHolder.setVisibilityViewH(false);
overlayThread=new OverlayThread();
choicecity_city_letterlistview.setOnTouchingLetterChangedListener(new LetterListView.OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(String s) {
if (mStringIntegerHashMap!=null&&mStringIntegerHashMap.get(s)!=null){
int position=mStringIntegerHashMap.get(s);
choicecity_recyclerview.scrollToPosition(position);
choicecity_overlay.setVisibility(View.VISIBLE);
choicecity_overlay.setText(s);
handler.removeCallbacks(overlayThread);
//延遲一秒後執行,讓overlay消失
handler.postDelayed(overlayThread,1000);
}
}
});
choicecity_close.setVisibility(View.VISIBLE);
}
public void init(RecyclerView.Adapter mAdapter,RecyclerView recyclerView) {
if (mAdapter == null) {
return;
}
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView,
int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setAdapter(mAdapter);
}
public class MyHandler extends Handler {
WeakReference<Activity> weakReference;
public MyHandler(WeakReference<Activity> reference) {
weakReference = reference;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Activity activity = weakReference.get();
if (null == activity || activity.isFinishing()) {
return;
}
switch (msg.what) {
case LocationOK_Msg:
Bundle bundle = msg.getData();
String city = bundle.getString("city");
String province = bundle.getString("province");
String district = bundle.getString("district");
onLocationOK(city, province, district);
break;
case LocationError_Msg:
onLocationError();
break;
default:
break;
}
}
}
private void onLocationError() {
mChoiceCityAdapter1.setLocation(LOCATION_STATE2, LOCATION_STATE2, LOCATION_STATE2);
if (NetworkUtil.checkNetWork(this)) {
showOrderDialog();
}
}
/**
* 顯示定位失敗彈框
*/
private void showOrderDialog() {
if (this.isFinishing()) {
return;
}
cancel();
dialog = new Dialog(this, R.style.MyDialogStyleBottomtishi);
dialog.setContentView(R.layout.order_estimate_exit_dialog);
TextView btn_ok_tips_title = dialog.findViewById(R.id.btn_ok_tips_title);
btn_ok_tips_title.setText("定位失敗");
TextView textView = dialog.findViewById(R.id.tv_tips);
textView.setText("定位服務已被關閉,\n\n請點擊\"設置\"-\"權限\"-打開所需權限。\n\n最後點擊兩次後退按鈕,即可返回。");
textView.setVisibility(View.VISIBLE);
RelativeLayout layout = dialog.findViewById(R.id.ordet_text_layout);
layout.setVisibility(View.GONE);
Button cancel_tips = dialog.findViewById(R.id.btn_cancel_tips);
cancel_tips.setText("取消");
cancel_tips.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
mChoiceCityAdapter1.setLocation(LOCATION_STATE2, LOCATION_STATE2, LOCATION_STATE2);
}
});
Button ok_tips = dialog.findViewById(R.id.btn_ok_tips);
ok_tips.setText("確定");
ok_tips.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
}
});
setWidthShowDialog(dialog);
}
public static void setWidthShowDialog(Dialog dialog) {
if (dialog != null && !dialog.isShowing()) {
dialog.show();
WindowManager.LayoutParams layoutParams = dialog.getWindow().getAttributes();
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
dialog.getWindow().getDecorView().setPadding(0, 0, 0, 0);
dialog.getWindow().setAttributes(layoutParams);
}
}
/**
* 取消定位失敗彈框
*/
private void cancel() {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
}
/**
* 定位成功
*/
private void onLocationOK(String city, String locationProvince, String district) {
if (locationProvince == null || locationProvince.equals("")) {
mChoiceCityAdapter1.setLocation(LOCATION_STATE2, LOCATION_STATE2, LOCATION_STATE2);
if (NetworkUtil.checkNetWork(this)) {
showOrderDialog();
}
} else {
if (TextUtils.isEmpty(district)
|| (!TextUtils.isEmpty(district)
&& !(district.endsWith("縣")
|| district.endsWith("市")
|| district.endsWith("旗")))) {
district = locationProvince;
}
mChoiceCityAdapter1.setLocation(city, locationProvince, district);
}
}
@OnClick({R.id.view_choicecity_topserach,R.id.choicecity_search_cancel,R.id.choicecity_search_del
,R.id.choicecity_search_ed,R.id.choicecity_close})
public void onClick(View view){
switch (view.getId()){
case R.id.view_choicecity_topserach:
//搜索框點擊事件
if (choicecity_txt_ts.getVisibility()==View.GONE){
view_choicecity_serach.setVisibility(View.VISIBLE);
view_choicecity_topserach.setVisibility(View.GONE);
choicecity_srecyclerview.setVisibility(View.VISIBLE);
addNoDataViewHolder("false");
//顯示軟鍵盤
KeyboardUtil.showKeyboard(choicecity_search_ed);
}
break;
case R.id.choicecity_search_cancel:
//搜索框取消按鈕
cancelSearch();
break;
case R.id.choicecity_search_del:
//搜索框內“叉”按鈕
//將EditText內容清空,並且將叉按鈕隱藏
choicecity_search_ed.setText("");
choicecity_search_del.setVisibility(View.GONE);
break;
case R.id.choicecity_search_ed:
//搜索框EditText
KeyboardUtil.showKeyboard(choicecity_search_ed);
break;
case R.id.choicecity_close:
if(!onBack()) {
finish();
}
break;
}
}
public boolean onBack() {
if (view_choicecity_serach.getVisibility() == View.VISIBLE) {
cancelSearch();
return true;
}
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode()==KeyEvent.KEYCODE_BACK&&event.getRepeatCount()==0){
if (onBack()){
return true;
}else {
RegexUtil.showInfo(this,"請選擇您所在的地區",false);
return true;
}
}
return super.onKeyDown(keyCode, event);
}
/**
* 取消搜索
*/
private void cancelSearch() {
//1.帶取消搜索框,搜索結果RecyclerView都消失,不帶取消的搜索框顯示
view_choicecity_serach.setVisibility(View.GONE);
choicecity_srecyclerview.setVisibility(View.GONE);
view_choicecity_topserach.setVisibility(View.VISIBLE);
//2.清空EditText內容
choicecity_search_ed.setText("");
//3.清空Adapter數據
if (mChoiceCityAdapter2!=null){
mChoiceCityAdapter2.clear();
}
}
/**
* 添加沒有數據的情況
* @param tag 沒數據,纔打開兩種情況,true爲沒數據
*/
private void addNoDataViewHolder(String tag) {
//1.清除RecyclerView的適配器數據
mChoiceCityAdapter2.getData().clear();
//2.獲取數據
List<Cities> datas=new ArrayList<>();
datas.add(Cities.newCities(tag,-10));
//3.將數據添加到數據適配器
mChoiceCityAdapter2.addData(datas);
//4.通知數據改變
mChoiceCityAdapter2.notifyDataSetChanged();
}
}