在开篇之前,首先,这篇博客只为分享记录自己的学习过程,这里要感谢技术分享的博主,大家可以查看原博客地址:点击打开链接
言回正传,前些日子,女友在面试的过程中,面试官问:如何实现省市区三级联动效果?这里记录下相关实现效果的学习过程,先上效果图:
准备步骤1、:android-wheel控件
如图看出,实现的效果类似于Android中android.widget.DatePicker和android.widget.TimePicker,但在Android中没办法用系统中的widget,因为Google没有提供widget的对外数据源适配接口,因此,我们就需要自己来自定义view来实现。其实也不用担心,在Github上有一个开源控件,叫android-wheel,项目地址:https://github.com/maarek/android-wheel,该组件对数据适配接口的抽取和事件的回调做了抽取,下载放入到自己的项目src目录下:如图 kankan.wheel.widget目录(这里对界面的代码做了改动)
准备步骤2:解析省市区(县)的XML文件
省市区(县)及邮编的数据,放在项目中的assets目录下:province_data.xml文件,因此需要对XML文件的进行解析,获取数据。(有关于XML文件的解析教程,请找度娘)
这里采用的是SAX解析方式,XML文件数据解析工具类代码如下:
package com.ctlive.framepackage.service;
import com.ctlive.framepackage.bean.model.CityModel;
import com.ctlive.framepackage.bean.model.DistrictModel;
import com.ctlive.framepackage.bean.model.ProvinceModel;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList;
import java.util.List;
/**
* 解析XML文件业务处理类——采用方式是SAX解析方式(SAX解析占用内存少,并且速度快)
* 注:XML文件解析方式三种:1、SAX解析;2、PULL解析;3、DOM解析
*/
public class XmlParserHandler extends DefaultHandler {
/**
* 存储所有的解析对象
*/
private List provinceList = new ArrayList();
public XmlParserHandler() {
}
//提供方法返回存储所有解析对象的集合
public List getDataList() {
return provinceList;
}
@Override
public void startDocument() throws SAXException {
// 当读到第一个开始标签的时候,会触发这个方法
}
//创建解析实体类对象
ProvinceModel provinceModel;
CityModel cityModel;
DistrictModel districtModel;
@Override
public void startElement(String uri, String localName, String qName,Attributes attributes) throws SAXException {
// 当遇到开始标记的时候,调用这个方法
if (qName.equals("province")) {
provinceModel = new ProvinceModel();
provinceModel.setName(attributes.getValue(0));
provinceModel.setCityList(new ArrayList());
} else if (qName.equals("city")) {
cityModel = new CityModel();
cityModel.setName(attributes.getValue(0));
cityModel.setDistrictList(new ArrayList());
} else if (qName.equals("district")) {
districtModel = new DistrictModel();
districtModel.setName(attributes.getValue(0));
districtModel.setZipcode(attributes.getValue(1));
}
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
// 遇到结束标记的时候,会调用这个方法
if (qName.equals("district")) {
cityModel.getDistrictList().add(districtModel);
} else if (qName.equals("city")) {
provinceModel.getCityList().add(cityModel);
} else if (qName.equals("province")) {
provinceList.add(provinceModel);
}
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
}
}
准备步骤3:获取省市区(县)的数据源(数据存入到HashMap)
package com.ctlive.framepackage.base;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import android.app.Activity;
import android.content.res.AssetManager;
import com.ctlive.framepackage.bean.model.CityModel;
import com.ctlive.framepackage.bean.model.DistrictModel;
import com.ctlive.framepackage.bean.model.ProvinceModel;
import com.ctlive.framepackage.service.XmlParserHandler;
/**
* 初始化省市区的数据源
* 解析省市区的Xml数据,并进行拆分储存到省、市、区的HashMap集合中业务处理类
*/
public class AddressBaseActivity extends Activity {
/**
* 存储所有省的数组
*/
protected String[] mProvinceDatas;
/**
* key - 省 value - 市
*/
protected Map mCitisDatasMap = new HashMap();
/**
* key - 市 values - 区
*/
protected Map mDistrictDatasMap = new HashMap();
/**
* key - 区 values - 邮编
*/
protected Map mZipcodeDatasMap = new HashMap();
/**
* 当前省的名称
*/
protected String mCurrentProviceName;
/**
* 当前市的名称
*/
protected String mCurrentCityName;
/**
* 当前区的名称
*/
protected String mCurrentDistrictName ="";
/**
* 当前区的邮政编码
*/
protected String mCurrentZipCode ="";
/**
* 解析省市区的XML数据
*/
protected void initProvinceDatas()
{
List provinceList = null;
AssetManager asset = getAssets();
try {
InputStream input = asset.open("province_data.xml");
// 创建一个解析xml的工厂对象
SAXParserFactory spf = SAXParserFactory.newInstance();
// 解析xml
SAXParser parser = spf.newSAXParser();
XmlParserHandler handler = new XmlParserHandler();
parser.parse(input, handler);
input.close();
// 获取解析出来的数据
provinceList = handler.getDataList();
// 初始化默认选中的省、市、区
if (provinceList!= null && !provinceList.isEmpty()) {
mCurrentProviceName = provinceList.get(0).getName(); //省
List cityList = provinceList.get(0).getCityList();
if (cityList!= null && !cityList.isEmpty()) {
mCurrentCityName = cityList.get(0).getName(); //市
List districtList = cityList.get(0).getDistrictList();
mCurrentDistrictName = districtList.get(0).getName(); //区
mCurrentZipCode = districtList.get(0).getZipcode(); //邮编
}
}
//进行拆分储存到省、市、区的HashMap集合
mProvinceDatas = new String[provinceList.size()]; //初始化所有省的数组
for (int i=0; i< provinceList.size(); i++) {
// 遍历所有省的数据,并存入到数组
mProvinceDatas[i] = provinceList.get(i).getName();
List cityList = provinceList.get(i).getCityList();
String[] cityNames = new String[cityList.size()]; //初始化省下面的所有市的数组
for (int j=0; j< cityList.size(); j++) {
// 遍历省下面的所有市的数据,并存入到的数组
cityNames[j] = cityList.get(j).getName();
List districtList = cityList.get(j).getDistrictList();
String[] distrinctNameArray = new String[districtList.size()]; //初始化市下面的所有区/县的数组
DistrictModel[] distrinctArray = new DistrictModel[districtList.size()];
for (int k=0; k
package com.ctlive.framepackage;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.ctlive.framepackage.base.AddressBaseActivity;
import kankan.wheel.widget.OnWheelChangedListener;
import kankan.wheel.widget.WheelView;
import kankan.wheel.widget.adapters.ArrayWheelAdapter;
/**
* Created by CTlive on 2016/7/25.
*/
public class SetAddressWheelActivity extends AddressBaseActivity implements View.OnClickListener,OnWheelChangedListener {
private WheelView mViewProvince; // 省
private WheelView mViewCity; // 市
private WheelView mViewDistrict; // 区/县
private Button mBtnConfirm; //确定按钮
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_set_address_wheel);
setUpViews();
setUpListener();
setUpData();
}
private void setUpViews() {
mViewProvince = (WheelView) findViewById(R.id.id_province);
mViewCity = (WheelView) findViewById(R.id.id_city);
mViewDistrict = (WheelView) findViewById(R.id.id_district);
mBtnConfirm = (Button) findViewById(R.id.btn_confirm);
}
private void setUpListener() {
// 添加change事件
mViewProvince.addChangingListener(SetAddressWheelActivity.this);
// 添加change事件
mViewCity.addChangingListener(SetAddressWheelActivity.this);
// 添加change事件
mViewDistrict.addChangingListener(SetAddressWheelActivity.this);
// 添加onclick事件
mBtnConfirm.setOnClickListener(SetAddressWheelActivity.this);
}
//在wheel组件中进行数据显示的配置和数据显示数量的配置,在这里设置一行可见显示7条数据
private void setUpData() {
initProvinceDatas();
mViewProvince.setViewAdapter(new ArrayWheelAdapter(SetAddressWheelActivity.this, mProvinceDatas));
// 设置可见条目数量
mViewProvince.setVisibleItems(7);
mViewCity.setVisibleItems(7);
mViewDistrict.setVisibleItems(7);
updateCities();
updateAreas();
}
/**
* 该回调方法中,对于省、市、区/县的滑动,分别做数据的适配
* @param wheel the wheel view whose state has changed
* @param oldValue the old value of current item
* @param newValue the new value of current item
*
* 在onChanged方法中加上代码:
* mCurrentDistrictName = mDistrictDatasMap.get(mCurrentCityName)[0];
* mCurrentZipCode = mZipcodeDatasMap.get(mCurrentDistrictName);
* 解决如果不滑动区,只滑动省或市的话,返回选中的区(邮政编码)始终就是上次选择的区(邮政编码)的问题
*/
@Override
public void onChanged(WheelView wheel, int oldValue, int newValue) {
// TODO Auto-generated method stub
mCurrentDistrictName = mDistrictDatasMap.get(mCurrentCityName)[0]; //未滑动选择时,区默认为第一项区的数据
mCurrentZipCode = mZipcodeDatasMap.get(mCurrentDistrictName); //区未滑动选择时,邮编默认为第一项区的数据的邮编
if (wheel == mViewProvince) {
updateCities();
mCurrentDistrictName = mDistrictDatasMap.get(mCurrentCityName)[0];
mCurrentZipCode = mZipcodeDatasMap.get(mCurrentDistrictName);
} else if (wheel == mViewCity) {
updateAreas();
mCurrentDistrictName = mDistrictDatasMap.get(mCurrentCityName)[0];
mCurrentZipCode = mZipcodeDatasMap.get(mCurrentDistrictName);
} else if (wheel == mViewDistrict) {
mCurrentDistrictName = mDistrictDatasMap.get(mCurrentCityName)[newValue];
mCurrentZipCode = mZipcodeDatasMap.get(mCurrentDistrictName);
}
}
/**
* 根据当前的市,更新区WheelView的信息
*/
private void updateAreas() {
int pCurrent = mViewCity.getCurrentItem();
mCurrentCityName = mCitisDatasMap.get(mCurrentProviceName)[pCurrent];
String[] areas = mDistrictDatasMap.get(mCurrentCityName);
if (areas == null) {
areas = new String[] { "" };
}
mViewDistrict.setViewAdapter(new ArrayWheelAdapter(this, areas));
mViewDistrict.setCurrentItem(0);
}
/**
* 根据当前的省,更新市WheelView的信息
*/
private void updateCities() {
int pCurrent = mViewProvince.getCurrentItem();
mCurrentProviceName = mProvinceDatas[pCurrent];
String[] cities = mCitisDatasMap.get(mCurrentProviceName);
if (cities == null) {
cities = new String[] { "" };
}
mViewCity.setViewAdapter(new ArrayWheelAdapter(this, cities));
mViewCity.setCurrentItem(0);
updateAreas();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_confirm:
showSelectedResult();
break;
default:
break;
}
}
private void showSelectedResult() {
Toast.makeText(SetAddressWheelActivity.this, "当前选中:" + mCurrentProviceName + "," + mCurrentCityName + ","
+ mCurrentDistrictName + "," + mCurrentZipCode, Toast.LENGTH_SHORT).show();
}
}
补充:
一、监听wheel组件的滑动、点击、选中事件,通过实现组件的三个事件监听接口实现,分别是:
1、OnWheelScrollListener 滑动事件;
2、OnWheelChangedListener 选中项的position变化事件
3、OnWheelClickedListener 条目点击事件
要知道哪个省、市、区被选中了,实现第二个接口就行,在方法回调时去作同步和更新数据,比如省级条目滑动的时候,市级和县级数据都要做对应的适配、市级滑动时需要去改变县级(区)的数据,这样才能实现级联的效果,至于如何改变,需要三个HashMap来分别保存他们的对应关系:
二、设置wheel组件中字体颜色、字体大小等信息,在如图的ArraywheelAdapter适配器中进行:
代码如下:
/*
* Copyright 2011 Yuri Kanivets
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kankan.wheel.widget.adapters;
import android.content.Context;
import android.graphics.Color;
/**
* The simple Array wheel adapter
* @param the element type
*/
public class ArrayWheelAdapter extends AbstractWheelTextAdapter {
// items
private T items[];
/**
* Constructor
* @param context the current context
* @param items the items
*/
public ArrayWheelAdapter(Context context, T items[]) {
super(context);
setTextColor(Color.BLACK); //设置字体颜色
// setTextSize(20); //设置字体大小
// setEmptyItemResource(TEXT_VIEW_ITEM_RESOURCE); //设置空的item资源
this.items = items;
}
@Override
public CharSequence getItemText(int index) {
if (index >= 0 && index < items.length) {
T item = items[index];
if (item instanceof CharSequence) {
return (CharSequence) item;
}
return item.toString();
}
return null;
}
@Override
public int getItemsCount() {
return items.length;
}
}
最终代码实现效果如下图: