Android_实现省市区(县)三级联动效果

在开篇之前,首先,这篇博客只为分享记录自己的学习过程,这里要感谢技术分享的博主,大家可以查看原博客地址:点击打开链接

言回正传,前些日子,女友在面试的过程中,面试官问:如何实现省市区三级联动效果?这里记录下相关实现效果的学习过程,先上效果图:


准备步骤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<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 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<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://schemas.android.com/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(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<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 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 <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;
    }
}


最终代码实现效果如下图:



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值