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…