滑动选择

先上图:

这是一个三级联动的选择城市控件(此处使用的是PopupWindow), 此效果最核心的就是CoreLibs中的ScrollerNumberPicker, 最难的部分则是数据分析. 如果明白了这个控件的原理, 那其他的比如三级不带联动, 二级不带联动(闹钟)等效果就不在话下了.

注: 由于是一个demo, 功能没有写全, 某些地方也不是很规范, 请谅解.

UI

三级联动式的城市选择一般以弹出窗的形式, 也有镶嵌在Activity/Fragment中的. 这里使用的PopupWindow, 当然也可以使用Dialog/View等. 原理都相通. 首先来看看xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:su="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#33FFFFFF"
    android:gravity="bottom">

    <View
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="@color/main"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="#FFF"
        android:padding="10dp"
        android:orientation="horizontal">

        <com.corelibs.views.ScrollerNumberPicker
            android:id="@+id/first_picker"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="1dp"
            android:layout_marginRight="1dp"
            android:layout_weight="1"
            su:itemNumber="5"
            su:lineColor="@color/main"
            su:maskHight="40dp"
            su:noEmpty="true"
            su:normalTextColor="#777"
            su:normalTextSize="12sp"
            su:selecredTextColor="@color/main"
            su:selecredTextSize="13sp"
            su:unitHight="50dp" />

        <com.corelibs.views.ScrollerNumberPicker
            android:id="@+id/second_picker"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="1dp"
            android:layout_marginRight="1dp"
            android:layout_weight="1"
            su:itemNumber="5"
            su:lineColor="@color/main"
            su:maskHight="40dp"
            su:noEmpty="true"
            su:normalTextColor="#777"
            su:normalTextSize="12sp"
            su:selecredTextColor="@color/main"
            su:selecredTextSize="13sp"
            su:unitHight="50dp" />

        <com.corelibs.views.ScrollerNumberPicker
            android:id="@+id/third_picker"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="1dp"
            android:layout_marginRight="1dp"
            android:layout_weight="1"
            su:itemNumber="5"
            su:lineColor="@color/main"
            su:maskHight="40dp"
            su:noEmpty="true"
            su:normalTextColor="#777"
            su:normalTextSize="12sp"
            su:selecredTextColor="@color/main"
            su:selecredTextSize="13sp"
            su:unitHight="50dp" />

    </LinearLayout>
</LinearLayout>

布局的重点就是显示在底部的横向的LinearLayout, 该LinearLayout中包含了三个权重都是1的ScrollerNumberPicker. 这三个ScrollerNumberPicker就对应了示例图中的省市区三列. 接着看看ScrollerNumberPicker中有哪些可以自定义的属性:

<declare-styleable name="NumberPicker">
    <attr name="normalTextColor" format="color" />
    <attr name="normalTextSize" format="dimension" />
    <attr name="selectedTextColor" format="color" />
    <attr name="selectedTextSize" format="dimension" />
    <attr name="unitHeight" format="dimension" />
    <attr name="itemNumber" format="integer" />
    <attr name="lineColor" format="color" />
    <attr name="maskHeight" format="dimension" />
    <attr name="noEmpty" format="boolean" />
    <attr name="isEnable" format="boolean" />
</declare-styleable>
  • normalTextColor 未被选中的文字的颜色

  • normalTextSize 未被选中的文字的大小

  • selectedTextColor 被选中的文字的颜色

  • selectedTextSize 被选中的文字的大小

  • unitHeight 条目的高度

  • itemNumber 一页显示多少个条目

  • lineColor 分割线的颜色

  • maskHeight 遮罩的高度

  • noEmpty 是否不允许为空

  • isEnable 是否被激活

接着看看PopupWindow的部分代码:

public class CityPicker extends PopupWindow {
    private ScrollerNumberPicker one;
    private ScrollerNumberPicker two;
    private ScrollerNumberPicker three;

