配色: 字号:
一步步实现一个城市选择器
2016-11-08 | 阅:  转:  |  分享 
  
一步步实现一个城市选择器



主要包含以下内容:



1、自动定位所在城市

2、热门城市列表展示

3、所有城市列表的展示

4、输入城市名或者城市拼音搜索对应城市

5、右侧的slidebar城市列表导航栏



请大家先下载Demo然后再一边看demo一边看博客。因为博客里很多代码因为比较简单就不贴了。



首先我们先搭建基本的UI:



分析效果图,我们需要一个顶部titleview,一个搜索框,一个定位功能的view,一个展示热门城市的view,一个侧边栏view和一个listview。



顶部titleView:



这里有一些需要注意的地方:

我们在新建工程的时候,Androidstudio会自动生成一个style作为我们的主题:







@color/colorPrimary

@color/colorPrimaryDark

@color/colorAccent





android:theme="@style/AppTheme"

这个默认的主题是带有actionbar的,如果我们要去掉这个actionbar,首先需要把DarkActionBar改为NoActionBar,因为使用AppCompatActivity的时候,Activity必须使用Theme.AppCompat主题及其子主题,所以我们的自定义的HD_NoActionBar样式必须继承这个主题:







@color/colorPrimary

@color/colorPrimaryDark

@color/colorAccent





true

false



然后引用这个style:



android:theme="@style/AppTheme.NoActionBar"

接下来写我们的头布局title_view.xml:




xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="vertical">




android:layout_width="match_parent"

android:layout_height="?attr/actionBarSize"

android:background="@color/light_blue">




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"/>




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"/>






android:layout_width="match_parent"

android:layout_height="1dp"

android:background="@color/white"/>

布局返回按钮用一个ImageView,title用一个Textview。



然后在我们的主布局里使用标签引入头布局:




android:id="@+id/activity_main"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@color/light_blue"

android:orientation="vertical">





现在的效果是这样的:



搜索框布局searchview



search_view/xml:




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">




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"/>




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"/>




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"/>

然后在主布局里引入这个布局:





搜索框的布局也非常简单,就不说明了。



现在的效果:



城市列表



接下来的定位城市、热门城市、以及所有城市的列表我们使用一个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:



publicclassCity{

privateStringname;

privateStringpinyin;



publicCity(){}



publicCity(Stringname,Stringpinyin){

this.name=name;

this.pinyin=pinyin;

}



publicStringgetName(){

returnname;

}



publicvoidsetName(Stringname){

this.name=name;

}



publicStringgetPinyin(){

returnpinyin;

}



publicvoidsetPinyin(Stringpinyin){

this.pinyin=pinyin;

}

}



数据库操作类:



DBManager.java:



publicclassDBManager{

privatestaticfinalStringASSETS_NAME="china_cities.db";

privatestaticfinalStringDB_NAME="china_cities.db";

privatestaticfinalStringTABLE_NAME="city";

privatestaticfinalStringNAME="name";

privatestaticfinalStringPINYIN="pinyin";

privatestaticfinalintBUFFER_SIZE=1024;

privateStringDB_PATH;

privateContextmContext;



//初始化

publicDBManager(Contextcontext){

this.mContext=context;

DB_PATH=File.separator+"data"

+Environment.getDataDirectory().getAbsolutePath()+File.separator

+context.getPackageName()+File.separator+"databases"+File.separator;

}

//保存数据库到本地

@SuppressWarnings("ResultOfMethodCallIgnored")

publicvoidcopyDBFile(){

Filedir=newFile(DB_PATH);

if(!dir.exists()){

dir.mkdirs();

}

FiledbFile=newFile(DB_PATH+DB_NAME);

if(!dbFile.exists()){

InputStreamis;

OutputStreamos;

try{

is=mContext.getResources().getAssets().open(ASSETS_NAME);

os=newFileOutputStream(dbFile);

byte[]buffer=newbyte[BUFFER_SIZE];

intlength;

while((length=is.read(buffer,0,buffer.length))>0){

os.write(buffer,0,length);

}

os.flush();

os.close();

is.close();

}catch(IOExceptione){

e.printStackTrace();

}

}

}

/

读取所有城市

@return

/

publicListgetAllCities(){

SQLiteDatabasedb=SQLiteDatabase.openOrCreateDatabase(DB_PATH+DB_NAME,null);

Cursorcursor=db.rawQuery("selectfrom"+TABLE_NAME,null);

Listresult=newArrayList<>();

Citycity;

while(cursor.moveToNext()){

Stringname=cursor.getString(cursor.getColumnIndex(NAME));

Stringpinyin=cursor.getString(cursor.getColumnIndex(PINYIN));

city=newCity(name,pinyin);

result.add(city);

}

cursor.close();

db.close();

Collections.sort(result,newCityComparator());

returnresult;

}



/

通过名字或者拼音搜索

@paramkeyword

@return

/

publicListsearchCity(finalStringkeyword){

SQLiteDatabasedb=SQLiteDatabase.openOrCreateDatabase(DB_PATH+DB_NAME,null);

Cursorcursor=db.rawQuery("selectfrom"+TABLE_NAME+"wherenamelike\"%"+keyword

+"%\"orpinyinlike\"%"+keyword+"%\"",null);

Listresult=newArrayList<>();

Citycity;

while(cursor.moveToNext()){

Stringname=cursor.getString(cursor.getColumnIndex(NAME));

Stringpinyin=cursor.getString(cursor.getColumnIndex(PINYIN));

city=newCity(name,pinyin);

result.add(city);

}

cursor.close();

db.close();

Collections.sort(result,newCityComparator());

returnresult;

}



/

a-z排序

/

privateclassCityComparatorimplementsComparator{

@Override

publicintcompare(Citylhs,Cityrhs){

Stringa=lhs.getPinyin().substring(0,1);

Stringb=rhs.getPinyin().substring(0,1);

returna.compareTo(b);

}

}

}



这个类使用SQLiteDatabase来管理数据库,同事写了一个排序类CityComparator用来对城市按照首字母进行排序



定位城市的布局:



view_locate_city.xml:




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">




style="@style/LetterIndexTextViewStyle"

android:text="@string/located_city"/>




android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="16dp"

android:background="@color/content_bg">




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">




android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:src="@mipmap/ic_locate"

tools:ignore="ContentDescription"/>




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"/>







然后是所有城市的布局:




xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="vertical"

tools:ignore="RtlHardcoded">




android:id="@+id/tv_item_city_listview_letter"

style="@style/LetterIndexTextViewStyle"

android:textSize="18sp"

android:clickable="false"/>




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"/>


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"/>

使用两个TextView一个用来显示城市的首字母,一个用来显示城市名字



上面的布局都很简单,接下来就是热门城市了:



如果我们使用gridview不做任何处理的话,最终效果是这样的:



不止在listview,gridview在其它任何可以滚动的view里都会出现这个问题,解决这个问题我们有固定的方案,那就是自定义一个gridview然后重写onMeasure方法,在onMeasure方法里,让gridview测量子view的高度,并全部显示出来。



代码其实非常简单:



publicclassWrapHeightGridViewextendsGridView{

publicWrapHeightGridView(Contextcontext){

this(context,null);

}



publicWrapHeightGridView(Contextcontext,AttributeSetattrs){

this(context,attrs,0);

}



publicWrapHeightGridView(Contextcontext,AttributeSetattrs,intdefStyleAttr){

super(context,attrs,defStyleAttr);

}



//如果把GridView放到一个垂直方向滚动的布局中,设置其高度属性为wrap_content,

//则该GridView的高度只有一行内容,其他内容通过滚动来显示。

//如果你想让该GridView的高度为所有行内容所占用的实际高度,则可以通过覆写GridView的onMeasure函数来修改布局参数

@Override

protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){

intheightSpec=MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,MeasureSpec.AT_MOST);

super.onMeasure(widthMeasureSpec,heightSpec);

}

}



这里重点理解makeMeasureSpec这个方法,publicstaticintmakeMeasureSpec(intsize,intmode)

这个是由我们给出的尺寸大小和模式生成一个包含这两个信息的int变量。这个int值有32位,其中高2位表示模式,低30位表示值。我们把Integer.MAX_VALUE>>2右移两位然后和MeasureSpec.AT_MOST合成一个新的int值。Integer.MAX_VALUE是INT类型的最大值是0xFFFFFFFF,所以这个值右移两位就表示新的合成的int值得低30位都是1。也就是说我们取最大值作为控件高度的最大值。



这样就可以了,效果:



接下来看一下adapter是怎么实现的:



CityListAdapter.java:



packagetest.study.select_city.adapter;

