CityPicker 自定义数据源

1,147 阅读7分钟

城市选择自定义数据

在使用 Citypicker 的时候,觉得其 UI 很不错,很多项目可以拿来即可使用。先来两张效果图镇楼!

效果图1

效果图2
问题是其数据源不支持自定义,因此使用起来很不方便。后来在看了源码之后,修改了其中几处代码,使其支持两种方式的数据源自定义。先看看作者对数据是怎么设计的?

一、代码设计

在原来的代码中,是通过一个 DBManager 的对象来获取所有数据以及被查询的数据源,在 DBManager 对象中有三个关键方法:

  • copyDBFile 该方法的目的是从 assets 目录下复制数据库 db 到 data 目录下,以便后续查询
  • getAllCities 该方法的目的是从数据库查询所有城市数据
  • searchCity 该方法的目的是从数据库搜索目标城市的数据
/**
 * Author Bro0cL on 2016/1/26.
 */
public class DBManager {
    private static final int BUFFER_SIZE = 1024;

    private String DB_PATH;
    private Context mContext;

    public DBManager(Context context) {
        this.mContext = context;
        DB_PATH = File.separator + "data"
                + Environment.getDataDirectory().getAbsolutePath() + File.separator
                + context.getPackageName() + File.separator + "databases" + File.separator;
        copyDBFile();
    }

    private void copyDBFile(){
        File dir = new File(DB_PATH);
        if (!dir.exists()){
            dir.mkdirs();
        }
        //如果旧版数据库存在,则删除
        File dbV1 = new File(DB_PATH + DB_NAME_V1);
        if (dbV1.exists()){
            dbV1.delete();
        }
        //创建新版本数据库
        File dbFile = new File(DB_PATH + LATEST_DB_NAME);
        if (!dbFile.exists()){
            InputStream is;
            OutputStream os;
            try {
                is = mContext.getResources().getAssets().open(LATEST_DB_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();
            }
        }
    }

    public List<City> getAllCities(){
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_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(COLUMN_C_NAME));
            String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
            String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
            String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
            city = new City(name, province, pinyin, code);
            result.add(city);
        }
        cursor.close();
        db.close();
        Collections.sort(result, new CityComparator());
        return result;
    }

    public List<City> searchCity(final String keyword){
        String sql = "select * from " + TABLE_NAME + " where "
                + COLUMN_C_NAME + " like ? " + "or "
                + COLUMN_C_PINYIN + " like ? ";
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_DB_NAME, null);
        Cursor cursor = db.rawQuery(sql, new String[]{"%"+keyword+"%", keyword+"%"});

        List<City> result = new ArrayList<>();
        while (cursor.moveToNext()){
            String name = cursor.getString(cursor.getColumnIndex(COLUMN_C_NAME));
            String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
            String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
            String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
            City city = new City(name, province, pinyin, code);
            result.add(city);
        }
        cursor.close();
        db.close();
        CityComparator comparator = new CityComparator();
        Collections.sort(result, comparator);
        return result;
    }

    /**
     * sort by 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);
        }
    }
}

我想到的第一个办法就是将 DBManager 对象进行抽象,然后默认数据通过默认实现来实现,并且在 CityPickerDialogFragment 以及CityPicker 支持设置数据源 DBManager。

public abstract class DBManager {
    public static final int BUFFER_SIZE = 1024;

    public String DB_PATH;
    public Context mContext;

    public DBManager(Context context) {
        this.mContext = context;
        DB_PATH = File.separator + "data"
                + Environment.getDataDirectory().getAbsolutePath() + File.separator
                + context.getPackageName() + File.separator + "databases" + File.separator;
        copyDBFile();
    }

    public void copyDBFile() {
        File dir = new File(DB_PATH);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        createDbFile(LATEST_DB_NAME);
    }

    public void createDbFile(String tbName) {
        //创建新版本数据库
        File dbFile = new File(DB_PATH + tbName);
        if (!dbFile.exists()) {
            InputStream is;
            OutputStream os;
            try {
                is = mContext.getResources().getAssets().open(tbName);
                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();
            }
        }
    }

    public abstract List<City> getAllCities();

    public abstract List<City> searchCity(final String keyword);

    /**
     * sort by a-z
     */
    public 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);
        }
    }
}

默认数据源:

/**
 * @author Vincent
 */
