Android搜索控件SearchView

由于项目很多地方需要搜索框,就自定义了一个SearchView控件,顺便复习下自定义View的操作。

一.复用性

虽然我自己在多个地方进行复制粘贴也很省时,但是总觉得这样的做法太Low了,所以还是抽出来自定义一个view,看看效果。

默认情况

这是一个默认样式下的搜索框,当然也可以改


image.png

抽离出以后再使用的话会比较方便。

二.默认输入框结构

目录
image.png

第一个是view,三个接口分别表示监听搜索框的焦点,监听搜索框的搜索操作和扩展自定义View时的行为。

View结构

默认的View的布局如下



    

        

        

    


简单说说这样设计的理由,本来图片和Edit是可以直接用一个EditText来完成的,但是为了考虑扩展性所以分成了ImageView和EditText。
然后我是不想在中间多加一层的LinearLayout布局的,但是如果显示在中间的情况布局就会看着挺别扭,而且直接用RelativeLayout 去动态改变两个子控件的布局的话就会做很多操作,所以在中间加了一层,我觉得性能方面也不会影响很大。

再讲讲这样设计是为了确保存在ImageView和EditText,布局可以自定义进行扩展,不一定要使用默认的,但是一定要有ImageView和EditText,这个之后会说。

三.自定义属性

添加部分自定义属性,方便改变一些常用的样式


    
        
        
        
        
            
            
            
        
        
        
        
        
        
    

show_location表示展示的位置,其它都有注解。

不仅如此,还会在View内部加入返回子控件的操作,可以在外部设置,因为我觉得如果在内部定义太多属性的话,要在View内写很多变量,这种做法我觉得很不划算,所以只写了一些常变化的,下面的方法返回控件。

public EditText getSearchEditText() {
        return edtSearch;
    }

    public ImageView getSearchImageView() {
        return ivSearch;
    }

    public ViewGroup getSearchFrameView() {
        return rlSearch;
    }

四.初始化操作

    protected void initView() {
        // 初始化搜索边框
        rlSearch.setBackgroundResource(seaBackgroup);
        rlSearch.setPadding((int) seaPadding, (int) seaPadding, (int) seaPadding, (int) seaPadding);

        ViewGroup.LayoutParams llLp = llSearch.getLayoutParams();
        if (showType == 0){
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        }else if (showType == 1) {
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.CENTER_IN_PARENT);
        }else if (showType == 2){
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        }
        llSearch.setLayoutParams(llLp);

        // 初始化图片
        ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
        lp.width = (int) imgSize;
        lp.height = (int) imgSize;
        ivSearch.setLayoutParams(lp);
        ivSearch.setImageResource(imgRid);

        // 初始化输入框
        edtHint = (edtHint == null || edtHint == "" || edtHint.equals(null) || edtHint.equals(""))
                ? "请输入搜索内容" : edtHint;
        edtSearch.setHint(edtHint);
        edtSearch.setHintTextColor(edtHintColor);
        edtSearch.setTextSize(edt_size);


    }

就是初始化设置那些常用的属性。关键是下面的操作,为了增加扩展性,我添加了一步类似钩子的操作。

 private void init(){
        // 提供自定义样式的钩子
        int layoutId = getLayoutId();
        if (layoutId == -1){
            seachView = LayoutInflater.from(getContext()).inflate(R.layout.layout_base_seach,this,false);
            this.addView(seachView);
            ivSearch = (ImageView) seachView.findViewById(R.id.iv_search);
            edtSearch = (EditText) seachView.findViewById(R.id.edt_search);
            rlSearch = (RelativeLayout) seachView.findViewById(R.id.rl_search);
            llSearch = (LinearLayout) seachView.findViewById(R.id.ll_search);
            initView();
        }else {
            seachView = LayoutInflater.from(getContext()).inflate(layoutId,this,false);
            this.addView(seachView);
            ivSearch = getImageView();
            edtSearch = getEditText();
            rlSearch = getSearchFrame();
        }
        // 初始化事件监听
        initAllListener();
    }

    @Override
    public int getLayoutId(){
        return -1;
    }

    @Override
    public ImageView getImageView(){
        return null;
    }

    @Override
    public EditText getEditText(){
        return null;
    }

    @Override
    public ViewGroup getSearchFrame(){
        return null;
    }

开发者可以写个类继承这个控件,然后重写上边说的SearchExtendImpl接口的4个方法

public interface SearchExtendImpl {

    int getLayoutId();

    ImageView getImageView();

    EditText getEditText();

    ViewGroup getSearchFrame();

}

