首先看一下效果动图:         



数据来源是公司的一个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;/*** 忽略该属性*/@Ignoreprivate List<Cities> mtags;/*** 忽略该属性*/@Ignorepublic 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() {@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throwsCertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throwsCertificateException {}@Overridepublic 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() {@Overridepublic 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);}}//获取LiveDatapublic LiveData<T> getData(){return mTMutableLiveData;}//当Activity被销毁时,取消所有订阅事件@Overrideprotected 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>>() {@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic boolean test(Areas areas) throws Exception {return areas!=null;}};
}

还做了一次数据的转换,将通过网络json封装好的对象,转换为列表页需要的数据,简而言之就是,将由各自字母打头的数据封装到一个集合中,然后排序,这个过程还比较复杂:

/*** 转换数据* @return*/
private Function<? super Areas, List<Cities>> changeData() {return new Function<Areas, List<Cities>>() {@Overridepublic List<Cities> apply(Areas areas) throws Exception {//用于存数据库的数据List<Cities> allData=new ArrayList<>();//文字索引对应的positionHashMap<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>>() {@Overridepublic void accept(List<Cities> cities) throws Exception {setValue(cities);loadNetData();}}, new Consumer<Throwable>() {@Overridepublic void accept(Throwable throwable) throws Exception {setValue(null);loadNetData();}}));
}

每次从数据库中取完数据,都再请求一次数据,然后重新存到数据库中,这样可以保证数据库中数据每次都是最新的数据,同时,如果没有网络时,也可以显示出数据;

同时,因为样本数据量过大,可以再本地也保存一份文件,这样就可以保证一定可以取到数据了:

public  Observable<Areas> selectJsonDatasByKey(){return Observable.create(new ObservableOnSubscribe<Areas>() {@Overridepublic 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"><!-- 标题 --><RelativeLayoutandroid:id="@+id/choicecity_head"android:layout_width="match_parent"android:layout_height="50dp"android:gravity="center_vertical"><LinearLayoutandroid:id="@+id/choicecity_close"android:layout_width="50dp"android:layout_height="50dp"android:gravity="center"><ImageViewandroid:id="@+id/img_back"android:layout_width="30dp"android:layout_height="30dp"android:src="@drawable/back" /></LinearLayout><TextViewandroid: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" /><LinearLayoutandroid: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"><ProgressBarstyle="?android:attr/progressBarStyleSmall"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginRight="5dp"android:indeterminate="false" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="搜索暂不可用" /></LinearLayout></RelativeLayout><!-- 分割线 --><Viewandroid: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" /><!-- 搜索 --><includeandroid: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" /><!-- 搜索 --><includeandroid: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" /><!-- 内容 --><FrameLayoutandroid: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"><RelativeLayoutandroid:id="@+id/choicecity_layout"android:layout_width="match_parent"android:layout_height="match_parent"><android.support.v7.widget.RecyclerViewandroid: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" /><includeandroid:id="@+id/choicecity_title"layout="@layout/item_choice_title"android:visibility="gone"/><com.example.pengganggui.choiceCity.Widget.LetterListViewandroid: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><TextViewandroid: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.RecyclerViewandroid: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;}}@Overridepublic 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;}@Overridepublic 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()));}}@Overridepublic int getItemViewType(int position) {if (position>data.size()){return -1;}return data.get(position).getViewType();}@Overridepublic 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() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);//title分组的实际高度赋值的地方h1 = mTitleViewHolder.itemView.getHeight();}@Overridepublic 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) {//如果是标题就获取這个viewView view = mLinearLayoutManager.findViewByPosition(mCurrentPosition + 1);//如果这个view不为nullif (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();}@Overrideprotected 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();}}}@Overridepublic 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;}@Overridepublic 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 {@Overridepublic void run() {choicecity_overlay.setVisibility(View.GONE);}}@Overrideprotected 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>>() {@Overridepublic void onChanged(@Nullable List<Cities> cities) {mChoiceCityAdapter1.setData(cities);mSuspendView.bindData();}});mCitiesViewModel.mAlphaIndexer.observe(this, new android.arch.lifecycle.Observer<HashMap<String, Integer>>() {@Overridepublic void onChanged(@Nullable HashMap<String, Integer> stringIntegerHashMap) {mStringIntegerHashMap = stringIntegerHashMap;}});mCitiesViewModel.mSections.observe(this, new android.arch.lifecycle.Observer<List<String>>() {@Overridepublic void onChanged(@Nullable List<String> strings) {choicecity_city_letterlistview.setChars(strings);}});mCitiesViewModel.mSaveDBSuccess.observe(this, new android.arch.lifecycle.Observer<Boolean>() {@Overridepublic 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>() {@Overridepublic void onSubscribe(@NonNull Disposable disposable) {}@Overridepublic void onError(@NonNull Throwable throwable) {}@Overridepublic void onComplete() {}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView,int newState) {super.onScrollStateChanged(recyclerView, newState);}@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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://搜索框EditTextKeyboardUtil.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;}@Overridepublic 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();}
}