publicclassCityListAdapterextendsBaseAdapter{

privatestaticfinalintVIEW_TYPE_COUNT=3;



privateContextmContext;

privateLayoutInflaterinflater;

privateListmCities;

privateHashMapletterIndexes;

privateString[]sections;

privateOnCityClickListeneronCityClickListener;

privateintlocateState=LocateState.LOCATING;

privateStringlocatedCity;



publicCityListAdapter(ContextmContext,ListmCities){

this.mContext=mContext;

this.mCities=mCities;

this.inflater=LayoutInflater.from(mContext);

if(mCities==null){

mCities=newArrayList<>();

}

mCities.add(0,newCity("定位","0"));

mCities.add(1,newCity("热门","1"));

intsize=mCities.size();

letterIndexes=newHashMap<>();

sections=newString[size];

for(intindex=0;index
//当前城市拼音首字母

StringcurrentLetter=PinyinUtils.getFirstLetter(mCities.get(index).getPinyin());

//上个首字母,如果不存在设为""

StringpreviousLetter=index>=1?PinyinUtils.getFirstLetter(mCities.get(index-1).getPinyin()):"";

if(!TextUtils.equals(currentLetter,previousLetter)){

letterIndexes.put(currentLetter,index);

sections[index]=currentLetter;

}

}

}



/

更新定位状态

@paramstate

/

publicvoidupdateLocateState(intstate,Stringcity){

this.locateState=state;

this.locatedCity=city;

notifyDataSetChanged();

}



/

获取字母索引的位置

@paramletter

@return

/

publicintgetLetterPosition(Stringletter){

Integerinteger=letterIndexes.get(letter);

returninteger==null?-1:integer;

}



@Override

publicintgetViewTypeCount(){

returnVIEW_TYPE_COUNT;

}



@Override

publicintgetItemViewType(intposition){

returnposition
}



@Override

publicintgetCount(){

returnmCities==null?0:mCities.size();

}



@Override

publicCitygetItem(intposition){

returnmCities==null?null:mCities.get(position);

}



@Override

publiclonggetItemId(intposition){

returnposition;

}



@Override

publicViewgetView(finalintposition,Viewview,ViewGroupparent){

CityViewHolderholder;

intviewType=getItemViewType(position);

switch(viewType){

case0://定位

view=inflater.inflate(R.layout.view_locate_city,parent,false);

ViewGroupcontainer=(ViewGroup)view.findViewById(R.id.layout_locate);

TextViewstate=(TextView)view.findViewById(R.id.tv_located_city);

switch(locateState){

caseLocateState.LOCATING:

state.setText(mContext.getString(R.string.locating));

break;

caseLocateState.FAILED:

state.setText(R.string.located_failed);

break;

caseLocateState.SUCCESS:

state.setText(locatedCity);

break;

}

container.setOnClickListener(newView.OnClickListener(){

@Override

publicvoidonClick(Viewv){

if(locateState==LocateState.FAILED){

//重新定位

if(onCityClickListener!=null){

onCityClickListener.onLocateClick();

}

}elseif(locateState==LocateState.SUCCESS){

//返回定位城市

if(onCityClickListener!=null){

onCityClickListener.onCityClick(locatedCity);

}

}

}

});

break;

case1://热门

view=inflater.inflate(R.layout.view_hot_city,parent,false);

WrapHeightGridViewgridView=(WrapHeightGridView)view.findViewById(R.id.gridview_hot_city);

finalHotCityGridAdapterhotCityGridAdapter=newHotCityGridAdapter(mContext);

gridView.setAdapter(hotCityGridAdapter);

gridView.setOnItemClickListener(newAdapterView.OnItemClickListener(){

@Override

publicvoidonItemClick(AdapterViewparent,Viewview,intposition,longid){

if(onCityClickListener!=null){

onCityClickListener.onCityClick(hotCityGridAdapter.getItem(position));

}

}

});

break;

case2://所有

if(view==null){

view=inflater.inflate(R.layout.item_city_listview,parent,false);

holder=newCityViewHolder();

holder.letter=(TextView)view.findViewById(R.id.tv_item_city_listview_letter);

holder.www.shanxiwang.netname=(TextView)view.findViewById(R.id.tv_item_city_listview_name);

view.setTag(holder);

}else{

holder=(CityViewHolder)view.getTag();

}

if(position>=1){

finalStringcity=mCities.get(position).getName();

holder.name.setText(city);

//如果当前的item的城市的首字母和上一个城市的首字母相同,就不显示首字母否则就显示。

//这样就可以实现让所有城市根据首字母分类的效果了。

StringcurrentLetter=PinyinUtils.getFirstLetter(mCities.get(position).getPinyin());

StringpreviousLetter=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(newView.OnClickListener(){

@Override

publicvoidonClick(Viewv){

if(onCityClickListener!=null){

onCityClickListener.onCityClick(city);

}

}

});

}

break;

}

returnview;

}



publicstaticclassCityViewHolder{

TextViewletter;

TextViewname;

}



publicvoidsetOnCityClickListener(OnCityClickListenerlistener){

this.onCityClickListener=listener;

}



publicinterfaceOnCityClickListener{

voidonCityClick(Stringname);

voidonLocateClick();

}

}



Listview加载不同的布局,请参考我的另一篇博客:

最主要的就是getItemViewType方法。

在adapter里使用LocateState类来标示不同的定位状态。代码比较多,但是都很好理解就不再解释了(其实是懒。。。)



gridview热门城市的item布局



就是一个Textview而已



gridview热门城市的adapter



请参考demo,就是一个简单的adapter



然后是侧边导航栏的实现:



写一个自定义view,

SideBar.java



publicclassSideBarextendsView{

privatestaticfinalString[]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"};

privateintchoose=-1;

privatePaintpaint=newPaint();

privatebooleanshowBg=false;

privateOnLetterChangedListeneronLetterChangedListener;

privateTextViewoverlay;



publicSideBar(Contextcontext,AttributeSetattrs,intdefStyle){

super(context,attrs,defStyle);

}



publicSideBar(Contextcontext,AttributeSetattrs){

super(context,attrs);

}



publicSideBar(Contextcontext){

super(context);

}



/

设置悬浮的textview

@paramoverlay

/

publicvoidsetOverlay(TextViewoverlay){

this.overlay=overlay;

}



@SuppressWarnings("deprecation")

@Override

protectedvoidonDraw(Canvascanvas){

super.onDraw(canvas);

if(showBg){

canvas.drawColor(Color.TRANSPARENT);

}



intheight=getHeight();

intwidth=getWidth();

intsingleHeight=height/b.length;//单行字母高度

for(inti=0;i
paint.setTextSize(getResources().getDimension(R.dimen.side_letter_bar_letter_size));

paint.setColor(getResources().getColor(R.color.deep_blue));

paint.setAntiAlias(true);

if(i==choose){

paint.setColor(getResources().getColor(R.color.gray_deep));

//paint.setFakeBoldText(true);//加粗

}

floatxPos=width/2-paint.measureText(b[i])/2;

floatyPos=singleHeighti+singleHeight;

canvas.drawText(b[i],xPos,yPos,paint);

paint.reset();

}



}



@Override

publicbooleandispatchTouchEvent(MotionEventevent){

finalintaction=event.getAction();

finalfloaty=event.getY();

finalintoldChoose=choose;

finalOnLetterChangedListenerlistener=onLetterChangedListener;

finalintc=(int)(y/getHeight()b.length);//获取字母的index



switch(action){

caseMotionEvent.ACTION_DOWN:

showBg=true;

if(oldChoose!=c&&listener!=null){

if(c>=0&&c
listener.onLetterChanged(b[c]);

choose=c;

invalidate();

if(overlay!=null){

overlay.setVisibility(VISIBLE);

overlay.setText(b[c]);

}

}

}



break;

caseMotionEvent.ACTION_MOVE:

if(oldChoose!=c&&listener!=null){

if(c>=0&&c
listener.onLetterChanged(b[c]);

choose=c;

invalidate();

if(overlay!=null){

overlay.setVisibility(VISIBLE);

overlay.setText(b[c]);

}

}

}

break;

caseMotionEvent.ACTION_UP:

showBg=false;

choose=-1;

invalidate();

if(overlay!=null){

overlay.setVisibility(GONE);

}

break;

}

returntrue;

}



@Override

publicbooleanonTouchEvent(MotionEventevent){

returnsuper.onTouchEvent(event);

}



publicvoidsetOnLetterChangedListener(OnLetterChangedListeneronLetterChangedListener){

this.onLetterChangedListener=onLetterChangedListener;

}



publicinterfaceOnLetterChangedListener{

voidonLetterChanged(Stringletter);

}

}



我们绘制了slidebar,并且重写了它的ontouch事件。当手指摁下和滑动的时候在布局中央显示当前城市的首字母,显示首字母的控件是一个Textview,这个Textview通过setOverlay方法传递进来。抬起的时候不显示。



现在的效果是这样:



UI总算实现的差不多了,接下来还有城市定位功能,搜索功能以及右侧导航功能的要实现。



搜索功能:



主要就是给edittext设置一个TextChangedListener,让它根据输入去数据库中查找数据并将数据传递给adapter,然后通过notifyDataSetChanged方法来更新UI。



代码请参照demo



右侧导航功能



当我们的手指在slidebar上滑动的时候会触发它的ontouch事件,然后通过回调将当前的字母传递回来,接着我们将点击的字母传递给mCityAdapter的getLetterPosition方法,来得到当前字母的位置,并通过mListview.setSelection(position);方法来改变listview的显示位置



代码参考demo



定位功能



这个需要接入百度地图的sdk



这个大家根据百度地图开发者中心的手册一点点来就可以了。

献花(0)
+1
(本文系网络学习天...首藏)