欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义View实现搜索框(SearchView)功能

程序员文章站 2022-05-26 22:52:20
概述 在android开发中,当系统数据项比较多时,常常会在app添加搜索功能,方便用户能快速获得需要的数据。搜索栏对于我们并不陌生,在许多app都能见到它,比如豌豆荚...

概述

在android开发中,当系统数据项比较多时,常常会在app添加搜索功能,方便用户能快速获得需要的数据。搜索栏对于我们并不陌生,在许多app都能见到它,比如豌豆荚

Android自定义View实现搜索框(SearchView)功能

在某些情况下,我们希望我们的自动补全信息可以不只是纯文本,还可以像豌豆荚这样,能显示相应的图片和其他数据信息,因此android给我们提供的autocompletetextview往往就不够用,在大多情况下我们都需要自己去实现搜索框。

分析

根据上面这张图,简单分析一下自定义搜索框的结构与功能,有
1. 搜索界面大致由三部门组成,如图:输入框+(自动补全)提示框+结果列表。
2. 提示框的数据与输入框输入的文本是实时联动的,而结果列表只有在每次进行搜索操作时才会更新数据

3. 输入框的ui应是动态的,即ui随着输入的文本的改变而改变,如:在未输入文本时,清除按钮Android自定义View实现搜索框(SearchView)功能应该是隐藏的;只有当框中有文本时才会显示。
4. 软键盘也应该是动态的,如完成搜索时应自动隐藏。
5. 选择提示框的选项会自动补全输入框,且自动进行搜索
6. (external)有热门搜索推荐/记录搜索记录的功能——热门搜索推荐列表只在刚要进行搜索的时候弹出,即未输入文本时,可供用户选择。

根据上面的分析,我们认为一个搜索框应该包含输入框和提示框两个部分。搜索框可以设置一个回调监听接口,当需要进行搜索操作时,调用监听者的search()方法,从而实现具体的搜索操作以及结果列表的数据联动。

演示demo

Android自定义View实现搜索框(SearchView)功能

注意:

1. 这里,博主图方便没有模拟太多数据,而且提示框和热搜列表也都只是使用string类型的数据,各位看官们可以根据自身需要去设置item_layout和相应的adapter。
2. 由于个人习惯,博主在这个demo中使用了通用适配器,所以生成和设置adapter的代码比较简略,看官们可以根据传统的viewholder模式打造自己的adapter。或者学习一下通用适配器的打造。可以参考这里(鸿神博客again)学习一下通用适配器的打造,在我的源码里面也有对应的源码。

实现

好了,说了那么多,开始来看代码吧

先看searchview的布局文件 search_layout.xml

<?xml version="1.0" encoding="utf-8"?> 
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" 
       android:background="#eee" 
       android:layout_width="match_parent" 
       android:layout_height="wrap_content" 
       android:orientation="vertical"> 
 
  <linearlayout 
    android:background="#eb4f38" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:orientation="horizontal"> 
 
 
    <framelayout 
 
      android:layout_weight="1" 
      android:layout_width="0dp" 
      android:layout_height="wrap_content"> 
 
      <edittext 
        android:id="@+id/search_et_input" 
        android:layout_gravity="center_vertical" 
        android:layout_margin="10dp" 
        android:drawableleft="@drawable/search_icon" 
        android:drawablepadding="5dp" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:background="@drawable/search_edittext_shape" 
        android:textsize="16sp" 
        android:imeoptions="actionsearch" 
        android:inputtype="text" 
        android:hint="请输入关键字"/> 
 
      <imageview 
        android:visibility="gone" 
        android:layout_marginright="20dp" 
        android:src="@drawable/iv_delete_bg" 
        android:id="@+id/search_iv_delete" 
        android:layout_gravity="right|center_vertical" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"/> 
    </framelayout> 
 
    <button 
      android:id="@+id/search_btn_back" 
      android:layout_marginright="10dp" 
      android:layout_margintop="10dp" 
      android:layout_marginbottom="10dp" 
      android:layout_gravity="center_vertical" 
      android:background="@drawable/btn_search_bg" 
      android:layout_width="@dimen/btn_width" 
      android:layout_height="@dimen/btn_height" 
      android:text="返回" 
      android:textcolor="@color/color_white"/> 
  </linearlayout> 
 
  <listview 
    android:visibility="gone" 
    android:id="@+id/search_lv_tips" 
    android:background="@drawable/lv_search_tips_bg" 
    android:layout_marginleft="20dp" 
    android:layout_marginright="20dp" 
    android:layout_marginbottom="10dp" 
    android:layout_width="match_parent" 
    android:layout_height="200dp"> 
  </listview> 