    public CityPicker(Context context) {
        init(context);
    }

    private void init(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.pop_city_picker, null);

        setContentView(view);
        setWidth(WindowManager.LayoutParams.MATCH_PARENT);
        setHeight(WindowManager.LayoutParams.MATCH_PARENT);

        setFocusable(true);
        setOutsideTouchable(true);
        setBackgroundDrawable(new BitmapDrawable(null, (Bitmap) null));

        one = (ScrollerNumberPicker) view.findViewById(R.id.first_picker);
        two = (ScrollerNumberPicker) view.findViewById(R.id.second_picker);
        three = (ScrollerNumberPicker) view.findViewById(R.id.third_picker);
    }

    public void showAtBottom(View view) {
        showAtLocation(view, Gravity.BOTTOM, 0, 0);
    }
}

此时的PopupWindow更多的是承载一些显示的工作, 如果对PopupWindow不是很熟悉的可以参考 弹窗. 到此整个UI效果就出来了. 但是还缺少核心数据这一块.

数据

这一部分就要考验大家的数据分析能力了. 我不准备讲的很详细, 毕竟虽然数据是三级联动的重点, 但不是此章的重点, 此章的重点是告诉大家ScrollerNumberPicker的用法.

首先我们看看如何为ScrollerNumberPicker设置数据. ScrollerNumberPicker最常用的公共方法有:

  • void setData(ArrayList<String> data) 设置数据, 只接受ArrayList<String>类型的数据

  • int getSelected() 获取当前控件中, 被选中的项的下标

  • String getSelectedText() 获取当前控件中, 被选中的项的内容

  • void setDefault(int index) 设置当数据加载完成后默认选中第几条数据

  • void setOnSelectListener(OnSelectListener onSelectListener) 设置滑动监听

public interface OnSelectListener {
    /** 当滑动结束时回调 */
    public void endSelect(int id, String text);

    /** 当滑动的时候回调 */
    public void selecting(int id, String text);
}

我们只需要调用void setData(ArrayList<String> data)为ScrollerNumberPicker设置数据即可. 同时也可以借助OnSelectListener的endSelect回调, 来实现联动. 比如当监听到用户选择了省, ScrollerNumberPicker停止滑动了, 我们就可以在endSelect里去设置市的数据.

然后分析一下, 要实现城市三级联动最重要的是什么? 是城市的数据. 后面的代码全部是围绕城市的数据来的. 本例的城市数据是以json形式放到assets文件夹中的. 其主要结构如下:

{
    "leverArea": [
        {
            "first": "hbs",
            "full": "hubeisheng",
            "fullName": "湖北省",
            "id": 1692,
            "isHot": 0,
            "isShow": null,
            "name": "湖北省",
            "nextArea": [
                {
                    "first": "whs",
                    "full": "wuhanshi",
                    "fullName": "湖北省武汉市",
                    "id": 1693,
                    "isHot": 1,
                    "isShow": null,
                    "name": "武汉市",
                    "nextArea": [
                        {
                            "first": "jaq",
                            "full": "jianganqu",
                            "fullName": "湖北省武汉市江岸区",
                            "id": 1694,
                            "isHot": 0,
                            "isShow": null,
                            "name": "江岸区",
                            "nextArea": null,
                            "parent": 1693
                        }, ......
                    ]
                }, ......
            ]
        }, ......
    ]
}

注: json带有省略号......的部分意味着还有不确定个数的Object, 结构与省略号前的Object相同. 整个json中共有三处省略.

分析json数据可以得到, 整个json是由一个叫leverArea的数组构成, leverArea中的每个元素, 又包含了一个名为nextArea的数组, nextArea中的每个元素的结构与leverArea中的元素的结构绝大部分一样, 同样, nextArea中的元素也包含了一个nextArea数组. 这是一个典型的递归结构, 可以无限延伸下去. 但是此json只有三层递归, 分别代表了省市区三层数据.