public class DefaultDBManager extends DBManager {


    public DefaultDBManager(Context context) {
        super(context);
        //如果旧版数据库存在,则删除
        File dbV1 = new File(DB_PATH + DB_NAME_V1);
        if (dbV1.exists()) {
            dbV1.delete();
        }
    }


    public List<City> getAllCities() {
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_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(COLUMN_C_NAME));
            String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
            String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
            String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
            city = new City(name, province, pinyin, code);
            result.add(city);
        }
        cursor.close();
        db.close();
        Collections.sort(result, new CityComparator());
        return result;
    }

    public List<City> searchCity(final String keyword) {
        String sql = "select * from " + TABLE_NAME + " where "
                + COLUMN_C_NAME + " like ? " + "or "
                + COLUMN_C_PINYIN + " like ? ";
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_DB_NAME, null);
        Cursor cursor = db.rawQuery(sql, new String[]{"%" + keyword + "%", keyword + "%"});

        List<City> result = new ArrayList<>();
        while (cursor.moveToNext()) {
            String name = cursor.getString(cursor.getColumnIndex(COLUMN_C_NAME));
            String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
            String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
            String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
            City city = new City(name, province, pinyin, code);
            result.add(city);
        }
        cursor.close();
        db.close();
        CityComparator comparator = new CityComparator();
        Collections.sort(result, comparator);
        return result;
    }


}

这样支持了数据的扩展,但是该如何使用呢?增加三个方法和一个判断即可:

CityPickerDialogFragment:

private void initData() {
    ...
     if(dbManager == null){
            dbManager = new DefaultDBManager(getActivity());
        }
    ...
}
 public void setDBManager(DBManager defaultDbManager){
        this.dbManager = defaultDbManager;
    }

CityPicker:

private DBManager mDBManager;
 /**
 * 设置数据库
 * @param defaultDbManager
 * @return
 */
public CityPicker setDbManager(DBManager defaultDbManager){
    this.mDBManager = defaultDbManager;
    return this;
}

public void show(){
 ...
    cityPickerFragment.setDBManager(mDBManager);
 ...
}

二、自定义数据源

实现了默认数据,就该实现数据自定义了,这里有两个方法:

1.通过List集合接收自定义参数

具体思路就是通过自定义数据源,来替换掉数据库里面的默认数据,并且第二次使用的时候调用默认数据源即可,因为我们的数据存储方式是数据库这种持久化。(如果将新数据填充到默认数据以后希望又直接使用默认数据可通过自定义数据源并重写 copyDBFile 方法)

/**
 * 创建日期:2019/4/1 0001on 下午 1:46
 * 描述:通过List集合接收数据源
 * @author Vincent
 * QQ:3332168769
 * 备注:
 */
public class CustomDBManager extends DefaultDBManager {
    public CustomDBManager(Context context, List<City> source) {
        super(context);
        updateDao(source);
    }

    private void updateDao(List<City> source) {
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_DB_NAME, null);

        try {
            db.delete(TABLE_NAME, null, null);

            db.beginTransaction();
            SQLiteStatement stat = db.compileStatement("insert into " + TABLE_NAME + " ('c_name','c_pinyin','c_code','c_province') VALUES (?,?, ?, ?)");
            for (int i = 0; i < source.size(); i++) {
                City city = source.get(i);
                stat.bindString(1, city.getName());
                stat.bindString(2, city.getPinyin());
                stat.bindString(3, city.getCode());
                stat.bindString(4, city.getProvince());
                stat.executeInsert();
            }
            db.setTransactionSuccessful();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            db.endTransaction();
            db.close();
        }
    }


}

上面需要说明的是,我们的数据一次性插入有接近3500条,因此使用了这种 SQLiteStatement 的方式(ps:特别需要提醒的是 compileStatement 方法接收的参数是一个 inset 命令,用于识别下面的内容插入到数据库指定列,我之前以为是普通的插入一条数据的命令,结果一直在这个地方崩溃,因为数据库无法识别这条命令,而百度的结果却没有这方面的提醒)。插入大量数据到数据库的具体方式有如下四种:

插入批量数据
插入方式参考:如何对SQLite数据库中进行大量的数据插入?

如果希望有性能的提升,建议调用的时候做一个标志位,第一次的时候使用自定义数据,非第一次使用默认方法,因为每次插入几千条数据确实是一个耗时过程,虽然不是常用工具,但是注重细节的性能提升总是个好习惯!

