android利用RecyclerView+自定义View实现城市选择界面

1,707 阅读8分钟

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…