美团城市选择源码解析
来源:互联网 发布:windows 10下崩溃绿屏 编辑:程序博客网 时间:2024/06/05 21:03
源码地址: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(); }}
0 0
- 美团城市选择源码解析
- Android 类似美团的选择城市界面
- Android中调用第三方实现美团城市选择
- [美团 CodeM 复赛]城市网络
- 城市选择器(仿照美团)
- 简单实现美团城市切换
- 解析阿里巴巴为什么选择赴美上市
- 解析阿里巴巴为什么选择赴美上市
- 城市选择
- 城市选择
- 美团复赛 城市网络 (双倍增)
- 美团CODEM 复赛 城市网络 询问离线,树上LCA
- 美团 Cat 源码文章
- 一座城市的美
- 城市选择js效果
- 省份 城市 选择列表
- 城市选择表单代码
- 二级联动城市选择
- markdown学习
- c++用指针遍历一维数组和二维数组
- Linux笔记
- “friend声明友元函数,友元函数却依旧无法访问该类的私有属性”的解决方法
- Caffe-CIFAR10实验
- 美团城市选择源码解析
- js之迭代器模式
- 服务器安全配置之一:用户管理
- {{}}、ng-bind和ng-model的区别
- 面向对象解析(一)
- Mac 下 nginx 的相关操作
- 系统中的yum服务
- 几种深度学习库的整理
- 好用的jQuery