android开发-城市选择页面相关推荐

  1. android个性化推荐选择页面,基于Android的菜谱个性化推荐系统的设计与开发

    汪丽娟+钱育蓉 摘要:据调查,当今社会中,有不少人存在"选择吃什么"的困难症,为解决这一问题,文章设计了一种菜谱个性化推荐系统.该系统分为客户端和服务端,服务端进行系统的推荐计算, ...

  2. android开发实现选择列表,Android使用RecyclerView实现列表数据选择操作

    Android使用RecyclerView实现列表数据选择操作 发布时间:2020-08-31 17:50:13 来源:脚本之家 阅读:76 作者:迟做总比不做强 这些时间做安卓盒子项目,因为安卓电视 ...

  3. Android开发:recycleView页面点击跳转

    对带有recycleView的页面进行点击跳转 比如,某一tab页是新闻列表,则点击某一行能跳转到新闻详情页面 主要步骤 一.设计recycleView页面 1.因为我们需要在消息界面建一个消息列表, ...

  4. android 开发 之欢迎页面

    基本功能: 显示欢迎页面3秒后跳转到主activity public class Welcome extends Activity{private final int SPLASH_DISPLAY_L ...

  5. Flutter 侧滑栏UI及城市选择UI的实现

    转载自北斗星_And大神的博客,未经原博主同意,禁止转载 前言   目前移动市场上很多业务都需要开发Android/IOS两个端,开发成本比较高. Flutter 在跨端上凭借着性能优势关注量,使用度 ...

  6. 微信小程序手把手教你实现带字母索引的城市选择列表

    微信小程序手把手教你实现带字母索引的城市选择列表 前言 需求分析 左边可滑动列表 滑动列表UI实现 item点击事件 右边带字母的索引条 索引条从上到下分别是定位和26个大写字母 索引条响应触摸和点击 ...

  7. android 线性布局蒙层,Android开发 - 掌握ConstraintLayout(一)传统布局的问题

    在传统的Android开发中,页面布局占用了我们很多的开发时间,而且面对复杂页面的时候,传统的一些布局会显得非常复杂,每种布局都有特定的应用场景,我们通常需要各种布局结合起来使用来实现复杂的页面.随着 ...

  8. Android开发 - 掌握ConstraintLayout(一)传统布局的问题

    在传统的Android开发中,页面布局占用了我们很多的开发时间,而且面对复杂页面的时候,传统的一些布局会显得非常复杂,每种布局都有特定的应用场景,我们通常需要各种布局结合起来使用来实现复杂的页面.随着 ...

  9. React项目实战之租房app项目(四)长列表性能优化城市选择模块渲染列表

    前言 目录 前言 一.长列表性能优化 1.1 概述 1.2 懒渲染 1.3 可视区渲染(React-virtualized) 二.react-virtualized组件 2.1 概述 2.2 基本使用 ...

最新文章

  1. Spring整合rabbitmq---消息接收
  2. UML 类之间的几种关系
  3. 启动与停止mysql服务的命令
  4. 定量遥感:计算地方时和太阳高度角(C++代码)
  5. 为什么人类的大脑认为数学“美”?
  6. 2017百度之星复赛:1003. Pokémon GO(递推)
  7. 数据分析岗位面试必备
  8. 深度剖析 C++ 对象池自动回收技术实现
  9. 面试题:用两个队列实现一个栈
  10. python转cython_说说cython的缺点
  11. 第五章第六题(英里与千米之间的互换)(Conversion from mile to kilometer and kilometer to mile)
  12. Android系统分区备份与还原
  13. SVG互动排版公众号图文 『两次物体移动与展开长图』 模板代码
  14. 怎么用python画出Excel表格数据的残差图
  15. 双端队列--------------------------------思维(性质)
  16. VoLTE常用12大知识点
  17. 为什么只有中国的程序员节是10月24日?
  18. 九章算法面试题54 带重复元素的全排列
  19. PIM-SM协议(ASM模型)
  20. 全面解析人、机、料、法、环,请收好

热门文章

  1. 市场营销策划的主要方法及应注意的问题
  2. python文档之查看帮助文档方法
  3. 2020.10.23--AI--小熊制作、太极制作、微信图标制作
  4. TensorFlow学习笔记之 bmp格式、txt格式数据转换成tfrecord 格式
  5. 复制加密内存卡(TF卡、U盘)资料的方法
  6. linux 怎么改系统字体,Linux如何更改字体
  7. 约瑟夫环问题(Josephus)
  8. EasyExcel使用
  9. 机器学习——决策树算法
  10. 【教程】Spire.Doc系列教程(3):C# Word查找和替换功能