前些日子,女友在面试的过程中,面试官问:如何实现省市区三级联动效果?这里记录下相关实现效果的学习过程,先上效果图:
准备步骤1、:android-wheel控件
如图看出,实现的效果类似于Android中android.widget.DatePicker和android.widget.TimePicker,但在Android中没办法用系统中的widget,因为Google没有提供widget的对外数据源适配接口,因此,我们就需要自己来自定义view来实现。其实也不用担心,在Github上有一个开源控件,叫android-wheel,项目地址:https:///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<ProvinceModel> provinceList = new ArrayList<ProvinceModel>();
public XmlParserHandler() {
}
//提供方法返回存储所有解析对象的集合
public List<ProvinceModel> 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<CityModel>());
} else if (qName.equals("city")) {
cityModel = new CityModel();
cityModel.setName(attributes.getValue(0));
cityModel.setDistrictList(new ArrayList<DistrictModel>());
} 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)
通过XmlParserHandler.java提供的getDateList()方法,获取省市区(县)的数据,然后拆分存放到各自的HashMap集合(准备数据源,进行数据适配),这里获取数据源自定义AddressBaseActivity类中实现,方便Activity继承该类,获取数据源,代码如下:
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 .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<String, String[]> mCitisDatasMap = new HashMap<String, String[]>();
/**
* key - 市 values - 区
*/
protected Map<String, String[]> mDistrictDatasMap = new HashMap<String, String[]>();
/**
* key - 区 values - 邮编
*/
protected Map<String, String> mZipcodeDatasMap = new HashMap<String, String>();
/**
* 当前省的名称
*/
protected String mCurrentProviceName;
/**
* 当前市的名称
*/
protected String mCurrentCityName;
/**
* 当前区的名称
*/
protected String mCurrentDistrictName ="";
/**
* 当前区的邮政编码
*/
protected String mCurrentZipCode ="";
/**
* 解析省市区的XML数据
*/
protected void initProvinceDatas()
{
List<ProvinceModel> 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<CityModel> cityList = provinceList.get(0).getCityList();
if (cityList!= null && !cityList.isEmpty()) {
mCurrentCityName = cityList.get(0).getName(); //市
List<DistrictModel> 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<CityModel> 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<DistrictModel> districtList = cityList.get(j).getDistrictList();
String[] distrinctNameArray = new String[districtList.size()]; //初始化市下面的所有区/县的数组
DistrictModel[] distrinctArray = new DistrictModel[districtList.size()];
for (int k=0; k<districtList.size(); k++) {
// 遍历市下面所有区/县的数据
DistrictModel districtModel = new DistrictModel(districtList.get(k).getName(), districtList.get(k).getZipcode());
// 区/县对于的邮编,保存到mZipcodeDatasMap
mZipcodeDatasMap.put(districtList.get(k).getName(), districtList.get(k).getZipcode());
distrinctArray[k] = districtModel;
distrinctNameArray[k] = districtModel.getName();
}
// 市-区/县的数据,保存到mDistrictDatasMap
mDistrictDatasMap.put(cityNames[j], distrinctNameArray);
}
// 省-市的数据,保存到mCitisDatasMap
mCitisDatasMap.put(provinceList.get(i).getName(), cityNames);
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
}
}
}
实现步骤1:xml布局文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http:///apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="20dp"
android:background="#E9E9E9">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:background="#1ca0aa"
android:paddingTop="5dip" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="20dip"
android:text="请选择省市区"
android:textColor="#FFF"
android:textSize="20sp" />
</RelativeLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dip"
android:orientation="horizontal" >
<kankan.wheel.widget.WheelView
android:id="@+id/id_province"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" >
</kankan.wheel.widget.WheelView>
<kankan.wheel.widget.WheelView
android:id="@+id/id_city"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" >
</kankan.wheel.widget.WheelView>
<kankan.wheel.widget.WheelView
android:id="@+id/id_district"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" >
</kankan.wheel.widget.WheelView>
</LinearLayout>
<Button
android:id="@+id/btn_confirm"
android:layout_marginTop="10dip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="确 定"
android:textColor="#FFF"
android:background="#1ca0aa"/>
</LinearLayout>
实现步骤2:java类实现过程代码如下:
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(.id_province);
mViewCity = (WheelView) findViewById(.id_city);
mViewDistrict = (WheelView) findViewById(.id_district);
mBtnConfirm = (Button) findViewById(.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<String>(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<String>(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<String>(this, cities));
mViewCity.setCurrentItem(0);
updateAreas();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case .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 <T> the element type
*/
public class ArrayWheelAdapter<T> 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;
}
}
最终代码实现效果如下图: