源码地址: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); } }总界面
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(); } }
看看最初界面最复杂的适配器
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(); } }
作者:AndroidFlying007 发表于2016/12/1 22:53:26 原文链接阅读:75 评论:0 查看评论