先上图:
这是一个三级联动的选择城市控件(此处使用的是PopupWindow), 此效果最核心的就是CoreLibs中的ScrollerNumberPicker, 最难的部分则是数据分析. 如果明白了这个控件的原理, 那其他的比如三级不带联动, 二级不带联动(闹钟)等效果就不在话下了.
注: 由于是一个demo, 功能没有写全, 某些地方也不是很规范, 请谅解.
UI
三级联动式的城市选择一般以弹出窗的形式, 也有镶嵌在Activity/Fragment中的. 这里使用的PopupWindow, 当然也可以使用Dialog/View等. 原理都相通. 首先来看看xml布局文件:
Copy <?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中有哪些可以自定义的属性:
Copy <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 被选中的文字的大小
接着看看PopupWindow的部分代码:
Copy 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)
设置滑动监听
Copy 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文件夹中的. 其主要结构如下:
Copy {
"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解析, 如果会递归算法应该不是难题. 实体类的设计也是显而易见的:
Copy 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>
类型的数据, 因此我们还需要三个集合, 分别存放所有的省的名称, 当前选择的省对应的市的名称, 以及当前选择的市对应的区的名称.
Copy 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
属性, 获取下一级联动的数据.
Copy 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) {
}
});
Copy 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()方法告诉外部我们选择了什么地址:
Copy 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();
}
到此, 整个三级联动的骨架都实现了, 剩下需要根据需求自行去完善.