一步步实现一个城市选择器
Posted 月色下的独轮车
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步步实现一个城市选择器相关的知识,希望对你有一定的参考价值。
城市选择器
今天我们一起实现一个城市选择器。O.O
代码下载:
城市选择器 - 下载频道 - CSDN.NET
http://download.csdn.net/detail/baidu_31093133/9675482
效果图预览
主要包含以下内容:
1、自动定位所在城市
2、热门城市列表展示
3、所有城市列表的展示
4、输入城市名或者城市拼音搜索对应城市
5、右侧的slidebar城市列表导航栏
请大家先下载Demo然后再一边看demo一边看博客。因为博客里很多代码因为比较简单就不贴了。
首先我们先搭建基本的UI:
分析效果图,我们需要一个顶部title view,一个搜索框,一个定位功能的view,一个展示热门城市的view,一个侧边栏view和一个listview。
###顶部title View:
这里有一些需要注意的地方:
我们在新建工程的时候,android studio会自动生成一个style作为我们的主题:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
android:theme="@style/AppTheme"
这个默认的主题是带有actionbar的,如果我们要去掉这个actionbar,首先需要把DarkActionBar改为NoActionBar,因为使用AppCompatActivity的时候,Activity必须使用Theme.AppCompat主题及其子主题,所以我们的自定义的HD_NoActionBar样式必须继承这个主题:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="HD_NoActionBar" parent="AppTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
</style>
然后引用这个style:
android:theme="@style/AppTheme.NoActionBar"
接下来写我们的头布局 title_view.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/light_blue">
<ImageView
android:id="@+id/back"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:scaleType="center"
android:src="@mipmap/ic_back"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/select_city"
android:textSize="20sp"
android:textColor="@color/white" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/white"/>
布局返回按钮用一个ImageView,title用一个Textview。
然后在我们的主布局里使用标签引入头布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_blue"
android:orientation="vertical">
<include layout="@layout/title_view" />
现在的效果是这样的:
搜索框布局 search view
search_view/xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:background="@drawable/search_box_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:src="@mipmap/ic_search"
android:scaleType="center"
tools:ignore="ContentDescription" />
<EditText
android:id="@+id/et_search"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@null"
android:gravity="center_vertical"
android:hint="@string/hint_search_box"
android:textColorHint="@color/deep_blue"
android:inputType="text"
android:singleLine="true"
android:textColor="@color/deep_blue"
android:textSize="14sp"
tools:ignore="RtlHardcoded" />
<ImageView
android:id="@+id/iv_search_clear"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:src="@mipmap/ic_search_clear"
android:visibility="gone"
tools:ignore="ContentDescription" />
然后在主布局里引入这个布局:
<include layout="@layout/search_view"/>
搜索框的布局也非常简单,就不说明了。
现在的效果:
###城市列表
接下来的定位城市、热门城市、以及所有城市的列表我们使用一个Listview搞定,让Listview加载三种不同的布局来展示。
定位城市和所有城市列表好说,这个热门城市的UI该怎么做呢?我们准备使用gridview来做,在listview里嵌套gridview会遇到gridview只能显示一行的问题,我们先重现这个问题,然后再分析怎么解决。
listview需要一个adapter适配器,adapter需要一个数据源,我们的数据源存放在一个db数据库里,所以我们要构建一个数据库操作类,从数据库中取出这些城市然后展示出来。这一段的代码比较多,前方高能预警(__)
我们把要做的事情按步骤划分:
1、导入数据库文件
2、构建City对象,用户存储城市信息
3、创建DBManager用来操作数据库,将查询到的数据传递给adapter
4、编写定位城市、热门城市、所有城市三种不同的item布局
5、编写adapter,在adapter里加载三种item布局
6、编写gridview热门城市的item布局
7、实现gridview的adapter
####1、建立assets文件,并把db文件放在assets目录下:
####2、City对象
City.java:
public class City {
private String name;
private String pinyin;
public City() {}
public City(String name, String pinyin) {
this.name = name;
this.pinyin = pinyin;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPinyin() {
return pinyin;
}
public void setPinyin(String pinyin) {
this.pinyin = pinyin;
}
}
####数据库操作类:
DBManager.java:
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 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(){
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);
}
}
}
这个类使用SQLiteDatabase来管理数据库,同事写了一个排序类CityComparator用来对城市按照首字母进行排序
###定位城市的布局:
view_locate_city.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
style="@style/LetterIndexTextViewStyle"
android:text="@string/located_city"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:background="@color/content_bg">
<LinearLayout
android:id="@+id/layout_locate"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:minWidth="96dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="center"
android:clickable="true"
android:background="@drawable/overlay_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_locate"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_located_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="@string/locating"
android:textSize="16sp"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
效果图
然后是所有城市的布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/tv_item_city_listview_letter"
style="@style/LetterIndexTextViewStyle"
android:textSize="18sp"
android:clickable="false"/>
<TextView
android:id="@+id/tv_item_city_listview_name"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingLeft="16dp"
android:paddingRight="@dimen/side_letter_bar_width"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:textSize="@dimen/city_text_size"
android:textColor="@color/light_blue"/>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginLeft="16dp"
android:layout_marginRight="@dimen/side_letter_bar_width"
android:background="@color/divider"/>
</LinearLayout>
使用两个TextView一个用来显示城市的首字母,一个用来显示城市名字
上面的布局都很简单,接下来就是热门城市了:
如果我们使用gridview不做任何处理的话,最终效果是这样的:
不止在listview,gridview在其它任何可以滚动的view里都会出现这个问题,解决这个问题我们有固定的方案,那就是自定义一个gridview然后重写onMeasure方法,在onMeasure方法里,让gridview测量子view的高度,并全部显示出来。
代码其实非常简单:
public class WrapHeightGridView extends GridView {
public WrapHeightGridView(Context context) {
this(context, null);
}
public WrapHeightGridView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WrapHeightGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//如果把GridView放到一个垂直方向滚动的布局中,设置其高度属性为 wrap_content ,
// 则该GridView的高度只有一行内容,其他内容通过滚动来显示。
// 如果你想让该GridView的高度为所有行内容所占用的实际高度,则可以通过覆写GridView的 onMeasure 函数来修改布局参数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightSpec);
}
}
这里重点理解makeMeasureSpec这个方法,public static int makeMeasureSpec(int size, int mode)
这个是由我们给出的尺寸大小和模式生成一个包含这两个信息的int变量。这个int值有32位,其中高2位表示模式,低30位表示值。我们把Integer.MAX_VALUE >> 2右移两位然后和MeasureSpec.AT_MOST合成一个新的int值。Integer.MAX_VALUE是INT类型的最大值是0xFFFFFFFF,所以这个值右移两位就表示新的合成的int值得低30位都是1。也就是说我们取最大值作为控件高度的最大值。
这样就可以了,效果:
接下来看一下adapter是怎么实现的:
CityListAdapter.java:
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 以上是关于一步步实现一个城市选择器的主要内容,如果未能解决你的问题,请参考以下文章