数据解析可以手动解析也可以Gson解析, 如果会递归算法应该不是难题. 实体类的设计也是显而易见的:

public static class TCity implements Cloneable {
    public long id;
    public String first;
    public String fullName;
    public String full;
    public String name;
    public List<TCity> nextArea;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

TCity实现了Cloneable接口, 方便克隆, 避免为nextArea赋值时引用的是同一个指针而产生意外的后果. 很多时候源数据都不是很符合我们的要求. 比如源数据中直辖市都只有两层, 不像省都是有三层的, 这个时候就需要我们自己处理, 为直辖市再加上一层数据. 具体算法就不贴出来了, 因为使用条件很特殊, 几乎没法复用.

解析完数据之后我们就得到了一个List<TCity>类型的集合, 这个集合还不能直接使用.

另一个待解决的问题就是, 如何做到选了省之后, 显示与之对应的市, 选好市, 显示与之对应区. 关键就在于对List<TCity>的操作. 我们需要将List<TCity>拆分成三个集合, 分别存放所有的省数据, 当前选择的省对应的市的数据, 以及当前选择的市对应的区的数据. 又因为ScrollerNumberPicker只接受ArrayList<String>类型的数据, 因此我们还需要三个集合, 分别存放所有的省的名称, 当前选择的省对应的市的名称, 以及当前选择的市对应的区的名称.

private List<TCity> tProvinces;
private List<TCity> tCities;
private List<TCity> tAreas;

private ArrayList<String> provinces = new ArrayList<>();
private ArrayList<String> cities = new ArrayList<>();
private ArrayList<String> areas = new ArrayList<>();

接下来, 借助OnSelectListener来获取被选中的项目的下标, 然后通过下标在List<TCity>中查找其nextArea属性, 获取下一级联动的数据.

        one.setOnSelectListener(new ScrollerNumberPicker.OnSelectListener() {
            @Override
            public void selecting(int id, String text) {
            }

            @Override
            public void endSelect(int id, String text) {
                initCities(id);
                two.setData(cities);
                two.setDefault(0);

                initAreas(0);
                three.setData(areas);
                three.setDefault(0);
            }
        });

        two.setOnSelectListener(new ScrollerNumberPicker.OnSelectListener() {
            @Override
            public void selecting(int id, String text) {
            }

            @Override
            public void endSelect(int id, String text) {
                initAreas(id);
                three.setData(areas);
                three.setDefault(0);
            }
        });

        three.setOnSelectListener(new ScrollerNumberPicker.OnSelectListener() {
            @Override
            public void selecting(int id, String text) {
            }

            @Override
            public void endSelect(int id, String text) {
            }
        });
    private void initProvinces() {
        provinces.clear();
        for (TCity city : tProvinces)
            provinces.add(city.name);
    }

    private void initCities(int position) {
        cities.clear();
        TCity province = tProvinces.get(position);
        tCities = province.nextArea;
        for (TCity city : tCities)
            cities.add(city.name);
    }

    private void initAreas(int position) {
        areas.clear();
        TCity city = tCities.get(position);
        tAreas = city.nextArea;
        for (TCity tmp : tAreas) {
            areas.add(tmp.name);
        }
    }

最后, 我们可以借助getSelectedText()方法告诉外部我们选择了什么地址:

    public String getFirstSelected() {
        selected = one.getSelectedText();
        return selected;
    }

    public String getSecondSelected() {
        if (two.getData().size() < 2)
            selected = "";
        else
            selected = two.getSelectedText();
        return selected;
    }

    public String getThirdSelected() {
        selected = three.getSelectedText();
        return selected;
    }

    public String getSelectedAddress() {
        return getFirstSelected() + getSecondSelected() + getThirdSelected();
    }

到此, 整个三级联动的骨架都实现了, 剩下需要根据需求自行去完善.

Last updated