美团城市选择源码解析

来源:互联网 发布: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
原创粉丝点击