</linearlayout> 

注意:demo中颜色什么的都直接用的rgb 值去设置,在实际开发时,需要把它们都统一管理到values目录下 。

比较简单,需要注意的是edittext的这个属性

android:imeoptions="actionsearch"
就是把enter键设置为search键,并把点击enter键的动作设为actionsearch,这样既可在代码中监听何时按下search键

没什么说的,bg属性可以直接看看源码。接下来看模拟的bean类,这里直接就叫bean.java

public class bean { 
 
  private int iconid; 
  private string title; 
  private string content; 
  private string comments; 
 
  public bean(int iconid, string title, string content, string comments) { 
    this.iconid = iconid; 
    this.title = title; 
    this.content = content; 
    this.comments = comments; 
  } 
 
  public int geticonid() { 
    return iconid; 
  } 
 
  public void seticonid(int iconid) { 
    this.iconid = iconid; 
  } 
 
  public string gettitle() { 
    return title; 
  } 
 
  public void settitle(string title) { 
    this.title = title; 
  } 
 
  public string getcontent() { 
    return content; 
  } 
 
  public void setcontent(string content) { 
    this.content = content; 
  } 
 
  public string getcomments() { 
    return comments; 
  } 
 
  public void setcomments(string comments) { 
    this.comments = comments; 
  } 
} 

接着看主角searchview.java

public class searchview extends linearlayout implements view.onclicklistener { 
 
  /** 
   * 输入框 
   */ 
  private edittext etinput; 
 
  /** 
   * 删除键 
   */ 
  private imageview ivdelete; 
 
  /** 
   * 返回按钮 
   */ 
  private button btnback; 
 
  /** 
   * 上下文对象 
   */ 
  private context mcontext; 
 
  /** 
   * 弹出列表 
   */ 
  private listview lvtips; 
 
  /** 
   * 提示adapter (推荐adapter) 
   */ 
  private arrayadapter<string> mhintadapter; 
 
  /** 
   * 自动补全adapter 只显示名字 
   */ 
  private arrayadapter<string> mautocompleteadapter; 
 
  /** 
   * 搜索回调接口 
   */ 
  private searchviewlistener mlistener; 
 
  /** 
   * 设置搜索回调接口 
   * 
   * @param listener 监听者 
   */ 
  public void setsearchviewlistener(searchviewlistener listener) { 
    mlistener = listener; 
  } 
 
  public searchview(context context, attributeset attrs) { 
    super(context, attrs); 
    mcontext = context; 
    layoutinflater.from(context).inflate(r.layout.search_layout, this); 
    initviews(); 
  } 
 
  private void initviews() { 
    etinput = (edittext) findviewbyid(r.id.search_et_input); 
    ivdelete = (imageview) findviewbyid(r.id.search_iv_delete); 
    btnback = (button) findviewbyid(r.id.search_btn_back); 
    lvtips = (listview) findviewbyid(r.id.search_lv_tips); 
 
    lvtips.setonitemclicklistener(new adapterview.onitemclicklistener() { 
      @override 
      public void onitemclick(adapterview<?> adapterview, view view, int i, long l) { 
        //set edit text 
        string text = lvtips.getadapter().getitem(i).tostring(); 
        etinput.settext(text); 
        etinput.setselection(text.length()); 
        //hint list view gone and result list view show 
        lvtips.setvisibility(view.gone); 
        notifystartsearching(text); 
      } 
    }); 
 
    ivdelete.setonclicklistener(this); 
    btnback.setonclicklistener(this); 
 
    etinput.addtextchangedlistener(new editchangedlistener()); 
    etinput.setonclicklistener(this); 
    etinput.setoneditoractionlistener(new textview.oneditoractionlistener() { 
      @override 
      public boolean oneditoraction(textview textview, int actionid, keyevent keyevent) { 
        if (actionid == editorinfo.ime_action_search) { 
          lvtips.setvisibility(gone); 
          notifystartsearching(etinput.gettext().tostring()); 
        } 
        return true; 
      } 
    }); 
  } 
 
  /** 
   * 通知监听者 进行搜索操作 
   * @param text 
   */ 
  private void notifystartsearching(string text){ 
    if (mlistener != null) { 
      mlistener.onsearch(etinput.gettext().tostring()); 
    } 
    //隐藏软键盘 
    inputmethodmanager imm = (inputmethodmanager) mcontext.getsystemservice(context.input_method_service); 
    imm.togglesoftinput(0, inputmethodmanager.hide_not_always); 
  } 
 
  /** 
   * 设置热搜版提示 adapter 
   */ 
  public void settipshintadapter(arrayadapter<string> adapter) { 
    this.mhintadapter = adapter; 
    if (lvtips.getadapter() == null) { 
      lvtips.setadapter(mhintadapter); 
    } 
  } 
 
  /** 
   * 设置自动补全adapter 
   */ 
  public void setautocompleteadapter(arrayadapter<string> adapter) { 
    this.mautocompleteadapter = adapter; 
  } 
 
  private class editchangedlistener implements textwatcher { 
    @override 
    public void beforetextchanged(charsequence charsequence, int i, int i2, int i3) { 
 
    } 
 
    @override 
    public void ontextchanged(charsequence charsequence, int i, int i2, int i3) { 
      if (!"".equals(charsequence.tostring())) { 
        ivdelete.setvisibility(visible); 
        lvtips.setvisibility(visible); 
        if (mautocompleteadapter != null && lvtips.getadapter() != mautocompleteadapter) { 
          lvtips.setadapter(mautocompleteadapter); 
        } 
        //更新autocomplete数据 
        if (mlistener != null) { 
          mlistener.onrefreshautocomplete(charsequence + ""); 
        } 
      } else { 
        ivdelete.setvisibility(gone); 
        if (mhintadapter != null) { 
          lvtips.setadapter(mhintadapter); 
        } 
        lvtips.setvisibility(gone); 
      } 
 
    } 
 
    @override 
    public void aftertextchanged(editable editable) { 
    } 
  } 
 
  @override 
  public void onclick(view view) { 
    switch (view.getid()) { 
      case r.id.search_et_input: 
        lvtips.setvisibility(visible); 
        break; 
      case r.id.search_iv_delete: 
        etinput.settext(""); 
        ivdelete.setvisibility(gone); 
        break; 
      case r.id.search_btn_back: 
        ((activity) mcontext).finish(); 
        break; 
    } 
  } 
 
  /** 
   * search view回调方法 
   */ 
  public interface searchviewlistener { 
 
    /** 
     * 更新自动补全内容 
     * 
     * @param text 传入补全后的文本 
     */ 
    void onrefreshautocomplete(string text); 
 
    /** 
     * 开始搜索 
     * 
     * @param text 传入输入框的文本 
     */ 
    void onsearch(string text); 
 
//    /** 
//     * 提示列表项点击时回调方法 (提示/自动补全) 
//     */ 
//    void ontipsitemclick(string text); 
  } 
 
} 

搜索框主要包含两个结构:输入栏+弹出框(自动补全或热门搜素推荐)。

代码不多,实现很简单,主要是需要给edittext(输入框)设置点击监听和文本改变监听,有以下几点:
1. 当输入框没有文本时,点击输入框,显示热门搜索列表框。
2. 当输入框有文本时,点击输入框,应显示自动补全列表框。
3. 当输入框的文本发生改变时,需要更新自动补全列表框的数据。由于这些数据应该是在外部(调用者)中获得的,所以可以通过接口回调的形式,当需要更新时,通知监听者更新数据。
4. 当输入框的文本从空”“变换到非空时,即有字符时,界面应显示自动补全框,隐藏热门搜索框。
5. 当输入框的文本从非空变为空时,系统应隐藏自动补全框和热门搜索框。
6. 需要监听是否按下search键(enter),按下时通知监听者执行search操作

