添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
精彩文章免费看

Android基于高德地图实现搜索框的自动输入提示功能

最近公司项目中一直在搞地图开发,今天产品经理就给我布置了一些(无法想象)任务,其中一个就是实现地点搜索输入框的自动输入提示功能。拿到任务肯定想讨价还价一番,但是想到以前也写过,就不再负隅顽抗了。
以前在学校的时候实现过类似功能,是使用高德自带的InputtipsListener来实现的,想了解可以看看: 文章传送点 ,这里就不详细介绍了。作为一名头脑发热的开发者,肯定不能安于现状,这里主要介绍其他两种方式 - poi实现和http请求接口实现,不管能不能成功,试了再说,撸起袖子就是干。先看看最终的效果:

关键词搜索

做之前先分析一下功能需求,首先输入框中要添加内容清除的icon,当输入框有文字时,需要显示,为空时隐藏;接着,需要实现地址搜索功能并通过listview展示结果;最后需要实现展示搜索历史的功能。好的,那么下面我们来一步步实现。

其实,实现效果中的输入框并不难,只需要三个东西就够了:LinearLayout,EditText,ImageView。直接上代码吧,上了代码你就知道它到底有多简单了:

<LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="36dp"
            android:layout_weight="1"
            android:layout_marginLeft="20dp"
            android:background="@drawable/search_view_bg"
            android:orientation="horizontal"
            android:gravity="center_vertical">
            <EditText
                android:id="@+id/search_edit_text"
                android:layout_width="wrap_content"
                android:layout_height="36dp"
                android:hint="@string/input_cross_location"
                android:textColorHint="#9B9B9B"
                android:textSize="12sp"
                android:maxLines="1"
                android:layout_weight="1"
                android:paddingBottom="10dp"
                android:paddingTop="10dp"
                android:paddingLeft="10dp"
                android:background="@drawable/search_edit_bg"
                android:drawableLeft="@mipmap/icon_edit_search"
                android:drawablePadding="16dp"/>
            <ImageView
                android:id="@+id/search_edit_delete"
                android:layout_width="12dp"
                android:layout_height="12dp"
                android:layout_marginLeft="5dp"
                android:layout_marginRight="8dp"
                android:visibility="gone"
                android:src="@mipmap/iocn_search_cancel"/>
        </LinearLayout>

没错,这里为EditText父容器LinearLayout设置背景,然后EditText设置同样的背景,只不过需要将右边的圆角效果去掉,达到预期效果。也即是说,我们的输入框相当于是LinearLayout,里面包含了edittext和删除图标imageview,来看看drawable的代码吧:

search_view_bg:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="false">
        <shape xmlns:android="http://schemas.android.com/apk/res/android">
            <!--<solid android:color="#F4F4F4" />-->
            <corners android:radius="3dp"/>
            <solid android:color="#F3F3F3"/>
            <!--<stroke android:color="#ffececec" android:width="1dp"/>-->
        </shape>
    </item>
    <item android:state_window_focused="true">
        <shape>
            <corners android:radius="3dp"/>
            <!--<stroke android:color="#ececec" android:width="1dp" />-->
            <solid android:color="#F3F3F3"/>
            <!--<solid android:color="#F4F4F4" />-->
        </shape>
    </item>
</selector>

search_edit_bg:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="false">
        <shape xmlns:android="http://schemas.android.com/apk/res/android">
            <!--<solid android:color="#F4F4F4" />-->
            <corners
                android:topLeftRadius="3dp"
                android:bottomLeftRadius="3dp"/>
            <solid android:color="#F3F3F3"/>
            <!--<stroke android:color="#ffececec" android:width="1dp"/>-->
        </shape>
    </item>
    <item android:state_window_focused="true">
        <shape>
            <corners
                android:topLeftRadius="3dp"
                android:bottomLeftRadius="3dp"/>
            <!--<stroke android:color="#ececec" android:width="1dp" />-->
            <solid android:color="#F3F3F3"/>
            <!--<solid android:color="#F4F4F4" />-->
        </shape>
    </item>
</selector>

ok,这就实现了最终的输入框UI,当然,你可以使用其他方式实现,比如自定义view,第三方开源等等,但我觉得这完全满足我们的需求,而且简单,不是吗?接下来,我们需要通过监听EditText的变化来实现搜索框中删除的变化,代码如下:

    @Bind(R.id.search_edit_text)
    EditText inputText;
    @Bind(R.id.search_edit_delete)
    ImageView buttonDelete;
    ......
    buttonDelete.setOnClickListener(this);
    inputText.addTextChangedListener(this);
    ......
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        if(charSequence!=null){
             buttonDelete.setVisibility(View.VISIBLE);
        }else {
             buttonDelete.setVisibility(View.GONE);
    @Override
    public void afterTextChanged(Editable editable) {

代码比较简单,就不解释了,不理解这个方法的可以谷歌一下,我们接着往下看。

用过高德地图api的开发者应该都知道里面有个常用的功能:POI搜索.高德提供了千万级别的 POI(Point of Interest,兴趣点)。在地图表达中,一个 POI 可代表一栋大厦、一家商铺、一处景点等等。通过POI搜索,完成找餐馆、找景点、找厕所等等的功能。如果我们的需求是获取周围兴趣点,那么搜索输入提示只要显示兴趣点就可以了。
  下面我们来依次通过两种方法来实现快捷输入提示功能:

  • POI搜索实现:
  • 话不多说,直接上代码:

    //方法一:使用poi搜索接口方法
        private PoiResult poiResult; // poi返回的结果
        private int currentPage = 0;// 当前页面,从0开始计数
        private PoiSearch.Query query;// Poi查询条件类
        private LatLonPoint latLonPoint;
        private PoiSearch poiSearch;
        private List<PoiItem> poiItems;// poi数据
        private String keyWord;
        private CommonAdapter adapter;
        private final int ADDRESS_LOCATION_GET = 3242;
        private String POI_SEARCH_TYPE = "汽车服务|汽车销售|" +
                "//汽车维修|摩托车服务|餐饮服务|购物服务|生活服务|体育休闲服务|医疗保健服务|" +
                "//住宿服务|风景名胜|商务住宅|政府机构及社会团体|科教文化服务|交通设施服务|" +
                "//金融保险服务|公司企业|道路附属设施|地名地址信息|公共设施";
        ......
         * 开始进行poi搜索
        protected void doSearchQuery() {
            latLonPoint = new LatLonPoint(MyApplication.mapLocation.getLatitude(), MyApplication.mapLocation.getLongitude());// 116.472995,39.993743
            keyWord = inputText.getText().toString().trim();
            currentPage = 0;
            //keyWord表示搜索字符串,
            //第二个参数表示POI搜索类型,二者选填其一,选用POI搜索类型时建议填写类型代码,码表可以参考下方(而非文字)
            //cityCode表示POI搜索区域,可以是城市编码也可以是城市名称,也可以传空字符串,空字符串代表全国在全国范围内进行搜索
            query = new PoiSearch.Query(keyWord, POI_SEARCH_TYPE, "");
            query.setPageSize(30);// 设置每页最多返回多少条poiItem
            query.setPageNum(currentPage);// 设置查第一页
            if (latLonPoint != null) {
                poiSearch = new PoiSearch(this, query);
                poiSearch.setOnPoiSearchListener(this);
                poiSearch.setBound(new PoiSearch.SearchBound(latLonPoint, 3000, true));//设置搜索范围
                poiSearch.searchPOIAsyn();// 异步搜索
        ......
        @Override
        public void onPoiSearched(PoiResult result, int code) {
            //DialogUtils.dismissProgressDialog();
            if (code == AMapException.CODE_AMAP_SUCCESS) {
                if (result != null && result.getQuery() != null) {// 搜索poi的结果
                    loge("搜索的code为===="+code+", result数量=="+result.getPois().size());
                    if (result.getQuery().equals(query)) {// 是否是同一次搜索
                        poiResult = result;
                        loge("搜索的code为===="+code+", result数量=="+poiResult.getPois().size());
                        List<SuggestionCity> suggestionCities = poiResult.getSearchSuggestionCitys();// 当搜索不到poiitem数据时,会返回含有搜索关键字的城市信息
                        if (poiItems != null && poiItems.size() > 0) {
                            poiItems.clear();
                            if (adapter != null) {
                                adapter.notifyDataSetChanged();
                        poiItems = poiResult.getPois();// 取得第一页的poiitem数据,页数从数字0开始
                    //通过listview显示搜索结果的操作省略
                    ......
                } else {
                    loge("没有搜索结果");
                    toast(getString(R.string.search_no_result));
                    empty_view.setText(getString(R.string.search_no_result));
            } else {
                loge("搜索出现错误");
                toast(getString(R.string.search_error));
                empty_view.setText(getString(R.string.search_error));
        @Override
        public void onPoiItemSearched(PoiItem poiItem, int i) {
    

    注释都比较清楚,大家理解起来应该也不难,具体用法可以参考高德官方文档,可以直接在onTextChangeed()方法中判断是否有内容来调用doSearchQuery()方法即可。

  • 通过实时访问http接口实现:
    除了以上方法实现,还可以用高德提供的web端API接口实现功能,详情见高德web服务开发文档。我们可以直接通过请求高德为我们提供的搜索url接口来访问并获取数据,输入提示API服务地址为:
    http://restapi.amap.com/v3/assistant/inputtips?
    需要我们填充相应的字段,如key,keyword等,具体介绍看官方文档就可以了,大波代码来袭:
  •     //方法二:使用http请求返回搜索结果
        private List<POISearchResultBean.Tips> tipsList;
        private POISearchResultBean resultBean;
        private String locationString;
        private String lon;
        private String lat;
        private final int SEARCH_OK = 3266;
        @Bind(R.id.search_result_listview)
        ListView resultListView;
        .......
        private MapSerchActivity.MyWeakReferenceHandler handler = new MapSerchActivity.MyWeakReferenceHandler(this) {
            @Override
            public void handleMessage(Message msg, Activity weakReferenceActivity) {
                if (msg.what == ADDRESS_LOCATION_GET) {
                    if (tipsList != null && tipsList.size() > 0) {
                        if (adapter == null && resultListView != null) {
                            //wrong
                            resultListView.setAdapter(adapter = new CommonAdapter<POISearchResultBean.Tips>(SearchAddressActivity.this, tipsList, R.layout.search_result_item) {
                                @Override
                                public void convert(ViewHolder helper, final POISearchResultBean.Tips item) {
                                    helper.setText(R.id.search_result_item_address_name, item.getName());
                                    helper.setText(R.id.search_result_item_address_detail, item.getDistrict()+item.getAddress());
                                    helper.getView(R.id.search_result_item_address_layout).setOnClickListener(new View.OnClickListener() {
                                        @Override
                                        public void onClick(View v) {
                                            loge("点击了item");
                                            toast(item.getName());
                                            boolean hasData = hasData(item.getName());
                                            if (!hasData) {
                                                insertData(item.getName());
                                                //queryData("");
                                            locationString = item.getLocation();
                                            lon = locationString.substring(0,locationString.indexOf(","));
                                            lat = locationString.substring(locationString.indexOf(",")+1,locationString.length());
                                            loge("经纬度信息为==="+lon+","+lat);
                                            Intent intent = new Intent();
                                            intent.putExtra("location_lon",lon);
                                            intent.putExtra("location_lat",lat);
                                            setResult(SEARCH_OK, intent);
                                            finish();
                        } else {
                            adapter = null;
                            resultListView.setAdapter(adapter = new CommonAdapter<POISearchResultBean.Tips>(SearchAddressActivity.this, tipsList, R.layout.search_result_item) {
                                @Override
                                public void convert(ViewHolder helper, final POISearchResultBean.Tips item) {
                                    helper.setText(R.id.search_result_item_address_name, item.getName());
                                    helper.setText(R.id.search_result_item_address_detail, item.getDistrict()+item.getAddress());
                                    helper.getView(R.id.search_result_item_address_layout).setOnClickListener(new View.OnClickListener() {
                                        @Override
                                        public void onClick(View v) {
    //                                        loge("点击了item");
                                            loge("点击了item");
                                            toast(item.getName());
                                            boolean hasData = hasData(item.getName());
                                            if (!hasData) {
                                                insertData(item.getName());
                                                //queryData("");
                                            locationString = item.getLocation();
                                            lon = locationString.substring(0,locationString.indexOf(","));
                                            lat = locationString.substring(locationString.indexOf(",")+1,locationString.length());
                                            loge("经纬度信息为==="+lon+"====="+lat);
                                            Intent intent = new Intent();
                                            intent.putExtra("location_lon",lon);
                                            intent.putExtra("location_lat",lat);
                                            setResult(SEARCH_OK, intent);
                                            finish();
        .......
         * by moos on 2017/09/11
         * func:http请求返回关键词搜索结果
         * 请求路径范例:http://restapi.amap.com/v3/assistant/inputtips?key=您的key&keywords=肯德基&types=050301&location=116.481488,39.990464&city=北京&datatype=all
        private void searchAddressByHttp(String keyWord){
            //DialogUtils.createProgressDialog(SearchAddressActivity.this,"Searching...");
            OkHttpUtils
                    .get()
                    .url(HttpAPI.AMAP_POI_SEARCH_URL + "key="+Const.amap_poi_search_key+"&keywords="+keyWord)
                    .build()
                    .execute(new StringCallback() {
                        @Override
                        public void onError(Call call, Exception e, int id) {
                            loge("获取http poi搜索结果失败=" + e.getMessage());
                            //DialogUtils.dismissProgressDialog();
                            Toast.makeText(SearchAddressActivity.this, getString(R.string.act_qr_code_fail), Toast.LENGTH_LONG).show();
                        @Override
                        public void onResponse(String response, int id) {
                            Logger.e("获取http poi搜索结果 =" + response);
                            resultBean = JSONObject.parseObject(response, POISearchResultBean.class);
                            if (resultBean.getStatus()==1) {
                                //处理和显示搜索数据列表
                                if (tipsList != null && tipsList.size() > 0) {
                                    tipsList.clear();
                                    if (adapter != null) {
                                        adapter.notifyDataSetChanged();
                                tipsList = resultBean.getTips();
                                Message message = Message.obtain(handler);
                                message.what = ADDRESS_LOCATION_GET;
                                handler.sendMessage(message);
    

    通过okhttp请求网络接口有很多大神封装好的工具库,这里我使用的鸿神的okHttpUtils,大家可以根据自己的需要来选择。同时,这里使用了CommonAdapter来作为listview的适配器,同样是鸿神的杰作,如果你对它不熟悉,建议去看一下这篇文章:打造listview万能适配器。其他的就没什么难点了,关键还是靠自己研究和练习一下了。

    最后,让我们来看看如何实现展示搜索历史的功能吧。先分析一下需求:首先进入到搜索界面要展示搜索历史列表,然后可以点击列表下方的清空历史来清除数据,接着,当我们搜索地名并选中时,自动存入搜索历史。其实,说到底,就是两个小功能,数据存储和数据展示,下面依次来探讨如何实现。

  • 搜索历史数据的存储:
  • 一般地,我们会将搜索的历史数据保存在本地。常用的两种方式分别为数据库存储和sp(SharedPreference)存储,两种方式都可以实现我们的需求,这里我才用的是数据库,有时间的话大家可以试试sp存储方式。这里不研究数据库的基本用法比较简单,就一笔带过了,直接上代码:

    首先是创建数据库:

    * Created by moos on 17/9/11. public class SearchHistorySQLiteHelper extends SQLiteOpenHelper { private static String name = "search.db"; private static Integer version = 1; public SearchHistorySQLiteHelper(Context context) { super(context, name, null, version); @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { sqLiteDatabase.execSQL("create table history(id integer primary key autoincrement,name varchar(200))"); @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    具体操作:

    //历史搜索功能
        private SearchHistorySQLiteHelper helper = new SearchHistorySQLiteHelper(this);
        private SQLiteDatabase db;
        private BaseAdapter baseAdapter;
        @Bind(R.id.search_history_listview)
        ListView search_history_listView;
        @Bind(R.id.search_history_view)
        LinearLayout search_history_view;
        ......
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_search_address);
            ButterKnife.bind(this);
            initView();
        private void initView(){
            buttonCancel.setOnClickListener(this);
            buttonDelete.setOnClickListener(this);
            inputText.addTextChangedListener(this);
            search_history_listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    loge("点击了第"+position+"个搜索历史item");
                    TextView textView = (TextView) view.findViewById(R.id.search_history_item_address_name);
                    String name = textView.getText().toString();
                    inputText.setText(name);
                    Toast.makeText(SearchAddressActivity.this, name, Toast.LENGTH_SHORT).show();
            // 第一次进入查询所有的历史记录
            queryData("");
        ......
        //采用本地数据库存储
         * 插入数据
        private void insertData(String tempName) {
            db = helper.getWritableDatabase();
            db.execSQL("insert into history(name) values('" + tempName + "')");
            db.close();
         * 模糊查询数据
        private void queryData(String tempName) {
            Cursor cursor = helper.getReadableDatabase().rawQuery(
                    "select id as _id,name from history where name like '%" + tempName + "%' order by id desc ", null);
            // 创建adapter适配器对象
            baseAdapter = new SimpleCursorAdapter(this, R.layout.search_history_item, cursor, new String[] { "name" },
                    new int[] { R.id.search_history_item_address_name }, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
            //添加footerView
            View footerView = LayoutInflater.from(this).inflate(R.layout.delete_search_history_bt,null);
            search_history_listView.addFooterView(footerView,null,false);
            footerView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    deleteData();
                    toast("清除成功");
            // 设置适配器
            search_history_listView.setAdapter(baseAdapter);
            baseAdapter.notifyDataSetChanged();
            if(baseAdapter.getCount()==0){
                //无历史搜索记录
                search_history_view.setVisibility(View.GONE);
         * 检查数据库中是否已经有该条记录
        private boolean hasData(String tempName) {
            Cursor cursor = helper.getReadableDatabase().rawQuery(
                    "select id as _id,name from history where name =?", new String[]{tempName});
            //判断是否有下一个
            return cursor.moveToNext();
         * 清空数据
        private void deleteData() {
            db = helper.getWritableDatabase();
            db.execSQL("delete from history");
            db.close();
            loge("搜索历史数据删除成功");
            queryData("");
    

    数据库的操作网上很多教程和文章,这里就不多加解释,主要说一下逻辑吧。首先,我们输入关键词搜索到相应的目标地点后,点击回调中,即插入一条该地名的数据。当然,为了防止重复,我们需要判断一下数据库中是否已经存在该数据。我们刚进入搜索界面的时候,需要查询数据库中所有的数据并展示。

  • 搜索历史数据的展示和删除:
  • 展示的话没什么太大的问题,一般采用listview展示并为item加上点击事件就OK了,主要是要设置好展示数据和刷新数据的逻辑。这里主要提一下如何实现下方"清除搜索历史"的友好展示以及逻辑。

    如果有人问你如何快速实现listview下面的button依附效果,你会怎么回答?常见的回答一般有两种:

    1.在listview下面放一个button,然后外面套一层ScrollerView

    2.使用listview的addFooterView()给其添加底部布局

    虽然两种方式很容易想到,但是对于我们这些新手来说,动手实现起来多少有些弯弯绕绕。比如第一种方式,我们需要考虑如何处理嵌套滑动的问题,至于第二种,无非是研究listview之footerview的用法,当然,抛开各自的难点,第二种方式无非更加优雅一些,所以,这里只讨论如何使用该方式实现。
    首先看一下footerview的布局:

    search_history_item:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal">
        <TextView
            android:id="@+id/item_search_history_delete"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:gravity="center"
            android:text="清楚搜索历史"
            android:textSize="12sp"
            android:layout_marginBottom="15dp"
            android:textColor="#828282"/>
    </LinearLayout>
    

    listview添加footerview比较简单,只通过简单的两行代码:

    //添加footerView
    View footerView = LayoutInflater.from(this).inflate(R.layout.delete_search_history_bt,null);
    search_history_listView.addFooterView(footerView,null,false);
    

    便可以实现,但是footerview的点击事件如何获取呢?很多人说直接用onItenClickListener()呀,但是,大家可以通过log或者toast看看,点击footerview是否真的响应了。答案是 - 并没有。我们应该尽量避免在onITemClickListener回调方法中实现footerview点击事件,因为position并没有变化,上限依旧是原来的adapter.getCount()。我们可以先禁止footerview在item中的点击响应,即addFooterView()方法第三个参数设为false,然后给footerView单独设置点击事件:

    footerView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    deleteData();
                    toast("清除成功");
    

    到这里,我们就完整实现了地图的搜索功能了,虽然实现的方式比较简单,但还是学到一些东西的。后面有时间会将该部分功能做个demo单独分享出来。另外大家如果有什么问题和优化建议,欢迎留言反馈,不胜感激😆。

    最后,请教一下大家mac如何录制gif呢,为什么我用licecap录制出来的是黑屏呢(⊙﹏⊙).

    最后编辑于:2017-12-10 16:47