android开发-城市选择页面

  首先看一下效果动图:         



数据来源是公司的一个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();
    }
}

 


版权声明:本文为pgg_cold原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>