结合以上6点和在上文分析过的内容,就能很轻松地实现该view。

之后来看看搜索界面的布局文activity_main.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" 
       tools:context=".mainactivity" 
       android:orientation="vertical"> 
 
  <com.yetwish.customsearchdemo.activity.widge.searchview 
    android:id="@+id/main_search_layout" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content"> 
  </com.yetwish.customsearchdemo.activity.widge.searchview> 
 
  <listview 
    android:visibility="gone" 
    android:id="@+id/main_lv_search_results" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content"> 
 
  </listview> 
</linearlayout> 

就是一个searchview加上一个结果列表,这些我们在上文都分析过了,所以也没什么好说的。布局可根据自身需求去自定义。

最后就是搜索界面调用该view  mainactiviy.java

public class mainactivity extends activity implements searchview.searchviewlistener { 
 
  /** 
   * 搜索结果列表view 
   */ 
  private listview lvresults; 
 
  /** 
   * 搜索view 
   */ 
  private searchview searchview; 
 
 
  /** 
   * 热搜框列表adapter 
   */ 
  private arrayadapter<string> hintadapter; 
 
  /** 
   * 自动补全列表adapter 
   */ 
  private arrayadapter<string> autocompleteadapter; 
 
  /** 
   * 搜索结果列表adapter 
   */ 
  private searchadapter resultadapter; 
 
  /** 
   * 数据库数据,总数据 
   */ 
  private list<bean> dbdata; 
 
  /** 
   * 热搜版数据 
   */ 
  private list<string> hintdata; 
 
  /** 
   * 搜索过程中自动补全数据 
   */ 
  private list<string> autocompletedata; 
 
  /** 
   * 搜索结果的数据 
   */ 
  private list<bean> resultdata; 
 
  /** 
   * 默认提示框显示项的个数 
   */ 
  private static int default_hint_size = 4; 
 
  /** 
   * 提示框显示项的个数 
   */ 
  private static int hintsize = default_hint_size; 
 
  /** 
   * 设置提示框显示项的个数 
   * 
   * @param hintsize 提示框显示个数 
   */ 
  public static void sethintsize(int hintsize) { 
    mainactivity.hintsize = hintsize; 
  } 
 
 
  @override 
  protected void oncreate(bundle savedinstancestate) { 
    super.oncreate(savedinstancestate); 
    requestwindowfeature(window.feature_no_title); 
    setcontentview(r.layout.activity_main); 
    initdata(); 
    initviews(); 
  } 
 
  /** 
   * 初始化视图 
   */ 
  private void initviews() { 
    lvresults = (listview) findviewbyid(r.id.main_lv_search_results); 
    searchview = (searchview) findviewbyid(r.id.main_search_layout); 
    //设置监听 
    searchview.setsearchviewlistener(this); 
    //设置adapter 
    searchview.settipshintadapter(hintadapter); 
    searchview.setautocompleteadapter(autocompleteadapter); 
 
    lvresults.setonitemclicklistener(new adapterview.onitemclicklistener() { 
      @override 
      public void onitemclick(adapterview<?> adapterview, view view, int position, long l) { 
        toast.maketext(mainactivity.this, position + "", toast.length_short).show(); 
      } 
    }); 
  } 
 
  /** 
   * 初始化数据 
   */ 
  private void initdata() { 
    //从数据库获取数据 
    getdbdata(); 
    //初始化热搜版数据 
    gethintdata(); 
    //初始化自动补全数据 
    getautocompletedata(null); 
    //初始化搜索结果数据 
    getresultdata(null); 
  } 
 
  /** 
   * 获取db 数据 
   */ 
  private void getdbdata() { 
    int size = 100; 
    dbdata = new arraylist<>(size); 
    for (int i = 0; i < size; i++) { 
      dbdata.add(new bean(r.drawable.icon, "android开发必备技能" + (i + 1), "android自定义view——自定义搜索view", i * 20 + 2 + "")); 
    } 
  } 
 
