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();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章