源码地址:https://github.com/helloworld107/CitySelect
效果图
源码分析
先从简单的来吧,先说数据,对于一个城市而言名字必须有的,其次因为控件还会有相关的导航字母,所以还需要每个城市的拼音,这样一个城市的实体类就完成了,因为城市数据量庞大,显然装在了一个数据库中,这样我们通过sqlite获取数据和查找也非常方便
数据库放在asset文件下,app运行后我们会以流的形式存到sd卡文件夹下,使用时从内存卡读取到集合中使用,相关管理类代码
public class DBManager {
private static final String ASSETS_NAME = "china_cities.db";
private static final String DB_NAME = "china_cities.db";
private static final String TABLE_NAME = "city";
private static final String NAME = "name";
private static final String PINYIN = "pinyin";
private static final int BUFFER_SIZE = 1024;
private String DB_PATH;
private Context mContext;
// public static DBManager init(){
// if (mInstance == null){
// synchronized (DBManager.class){
// if (mInstance != null){
// mInstance = new DBManager();
// }
// }
// }
// return mInstance;
// }
public DBManager(Context context) {
this.mContext = context;
DB_PATH = File.separator + "data"
+ Environment.getDataDirectory().getAbsolutePath() + File.separator
+ context.getPackageName() + File.separator + "databases" + File.separator;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public void copyDBFile(){
File dir = new File(DB_PATH);
if (!dir.exists()){
dir.mkdirs();
}
File dbFile = new File(DB_PATH + DB_NAME);
if (!dbFile.exists()){
InputStream is;
OutputStream os;
try {
is = mContext.getResources().getAssets().open(ASSETS_NAME);
os = new FileOutputStream(dbFile);
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buffer, 0, buffer.length)) > 0){
os.write(buffer, 0, length);
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读取所有城市
* @return
*/
public List<City> getAllCities(){
//有了数据干什么都soeasy啊
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
Cursor cursor = db.rawQuery("select * from " + TABLE_NAME, null);
List<City> result = new ArrayList<>();
City city;
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(NAME));
String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
city = new City(name, pinyin);
result.add(city);
}
cursor.close();
db.close();
Collections.sort(result, new CityComparator());
return result;
}
/**
* 通过名字或者拼音搜索
* @param keyword
* @return
*/
public List<City> searchCity(final String keyword){
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
Cursor cursor = db.rawQuery("select * from " + TABLE_NAME +" where name like \"%" + keyword
+ "%\" or pinyin like \"%" + keyword + "%\"", null);
List<City> result = new ArrayList<>();
City city;
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(NAME));
String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
city = new City(name, pinyin);
result.add(city);
}
cursor.close();
db.close();
Collections.sort(result, new CityComparator());
return result;
}
/**
* a-z排序
*/
private class CityComparator implements Comparator<City>{
@Override
public int compare(City lhs, City rhs) {
String a = lhs.getPinyin().substring(0, 1);
String b = rhs.getPinyin().substring(0, 1);
return a.compareTo(b);
}
}
}
之后看看搜索布局,显然列表是一个listview,多了一个右侧的导航条,并且点击时中间还会出现中的大方块字母,其实一直存在于总布局中,只不过我们只在点击的时候让它显示,右边的导航条目是个自定义控件,略有难度,上代码,其实就是把26个字母加特殊符号打印了下来,并且设置了点击相应的位置的接口回调
ublic class SideLetterBar extends View {
private static final String[] b = {"定位", "热门", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
private int choose = -1;
private Paint paint = new Paint();
private boolean showBg = false;
private OnLetterChangedListener onLetterChangedListener;
private TextView overlay;
public SideLetterBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SideLetterBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SideLetterBar(Context context) {
super(context);
}
/**
* 设置悬浮的textview
* @param overlay
*/
public void setOverlay(TextView overlay){
this.overlay = overlay;
}
@SuppressWarnings("deprecation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (showBg) {
canvas.drawColor(Color.TRANSPARENT);
}
//画出索引字母
int height = getHeight();
int width = getWidth();
int singleHeight = height / b.length;
for (int i = 0; i < b.length; i++) {
paint.setTextSize(getResources().getDimension(R.dimen.side_letter_bar_letter_size));
paint.setColor(getResources().getColor(R.color.gray));
paint.setAntiAlias(true);
//如果手指戳到相应位置,选中颜色变深 字母比较小,看的不明显
if (i == choose) {
paint.setColor(getResources().getColor(R.color.gray_deep));
paint.setFakeBoldText(true); //加粗
}
//计算相应字母的距离居中
float xPos = width / 2 - paint.measureText(b[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(b[i], xPos, yPos, paint);
paint.reset();
}
}
// 设置中间显示的结果
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getAction();
final float y = event.getY();
final int oldChoose = choose;
final OnLetterChangedListener listener = onLetterChangedListener;
//相应高度的比例乘以字符数组长度就是我们数组对应角标
final int c = (int) (y / getHeight() * b.length);
switch (action) {
case MotionEvent.ACTION_DOWN:
showBg = true;
if (oldChoose != c && listener != null) {
if (c >= 0 && c < b.length) {
listener.onLetterChanged(b[c]);
choose = c;
invalidate();
if (overlay != null){
overlay.setVisibility(VISIBLE);
overlay.setText(b[c]);
}
}
}
break;
case MotionEvent.ACTION_MOVE:
if (oldChoose != c && listener != null) {
if (c >= 0 && c < b.length) {
listener.onLetterChanged(b[c]);
choose = c;
invalidate();
if (overlay != null){
overlay.setVisibility(VISIBLE);
overlay.setText(b[c]);
}
}
}
break;
case MotionEvent.ACTION_UP:
showBg = false;
choose = -1;
invalidate();
if (overlay != null){
overlay.setVisibility(GONE);
}
break;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
public void setOnLetterChangedListener(OnLetterChangedListener onLetterChangedListener) {
this.onLetterChangedListener = onLetterChangedListener;
}
//点击事件交给外部类调用
public interface OnLetterChangedListener {
void onLetterChanged(String letter);
}
}
总界面
初始化列表布局填充数据,同时处理了搜索框的逻辑,搜索框的结果是布局里另外一个listview,和最初的重叠显示,只是在搜索的时候去显示出来,同时结果列表和最初列表的适配器都需要单独定义,复杂的处理还是在适配器里,自定义索引的点击事件处理就是改变列表的位置setpoistion
public class CityPickerActivity extends AppCompatActivity implements View.OnClickListener {
public static final int REQUEST_CODE_PICK_CITY = 2333;
public static final String KEY_PICKED_CITY = "picked_city";
private ListView mListView;
private ListView mResultListView;
private SideLetterBar mLetterBar;
private EditText searchBox;
private ImageView clearBtn;
private ImageView backBtn;
private ViewGroup emptyView;
private CityListAdapter mCityAdapter;
private ResultListAdapter mResultAdapter;
private List<City> mAllCities;
private DBManager dbManager;
private AMapLocationClient mLocationClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_city_list);
initData();//从数据拿到城市集合,并且设置城市列表适配器
initView();//初始化布局,设置相关监听
initLocation();//定位功能根据自己使用的第三方api来使用,这里不考虑
}
private void initLocation() {
mLocationClient = new AMapLocationClient(this);
AMapLocationClientOption option = new AMapLocationClientOption();
option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
option.setOnceLocation(true);
mLocationClient.setLocationOption(option);
mLocationClient.setLocationListener(new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
if (aMapLocation != null) {
if (aMapLocation.getErrorCode() == 0) {
String city = aMapLocation.getCity();
String district = aMapLocation.getDistrict();
Log.e("onLocationChanged", "city: " + city);
Log.e("onLocationChanged", "district: " + district);
String location = StringUtils.extractLocation(city, district);
mCityAdapter.updateLocateState(LocateState.SUCCESS, location);
} else {
//定位失败
mCityAdapter.updateLocateState(LocateState.FAILED, null);
}
}
}
});
mLocationClient.startLocation();
}
private void initData() {
dbManager = new DBManager(this);
dbManager.copyDBFile();
mAllCities = dbManager.getAllCities();
mCityAdapter = new CityListAdapter(this, mAllCities);
mCityAdapter.setOnCityClickListener(new CityListAdapter.OnCityClickListener() {
@Override
public void onCityClick(String name) {
back(name);//点击吐司
}
@Override
public void onLocateClick() {
Log.e("onLocateClick", "重新定位...");
mCityAdapter.updateLocateState(LocateState.LOCATING, null);
mLocationClient.startLocation();
}
});
//搜索框用了另外一个列表,更加简单,这个列表跟原来的列表是重叠的,两者根据业务逻辑只显示其中之一
mResultAdapter = new ResultListAdapter(this, null);
}
private void initView() {
mListView = (ListView) findViewById(R.id.listview_all_city);
mListView.setAdapter(mCityAdapter);
TextView overlay = (TextView) findViewById(R.id.tv_letter_overlay);
mLetterBar = (SideLetterBar) findViewById(R.id.side_letter_bar);
mLetterBar.setOverlay(overlay);
mLetterBar.setOnLetterChangedListener(new SideLetterBar.OnLetterChangedListener() {
@Override
public void onLetterChanged(String letter) {
//通过自定义导航的接口回调就控制了列表的选择项
int position = mCityAdapter.getLetterPosition(letter);
mListView.setSelection(position);
}
});
//搜索框使用了另外一个列表跟适配器,也非常简单,两者列表位置一样,根据逻辑只能显示其中一个
searchBox = (EditText) findViewById(R.id.et_search);
searchBox.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String keyword = s.toString();
if (TextUtils.isEmpty(keyword)) {
clearBtn.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
mResultListView.setVisibility(View.GONE);
} else {
clearBtn.setVisibility(View.VISIBLE);
mResultListView.setVisibility(View.VISIBLE);
List<City> result = dbManager.searchCity(keyword);
if (result == null || result.size() == 0) {
emptyView.setVisibility(View.VISIBLE);
} else {
emptyView.setVisibility(View.GONE);
mResultAdapter.changeData(result);
}
}
}
});
emptyView = (ViewGroup) findViewById(R.id.empty_view);
mResultListView = (ListView) findViewById(R.id.listview_search_result);
mResultListView.setAdapter(mResultAdapter);
mResultListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
back(mResultAdapter.getItem(position).getName());
}
});
clearBtn = (ImageView) findViewById(R.id.iv_search_clear);
backBtn = (ImageView) findViewById(R.id.back);
clearBtn.setOnClickListener(this);
backBtn.setOnClickListener(this);
}
private void back(String city){
ToastUtils.showToast(this, "点击的城市:" + city);
// Intent data = new Intent();
// data.putExtra(KEY_PICKED_CITY, city);
// setResult(RESULT_OK, data);
// finish();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.iv_search_clear:
searchBox.setText("");
clearBtn.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
mResultListView.setVisibility(View.GONE);
break;
case R.id.back:
finish();
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mLocationClient.stopLocation();
}
}
列表显示适配器稍微复杂,一共分为定位 热门 基本三种类型,前两种比较简单,就是一个简单的布局,热门的布局是一个gridview,所以并没有做holder优化,基本布局的显示
因为有了索引所以增加了复杂性,需要判断前后的城市的拼音是否一致才去显示索引,并且为了记录索引的位置还引入了hashmap来封装
public class CityListAdapter extends BaseAdapter { private static final int VIEW_TYPE_COUNT = 3; private Context mContext; private LayoutInflater inflater; private List<City> mCities; //导航字母,因为每个拼音只有一个,所以我们需要记住每个导航的具体位置,需要键值对集合封装 private HashMap<String, Integer> letterIndexes; //此业务暂时不需要,请无视 private String[] sections; private OnCityClickListener onCityClickListener; private int locateState = LocateState.LOCATING; private String locatedCity; public CityListAdapter(Context mContext, List<City> mCities) { this.mContext = mContext; this.mCities = mCities; this.inflater = LayoutInflater.from(mContext); if (mCities == null){ mCities = new ArrayList<>(); } //强行补了两个数据为了增加类型,热门和定位,对于普通条目无意义 mCities.add(0, new City("定位", "0")); mCities.add(1, new City("热门", "1")); int size = mCities.size(); letterIndexes = new HashMap<>(); sections = new String[size];
//通过比较两个条目的拼音是否一样即可确定需要几个导航 for (int index = 0; index < size; index++){ //当前城市拼音首字母 String currentLetter = PinyinUtils.getFirstLetter(mCities.get(index).getPinyin()); //上个首字母,如果不存在设为"" String previousLetter = index >= 1 ? PinyinUtils.getFirstLetter(mCities.get(index - 1).getPinyin()) : ""; if (!TextUtils.equals(currentLetter, previousLetter)){ letterIndexes.put(currentLetter, index); sections[index] = currentLetter; } } } /** * 更新定位状态 * @param state */ public void updateLocateState(int state, String city){ this.locateState = state; this.locatedCity = city; notifyDataSetChanged(); } /** * 获取字母索引的位置 * @param letter * @return */ public int getLetterPosition(String letter){ Integer integer = letterIndexes.get(letter); return integer == null ? -1 : integer; } @Override public int getViewTypeCount() { return VIEW_TYPE_COUNT; } @Override public int getItemViewType(int position) { return position < VIEW_TYPE_COUNT - 1 ? position : VIEW_TYPE_COUNT - 1; } @Override public int getCount() { return mCities == null ? 0: mCities.size(); } @Override public City getItem(int position) { return mCities == null ? null : mCities.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View view, ViewGroup parent) { CityViewHolder holder; int viewType = getItemViewType(position); switch (viewType){ case 0: //定位 view = inflater.inflate(R.layout.view_locate_city, parent, false); ViewGroup container = (ViewGroup) view.findViewById(R.id.layout_locate); TextView state = (TextView) view.findViewById(R.id.tv_located_city); switch (locateState){ case LocateState.LOCATING: state.setText(mContext.getString(R.string.locating)); break; case LocateState.FAILED: state.setText(R.string.located_failed); break; case LocateState.SUCCESS: state.setText(locatedCity); break; } container.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (locateState == LocateState.FAILED){ //重新定位 if (onCityClickListener != null){ onCityClickListener.onLocateClick(); } }else if (locateState == LocateState.SUCCESS){ //返回定位城市 if (onCityClickListener != null){ onCityClickListener.onCityClick(locatedCity); } } } }); break; case 1: //热门城市 view = inflater.inflate(R.layout.view_hot_city, parent, false); WrapHeightGridView gridView = (WrapHeightGridView) view.findViewById(R.id.gridview_hot_city); final HotCityGridAdapter hotCityGridAdapter = new HotCityGridAdapter(mContext); gridView.setAdapter(hotCityGridAdapter); gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (onCityClickListener != null){ onCityClickListener.onCityClick(hotCityGridAdapter.getItem(position)); } } }); break; case 2: //正常条目 if (view == null){ //默认布局每一个布局都是在导航首字母的,只不过通过判断前一个删除掉了 view = inflater.inflate(R.layout.item_city_listview, parent, false); holder = new CityViewHolder(); holder.letter = (TextView) view.findViewById(R.id.tv_item_city_listview_letter); holder.name = (TextView) view.findViewById(R.id.tv_item_city_listview_name); view.setTag(holder); }else{ holder = (CityViewHolder) view.getTag(); } if (position >= 1){ final String city = mCities.get(position).getName(); holder.name.setText(city); String currentLetter = PinyinUtils.getFirstLetter(mCities.get(position).getPinyin()); String previousLetter = position >= 1 ? PinyinUtils.getFirstLetter(mCities.get(position - 1).getPinyin()) : ""; //如果跟上一个字母拼音不一样就显示导航字母,否则就删除,大部分都是删除 if (!TextUtils.equals(currentLetter, previousLetter)){ holder.letter.setVisibility(View.VISIBLE); holder.letter.setText(currentLetter); }else{ holder.letter.setVisibility(View.GONE); } holder.name.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onCityClickListener != null){ onCityClickListener.onCityClick(city); } } }); } break; } return view; } public static class CityViewHolder{ TextView letter; TextView name; } public void setOnCityClickListener(OnCityClickListener listener){ this.onCityClickListener = listener; } public interface OnCityClickListener{ void onCityClick(String name); void onLocateClick(); } }