  /** 
   * 获取热搜版data 和adapter 
   */ 
  private void gethintdata() { 
    hintdata = new arraylist<>(hintsize); 
    for (int i = 1; i <= hintsize; i++) { 
      hintdata.add("热搜版" + i + ":android自定义view"); 
    } 
    hintadapter = new arrayadapter<>(this, android.r.layout.simple_list_item_1, hintdata); 
  } 
 
  /** 
   * 获取自动补全data 和adapter 
   */ 
  private void getautocompletedata(string text) { 
    if (autocompletedata == null) { 
      //初始化 
      autocompletedata = new arraylist<>(hintsize); 
    } else { 
      // 根据text 获取auto data 
      autocompletedata.clear(); 
      for (int i = 0, count = 0; i < dbdata.size() 
          && count < hintsize; i++) { 
        if (dbdata.get(i).gettitle().contains(text.trim())) { 
          autocompletedata.add(dbdata.get(i).gettitle()); 
          count++; 
        } 
      } 
    } 
    if (autocompleteadapter == null) { 
      autocompleteadapter = new arrayadapter<>(this, android.r.layout.simple_list_item_1, autocompletedata); 
    } else { 
      autocompleteadapter.notifydatasetchanged(); 
    } 
  } 
 
  /** 
   * 获取搜索结果data和adapter 
   */ 
  private void getresultdata(string text) { 
    if (resultdata == null) { 
      // 初始化 
      resultdata = new arraylist<>(); 
    } else { 
      resultdata.clear(); 
      for (int i = 0; i < dbdata.size(); i++) { 
        if (dbdata.get(i).gettitle().contains(text.trim())) { 
          resultdata.add(dbdata.get(i)); 
        } 
      } 
    } 
    if (resultadapter == null) { 
      resultadapter = new searchadapter(this, resultdata, r.layout.item_bean_list); 
    } else { 
      resultadapter.notifydatasetchanged(); 
    } 
  } 
 
  /** 
   * 当搜索框 文本改变时 触发的回调 ,更新自动补全数据 
   * @param text 
   */ 
  @override 
  public void onrefreshautocomplete(string text) { 
    //更新数据 
    getautocompletedata(text); 
  } 
 
  /** 
   * 点击搜索键时edit text触发的回调 
   * 
   * @param text 
   */ 
  @override 
  public void onsearch(string text) { 
    //更新result数据 
    getresultdata(text); 
    lvresults.setvisibility(view.visible); 
    //第一次获取结果 还未配置适配器 
    if (lvresults.getadapter() == null) { 
      //获取搜索数据 设置适配器 
      lvresults.setadapter(resultadapter); 
    } else { 
      //更新搜索数据 
      resultadapter.notifydatasetchanged(); 
    } 
    toast.maketext(this, "完成搜素", toast.length_short).show(); 
  } 
 
} 

使用searchview比较简单,只要给searchview设置onsearchviewlistener监听接口,实现对应的方法,并给searchview传入热搜版和自动补全的adapter既可。

这里使用的匹配算法比较简单,也没有考虑多个搜索词的情况,(这些之后都可以再完善),主要实现就是在总数据中匹配每个bean的title是否包含搜索词,包含则表示该数据匹配,否则不匹配。然后将所有匹配的bean显示到结果列表中。

考虑到实际开发中,数据量十分庞大,可以只把结果集的一部分(如前10个)显示出来,上拉到底的时候再加载之后的记录,也就是可以加入上拉加载的机制,使app性能更优化。

自动补全匹配也是采用相同的算法。算法都比较简单,当然也可以弄得复杂点,比如根据“ ”(空格)去分割输入文本,再逐个考虑单个搜索词的匹配项,把匹配次数从多到少排列出结果集等等。这里不细说。

这里有一个问题是进入该搜索界面时需要加载所有的数据项到内存,当数据项很多时,是否会占用大量的内存?如果是应该如何避免?是采用只加载一部分数据的形式,还是直接使用搜索词到数据库中查询更优?还请各位看官大神们给出宝贵的意见~

好了,自定义搜索框到这就打造完成啦,是不是感觉简单过头了。

各位看官如果有任何问题可评论或者发邮件跟我联系yetwish@gmail.com

囧~忘记贴代码了,代码放在github上,各位看官直接download即可
链接:https://github.com/yetwish/customsearchview

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。