如果布局和默认布局很大程度不同的话,可以使用继承的方式,但是要把搜索框的图标用getImageView来返给父类,输入框用getEditText传,搜索边框用getSearchFrame传。我这里这样设计的目的是因为,这东西子控件就两三个,所以在布局方面我没必要做太多的扩展,就几个子控件,做过多的扩展不如重做一个新的,但是逻辑是可以复用的。

五.搜索逻辑

这部分是可以服用的,所以不管是使用我写的默认的搜索框样式,还是开发者自定义的布局都可以服用这个逻辑。

protected void initAllListener(){
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(getContext().INPUT_METHOD_SERVICE);
        if (edtSearch != null) {
            // 点击事件
            edtSearch.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    edtSearch.setFocusable(true);//设置输入框可聚集
                    edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
                    edtSearch.requestFocus();//请求焦点
                    edtSearch.findFocus();//获取焦点
                }
            });
            // 监听焦点
            edtSearch.setOnFocusChangeListener(new OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    if (hasFocus) {
                        imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
                    } else {
                        imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
                    }
                    if (onSearchFocusListener != null){
                        onSearchFocusListener.searchFocusChange(v,hasFocus);
                    }
                }
            });
            // 监听软键盘的按键
            edtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                    //回车等操作
                    if (actionId == EditorInfo.IME_ACTION_SEND
                            || actionId == EditorInfo.IME_ACTION_DONE
                            || actionId == EditorInfo.IME_ACTION_SEARCH
                            || actionId == EditorInfo.IME_ACTION_GO
                            || (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode()
                            && KeyEvent.ACTION_DOWN == event.getAction())) {
                        // 搜索
                        search();
                    }
                    return true;
                }
            });
        }
    }

这里涉及两个比较啰嗦的东西,光标(焦点)和软键盘,软键盘的操做相关的一个类InputMethodManager,我没有很好去理解,只是查了一些大概的用法。

(1)我先给输入框设置点击之后获取焦点
edtSearch.setFocusable(true);//设置输入框可聚集
edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
edtSearch.requestFocus();//请求焦点
edtSearch.findFocus();//获取焦点

为什么要这样做,因为我之后要做失焦操作,如果不写这个代码的话,我这边会出个BUG,失焦后就无法再重新获取焦点。

(2)失焦操作
/**
     *  让EditText失去焦点
     */
    public void lostRocus(){
        if(edtSearch != null) {
            edtSearch.setFocusable(false);
        }
    }
(3)关联软键盘
              public void onFocusChange(View v, boolean hasFocus) {
                    if (hasFocus) {
                        imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
                    } else {
                        imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
                    }
                }

获取焦点后软键盘弹起,失去焦点后软键盘消失。

六.全部代码

项目没有放到gayhub,一是有些BUG,我等下会说,二是我没写自定义布局的demo,三是我没写文档。所以我先写文章,过后完善了再把项目丢到gayhub。

1.默认样式布局



    

        

        

    


2.自定义属性

在attrs中添加


    
        
        
        
        
            
            
            
        
        
        
        
        
        
    

3.三个定义的接口

public interface OnSearchFocusListener {

    void searchFocusChange(View v, boolean hasFocus);

}
public interface OnSearchListener {
    void search(String content);
}
public interface SearchExtendImpl {

    int getLayoutId();

    ImageView getImageView();

    EditText getEditText();

    ViewGroup getSearchFrame();

}

4.View代码

/**
 * Created by kylin on 2018/2/23.
 */

public class KylinSearchView extends FrameLayout implements SearchExtendImpl{

    protected Context context;
    protected View seachView;
    protected ImageView ivSearch;
    protected EditText edtSearch;
    protected ViewGroup rlSearch;
    protected LinearLayout llSearch;
    /**
     *  属性定义
     */
    protected int seaBackgroup;
    protected int imgRid;
    protected float imgSize;
    protected String edtHint;
    protected int edtHintColor;
    protected float seaPadding;
    protected int showType;
    protected float edt_size;
    /**
     *  事件
     */
    protected OnSearchListener onSearchListener;
    protected OnSearchFocusListener onSearchFocusListener;


    public KylinSearchView(Context context) {
        super(context);
        init();
    }

    public KylinSearchView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);
        init();
    }

    public KylinSearchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
        init();
    }

    /**
     * 设置属性
     */
    private void initAttrs(Context context,AttributeSet attrs)  {
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.kylin_search_style);

        imgRid = typedArray.getInteger(R.styleable.kylin_search_style_img_src,R.mipmap.product_serch);
        // 默认的float是px,所以要转成dp
        imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size,  DimensionUtils.dip2px(context,24));
        edtHint = typedArray.getString(R.styleable.kylin_search_style_edt_hint);
        edtHintColor = typedArray.getColor(R.styleable.kylin_search_style_edt_hint, getResources().getColor(R.color.get_gray_code));
        seaBackgroup = typedArray.getInteger(R.styleable.kylin_search_style_search_backgroup,R.drawable.bg_search_default);
        seaPadding = typedArray.getDimension(R.styleable.kylin_search_style_img_size, DimensionUtils.dip2px(context,8));
        showType = typedArray.getInteger(R.styleable.kylin_search_style_show_location,0);
        edt_size = typedArray.getDimension(R.styleable.kylin_search_style_edt_size, 18);

        typedArray.recycle();

    }

    /**
     *  初始化操作
     */
    private void init(){
        // 提供自定义样式的钩子
        int layoutId = getLayoutId();
        if (layoutId == -1){
            seachView = LayoutInflater.from(getContext()).inflate(R.layout.layout_base_seach,this,false);
            this.addView(seachView);
            ivSearch = (ImageView) seachView.findViewById(R.id.iv_search);
            edtSearch = (EditText) seachView.findViewById(R.id.edt_search);
            rlSearch = (RelativeLayout) seachView.findViewById(R.id.rl_search);
            llSearch = (LinearLayout) seachView.findViewById(R.id.ll_search);
            initView();
        }else {
            seachView = LayoutInflater.from(getContext()).inflate(layoutId,this,false);
            this.addView(seachView);
            ivSearch = getImageView();
            edtSearch = getEditText();
            rlSearch = getSearchFrame();
        }
        // 初始化事件监听
        initAllListener();
    }

    @Override
    public int getLayoutId(){
        return -1;
    }

    @Override
    public ImageView getImageView(){
        return null;
    }

    @Override
    public EditText getEditText(){
        return null;
    }

    @Override
    public ViewGroup getSearchFrame(){
        return null;
    }

    protected void initView() {
        // 初始化搜索边框
        rlSearch.setBackgroundResource(seaBackgroup);
        rlSearch.setPadding((int) seaPadding, (int) seaPadding, (int) seaPadding, (int) seaPadding);

        ViewGroup.LayoutParams llLp = llSearch.getLayoutParams();
        if (showType == 0){
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        }else if (showType == 1) {
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.CENTER_IN_PARENT);
        }else if (showType == 2){
            ((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        }
        llSearch.setLayoutParams(llLp);

        // 初始化图片
        ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
        lp.width = (int) imgSize;
        lp.height = (int) imgSize;
        ivSearch.setLayoutParams(lp);
        ivSearch.setImageResource(imgRid);

        // 初始化输入框
        edtHint = (edtHint == null || edtHint == "" || edtHint.equals(null) || edtHint.equals(""))
                ? "请输入搜索内容" : edtHint;
        edtSearch.setHint(edtHint);
        edtSearch.setHintTextColor(edtHintColor);
        edtSearch.setTextSize(edt_size);


    }

    protected void initAllListener(){
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(getContext().INPUT_METHOD_SERVICE);
        if (edtSearch != null) {
            // 点击事件
            edtSearch.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    edtSearch.setFocusable(true);//设置输入框可聚集
                    edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
                    edtSearch.requestFocus();//请求焦点
                    edtSearch.findFocus();//获取焦点
                }
            });
            // 监听焦点
            edtSearch.setOnFocusChangeListener(new OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    if (hasFocus) {
                        imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
                    } else {
                        imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
                    }
                    if (onSearchFocusListener != null){
                        onSearchFocusListener.searchFocusChange(v,hasFocus);
                    }
                }
            });
            // 监听软键盘的按键
            edtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                    //回车等操作
                    if (actionId == EditorInfo.IME_ACTION_SEND
                            || actionId == EditorInfo.IME_ACTION_DONE
                            || actionId == EditorInfo.IME_ACTION_SEARCH
                            || actionId == EditorInfo.IME_ACTION_GO
                            || (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode()
                            && KeyEvent.ACTION_DOWN == event.getAction())) {
                        // 搜索
                        search();
                    }
                    return true;
                }
            });
        }
    }

    /**
     *  获取搜索框的内容
     */
    public String getSearchContent(){
        if (edtSearch == null){
            return null;
        }
        return edtSearch.getText().toString();
    }

    /**
     *  清空搜索框
     */
    public void clearSearch(){
        edtSearch.setText("");
    }

    /**
     *  让EditText失去焦点
     */
    public void lostRocus(){
        if(edtSearch != null) {
            edtSearch.setFocusable(false);
        }
    }

    /**
     *  搜索
     */
    public void search(){
        lostRocus();
        if (onSearchListener != null){
            onSearchListener.search(getSearchContent());
        }
    }

    public void setOnSearchListener(OnSearchListener onSearchListener) {
        this.onSearchListener = onSearchListener;
    }

    public void setOnSearchFocusListener(OnSearchFocusListener onSearchFocusListener) {
        this.onSearchFocusListener = onSearchFocusListener;
    }

    public EditText getSearchEditText() {
        return edtSearch;
    }

    public ImageView getSearchImageView() {
        return ivSearch;
    }

    public ViewGroup getSearchFrameView() {
        return rlSearch;
    }

    public void setImgSize(float size){
        ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
        lp.width = (int) size;
        lp.height = (int) size;
        ivSearch.setLayoutParams(lp);
    }

    public void setTextSize(float size){
        edtSearch.setTextSize(size);
    }
    
}