2.直接使用sqlite数据库

这种方式和默认的方式特别像,均是将 assets 下的数据库 db 文件复制到 data 目录下供查询使用。正常流程应该是先生成一个数据库db文件,然后自定义一个继承自 DBManager 的数据源。此处我们先自定义一个 DBManager ,代码如下:

/**
 * 创建日期:2019/4/2 0002on 上午 10:14
 * 描述:通过db文件获取自定义数据
 * @author:Vincent
 * QQ:3332168769
 * 备注:
 */
class FileDBManager(val ctx: Context) : DBManager(ctx) {

    /**
     * assets文件夹下数据库文件名称
     */
    private val tbName = "myCities.db"
    private val tableName = "targetcity"
    override fun copyDBFile() {
        val dir = File(DB_PATH)
        if (!dir.exists()) {
            dir.mkdirs()
        }

        createDbFile(tbName)
    }

    override fun getAllCities(): MutableList<City> {
        val db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + tbName, null)
        val cursor = db.rawQuery("select * from $tableName", null)
        val result = mutableListOf<City>()
        var city: City
        while (cursor.moveToNext()) {
            val name = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_NAME))
            val province = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_PROVINCE))
            val pinyin = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_PINYIN))
            val code = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_CODE))
            city = City(name, province, pinyin, code)
            result.add(city)
        }
        cursor.close()
        db.close()
        Collections.sort(result, CityComparator())
        return result
    }

    override fun searchCity(keyword: String?): MutableList<City> {
        val sql = ("select * from " + tableName + " where "
                + DBConfig.COLUMN_C_NAME + " like ? " + "or "
                + DBConfig.COLUMN_C_PINYIN + " like ? ")
        val db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + tbName, null)
        val cursor = db.rawQuery(sql, arrayOf("%$keyword%", "$keyword%"))

        val result = mutableListOf<City>()
        while (cursor.moveToNext()) {
            val name = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_NAME))
            val province = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_PROVINCE))
            val pinyin = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_PINYIN))
            val code = cursor.getString(cursor.getColumnIndex(DBConfig.COLUMN_C_CODE))
            val city = City(name, province, pinyin, code)
            result.add(city)
        }
        cursor.close()
        db.close()
        val comparator = CityComparator()
        Collections.sort(result, comparator)
        return result
    }
}

这个方法写好了,接下来要做的就是生成db文件,我是直接通过郭神(郭霖)的 Litepal 来生成一个数据库,然后将 assets 文件夹下的 json 文件读出来,转成 SourceCity 对象集合,接下来遍历集合的时候将 SourceCity 对象转成 TargetCity 并保存,其中文本转拼音使用了一个 pinyin4j 的开源库,这部分代码我单独放到一个项目,参考地址:CityTest
最后将sd卡根目录下的Android/data/包名/files/database/文件名.db文件放到assets文件下即可。

三、其他bug

1.定位成功无法刷新数据

当定位信息是已知的时候,直接回调定位信息的时候 RecyclerView 不显示更新的定位信息。经过断点跟踪,发现是因为定位回调的时候RecyclerView不可见,因此更新失败,修改方式是延迟300毫秒刷新:

public void locationChanged(final LocatedCity location, final int state){
        // 防止在RecycleView不可见的时候定位成功无法刷新 RecycleView
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mAdapter.updateLocateState(location, state);
            }
        },300);

    }

2.输入的时候按下enter 键有空格

经过检查,发现作者已经设置过EditText为单行,但是按下的时候依然有空格。刚开始的想法是在判断输入内容之前去除两段空格,然后将空字符串赋值给EditText,最后发现把软键盘卡死了!你没有看错,是把软键盘卡死了! 最后的解决办法也超级简单:

@Override
    public void afterTextChanged(Editable s) {
        String keyword = s.toString();
        if (TextUtils.isEmpty(keyword)){
            ......
        }else if(keyword.trim().isEmpty()){
            // 实现禁用Enter键的效果
            mSearchBox.setText("");
        }
        else {
            ......
        }
        mRecyclerView.scrollToPosition(0);
    }

以上就是关于选择城市自定义数据源的一些思考与总结。


CityPicker 开源库:CityPicker

源码地址:BaseProject