1、設計的效果圖
2、分析
根據產品經理的要求:
1、城市選擇介面直接選擇二級城市;
2、不像一般的城市列表一行一個,按照設計圖所示按首字母分組;
3、右側有快速首字母索引。
拿到需求的第一反應,選擇介面採用RecyclerView巢狀GridView來實現,右側快速索引採用自定義view來實現。
資料來源服務端同學已經包裝好,json格式字串為{"datas":{"A":[{"id":"21012","name":"安陽市"},...],"B":[...]},...}
3、實現
首先來實現右側的快速索引,自定義view重寫onDraw方法,新增觸控監聽,並新增觸控監聽回撥,新增可配置化的選中TextView顯示
/**
* 右側的字母索引View
*/
public class SideBar extends View {
public String[] INDEX_STRING = {"A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"};
private OnTouchingLetterChangedListener onTouchingLetterChangedListener;
private List<String> letterList;
private int choose = -1;
private Paint paint = new Paint();
private TextView mTextDialog;
public SideBar(Context context) {
this(context, null);
}
public SideBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
//根據INDEX_STRING生成字母list
letterList = Arrays.asList(INDEX_STRING);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();// 獲取對應高度
int width = getWidth();// 獲取對應寬度
int singleHeight = height / letterList.size();// 獲取每一個字母的高度
for (int i = 0; i < letterList.size(); i++) {
paint.setColor(Color.parseColor("#a9a9a9"));
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(22);
// 選中的狀態
if (i == choose) {
paint.setColor(Color.parseColor("#05c0ab"));
paint.setFakeBoldText(true);
}
// x座標等於中間-字串寬度的一半.
float xPos = width / 2 - paint.measureText(letterList.get(i)) / 2;
float yPos = singleHeight * i + singleHeight / 2;
canvas.drawText(letterList.get(i), xPos, yPos, paint);
paint.reset();// 重置畫筆
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getAction();
final float y = event.getY();// 點選y座標
final int oldChoose = choose;
final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;
final int c = (int) (y / getHeight() * letterList.size());// 點選y座標所佔總高度的比例*b陣列的長度就等於點選b中的個數.
switch (action) {
case MotionEvent.ACTION_UP:
setBackgroundResource(R.color.alpha);
choose = -1;
invalidate();
if (mTextDialog != null) {
mTextDialog.setVisibility(View.GONE);
}
if (c >= 0 && c < letterList.size()) {
if (listener != null) {
listener.onTouchingLetterChanged(letterList.get(c));
}
}
break;
default:
setBackgroundResource(R.color.background); if (oldChoose != c) {
if (c >= 0 && c < letterList.size()) {
if (mTextDialog != null) {
mTextDialog.setText(letterList.get(c));
mTextDialog.setVisibility(View.VISIBLE);
}
choose = c;
invalidate();
if (listener != null) {
listener.onTouchingLetterChanging(letterList.get(c));
}
}
}
break;
}
return true;
}
public void setIndexText(ArrayList<String> indexStrings) {
this.letterList = indexStrings;
invalidate();
}
/**
* 為SideBar設定顯示當前按下的字母的TextView
*
* @param mTextDialog
*/
public void setTextView(TextView mTextDialog) {
this.mTextDialog = mTextDialog;
}
/**
* 向外公開的方法
*
* @param onTouchingLetterChangedListener
*/
public void setOnTouchingLetterChangedListener(
OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
}
public void refresh() {
init();
invalidate();
}
/**
* 介面
*/
public interface OnTouchingLetterChangedListener {
void onTouchingLetterChanged(String s);
void onTouchingLetterChanging(String s);
}
}複製程式碼
在佈局檔案中引入
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<RelativeLayout android:id="@+id/main_title"
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@color/main_color"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_vertical"
android:layout_alignParentTop="true">
<ImageView android:id="@+id/imageView_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:src="@drawable/back_white"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_marginRight="10dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="選擇城市"
android:textColor="@color/white"
android:textSize="18sp"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:background="@color/background"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_citys"
android:layout_width="match_parent"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
<com.gui.ggtest.view.SideBar
android:id="@+id/sidebar_index"
android:layout_width="15dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="30dp"
android:layout_marginTop="30dp" />
<TextView
android:id="@+id/tv_index"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:background="@color/alpha"
android:gravity="center"
android:textColor="@color/main_color"
android:textSize="24sp"
android:textStyle="bold"
android:visibility="visible" />
</RelativeLayout>
</LinearLayout>複製程式碼
跑起來效果圖如下
下面該攻克左側城市列表了,在實際實現的過程中,發現如果使用RecyclerView+GridView的方式或者RecyclerView+RecyclerView的方式,都會出現城市列表滑動卡頓的問題,經分析,是RecyclerView在回收利用的時候動態建立新的子View,如果子View也是一個動態重新整理的控制元件,整個列表在滑動的時候沒有那種絲般的順滑(ε=ε=ε=┏(゜ロ゜;)┛所以,決定直接採用一個RecyclerView的方式來實現,採用GridLayoutManager的setSpanSizeLookup來控制多種佈局
關鍵程式碼如下
public class ChooseCityActivity extends Activity{
//樣例資料
public String data = "{\"datas\":{\"A\":[{\"id\":\"21012\",\"name\":\"安陽市\"},{\"id\":\"21308\",\"name\":\"安慶市\"},{\"id\":\"21623\",\"name\":\"阿壩\"},{\"id\":\"21803\",\"name\":\"鞍山市\"},{\"id\":\"21913\",\"name\":\"阿拉善盟\"},{\"id\":\"22013\",\"name\":\"安康市\"},{\"id\":\"22304\",\"name\":\"安順市\"},{\"id\":\"22907\",\"name\":\"阿勒泰\"},{\"id\":\"22911\",\"name\":\"阿克蘇\"},{\"id\":\"23002\",\"name\":\"阿里地區\"}],\"B\":[{\"id\":\"20311\",\"name\":\"寶山區\"},{\"id\":\"20833\",\"name\":\"濱州市\"},{\"id\":\"20909\",\"name\":\"保定市\"},{\"id\":\"21305\",\"name\":\"蚌埠市\"},{\"id\":\"21616\",\"name\":\"巴中市\"},{\"id\":\"21812\",\"name\":\"本溪市\"},{\"id\":\"21903\",\"name\":\"包頭市\"},{\"id\":\"21916\",\"name\":\"巴彥淖爾\"},{\"id\":\"22004\",\"name\":\"寶雞市\"},{\"id\":\"22105\",\"name\":\"保山市\"},{\"id\":\"22204\",\"name\":\"北海市\"},{\"id\":\"22212\",\"name\":\"百色市\"},{\"id\":\"22308\",\"name\":\"畢節地區\"},{\"id\":\"22507\",\"name\":\"白山市\"},{\"id\":\"22513\",\"name\":\"白城市\"},{\"id\":\"22607\",\"name\":\"白銀市\"},{\"id\":\"22710\",\"name\":\"白沙\"},{\"id\":\"22714\",\"name\":\"保亭\"},{\"id\":\"22909\",\"name\":\"博爾塔拉\"},{\"id\":\"22910\",\"name\":\"巴音郭楞\"}],\"C\":[{\"id\":\"10400\",\"name\":\"重慶市\"},{\"id\":\"20103\",\"name\":\"崇文區\"},{\"id\":\"20105\",\"name\":\"朝陽區\"},{\"id\":\"20113\",\"name\":\"昌平區\"},{\"id\":\"20304\",\"name\":\"長寧區\"},{\"id\":\"20319\",\"name\":\"崇明縣\"},{\"id\":\"20513\",\"name\":\"潮州市\"},{\"id\":\"20613\",\"name\":\"常州市\"},{\"id\":\"20905\",\"name\":\"承德市\"},{\"id\":\"20914\",\"name\":\"滄州市\"},{\"id\":\"21201\",\"name\":\"長沙市\"},{\"id\":\"21210\",\"name\":\"常德市\"},{\"id\":\"21213\",\"name\":\"郴州市\"},{\"id\":\"21309\",\"name\":\"池州市\"},{\"id\":\"21316\",\"name\":\"滁州市\"},{\"id\":\"21601\",\"name\":\"成都市\"},{\"id\":\"21706\",\"name\":\"長治市\"},{\"id\":\"21816\",\"name\":\"朝陽市\"},{\"id\":\"21904\",\"name\":\"赤峰市\"},{\"id\":\"22110\",\"name\":\"楚雄\"},{\"id\":\"22215\",\"name\":\"崇左市\"},{\"id\":\"22501\",\"name\":\"長春市\"},{\"id\":\"22708\",\"name\":\"澄邁縣\"},{\"id\":\"22711\",\"name\":\"昌江\"},{\"id\":\"22908\",\"name\":\"昌吉\"},{\"id\":\"23004\",\"name\":\"昌都地區\"}],\"D\":[{\"id\":\"20101\",\"name\":\"東城區\"},{\"id\":\"20114\",\"name\":\"大興區\"},{\"id\":\"20504\",\"name\":\"東莞市\"},{\"id\":\"20810\",\"name\":\"東營市\"},{\"id\":\"20812\",\"name\":\"德州市\"},{\"id\":\"21029\",\"name\":\"鄧州市\"},{\"id\":\"21609\",\"name\":\"德陽市\"},{\"id\":\"21621\",\"name\":\"達州市\"},{\"id\":\"21703\",\"name\":\"大同市\"},{\"id\":\"21802\",\"name\":\"大連市\"},{\"id\":\"21808\",\"name\":\"丹東市\"},{\"id\":\"22102\",\"name\":\"迪慶\"},{\"id\":\"22114\",\"name\":\"大理\"},{\"id\":\"22115\",\"name\":\"德巨集\"},{\"id\":\"22402\",\"name\":\"大慶市\"},{\"id\":\"22414\",\"name\":\"大興安嶺\"},{\"id\":\"22609\",\"name\":\"定西市\"},{\"id\":\"22705\",\"name\":\"東方市\"},{\"id\":\"22706\",\"name\":\"定安縣\"},{\"id\":\"22725\",\"name\":\"儋州市\"}],\"E\":[{\"id\":\"21105\",\"name\":\"鄂州市\"},{\"id\":\"21114\",\"name\":\"恩施\"},{\"id\":\"21902\",\"name\":\"鄂爾多斯\"}],\"F\":[{\"id\":\"20106\",\"name\":\"豐臺區\"},{\"id\":\"20110\",\"name\":\"房山區\"},{\"id\":\"20318\",\"name\":\"奉賢區\"},{\"id\":\"20503\",\"name\":\"佛山市\"},{\"id\":\"21306\",\"name\":\"阜陽市\"},{\"id\":\"21401\",\"name\":\"福州市\"},{\"id\":\"21508\",\"name\":\"撫州市\"},{\"id\":\"21813\",\"name\":\"撫順市\"},{\"id\":\"21815\",\"name\":\"阜新市\"},{\"id\":\"22206\",\"name\":\"防城港\"}],\"G\":[{\"id\":\"20501\",\"name\":\"廣州市\"},{\"id\":\"21502\",\"name\":\"贛州市\"},{\"id\":\"21619\",\"name\":\"廣元市\"}]}}";
ImageView back;
RecyclerView recycler_citys;
CityChooseRecyclerAdapter cityChooseRecyclerAdapter;
TextView tv_index;
SideBar sidebar_index;
Map<String, Object> datas;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_citychoose);
initView();
initData();
addListener();
}
private void addListener() {
back.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
ChooseCityActivity.this.finish();
}
});
}
private void initData() {
//實際專案中資料從網路獲取,這邊寫死資料
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>()
{
}.getType();
Map<String, Object> citysMap = gson.fromJson(data,type);
datas = (Map<String, Object>) citysMap.get("datas");
//初始化recyclerview
initRecyclerView();
//初始化sidebar
initSideBar();
}
private void initRecyclerView() {
cityChooseRecyclerAdapter= new CityChooseRecyclerAdapter(ChooseCityActivity.this,datas);
//定義佈局管理
final GridLayoutManager linearLayoutManager = new GridLayoutManager(ChooseCityActivity.this,3);
//佈局分類的關鍵方法
linearLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int result = cityChooseRecyclerAdapter.getItemViewType(position)== CityChooseRecyclerAdapter.ITEM_HEAD? 3 : 1;
return result;
}
});
recycler_citys.setLayoutManager(linearLayoutManager);
recycler_citys.setAdapter(cityChooseRecyclerAdapter);
}
private void initSideBar() {
//根據資料裡的列表控制右側導航欄
final List<String> indexList = new ArrayList(datas.keySet());
String[] strings = new String[indexList.size()];
sidebar_index.INDEX_STRING = indexList.toArray(strings);
sidebar_index.refresh();
sidebar_index.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(String s) {
//滑動結束監聽
// recycler_citys.smoothScrollToPosition(index);
}
@Override
public void onTouchingLetterChanging(String s) {
//滑動過程中監聽
int index = cityChooseRecyclerAdapter.getIndexPosition(s);
recycler_citys.scrollToPosition(index);
}
});
}
private void initView() {
sidebar_index = (SideBar) findViewById(R.id.sidebar_index);
tv_index = (TextView) findViewById(R.id.tv_index);
recycler_citys = (RecyclerView) findViewById(R.id.recycler_citys);
sidebar_index.setTextView(tv_index);
back = (ImageView) findViewById(R.id.imageView_back);
}
public void selectCity(String cityId, String cityName) {
if (!TextUtils.isEmpty(cityName)&&!TextUtils.isEmpty(cityId)){
Toast.makeText(ChooseCityActivity.this,"cityId="+cityId+"--cityName="+cityName,Toast.LENGTH_SHORT).show();
}
}
}複製程式碼
public class CityChooseRecyclerAdapter extends RecyclerView.Adapter{
public static final int ITEM_HEAD = 0;
public static final int ITEM_CONTENT = 1;
private LayoutInflater mInflater;
public Context mContext;
public List<Map<String,String>> dataList = new ArrayList<>();
public List<Integer> numList = new ArrayList<>();
public List<String> indexList = new ArrayList<>();
public CityChooseRecyclerAdapter(Context context, Map<String, Object> datas){
mInflater = LayoutInflater.from(context);
mContext = context;
List<String> indexList = new ArrayList(datas.keySet());
for (String string : indexList) {
numList.add(dataList.size());
Map<String,String> headMap = new HashMap<>();
headMap.put("type","head");
headMap.put("name",string);
dataList.add(headMap);
List<Map<String, String>> templist = (List<Map<String, String>>) datas.get(string);
dataList.addAll(templist);
}
this.indexList = indexList;
}
@Override
public int getItemViewType(int position) {
Map<String,String> item = dataList.get(position);
if (TextUtils.isEmpty(item.get("type"))){
return ITEM_CONTENT;
}else if ("head".equals(item.get("type"))){
return ITEM_HEAD;
}else{
return ITEM_CONTENT;
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder = null;
switch (viewType){
case ITEM_HEAD:
viewHolder = new CityChooseGridRViewHolder(mInflater.inflate(R.layout.item_grid_title, null,false));
break;
case ITEM_CONTENT:
viewHolder = new CityChooseGridRViewHolder(mInflater.inflate(R.layout.item_grid_textvie, null,false));
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
CityChooseGridRViewHolder cityChooseViewHolder = (CityChooseGridRViewHolder)holder;
final Map<String,String> item = dataList.get(position);
cityChooseViewHolder.tv_name.setText(item.get("name"));
cityChooseViewHolder.tv_name.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (getItemViewType(position)==ITEM_CONTENT){
((ChooseCityActivity)mContext).selectCity(item.get("id"),item.get("name"));
}
}
});
}
@Override
public int getItemCount() {
return dataList.size();
}
private class CityChooseGridRViewHolder extends RecyclerView.ViewHolder
{
TextView tv_name;
public CityChooseGridRViewHolder(View itemView) {
super(itemView);
tv_name = (TextView) itemView.findViewById(R.id.tv_name);
}
}
public int getIndexPosition(String s){
try{
return numList.get(indexList.indexOf(s));
}catch (Exception e){
return 0;
}
}
}複製程式碼
最終實現效果
定位不在本篇討論範圍之內,用的是百度定位,接入起來也很簡單,暫且不表
ε=ε=ε=┏(゜ロ゜;)┛
4、總結
實現該頁面的難點主要就兩個,android不像ios那樣自帶索引view,需要自己實現,再一個就是這個分組的城市列表坑了一下,不像我們常見的
這種形式
因此在實現的時候要特別注意一些細節,比如首字母分組,這邊是伺服器幫忙做了,比如RecyclerView巢狀的效能,我就在這個坑裡爬了一上午。。嘗試了RecyclerView+GridView和RecyclerView+RecyclerView的模式,都是卡頓,最後直接採用單個RecyclerView的模式,整個世界都清靜了。
好了,我要繼續寫程式碼了 ε=ε=ε=┏(゜ロ゜;)┛
程式碼git:github.com/gh8623/GGTe…