之中有些操作,比如 DimensionUtils.dip2px就是转尺寸单位,这些命名就很明显,自己写个工具类转就行。

伸手党要用的话只能全抄了,gayhub没这么快。

tmp2.gif

模拟一下,软键盘回车后失去焦点。

六.BUG

我在玩的时候玩出个BUG,关键是这个BUG我还不知道要怎么去表达,设置图片的尺寸

imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size,  DimensionUtils.dip2px(context,24));

我这里默认填24dp是正常,我如果在控件中设置

app:img_size="24dp"
image.png

图片大小没变,但是间距莫名其妙变大了。还有文字也是,用sp的话比正常情况的sp大得多。所以我现在还不懂这个format="dimension"出了什么毛病,这是其中一个问题。

还有一个问题是软键盘,这个东西比较容易出BUG,我无法保证不同的软键盘或者不同的版本不会出问题,我是感觉之后可能要做适配的问题。

暂时就这些,简单封装了一下,完善也只能在之后碰到问题再去完善,然后之后我有时间写个demo写个文档再放到gayhub


更新

项目地址

https://github.com/994866755/handsomeYe.SearchView


BUG修改

之前说有几个传资源时的BUG,我先在这写出来,gayhub的过后我改了再传。

1.背景资源的类型写错了
seaBackgroup = typedArray.getResourceId(R.styleable.kylin_search_style_search_backgroup,R.drawable.bg_search_default);

这里我之前用getInteger用错了。

2.设置字体大小

我之前说怎么设置字体大小总是出问题,然后发现是setTextSize方法默认是sp的,然后我用getDimension传进来的会转成px,这样就造成了px套到sp上,尺寸就错了,应该把类型再做一次转换

edtSearch.setTextSize(TypedValue.COMPLEX_UNIT_PX,edt_size);

这个问题我之前也碰到过,只是忘了做笔记,所以不记得了。详细的原因可以看下这篇博客,讲得很不错。
https://www.jianshu.com/p/7f2941dbfb17

3.设置自适应大小

我发现用getDimension如果设默认大小的话就扩展性太差了,所以想改成自适应,但是我又不知道这个方法要怎么设置默认值为自适应,所以我就投机用正负值来判断,默认为负数就是自适应的情况。

imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size, -1);
 // 初始化图片
        ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
        if (imgSize >= 0) {
            lp.width = (int) imgSize;
            lp.height = (int) imgSize;
        }else {
            lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        }
        ivSearch.setLayoutParams(lp);

补充

我想做个功能的补充是关于软键盘的,有很多时候,我们需要在软键盘弹出的情况下,点击空白处的话就隐藏软键盘。
而这个操作我这边不好一起封装到searchView里面,我觉得可以直接在页面中用拦截事件来实现这功能。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            // 收软键盘
            InputMethodManager imm =  (InputMethodManager) getSystemService(this.INPUT_METHOD_SERVICE);
            if (imm.isActive()){
                imm.hideSoftInputFromWindow(searchView.getSearchEditText().getWindowToken(), 0); //隐藏软键盘
            }
        }
        return super.dispatchTouchEvent(ev);
    }

这样写就能实现点击页面时隐藏软键盘的操作,比如美团的搜索就是点页面能隐藏软键盘。

但是这里有个问题,dispatchTouchEvent方法会频繁的调用,只要一碰到这个页面就会调用这个方法,而我在这里面创建InputMethodManager 的话是不是会一直创建对象。

我看Monitors来测试,其实我不是很会用Monitors,然后用我单身20年的手速不断搓屏幕,发现内存一直在增长,所以我觉得InputMethodManager 在dispatchTouchEvent中创建不是很好,把它抽出去,在全局中创建。

你可能感兴趣的:(Android搜索控件SearchView)